diff --git a/Tests/CallUITest/Sources/ViewController.swift b/Tests/CallUITest/Sources/ViewController.swift index da7ac61a97..d9103d2dad 100644 --- a/Tests/CallUITest/Sources/ViewController.swift +++ b/Tests/CallUITest/Sources/ViewController.swift @@ -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)) } diff --git a/submodules/MetalEngine/Sources/MetalEngine.swift b/submodules/MetalEngine/Sources/MetalEngine.swift index ab5bb810ce..bf25c2ad1d 100644 --- a/submodules/MetalEngine/Sources/MetalEngine.swift +++ b/submodules/MetalEngine/Sources/MetalEngine.swift @@ -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 { diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal index 1306472e92..928f110668 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Metal/CallScreenShaders.metal @@ -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 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); } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift index 61ccb20fb9..8eddb2ae02 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ButtonGroupView.swift @@ -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 } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift index 465b0f2c69..ee62e7c121 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/CallBackgroundLayer.swift @@ -205,10 +205,12 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject { var colors: [SIMD4] = self.colorTransition.value.colors encoder.setFragmentBytes(&colors, length: 4 * MemoryLayout>.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 = isBlur ? SIMD4(1.0, 1.0, 1.0, 0.2) : SIMD4() 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) }) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift index be9a3504ee..9347cbf35a 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/ContentOverlayButton.swift @@ -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)) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView copy.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MinimizedVideoContainerView.swift similarity index 54% rename from submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView copy.swift rename to submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MinimizedVideoContainerView.swift index fc58433faa..90b9434f43 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/TitleView copy.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/MinimizedVideoContainerView.swift @@ -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))) } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift index 77655c6c68..e9098f3032 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/PrivateCallVideoLayer.swift @@ -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 = SIMD4(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 = SIMD4() 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) }) diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift new file mode 100644 index 0000000000..022fae6ebb --- /dev/null +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/VideoContainerView.swift @@ -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) + } + } +} diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/WeakSignalView.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/WeakSignalView.swift index 6fea71c89a..21f572d0c5 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/WeakSignalView.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Components/WeakSignalView.swift @@ -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 diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift index df63cc495a..90409c4966 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/Media/VideoInput.swift @@ -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 } } diff --git a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift index 3926c1faa9..a1d7b0dce6 100644 --- a/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift +++ b/submodules/TelegramUI/Components/Calls/CallScreen/Sources/PrivateCallScreen.swift @@ -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 { 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 {