Throttling playback status change

This commit is contained in:
Ilya Yelagov 2022-12-08 00:07:59 +04:00
parent bfa7de8e27
commit 17ef4ae35b
2 changed files with 153 additions and 110 deletions

View File

@ -801,7 +801,8 @@ public final class _MediaStreamComponent: CombinedComponent {
strongSelf.updated(transition: .immediate) 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 } guard let strongSelf = self else { return }
switch state { switch state {
case .waitingForNetwork, .connecting: case .waitingForNetwork, .connecting:
@ -828,7 +829,7 @@ public final class _MediaStreamComponent: CombinedComponent {
if prev != self?.videoStalled { if prev != self?.videoStalled {
self?.updated(transition: .immediate) self?.updated(transition: .immediate)
} }
}) })*/
let callPeer = call.accountContext.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: call.peerId)) 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 { public static var body: Body {
let background = Child(Rectangle.self) let background = Child(Rectangle.self)
let dismissTapComponent = Child(Rectangle.self)
let video = Child(MediaStreamVideoComponent.self) let video = Child(MediaStreamVideoComponent.self)
// let navigationBar = Child(NavigationBarComponent.self) // let navigationBar = Child(NavigationBarComponent.self)
// let toolbar = Child(ToolbarComponent.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 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( let video = video.update(
component: MediaStreamVideoComponent( component: MediaStreamVideoComponent(
call: context.component.call, call: context.component.call,
@ -1044,7 +1054,7 @@ public final class _MediaStreamComponent: CombinedComponent {
isVisible: environment.isVisible && context.state.isVisibleInHierarchy, isVisible: environment.isVisible && context.state.isVisibleInHierarchy,
isAdmin: context.state.canManageCall, isAdmin: context.state.canManageCall,
peerTitle: context.state.peerTitle, peerTitle: context.state.peerTitle,
// TODO: find out how to get image // TODO: remove // find out how to get image
peerImage: nil, peerImage: nil,
isFullscreen: isFullscreen, isFullscreen: isFullscreen,
videoLoading: context.state.videoStalled, videoLoading: context.state.videoStalled,
@ -1068,8 +1078,12 @@ public final class _MediaStreamComponent: CombinedComponent {
state?.videoSize = size state?.videoSize = size
}, },
onVideoPlaybackLiveChange: { [weak state] isLive in onVideoPlaybackLiveChange: { [weak state] isLive in
state?.videoStalled = !isLive guard let state else { return }
state?.updated() let wasLive = !state.videoStalled
if isLive != wasLive {
state.videoStalled = !isLive
state.updated()
}
} }
), ),
availableSize: context.availableSize, availableSize: context.availableSize,
@ -1381,15 +1395,8 @@ public final class _MediaStreamComponent: CombinedComponent {
} }
let availableSize = context.availableSize let availableSize = context.availableSize
let safeAreaTop = context.view.safeAreaInsets.top let safeAreaTop = context.view.safeAreaInsets.top
context.add(background
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) let onPanGesture: ((Gesture.PanGestureState) -> Void) = { [weak state] panState in
.gesture(.tap { [weak state] in
guard let state = state else {
return
}
state.toggleDisplayUI()
})
.gesture(.pan { [weak state] panState in
guard let state = state else { guard let state = state else {
return return
} }
@ -1434,11 +1441,31 @@ public final class _MediaStreamComponent: CombinedComponent {
} }
} }
} }
}
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, state.isFullscreen else {
return
}
state.toggleDisplayUI()
})
.gesture(.pan { panState in
onPanGesture(panState)
}) })
) )
// var bottomComponent: AnyComponent<Empty>? // var bottomComponent: AnyComponent<Empty>?
// var fullScreenToolbarComponent: AnyComponent<Empty>? // var fullScreenToolbarComponent: AnyComponent<Empty>?
context.add(dismissTapComponent
.position(CGPoint(x: context.availableSize.width / 2, y: dismissTapAreaHeight / 2))
.gesture(.tap {
_ = call.leave(terminateIfPossible: false)
})
.gesture(.pan(onPanGesture))
)
if !isFullscreen { if !isFullscreen {
let bottomComponent = AnyComponent(ButtonsRowComponent( let bottomComponent = AnyComponent(ButtonsRowComponent(
bottomInset: environment.safeInsets.bottom, bottomInset: environment.safeInsets.bottom,

View File

@ -238,10 +238,10 @@ final class _MediaStreamVideoComponent: Component {
// self.loadingBlurView.animator.fractionComplete = intensity // self.loadingBlurView.animator.fractionComplete = intensity
// self.loadingBlurView.animator.fractionComplete = 0.4 // self.loadingBlurView.animator.fractionComplete = 0.4
// self.loadingBlurView.effect = UIBlurEffect(style: .light) // 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.subviews.forEach { $0.removeFromSuperview() }
placeholderView.addSubview(frame) placeholderView.addSubview(frameView)
frame.frame = placeholderView.bounds frameView.frame = placeholderView.bounds
// placeholderView.backgroundColor = .green // placeholderView.backgroundColor = .green
} else { } else {
// placeholderView.addSubview(avatarPlaceholderView) // placeholderView.addSubview(avatarPlaceholderView)
@ -252,8 +252,12 @@ final class _MediaStreamVideoComponent: Component {
if !hadVideo && placeholderView.superview == nil { if !hadVideo && placeholderView.superview == nil {
addSubview(placeholderView) addSubview(placeholderView)
} }
let needsFadeInAnimation = hadVideo
if loadingBlurView.superview == nil { if loadingBlurView.superview == nil {
addSubview(loadingBlurView) addSubview(loadingBlurView)
if needsFadeInAnimation {
let anim = CABasicAnimation(keyPath: "opacity") let anim = CABasicAnimation(keyPath: "opacity")
anim.duration = 0.5 anim.duration = 0.5
anim.fromValue = 0 anim.fromValue = 0
@ -262,20 +266,22 @@ final class _MediaStreamVideoComponent: Component {
anim.isRemovedOnCompletion = false anim.isRemovedOnCompletion = false
loadingBlurView.layer.add(anim, forKey: "opacity") loadingBlurView.layer.add(anim, forKey: "opacity")
} }
}
if shimmerBorderLayer.superlayer == nil { if shimmerBorderLayer.superlayer == nil {
// loadingBlurView.contentView.layer.addSublayer(shimmerOverlayLayer) // loadingBlurView.contentView.layer.addSublayer(shimmerOverlayLayer)
loadingBlurView.contentView.layer.addSublayer(shimmerBorderLayer) loadingBlurView.contentView.layer.addSublayer(shimmerBorderLayer)
} }
loadingBlurView.clipsToBounds = true loadingBlurView.clipsToBounds = true
if shimmerOverlayLayer.mask == nil { // if shimmerOverlayLayer.mask == nil {
shimmer = .init() // shimmer = .init()
shimmer.layer = shimmerOverlayLayer // shimmer.layer = shimmerOverlayLayer
shimmerOverlayView.compositingFilter = "softLightBlendMode" // shimmerOverlayView.compositingFilter = "softLightBlendMode"
shimmer.testUpdate(background: .clear, foreground: .white.withAlphaComponent(0.4)) // shimmer.testUpdate(background: .clear, foreground: .white.withAlphaComponent(0.4))
} // }
// loadingBlurView.layer.cornerRadius = 10 // loadingBlurView.layer.cornerRadius = 10
shimmerOverlayLayer.opacity = 0.6
let cornerRadius = loadingBlurView.layer.cornerRadius let cornerRadius = loadingBlurView.layer.cornerRadius
// shimmerOverlayLayer.opacity = 0.6
shimmerBorderLayer.cornerRadius = cornerRadius // TODO: check isFullScreeen shimmerBorderLayer.cornerRadius = cornerRadius // TODO: check isFullScreeen
shimmerBorderLayer.masksToBounds = true shimmerBorderLayer.masksToBounds = true
shimmerBorderLayer.compositingFilter = "softLightBlendMode" shimmerBorderLayer.compositingFilter = "softLightBlendMode"
@ -313,32 +319,32 @@ final class _MediaStreamVideoComponent: Component {
anim.toValue = 0 anim.toValue = 0
anim.fillMode = .forwards anim.fillMode = .forwards
anim.isRemovedOnCompletion = false anim.isRemovedOnCompletion = false
anim.completion = { [self] _ in anim.completion = { [weak self] _ in
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in guard self?.videoStalled == false else { return }
loadingBlurView.removeFromSuperview() self?.loadingBlurView.removeFromSuperview()
// loadingBlurView = .init(effect: UIBlurEffect(style: .light), intensity: 0.4) self?.placeholderView.removeFromSuperview()
placeholderView.removeFromSuperview()
} }
loadingBlurView.layer.add(anim, forKey: "opacity") loadingBlurView.layer.add(anim, forKey: "opacity")
} else { } else {
// Accounting for delay in first frame received // Accounting for delay in first frame received
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [self] in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
guard !self.videoStalled else { return } guard self?.videoStalled == false else { return }
// TODO: animate blur intesity with UIPropertyAnimator // TODO: animate blur intesity with UIPropertyAnimator
loadingBlurView.layer.removeAllAnimations() self?.loadingBlurView.layer.removeAllAnimations()
let anim = CABasicAnimation(keyPath: "opacity") let anim = CABasicAnimation(keyPath: "opacity")
anim.duration = 0.5 anim.duration = 0.5
anim.fromValue = 1 anim.fromValue = 1
anim.toValue = 0 anim.toValue = 0
anim.fillMode = .forwards anim.fillMode = .forwards
anim.isRemovedOnCompletion = false 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 // DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [self] in
self.loadingBlurView.removeFromSuperview() self?.loadingBlurView.removeFromSuperview()
self.placeholderView.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: { // UIView.transition(with: self, duration: 0.2, animations: {
//// self.loadingBlurView.animator.fractionComplete = 0 //// self.loadingBlurView.animator.fractionComplete = 0
//// self.loadingBlurView.effect = nil //// self.loadingBlurView.effect = nil
@ -361,6 +367,7 @@ final class _MediaStreamVideoComponent: Component {
var timeLastFrameReceived: CFAbsoluteTime? var timeLastFrameReceived: CFAbsoluteTime?
var isFullscreen: Bool = false var isFullscreen: Bool = false
let videoLoadingThrottler = Throttler<Bool>(duration: 1, queue: .main)
func update(component: _MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize { func update(component: _MediaStreamVideoComponent, availableSize: CGSize, state: State, transition: Transition) -> CGSize {
self.state = state 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) updateVideoStalled(isStalled: true)
} else { } else {
updateVideoStalled(isStalled: false) updateVideoStalled(isStalled: false)
@ -396,10 +403,13 @@ final class _MediaStreamVideoComponent: Component {
let currentTime = CFAbsoluteTimeGetCurrent() let currentTime = CFAbsoluteTimeGetCurrent()
if let lastFrameTime = strongSelf.timeLastFrameReceived, if let lastFrameTime = strongSelf.timeLastFrameReceived,
currentTime - lastFrameTime > 0.5 { currentTime - lastFrameTime > 0.5 {
DispatchQueue.main.async { // DispatchQueue.main.async {
strongSelf.videoStalled = true strongSelf.videoLoadingThrottler.publish(true, includingLatest: true) { isStalled in
strongSelf.onVideoPlaybackChange(false) strongSelf.videoStalled = isStalled
strongSelf.onVideoPlaybackChange(!isStalled)
} }
// }
} }
} } } }
// TODO: use mapToThrottled (?) // TODO: use mapToThrottled (?)
@ -409,7 +419,7 @@ final class _MediaStreamVideoComponent: Component {
// strongSelf.stallTimer?.invalidate() // strongSelf.stallTimer?.invalidate()
// TODO: optimize with throttle // TODO: optimize with throttle
strongSelf.timeLastFrameReceived = CFAbsoluteTimeGetCurrent() strongSelf.timeLastFrameReceived = CFAbsoluteTimeGetCurrent()
DispatchQueue.main.async { // DispatchQueue.main.async {
// strongSelf.stallTimer = _stallTimer // strongSelf.stallTimer = _stallTimer
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// print(strongSelf.videoStalled) // print(strongSelf.videoStalled)
@ -417,9 +427,13 @@ final class _MediaStreamVideoComponent: Component {
// strongSelf.stallTimer?.fire() // strongSelf.stallTimer?.fire()
// } // }
// RunLoop.main.add(strongSelf.stallTimer!, forMode: .common) // RunLoop.main.add(strongSelf.stallTimer!, forMode: .common)
strongSelf.videoStalled = false strongSelf.videoLoadingThrottler.publish(false, includingLatest: true) { isStalled in
strongSelf.onVideoPlaybackChange(true) strongSelf.videoStalled = isStalled
strongSelf.onVideoPlaybackChange(!isStalled)
} }
// strongSelf.videoStalled = false
// strongSelf.onVideoPlaybackChange(true)
// }
}) })
stallTimer = _stallTimer stallTimer = _stallTimer
// RunLoop.main.add(stallTimer!, forMode: .common) // RunLoop.main.add(stallTimer!, forMode: .common)
@ -743,10 +757,11 @@ final class _MediaStreamVideoComponent: Component {
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// Fading to make // Fading to make
let presentation = self.videoView!.snapshotView(afterScreenUpdates: false)! if let presentation = self.videoView!.snapshotView(afterScreenUpdates: false) {
self.addSubview(presentation) self.addSubview(presentation)
presentation.frame = self.videoView!.frame presentation.frame = self.videoView!.frame
lastFrame[self.component!.call.peerId.id.description] = presentation lastFrame[self.component!.call.peerId.id.description] = presentation
// let image = UIGraphicsImageRenderer(size: presentation.bounds.size).image { context in // let image = UIGraphicsImageRenderer(size: presentation.bounds.size).image { context in
// presentation.render(in: context.cgContext) // presentation.render(in: context.cgContext)
// } // }
@ -759,6 +774,7 @@ final class _MediaStreamVideoComponent: Component {
}, completion: { _ in }, completion: { _ in
presentation.removeFromSuperview() presentation.removeFromSuperview()
}) })
}
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
// presentation.removeFromSuperlayer() // presentation.removeFromSuperlayer()
// } // }