Swiftgram/submodules/TelegramCallsUI/Sources/CallControllerNode.swift
2024-12-06 22:15:52 +08:00

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)
}
}
}
}
}