diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 5efce89817..061901b673 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -801,7 +801,8 @@ public final class _MediaStreamComponent: CombinedComponent { strongSelf.updated(transition: .immediate) }) - self.networkStateDisposable = (call.account.networkState |> deliverOnMainQueue).start(next: { [weak self] state in + // 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: @@ -828,7 +829,7 @@ public final class _MediaStreamComponent: CombinedComponent { if prev != self?.videoStalled { self?.updated(transition: .immediate) } - }) + })*/ let callPeer = call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) @@ -956,6 +957,7 @@ public final class _MediaStreamComponent: CombinedComponent { public static var body: Body { 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) @@ -1037,6 +1039,14 @@ public final class _MediaStreamComponent: CombinedComponent { dragOffset = max(context.state.dismissOffset, sheetHeight - context.availableSize.height + context.view.safeAreaInsets.top)// sheetHeight - UIScreen.main.bounds.height } + let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - sheetHeight + dragOffset) + let dismissTapComponent = dismissTapComponent.update( + component: Rectangle(color: .red.withAlphaComponent(0)), + availableSize: CGSize(width: context.availableSize.width, height: dismissTapAreaHeight), + transition: context.transition + ) + + let video = video.update( component: MediaStreamVideoComponent( call: context.component.call, @@ -1044,7 +1054,7 @@ public final class _MediaStreamComponent: CombinedComponent { isVisible: environment.isVisible && context.state.isVisibleInHierarchy, isAdmin: context.state.canManageCall, peerTitle: context.state.peerTitle, - // TODO: find out how to get image + // TODO: remove // find out how to get image peerImage: nil, isFullscreen: isFullscreen, videoLoading: context.state.videoStalled, @@ -1068,8 +1078,12 @@ public final class _MediaStreamComponent: CombinedComponent { state?.videoSize = size }, onVideoPlaybackLiveChange: { [weak state] isLive in - state?.videoStalled = !isLive - state?.updated() + guard let state else { return } + let wasLive = !state.videoStalled + if isLive != wasLive { + state.videoStalled = !isLive + state.updated() + } } ), availableSize: context.availableSize, @@ -1381,64 +1395,77 @@ public final class _MediaStreamComponent: CombinedComponent { } let availableSize = context.availableSize let safeAreaTop = context.view.safeAreaInsets.top + + let onPanGesture: ((Gesture.PanGestureState) -> Void) = { [weak state] panState in + guard let state = state else { + return + } + switch panState { + case .began: + state.initialOffset = state.dismissOffset + case let .updated(offset): + state.updateDismissOffset(value: state.initialOffset + offset.y, interactive: true) + case let .ended(velocity): + // TODO: Dismiss sheet depending on velocity + if velocity.y > 200.0 { + if state.isFullscreen { + state.isFullscreen = false + state.updateDismissOffset(value: 0.0, interactive: false) + if let controller = controller() as? MediaStreamComponentController { + controller.updateOrientation(orientation: .portrait) + } + } else { + if isFullyDragged || state.initialOffset != 0 { + state.updateDismissOffset(value: 0.0, interactive: false) + } else { + let _ = call.leave(terminateIfPossible: false) + } + } + /*activatePictureInPicture.invoke(Action { [weak state] in + guard let state = state, let controller = controller() as? MediaStreamComponentController else { + return + } + state.updateDismissOffset(value: velocity.y < 0 ? -height : height, interactive: false) + controller.dismiss(closing: false, manual: true) + })*/ + } else { + if isFullyDragged { + state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false) + } else { + if velocity.y < -200 { + // Expand + state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false) + } else { + state.updateDismissOffset(value: 0.0, interactive: false) + } + } + } + } + } + context.add(background .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) .gesture(.tap { [weak state] in - guard let state = state else { + guard let state = state, state.isFullscreen else { return } state.toggleDisplayUI() }) - .gesture(.pan { [weak state] panState in - guard let state = state else { - return - } - switch panState { - case .began: - state.initialOffset = state.dismissOffset - case let .updated(offset): - state.updateDismissOffset(value: state.initialOffset + offset.y, interactive: true) - case let .ended(velocity): - // TODO: Dismiss sheet depending on velocity - if velocity.y > 200.0 { - if state.isFullscreen { - state.isFullscreen = false - state.updateDismissOffset(value: 0.0, interactive: false) - if let controller = controller() as? MediaStreamComponentController { - controller.updateOrientation(orientation: .portrait) - } - } else { - if isFullyDragged || state.initialOffset != 0 { - state.updateDismissOffset(value: 0.0, interactive: false) - } else { - let _ = call.leave(terminateIfPossible: false) - } - } - /*activatePictureInPicture.invoke(Action { [weak state] in - guard let state = state, let controller = controller() as? MediaStreamComponentController else { - return - } - state.updateDismissOffset(value: velocity.y < 0 ? -height : height, interactive: false) - controller.dismiss(closing: false, manual: true) - })*/ - } else { - if isFullyDragged { - state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false) - } else { - if velocity.y < -200 { - // Expand - state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false) - } else { - state.updateDismissOffset(value: 0.0, interactive: false) - } - } - } - } + .gesture(.pan { panState in + onPanGesture(panState) }) ) // var bottomComponent: AnyComponent? // var fullScreenToolbarComponent: AnyComponent? + context.add(dismissTapComponent + .position(CGPoint(x: context.availableSize.width / 2, y: dismissTapAreaHeight / 2)) + .gesture(.tap { + _ = call.leave(terminateIfPossible: false) + }) + .gesture(.pan(onPanGesture)) + ) + if !isFullscreen { let bottomComponent = AnyComponent(ButtonsRowComponent( bottomInset: environment.safeInsets.bottom, diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index 8b84a9780c..e60772bed3 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -238,10 +238,10 @@ final class _MediaStreamVideoComponent: Component { // 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] { + if let frameView = lastFrame[component.call.peerId.id.description] { placeholderView.subviews.forEach { $0.removeFromSuperview() } - placeholderView.addSubview(frame) - frame.frame = placeholderView.bounds + placeholderView.addSubview(frameView) + frameView.frame = placeholderView.bounds // placeholderView.backgroundColor = .green } else { // placeholderView.addSubview(avatarPlaceholderView) @@ -252,30 +252,36 @@ final class _MediaStreamVideoComponent: Component { if !hadVideo && placeholderView.superview == nil { addSubview(placeholderView) } + + let needsFadeInAnimation = hadVideo + if loadingBlurView.superview == nil { addSubview(loadingBlurView) - let anim = CABasicAnimation(keyPath: "opacity") - anim.duration = 0.5 - anim.fromValue = 0 - anim.toValue = 1 - anim.fillMode = .forwards - anim.isRemovedOnCompletion = false - loadingBlurView.layer.add(anim, forKey: "opacity") + if needsFadeInAnimation { + let anim = CABasicAnimation(keyPath: "opacity") + anim.duration = 0.5 + anim.fromValue = 0 + anim.toValue = 1 + anim.fillMode = .forwards + anim.isRemovedOnCompletion = false + loadingBlurView.layer.add(anim, forKey: "opacity") + } } if shimmerBorderLayer.superlayer == nil { // loadingBlurView.contentView.layer.addSublayer(shimmerOverlayLayer) loadingBlurView.contentView.layer.addSublayer(shimmerBorderLayer) } loadingBlurView.clipsToBounds = true - if shimmerOverlayLayer.mask == nil { - shimmer = .init() - shimmer.layer = shimmerOverlayLayer - shimmerOverlayView.compositingFilter = "softLightBlendMode" - shimmer.testUpdate(background: .clear, foreground: .white.withAlphaComponent(0.4)) - } +// if shimmerOverlayLayer.mask == nil { +// 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 + let cornerRadius = loadingBlurView.layer.cornerRadius +// shimmerOverlayLayer.opacity = 0.6 shimmerBorderLayer.cornerRadius = cornerRadius // TODO: check isFullScreeen shimmerBorderLayer.masksToBounds = true shimmerBorderLayer.compositingFilter = "softLightBlendMode" @@ -295,14 +301,14 @@ final class _MediaStreamVideoComponent: Component { // testBorder.frame = shimmerBorderLayer.bounds // let borderMask = CALayer() // shimmerBorderLayer.removeAllAnimations() -// if shimmerBorderLayer.mask == nil { - borderShimmer = .init() - shimmerBorderLayer.mask = borderMask - borderShimmer.layer = shimmerBorderLayer + // if shimmerBorderLayer.mask == nil { + borderShimmer = .init() + shimmerBorderLayer.mask = borderMask + borderShimmer.layer = shimmerBorderLayer shimmerBorderLayer.backgroundColor = UIColor.clear.cgColor -// shimmerBorderLayer.backgroundColor = UIColor.green.withAlphaComponent(0.4).cgColor - borderShimmer.testUpdate(background: .clear, foreground: .white) -// } + // shimmerBorderLayer.backgroundColor = UIColor.green.withAlphaComponent(0.4).cgColor + borderShimmer.testUpdate(background: .clear, foreground: .white) + // } loadingBlurView.alpha = 1 } else { if hadVideo { @@ -313,32 +319,32 @@ final class _MediaStreamVideoComponent: Component { anim.toValue = 0 anim.fillMode = .forwards anim.isRemovedOnCompletion = false - anim.completion = { [self] _ in -// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in - loadingBlurView.removeFromSuperview() - // loadingBlurView = .init(effect: UIBlurEffect(style: .light), intensity: 0.4) - placeholderView.removeFromSuperview() + anim.completion = { [weak self] _ in + guard self?.videoStalled == false else { return } + self?.loadingBlurView.removeFromSuperview() + self?.placeholderView.removeFromSuperview() } loadingBlurView.layer.add(anim, forKey: "opacity") } else { // Accounting for delay in first frame received - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [self] in - guard !self.videoStalled else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in + guard self?.videoStalled == false else { return } // TODO: animate blur intesity with UIPropertyAnimator - loadingBlurView.layer.removeAllAnimations() + 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 = { _ in + 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.removeFromSuperview() + self?.placeholderView.removeFromSuperview() } - loadingBlurView.layer.add(anim, forKey: "opacity") + self?.loadingBlurView.layer.add(anim, forKey: "opacity") // UIView.transition(with: self, duration: 0.2, animations: { //// self.loadingBlurView.animator.fractionComplete = 0 //// self.loadingBlurView.effect = nil @@ -361,6 +367,7 @@ final class _MediaStreamVideoComponent: Component { var timeLastFrameReceived: CFAbsoluteTime? var isFullscreen: Bool = false + let videoLoadingThrottler = Throttler(duration: 1, queue: .main) func update(component: _MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { self.state = state @@ -381,7 +388,7 @@ final class _MediaStreamVideoComponent: Component { }) } - if component.videoLoading || self.videoStalled { + if !component.hasVideo || component.videoLoading || self.videoStalled { updateVideoStalled(isStalled: true) } else { updateVideoStalled(isStalled: false) @@ -396,10 +403,13 @@ final class _MediaStreamVideoComponent: Component { let currentTime = CFAbsoluteTimeGetCurrent() if let lastFrameTime = strongSelf.timeLastFrameReceived, currentTime - lastFrameTime > 0.5 { - DispatchQueue.main.async { - strongSelf.videoStalled = true - strongSelf.onVideoPlaybackChange(false) +// DispatchQueue.main.async { + strongSelf.videoLoadingThrottler.publish(true, includingLatest: true) { isStalled in + strongSelf.videoStalled = isStalled + strongSelf.onVideoPlaybackChange(!isStalled) } + + // } } } } // TODO: use mapToThrottled (?) @@ -409,7 +419,7 @@ final class _MediaStreamVideoComponent: Component { // strongSelf.stallTimer?.invalidate() // TODO: optimize with throttle strongSelf.timeLastFrameReceived = CFAbsoluteTimeGetCurrent() - DispatchQueue.main.async { +// DispatchQueue.main.async { // strongSelf.stallTimer = _stallTimer // DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // print(strongSelf.videoStalled) @@ -417,9 +427,13 @@ final class _MediaStreamVideoComponent: Component { // strongSelf.stallTimer?.fire() // } // RunLoop.main.add(strongSelf.stallTimer!, forMode: .common) - strongSelf.videoStalled = false - strongSelf.onVideoPlaybackChange(true) + strongSelf.videoLoadingThrottler.publish(false, includingLatest: true) { isStalled in + strongSelf.videoStalled = isStalled + strongSelf.onVideoPlaybackChange(!isStalled) } +// strongSelf.videoStalled = false +// strongSelf.onVideoPlaybackChange(true) +// } }) stallTimer = _stallTimer // RunLoop.main.add(stallTimer!, forMode: .common) @@ -743,22 +757,24 @@ final class _MediaStreamVideoComponent: Component { func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { // Fading to make - let presentation = self.videoView!.snapshotView(afterScreenUpdates: false)! - self.addSubview(presentation) - presentation.frame = self.videoView!.frame - lastFrame[self.component!.call.peerId.id.description] = presentation -// let image = UIGraphicsImageRenderer(size: presentation.bounds.size).image { context in -// presentation.render(in: context.cgContext) -// } -// print(image) - self.videoView?.alpha = 0 -// self.videoView?.alpha = 0.5 -// presentation.animateAlpha(from: 1, to: 0, duration: 0.1, completion: { _ in presentation.removeFromSuperlayer() }) - UIView.animate(withDuration: 0.1, animations: { - presentation.alpha = 0 - }, completion: { _ in - presentation.removeFromSuperview() - }) + if let presentation = self.videoView!.snapshotView(afterScreenUpdates: false) { + self.addSubview(presentation) + presentation.frame = self.videoView!.frame + lastFrame[self.component!.call.peerId.id.description] = presentation + + // let image = UIGraphicsImageRenderer(size: presentation.bounds.size).image { context in + // presentation.render(in: context.cgContext) + // } + // print(image) + self.videoView?.alpha = 0 + // self.videoView?.alpha = 0.5 + // presentation.animateAlpha(from: 1, to: 0, duration: 0.1, completion: { _ in presentation.removeFromSuperlayer() }) + UIView.animate(withDuration: 0.1, animations: { + presentation.alpha = 0 + }, completion: { _ in + presentation.removeFromSuperview() + }) + } // DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // presentation.removeFromSuperlayer() // }