mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Call UI V2
This commit is contained in:
parent
fdb11c792b
commit
928121b194
@ -51,15 +51,35 @@ public final class ViewController: UIViewController {
|
||||
activeState.signalInfo.quality = activeState.signalInfo.quality == 1.0 ? 0.1 : 1.0
|
||||
self.callState.lifecycleState = .active(activeState)
|
||||
case .terminated:
|
||||
break
|
||||
self.callState.lifecycleState = .active(PrivateCallScreen.State.ActiveState(
|
||||
startTime: Date().timeIntervalSince1970,
|
||||
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
|
||||
emojiKey: ["A", "B", "C", "D"]
|
||||
))
|
||||
}
|
||||
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
callScreenView.flipCameraAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let input = self.callState.localVideo as? FileVideoSource {
|
||||
input.sourceId = input.sourceId == 0 ? 1 : 0
|
||||
}
|
||||
}
|
||||
callScreenView.videoAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.callState.localVideo == nil {
|
||||
self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!)
|
||||
} else {
|
||||
self.callState.localVideo = nil
|
||||
}
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
callScreenView.microhoneMuteAction = {
|
||||
if self.callState.remoteVideo == nil {
|
||||
self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!)
|
||||
} else {
|
||||
@ -67,15 +87,13 @@ public final class ViewController: UIViewController {
|
||||
}
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
callScreenView.microhoneMuteAction = {
|
||||
self.callState.isMicrophoneMuted = !self.callState.isMicrophoneMuted
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
callScreenView.endCallAction = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0))
|
||||
self.callState.remoteVideo = nil
|
||||
self.callState.localVideo = nil
|
||||
self.update(transition: .spring(duration: 0.4))
|
||||
}
|
||||
|
||||
|
@ -780,7 +780,7 @@ public final class MetalEngine {
|
||||
|
||||
if previousSurfaceId != nil {
|
||||
#if DEBUG
|
||||
print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight)")
|
||||
print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight))")
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
|
@ -59,7 +59,8 @@ fragment half4 callBackgroundFragment(
|
||||
const device float2 *positions [[ buffer(0) ]],
|
||||
const device float4 *colors [[ buffer(1) ]],
|
||||
const device float &brightness [[ buffer(2) ]],
|
||||
const device float &saturation [[ buffer(3) ]]
|
||||
const device float &saturation [[ buffer(3) ]],
|
||||
const device float4 &overlay [[ buffer(4) ]]
|
||||
) {
|
||||
half centerDistanceX = in.uv.x - 0.5;
|
||||
half centerDistanceY = in.uv.y - 0.5;
|
||||
@ -106,6 +107,8 @@ fragment half4 callBackgroundFragment(
|
||||
color.b = clamp(color.b * brightness, 0.0, 1.0);
|
||||
color.g = clamp(color.g * saturation, 0.0, 1.0);
|
||||
color = hsv2rgb(color);
|
||||
color.rgb += half3(overlay.rgb * overlay.a);
|
||||
color.rgb = min(color.rgb, half3(1.0, 1.0, 1.0));
|
||||
|
||||
return color;
|
||||
}
|
||||
@ -267,7 +270,8 @@ fragment half4 mainVideoFragment(
|
||||
QuadVertexOut in [[stage_in]],
|
||||
texture2d<half> texture [[ texture(0) ]],
|
||||
const device float &brightness [[ buffer(0) ]],
|
||||
const device float &saturation [[ buffer(1) ]]
|
||||
const device float &saturation [[ buffer(1) ]],
|
||||
const device float4 &overlay [[ buffer(2) ]]
|
||||
) {
|
||||
constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
|
||||
half4 color = texture.sample(sampler, in.uv);
|
||||
@ -275,6 +279,8 @@ fragment half4 mainVideoFragment(
|
||||
color.b = clamp(color.b * brightness, 0.0, 1.0);
|
||||
color.g = clamp(color.g * saturation, 0.0, 1.0);
|
||||
color = hsv2rgb(color);
|
||||
color.rgb += half3(overlay.rgb * overlay.a);
|
||||
color.rgb = min(color.rgb, half3(1.0, 1.0, 1.0));
|
||||
|
||||
return half4(color.r, color.g, color.b, color.a);
|
||||
}
|
||||
|
@ -9,12 +9,14 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
enum Content: Equatable {
|
||||
enum Key: Hashable {
|
||||
case speaker
|
||||
case flipCamera
|
||||
case video
|
||||
case microphone
|
||||
case end
|
||||
}
|
||||
|
||||
case speaker(isActive: Bool)
|
||||
case flipCamera
|
||||
case video(isActive: Bool)
|
||||
case microphone(isMuted: Bool)
|
||||
case end
|
||||
@ -23,6 +25,8 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
switch self {
|
||||
case .speaker:
|
||||
return .speaker
|
||||
case .flipCamera:
|
||||
return .flipCamera
|
||||
case .video:
|
||||
return .video
|
||||
case .microphone:
|
||||
@ -72,6 +76,10 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
title = "speaker"
|
||||
image = UIImage(bundleImageName: "Call/Speaker")
|
||||
isActive = isActiveValue
|
||||
case .flipCamera:
|
||||
title = "flip"
|
||||
image = UIImage(bundleImageName: "Call/Flip")
|
||||
isActive = false
|
||||
case let .video(isActiveValue):
|
||||
title = "video"
|
||||
image = UIImage(bundleImageName: "Call/Video")
|
||||
@ -87,10 +95,12 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
isDestructive = true
|
||||
}
|
||||
|
||||
var buttonTransition = transition
|
||||
let buttonView: ContentOverlayButton
|
||||
if let current = self.buttonViews[button.content.key] {
|
||||
buttonView = current
|
||||
} else {
|
||||
buttonTransition = transition.withAnimation(.none)
|
||||
buttonView = ContentOverlayButton(frame: CGRect())
|
||||
self.addSubview(buttonView)
|
||||
self.buttonViews[button.content.key] = buttonView
|
||||
@ -102,10 +112,15 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
||||
}
|
||||
button.action()
|
||||
}
|
||||
|
||||
Transition.immediate.setScale(view: buttonView, scale: 0.001)
|
||||
buttonView.alpha = 0.0
|
||||
transition.setScale(view: buttonView, scale: 1.0)
|
||||
transition.setAlpha(view: buttonView, alpha: 1.0)
|
||||
}
|
||||
|
||||
buttonView.frame = CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize))
|
||||
buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: transition)
|
||||
buttonTransition.setFrame(view: buttonView, frame: CGRect(origin: CGPoint(x: buttonX, y: buttonY), size: CGSize(width: buttonSize, height: buttonSize)))
|
||||
buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition)
|
||||
buttonX += buttonSize + buttonSpacing
|
||||
}
|
||||
|
||||
|
@ -205,10 +205,12 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||
var colors: [SIMD4<Float>] = self.colorTransition.value.colors
|
||||
|
||||
encoder.setFragmentBytes(&colors, length: 4 * MemoryLayout<SIMD4<Float>>.size, index: 1)
|
||||
var brightness: Float = isBlur ? 1.1 : 1.0
|
||||
var saturation: Float = isBlur ? 1.2 : 1.0
|
||||
var brightness: Float = isBlur ? 0.9 : 1.0
|
||||
var saturation: Float = isBlur ? 1.1 : 1.0
|
||||
var overlay: SIMD4<Float> = isBlur ? SIMD4<Float>(1.0, 1.0, 1.0, 0.2) : SIMD4<Float>()
|
||||
encoder.setFragmentBytes(&brightness, length: 4, index: 2)
|
||||
encoder.setFragmentBytes(&saturation, length: 4, index: 3)
|
||||
encoder.setFragmentBytes(&overlay, length: 4 * 4, index: 4)
|
||||
|
||||
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||
})
|
||||
|
@ -114,7 +114,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
|
||||
if contentParams.isDestructive {
|
||||
context.setFillColor(UIColor(rgb: 0xFF3B30).cgColor)
|
||||
} else {
|
||||
context.setFillColor(UIColor(white: 1.0, alpha: contentParams.isSelected ? 1.0 : 0.2).cgColor)
|
||||
context.setFillColor(UIColor(white: 1.0, alpha: contentParams.isSelected ? 1.0 : 0.0).cgColor)
|
||||
}
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
|
@ -1,41 +1,33 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import MetalEngine
|
||||
import ComponentFlow
|
||||
|
||||
private let shadowImage: UIImage? = {
|
||||
UIImage(named: "Call/VideoGradient")?.precomposed()
|
||||
}()
|
||||
|
||||
final class VideoContainerView: UIView {
|
||||
final class MinimizedVideoContainerView: UIView {
|
||||
private struct Params: Equatable {
|
||||
var size: CGSize
|
||||
var cornerRadius: CGFloat
|
||||
var isExpanded: Bool
|
||||
var insets: UIEdgeInsets
|
||||
|
||||
init(size: CGSize, cornerRadius: CGFloat, isExpanded: Bool) {
|
||||
init(size: CGSize, insets: UIEdgeInsets) {
|
||||
self.size = size
|
||||
self.cornerRadius = cornerRadius
|
||||
self.isExpanded = isExpanded
|
||||
self.insets = insets
|
||||
}
|
||||
}
|
||||
|
||||
private struct VideoMetrics: Equatable {
|
||||
var resolution: CGSize
|
||||
var rotationAngle: Float
|
||||
var sourceId: Int
|
||||
|
||||
init(resolution: CGSize, rotationAngle: Float) {
|
||||
init(resolution: CGSize, rotationAngle: Float, sourceId: Int) {
|
||||
self.resolution = resolution
|
||||
self.rotationAngle = rotationAngle
|
||||
self.sourceId = sourceId
|
||||
}
|
||||
}
|
||||
|
||||
private let videoLayer: PrivateCallVideoLayer
|
||||
let blurredContainerLayer: SimpleLayer
|
||||
|
||||
private let topShadowView: UIImageView
|
||||
private let bottomShadowView: UIImageView
|
||||
|
||||
private var params: Params?
|
||||
private var videoMetrics: VideoMetrics?
|
||||
@ -50,7 +42,7 @@ final class VideoContainerView: UIView {
|
||||
var videoMetrics: VideoMetrics?
|
||||
if let currentOutput = self.video?.currentOutput {
|
||||
self.videoLayer.video = currentOutput
|
||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle)
|
||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
|
||||
} else {
|
||||
self.videoLayer.video = nil
|
||||
}
|
||||
@ -64,7 +56,7 @@ final class VideoContainerView: UIView {
|
||||
var videoMetrics: VideoMetrics?
|
||||
if let currentOutput = self.video?.currentOutput {
|
||||
self.videoLayer.video = currentOutput
|
||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle)
|
||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
|
||||
} else {
|
||||
self.videoLayer.video = nil
|
||||
}
|
||||
@ -79,30 +71,21 @@ final class VideoContainerView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.videoLayer = PrivateCallVideoLayer()
|
||||
self.blurredContainerLayer = SimpleLayer()
|
||||
|
||||
self.topShadowView = UIImageView()
|
||||
self.topShadowView.transform = CGAffineTransformMakeScale(1.0, -1.0)
|
||||
self.bottomShadowView = UIImageView()
|
||||
self.videoLayer.masksToBounds = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = UIColor.black
|
||||
self.blurredContainerLayer.backgroundColor = UIColor.black.cgColor
|
||||
|
||||
self.layer.addSublayer(self.videoLayer)
|
||||
self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer)
|
||||
|
||||
self.topShadowView.image = shadowImage
|
||||
self.bottomShadowView.image = shadowImage
|
||||
self.addSubview(self.topShadowView)
|
||||
self.addSubview(self.bottomShadowView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func update(transition: Transition) {
|
||||
guard let params = self.params else {
|
||||
return
|
||||
@ -110,29 +93,13 @@ final class VideoContainerView: UIView {
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
|
||||
func update(size: CGSize, cornerRadius: CGFloat, isExpanded: Bool, transition: Transition) {
|
||||
let params = Params(size: size, cornerRadius: cornerRadius, isExpanded: isExpanded)
|
||||
func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
|
||||
let params = Params(size: size, insets: insets)
|
||||
if self.params == params {
|
||||
return
|
||||
}
|
||||
|
||||
self.layer.masksToBounds = true
|
||||
if self.layer.animation(forKey: "cornerRadius") == nil {
|
||||
self.layer.cornerRadius = self.params?.cornerRadius ?? 0.0
|
||||
}
|
||||
|
||||
self.params = params
|
||||
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
|
||||
guard let self, let params = self.params, completed else {
|
||||
return
|
||||
}
|
||||
if params.isExpanded {
|
||||
self.layer.masksToBounds = false
|
||||
self.layer.cornerRadius = 0.0
|
||||
}
|
||||
})
|
||||
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
|
||||
@ -140,6 +107,7 @@ final class VideoContainerView: UIView {
|
||||
guard let videoMetrics = self.videoMetrics else {
|
||||
return
|
||||
}
|
||||
|
||||
var transition = transition
|
||||
if self.appliedVideoMetrics == nil {
|
||||
transition = .immediate
|
||||
@ -153,40 +121,24 @@ final class VideoContainerView: UIView {
|
||||
videoIsRotated = true
|
||||
}
|
||||
|
||||
var videoSize = rotatedResolution.aspectFitted(params.size)
|
||||
let boundingAspectRatio = params.size.width / params.size.height
|
||||
let videoAspectRatio = videoSize.width / videoSize.height
|
||||
if abs(boundingAspectRatio - videoAspectRatio) < 0.15 {
|
||||
videoSize = rotatedResolution.aspectFilled(params.size)
|
||||
}
|
||||
let videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
|
||||
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
||||
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
||||
|
||||
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
||||
let rotatedBoundingSize = params.size
|
||||
let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize)
|
||||
let rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
|
||||
|
||||
transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center)
|
||||
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
|
||||
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
|
||||
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
|
||||
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
|
||||
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
|
||||
|
||||
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||
|
||||
if params.isExpanded {
|
||||
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
|
||||
}
|
||||
transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0)
|
||||
|
||||
let topShadowHeight: CGFloat = 200.0
|
||||
let topShadowFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.size.width, height: topShadowHeight))
|
||||
transition.setPosition(view: self.topShadowView, position: topShadowFrame.center)
|
||||
transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(), size: topShadowFrame.size))
|
||||
transition.setAlpha(view: self.topShadowView, alpha: params.isExpanded ? 1.0 : 0.0)
|
||||
|
||||
let bottomShadowHeight: CGFloat = 200.0
|
||||
transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
|
||||
transition.setAlpha(view: self.bottomShadowView, alpha: params.isExpanded ? 1.0 : 0.0)
|
||||
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
|
||||
}
|
||||
}
|
@ -223,10 +223,12 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
|
||||
encoder.setFragmentTexture(blurredTexture, index: 0)
|
||||
|
||||
var brightness: Float = 1.4
|
||||
var saturation: Float = 1.1
|
||||
var brightness: Float = 1.0
|
||||
var saturation: Float = 1.2
|
||||
var overlay: SIMD4<Float> = SIMD4<Float>(1.0, 1.0, 1.0, 0.2)
|
||||
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
|
||||
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
|
||||
encoder.setFragmentBytes(&overlay, length: 4 * 4, index: 2)
|
||||
|
||||
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||
})
|
||||
@ -245,8 +247,10 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||
|
||||
var brightness: Float = 1.0
|
||||
var saturation: Float = 1.0
|
||||
var overlay: SIMD4<Float> = SIMD4<Float>()
|
||||
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
|
||||
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
|
||||
encoder.setFragmentBytes(&overlay, length: 4 * 4, index: 2)
|
||||
|
||||
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||
})
|
||||
|
@ -0,0 +1,236 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import MetalEngine
|
||||
|
||||
private let shadowImage: UIImage? = {
|
||||
UIImage(named: "Call/VideoGradient")?.precomposed()
|
||||
}()
|
||||
|
||||
final class VideoContainerView: UIView {
|
||||
private struct Params: Equatable {
|
||||
var size: CGSize
|
||||
var insets: UIEdgeInsets
|
||||
var cornerRadius: CGFloat
|
||||
var isMinimized: Bool
|
||||
var isAnimatingOut: Bool
|
||||
|
||||
init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, isMinimized: Bool, isAnimatingOut: Bool) {
|
||||
self.size = size
|
||||
self.insets = insets
|
||||
self.cornerRadius = cornerRadius
|
||||
self.isMinimized = isMinimized
|
||||
self.isAnimatingOut = isAnimatingOut
|
||||
}
|
||||
}
|
||||
|
||||
private struct VideoMetrics: Equatable {
|
||||
var resolution: CGSize
|
||||
var rotationAngle: Float
|
||||
|
||||
init(resolution: CGSize, rotationAngle: Float) {
|
||||
self.resolution = resolution
|
||||
self.rotationAngle = rotationAngle
|
||||
}
|
||||
}
|
||||
|
||||
private let videoLayer: PrivateCallVideoLayer
|
||||
let blurredContainerLayer: SimpleLayer
|
||||
|
||||
private let topShadowView: UIImageView
|
||||
private let bottomShadowView: UIImageView
|
||||
|
||||
private var params: Params?
|
||||
private var videoMetrics: VideoMetrics?
|
||||
private var appliedVideoMetrics: VideoMetrics?
|
||||
|
||||
var video: VideoSource? {
|
||||
didSet {
|
||||
self.video?.updated = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var videoMetrics: VideoMetrics?
|
||||
if let currentOutput = self.video?.currentOutput {
|
||||
self.videoLayer.video = currentOutput
|
||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle)
|
||||
} else {
|
||||
self.videoLayer.video = nil
|
||||
}
|
||||
self.videoLayer.setNeedsUpdate()
|
||||
|
||||
if self.videoMetrics != videoMetrics {
|
||||
self.videoMetrics = videoMetrics
|
||||
self.update(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
var videoMetrics: VideoMetrics?
|
||||
if let currentOutput = self.video?.currentOutput {
|
||||
self.videoLayer.video = currentOutput
|
||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle)
|
||||
} else {
|
||||
self.videoLayer.video = nil
|
||||
}
|
||||
self.videoLayer.setNeedsUpdate()
|
||||
|
||||
if self.videoMetrics != videoMetrics {
|
||||
self.videoMetrics = videoMetrics
|
||||
self.update(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.videoLayer = PrivateCallVideoLayer()
|
||||
self.blurredContainerLayer = SimpleLayer()
|
||||
|
||||
self.topShadowView = UIImageView()
|
||||
self.topShadowView.transform = CGAffineTransformMakeScale(1.0, -1.0)
|
||||
self.bottomShadowView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = UIColor.black
|
||||
self.blurredContainerLayer.backgroundColor = UIColor.black.cgColor
|
||||
|
||||
self.layer.addSublayer(self.videoLayer)
|
||||
self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer)
|
||||
|
||||
self.topShadowView.image = shadowImage
|
||||
self.bottomShadowView.image = shadowImage
|
||||
self.addSubview(self.topShadowView)
|
||||
self.addSubview(self.bottomShadowView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func update(transition: Transition) {
|
||||
guard let params = self.params else {
|
||||
return
|
||||
}
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
|
||||
func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, isMinimized: Bool, isAnimatingOut: Bool, transition: Transition) {
|
||||
let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, isMinimized: isMinimized, isAnimatingOut: isAnimatingOut)
|
||||
if self.params == params {
|
||||
return
|
||||
}
|
||||
|
||||
self.layer.masksToBounds = true
|
||||
if self.layer.animation(forKey: "cornerRadius") == nil {
|
||||
self.layer.cornerRadius = self.params?.cornerRadius ?? 0.0
|
||||
}
|
||||
|
||||
self.params = params
|
||||
|
||||
transition.setCornerRadius(layer: self.layer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
|
||||
guard let self, let params = self.params, completed else {
|
||||
return
|
||||
}
|
||||
if !params.isAnimatingOut {
|
||||
self.layer.masksToBounds = false
|
||||
self.layer.cornerRadius = 0.0
|
||||
}
|
||||
})
|
||||
|
||||
self.update(params: params, transition: transition)
|
||||
}
|
||||
|
||||
private func update(params: Params, transition: Transition) {
|
||||
guard let videoMetrics = self.videoMetrics else {
|
||||
return
|
||||
}
|
||||
var transition = transition
|
||||
if self.appliedVideoMetrics == nil {
|
||||
transition = .immediate
|
||||
}
|
||||
self.appliedVideoMetrics = videoMetrics
|
||||
|
||||
if params.isMinimized {
|
||||
var rotatedResolution = videoMetrics.resolution
|
||||
var videoIsRotated = false
|
||||
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
|
||||
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
|
||||
videoIsRotated = true
|
||||
}
|
||||
|
||||
let videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||
|
||||
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
||||
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
||||
|
||||
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
||||
let rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
|
||||
let effectiveVideoFrame = videoSize.centered(around: rotatedVideoFrame.center)
|
||||
|
||||
transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center)
|
||||
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
|
||||
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
|
||||
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
|
||||
|
||||
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||
|
||||
transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0)
|
||||
|
||||
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
|
||||
|
||||
let topShadowHeight: CGFloat = floor(effectiveVideoFrame.height * 0.2)
|
||||
let topShadowFrame = CGRect(origin: effectiveVideoFrame.origin, size: CGSize(width: effectiveVideoFrame.width, height: topShadowHeight))
|
||||
transition.setPosition(view: self.topShadowView, position: topShadowFrame.center)
|
||||
transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(x: effectiveVideoFrame.minX, y: effectiveVideoFrame.maxY - topShadowHeight), size: topShadowFrame.size))
|
||||
transition.setAlpha(view: self.topShadowView, alpha: 0.0)
|
||||
|
||||
let bottomShadowHeight: CGFloat = 200.0
|
||||
transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
|
||||
transition.setAlpha(view: self.bottomShadowView, alpha: 0.0)
|
||||
} else {
|
||||
var rotatedResolution = videoMetrics.resolution
|
||||
var videoIsRotated = false
|
||||
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
|
||||
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
|
||||
videoIsRotated = true
|
||||
}
|
||||
|
||||
var videoSize = rotatedResolution.aspectFitted(params.size)
|
||||
let boundingAspectRatio = params.size.width / params.size.height
|
||||
let videoAspectRatio = videoSize.width / videoSize.height
|
||||
if abs(boundingAspectRatio - videoAspectRatio) < 0.15 {
|
||||
videoSize = rotatedResolution.aspectFilled(params.size)
|
||||
}
|
||||
|
||||
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
||||
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
||||
|
||||
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
||||
let rotatedBoundingSize = params.size
|
||||
let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize)
|
||||
|
||||
transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center)
|
||||
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
|
||||
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
|
||||
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
|
||||
|
||||
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||
|
||||
if !params.isAnimatingOut {
|
||||
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
|
||||
}
|
||||
|
||||
let topShadowHeight: CGFloat = 200.0
|
||||
let topShadowFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.size.width, height: topShadowHeight))
|
||||
transition.setPosition(view: self.topShadowView, position: topShadowFrame.center)
|
||||
transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(), size: topShadowFrame.size))
|
||||
transition.setAlpha(view: self.topShadowView, alpha: 1.0)
|
||||
|
||||
let bottomShadowHeight: CGFloat = 200.0
|
||||
transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
|
||||
transition.setAlpha(view: self.bottomShadowView, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
@ -22,19 +22,16 @@ final class WeakSignalView: OverlayMaskContainerView {
|
||||
|
||||
private let titleView: TextView
|
||||
private let overlayBackgroundView: UIImageView
|
||||
private let backgroundView: UIImageView
|
||||
|
||||
private var currentLayout: Layout?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.titleView = TextView()
|
||||
self.overlayBackgroundView = UIImageView()
|
||||
self.backgroundView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.maskContents.addSubview(self.overlayBackgroundView)
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.titleView)
|
||||
}
|
||||
|
||||
@ -48,7 +45,7 @@ final class WeakSignalView: OverlayMaskContainerView {
|
||||
return currentLayout.size
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 8.0
|
||||
let sideInset: CGFloat = 11.0
|
||||
let height: CGFloat = 30.0
|
||||
|
||||
let titleSize = self.titleView.update(string: "Weak network signal", fontSize: 16.0, fontWeight: 0.0, color: .white, constrainedWidth: constrainedSize.width - sideInset * 2.0, transition: .immediate)
|
||||
@ -57,10 +54,8 @@ final class WeakSignalView: OverlayMaskContainerView {
|
||||
|
||||
if self.overlayBackgroundView.image?.size.height != height {
|
||||
self.overlayBackgroundView.image = generateStretchableFilledCircleImage(diameter: height, color: .white)
|
||||
self.backgroundView.image = generateStretchableFilledCircleImage(diameter: height, color: UIColor(white: 1.0, alpha: 0.2))
|
||||
}
|
||||
self.overlayBackgroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.backgroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
self.currentLayout = Layout(params: params, size: size)
|
||||
return size
|
||||
|
@ -6,11 +6,13 @@ public final class VideoSourceOutput {
|
||||
public let y: MTLTexture
|
||||
public let uv: MTLTexture
|
||||
public let rotationAngle: Float
|
||||
public let sourceId: Int
|
||||
|
||||
public init(y: MTLTexture, uv: MTLTexture, rotationAngle: Float) {
|
||||
public init(y: MTLTexture, uv: MTLTexture, rotationAngle: Float, sourceId: Int) {
|
||||
self.y = y
|
||||
self.uv = uv
|
||||
self.rotationAngle = rotationAngle
|
||||
self.sourceId = sourceId
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +38,8 @@ public final class FileVideoSource: VideoSource {
|
||||
|
||||
private var displayLink: SharedDisplayLink.Subscription?
|
||||
|
||||
public var sourceId: Int = 0
|
||||
|
||||
public init?(device: MTLDevice, url: URL) {
|
||||
self.device = device
|
||||
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
||||
@ -114,7 +118,7 @@ public final class FileVideoSource: VideoSource {
|
||||
|
||||
rotationAngle = Float.pi * 0.5
|
||||
|
||||
self.currentOutput = Output(y: yTexture, uv: uvTexture, rotationAngle: rotationAngle)
|
||||
self.currentOutput = Output(y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: self.sourceId)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -128,10 +128,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
|
||||
private var emojiView: KeyEmojiView?
|
||||
|
||||
private var videoContainerView: VideoContainerView?
|
||||
private var localVideoContainerView: VideoContainerView?
|
||||
private var remoteVideoContainerView: VideoContainerView?
|
||||
|
||||
private var activeRemoteVideoSource: VideoSource?
|
||||
private var waitingForFirstVideoFrameDisposable: Disposable?
|
||||
private var waitingForFirstRemoteVideoFrameDisposable: Disposable?
|
||||
|
||||
private var activeLocalVideoSource: VideoSource?
|
||||
private var waitingForFirstLocalVideoFrameDisposable: Disposable?
|
||||
|
||||
private var processedInitialAudioLevelBump: Bool = false
|
||||
private var audioLevelBump: Float = 0.0
|
||||
@ -141,6 +145,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
private var audioLevelUpdateSubscription: SharedDisplayLinkDriver.Link?
|
||||
|
||||
public var speakerAction: (() -> Void)?
|
||||
public var flipCameraAction: (() -> Void)?
|
||||
public var videoAction: (() -> Void)?
|
||||
public var microhoneMuteAction: (() -> Void)?
|
||||
public var endCallAction: (() -> Void)?
|
||||
@ -164,14 +169,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
self.layer.addSublayer(self.backgroundLayer)
|
||||
self.overlayContentsView.layer.addSublayer(self.backgroundLayer.blurredLayer)
|
||||
|
||||
self.layer.addSublayer(self.blobLayer)
|
||||
self.layer.addSublayer(self.avatarLayer)
|
||||
|
||||
self.overlayContentsView.mask = self.maskContents
|
||||
self.addSubview(self.overlayContentsView)
|
||||
|
||||
self.addSubview(self.buttonGroupView)
|
||||
|
||||
self.layer.addSublayer(self.blobLayer)
|
||||
self.layer.addSublayer(self.avatarLayer)
|
||||
|
||||
self.addSubview(self.titleView)
|
||||
|
||||
self.addSubview(self.statusView)
|
||||
@ -179,12 +184,23 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
self?.update(transition: .immediate)
|
||||
}
|
||||
|
||||
self.audioLevelUpdateSubscription = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in
|
||||
(self.layer as? SimpleLayer)?.didEnterHierarchy = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.attenuateAudioLevelStep()
|
||||
})
|
||||
self.audioLevelUpdateSubscription = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.attenuateAudioLevelStep()
|
||||
})
|
||||
}
|
||||
(self.layer as? SimpleLayer)?.didExitHierarchy = { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.audioLevelUpdateSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
@ -192,7 +208,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.waitingForFirstVideoFrameDisposable?.dispose()
|
||||
self.waitingForFirstRemoteVideoFrameDisposable?.dispose()
|
||||
self.waitingForFirstLocalVideoFrameDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
@ -216,7 +233,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
}
|
||||
|
||||
private func updateAudioLevel() {
|
||||
if self.activeRemoteVideoSource == nil {
|
||||
if self.activeRemoteVideoSource == nil && self.activeLocalVideoSource == nil {
|
||||
let additionalAvatarScale = CGFloat(max(0.0, min(self.audioLevel, 5.0)) * 0.05)
|
||||
self.avatarLayer.transform = CATransform3DMakeScale(1.0 + additionalAvatarScale, 1.0 + additionalAvatarScale, 1.0)
|
||||
|
||||
@ -235,7 +252,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
}
|
||||
|
||||
if self.params?.state.remoteVideo !== params.state.remoteVideo {
|
||||
self.waitingForFirstVideoFrameDisposable?.dispose()
|
||||
self.waitingForFirstRemoteVideoFrameDisposable?.dispose()
|
||||
|
||||
if let remoteVideo = params.state.remoteVideo {
|
||||
if remoteVideo.currentOutput != nil {
|
||||
@ -255,7 +272,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
return EmptyDisposable
|
||||
}
|
||||
var shouldUpdate = false
|
||||
self.waitingForFirstVideoFrameDisposable = (firstVideoFrameSignal
|
||||
self.waitingForFirstRemoteVideoFrameDisposable = (firstVideoFrameSignal
|
||||
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|
||||
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
||||
guard let self else {
|
||||
@ -272,6 +289,44 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
self.activeRemoteVideoSource = nil
|
||||
}
|
||||
}
|
||||
if self.params?.state.localVideo !== params.state.localVideo {
|
||||
self.waitingForFirstLocalVideoFrameDisposable?.dispose()
|
||||
|
||||
if let localVideo = params.state.localVideo {
|
||||
if localVideo.currentOutput != nil {
|
||||
self.activeLocalVideoSource = localVideo
|
||||
} else {
|
||||
let firstVideoFrameSignal = Signal<Never, NoError> { subscriber in
|
||||
localVideo.updated = { [weak localVideo] in
|
||||
guard let localVideo else {
|
||||
subscriber.putCompletion()
|
||||
return
|
||||
}
|
||||
if localVideo.currentOutput != nil {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
return EmptyDisposable
|
||||
}
|
||||
var shouldUpdate = false
|
||||
self.waitingForFirstLocalVideoFrameDisposable = (firstVideoFrameSignal
|
||||
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|
||||
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.activeLocalVideoSource = localVideo
|
||||
if shouldUpdate {
|
||||
self.update(transition: .spring(duration: 0.3))
|
||||
}
|
||||
})
|
||||
shouldUpdate = true
|
||||
}
|
||||
} else {
|
||||
self.activeLocalVideoSource = nil
|
||||
}
|
||||
}
|
||||
|
||||
self.params = params
|
||||
self.updateInternal(params: params, transition: transition)
|
||||
@ -292,6 +347,24 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
|
||||
let edgeSize: Int = 2
|
||||
|
||||
let primaryVideoSource: VideoSource?
|
||||
let secondaryVideoSource: VideoSource?
|
||||
if let activeRemoteVideoSource = self.activeRemoteVideoSource, let activeLocalVideoSource = self.activeLocalVideoSource {
|
||||
primaryVideoSource = activeRemoteVideoSource
|
||||
secondaryVideoSource = activeLocalVideoSource
|
||||
} else if let activeRemoteVideoSource = self.activeRemoteVideoSource {
|
||||
primaryVideoSource = activeRemoteVideoSource
|
||||
secondaryVideoSource = nil
|
||||
} else if let activeLocalVideoSource = self.activeLocalVideoSource {
|
||||
primaryVideoSource = activeLocalVideoSource
|
||||
secondaryVideoSource = nil
|
||||
} else {
|
||||
primaryVideoSource = nil
|
||||
secondaryVideoSource = nil
|
||||
}
|
||||
|
||||
let havePrimaryVideo = self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil
|
||||
|
||||
let visualBackgroundFrame = backgroundFrame.insetBy(dx: -CGFloat(edgeSize) / renderingSize.width * backgroundFrame.width, dy: -CGFloat(edgeSize) / renderingSize.height * backgroundFrame.height)
|
||||
|
||||
self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
|
||||
@ -319,13 +392,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
|
||||
transition.setFrame(view: self.buttonGroupView, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||
|
||||
let buttons: [ButtonGroupView.Button] = [
|
||||
ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.speakerAction?()
|
||||
}),
|
||||
var buttons: [ButtonGroupView.Button] = [
|
||||
ButtonGroupView.Button(content: .video(isActive: params.state.localVideo != nil), action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@ -345,6 +412,21 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
self.endCallAction?()
|
||||
})
|
||||
]
|
||||
if self.activeLocalVideoSource != nil {
|
||||
buttons.insert(ButtonGroupView.Button(content: .flipCamera, action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.flipCameraAction?()
|
||||
}), at: 0)
|
||||
} else {
|
||||
buttons.insert(ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.speakerAction?()
|
||||
}), at: 0)
|
||||
}
|
||||
self.buttonGroupView.update(size: params.size, buttons: buttons, transition: transition)
|
||||
|
||||
if case let .active(activeState) = params.state.lifecycleState {
|
||||
@ -367,7 +449,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
} else {
|
||||
if let emojiView = self.emojiView {
|
||||
self.emojiView = nil
|
||||
emojiView.removeFromSuperview()
|
||||
transition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in
|
||||
emojiView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,56 +460,112 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
|
||||
let collapsedAvatarFrame = CGRect(origin: CGPoint(x: floor((params.size.width - collapsedAvatarSize) * 0.5), y: 222.0), size: CGSize(width: collapsedAvatarSize, height: collapsedAvatarSize))
|
||||
let expandedAvatarFrame = CGRect(origin: CGPoint(), size: params.size)
|
||||
let avatarFrame = self.activeRemoteVideoSource != nil ? expandedAvatarFrame : collapsedAvatarFrame
|
||||
let avatarCornerRadius = self.activeRemoteVideoSource != nil ? params.screenCornerRadius : collapsedAvatarSize * 0.5
|
||||
let expandedVideoFrame = CGRect(origin: CGPoint(), size: params.size)
|
||||
let avatarFrame = havePrimaryVideo ? expandedAvatarFrame : collapsedAvatarFrame
|
||||
let avatarCornerRadius = havePrimaryVideo ? params.screenCornerRadius : collapsedAvatarSize * 0.5
|
||||
|
||||
if let activeRemoteVideoSource = self.activeRemoteVideoSource {
|
||||
let videoContainerView: VideoContainerView
|
||||
if let current = self.videoContainerView {
|
||||
videoContainerView = current
|
||||
let minimizedVideoInsets = UIEdgeInsets(top: 124.0, left: 12.0, bottom: 178.0, right: 12.0)
|
||||
|
||||
if let primaryVideoSource {
|
||||
let remoteVideoContainerView: VideoContainerView
|
||||
if let current = self.remoteVideoContainerView {
|
||||
remoteVideoContainerView = current
|
||||
} else {
|
||||
videoContainerView = VideoContainerView(frame: CGRect())
|
||||
self.videoContainerView = videoContainerView
|
||||
self.insertSubview(videoContainerView, belowSubview: self.titleView)
|
||||
self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer)
|
||||
remoteVideoContainerView = VideoContainerView(frame: CGRect())
|
||||
self.remoteVideoContainerView = remoteVideoContainerView
|
||||
self.insertSubview(remoteVideoContainerView, belowSubview: self.overlayContentsView)
|
||||
self.overlayContentsView.layer.addSublayer(remoteVideoContainerView.blurredContainerLayer)
|
||||
|
||||
videoContainerView.layer.position = self.avatarLayer.position
|
||||
videoContainerView.layer.bounds = self.avatarLayer.bounds
|
||||
videoContainerView.alpha = 0.0
|
||||
videoContainerView.blurredContainerLayer.position = self.avatarLayer.position
|
||||
videoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
|
||||
videoContainerView.blurredContainerLayer.opacity = 0.0
|
||||
videoContainerView.update(size: self.avatarLayer.bounds.size, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isExpanded: false, transition: .immediate)
|
||||
remoteVideoContainerView.layer.position = self.avatarLayer.position
|
||||
remoteVideoContainerView.layer.bounds = self.avatarLayer.bounds
|
||||
remoteVideoContainerView.alpha = 0.0
|
||||
remoteVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position
|
||||
remoteVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
|
||||
remoteVideoContainerView.blurredContainerLayer.opacity = 0.0
|
||||
remoteVideoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isMinimized: false, isAnimatingOut: false, transition: .immediate)
|
||||
}
|
||||
|
||||
if videoContainerView.video !== activeRemoteVideoSource {
|
||||
videoContainerView.video = activeRemoteVideoSource
|
||||
if remoteVideoContainerView.video !== primaryVideoSource {
|
||||
remoteVideoContainerView.video = primaryVideoSource
|
||||
}
|
||||
|
||||
transition.setPosition(view: videoContainerView, position: avatarFrame.center)
|
||||
transition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
transition.setAlpha(view: videoContainerView, alpha: 1.0)
|
||||
transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
||||
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0)
|
||||
videoContainerView.update(size: avatarFrame.size, cornerRadius: avatarCornerRadius, isExpanded: self.activeRemoteVideoSource != nil, transition: transition)
|
||||
transition.setPosition(view: remoteVideoContainerView, position: expandedVideoFrame.center)
|
||||
transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
||||
transition.setAlpha(view: remoteVideoContainerView, alpha: 1.0)
|
||||
transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
|
||||
transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
||||
transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 1.0)
|
||||
remoteVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: false, isAnimatingOut: false, transition: transition)
|
||||
} else {
|
||||
if let videoContainerView = self.videoContainerView {
|
||||
videoContainerView.update(size: avatarFrame.size, cornerRadius: avatarCornerRadius, isExpanded: self.activeRemoteVideoSource != nil, transition: transition)
|
||||
transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
||||
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
|
||||
transition.setPosition(view: videoContainerView, position: avatarFrame.center)
|
||||
transition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
if videoContainerView.alpha != 0.0 {
|
||||
transition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak self, weak videoContainerView] completed in
|
||||
guard let self, let videoContainerView, completed else {
|
||||
if let remoteVideoContainerView = self.remoteVideoContainerView {
|
||||
remoteVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition)
|
||||
transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
||||
transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 0.0)
|
||||
transition.setPosition(view: remoteVideoContainerView, position: avatarFrame.center)
|
||||
transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
if remoteVideoContainerView.alpha != 0.0 {
|
||||
transition.setAlpha(view: remoteVideoContainerView, alpha: 0.0, completion: { [weak self, weak remoteVideoContainerView] completed in
|
||||
guard let self, let remoteVideoContainerView, completed else {
|
||||
return
|
||||
}
|
||||
videoContainerView.removeFromSuperview()
|
||||
videoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
||||
if self.videoContainerView === videoContainerView {
|
||||
self.videoContainerView = nil
|
||||
remoteVideoContainerView.removeFromSuperview()
|
||||
remoteVideoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
||||
if self.remoteVideoContainerView === remoteVideoContainerView {
|
||||
self.remoteVideoContainerView = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let secondaryVideoSource {
|
||||
let localVideoContainerView: VideoContainerView
|
||||
if let current = self.localVideoContainerView {
|
||||
localVideoContainerView = current
|
||||
} else {
|
||||
localVideoContainerView = VideoContainerView(frame: CGRect())
|
||||
self.localVideoContainerView = localVideoContainerView
|
||||
self.insertSubview(localVideoContainerView, belowSubview: self.overlayContentsView)
|
||||
self.overlayContentsView.layer.addSublayer(localVideoContainerView.blurredContainerLayer)
|
||||
|
||||
localVideoContainerView.layer.position = self.avatarLayer.position
|
||||
localVideoContainerView.layer.bounds = self.avatarLayer.bounds
|
||||
localVideoContainerView.alpha = 0.0
|
||||
localVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position
|
||||
localVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
|
||||
localVideoContainerView.blurredContainerLayer.opacity = 0.0
|
||||
localVideoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isMinimized: true, isAnimatingOut: false, transition: .immediate)
|
||||
}
|
||||
|
||||
if localVideoContainerView.video !== secondaryVideoSource {
|
||||
localVideoContainerView.video = secondaryVideoSource
|
||||
}
|
||||
|
||||
transition.setPosition(view: localVideoContainerView, position: expandedVideoFrame.center)
|
||||
transition.setBounds(view: localVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
||||
transition.setAlpha(view: localVideoContainerView, alpha: 1.0)
|
||||
transition.setPosition(layer: localVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
|
||||
transition.setBounds(layer: localVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
||||
transition.setAlpha(layer: localVideoContainerView.blurredContainerLayer, alpha: 1.0)
|
||||
localVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: true, isAnimatingOut: false, transition: transition)
|
||||
} else {
|
||||
if let localVideoContainerView = self.localVideoContainerView {
|
||||
localVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition)
|
||||
transition.setPosition(layer: localVideoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
||||
transition.setBounds(layer: localVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
transition.setAlpha(layer: localVideoContainerView.blurredContainerLayer, alpha: 0.0)
|
||||
transition.setPosition(view: localVideoContainerView, position: avatarFrame.center)
|
||||
transition.setBounds(view: localVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
if localVideoContainerView.alpha != 0.0 {
|
||||
transition.setAlpha(view: localVideoContainerView, alpha: 0.0, completion: { [weak self, weak localVideoContainerView] completed in
|
||||
guard let self, let localVideoContainerView, completed else {
|
||||
return
|
||||
}
|
||||
localVideoContainerView.removeFromSuperview()
|
||||
localVideoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
||||
if self.localVideoContainerView === localVideoContainerView {
|
||||
self.localVideoContainerView = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -437,7 +577,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
}
|
||||
transition.setPosition(layer: self.avatarLayer, position: avatarFrame.center)
|
||||
transition.setBounds(layer: self.avatarLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||
self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded: self.activeRemoteVideoSource != nil, cornerRadius: avatarCornerRadius, transition: transition)
|
||||
self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded:havePrimaryVideo, cornerRadius: avatarCornerRadius, transition: transition)
|
||||
|
||||
let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize))
|
||||
transition.setPosition(layer: self.blobLayer, position: CGPoint(x: blobFrame.midX, y: blobFrame.midY))
|
||||
@ -447,16 +587,21 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
switch params.state.lifecycleState {
|
||||
case .terminated:
|
||||
titleString = "Call Ended"
|
||||
transition.setScale(layer: self.blobLayer, scale: 0.001)
|
||||
if !transition.animation.isImmediate {
|
||||
transition.withAnimation(.curve(duration: 0.3, curve: .easeInOut)).setScale(layer: self.blobLayer, scale: 0.3)
|
||||
} else {
|
||||
transition.setScale(layer: self.blobLayer, scale: 0.3)
|
||||
}
|
||||
transition.setAlpha(layer: self.blobLayer, alpha: 0.0)
|
||||
default:
|
||||
titleString = params.state.name
|
||||
transition.setAlpha(layer: self.blobLayer, alpha: 1.0)
|
||||
}
|
||||
|
||||
let titleSize = self.titleView.update(
|
||||
string: titleString,
|
||||
fontSize: self.activeRemoteVideoSource == nil ? 28.0 : 17.0,
|
||||
fontWeight: self.activeRemoteVideoSource == nil ? 0.0 : 0.25,
|
||||
fontSize: !havePrimaryVideo ? 28.0 : 17.0,
|
||||
fontWeight: !havePrimaryVideo ? 0.0 : 0.25,
|
||||
color: .white,
|
||||
constrainedWidth: params.size.width - 16.0 * 2.0,
|
||||
transition: transition
|
||||
@ -464,7 +609,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
let titleFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: (params.size.width - titleSize.width) * 0.5,
|
||||
y: self.activeRemoteVideoSource == nil ? collapsedAvatarFrame.maxY + 39.0 : params.insets.top + 17.0
|
||||
y: !havePrimaryVideo ? collapsedAvatarFrame.maxY + 39.0 : params.insets.top + 17.0
|
||||
),
|
||||
size: titleSize
|
||||
)
|
||||
@ -492,6 +637,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
})
|
||||
}
|
||||
case let .terminated(terminatedState):
|
||||
self.processedInitialAudioLevelBump = false
|
||||
statusState = .terminated(StatusView.TerminatedState(duration: terminatedState.duration))
|
||||
}
|
||||
|
||||
@ -518,7 +664,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
let statusFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: (params.size.width - statusSize.width) * 0.5,
|
||||
y: titleFrame.maxY + (self.activeRemoteVideoSource != nil ? 0.0 : 4.0)
|
||||
y: titleFrame.maxY + (havePrimaryVideo ? 0.0 : 4.0)
|
||||
),
|
||||
size: statusSize
|
||||
)
|
||||
@ -534,7 +680,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
transition.setFrame(view: self.statusView, frame: statusFrame)
|
||||
}
|
||||
|
||||
if "".isEmpty {//} case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2 {
|
||||
if case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2 {
|
||||
let weakSignalView: WeakSignalView
|
||||
if let current = self.weakSignalView {
|
||||
weakSignalView = current
|
||||
@ -544,7 +690,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||
self.addSubview(weakSignalView)
|
||||
}
|
||||
let weakSignalSize = weakSignalView.update(constrainedSize: CGSize(width: params.size.width - 32.0, height: 100.0))
|
||||
let weakSignalFrame = CGRect(origin: CGPoint(x: floor((params.size.width - weakSignalSize.width) * 0.5), y: statusFrame.maxY + (self.activeRemoteVideoSource != nil ? 4.0 : 4.0)), size: weakSignalSize)
|
||||
let weakSignalFrame = CGRect(origin: CGPoint(x: floor((params.size.width - weakSignalSize.width) * 0.5), y: statusFrame.maxY + (havePrimaryVideo ? 12.0 : 12.0)), size: weakSignalSize)
|
||||
if weakSignalView.bounds.isEmpty {
|
||||
weakSignalView.frame = weakSignalFrame
|
||||
if !transition.animation.isImmediate {
|
||||
|
Loading…
x
Reference in New Issue
Block a user