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
|
activeState.signalInfo.quality = activeState.signalInfo.quality == 1.0 ? 0.1 : 1.0
|
||||||
self.callState.lifecycleState = .active(activeState)
|
self.callState.lifecycleState = .active(activeState)
|
||||||
case .terminated:
|
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))
|
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
|
callScreenView.videoAction = { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
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 {
|
if self.callState.remoteVideo == nil {
|
||||||
self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!)
|
self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!)
|
||||||
} else {
|
} else {
|
||||||
@ -67,15 +87,13 @@ public final class ViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
self.update(transition: .spring(duration: 0.4))
|
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
|
callScreenView.endCallAction = { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0))
|
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))
|
self.update(transition: .spring(duration: 0.4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -780,7 +780,7 @@ public final class MetalEngine {
|
|||||||
|
|
||||||
if previousSurfaceId != nil {
|
if previousSurfaceId != nil {
|
||||||
#if DEBUG
|
#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
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,7 +59,8 @@ fragment half4 callBackgroundFragment(
|
|||||||
const device float2 *positions [[ buffer(0) ]],
|
const device float2 *positions [[ buffer(0) ]],
|
||||||
const device float4 *colors [[ buffer(1) ]],
|
const device float4 *colors [[ buffer(1) ]],
|
||||||
const device float &brightness [[ buffer(2) ]],
|
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 centerDistanceX = in.uv.x - 0.5;
|
||||||
half centerDistanceY = in.uv.y - 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.b = clamp(color.b * brightness, 0.0, 1.0);
|
||||||
color.g = clamp(color.g * saturation, 0.0, 1.0);
|
color.g = clamp(color.g * saturation, 0.0, 1.0);
|
||||||
color = hsv2rgb(color);
|
color = hsv2rgb(color);
|
||||||
|
color.rgb += half3(overlay.rgb * overlay.a);
|
||||||
|
color.rgb = min(color.rgb, half3(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
@ -267,7 +270,8 @@ fragment half4 mainVideoFragment(
|
|||||||
QuadVertexOut in [[stage_in]],
|
QuadVertexOut in [[stage_in]],
|
||||||
texture2d<half> texture [[ texture(0) ]],
|
texture2d<half> texture [[ texture(0) ]],
|
||||||
const device float &brightness [[ buffer(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);
|
constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
|
||||||
half4 color = texture.sample(sampler, in.uv);
|
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.b = clamp(color.b * brightness, 0.0, 1.0);
|
||||||
color.g = clamp(color.g * saturation, 0.0, 1.0);
|
color.g = clamp(color.g * saturation, 0.0, 1.0);
|
||||||
color = hsv2rgb(color);
|
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);
|
return half4(color.r, color.g, color.b, color.a);
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,14 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
|||||||
enum Content: Equatable {
|
enum Content: Equatable {
|
||||||
enum Key: Hashable {
|
enum Key: Hashable {
|
||||||
case speaker
|
case speaker
|
||||||
|
case flipCamera
|
||||||
case video
|
case video
|
||||||
case microphone
|
case microphone
|
||||||
case end
|
case end
|
||||||
}
|
}
|
||||||
|
|
||||||
case speaker(isActive: Bool)
|
case speaker(isActive: Bool)
|
||||||
|
case flipCamera
|
||||||
case video(isActive: Bool)
|
case video(isActive: Bool)
|
||||||
case microphone(isMuted: Bool)
|
case microphone(isMuted: Bool)
|
||||||
case end
|
case end
|
||||||
@ -23,6 +25,8 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
|||||||
switch self {
|
switch self {
|
||||||
case .speaker:
|
case .speaker:
|
||||||
return .speaker
|
return .speaker
|
||||||
|
case .flipCamera:
|
||||||
|
return .flipCamera
|
||||||
case .video:
|
case .video:
|
||||||
return .video
|
return .video
|
||||||
case .microphone:
|
case .microphone:
|
||||||
@ -72,6 +76,10 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
|||||||
title = "speaker"
|
title = "speaker"
|
||||||
image = UIImage(bundleImageName: "Call/Speaker")
|
image = UIImage(bundleImageName: "Call/Speaker")
|
||||||
isActive = isActiveValue
|
isActive = isActiveValue
|
||||||
|
case .flipCamera:
|
||||||
|
title = "flip"
|
||||||
|
image = UIImage(bundleImageName: "Call/Flip")
|
||||||
|
isActive = false
|
||||||
case let .video(isActiveValue):
|
case let .video(isActiveValue):
|
||||||
title = "video"
|
title = "video"
|
||||||
image = UIImage(bundleImageName: "Call/Video")
|
image = UIImage(bundleImageName: "Call/Video")
|
||||||
@ -87,10 +95,12 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
|||||||
isDestructive = true
|
isDestructive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var buttonTransition = transition
|
||||||
let buttonView: ContentOverlayButton
|
let buttonView: ContentOverlayButton
|
||||||
if let current = self.buttonViews[button.content.key] {
|
if let current = self.buttonViews[button.content.key] {
|
||||||
buttonView = current
|
buttonView = current
|
||||||
} else {
|
} else {
|
||||||
|
buttonTransition = transition.withAnimation(.none)
|
||||||
buttonView = ContentOverlayButton(frame: CGRect())
|
buttonView = ContentOverlayButton(frame: CGRect())
|
||||||
self.addSubview(buttonView)
|
self.addSubview(buttonView)
|
||||||
self.buttonViews[button.content.key] = buttonView
|
self.buttonViews[button.content.key] = buttonView
|
||||||
@ -102,10 +112,15 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
|||||||
}
|
}
|
||||||
button.action()
|
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))
|
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: transition)
|
buttonView.update(size: CGSize(width: buttonSize, height: buttonSize), image: image, isSelected: isActive, isDestructive: isDestructive, title: title, transition: buttonTransition)
|
||||||
buttonX += buttonSize + buttonSpacing
|
buttonX += buttonSize + buttonSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,10 +205,12 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
var colors: [SIMD4<Float>] = self.colorTransition.value.colors
|
var colors: [SIMD4<Float>] = self.colorTransition.value.colors
|
||||||
|
|
||||||
encoder.setFragmentBytes(&colors, length: 4 * MemoryLayout<SIMD4<Float>>.size, index: 1)
|
encoder.setFragmentBytes(&colors, length: 4 * MemoryLayout<SIMD4<Float>>.size, index: 1)
|
||||||
var brightness: Float = isBlur ? 1.1 : 1.0
|
var brightness: Float = isBlur ? 0.9 : 1.0
|
||||||
var saturation: Float = isBlur ? 1.2 : 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(&brightness, length: 4, index: 2)
|
||||||
encoder.setFragmentBytes(&saturation, length: 4, index: 3)
|
encoder.setFragmentBytes(&saturation, length: 4, index: 3)
|
||||||
|
encoder.setFragmentBytes(&overlay, length: 4 * 4, index: 4)
|
||||||
|
|
||||||
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||||
})
|
})
|
||||||
|
@ -114,7 +114,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
|
|||||||
if contentParams.isDestructive {
|
if contentParams.isDestructive {
|
||||||
context.setFillColor(UIColor(rgb: 0xFF3B30).cgColor)
|
context.setFillColor(UIColor(rgb: 0xFF3B30).cgColor)
|
||||||
} else {
|
} 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))
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
@ -1,41 +1,33 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
import ComponentFlow
|
|
||||||
import MetalEngine
|
import MetalEngine
|
||||||
|
import ComponentFlow
|
||||||
|
|
||||||
private let shadowImage: UIImage? = {
|
final class MinimizedVideoContainerView: UIView {
|
||||||
UIImage(named: "Call/VideoGradient")?.precomposed()
|
|
||||||
}()
|
|
||||||
|
|
||||||
final class VideoContainerView: UIView {
|
|
||||||
private struct Params: Equatable {
|
private struct Params: Equatable {
|
||||||
var size: CGSize
|
var size: CGSize
|
||||||
var cornerRadius: CGFloat
|
var insets: UIEdgeInsets
|
||||||
var isExpanded: Bool
|
|
||||||
|
|
||||||
init(size: CGSize, cornerRadius: CGFloat, isExpanded: Bool) {
|
init(size: CGSize, insets: UIEdgeInsets) {
|
||||||
self.size = size
|
self.size = size
|
||||||
self.cornerRadius = cornerRadius
|
self.insets = insets
|
||||||
self.isExpanded = isExpanded
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct VideoMetrics: Equatable {
|
private struct VideoMetrics: Equatable {
|
||||||
var resolution: CGSize
|
var resolution: CGSize
|
||||||
var rotationAngle: Float
|
var rotationAngle: Float
|
||||||
|
var sourceId: Int
|
||||||
|
|
||||||
init(resolution: CGSize, rotationAngle: Float) {
|
init(resolution: CGSize, rotationAngle: Float, sourceId: Int) {
|
||||||
self.resolution = resolution
|
self.resolution = resolution
|
||||||
self.rotationAngle = rotationAngle
|
self.rotationAngle = rotationAngle
|
||||||
|
self.sourceId = sourceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let videoLayer: PrivateCallVideoLayer
|
private let videoLayer: PrivateCallVideoLayer
|
||||||
let blurredContainerLayer: SimpleLayer
|
|
||||||
|
|
||||||
private let topShadowView: UIImageView
|
|
||||||
private let bottomShadowView: UIImageView
|
|
||||||
|
|
||||||
private var params: Params?
|
private var params: Params?
|
||||||
private var videoMetrics: VideoMetrics?
|
private var videoMetrics: VideoMetrics?
|
||||||
@ -50,7 +42,7 @@ final class VideoContainerView: UIView {
|
|||||||
var videoMetrics: VideoMetrics?
|
var videoMetrics: VideoMetrics?
|
||||||
if let currentOutput = self.video?.currentOutput {
|
if let currentOutput = self.video?.currentOutput {
|
||||||
self.videoLayer.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 {
|
} else {
|
||||||
self.videoLayer.video = nil
|
self.videoLayer.video = nil
|
||||||
}
|
}
|
||||||
@ -64,7 +56,7 @@ final class VideoContainerView: UIView {
|
|||||||
var videoMetrics: VideoMetrics?
|
var videoMetrics: VideoMetrics?
|
||||||
if let currentOutput = self.video?.currentOutput {
|
if let currentOutput = self.video?.currentOutput {
|
||||||
self.videoLayer.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 {
|
} else {
|
||||||
self.videoLayer.video = nil
|
self.videoLayer.video = nil
|
||||||
}
|
}
|
||||||
@ -79,30 +71,21 @@ final class VideoContainerView: UIView {
|
|||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.videoLayer = PrivateCallVideoLayer()
|
self.videoLayer = PrivateCallVideoLayer()
|
||||||
self.blurredContainerLayer = SimpleLayer()
|
self.videoLayer.masksToBounds = true
|
||||||
|
|
||||||
self.topShadowView = UIImageView()
|
|
||||||
self.topShadowView.transform = CGAffineTransformMakeScale(1.0, -1.0)
|
|
||||||
self.bottomShadowView = UIImageView()
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.backgroundColor = UIColor.black
|
|
||||||
self.blurredContainerLayer.backgroundColor = UIColor.black.cgColor
|
|
||||||
|
|
||||||
self.layer.addSublayer(self.videoLayer)
|
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) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private func update(transition: Transition) {
|
private func update(transition: Transition) {
|
||||||
guard let params = self.params else {
|
guard let params = self.params else {
|
||||||
return
|
return
|
||||||
@ -110,29 +93,13 @@ final class VideoContainerView: UIView {
|
|||||||
self.update(params: params, transition: transition)
|
self.update(params: params, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, cornerRadius: CGFloat, isExpanded: Bool, transition: Transition) {
|
func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
|
||||||
let params = Params(size: size, cornerRadius: cornerRadius, isExpanded: isExpanded)
|
let params = Params(size: size, insets: insets)
|
||||||
if self.params == params {
|
if self.params == params {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.layer.masksToBounds = true
|
|
||||||
if self.layer.animation(forKey: "cornerRadius") == nil {
|
|
||||||
self.layer.cornerRadius = self.params?.cornerRadius ?? 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
self.params = params
|
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)
|
self.update(params: params, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +107,7 @@ final class VideoContainerView: UIView {
|
|||||||
guard let videoMetrics = self.videoMetrics else {
|
guard let videoMetrics = self.videoMetrics else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var transition = transition
|
var transition = transition
|
||||||
if self.appliedVideoMetrics == nil {
|
if self.appliedVideoMetrics == nil {
|
||||||
transition = .immediate
|
transition = .immediate
|
||||||
@ -153,40 +121,24 @@ final class VideoContainerView: UIView {
|
|||||||
videoIsRotated = true
|
videoIsRotated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var videoSize = rotatedResolution.aspectFitted(params.size)
|
let videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
||||||
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 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 rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
||||||
|
|
||||||
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
||||||
let rotatedBoundingSize = params.size
|
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 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.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.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, 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.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||||
|
|
||||||
if params.isExpanded {
|
transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0)
|
||||||
|
|
||||||
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
|
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: 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -223,10 +223,12 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
|
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
|
||||||
encoder.setFragmentTexture(blurredTexture, index: 0)
|
encoder.setFragmentTexture(blurredTexture, index: 0)
|
||||||
|
|
||||||
var brightness: Float = 1.4
|
var brightness: Float = 1.0
|
||||||
var saturation: Float = 1.1
|
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(&brightness, length: 4, index: 0)
|
||||||
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
|
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
|
||||||
|
encoder.setFragmentBytes(&overlay, length: 4 * 4, index: 2)
|
||||||
|
|
||||||
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||||
})
|
})
|
||||||
@ -245,8 +247,10 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
|
|
||||||
var brightness: Float = 1.0
|
var brightness: Float = 1.0
|
||||||
var saturation: Float = 1.0
|
var saturation: Float = 1.0
|
||||||
|
var overlay: SIMD4<Float> = SIMD4<Float>()
|
||||||
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
|
encoder.setFragmentBytes(&brightness, length: 4, index: 0)
|
||||||
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
|
encoder.setFragmentBytes(&saturation, length: 4, index: 1)
|
||||||
|
encoder.setFragmentBytes(&overlay, length: 4 * 4, index: 2)
|
||||||
|
|
||||||
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
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 titleView: TextView
|
||||||
private let overlayBackgroundView: UIImageView
|
private let overlayBackgroundView: UIImageView
|
||||||
private let backgroundView: UIImageView
|
|
||||||
|
|
||||||
private var currentLayout: Layout?
|
private var currentLayout: Layout?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.titleView = TextView()
|
self.titleView = TextView()
|
||||||
self.overlayBackgroundView = UIImageView()
|
self.overlayBackgroundView = UIImageView()
|
||||||
self.backgroundView = UIImageView()
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.maskContents.addSubview(self.overlayBackgroundView)
|
self.maskContents.addSubview(self.overlayBackgroundView)
|
||||||
self.addSubview(self.backgroundView)
|
|
||||||
self.addSubview(self.titleView)
|
self.addSubview(self.titleView)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +45,7 @@ final class WeakSignalView: OverlayMaskContainerView {
|
|||||||
return currentLayout.size
|
return currentLayout.size
|
||||||
}
|
}
|
||||||
|
|
||||||
let sideInset: CGFloat = 8.0
|
let sideInset: CGFloat = 11.0
|
||||||
let height: CGFloat = 30.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)
|
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 {
|
if self.overlayBackgroundView.image?.size.height != height {
|
||||||
self.overlayBackgroundView.image = generateStretchableFilledCircleImage(diameter: height, color: .white)
|
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.overlayBackgroundView.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
self.backgroundView.frame = CGRect(origin: CGPoint(), size: size)
|
|
||||||
|
|
||||||
self.currentLayout = Layout(params: params, size: size)
|
self.currentLayout = Layout(params: params, size: size)
|
||||||
return size
|
return size
|
||||||
|
@ -6,11 +6,13 @@ public final class VideoSourceOutput {
|
|||||||
public let y: MTLTexture
|
public let y: MTLTexture
|
||||||
public let uv: MTLTexture
|
public let uv: MTLTexture
|
||||||
public let rotationAngle: Float
|
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.y = y
|
||||||
self.uv = uv
|
self.uv = uv
|
||||||
self.rotationAngle = rotationAngle
|
self.rotationAngle = rotationAngle
|
||||||
|
self.sourceId = sourceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,6 +38,8 @@ public final class FileVideoSource: VideoSource {
|
|||||||
|
|
||||||
private var displayLink: SharedDisplayLink.Subscription?
|
private var displayLink: SharedDisplayLink.Subscription?
|
||||||
|
|
||||||
|
public var sourceId: Int = 0
|
||||||
|
|
||||||
public init?(device: MTLDevice, url: URL) {
|
public init?(device: MTLDevice, url: URL) {
|
||||||
self.device = device
|
self.device = device
|
||||||
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
||||||
@ -114,7 +118,7 @@ public final class FileVideoSource: VideoSource {
|
|||||||
|
|
||||||
rotationAngle = Float.pi * 0.5
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,10 +128,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
|
|
||||||
private var emojiView: KeyEmojiView?
|
private var emojiView: KeyEmojiView?
|
||||||
|
|
||||||
private var videoContainerView: VideoContainerView?
|
private var localVideoContainerView: VideoContainerView?
|
||||||
|
private var remoteVideoContainerView: VideoContainerView?
|
||||||
|
|
||||||
private var activeRemoteVideoSource: VideoSource?
|
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 processedInitialAudioLevelBump: Bool = false
|
||||||
private var audioLevelBump: Float = 0.0
|
private var audioLevelBump: Float = 0.0
|
||||||
@ -141,6 +145,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
private var audioLevelUpdateSubscription: SharedDisplayLinkDriver.Link?
|
private var audioLevelUpdateSubscription: SharedDisplayLinkDriver.Link?
|
||||||
|
|
||||||
public var speakerAction: (() -> Void)?
|
public var speakerAction: (() -> Void)?
|
||||||
|
public var flipCameraAction: (() -> Void)?
|
||||||
public var videoAction: (() -> Void)?
|
public var videoAction: (() -> Void)?
|
||||||
public var microhoneMuteAction: (() -> Void)?
|
public var microhoneMuteAction: (() -> Void)?
|
||||||
public var endCallAction: (() -> Void)?
|
public var endCallAction: (() -> Void)?
|
||||||
@ -164,14 +169,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.layer.addSublayer(self.backgroundLayer)
|
self.layer.addSublayer(self.backgroundLayer)
|
||||||
self.overlayContentsView.layer.addSublayer(self.backgroundLayer.blurredLayer)
|
self.overlayContentsView.layer.addSublayer(self.backgroundLayer.blurredLayer)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.blobLayer)
|
||||||
|
self.layer.addSublayer(self.avatarLayer)
|
||||||
|
|
||||||
self.overlayContentsView.mask = self.maskContents
|
self.overlayContentsView.mask = self.maskContents
|
||||||
self.addSubview(self.overlayContentsView)
|
self.addSubview(self.overlayContentsView)
|
||||||
|
|
||||||
self.addSubview(self.buttonGroupView)
|
self.addSubview(self.buttonGroupView)
|
||||||
|
|
||||||
self.layer.addSublayer(self.blobLayer)
|
|
||||||
self.layer.addSublayer(self.avatarLayer)
|
|
||||||
|
|
||||||
self.addSubview(self.titleView)
|
self.addSubview(self.titleView)
|
||||||
|
|
||||||
self.addSubview(self.statusView)
|
self.addSubview(self.statusView)
|
||||||
@ -179,6 +184,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self?.update(transition: .immediate)
|
self?.update(transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(self.layer as? SimpleLayer)?.didEnterHierarchy = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
self.audioLevelUpdateSubscription = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in
|
self.audioLevelUpdateSubscription = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -186,13 +195,21 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.attenuateAudioLevelStep()
|
self.attenuateAudioLevelStep()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
(self.layer as? SimpleLayer)?.didExitHierarchy = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.audioLevelUpdateSubscription = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.waitingForFirstVideoFrameDisposable?.dispose()
|
self.waitingForFirstRemoteVideoFrameDisposable?.dispose()
|
||||||
|
self.waitingForFirstLocalVideoFrameDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
@ -216,7 +233,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateAudioLevel() {
|
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)
|
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)
|
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 {
|
if self.params?.state.remoteVideo !== params.state.remoteVideo {
|
||||||
self.waitingForFirstVideoFrameDisposable?.dispose()
|
self.waitingForFirstRemoteVideoFrameDisposable?.dispose()
|
||||||
|
|
||||||
if let remoteVideo = params.state.remoteVideo {
|
if let remoteVideo = params.state.remoteVideo {
|
||||||
if remoteVideo.currentOutput != nil {
|
if remoteVideo.currentOutput != nil {
|
||||||
@ -255,7 +272,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
}
|
}
|
||||||
var shouldUpdate = false
|
var shouldUpdate = false
|
||||||
self.waitingForFirstVideoFrameDisposable = (firstVideoFrameSignal
|
self.waitingForFirstRemoteVideoFrameDisposable = (firstVideoFrameSignal
|
||||||
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|
||||||
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -272,6 +289,44 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.activeRemoteVideoSource = nil
|
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.params = params
|
||||||
self.updateInternal(params: params, transition: transition)
|
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 renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
|
||||||
let edgeSize: Int = 2
|
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)
|
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))
|
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))
|
transition.setFrame(view: self.buttonGroupView, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||||
|
|
||||||
let buttons: [ButtonGroupView.Button] = [
|
var buttons: [ButtonGroupView.Button] = [
|
||||||
ButtonGroupView.Button(content: .speaker(isActive: params.state.audioOutput != .internalSpeaker), action: { [weak self] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.speakerAction?()
|
|
||||||
}),
|
|
||||||
ButtonGroupView.Button(content: .video(isActive: params.state.localVideo != nil), action: { [weak self] in
|
ButtonGroupView.Button(content: .video(isActive: params.state.localVideo != nil), action: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -345,6 +412,21 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.endCallAction?()
|
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)
|
self.buttonGroupView.update(size: params.size, buttons: buttons, transition: transition)
|
||||||
|
|
||||||
if case let .active(activeState) = params.state.lifecycleState {
|
if case let .active(activeState) = params.state.lifecycleState {
|
||||||
@ -367,7 +449,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
} else {
|
} else {
|
||||||
if let emojiView = self.emojiView {
|
if let emojiView = self.emojiView {
|
||||||
self.emojiView = nil
|
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 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 expandedAvatarFrame = CGRect(origin: CGPoint(), size: params.size)
|
||||||
let avatarFrame = self.activeRemoteVideoSource != nil ? expandedAvatarFrame : collapsedAvatarFrame
|
let expandedVideoFrame = CGRect(origin: CGPoint(), size: params.size)
|
||||||
let avatarCornerRadius = self.activeRemoteVideoSource != nil ? params.screenCornerRadius : collapsedAvatarSize * 0.5
|
let avatarFrame = havePrimaryVideo ? expandedAvatarFrame : collapsedAvatarFrame
|
||||||
|
let avatarCornerRadius = havePrimaryVideo ? params.screenCornerRadius : collapsedAvatarSize * 0.5
|
||||||
|
|
||||||
if let activeRemoteVideoSource = self.activeRemoteVideoSource {
|
let minimizedVideoInsets = UIEdgeInsets(top: 124.0, left: 12.0, bottom: 178.0, right: 12.0)
|
||||||
let videoContainerView: VideoContainerView
|
|
||||||
if let current = self.videoContainerView {
|
if let primaryVideoSource {
|
||||||
videoContainerView = current
|
let remoteVideoContainerView: VideoContainerView
|
||||||
|
if let current = self.remoteVideoContainerView {
|
||||||
|
remoteVideoContainerView = current
|
||||||
} else {
|
} else {
|
||||||
videoContainerView = VideoContainerView(frame: CGRect())
|
remoteVideoContainerView = VideoContainerView(frame: CGRect())
|
||||||
self.videoContainerView = videoContainerView
|
self.remoteVideoContainerView = remoteVideoContainerView
|
||||||
self.insertSubview(videoContainerView, belowSubview: self.titleView)
|
self.insertSubview(remoteVideoContainerView, belowSubview: self.overlayContentsView)
|
||||||
self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer)
|
self.overlayContentsView.layer.addSublayer(remoteVideoContainerView.blurredContainerLayer)
|
||||||
|
|
||||||
videoContainerView.layer.position = self.avatarLayer.position
|
remoteVideoContainerView.layer.position = self.avatarLayer.position
|
||||||
videoContainerView.layer.bounds = self.avatarLayer.bounds
|
remoteVideoContainerView.layer.bounds = self.avatarLayer.bounds
|
||||||
videoContainerView.alpha = 0.0
|
remoteVideoContainerView.alpha = 0.0
|
||||||
videoContainerView.blurredContainerLayer.position = self.avatarLayer.position
|
remoteVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position
|
||||||
videoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
|
remoteVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
|
||||||
videoContainerView.blurredContainerLayer.opacity = 0.0
|
remoteVideoContainerView.blurredContainerLayer.opacity = 0.0
|
||||||
videoContainerView.update(size: self.avatarLayer.bounds.size, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isExpanded: false, transition: .immediate)
|
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 {
|
if remoteVideoContainerView.video !== primaryVideoSource {
|
||||||
videoContainerView.video = activeRemoteVideoSource
|
remoteVideoContainerView.video = primaryVideoSource
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.setPosition(view: videoContainerView, position: avatarFrame.center)
|
transition.setPosition(view: remoteVideoContainerView, position: expandedVideoFrame.center)
|
||||||
transition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
||||||
transition.setAlpha(view: videoContainerView, alpha: 1.0)
|
transition.setAlpha(view: remoteVideoContainerView, alpha: 1.0)
|
||||||
transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
|
||||||
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
||||||
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0)
|
transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 1.0)
|
||||||
videoContainerView.update(size: avatarFrame.size, cornerRadius: avatarCornerRadius, isExpanded: self.activeRemoteVideoSource != nil, transition: transition)
|
remoteVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: false, isAnimatingOut: false, transition: transition)
|
||||||
} else {
|
} else {
|
||||||
if let videoContainerView = self.videoContainerView {
|
if let remoteVideoContainerView = self.remoteVideoContainerView {
|
||||||
videoContainerView.update(size: avatarFrame.size, cornerRadius: avatarCornerRadius, isExpanded: self.activeRemoteVideoSource != nil, transition: transition)
|
remoteVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition)
|
||||||
transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
||||||
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
|
transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 0.0)
|
||||||
transition.setPosition(view: videoContainerView, position: avatarFrame.center)
|
transition.setPosition(view: remoteVideoContainerView, position: avatarFrame.center)
|
||||||
transition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
if videoContainerView.alpha != 0.0 {
|
if remoteVideoContainerView.alpha != 0.0 {
|
||||||
transition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak self, weak videoContainerView] completed in
|
transition.setAlpha(view: remoteVideoContainerView, alpha: 0.0, completion: { [weak self, weak remoteVideoContainerView] completed in
|
||||||
guard let self, let videoContainerView, completed else {
|
guard let self, let remoteVideoContainerView, completed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
videoContainerView.removeFromSuperview()
|
remoteVideoContainerView.removeFromSuperview()
|
||||||
videoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
remoteVideoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
||||||
if self.videoContainerView === videoContainerView {
|
if self.remoteVideoContainerView === remoteVideoContainerView {
|
||||||
self.videoContainerView = nil
|
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.setPosition(layer: self.avatarLayer, position: avatarFrame.center)
|
||||||
transition.setBounds(layer: self.avatarLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
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))
|
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))
|
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 {
|
switch params.state.lifecycleState {
|
||||||
case .terminated:
|
case .terminated:
|
||||||
titleString = "Call Ended"
|
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)
|
transition.setAlpha(layer: self.blobLayer, alpha: 0.0)
|
||||||
default:
|
default:
|
||||||
titleString = params.state.name
|
titleString = params.state.name
|
||||||
|
transition.setAlpha(layer: self.blobLayer, alpha: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleSize = self.titleView.update(
|
let titleSize = self.titleView.update(
|
||||||
string: titleString,
|
string: titleString,
|
||||||
fontSize: self.activeRemoteVideoSource == nil ? 28.0 : 17.0,
|
fontSize: !havePrimaryVideo ? 28.0 : 17.0,
|
||||||
fontWeight: self.activeRemoteVideoSource == nil ? 0.0 : 0.25,
|
fontWeight: !havePrimaryVideo ? 0.0 : 0.25,
|
||||||
color: .white,
|
color: .white,
|
||||||
constrainedWidth: params.size.width - 16.0 * 2.0,
|
constrainedWidth: params.size.width - 16.0 * 2.0,
|
||||||
transition: transition
|
transition: transition
|
||||||
@ -464,7 +609,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
let titleFrame = CGRect(
|
let titleFrame = CGRect(
|
||||||
origin: CGPoint(
|
origin: CGPoint(
|
||||||
x: (params.size.width - titleSize.width) * 0.5,
|
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
|
size: titleSize
|
||||||
)
|
)
|
||||||
@ -492,6 +637,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
case let .terminated(terminatedState):
|
case let .terminated(terminatedState):
|
||||||
|
self.processedInitialAudioLevelBump = false
|
||||||
statusState = .terminated(StatusView.TerminatedState(duration: terminatedState.duration))
|
statusState = .terminated(StatusView.TerminatedState(duration: terminatedState.duration))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -518,7 +664,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
let statusFrame = CGRect(
|
let statusFrame = CGRect(
|
||||||
origin: CGPoint(
|
origin: CGPoint(
|
||||||
x: (params.size.width - statusSize.width) * 0.5,
|
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
|
size: statusSize
|
||||||
)
|
)
|
||||||
@ -534,7 +680,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
transition.setFrame(view: self.statusView, frame: statusFrame)
|
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
|
let weakSignalView: WeakSignalView
|
||||||
if let current = self.weakSignalView {
|
if let current = self.weakSignalView {
|
||||||
weakSignalView = current
|
weakSignalView = current
|
||||||
@ -544,7 +690,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.addSubview(weakSignalView)
|
self.addSubview(weakSignalView)
|
||||||
}
|
}
|
||||||
let weakSignalSize = weakSignalView.update(constrainedSize: CGSize(width: params.size.width - 32.0, height: 100.0))
|
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 {
|
if weakSignalView.bounds.isEmpty {
|
||||||
weakSignalView.frame = weakSignalFrame
|
weakSignalView.frame = weakSignalFrame
|
||||||
if !transition.animation.isImmediate {
|
if !transition.animation.isImmediate {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user