diff --git a/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift b/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift index 255b419402..56e561b012 100644 --- a/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift +++ b/submodules/TelegramCallsUI/Sources/Components/AnimatedCounterView.swift @@ -50,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 - 8 : 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 - 10, width: subtitleLabel.intrinsicContentSize.width + 20, height: 20) } func update(countString: String, subtitle: String) { diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 8960b73879..5efce89817 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -969,7 +969,7 @@ public final class _MediaStreamComponent: CombinedComponent { let moreAnimationTag = GenericComponentViewTag() return { context in - var forceFullScreenInLandscape: Bool { false } + var forceFullScreenInLandscape: Bool { true } let environment = context.environment[ViewControllerComponentContainer.Environment.self].value if environment.isVisible { } else { @@ -1000,24 +1000,36 @@ public final class _MediaStreamComponent: CombinedComponent { state.updated(transition: .easeInOut(duration: 3)) deactivatePictureInPicture.invoke(Void()) } - var isFullscreen = state.isFullscreen + let isFullscreen: Bool // = state.isFullscreen let isLandscape = context.availableSize.width > context.availableSize.height // if let videoSize = context.state.videoSize { // Always fullscreen in landscape // 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 let videoSize = context.state.videoSize, videoSize.width > videoSize.height && !isLandscape && isFullscreen { - state.isFullscreen = false - isFullscreen = false - } -// } + if forceFullScreenInLandscape && /*videoSize.width > videoSize.height &&*/ isLandscape && !state.isFullscreen { + state.isFullscreen = true + isFullscreen = true + } else if let videoSize = context.state.videoSize, videoSize.width > videoSize.height && !isLandscape && state.isFullscreen { + state.isFullscreen = false + isFullscreen = false + } else { + isFullscreen = state.isFullscreen + } + // } + let videoInset: CGFloat + if !isFullscreen { + videoInset = 16 + } else { + videoInset = 0 + } - let videoHeight: CGFloat = context.availableSize.width / 16 * 9 + let videoHeight: CGFloat = forceFullScreenInLandscape + ? (context.availableSize.width - videoInset * 2) / 16 * 9 + : context.state.videoSize?.height ?? (context.availableSize.width - videoInset * 2) / 16 * 9 let bottomPadding = 40 + environment.safeInsets.bottom - let sheetHeight: CGFloat = isFullscreen ? context.availableSize.height : (44 + videoHeight + 40 + 69 + 16 + 32 + 70 + bottomPadding) + let sheetHeight: CGFloat = isFullscreen + ? context.availableSize.height + : (44 + videoHeight + 40 + 69 + 16 + 32 + 70 + bottomPadding) let isFullyDragged = context.availableSize.height - sheetHeight + state.dismissOffset - context.view.safeAreaInsets.top < 30 var dragOffset = context.state.dismissOffset @@ -1054,6 +1066,10 @@ public final class _MediaStreamComponent: CombinedComponent { }, onVideoSizeRetrieved: { [weak state] size in state?.videoSize = size + }, + onVideoPlaybackLiveChange: { [weak state] isLive in + state?.videoStalled = !isLive + state?.updated() } ), availableSize: context.availableSize, @@ -1538,7 +1554,8 @@ public final class _MediaStreamComponent: CombinedComponent { participantsCount: context.state.originInfo?.memberCount ?? 0, // Int.random(in: 0...999998)// [0, 5, 15, 16, 95, 100, 16042, 942539].randomElement()! // isFullyExtended: isFullyDragged, - deviceCornerRadius: deviceCornerRadius ?? 0 + deviceCornerRadius: deviceCornerRadius ?? 0, + videoHeight: videoHeight ), availableSize: context.availableSize, transition: context.transition @@ -1559,7 +1576,7 @@ public final class _MediaStreamComponent: CombinedComponent { if isFullscreen { videoPos = context.availableSize.height / 2 + dragOffset } else { - videoPos = sheetPosition - sheetHeight / 2 + videoHeight / 2 + 50 + 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)) @@ -1623,7 +1640,8 @@ public final class _MediaStreamComponent: CombinedComponent { bottomPadding: 12, participantsCount: -1, // context.state.originInfo?.memberCount ?? 0 isFullyExtended: isFullyDragged, - deviceCornerRadius: deviceCornerRadius ?? 0 + deviceCornerRadius: deviceCornerRadius ?? 0, + videoHeight: videoHeight ), availableSize: context.availableSize, transition: context.transition diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift index b9236d73bd..8b84a9780c 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamVideoComponent.swift @@ -80,6 +80,7 @@ final class _MediaStreamVideoComponent: Component { let onVideoSizeRetrieved: (CGSize) -> Void let videoLoading: Bool let callPeer: Peer? + let onVideoPlaybackLiveChange: (Bool) -> Void init( call: PresentationGroupCallImpl, @@ -95,7 +96,8 @@ final class _MediaStreamVideoComponent: Component { deactivatePictureInPicture: ActionSlot, bringBackControllerForPictureInPictureDeactivation: @escaping (@escaping () -> Void) -> Void, pictureInPictureClosed: @escaping () -> Void, - onVideoSizeRetrieved: @escaping (CGSize) -> Void + onVideoSizeRetrieved: @escaping (CGSize) -> Void, + onVideoPlaybackLiveChange: @escaping (Bool) -> Void ) { self.call = call self.hasVideo = hasVideo @@ -107,6 +109,7 @@ final class _MediaStreamVideoComponent: Component { self.deactivatePictureInPicture = deactivatePictureInPicture self.bringBackControllerForPictureInPictureDeactivation = bringBackControllerForPictureInPictureDeactivation self.pictureInPictureClosed = pictureInPictureClosed + self.onVideoPlaybackLiveChange = onVideoPlaybackLiveChange self.callPeer = callPeer self.peerImage = peerImage @@ -220,6 +223,8 @@ final class _MediaStreamVideoComponent: Component { } } } + var onVideoPlaybackChange: ((Bool) -> Void) = { _ in } + private var frameInputDisposable: Disposable? private func updateVideoStalled(isStalled: Bool) { @@ -268,15 +273,15 @@ final class _MediaStreamVideoComponent: Component { shimmerOverlayView.compositingFilter = "softLightBlendMode" shimmer.testUpdate(background: .clear, foreground: .white.withAlphaComponent(0.4)) } - loadingBlurView.layer.cornerRadius = 10 +// loadingBlurView.layer.cornerRadius = 10 shimmerOverlayLayer.opacity = 0.6 - - shimmerBorderLayer.cornerRadius = 10 + let cornerRadius = loadingBlurView.layer.cornerRadius + shimmerBorderLayer.cornerRadius = cornerRadius // TODO: check isFullScreeen shimmerBorderLayer.masksToBounds = true shimmerBorderLayer.compositingFilter = "softLightBlendMode" 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: 10, cornerHeight: 10, transform: nil) + borderMask.path = CGPath(roundedRect: .init(x: 0, y: 0, width: shimmerBorderLayer.bounds.width, height: shimmerBorderLayer.bounds.height), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) borderMask.fillColor = UIColor.white.withAlphaComponent(0.4).cgColor borderMask.strokeColor = UIColor.white.withAlphaComponent(0.8).cgColor borderMask.lineWidth = 2 @@ -353,12 +358,17 @@ final class _MediaStreamVideoComponent: Component { var avatarDisposable: Disposable? var didBeginLoadingAvatar = false // let avatarPlaceholderView = UIImageView() + var timeLastFrameReceived: CFAbsoluteTime? + + var isFullscreen: Bool = false 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 if let peer = component.callPeer, !didBeginLoadingAvatar { didBeginLoadingAvatar = true @@ -373,96 +383,46 @@ final class _MediaStreamVideoComponent: 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 - UIView.transition(with: self.loadingBlurView, duration: 0.2, animations: { - self.loadingBlurView.alpha = 0 - }, completion: { _ in - self.loadingBlurView.removeFromSuperview() - }) - 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)") + +// print("Timer emitting \(timer)") + let currentTime = CFAbsoluteTimeGetCurrent() + if let lastFrameTime = strongSelf.timeLastFrameReceived, + currentTime - lastFrameTime > 0.5 { DispatchQueue.main.async { strongSelf.videoStalled = true + strongSelf.onVideoPlaybackChange(false) } } - } + } } // TODO: use mapToThrottled (?) frameInputDisposable = input.start(next: { [weak self] input in guard let strongSelf = self else { return } - print("input") - strongSelf.stallTimer?.invalidate() +// print("input") + // strongSelf.stallTimer?.invalidate() // TODO: optimize with throttle + strongSelf.timeLastFrameReceived = CFAbsoluteTimeGetCurrent() DispatchQueue.main.async { - 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.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 + strongSelf.onVideoPlaybackChange(true) } }) stallTimer = _stallTimer -// RunLoop.main.add(stallTimer!, forMode: .common) + // RunLoop.main.add(stallTimer!, forMode: .common) if let videoBlurView = self.videoRenderingContext.makeView(input: input, blur: true) { self.videoBlurView = videoBlurView @@ -487,7 +447,7 @@ final class _MediaStreamVideoComponent: Component { } if let sampleBufferVideoView = videoView as? SampleBufferVideoRenderingView { sampleBufferVideoView.sampleBufferLayer.masksToBounds = true - sampleBufferVideoView.sampleBufferLayer.cornerRadius = 20 +// sampleBufferVideoView.sampleBufferLayer.cornerRadius = 10 if #available(iOS 13.0, *) { sampleBufferVideoView.sampleBufferLayer.preventsDisplaySleepDuringVideoPlayback = true @@ -532,7 +492,7 @@ final class _MediaStreamVideoComponent: Component { return delegate }())) pictureInPictureController?.playerLayer.masksToBounds = false - pictureInPictureController?.playerLayer.cornerRadius = 30 + pictureInPictureController?.playerLayer.cornerRadius = 10 } else if AVPictureInPictureController.isPictureInPictureSupported() { // TODO: support PiP for iOS < 15.0 // sampleBufferVideoView.sampleBufferLayer @@ -577,9 +537,11 @@ final class _MediaStreamVideoComponent: Component { fullScreenBackgroundPlaceholder.removeFromSuperview() } else if component.isFullscreen { if fullScreenBackgroundPlaceholder.superview == nil { -// insertSubview(fullScreenBackgroundPlaceholder, at: 0) + insertSubview(fullScreenBackgroundPlaceholder, at: 0) } fullScreenBackgroundPlaceholder.frame = self.bounds + } else { + fullScreenBackgroundPlaceholder.removeFromSuperview() } // sheetView.frame = .init(x: 0, y: sheetTop, width: availableSize.width, height: sheetHeight) @@ -595,6 +557,8 @@ final class _MediaStreamVideoComponent: Component { videoInset = 0 } + let videoSize: CGSize + let videoCornerRadius: CGFloat = component.isFullscreen ? 0 : 10 if let videoView = self.videoView { // TODO: REMOVE FROM HERE and move to call end (or at least to background) // if let presentation = videoView.snapshotView(afterScreenUpdates: false) { @@ -608,13 +572,17 @@ final class _MediaStreamVideoComponent: Component { // saveAspect(aspect) if component.isFullscreen { if aspect <= 0.01 { - aspect = 3.0 / 4.0 + aspect = 16.0 / 9 // 3.0 / 4.0 } } else { aspect = 16.0 / 9 } - let videoSize = CGSize(width: aspect * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height)) + if component.isFullscreen { + videoSize = CGSize(width: aspect * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height)) + } else { + videoSize = CGSize(width: aspect * 100.0, height: 100.0).aspectFitted(.init(width: min(availableSize.width, availableSize.height) - videoInset * 2, height: max(availableSize.height, availableSize.width))) + } let blurredVideoSize = videoSize.aspectFilled(availableSize) component.onVideoSizeRetrieved(videoSize) @@ -635,7 +603,7 @@ final class _MediaStreamVideoComponent: Component { videoView.updateIsEnabled(isVideoVisible) videoView.clipsToBounds = true - videoView.layer.cornerRadius = component.isFullscreen ? 0 : 10 + videoView.layer.cornerRadius = videoCornerRadius // var aspect = videoView.getAspect() // if aspect <= 0.01 { // TODO: remove debug @@ -664,21 +632,21 @@ final class _MediaStreamVideoComponent: Component { self.maskGradientLayer.frame = videoBlurView.bounds } + } else { + videoSize = CGSize(width: 16 / 9 * 100.0, height: 100.0).aspectFitted(.init(width: availableSize.width - videoInset * 2, height: availableSize.height)) } - - let 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) - loadingBlurView.layer.cornerRadius = 10 + loadingBlurView.layer.cornerRadius = videoCornerRadius placeholderView.frame = loadingBlurView.frame - placeholderView.layer.cornerRadius = 10 + placeholderView.layer.cornerRadius = videoCornerRadius placeholderView.clipsToBounds = true -// avatarPlaceholderView.frame = placeholderView.bounds + shimmerOverlayLayer.frame = loadingBlurView.bounds shimmerBorderLayer.frame = loadingBlurView.bounds -// shimmerBorderLayer.mask?.frame = loadingBlurView.bounds + if component.isFullscreen { - loadingBlurView.removeFromSuperview() +// loadingBlurView.removeFromSuperview() } if !self.hadVideo { // TODO: hide fullscreen button without video diff --git a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift index 971e8b0d6b..d15603a52e 100644 --- a/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/StreamSheetComponent.swift @@ -21,6 +21,7 @@ final class StreamSheetComponent: CombinedComponent { let bottomPadding: CGFloat let isFullyExtended: Bool let deviceCornerRadius: CGFloat + let videoHeight: CGFloat init( // color: UIColor, @@ -32,7 +33,8 @@ final class StreamSheetComponent: CombinedComponent { bottomPadding: CGFloat, participantsCount: Int, isFullyExtended: Bool, - deviceCornerRadius: CGFloat + deviceCornerRadius: CGFloat, + videoHeight: CGFloat ) { // self.leftItem = leftItem self.topComponent = topComponent @@ -45,6 +47,7 @@ final class StreamSheetComponent: CombinedComponent { self.participantsCount = participantsCount self.isFullyExtended = isFullyExtended self.deviceCornerRadius = deviceCornerRadius + self.videoHeight = videoHeight } static func ==(lhs: StreamSheetComponent, rhs: StreamSheetComponent) -> Bool { @@ -75,6 +78,9 @@ final class StreamSheetComponent: CombinedComponent { if lhs.isFullyExtended != rhs.isFullyExtended { return false } + if lhs.videoHeight != rhs.videoHeight { + return false + } return true } // @@ -95,7 +101,7 @@ final class StreamSheetComponent: CombinedComponent { override func draw(_ rect: CGRect) { super.draw(rect) - + // Debug interactive area // guard let context = UIGraphicsGetCurrentContext() else { return } // context.setFillColor(UIColor.red.cgColor) // overlayComponentsFrames.forEach { frame in @@ -193,7 +199,7 @@ final class StreamSheetComponent: CombinedComponent { ) (context.view as? StreamSheetComponent.View)?.overlayComponentsFrames.append(.init(x: 0, y: topOffset, width: topItem.size.width, height: topItem.size.height)) } - let videoHeight = (availableWidth - 32) / 16 * 9 + let videoHeight = context.component.videoHeight // ?? (min(availableWidth, context.availableSize.height) - 32) / 16 * 9 let sheetHeight = context.component.sheetHeight let animatedParticipantsVisible = context.component.participantsCount != -1 if true {