mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
371 lines
15 KiB
Swift
371 lines
15 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import TelegramAudio
|
|
import AccountContext
|
|
import LocalizedPeerData
|
|
import PhotoResources
|
|
import CallsEmoji
|
|
import TooltipUI
|
|
import AlertUI
|
|
import PresentationDataUtils
|
|
import DeviceAccess
|
|
import ContextUI
|
|
import AppBundle
|
|
|
|
private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
|
|
return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
|
|
}
|
|
|
|
private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
|
|
return (1.0 - value) * from + value * to
|
|
}
|
|
|
|
final class CallVideoNode: ASDisplayNode, PreviewVideoNode {
|
|
private var placeholderImageNode: ASImageNode?
|
|
|
|
private let videoTransformContainer: ASDisplayNode
|
|
private let videoView: PresentationCallVideoView
|
|
|
|
private var effectView: UIVisualEffectView?
|
|
private let videoPausedNode: ImmediateTextNode
|
|
|
|
private var isBlurred: Bool = false
|
|
private var currentCornerRadius: CGFloat = 0.0
|
|
|
|
private let isReadyUpdated: () -> Void
|
|
private(set) var isReady: Bool = false
|
|
private var isReadyTimer: SwiftSignalKit.Timer?
|
|
|
|
private let readyPromise = ValuePromise(false)
|
|
var ready: Signal<Bool, NoError> {
|
|
return self.readyPromise.get()
|
|
}
|
|
|
|
private let isFlippedUpdated: (CallVideoNode) -> Void
|
|
|
|
private(set) var currentOrientation: PresentationCallVideoView.Orientation
|
|
private(set) var currentAspect: CGFloat = 0.0
|
|
|
|
private var previousVideoHeight: CGFloat?
|
|
|
|
init(videoView: PresentationCallVideoView, displayPlaceholderUntilReady: Bool = false, disabledText: String?, assumeReadyAfterTimeout: Bool, isReadyUpdated: @escaping () -> Void, orientationUpdated: @escaping () -> Void, isFlippedUpdated: @escaping (CallVideoNode) -> Void) {
|
|
self.isReadyUpdated = isReadyUpdated
|
|
self.isFlippedUpdated = isFlippedUpdated
|
|
|
|
self.videoTransformContainer = ASDisplayNode()
|
|
self.videoView = videoView
|
|
videoView.view.clipsToBounds = true
|
|
videoView.view.backgroundColor = .black
|
|
|
|
self.currentOrientation = videoView.getOrientation()
|
|
self.currentAspect = videoView.getAspect()
|
|
|
|
self.videoPausedNode = ImmediateTextNode()
|
|
self.videoPausedNode.alpha = 0.0
|
|
self.videoPausedNode.maximumNumberOfLines = 3
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = .black
|
|
self.clipsToBounds = true
|
|
|
|
if #available(iOS 13.0, *) {
|
|
self.layer.cornerCurve = .continuous
|
|
}
|
|
|
|
self.videoTransformContainer.view.addSubview(self.videoView.view)
|
|
self.addSubnode(self.videoTransformContainer)
|
|
|
|
if displayPlaceholderUntilReady {
|
|
let placeholderImageNode = ASImageNode()
|
|
placeholderImageNode.image = UIImage(bundleImageName: "Camera/SelfiePlaceholder")
|
|
self.placeholderImageNode = placeholderImageNode
|
|
self.addSubnode(placeholderImageNode)
|
|
}
|
|
|
|
if let disabledText = disabledText {
|
|
self.videoPausedNode.attributedText = NSAttributedString(string: disabledText, font: Font.regular(17.0), textColor: .white)
|
|
self.addSubnode(self.videoPausedNode)
|
|
}
|
|
|
|
self.videoView.setOnFirstFrameReceived { [weak self] aspectRatio in
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.isReady {
|
|
strongSelf.isReady = true
|
|
strongSelf.readyPromise.set(true)
|
|
strongSelf.isReadyTimer?.invalidate()
|
|
strongSelf.isReadyUpdated()
|
|
|
|
if let placeholderImageNode = strongSelf.placeholderImageNode {
|
|
strongSelf.placeholderImageNode = nil
|
|
placeholderImageNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak placeholderImageNode] _ in
|
|
placeholderImageNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.videoView.setOnOrientationUpdated { [weak self] orientation, aspect in
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.currentOrientation != orientation || strongSelf.currentAspect != aspect {
|
|
strongSelf.currentOrientation = orientation
|
|
strongSelf.currentAspect = aspect
|
|
orientationUpdated()
|
|
}
|
|
}
|
|
}
|
|
|
|
self.videoView.setOnIsMirroredUpdated { [weak self] _ in
|
|
Queue.mainQueue().async {
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.isFlippedUpdated(strongSelf)
|
|
}
|
|
}
|
|
|
|
if assumeReadyAfterTimeout {
|
|
self.isReadyTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if !strongSelf.isReady {
|
|
strongSelf.isReady = true
|
|
strongSelf.readyPromise.set(true)
|
|
strongSelf.isReadyUpdated()
|
|
}
|
|
}, queue: .mainQueue())
|
|
}
|
|
self.isReadyTimer?.start()
|
|
}
|
|
|
|
deinit {
|
|
self.isReadyTimer?.invalidate()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
if #available(iOS 13.0, *) {
|
|
self.layer.cornerCurve = .continuous
|
|
}
|
|
}
|
|
|
|
func animateRadialMask(from fromRect: CGRect, to toRect: CGRect) {
|
|
let maskLayer = CAShapeLayer()
|
|
maskLayer.frame = fromRect
|
|
|
|
let path = CGMutablePath()
|
|
path.addEllipse(in: CGRect(origin: CGPoint(), size: fromRect.size))
|
|
maskLayer.path = path
|
|
|
|
self.layer.mask = maskLayer
|
|
|
|
let topLeft = CGPoint(x: 0.0, y: 0.0)
|
|
let topRight = CGPoint(x: self.bounds.width, y: 0.0)
|
|
let bottomLeft = CGPoint(x: 0.0, y: self.bounds.height)
|
|
let bottomRight = CGPoint(x: self.bounds.width, y: self.bounds.height)
|
|
|
|
func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat {
|
|
let dx = v1.x - v2.x
|
|
let dy = v1.y - v2.y
|
|
return sqrt(dx * dx + dy * dy)
|
|
}
|
|
|
|
var maxRadius = distance(toRect.center, topLeft)
|
|
maxRadius = max(maxRadius, distance(toRect.center, topRight))
|
|
maxRadius = max(maxRadius, distance(toRect.center, bottomLeft))
|
|
maxRadius = max(maxRadius, distance(toRect.center, bottomRight))
|
|
maxRadius = ceil(maxRadius)
|
|
|
|
let targetFrame = CGRect(origin: CGPoint(x: toRect.center.x - maxRadius, y: toRect.center.y - maxRadius), size: CGSize(width: maxRadius * 2.0, height: maxRadius * 2.0))
|
|
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
|
transition.updatePosition(layer: maskLayer, position: targetFrame.center)
|
|
transition.updateTransformScale(layer: maskLayer, scale: maxRadius * 2.0 / fromRect.width, completion: { [weak self] _ in
|
|
self?.layer.mask = nil
|
|
})
|
|
}
|
|
|
|
func updateLayout(size: CGSize, layoutMode: VideoNodeLayoutMode, transition: ContainedViewLayoutTransition) {
|
|
self.updateLayout(size: size, cornerRadius: self.currentCornerRadius, isOutgoing: true, deviceOrientation: .portrait, isCompactLayout: false, transition: transition)
|
|
}
|
|
|
|
func updateLayout(size: CGSize, cornerRadius: CGFloat, isOutgoing: Bool, deviceOrientation: UIDeviceOrientation, isCompactLayout: Bool, transition: ContainedViewLayoutTransition) {
|
|
self.currentCornerRadius = cornerRadius
|
|
|
|
if let placeholderImageNode = self.placeholderImageNode, let image = placeholderImageNode.image {
|
|
let placeholderSize = image.size.aspectFilled(size)
|
|
transition.updateFrame(node: placeholderImageNode, frame: CGRect(origin: CGPoint(x: (size.width - placeholderSize.width) * 0.5, y: (size.height - placeholderSize.height) * 0.5), size: placeholderSize))
|
|
}
|
|
|
|
var rotationAngle: CGFloat
|
|
if false && isOutgoing && isCompactLayout {
|
|
rotationAngle = CGFloat.pi / 2.0
|
|
} else {
|
|
switch self.currentOrientation {
|
|
case .rotation0:
|
|
rotationAngle = 0.0
|
|
case .rotation90:
|
|
rotationAngle = CGFloat.pi / 2.0
|
|
case .rotation180:
|
|
rotationAngle = CGFloat.pi
|
|
case .rotation270:
|
|
rotationAngle = -CGFloat.pi / 2.0
|
|
}
|
|
|
|
var additionalAngle: CGFloat = 0.0
|
|
switch deviceOrientation {
|
|
case .portrait:
|
|
additionalAngle = 0.0
|
|
case .landscapeLeft:
|
|
additionalAngle = CGFloat.pi / 2.0
|
|
case .landscapeRight:
|
|
additionalAngle = -CGFloat.pi / 2.0
|
|
case .portraitUpsideDown:
|
|
rotationAngle = CGFloat.pi
|
|
default:
|
|
additionalAngle = 0.0
|
|
}
|
|
rotationAngle += additionalAngle
|
|
if abs(rotationAngle - CGFloat.pi * 3.0 / 2.0) < 0.01 {
|
|
rotationAngle = -CGFloat.pi / 2.0
|
|
}
|
|
if abs(rotationAngle - (-CGFloat.pi)) < 0.01 {
|
|
rotationAngle = -CGFloat.pi + 0.001
|
|
}
|
|
}
|
|
|
|
let rotateFrame = abs(rotationAngle.remainder(dividingBy: CGFloat.pi)) > 1.0
|
|
let fittingSize: CGSize
|
|
if rotateFrame {
|
|
fittingSize = CGSize(width: size.height, height: size.width)
|
|
} else {
|
|
fittingSize = size
|
|
}
|
|
|
|
let unboundVideoSize = CGSize(width: self.currentAspect * 10000.0, height: 10000.0)
|
|
|
|
var fittedVideoSize = unboundVideoSize.fitted(fittingSize)
|
|
if fittedVideoSize.width < fittingSize.width || fittedVideoSize.height < fittingSize.height {
|
|
let isVideoPortrait = unboundVideoSize.width < unboundVideoSize.height
|
|
let isFittingSizePortrait = fittingSize.width < fittingSize.height
|
|
|
|
if isCompactLayout && isVideoPortrait == isFittingSizePortrait {
|
|
fittedVideoSize = unboundVideoSize.aspectFilled(fittingSize)
|
|
} else {
|
|
let maxFittingEdgeDistance: CGFloat
|
|
if isCompactLayout {
|
|
maxFittingEdgeDistance = 200.0
|
|
} else {
|
|
maxFittingEdgeDistance = 400.0
|
|
}
|
|
if fittedVideoSize.width > fittingSize.width - maxFittingEdgeDistance && fittedVideoSize.height > fittingSize.height - maxFittingEdgeDistance {
|
|
fittedVideoSize = unboundVideoSize.aspectFilled(fittingSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
let rotatedVideoHeight: CGFloat = max(fittedVideoSize.height, fittedVideoSize.width)
|
|
|
|
let videoFrame: CGRect = CGRect(origin: CGPoint(), size: fittedVideoSize)
|
|
|
|
let videoPausedSize = self.videoPausedNode.updateLayout(CGSize(width: size.width - 16.0, height: 100.0))
|
|
transition.updateFrame(node: self.videoPausedNode, frame: CGRect(origin: CGPoint(x: floor((size.width - videoPausedSize.width) / 2.0), y: floor((size.height - videoPausedSize.height) / 2.0)), size: videoPausedSize))
|
|
|
|
self.videoTransformContainer.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
|
|
if transition.isAnimated && !videoFrame.height.isZero, let previousVideoHeight = self.previousVideoHeight, !previousVideoHeight.isZero {
|
|
let scaleDifference = previousVideoHeight / rotatedVideoHeight
|
|
if abs(scaleDifference - 1.0) > 0.001 {
|
|
transition.animateTransformScale(node: self.videoTransformContainer, from: scaleDifference, additive: true)
|
|
}
|
|
}
|
|
self.previousVideoHeight = rotatedVideoHeight
|
|
transition.updatePosition(node: self.videoTransformContainer, position: CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
|
transition.updateTransformRotation(view: self.videoTransformContainer.view, angle: rotationAngle)
|
|
|
|
let localVideoFrame = CGRect(origin: CGPoint(), size: videoFrame.size)
|
|
self.videoView.view.bounds = localVideoFrame
|
|
self.videoView.view.center = localVideoFrame.center
|
|
// TODO: properly fix the issue
|
|
// On iOS 13 and later metal layer transformation is broken if the layer does not require compositing
|
|
self.videoView.view.alpha = 0.995
|
|
|
|
if let effectView = self.effectView {
|
|
transition.updateFrame(view: effectView, frame: localVideoFrame)
|
|
}
|
|
|
|
transition.updateCornerRadius(layer: self.layer, cornerRadius: self.currentCornerRadius)
|
|
}
|
|
|
|
func updateIsBlurred(isBlurred: Bool, light: Bool = false, animated: Bool = true) {
|
|
if self.hasScheduledUnblur {
|
|
self.hasScheduledUnblur = false
|
|
}
|
|
if self.isBlurred == isBlurred {
|
|
return
|
|
}
|
|
self.isBlurred = isBlurred
|
|
|
|
if isBlurred {
|
|
if self.effectView == nil {
|
|
let effectView = UIVisualEffectView()
|
|
self.effectView = effectView
|
|
effectView.frame = self.videoTransformContainer.bounds
|
|
self.videoTransformContainer.view.addSubview(effectView)
|
|
}
|
|
if animated {
|
|
UIView.animate(withDuration: 0.3, animations: {
|
|
self.videoPausedNode.alpha = 1.0
|
|
self.effectView?.effect = UIBlurEffect(style: light ? .light : .dark)
|
|
})
|
|
} else {
|
|
self.effectView?.effect = UIBlurEffect(style: light ? .light : .dark)
|
|
}
|
|
} else if let effectView = self.effectView {
|
|
self.effectView = nil
|
|
UIView.animate(withDuration: 0.3, animations: {
|
|
self.videoPausedNode.alpha = 0.0
|
|
effectView.effect = nil
|
|
}, completion: { [weak effectView] _ in
|
|
effectView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
private var hasScheduledUnblur = false
|
|
func flip(withBackground: Bool) {
|
|
if withBackground {
|
|
self.backgroundColor = .black
|
|
}
|
|
UIView.transition(with: withBackground ? self.videoTransformContainer.view : self.view, duration: 0.4, options: [.transitionFlipFromLeft, .curveEaseOut], animations: {
|
|
UIView.performWithoutAnimation {
|
|
self.updateIsBlurred(isBlurred: true, light: false, animated: false)
|
|
}
|
|
}) { finished in
|
|
self.backgroundColor = nil
|
|
self.hasScheduledUnblur = true
|
|
Queue.mainQueue().after(0.5) {
|
|
if self.hasScheduledUnblur {
|
|
self.updateIsBlurred(isBlurred: false)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|