Swiftgram/submodules/TelegramCallsUI/Sources/VideoChatParticipantVideoComponent.swift
2025-05-05 18:04:32 +02:00

764 lines
38 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
import MultilineTextComponent
import TelegramPresentationData
import BundleIconComponent
import MetalEngine
import CallScreen
import TelegramCore
import AccountContext
import SwiftSignalKit
import DirectMediaImageCache
import FastBlur
import ContextUI
import ComponentDisplayAdapters
import AvatarNode
private func blurredAvatarImage(_ dataImage: UIImage) -> UIImage? {
let imageContextSize = CGSize(width: 64.0, height: 64.0)
if let imageContext = DrawingContext(size: imageContextSize, scale: 1.0, clear: true) {
imageContext.withFlippedContext { c in
if let cgImage = dataImage.cgImage {
c.draw(cgImage, in: CGRect(origin: CGPoint(), size: imageContextSize))
}
}
telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes)
return imageContext.generateImage()
} else {
return nil
}
}
private let activityBorderImage: UIImage = {
return generateStretchableFilledCircleImage(diameter: 20.0, color: nil, strokeColor: .white, strokeWidth: 2.0)!.withRenderingMode(.alwaysTemplate)
}()
final class VideoChatParticipantVideoComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let call: VideoChatCall
let participant: GroupCallParticipantsContext.Participant
let isMyPeer: Bool
let isPresentation: Bool
let isSpeaking: Bool
let maxVideoQuality: Int
let isExpanded: Bool
let isUIHidden: Bool
let contentInsets: UIEdgeInsets
let controlInsets: UIEdgeInsets
let interfaceOrientation: UIInterfaceOrientation
let enableVideoSharpening: Bool
let action: (() -> Void)?
let contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?
let activatePinch: ((PinchSourceContainerNode) -> Void)?
let deactivatedPinch: (() -> Void)?
init(
theme: PresentationTheme,
strings: PresentationStrings,
call: VideoChatCall,
participant: GroupCallParticipantsContext.Participant,
isMyPeer: Bool,
isPresentation: Bool,
isSpeaking: Bool,
maxVideoQuality: Int,
isExpanded: Bool,
isUIHidden: Bool,
contentInsets: UIEdgeInsets,
controlInsets: UIEdgeInsets,
interfaceOrientation: UIInterfaceOrientation,
enableVideoSharpening: Bool,
action: (() -> Void)?,
contextAction: ((EnginePeer, ContextExtractedContentContainingView, ContextGesture) -> Void)?,
activatePinch: ((PinchSourceContainerNode) -> Void)?,
deactivatedPinch: (() -> Void)?
) {
self.theme = theme
self.strings = strings
self.call = call
self.participant = participant
self.isMyPeer = isMyPeer
self.isPresentation = isPresentation
self.isSpeaking = isSpeaking
self.maxVideoQuality = maxVideoQuality
self.isExpanded = isExpanded
self.isUIHidden = isUIHidden
self.contentInsets = contentInsets
self.controlInsets = controlInsets
self.interfaceOrientation = interfaceOrientation
self.enableVideoSharpening = enableVideoSharpening
self.action = action
self.contextAction = contextAction
self.activatePinch = activatePinch
self.deactivatedPinch = deactivatedPinch
}
static func ==(lhs: VideoChatParticipantVideoComponent, rhs: VideoChatParticipantVideoComponent) -> Bool {
if lhs.call != rhs.call {
return false
}
if lhs.participant != rhs.participant {
return false
}
if lhs.isMyPeer != rhs.isMyPeer {
return false
}
if lhs.isPresentation != rhs.isPresentation {
return false
}
if lhs.isSpeaking != rhs.isSpeaking {
return false
}
if lhs.maxVideoQuality != rhs.maxVideoQuality {
return false
}
if lhs.isExpanded != rhs.isExpanded {
return false
}
if lhs.isUIHidden != rhs.isUIHidden {
return false
}
if lhs.contentInsets != rhs.contentInsets {
return false
}
if lhs.controlInsets != rhs.controlInsets {
return false
}
if lhs.interfaceOrientation != rhs.interfaceOrientation {
return false
}
if lhs.enableVideoSharpening != rhs.enableVideoSharpening {
return false
}
if (lhs.action == nil) != (rhs.action == nil) {
return false
}
if (lhs.contextAction == nil) != (rhs.contextAction == nil) {
return false
}
if (lhs.activatePinch == nil) != (rhs.activatePinch == nil) {
return false
}
if (lhs.deactivatedPinch == nil) != (rhs.deactivatedPinch == nil) {
return false
}
return true
}
private struct VideoSpec: Equatable {
var resolution: CGSize
var rotationAngle: Float
var followsDeviceOrientation: Bool
init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool) {
self.resolution = resolution
self.rotationAngle = rotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
}
}
private struct ReferenceLocation: Equatable {
var containerWidth: CGFloat
var positionX: CGFloat
init(containerWidth: CGFloat, positionX: CGFloat) {
self.containerWidth = containerWidth
self.positionX = positionX
}
}
private final class AnimationHint {
enum Kind {
case videoAvailabilityChanged
}
let kind: Kind
init(kind: Kind) {
self.kind = kind
}
}
final class View: ContextControllerSourceView {
private var component: VideoChatParticipantVideoComponent?
private weak var componentState: EmptyComponentState?
private var isUpdating: Bool = false
private var previousSize: CGSize?
private let backgroundGradientView: UIImageView
private let muteStatus = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var blurredAvatarDisposable: Disposable?
private var blurredAvatarView: UIImageView?
private let pinchContainerNode: PinchSourceContainerNode
private let extractedContainerView: ContextExtractedContentContainingView
private var videoSource: AdaptedCallVideoSource?
private var videoPlaceholder: VideoSource.Output?
private var videoDisposable: Disposable?
private var videoBackgroundLayer: SimpleLayer?
private var videoLayer: PrivateCallVideoLayer?
private var videoSpec: VideoSpec?
private var awaitingFirstVideoFrameForUnpause: Bool = false
private var videoStatus: ComponentView<Empty>?
private var activityBorderView: UIImageView?
private var referenceLocation: ReferenceLocation?
private var loadingEffectView: VideoChatVideoLoadingEffectView?
override init(frame: CGRect) {
self.backgroundGradientView = UIImageView()
self.pinchContainerNode = PinchSourceContainerNode()
self.extractedContainerView = ContextExtractedContentContainingView()
super.init(frame: frame)
self.addSubview(self.extractedContainerView)
self.targetViewForActivationProgress = self.extractedContainerView.contentView
self.extractedContainerView.contentView.addSubview(self.pinchContainerNode.view)
self.pinchContainerNode.contentNode.view.addSubview(self.backgroundGradientView)
//TODO:release optimize
self.pinchContainerNode.contentNode.view.layer.cornerRadius = 10.0
self.pinchContainerNode.contentNode.view.clipsToBounds = true
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.pinchContainerNode.activate = { [weak self] sourceNode in
guard let self, let component = self.component else {
return
}
component.activatePinch?(sourceNode)
}
self.pinchContainerNode.animatedOut = { [weak self] in
guard let self, let component = self.component else {
return
}
component.deactivatedPinch?()
}
self.activated = { [weak self] gesture, _ in
guard let self, let component = self.component else {
gesture.cancel()
return
}
if let participantPeer = component.participant.peer {
component.contextAction?(participantPeer, self.extractedContainerView, gesture)
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.videoDisposable?.dispose()
self.blurredAvatarDisposable?.dispose()
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
guard let component = self.component, let action = component.action else {
return
}
action()
}
}
func updatePlaceholder(placeholder: VideoSource.Output) {
self.videoPlaceholder = placeholder
self.componentState?.updated(transition: .immediate, isLocal: true)
}
func update(component: VideoChatParticipantVideoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.isUpdating = true
defer {
self.isUpdating = false
}
let previousComponent = self.component
self.component = component
self.componentState = state
self.isGestureEnabled = !component.isExpanded
self.pinchContainerNode.isPinchGestureEnabled = component.activatePinch != nil
transition.setPosition(view: self.pinchContainerNode.view, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.pinchContainerNode.view, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.pinchContainerNode.update(size: availableSize, transition: transition.containedViewLayoutTransition)
transition.setPosition(view: self.extractedContainerView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.extractedContainerView.contentView, position: CGRect(origin: CGPoint(), size: availableSize).center)
transition.setBounds(view: self.extractedContainerView.contentView, bounds: CGRect(origin: CGPoint(), size: availableSize))
self.extractedContainerView.contentRect = CGRect(origin: CGPoint(), size: availableSize)
transition.setFrame(view: self.pinchContainerNode.contentNode.view, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: self.backgroundGradientView, frame: CGRect(origin: CGPoint(), size: availableSize))
let alphaTransition: ComponentTransition
if !transition.animation.isImmediate {
alphaTransition = .easeInOut(duration: 0.2)
} else {
alphaTransition = .immediate
}
let videoAlphaTransition: ComponentTransition
if let animationHint = transition.userData(AnimationHint.self), case .videoAvailabilityChanged = animationHint.kind {
videoAlphaTransition = .easeInOut(duration: 0.2)
} else {
videoAlphaTransition = alphaTransition
}
let controlsAlpha: CGFloat = component.isUIHidden ? 0.0 : 1.0
if previousComponent == nil {
let colors = calculateAvatarColors(context: component.call.accountContext, explicitColorIndex: nil, peerId: component.participant.peer?.id, nameColor: component.participant.peer?.nameColor, icon: .none, theme: component.theme)
self.backgroundGradientView.image = generateGradientImage(size: CGSize(width: 8.0, height: 32.0), colors: colors.reversed(), locations: [0.0, 1.0], direction: .vertical)
}
if let smallProfileImage = component.participant.peer?.smallProfileImage {
let blurredAvatarView: UIImageView
if let current = self.blurredAvatarView {
blurredAvatarView = current
transition.setFrame(view: blurredAvatarView, frame: CGRect(origin: CGPoint(), size: availableSize))
} else {
blurredAvatarView = UIImageView()
blurredAvatarView.contentMode = .scaleAspectFill
self.blurredAvatarView = blurredAvatarView
self.pinchContainerNode.contentNode.view.insertSubview(blurredAvatarView, aboveSubview: self.backgroundGradientView)
blurredAvatarView.frame = CGRect(origin: CGPoint(), size: availableSize)
}
if self.blurredAvatarDisposable == nil {
//TODO:release synchronous
if let participantPeer = component.participant.peer, let imageCache = component.call.accountContext.imageCache as? DirectMediaImageCache, let peerReference = PeerReference(participantPeer._asPeer()) {
if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: participantPeer.profileImageRepresentations.first?.immediateThumbnailData, size: 64, synchronous: false) {
if let image = result.image {
blurredAvatarView.image = blurredAvatarImage(image)
}
if let loadSignal = result.loadSignal {
self.blurredAvatarDisposable = (loadSignal
|> deliverOnMainQueue).startStrict(next: { [weak self] image in
guard let self else {
return
}
if let image {
self.blurredAvatarView?.image = blurredAvatarImage(image)
} else {
self.blurredAvatarView?.image = nil
}
})
}
}
}
}
} else {
if let blurredAvatarView = self.blurredAvatarView {
self.blurredAvatarView = nil
blurredAvatarView.removeFromSuperview()
}
if let blurredAvatarDisposable = self.blurredAvatarDisposable {
self.blurredAvatarDisposable = nil
blurredAvatarDisposable.dispose()
}
}
let muteStatusSize = self.muteStatus.update(
transition: transition,
component: AnyComponent(VideoChatMuteIconComponent(
color: .white,
content: component.isPresentation ? .screenshare : .mute(isFilled: true, isMuted: component.participant.muteState != nil && !component.isSpeaking),
shadowColor: UIColor(white: 0.0, alpha: 0.7),
shadowBlur: 8.0
)),
environment: {},
containerSize: CGSize(width: 36.0, height: 36.0)
)
let muteStatusFrame: CGRect
if component.isExpanded {
muteStatusFrame = CGRect(origin: CGPoint(x: 5.0, y: availableSize.height - component.controlInsets.bottom + 1.0 - muteStatusSize.height), size: muteStatusSize)
} else {
muteStatusFrame = CGRect(origin: CGPoint(x: 1.0, y: availableSize.height - component.controlInsets.bottom + 3.0 - muteStatusSize.height), size: muteStatusSize)
}
if let muteStatusView = self.muteStatus.view {
if muteStatusView.superview == nil {
self.pinchContainerNode.contentNode.view.addSubview(muteStatusView)
muteStatusView.alpha = controlsAlpha
}
transition.setPosition(view: muteStatusView, position: muteStatusFrame.center)
transition.setBounds(view: muteStatusView, bounds: CGRect(origin: CGPoint(), size: muteStatusFrame.size))
transition.setScale(view: muteStatusView, scale: component.isExpanded ? 1.0 : 0.7)
alphaTransition.setAlpha(view: muteStatusView, alpha: controlsAlpha)
}
let titleInnerInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.participant.peer?.debugDisplayTitle ?? "User \(component.participant.id)", font: Font.semibold(16.0), textColor: .white)),
insets: titleInnerInsets,
textShadowColor: UIColor(white: 0.0, alpha: 0.7),
textShadowBlur: 8.0
)),
environment: {},
containerSize: CGSize(width: availableSize.width - 8.0 * 2.0 - 4.0, height: 100.0)
)
let titleFrame: CGRect
if component.isExpanded {
titleFrame = CGRect(origin: CGPoint(x: 36.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 8.0 - titleSize.height + titleInnerInsets.top), size: titleSize)
} else {
titleFrame = CGRect(origin: CGPoint(x: 29.0 - titleInnerInsets.left, y: availableSize.height - component.controlInsets.bottom - 4.0 - titleSize.height + titleInnerInsets.top + 1.0), size: titleSize)
}
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.layer.anchorPoint = CGPoint()
self.pinchContainerNode.contentNode.view.addSubview(titleView)
titleView.alpha = controlsAlpha
}
transition.setPosition(view: titleView, position: titleFrame.origin)
titleView.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
transition.setScale(view: titleView, scale: component.isExpanded ? 1.0 : 0.825)
alphaTransition.setAlpha(view: titleView, alpha: controlsAlpha)
}
var previousVideoDescription: GroupCallParticipantsContext.Participant.VideoDescription?
if let previousComponent {
if previousComponent.isMyPeer && previousComponent.isPresentation {
previousVideoDescription = nil
} else {
previousVideoDescription = previousComponent.maxVideoQuality == 0 ? nil : (previousComponent.isPresentation ? previousComponent.participant.presentationDescription : previousComponent.participant.videoDescription)
}
}
let videoDescription: GroupCallParticipantsContext.Participant.VideoDescription?
if component.isMyPeer && component.isPresentation {
videoDescription = nil
} else {
videoDescription = component.maxVideoQuality == 0 ? nil : (component.isPresentation ? component.participant.presentationDescription : component.participant.videoDescription)
}
var isEffectivelyPaused = false
if let videoDescription, videoDescription.isPaused {
isEffectivelyPaused = true
} else if let previousComponent {
let previousVideoDescription = previousComponent.isPresentation ? previousComponent.participant.presentationDescription : previousComponent.participant.videoDescription
if let previousVideoDescription, previousVideoDescription.isPaused {
self.awaitingFirstVideoFrameForUnpause = true
}
if self.awaitingFirstVideoFrameForUnpause {
isEffectivelyPaused = true
}
}
if let videoDescription {
let videoBackgroundLayer: SimpleLayer
if let current = self.videoBackgroundLayer {
videoBackgroundLayer = current
} else {
videoBackgroundLayer = SimpleLayer()
videoBackgroundLayer.backgroundColor = UIColor(white: 0.1, alpha: 1.0).cgColor
videoBackgroundLayer.opacity = 0.0
self.videoBackgroundLayer = videoBackgroundLayer
if let blurredAvatarView = self.blurredAvatarView {
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: blurredAvatarView.layer)
} else {
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoBackgroundLayer, above: self.backgroundGradientView.layer)
}
videoBackgroundLayer.isHidden = true
}
let videoUpdated: () -> Void = { [weak self] in
guard let self, let videoSource = self.videoSource, let videoLayer = self.videoLayer else {
return
}
var videoOutput = videoSource.currentOutput
var isPlaceholder = false
if videoOutput == nil {
isPlaceholder = true
videoOutput = self.videoPlaceholder
} else {
self.videoPlaceholder = nil
}
videoLayer.video = videoOutput
if let videoOutput {
let videoSpec = VideoSpec(resolution: videoOutput.resolution, rotationAngle: videoOutput.rotationAngle, followsDeviceOrientation: videoOutput.followsDeviceOrientation)
if self.videoSpec != videoSpec || self.awaitingFirstVideoFrameForUnpause {
self.awaitingFirstVideoFrameForUnpause = false
self.videoSpec = videoSpec
if !self.isUpdating {
var transition: ComponentTransition = .immediate
if !isPlaceholder {
transition = transition.withUserData(AnimationHint(kind: .videoAvailabilityChanged))
}
self.componentState?.updated(transition: transition, isLocal: true)
}
}
} else {
if self.videoSpec != nil {
self.videoSpec = nil
if !self.isUpdating {
self.componentState?.updated(transition: .immediate, isLocal: true)
}
}
}
}
let videoLayer: PrivateCallVideoLayer
var resetVideoSource = false
if let current = self.videoLayer {
videoLayer = current
if let previousVideoDescription, previousVideoDescription.endpointId != videoDescription.endpointId {
resetVideoSource = true
}
} else {
videoLayer = PrivateCallVideoLayer(enableSharpening: component.enableVideoSharpening)
self.videoLayer = videoLayer
videoLayer.opacity = 0.0
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer.blurredLayer, above: videoBackgroundLayer)
self.pinchContainerNode.contentNode.view.layer.insertSublayer(videoLayer, above: videoLayer.blurredLayer)
videoLayer.blurredLayer.opacity = 0.0
resetVideoSource = true
}
if resetVideoSource {
if let input = component.call.video(endpointId: videoDescription.endpointId) {
let videoSource = AdaptedCallVideoSource(videoStreamSignal: input)
self.videoSource = videoSource
self.videoDisposable?.dispose()
self.videoDisposable = videoSource.addOnUpdated {
videoUpdated()
}
}
}
if let _ = self.videoPlaceholder, videoLayer.video == nil {
videoUpdated()
}
transition.setFrame(layer: videoBackgroundLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
if let videoSpec = self.videoSpec {
if videoBackgroundLayer.isHidden {
videoBackgroundLayer.isHidden = false
}
videoAlphaTransition.setAlpha(layer: videoBackgroundLayer, alpha: 1.0)
if isEffectivelyPaused {
videoAlphaTransition.setAlpha(layer: videoLayer, alpha: 0.0)
videoAlphaTransition.setAlpha(layer: videoLayer.blurredLayer, alpha: 0.9)
} else {
videoAlphaTransition.setAlpha(layer: videoLayer, alpha: 1.0)
videoAlphaTransition.setAlpha(layer: videoLayer.blurredLayer, alpha: 0.25)
}
let rotationAngle = resolveCallVideoRotationAngle(angle: videoSpec.rotationAngle, followsDeviceOrientation: videoSpec.followsDeviceOrientation, interfaceOrientation: component.interfaceOrientation)
var rotatedResolution = videoSpec.resolution
var videoIsRotated = false
if abs(rotationAngle - Float.pi * 0.5) < .ulpOfOne || abs(rotationAngle - Float.pi * 3.0 / 2.0) < .ulpOfOne {
videoIsRotated = true
}
if videoIsRotated {
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
}
let videoSize = rotatedResolution.aspectFitted(availableSize)
let videoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - videoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - videoSize.height) * 0.5)), size: videoSize)
let blurredVideoSize = rotatedResolution.aspectFilled(availableSize)
let blurredVideoFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - blurredVideoSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - blurredVideoSize.height) * 0.5)), size: blurredVideoSize)
let videoResolution = rotatedResolution
var rotatedVideoResolution = videoResolution
var rotatedVideoFrame = videoFrame
var rotatedBlurredVideoFrame = blurredVideoFrame
var rotatedVideoBoundsSize = videoFrame.size
var rotatedBlurredVideoBoundsSize = blurredVideoFrame.size
if videoIsRotated {
rotatedVideoBoundsSize = CGSize(width: rotatedVideoBoundsSize.height, height: rotatedVideoBoundsSize.width)
rotatedVideoFrame = rotatedVideoFrame.size.centered(around: rotatedVideoFrame.center)
rotatedBlurredVideoBoundsSize = CGSize(width: rotatedBlurredVideoBoundsSize.height, height: rotatedBlurredVideoBoundsSize.width)
rotatedBlurredVideoFrame = rotatedBlurredVideoFrame.size.centered(around: rotatedBlurredVideoFrame.center)
}
rotatedVideoResolution = rotatedVideoResolution.aspectFittedOrSmaller(CGSize(width: rotatedVideoFrame.width * UIScreenScale, height: rotatedVideoFrame.height * UIScreenScale))
transition.setPosition(layer: videoLayer, position: rotatedVideoFrame.center)
transition.setBounds(layer: videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoBoundsSize))
transition.setTransform(layer: videoLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0))
videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
transition.setPosition(layer: videoLayer.blurredLayer, position: rotatedBlurredVideoFrame.center)
transition.setBounds(layer: videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBlurredVideoBoundsSize))
transition.setTransform(layer: videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(rotationAngle), 0.0, 0.0, 1.0))
}
} else {
if let videoBackgroundLayer = self.videoBackgroundLayer {
self.videoBackgroundLayer = nil
videoBackgroundLayer.removeFromSuperlayer()
}
if let videoLayer = self.videoLayer {
self.videoLayer = nil
videoLayer.blurredLayer.removeFromSuperlayer()
videoLayer.removeFromSuperlayer()
}
self.videoDisposable?.dispose()
self.videoDisposable = nil
self.videoSource = nil
self.videoSpec = nil
}
var statusKind: VideoChatParticipantVideoStatusComponent.Kind?
if component.isPresentation && component.isMyPeer {
statusKind = .ownScreenshare
} else if isEffectivelyPaused {
statusKind = .paused
}
if let statusKind {
let videoStatus: ComponentView<Empty>
var videoStatusTransition = transition
if let current = self.videoStatus {
videoStatus = current
} else {
videoStatusTransition = videoStatusTransition.withAnimation(.none)
videoStatus = ComponentView()
self.videoStatus = videoStatus
}
let _ = videoStatus.update(
transition: videoStatusTransition,
component: AnyComponent(VideoChatParticipantVideoStatusComponent(
strings: component.strings,
kind: statusKind,
isExpanded: component.isExpanded
)),
environment: {},
containerSize: availableSize
)
if let videoStatusView = videoStatus.view {
if videoStatusView.superview == nil {
videoStatusView.isUserInteractionEnabled = false
videoStatusView.alpha = 0.0
self.pinchContainerNode.contentNode.view.addSubview(videoStatusView)
}
videoStatusTransition.setFrame(view: videoStatusView, frame: CGRect(origin: CGPoint(), size: availableSize))
videoAlphaTransition.setAlpha(view: videoStatusView, alpha: 1.0)
}
} else if let videoStatus = self.videoStatus {
self.videoStatus = nil
if let videoStatusView = videoStatus.view {
videoAlphaTransition.setAlpha(view: videoStatusView, alpha: 0.0, completion: { [weak videoStatusView] _ in
videoStatusView?.removeFromSuperview()
})
}
}
if videoDescription != nil && self.videoSpec == nil && !isEffectivelyPaused {
if self.loadingEffectView == nil {
let loadingEffectView = VideoChatVideoLoadingEffectView(effectAlpha: 0.1, borderAlpha: 0.2, cornerRadius: 10.0, duration: 1.0)
self.loadingEffectView = loadingEffectView
loadingEffectView.alpha = 0.0
loadingEffectView.isUserInteractionEnabled = false
self.pinchContainerNode.contentNode.view.addSubview(loadingEffectView)
if let referenceLocation = self.referenceLocation {
self.updateHorizontalReferenceLocation(containerWidth: referenceLocation.containerWidth, positionX: referenceLocation.positionX, transition: .immediate)
}
videoAlphaTransition.setAlpha(view: loadingEffectView, alpha: 1.0)
}
} else if let loadingEffectView = self.loadingEffectView {
self.loadingEffectView = nil
videoAlphaTransition.setAlpha(view: loadingEffectView, alpha: 0.0, completion: { [weak loadingEffectView] _ in
loadingEffectView?.removeFromSuperview()
})
}
if component.isSpeaking && !component.isExpanded {
let activityBorderView: UIImageView
if let current = self.activityBorderView {
activityBorderView = current
} else {
activityBorderView = UIImageView()
self.activityBorderView = activityBorderView
self.pinchContainerNode.contentNode.view.addSubview(activityBorderView)
activityBorderView.image = activityBorderImage
activityBorderView.tintColor = UIColor(rgb: 0x33C758)
if let previousSize {
activityBorderView.frame = CGRect(origin: CGPoint(), size: previousSize)
}
}
} else if let activityBorderView = self.activityBorderView {
if !transition.animation.isImmediate {
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
if activityBorderView.alpha != 0.0 {
alphaTransition.setAlpha(view: activityBorderView, alpha: 0.0, completion: { [weak self, weak activityBorderView] completed in
guard let self, let component = self.component, let activityBorderView, self.activityBorderView === activityBorderView, completed else {
return
}
if !component.isSpeaking {
activityBorderView.removeFromSuperview()
self.activityBorderView = nil
}
})
}
} else {
self.activityBorderView = nil
activityBorderView.removeFromSuperview()
}
}
if let activityBorderView = self.activityBorderView {
transition.setFrame(view: activityBorderView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
self.previousSize = availableSize
return availableSize
}
func updateHorizontalReferenceLocation(containerWidth: CGFloat, positionX: CGFloat, transition: ComponentTransition) {
self.referenceLocation = ReferenceLocation(containerWidth: containerWidth, positionX: positionX)
if let loadingEffectView = self.loadingEffectView, let size = self.previousSize {
transition.setFrame(view: loadingEffectView, frame: CGRect(origin: CGPoint(), size: size))
loadingEffectView.update(size: size, containerWidth: containerWidth, offsetX: positionX, gradientWidth: floor(containerWidth * 0.8), transition: transition)
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}