diff --git a/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift b/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift index 71fa96d44a..255b419402 100644 --- a/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift +++ b/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift @@ -16,6 +16,7 @@ public final class AnimatedCountView: UIView { private let foregroundView = UIView() private let foregroundGradientLayer = CAGradientLayer() private let maskingView = UIView() + private var scaleFactor: CGFloat { 0.7 } override init(frame: CGRect = .zero) { super.init(frame: frame) @@ -49,7 +50,7 @@ public final class AnimatedCountView: UIView { self.foregroundGradientLayer.frame = CGRect(origin: .zero, size: bounds.size).insetBy(dx: -60, dy: -60) self.maskingView.frame = CGRect(origin: .zero, size: bounds.size) countLabel.frame = CGRect(origin: .zero, size: bounds.size) - subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 10 : bounds.height - 6, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20) + subtitleLabel.frame = .init(x: bounds.midX - subtitleLabel.intrinsicContentSize.width / 2 - 10, y: subtitleLabel.text == "No viewers" ? bounds.midY - 8 : bounds.height - 6, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20) } func update(countString: String, subtitle: String) { @@ -64,8 +65,9 @@ public final class AnimatedCountView: UIView { // self.countLabel.attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 60, weight: .semibold)]) // } else { // self.countLabel.attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 54, weight: .semibold)]) -// } - self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) +// + self.countLabel.fontSize = 48 + self.countLabel.attributedText = NSAttributedString(string: text, font: Font.with(size: 48, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) // self.countLabel.attributedText = NSAttributedString(string: text, attributes: [.font: UIFont.systemFont(ofSize: 60, weight: .semibold)]) // var timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height)) // if timerSize.width > size.width - 32.0 { @@ -176,6 +178,14 @@ class AnimatedCountLabel: UILabel { private var chars = [AnimatedCharLayer]() private let containerView = UIView() + var itemWidth: CGFloat { 36 * fontSize / 60 } + var commaWidthForSpacing: CGFloat { 8 * fontSize / 60 } + var commaFrameWidth: CGFloat { 36 * fontSize / 60 } + var interItemSpacing: CGFloat { 0 * fontSize / 60 } + var didBegin = false + var fontSize: CGFloat = 60 + var scaleFactor: CGFloat { 1 } + override init(frame: CGRect = .zero) { super.init(frame: frame) containerView.clipsToBounds = false @@ -186,11 +196,6 @@ class AnimatedCountLabel: UILabel { required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - var itemWidth: CGFloat { 36 } - var commaWidthForSpacing: CGFloat { 8 } - var commaFrameWidth: CGFloat { 36 } - var interItemSpacing: CGFloat { 0 } - var didBegin = false private func offsetForChar(at index: Int, within characters: [NSAttributedString]? = nil) -> CGFloat { if let characters { @@ -201,7 +206,7 @@ class AnimatedCountLabel: UILabel { return $0 + itemWidth + interItemSpacing } if characters.count > index && characters[index].string == "," { - offset -= 4 + offset -= commaWidthForSpacing / 2 // 4 } return offset } else { @@ -212,7 +217,7 @@ class AnimatedCountLabel: UILabel { return $0 + itemWidth + interItemSpacing } if self.chars.count > index && self.chars[index].attributedText?.string == "," { - offset -= 4 + offset -= commaWidthForSpacing / 2 } return offset } @@ -226,8 +231,7 @@ class AnimatedCountLabel: UILabel { } return $0 + itemWidth + interItemSpacing }*/ - interItemSpacing - - containerView.frame = .init(x: bounds.midX - countWidth / 2, y: 0, width: countWidth, height: bounds.height) + containerView.frame = .init(x: bounds.midX - countWidth / 2 * scaleFactor, y: 0, width: countWidth * scaleFactor, height: bounds.height) chars.enumerated().forEach { (index, char) in let offset = offsetForChar(at: index) // char.frame.size.width = char.attributedText?.string == "," ? commaFrameWidth : itemWidth @@ -362,16 +366,16 @@ class AnimatedCountLabel: UILabel { if didBegin && prevCount != chars.count { UIView.animate(withDuration: Double(changeIndex) * initialDuration/*, delay: initialDuration * Double(changeIndex)*/) { [self] in containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height) - if countWidth > self.bounds.width { - let scale = countWidth / self.bounds.width - self.transform = .init(scaleX: scale, y: scale) + if countWidth * scaleFactor > self.bounds.width { + let scale = (self.bounds.width - 32) / (countWidth * scaleFactor) + containerView.transform = .init(scaleX: scale, y: scale) } else { - self.transform = .identity + containerView.transform = .init(scaleX: scaleFactor, y: scaleFactor) } // containerView.backgroundColor = .red.withAlphaComponent(0.3) } } else if countWidth > 0 { - containerView.frame = .init(x: self.bounds.midX - countWidth / 2, y: 0, width: countWidth, height: self.bounds.height) + containerView.frame = .init(x: self.bounds.midX - countWidth / 2 * scaleFactor, y: 0, width: countWidth * scaleFactor, height: self.bounds.height) didBegin = true } // self.backgroundColor = .green.withAlphaComponent(0.2) @@ -451,7 +455,7 @@ class AnimatedCountLabel: UILabel { func animateIn(for newLayer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) { let beginTimeOffset: CFTimeInterval = 0// CFTimeInterval(DispatchTime.now().uptimeNanoseconds / 1000000000)// CACurrentMediaTime() - DispatchQueue.main.asyncAfter(deadline: .now() + beginTime) { + DispatchQueue.main.asyncAfter(deadline: .now() + beginTime) { [self] in let currentTime = CFTimeInterval(DispatchTime.now().uptimeNanoseconds / 1000000000) let beginTime: CFTimeInterval = 0 print("[DIFF-in] \(currentTime - beginTimeOffset)") @@ -479,7 +483,7 @@ class AnimatedCountLabel: UILabel { let animation = CAKeyframeAnimation() animation.keyPath = "position.y" - animation.values = [20, -6, 0] + animation.values = [20 * fontSize / 60, -6 * fontSize / 60, 0] animation.keyTimes = [0, 0.64, 1] animation.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut) animation.duration = duration / 0.64 diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index ea8e3de213..276bdf7365 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -967,6 +967,7 @@ public final class _MediaStreamComponent: CombinedComponent { let moreAnimationTag = GenericComponentViewTag() return { context in + var forceFullScreenInLandscape: Bool { false } let environment = context.environment[ViewControllerComponentContainer.Environment.self].value if environment.isVisible { } else { @@ -1000,16 +1001,17 @@ public final class _MediaStreamComponent: CombinedComponent { var isFullscreen = state.isFullscreen let isLandscape = context.availableSize.width > context.availableSize.height - if let videoSize = context.state.videoSize { +// if let videoSize = context.state.videoSize { // Always fullscreen in landscape - if /*videoSize.width > videoSize.height &&*/ isLandscape && !isFullscreen { + // TODO: support landscape sheet (wrap in scrollview, video size same as portrait) + if forceFullScreenInLandscape && /*videoSize.width > videoSize.height &&*/ isLandscape && !isFullscreen { state.isFullscreen = true isFullscreen = true - } else if videoSize.width > videoSize.height && !isLandscape && isFullscreen { + } else if let videoSize = context.state.videoSize, videoSize.width > videoSize.height && !isLandscape && isFullscreen { state.isFullscreen = false isFullscreen = false } - } +// } let videoHeight: CGFloat = context.availableSize.width / 16 * 9 let bottomPadding = 40 + environment.safeInsets.bottom diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 273f6e8ef1..c56bde81f0 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -9,15 +9,11 @@ import Display import ShimmerEffect import TelegramCore +import SwiftSignalKit + typealias MediaStreamVideoComponent = _MediaStreamVideoComponent class CustomIntensityVisualEffectView: UIVisualEffectView { - - /// Create visual effect view with given effect and its intensity - /// - /// - Parameters: - /// - effect: visual effect, eg UIBlurEffect(style: .dark) - /// - intensity: custom intensity from 0.0 (no effect) to 1.0 (full effect) using linear scale init(effect: UIVisualEffect, intensity: CGFloat) { super.init(effect: nil) animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in self.effect = effect } @@ -27,10 +23,27 @@ class CustomIntensityVisualEffectView: UIVisualEffectView { required init?(coder aDecoder: NSCoder) { fatalError() } - - // MARK: Private - private var animator: UIViewPropertyAnimator! - + + var animator: UIViewPropertyAnimator! + + private var displayLink: CADisplayLink? + + func setIntensity(_ intensity: CGFloat, animated: Bool) { + self.displayLink?.invalidate() + let displaylink = CADisplayLink( + target: self, + selector: #selector(displayLinkStep) + ) + self.displayLink = displaylink + displaylink.add( + to: .current, + forMode: RunLoop.Mode.default + ) + } + + @objc func displayLinkStep() { + + } } final class _MediaStreamVideoComponent: Component { @@ -135,7 +148,7 @@ final class _MediaStreamVideoComponent: Component { private var requestedExpansion: Bool = false - private var noSignalTimer: Timer? + private var noSignalTimer: Foundation.Timer? private var noSignalTimeout: Bool = false private weak var state: State? @@ -176,12 +189,27 @@ final class _MediaStreamVideoComponent: Component { let shimmerBorderLayer = CALayer() let placeholderView = UIImageView() - func update(component: _MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { - self.state = state -// placeholderView.alpha = 0.7 -// placeholderView.image = lastFrame[component.call.peerId.id.description] - - if component.videoLoading { + var videoStalled = false { + didSet { + if videoStalled != oldValue { + self.updateVideoStalled(isStalled: self.videoStalled) +// state?.updated() + } + } + } + private var frameInputDisposable: Disposable? + + private func updateVideoStalled(isStalled: Bool) { + if isStalled { + guard let component = self.component else { return } +// let effect = UIBlurEffect(style: .light) +// let intensity: CGFloat = 0.4 +// self.loadingBlurView.effect = nil +// self.loadingBlurView.animator.stopAnimation(true) +// self.loadingBlurView.animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned loadingBlurView] in loadingBlurView.effect = effect } +// self.loadingBlurView.animator.fractionComplete = intensity +// self.loadingBlurView.animator.fractionComplete = 0.4 +// self.loadingBlurView.effect = UIBlurEffect(style: .light) if let frame = lastFrame[component.call.peerId.id.description] { placeholderView.subviews.forEach { $0.removeFromSuperview() } placeholderView.addSubview(frame) @@ -220,8 +248,8 @@ final class _MediaStreamVideoComponent: Component { borderMask.strokeColor = UIColor.white.cgColor borderMask.lineWidth = 4 // let borderMask = CALayer() - shimmerBorderLayer.mask = borderMask borderShimmer = .init() + shimmerBorderLayer.mask = borderMask borderShimmer.layer = shimmerBorderLayer borderShimmer.testUpdate(background: .clear, foreground: .white) loadingBlurView.alpha = 1 @@ -229,6 +257,81 @@ final class _MediaStreamVideoComponent: Component { if hadVideo { self.loadingBlurView.removeFromSuperview() placeholderView.removeFromSuperview() + } else { + // Accounting for delay in first frame received + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [self] in + guard !self.videoStalled else { return } + UIView.transition(with: self, duration: 0.2, animations: { +// self.loadingBlurView.animator.fractionComplete = 0 +// self.loadingBlurView.effect = nil + self.loadingBlurView.alpha = 0 + }, completion: { _ in + self.loadingBlurView.removeFromSuperview() + }) + placeholderView.removeFromSuperview() + } + } + } + } + + var stallTimer: Foundation.Timer? + let fullScreenBackgroundPlaceholder = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + func update(component: _MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { + self.state = state +// placeholderView.alpha = 0.7 +// placeholderView.image = lastFrame[component.call.peerId.id.description] + self.component = component + + if component.videoLoading || self.videoStalled { + updateVideoStalled(isStalled: true) + /*if let frame = lastFrame[component.call.peerId.id.description] { + placeholderView.subviews.forEach { $0.removeFromSuperview() } + placeholderView.addSubview(frame) + frame.frame = placeholderView.bounds + // placeholderView.backgroundColor = .green + } else { + // placeholderView.subviews.forEach { $0.removeFromSuperview() } + // placeholderView.backgroundColor = .red + } + + if !hadVideo && placeholderView.superview == nil { + addSubview(placeholderView) + } + if loadingBlurView.superview == nil { + addSubview(loadingBlurView) + } + if shimmerOverlayLayer.superlayer == nil { + loadingBlurView.layer.addSublayer(shimmerOverlayLayer) + loadingBlurView.layer.addSublayer(shimmerBorderLayer) + } + loadingBlurView.clipsToBounds = true + shimmer = .init() + shimmer.layer = shimmerOverlayLayer + shimmerOverlayView.compositingFilter = "softLightBlendMode" + shimmer.testUpdate(background: .clear, foreground: .white.withAlphaComponent(0.4)) + loadingBlurView.layer.cornerRadius = 10 + shimmerOverlayLayer.opacity = 0.6 + + shimmerBorderLayer.cornerRadius = 10 + shimmerBorderLayer.masksToBounds = true + shimmerBorderLayer.compositingFilter = "softLightBlendMode" + + let borderMask = CAShapeLayer() + borderMask.path = CGPath(roundedRect: .init(x: 0, y: 0, width: loadingBlurView.bounds.width, height: loadingBlurView.bounds.height), cornerWidth: 10, cornerHeight: 10, transform: nil) + borderMask.fillColor = UIColor.clear.cgColor + borderMask.strokeColor = UIColor.white.cgColor + borderMask.lineWidth = 4 +// let borderMask = CALayer() + borderShimmer = .init() + shimmerBorderLayer.mask = borderMask + borderShimmer.layer = shimmerBorderLayer + borderShimmer.testUpdate(background: .clear, foreground: .white) + loadingBlurView.alpha = 1*/ + } else { + updateVideoStalled(isStalled: false) + /*if hadVideo { + self.loadingBlurView.removeFromSuperview() + placeholderView.removeFromSuperview() } else { // Accounting for delay in first frame received DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [self] in @@ -239,11 +342,39 @@ final class _MediaStreamVideoComponent: Component { }) placeholderView.removeFromSuperview() } - } + }*/ } if component.hasVideo, self.videoView == nil { if let input = component.call.video(endpointId: "unified") { + var _stallTimer: Foundation.Timer { Foundation.Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in + guard let strongSelf = self else { return timer.invalidate() } + print("Timer emitting \(timer)") + DispatchQueue.main.async { + strongSelf.videoStalled = true + } + } + } + // TODO: use mapToThrottled (?) + frameInputDisposable = input.start(next: { [weak self] input in + guard let strongSelf = self else { return } + print("input") + // TODO: optimize with throttle + DispatchQueue.main.async { + strongSelf.stallTimer?.invalidate() + strongSelf.stallTimer = _stallTimer +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { +// print(strongSelf.videoStalled) +// if strongSelf.videoStalled { +// strongSelf.stallTimer?.fire() +// } + RunLoop.main.add(strongSelf.stallTimer!, forMode: .common) + strongSelf.videoStalled = false + } + }) + stallTimer = _stallTimer +// RunLoop.main.add(stallTimer!, forMode: .common) + if let videoBlurView = self.videoRenderingContext.makeView(input: input, blur: true) { self.videoBlurView = videoBlurView self.insertSubview(videoBlurView, belowSubview: self.blurTintView) @@ -354,6 +485,12 @@ final class _MediaStreamVideoComponent: Component { } } } + fullScreenBackgroundPlaceholder.removeFromSuperview() + } else if component.isFullscreen { + if fullScreenBackgroundPlaceholder.superview == nil { + insertSubview(fullScreenBackgroundPlaceholder, at: 0) + } + fullScreenBackgroundPlaceholder.frame = self.bounds } // sheetView.frame = .init(x: 0, y: sheetTop, width: availableSize.width, height: sheetHeight)