[WIP] Call UI V2

This commit is contained in:
Ali 2023-11-16 17:29:35 +04:00
parent fdb11c792b
commit 928121b194
12 changed files with 541 additions and 163 deletions

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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 {