From 197128c3feace3936e21a1f0acfdc801b6acfb30 Mon Sep 17 00:00:00 2001 From: Ilya Yelagov Date: Thu, 8 Dec 2022 09:41:39 +0400 Subject: [PATCH] Refactoring --- .../ShimmerEffect/Sources/ShimmerEffect.swift | 6 +- .../Components/AnimatedCounterView.swift | 168 ++---------- .../Components/MediaStreamComponent.swift | 123 ++------- .../MediaStreamVideoComponent.swift | 239 +++++++----------- 4 files changed, 135 insertions(+), 401 deletions(-) diff --git a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift index 65c86728dd..1a35997e17 100644 --- a/submodules/ShimmerEffect/Sources/ShimmerEffect.swift +++ b/submodules/ShimmerEffect/Sources/ShimmerEffect.swift @@ -477,7 +477,7 @@ public final class StandaloneShimmerEffect { self.updateLayer() } - public func testUpdate(background: UIColor, foreground: UIColor) { + public func updateHorizontal(background: UIColor, foreground: UIColor) { if self.background == background && self.foreground == foreground { return } @@ -503,7 +503,7 @@ public final class StandaloneShimmerEffect { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.3), options: CGGradientDrawingOptions()) }) - self.testUpdateLayer() + self.updateHorizontalLayer() } public func updateLayer() { @@ -525,7 +525,7 @@ public final class StandaloneShimmerEffect { } } - private func testUpdateLayer() { + private func updateHorizontalLayer() { guard let layer = self.layer, let image = self.image else { return } diff --git a/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift b/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift index 21484ebcc9..969baac579 100644 --- a/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift +++ b/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift @@ -10,7 +10,6 @@ private let latePink = UIColor(rgb: 0xf0436c) public final class AnimatedCountView: UIView { let countLabel = AnimatedCountLabel() -// let titleLabel = UILabel() let subtitleLabel = UILabel() private let foregroundView = UIView() @@ -31,7 +30,6 @@ public final class AnimatedCountView: UIView { self.foregroundView.layer.addSublayer(self.foregroundGradientLayer) self.addSubview(self.foregroundView) -// self.addSubview(self.titleLabel) self.addSubview(self.subtitleLabel) self.maskingView.addSubview(countLabel) @@ -40,7 +38,6 @@ public final class AnimatedCountView: UIView { self.clipsToBounds = false subtitleLabel.textColor = .white -// self.backgroundColor = UIColor.white.withAlphaComponent(0.1) } override public func layoutSubviews() { @@ -56,34 +53,12 @@ public final class AnimatedCountView: UIView { func update(countString: String, subtitle: String) { self.setupGradientAnimations() - let text: String = countString// presentationStringsFormattedNumber(Int32(count), ",") - - // self.titleNode.attributedText = NSAttributedString(string: "", font: Font.with(size: 23.0, design: .round, weight: .semibold, traits: []), textColor: .white) - // let titleSize = self.titleNode.updateLayout(size) - // self.titleNode.frame = CGRect(x: floor((size.width - titleSize.width) / 2.0), y: 48.0, width: titleSize.width, height: titleSize.height) -// if CGFloat(text.count * 40) < bounds.width - 32 { -// 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)]) -// + let text: String = countString 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 { -// self.timerNode.attributedText = NSAttributedString(string: text, font: Font.with(size: 60.0, design: .round, weight: .semibold, traits: [.monospacedNumbers]), textColor: .white) -// timerSize = self.timerNode.updateLayout(CGSize(width: size.width + 100.0, height: size.height)) -// } - -// self.timerNode.frame = CGRect(x: floor((size.width - timerSize.width) / 2.0), y: 78.0, width: timerSize.width, height: timerSize.height) self.subtitleLabel.attributedText = NSAttributedString(string: subtitle, attributes: [.font: UIFont.systemFont(ofSize: 16, weight: .semibold)]) self.subtitleLabel.isHidden = subtitle.isEmpty -// let subtitleSize = self.subtitleNode.updateLayout(size) -// self.subtitleNode.frame = CGRect(x: floor((size.width - subtitleSize.width) / 2.0), y: 164.0, width: subtitleSize.width, height: subtitleSize.height) - -// self.foregroundView.frame = CGRect(origin: CGPoint(), size: size) - // self.setNeedsLayout() } required init?(coder: NSCoder) { @@ -105,9 +80,7 @@ public final class AnimatedCountView: UIView { animation.toValue = newValue CATransaction.setCompletionBlock { [weak self] in -// if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { - self?.setupGradientAnimations() -// } + self?.setupGradientAnimations() } self.foregroundGradientLayer.add(animation, forKey: "movement") CATransaction.commit() @@ -126,7 +99,7 @@ class AnimatedCharLayer: CATextLayer { } var attributedText: NSAttributedString? { get { - self.string as? NSAttributedString //?? (self.string as? String).map { NSAttributed.init + self.string as? NSAttributedString } set { self.string = newValue @@ -214,37 +187,17 @@ class AnimatedCountLabel: UILabel { } return offset } else { - var offset = self.chars[0.. index && self.chars[index].attributedText?.string == "," { - if index > 0, let prevChar = self.chars[index - 1].attributedText?.string, ["1", "7"].contains(prevChar) { - offset -= commaWidthForSpacing * 0.7 - } else { - offset -= commaWidthForSpacing / 3 - } - } - return offset + return offsetForChar(at: index, within: self.chars.compactMap(\.attributedText)) } } override func layoutSubviews() { super.layoutSubviews() - let countWidth = offsetForChar(at: chars.count) /*chars.reduce(0) { - if $1.attributedText?.string == "," { - return $0 + commaWidth + interItemSpacing - } - return $0 + itemWidth + interItemSpacing - }*/ - interItemSpacing + let countWidth = offsetForChar(at: chars.count) - interItemSpacing 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 char.frame.origin.x = offset -// char.frame.origin.x = CGFloat(chars.count - 1 - index) * (40 + interItemSpacing) char.frame.origin.y = 0 } } @@ -274,10 +227,7 @@ class AnimatedCountLabel: UILabel { } } - let initialDuration: TimeInterval = min(0.25, maxAnimationDuration / Double(numberOfChanges)) /// 0.25 - -// let currentWidth = itemWidth * CGFloat(currentChars.count) -// let newWidth = itemWidth * CGFloat(newChars.count) + let initialDuration: TimeInterval = min(0.25, maxAnimationDuration / Double(numberOfChanges)) let interItemDelay: TimeInterval = 0.08 var changeIndex = 0 @@ -288,11 +238,7 @@ class AnimatedCountLabel: UILabel { let newCharIndex = newChars.count - 1 - index let currCharIndex = currentChars.count - 1 - index - if true || newChars[newCharIndex] != currentChars[currCharIndex] { -// if newChars[newCharIndex].string != "," { -// continue -// } - + if newChars[newCharIndex] != currentChars[currCharIndex] { let initialDuration = newChars[newCharIndex] != currentChars[currCharIndex] ? initialDuration : 0 if !isInitialSet && newChars[newCharIndex] != currentChars[currCharIndex] { @@ -302,19 +248,14 @@ class AnimatedCountLabel: UILabel { } let newLayer = AnimatedCharLayer() newLayer.attributedText = newChars[newCharIndex] - let offset = offsetForChar(at: newCharIndex, within: newChars)/* newChars[0.. self.bounds.width { let scale = (self.bounds.width - 32) / (countWidth * scaleFactor) @@ -380,71 +309,31 @@ class AnimatedCountLabel: UILabel { } else { 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 * scaleFactor, y: 0, width: countWidth * scaleFactor, height: self.bounds.height) didBegin = true } -// self.backgroundColor = .green.withAlphaComponent(0.2) self.clipsToBounds = false } func animateOut(for layer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) { -// let animation = CAKeyframeAnimation() -// animation.keyPath = "opacity" -// animation.values = [layer.presentation()?.value(forKey: "opacity") ?? 1, 0.0] -// animation.keyTimes = [0, 1] -// animation.duration = duration -// animation.beginTime = CACurrentMediaTime() + beginTime -//// animation.isAdditive = true -// animation.isRemovedOnCompletion = false -// animation.fillMode = .backwards -// layer.opacity = 0 -// layer.add(animation, forKey: "opacity") -// -// let beginTimeOffset: CFTimeInterval = 0/*beginTime == .zero ? 0 :*/ // CFTimeInterval(DispatchTime.now().uptimeNanoseconds / 1000000000) /*layer.convertTime(*/// CACurrentMediaTime()//, to: nil) DispatchQueue.main.asyncAfter(deadline: .now() + beginTime) { - let currentTime = CFTimeInterval(DispatchTime.now().uptimeNanoseconds / 1000000000) let beginTime: CFTimeInterval = 0 - print("[DIFF-out] \(currentTime - beginTimeOffset)") + let opacityInAnimation = CABasicAnimation(keyPath: "opacity") opacityInAnimation.fromValue = 1 opacityInAnimation.toValue = 0 opacityInAnimation.fillMode = .forwards opacityInAnimation.isRemovedOnCompletion = false - // opacityInAnimation.duration = duration - // opacityInAnimation.beginTime = beginTimeOffset + beginTime - // opacityInAnimation.completion = { _ in - // layer.removeFromSuperlayer() - // } - // layer.add(opacityInAnimation, forKey: "opacity") - - // let timer = Timer.scheduledTimer(withTimeInterval: duration + beginTime, repeats: false) { timer in - // DispatchQueue.main.asyncAfter(deadline: .now() + duration + beginTime) { - // DispatchQueue.main.async { - // layer.backgroundColor = UIColor.red.withAlphaComponent(0.3).cgColor - // } - // DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - // layer.removeFromSuperlayer() - // } - // timer.invalidate() - // } - // RunLoop.current.add(timer, forMode: .common) let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale") - scaleOutAnimation.fromValue = 1 // layer.presentation()?.value(forKey: "transform.scale") ?? 1 + scaleOutAnimation.fromValue = 1 scaleOutAnimation.toValue = 0.0 - // scaleOutAnimation.duration = duration - // scaleOutAnimation.beginTime = beginTimeOffset + beginTime - // layer.add(scaleOutAnimation, forKey: "scaleout") let translate = CABasicAnimation(keyPath: "transform.translation") translate.fromValue = CGPoint.zero - translate.toValue = CGPoint(x: 0, y: -layer.bounds.height * 0.3)// -layer.bounds.height + 3.0) - // translate.duration = duration - // translate.beginTime = beginTimeOffset + beginTime - // layer.add(translate, forKey: "translate") + translate.toValue = CGPoint(x: 0, y: -layer.bounds.height * 0.3) let group = CAAnimationGroup() group.animations = [opacityInAnimation, scaleOutAnimation, translate] @@ -455,38 +344,31 @@ class AnimatedCountLabel: UILabel { group.completion = { _ in layer.removeFromSuperlayer() } - // layer.opacity = 0 layer.add(group, forKey: "out") } } func animateIn(for newLayer: CALayer, duration: CFTimeInterval, beginTime: CFTimeInterval) { - let beginTimeOffset: CFTimeInterval = 0// CFTimeInterval(DispatchTime.now().uptimeNanoseconds / 1000000000)// CACurrentMediaTime() + let beginTimeOffset: CFTimeInterval = 0 // CACurrentMediaTime() DispatchQueue.main.asyncAfter(deadline: .now() + beginTime) { [self] in - let currentTime = CFTimeInterval(DispatchTime.now().uptimeNanoseconds / 1000000000) let beginTime: CFTimeInterval = 0 - print("[DIFF-in] \(currentTime - beginTimeOffset)") newLayer.opacity = 0 - // newLayer.backgroundColor = UIColor.red.cgColor let opacityInAnimation = CABasicAnimation(keyPath: "opacity") opacityInAnimation.fromValue = 0 opacityInAnimation.toValue = 1 opacityInAnimation.duration = duration opacityInAnimation.beginTime = beginTimeOffset + beginTime - // opacityInAnimation.isAdditive = true opacityInAnimation.fillMode = .backwards newLayer.opacity = 1 newLayer.add(opacityInAnimation, forKey: "opacity") - // newLayer.opacity = 1 let scaleOutAnimation = CABasicAnimation(keyPath: "transform.scale") scaleOutAnimation.fromValue = 0 scaleOutAnimation.toValue = 1 scaleOutAnimation.duration = duration scaleOutAnimation.beginTime = beginTimeOffset + beginTime - // scaleOutAnimation.isAdditive = true newLayer.add(scaleOutAnimation, forKey: "scalein") let animation = CAKeyframeAnimation() diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index b9cf8f6db2..0347c1f1d9 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -48,8 +48,6 @@ public final class MediaStreamComponent: CombinedComponent { private(set) var hasVideo: Bool = false private var stateDisposable: Disposable? private var infoDisposable: Disposable? - private var connectionDisposable: Disposable? - private var networkStateDisposable: Disposable? private(set) var originInfo: OriginInfo? @@ -113,36 +111,6 @@ public final class MediaStreamComponent: CombinedComponent { strongSelf.updated(transition: .immediate) }) - // TODO: retest to uncomment or delete. Relying only on video frames - /*self.networkStateDisposable = (call.account.networkState |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { return } - switch state { - case .waitingForNetwork, .connecting: - print("[NEW] videoStalled") - strongSelf.videoStalled = true - default: - strongSelf.videoStalled = !strongSelf.hasVideo - } - strongSelf.updated(transition: .immediate) -// if let strongSelf = self, case .standard(previewing: false) = strongSelf.presentationInterfaceState.mode { -// strongSelf.chatTitleView?.networkState = state -// } - }) - - self.connectionDisposable = call.state.start(next: { [weak self] state in - let prev = self?.videoStalled - switch state.networkState { - case .connected: - self?.videoStalled = false - default: - print("[ALERT] video stalled") - self?.videoStalled = true - } - if prev != self?.videoStalled { - self?.updated(transition: .immediate) - } - })*/ - let callPeer = call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) self.infoDisposable = (combineLatest(queue: .mainQueue(), call.state, call.members, callPeer) @@ -153,8 +121,8 @@ public final class MediaStreamComponent: CombinedComponent { var updated = false // TODO: remove debug timer -// Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in - strongSelf.infoThrottler.publish(members.totalCount/*Int.random(in: 0..<10000000)*/) { [weak strongSelf] latestCount in + Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in + strongSelf.infoThrottler.publish(/*members.totalCount*/Int.random(in: 0..<1000000000)) { [weak strongSelf] latestCount in print(members.totalCount) guard let strongSelf = strongSelf else { return } var updated = false @@ -167,7 +135,7 @@ public final class MediaStreamComponent: CombinedComponent { strongSelf.updated(transition: .immediate) } } -// }.fire() + }.fire() if state.canManageCall != strongSelf.canManageCall { strongSelf.canManageCall = state.canManageCall updated = true @@ -188,12 +156,6 @@ public final class MediaStreamComponent: CombinedComponent { updated = true } -// let originInfo = OriginInfo(title: callPeer.debugDisplayTitle, memberCount: members.totalCount) -// if strongSelf.originInfo != originInfo { -// strongSelf.originInfo = originInfo -// updated = true -// } -// if updated { strongSelf.updated(transition: .immediate) } @@ -225,8 +187,6 @@ public final class MediaStreamComponent: CombinedComponent { self.stateDisposable?.dispose() self.infoDisposable?.dispose() self.isVisibleInHierarchyDisposable?.dispose() - self.connectionDisposable?.dispose() - self.networkStateDisposable?.dispose() } func toggleDisplayUI() { @@ -272,9 +232,6 @@ public final class MediaStreamComponent: CombinedComponent { let background = Child(Rectangle.self) let dismissTapComponent = Child(Rectangle.self) let video = Child(MediaStreamVideoComponent.self) -// let navigationBar = Child(NavigationBarComponent.self) -// let toolbar = Child(ToolbarComponent.self) - let sheet = Child(StreamSheetComponent.self) let fullscreenOverlay = Child(StreamSheetComponent.self) @@ -312,11 +269,10 @@ public final class MediaStreamComponent: CombinedComponent { state.updated(transition: .easeInOut(duration: 3)) deactivatePictureInPicture.invoke(Void()) } - let isFullscreen: Bool // = state.isFullscreen + let isFullscreen: Bool let isLandscape = context.availableSize.width > context.availableSize.height -// if let videoSize = context.state.videoSize { - // Always fullscreen in landscape + // Always fullscreen in landscape // TODO: support landscape sheet (wrap in scrollview, video size same as portrait) if forceFullScreenInLandscape && isLandscape && !state.isFullscreen { state.isFullscreen = true @@ -327,7 +283,7 @@ public final class MediaStreamComponent: CombinedComponent { } else { isFullscreen = state.isFullscreen } - // } + let videoInset: CGFloat if !isFullscreen { videoInset = 16 @@ -346,7 +302,7 @@ public final class MediaStreamComponent: CombinedComponent { var dragOffset = context.state.dismissOffset if isFullyDragged { - dragOffset = max(context.state.dismissOffset, sheetHeight - context.availableSize.height + context.view.safeAreaInsets.top)// sheetHeight - UIScreen.main.bounds.height + dragOffset = max(context.state.dismissOffset, sheetHeight - context.availableSize.height + context.view.safeAreaInsets.top) } let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - sheetHeight + dragOffset) @@ -356,7 +312,6 @@ public final class MediaStreamComponent: CombinedComponent { transition: context.transition ) - let video = video.update( component: MediaStreamVideoComponent( call: context.component.call, @@ -425,8 +380,7 @@ public final class MediaStreamComponent: CombinedComponent { var topLeftButton: AnyComponent? if context.state.canManageCall { let whiteColor = UIColor(white: 1.0, alpha: 1.0) - /*navigationRightItems.append(*/ topLeftButton = //AnyComponentWithIdentity(id: "more", component: - AnyComponent(Button( + topLeftButton = AnyComponent(Button( content: AnyComponent(ZStack([ AnyComponentWithIdentity(id: "b", component: AnyComponent(Circle( fillColor: .white.withAlphaComponent(0.08), @@ -562,7 +516,6 @@ public final class MediaStreamComponent: CombinedComponent { return } - let presentationData = call.accountContext.sharedContext.currentPresentationData.with { $0 } if let title = title { @@ -659,22 +612,11 @@ public final class MediaStreamComponent: CombinedComponent { let navigationComponent = NavigationBarComponent( topInset: environment.statusBarHeight, sideInset: environment.safeInsets.left, - leftItem: topLeftButton/*AnyComponent(Button( - content: AnyComponent(Text(text: environment.strings.Common_Close, font: Font.regular(17.0), color: .white)), - action: { [weak call] in - let _ = call?.leave(terminateIfPossible: false) - }) - )*/, + leftItem: topLeftButton, rightItems: navigationRightItems, centerItem: AnyComponent(StreamTitleComponent(text: state.peerTitle, isRecording: state.recordingStartTimestamp != nil, isActive: context.state.videoIsPlayable)) ) -// let navigationBar = navigationBar.update( -// component: navigationComponent, -// availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height), -// transition: context.transition -// ) - if context.state.storedIsFullscreen != isFullscreen { context.state.storedIsFullscreen = isFullscreen if isFullscreen { @@ -751,8 +693,6 @@ public final class MediaStreamComponent: CombinedComponent { onPanGesture(panState) }) ) -// var bottomComponent: AnyComponent? -// var fullScreenToolbarComponent: AnyComponent? context.add(dismissTapComponent .position(CGPoint(x: context.availableSize.width / 2, y: dismissTapAreaHeight / 2)) @@ -821,7 +761,6 @@ public final class MediaStreamComponent: CombinedComponent { context.setLineWidth(2.4 * imageRenderScale - UIScreenPixel) context.setLineCap(.round) context.setStrokeColor(imageColor.cgColor) -// context.setLineJoin(.round) let lineSide = size.width / 5 let centerOffset = size.width / 20 @@ -860,9 +799,8 @@ public final class MediaStreamComponent: CombinedComponent { controller.updateOrientation(orientation: .portrait) } if !canEnforceOrientation { - state.updated() // updated(.easeInOut(duration: 0.3)) + state.updated() } - // controller.updateOrientation(orientation: isLandscape ? .portrait : .landscapeRight) } } ).minSize(CGSize(width: 44.0, height: 44.0))) @@ -877,7 +815,6 @@ public final class MediaStreamComponent: CombinedComponent { backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), bottomPadding: bottomPadding, participantsCount: context.state.originInfo?.memberCount ?? 0, // Int.random(in: 0...999998)// [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()! - // isFullyExtended: isFullyDragged, deviceCornerRadius: (controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 0, videoHeight: videoHeight @@ -888,7 +825,7 @@ public final class MediaStreamComponent: CombinedComponent { let sheetOffset: CGFloat = context.availableSize.height - sheetHeight + dragOffset let sheetPosition = sheetOffset + sheetHeight / 2 - // Sheet underneath the video when in sheet + // Sheet underneath the video when in modal sheet context.add(sheet .position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2)) ) @@ -900,7 +837,7 @@ public final class MediaStreamComponent: CombinedComponent { videoPos = sheetPosition - sheetHeight / 2 + videoHeight / 2 + 50 + 12 } context.add(video - .position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos)/*sheetPosition + videoHeight / 2 + 50 - context.availableSize.height / 2*/)// context.availableSize.height / 2.0 + context.state.dismissOffset)) + .position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos)) ) } else { context.add(video @@ -950,7 +887,7 @@ public final class MediaStreamComponent: CombinedComponent { sheetHeight: max(sheetHeight - context.state.dismissOffset, sheetHeight), backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor), bottomPadding: 12, - participantsCount: -1, // context.state.originInfo?.memberCount ?? 0 + participantsCount: -1, isFullyExtended: isFullyDragged, deviceCornerRadius: (controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 0, videoHeight: videoHeight @@ -964,24 +901,11 @@ public final class MediaStreamComponent: CombinedComponent { ) } -// context.add(navigationBar -// .position(CGPoint(x: context.availableSize.width / 2.0, y: navigationBar.size.height / 2.0)) -// .opacity(context.state.displayUI ? 1.0 : 0.0) -// ) - -// context.add(toolbar -// .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height - toolbar.size.height / 2.0)) -// .opacity(context.state.displayUI ? 1.0 : 0.0) -// ) - return context.availableSize } } } -// TODO: pass to component properly -//internal var deviceCornerRadius: CGFloat? = nil - public final class MediaStreamComponentController: ViewControllerComponentContainer, VoiceChatController { private let context: AccountContext public let call: PresentationGroupCall @@ -1048,11 +972,6 @@ public final class MediaStreamComponentController: ViewControllerComponentContai DispatchQueue.main.async { self.onViewDidDisappear?() } - -// if let initialOrientation = self.initialOrientation { -// self.initialOrientation = nil -// self.call.accountContext.sharedContext.applicationBindings.forceOrientation(initialOrientation) -// } } override public func viewDidLoad() { @@ -1088,17 +1007,8 @@ public final class MediaStreamComponentController: ViewControllerComponentContai strongSelf.dismissImpl(completion: completion) }) self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0, duration: 0.3, removeOnCompletion: false) - // if let validLayout = self.validLayout { - // self.view.clipsToBounds = true - // self.view.layer.cornerRadius = validLayout.deviceMetrics.screenCornerRadius - // if #available(iOS 13.0, *) { - // self.view.layer.cornerCurve = .continuous - // } - - self.view.layer.animatePosition(from: self.view.center, to: CGPoint(x: self.view.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), duration: 0.4, /*timingFunction: kCAMediaTimingFunctionSpring, */completion: { _ in + self.view.layer.animatePosition(from: self.view.center, to: CGPoint(x: self.view.center.x, y: self.view.bounds.maxY + self.view.bounds.height / 2), duration: 0.4, completion: { _ in }) - // self.view.layer.animateScale(from: 1.0, to: 0.001, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - // } } private func dismissImpl(completion: (() -> Void)? = nil) { @@ -1505,21 +1415,18 @@ private final class NavigationBarComponent: CombinedComponent { centerLeftInset += leftItem.size.width + 4.0 } -// var centerRightInset = sideInset var rightItemX = context.availableSize.width - sideInset for item in rightItemList.reversed() { context.add(item .position(CGPoint(x: rightItemX - item.size.width / 2.0, y: context.component.topInset + contentHeight / 2.0)) ) rightItemX -= item.size.width + 8.0 -// centerRightInset += item.size.width + 8.0 } -// let maxCenterInset = max(centerLeftInset, centerRightInset) let someUndesiredOffset: CGFloat = 16 if let centerItem = centerItem { context.add(centerItem - .position(CGPoint(x: context.availableSize.width / 2 - someUndesiredOffset /*maxCenterInset + (context.availableSize.width - maxCenterInset - maxCenterInset) / 2.0*/, y: context.component.topInset + contentHeight / 2.0)) + .position(CGPoint(x: context.availableSize.width / 2 - someUndesiredOffset, y: context.component.topInset + contentHeight / 2.0)) ) } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 1b51a10400..f5bb89be04 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -12,23 +12,6 @@ import SwiftSignalKit import AvatarNode import Postbox -class CustomIntensityVisualEffectView: UIVisualEffectView { - init(effect: UIVisualEffect, intensity: CGFloat) { - super.init(effect: nil) - animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in self.effect = effect } - animator.startAnimation() - animator.pauseAnimation() - animator.fractionComplete = intensity - animator.pausesOnCompletion = true - } - - required init?(coder aDecoder: NSCoder) { - fatalError() - } - - var animator: UIViewPropertyAnimator! -} - final class MediaStreamVideoComponent: Component { let call: PresentationGroupCallImpl let hasVideo: Bool @@ -137,6 +120,34 @@ final class MediaStreamVideoComponent: Component { private var noSignalTimer: Foundation.Timer? private var noSignalTimeout: Bool = false + private let maskGradientLayer = CAGradientLayer() + private var wasVisible = true + private var borderShimmer = StandaloneShimmerEffect() + private let shimmerBorderLayer = CALayer() + private let placeholderView = UIImageView() + + private var videoStalled = false { + didSet { + if videoStalled != oldValue { + self.updateVideoStalled(isStalled: self.videoStalled) +// state?.updated() + } + } + } + var onVideoPlaybackChange: ((Bool) -> Void) = { _ in } + + private var frameInputDisposable: Disposable? + + private var stallTimer: Foundation.Timer? + private let fullScreenBackgroundPlaceholder = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + + private var avatarDisposable: Disposable? + private var didBeginLoadingAvatar = false + private var timeLastFrameReceived: CFAbsoluteTime? + + private var isFullscreen: Bool = false + private let videoLoadingThrottler = Throttler(duration: 1, queue: .main) + private weak var state: State? override init(frame: CGRect) { @@ -154,6 +165,11 @@ final class MediaStreamVideoComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + avatarDisposable?.dispose() + frameInputDisposable?.dispose() + } + public func matches(tag: Any) -> Bool { if let _ = tag as? Tag { return true @@ -167,23 +183,6 @@ final class MediaStreamVideoComponent: Component { self.pictureInPictureController?.stopPictureInPicture() } } - let maskGradientLayer = CAGradientLayer() - private var wasVisible = true - var borderShimmer = StandaloneShimmerEffect() - let shimmerBorderLayer = CALayer() - let placeholderView = UIImageView() - - var videoStalled = false { - didSet { - if videoStalled != oldValue { - self.updateVideoStalled(isStalled: self.videoStalled) -// state?.updated() - } - } - } - var onVideoPlaybackChange: ((Bool) -> Void) = { _ in } - - private var frameInputDisposable: Disposable? private func updateVideoStalled(isStalled: Bool) { if isStalled { @@ -215,7 +214,6 @@ final class MediaStreamVideoComponent: Component { } } if shimmerBorderLayer.superlayer == nil { -// loadingBlurView.contentView.layer.addSublayer(shimmerOverlayLayer) loadingBlurView.contentView.layer.addSublayer(shimmerBorderLayer) } loadingBlurView.clipsToBounds = true @@ -234,7 +232,7 @@ final class MediaStreamVideoComponent: Component { borderShimmer = .init() borderShimmer.layer = shimmerBorderLayer - borderShimmer.testUpdate(background: .clear, foreground: .white) + borderShimmer.updateHorizontal(background: .clear, foreground: .white) loadingBlurView.alpha = 1 } else { if hadVideo { @@ -251,60 +249,12 @@ final class MediaStreamVideoComponent: Component { self?.loadingBlurView.layer.removeAllAnimations() } loadingBlurView.layer.add(anim, forKey: "opacity") - } else { - // Wait for state to update with first frame - // Accounting for delay in first frame received - /*DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in - guard self?.videoStalled == false else { return } - - // TODO: animate blur intesity with UIPropertyAnimator - self?.loadingBlurView.layer.removeAllAnimations() - let anim = CABasicAnimation(keyPath: "opacity") - anim.duration = 0.5 - anim.fromValue = 1 - anim.toValue = 0 - anim.fillMode = .forwards - anim.isRemovedOnCompletion = false - anim.completion = { [weak self] _ in - guard self?.videoStalled == false else { return } -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in - self?.loadingBlurView.removeFromSuperview() - self?.placeholderView.removeFromSuperview() - } - self?.loadingBlurView.layer.add(anim, forKey: "opacity") -// 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 = .init(effect: UIBlurEffect(style: .light), intensity: 0.4) -// }) - }*/ } -// loadingBlurView.backgroundColor = .yellow.withAlphaComponent(0.4) } } - var stallTimer: Foundation.Timer? - let fullScreenBackgroundPlaceholder = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) - - var avatarDisposable: Disposable? - var didBeginLoadingAvatar = false -// let avatarPlaceholderView = UIImageView() - var timeLastFrameReceived: CFAbsoluteTime? - - var isFullscreen: Bool = false - let videoLoadingThrottler = Throttler(duration: 1, queue: .main) - - deinit { - avatarDisposable?.dispose() - frameInputDisposable?.dispose() - } - 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 self.onVideoPlaybackChange = component.onVideoPlaybackLiveChange self.isFullscreen = component.isFullscreen @@ -331,19 +281,16 @@ final class MediaStreamVideoComponent: Component { 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)") let currentTime = CFAbsoluteTimeGetCurrent() if let lastFrameTime = strongSelf.timeLastFrameReceived, currentTime - lastFrameTime > 0.5 { -// DispatchQueue.main.async { strongSelf.videoLoadingThrottler.publish(true, includingLatest: true) { isStalled in strongSelf.videoStalled = isStalled strongSelf.onVideoPlaybackChange(!isStalled) } - - // } } } } + // TODO: use mapToThrottled (?) frameInputDisposable = input.start(next: { [weak self] input in guard let strongSelf = self else { return } @@ -355,7 +302,6 @@ final class MediaStreamVideoComponent: Component { } }) stallTimer = _stallTimer - // RunLoop.main.add(stallTimer!, forMode: .common) if let videoBlurView = self.videoRenderingContext.makeView(input: input, blur: true) { self.videoBlurView = videoBlurView @@ -373,75 +319,68 @@ final class MediaStreamVideoComponent: Component { if let videoView = self.videoRenderingContext.makeView(input: input, blur: false, forceSampleBufferDisplayLayer: true) { self.videoView = videoView - self/*.insertSubview(videoView, belowSubview: loadingBlurView)*/.addSubview(videoView) + self.addSubview(videoView) videoView.alpha = 0 UIView.animate(withDuration: 0.3) { videoView.alpha = 1 } if let sampleBufferVideoView = videoView as? SampleBufferVideoRenderingView { sampleBufferVideoView.sampleBufferLayer.masksToBounds = true -// sampleBufferVideoView.sampleBufferLayer.cornerRadius = 10 if #available(iOS 13.0, *) { sampleBufferVideoView.sampleBufferLayer.preventsDisplaySleepDuringVideoPlayback = true } -// if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() { - final class PlaybackDelegateImpl: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { - var onTransitionFinished: (() -> Void)? - func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) { - - } - - func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange { - return CMTimeRange(start: .zero, duration: .positiveInfinity) - } - - func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool { - return false - } - - func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) { - onTransitionFinished?() - print("pip finished") - } - - func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { - completionHandler() - } - - public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool { - return false - } + // if #available(iOSApplicationExtension 15.0, iOS 15.0, *), AVPictureInPictureController.isPictureInPictureSupported() { + final class PlaybackDelegateImpl: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate { + var onTransitionFinished: (() -> Void)? + func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) { + } + + func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange { + return CMTimeRange(start: .zero, duration: .positiveInfinity) + } + + func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool { + return false + } + + func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) { + onTransitionFinished?() + } + + func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { + completionHandler() + } + + public func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ pictureInPictureController: AVPictureInPictureController) -> Bool { + return false + } + } var pictureInPictureController: AVPictureInPictureController? = nil if #available(iOS 15.0, *) { pictureInPictureController = AVPictureInPictureController(contentSource: AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: sampleBufferVideoView.sampleBufferLayer, playbackDelegate: { let delegate = PlaybackDelegateImpl() - delegate.onTransitionFinished = { [weak self] in - if self?.videoView?.alpha == 0 { -// self?.videoView?.alpha = 1 - } + delegate.onTransitionFinished = { } return delegate }())) pictureInPictureController?.playerLayer.masksToBounds = false pictureInPictureController?.playerLayer.cornerRadius = 10 } else if AVPictureInPictureController.isPictureInPictureSupported() { - // TODO: support PiP for iOS < 15.0 - // sampleBufferVideoView.sampleBufferLayer pictureInPictureController = AVPictureInPictureController.init(playerLayer: AVPlayerLayer(player: AVPlayer())) } - - pictureInPictureController?.delegate = self + + pictureInPictureController?.delegate = self if #available(iOS 14.2, *) { pictureInPictureController?.canStartPictureInPictureAutomaticallyFromInline = true } if #available(iOS 14.0, *) { pictureInPictureController?.requiresLinearPlayback = true } - - self.pictureInPictureController = pictureInPictureController -// } + + self.pictureInPictureController = pictureInPictureController + // } } videoView.setOnOrientationUpdated { [weak state] _, _ in @@ -464,7 +403,6 @@ final class MediaStreamVideoComponent: Component { } } } -// fullScreenBackgroundPlaceholder.removeFromSuperview() } else if component.isFullscreen { if fullScreenBackgroundPlaceholder.superview == nil { insertSubview(fullScreenBackgroundPlaceholder, at: 0) @@ -475,12 +413,6 @@ final class MediaStreamVideoComponent: Component { } fullScreenBackgroundPlaceholder.frame = .init(origin: .zero, size: availableSize) -// sheetView.frame = .init(x: 0, y: sheetTop, width: availableSize.width, height: sheetHeight) - // var aspect = videoView.getAspect() -// if aspect <= 0.01 { - // let aspect = !component.isFullscreen ? 16.0 / 9.0 : // 3.0 / 4.0 -// } - let videoInset: CGFloat if !component.isFullscreen { videoInset = 16 @@ -498,9 +430,8 @@ final class MediaStreamVideoComponent: Component { let snapshot = videoView.snapshotView(afterScreenUpdates: false) ?? videoView.snapshotView(afterScreenUpdates: true) { lastFrame[component.call.peerId.id.description] = snapshot// ()! } -// } + var aspect = videoView.getAspect() - // aspect == 1 the first run if component.isFullscreen && self.hadVideo { if aspect <= 0.01 { aspect = 16.0 / 9 @@ -545,7 +476,6 @@ final class MediaStreamVideoComponent: Component { if let videoBlurView = self.videoBlurView { videoBlurView.updateIsEnabled(component.isVisible) -// videoBlurView.isHidden = component.isFullscreen if component.isFullscreen { transition.withAnimation(.none).setFrame(view: videoBlurView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - blurredVideoSize.width) / 2.0), y: floor((availableSize.height - blurredVideoSize.height) / 2.0)), size: blurredVideoSize), completion: nil) } else { @@ -564,15 +494,15 @@ final class MediaStreamVideoComponent: Component { videoSize = CGSize(width: 16 / 9 * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height)) } loadingBlurView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize) - print("[LBVFrame] \(loadingBlurView.frame)") + loadingBlurView.layer.cornerRadius = videoCornerRadius placeholderView.frame = loadingBlurView.frame placeholderView.layer.cornerRadius = videoCornerRadius placeholderView.clipsToBounds = true -// shimmerOverlayLayer.frame = loadingBlurView.bounds shimmerBorderLayer.frame = loadingBlurView.bounds + let borderMask = CAShapeLayer() borderMask.path = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: videoCornerRadius, cornerHeight: videoCornerRadius, transform: nil) borderMask.fillColor = UIColor.white.withAlphaComponent(0.4).cgColor @@ -581,9 +511,6 @@ final class MediaStreamVideoComponent: Component { shimmerBorderLayer.mask = borderMask shimmerBorderLayer.cornerRadius = videoCornerRadius - if component.isFullscreen { -// loadingBlurView.removeFromSuperview() - } if !self.hadVideo { if self.noSignalTimer == nil { @@ -668,9 +595,6 @@ final class MediaStreamVideoComponent: Component { presentation.removeFromSuperview() }) } -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { -// presentation.removeFromSuperlayer() -// } UIView.animate(withDuration: 0.1) { [self] in videoBlurView?.alpha = 0 } @@ -726,3 +650,24 @@ final class MediaStreamVideoComponent: Component { // TODO: move to appropriate place fileprivate var lastFrame: [String: UIView] = [:] + +class CustomIntensityVisualEffectView: UIVisualEffectView { + private var animator: UIViewPropertyAnimator! + + init(effect: UIVisualEffect, intensity: CGFloat) { + super.init(effect: nil) + animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [weak self] in self?.effect = effect } + animator.startAnimation() + animator.pauseAnimation() + animator.fractionComplete = intensity + animator.pausesOnCompletion = true + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + + deinit { + animator.stopAnimation(true) + } +}