Smooth fullscreen transitions and tweaking PiP placeholder visibility

This commit is contained in:
Ilya Yelagov 2023-01-05 04:13:26 +04:00
parent 1424e7135d
commit c8ab1b8971
4 changed files with 314 additions and 73 deletions

View File

@ -180,7 +180,15 @@ public final class _UpdatedChildComponent {
var _opacity: CGFloat?
var _cornerRadius: CGFloat?
var _clipsToBounds: Bool?
/// Quick animation addition
var _animations: [AnimationKey: AnimationType] = [:]
public typealias AnimationKey = String
public enum AnimationType {
case transition
case custom(duration: TimeInterval, curveAndOtherParams: Any)
}
fileprivate var transitionAppear: Transition.Appear?
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
fileprivate var transitionDisappear: Transition.Disappear?
@ -240,6 +248,11 @@ public final class _UpdatedChildComponent {
self._position = position
return self
}
@discardableResult public func animation(key: AnimationKey) -> _UpdatedChildComponent {
self._animations[key] = .transition
return self
}
@discardableResult public func scale(_ scale: CGFloat) -> _UpdatedChildComponent {
self._scale = scale
@ -695,6 +708,8 @@ public extension CombinedComponent {
view.insertSubview(updatedChild.view, at: index)
let currentPosition = updatedChild.view.center
if let scale = updatedChild._scale {
updatedChild.view.bounds = CGRect(origin: CGPoint(), size: updatedChild.size)
updatedChild.view.center = updatedChild._position ?? CGPoint()
@ -702,6 +717,15 @@ public extension CombinedComponent {
} else {
updatedChild.view.frame = updatedChild.size.centered(around: updatedChild._position ?? CGPoint())
}
// for animation in updatedChild._animations { }
if updatedChild._animations["position"] != nil, let position = updatedChild._position {
transition.animatePosition(view: updatedChild.view, from: currentPosition, to: position)
}
if updatedChild._animations["opacity"] != nil, let opacity = updatedChild._opacity {
transition.animateAlpha(view: updatedChild.view, from: updatedChild.view.alpha, to: opacity)
}
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0

View File

@ -283,7 +283,11 @@ public final class MediaStreamComponent: CombinedComponent {
let dismissTapComponent = Child(Rectangle.self)
let video = Child(MediaStreamVideoComponent.self)
let sheet = Child(StreamSheetComponent.self)
let fullscreenOverlay = Child(StreamSheetComponent.self)
// let fullscreenOverlay = Child(StreamSheetComponent.self)
let topItem = Child(environment: Empty.self)
// let viewerCounter = Child(ParticipantsComponent.self)
let fullscreenBottomItem = Child(environment: Empty.self)
let buttonsRow = Child(environment: Empty.self)
let activatePictureInPicture = StoredActionSlot(Action<Void>.self)
let deactivatePictureInPicture = StoredActionSlot(Void.self)
@ -346,7 +350,7 @@ public final class MediaStreamComponent: CombinedComponent {
? (context.availableSize.width - videoInset * 2) / 16 * 9
: context.state.videoSize?.height ?? (min(context.availableSize.width, context.availableSize.height) - videoInset * 2) / 16 * 9
let bottomPadding = 40 + environment.safeInsets.bottom
let sheetHeight: CGFloat = isFullscreen
let requiredSheetHeight: CGFloat = isFullscreen
? context.availableSize.height
: (44 + videoHeight + 40 + 69 + 16 + 32 + 70 + bottomPadding)
@ -357,14 +361,14 @@ public final class MediaStreamComponent: CombinedComponent {
safeAreaTopInView = context.view.safeAreaInsets.top
}
let isFullyDragged = context.availableSize.height - sheetHeight + state.dismissOffset - safeAreaTopInView < 30
let isFullyDragged = context.availableSize.height - requiredSheetHeight + state.dismissOffset - safeAreaTopInView < 30
var dragOffset = context.state.dismissOffset
if isFullyDragged {
dragOffset = max(context.state.dismissOffset, sheetHeight - context.availableSize.height + safeAreaTopInView)
dragOffset = max(context.state.dismissOffset, requiredSheetHeight - context.availableSize.height + safeAreaTopInView)
}
let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - sheetHeight + dragOffset)
let dismissTapAreaHeight = isFullscreen ? 0 : (context.availableSize.height - requiredSheetHeight + dragOffset)
let dismissTapComponent = dismissTapComponent.update(
component: Rectangle(color: .red.withAlphaComponent(0)),
availableSize: CGSize(width: context.availableSize.width, height: dismissTapAreaHeight),
@ -742,23 +746,31 @@ public final class MediaStreamComponent: CombinedComponent {
} else {
if isFullyDragged || state.initialOffset != 0 {
state.updateDismissOffset(value: 0.0, interactive: false)
state.updateDismissOffset(value: 0.0, interactive: false)
} else {
activatePictureInPicture.invoke(Action {
if state.isPictureInPictureSupported {
activatePictureInPicture.invoke(Action {
guard let controller = controller() as? MediaStreamComponentController else {
return
}
controller.dismiss(closing: false, manual: true)
})
} else {
guard let controller = controller() as? MediaStreamComponentController else {
return
}
controller.dismiss(closing: false, manual: true)
})
}
// let _ = call.leave(terminateIfPossible: false)
}
}
} else {
if isFullyDragged {
state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false)
state.updateDismissOffset(value: requiredSheetHeight - availableSize.height + safeAreaTop, interactive: false)
} else {
if velocity.y < -200 {
// Expand
state.updateDismissOffset(value: sheetHeight - availableSize.height + safeAreaTop, interactive: false)
state.updateDismissOffset(value: requiredSheetHeight - availableSize.height + safeAreaTop, interactive: false)
} else {
state.updateDismissOffset(value: 0.0, interactive: false)
}
@ -792,7 +804,7 @@ public final class MediaStreamComponent: CombinedComponent {
.gesture(.pan(onPanGesture))
)
if !isFullscreen {
if !isFullscreen || state.isFullscreen {
let imageRenderScale = UIScreen.main.scale
let bottomComponent = AnyComponent(ButtonsRowComponent(
bottomInset: environment.safeInsets.bottom,
@ -872,10 +884,10 @@ public final class MediaStreamComponent: CombinedComponent {
)),
action: { [weak state] in
guard let state = state else { return }
guard state.videoIsPlayable else {
state.isFullscreen = false
return
}
// guard state.videoIsPlayable else {
// state.isFullscreen = false
// return
// }
if let controller = controller() as? MediaStreamComponentController {
// guard let _ = state.videoSize else { return }
state.isFullscreen.toggle()
@ -907,25 +919,33 @@ public final class MediaStreamComponent: CombinedComponent {
).minSize(CGSize(width: 44.0, height: 44.0)))
))
let sheetHeight: CGFloat = max(requiredSheetHeight - dragOffset, requiredSheetHeight)
let topOffset: CGFloat = isFullscreen
? max(context.state.dismissOffset, 0)
: (context.availableSize.height - requiredSheetHeight + dragOffset)
let sheet = sheet.update(
component: StreamSheetComponent(
topComponent: AnyComponent(navigationComponent),
bottomButtonsRow: bottomComponent,
topOffset: context.availableSize.height - sheetHeight + dragOffset,
sheetHeight: max(sheetHeight - dragOffset, sheetHeight),
topOffset: topOffset,
sheetHeight: sheetHeight,
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 ?? 1) - 1,
videoHeight: videoHeight
videoHeight: videoHeight,
isFullscreen: isFullscreen,
fullscreenTopComponent: AnyComponent(navigationComponent),
fullscreenBottomComponent: bottomComponent
),
availableSize: context.availableSize,
transition: context.transition
)
let sheetOffset: CGFloat = context.availableSize.height - sheetHeight + dragOffset
let sheetPosition = sheetOffset + sheetHeight / 2
let sheetOffset: CGFloat = context.availableSize.height - requiredSheetHeight + dragOffset
let sheetPosition = sheetOffset + requiredSheetHeight / 2
// Sheet underneath the video when in modal sheet
context.add(sheet
.position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2))
@ -935,18 +955,26 @@ public final class MediaStreamComponent: CombinedComponent {
if isFullscreen {
videoPos = context.availableSize.height / 2 + dragOffset
} else {
videoPos = sheetPosition - sheetHeight / 2 + videoHeight / 2 + 50 + 12
videoPos = sheetPosition - requiredSheetHeight / 2 + videoHeight / 2 + 50 + 12
}
context.add(video
.position(CGPoint(x: context.availableSize.width / 2.0, y: videoPos))
)
} else {
context.add(video
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2 + dragOffset)
))
}
if isFullscreen {
//
//
//
var availableWidth: CGFloat { context.availableSize.width }
var contentHeight: CGFloat { 44.0 }
// print(topItem)
// let size = context.availableSize
let topItem = topItem.update(
component: AnyComponent(navigationComponent),
availableSize: CGSize(width: availableWidth, height: contentHeight),
transition: context.transition
)
let fullScreenToolbarComponent = AnyComponent(ToolbarComponent(
bottomInset: environment.safeInsets.bottom,
sideInset: environment.safeInsets.left,
@ -981,26 +1009,102 @@ public final class MediaStreamComponent: CombinedComponent {
).minSize(CGSize(width: 64.0, height: 80)))/* : nil*/,
centerItem: infoItem
))
let fullScreenOverlayComponent = fullscreenOverlay.update(
let buttonsRow = buttonsRow.update(
component: bottomComponent,
availableSize: CGSize(width: availableWidth, height: contentHeight),
transition: context.transition
)
let fullscreenBottomItem = fullscreenBottomItem.update(
component: fullScreenToolbarComponent,
availableSize: CGSize(width: availableWidth, height: contentHeight),
transition: context.transition
)
context.add(topItem
.position(CGPoint(x: topItem.size.width / 2.0, y: topOffset + (isFullscreen ? topItem.size.height / 2.0 : 32)))
.opacity((!isFullscreen || state.displayUI) ? 1 : 0)
// .animation(key: "position")
)
context.add(buttonsRow
.opacity(isFullscreen ? 0 : 1)
// .animation(key: "opacity")
.position(CGPoint(x: buttonsRow.size.width / 2, y: sheetHeight - 50 / 2 + topOffset - bottomPadding))
)
context.add(fullscreenBottomItem
.opacity((isFullscreen && state.displayUI) ? 1 : 0)
// .animation(key: "opacity")
.position(CGPoint(x: fullscreenBottomItem.size.width / 2, y: context.availableSize.height - fullscreenBottomItem.size.height / 2 + topOffset - 0.0))
)
//
//
//
} else {
let fullScreenToolbarComponent = AnyComponent(ToolbarComponent(
bottomInset: environment.safeInsets.bottom,
sideInset: environment.safeInsets.left,
leftItem: AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: "Chat/Input/Accessory Panels/MessageSelectionForward",
tintColor: .white
)),
action: {
guard let controller = controller() as? MediaStreamComponentController else {
return
}
controller.presentShare()
}
).minSize(CGSize(width: 64.0, height: 80))),
rightItem: /*state.hasVideo ?*/ AnyComponent(Button(
content: AnyComponent(BundleIconComponent(
name: isFullscreen ? "Media Gallery/Minimize" : "Media Gallery/Fullscreen",
tintColor: .white
)),
action: {
state.isFullscreen = false
state.prevFullscreenOrientation = UIDevice.current.orientation
if let controller = controller() as? MediaStreamComponentController {
if canEnforceOrientation {
controller.updateOrientation(orientation: .portrait)
} else {
state.updated(transition: .easeInOut(duration: 0.25)) // updated(.easeInOut(duration: 0.3))
}
}
}
).minSize(CGSize(width: 64.0, height: 80)))/* : nil*/,
centerItem: infoItem
))
let fullScreenOverlayComponent = sheet.update(
component: StreamSheetComponent(
topComponent: AnyComponent(navigationComponent),
bottomButtonsRow: fullScreenToolbarComponent,
topOffset: /*context.availableSize.height - sheetHeight +*/ max(context.state.dismissOffset, 0),
sheetHeight: context.availableSize.height,// max(sheetHeight - context.state.dismissOffset, sheetHeight),
backgroundColor: /*isFullscreen ? .clear : */ (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
backgroundColor: isFullscreen ? .clear : (isFullyDragged ? fullscreenBackgroundColor : panelBackgroundColor),
bottomPadding: 0,
participantsCount: -1,
isFullyExtended: isFullyDragged,
deviceCornerRadius: ((controller() as? MediaStreamComponentController)?.validLayout?.deviceMetrics.screenCornerRadius ?? 1) - 1,
videoHeight: videoHeight
videoHeight: videoHeight,
isFullscreen: isFullscreen,
fullscreenTopComponent: AnyComponent(navigationComponent),
fullscreenBottomComponent: fullScreenToolbarComponent
),
availableSize: context.availableSize,
transition: context.transition
)
context.add(fullScreenOverlayComponent
.position(.init(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2))
.opacity(state.displayUI ? 1 : 0)
)
context.add(video
.position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2 + dragOffset)
))
}
return context.availableSize
@ -1446,11 +1550,21 @@ final class StreamTitleComponent: Component {
func update(component: StreamTitleComponent, availableSize: CGSize, transition: Transition) -> CGSize {
let liveIndicatorWidth: CGFloat = 40
self.titleLabel.text = component.text
let currentText = self.titleLabel.text
if currentText != component.text {
if currentText?.isEmpty == false {
UIView.transition(with: self.titleLabel, duration: 0.2) {
self.titleLabel.text = component.text
self.titleLabel.invalidateIntrinsicContentSize()
}
} else {
self.titleLabel.text = component.text
self.titleLabel.invalidateIntrinsicContentSize()
}
}
self.titleLabel.font = Font.semibold(17.0)
self.titleLabel.textColor = .white
self.titleLabel.numberOfLines = 1
self.titleLabel.invalidateIntrinsicContentSize()
let textSize = CGSize(width: min(availableSize.width - 4 - liveIndicatorWidth, self.titleLabel.intrinsicContentSize.width), height: availableSize.height)
@ -1483,7 +1597,13 @@ final class StreamTitleComponent: Component {
let size = CGSize(width: textSize.width + sideInset * 2.0, height: textSize.height)
let textFrame = CGRect(origin: CGPoint(x: sideInset, y: floor((size.height - textSize.height) / 2.0)), size: textSize)
// self.textView.frame = textFrame
self.updateTitleFadeLayer(textFrame: textFrame)
if currentText?.isEmpty == false {
UIView.transition(with: self.titleLabel, duration: 0.2) {
self.updateTitleFadeLayer(textFrame: textFrame)
}
} else {
self.updateTitleFadeLayer(textFrame: textFrame)
}
liveIndicatorView.frame = CGRect(origin: CGPoint(x: textFrame.maxX + 6.0, y: /*floorToScreenPixels((size.height - textSize.height) / 2.0 - 2) + 1.0*/textFrame.midY - 22 / 2), size: .init(width: 40, height: 22))
self.liveIndicatorView.toggle(isLive: component.isActive)
@ -1563,7 +1683,11 @@ private final class NavigationBarComponent: CombinedComponent {
let contentHeight: CGFloat = 44.0
let size = CGSize(width: context.availableSize.width, height: context.component.topInset + contentHeight)
let background = background.update(component: Rectangle(color: UIColor(white: 0.0, alpha: context.component.backgroundVisible ? 0.5 : 0)), availableSize: CGSize(width: size.width, height: size.height), transition: context.transition)
let background = background.update(
component: Rectangle(color: UIColor(white: 0.0, alpha: 0.5/*context.component.backgroundVisible ? 0.5 : 0*/)),
availableSize: CGSize(width: size.width, height: size.height),
transition: context.transition
)
let leftItem = context.component.leftItem.flatMap { leftItemComponent in
return leftItem.update(
@ -1600,6 +1724,8 @@ private final class NavigationBarComponent: CombinedComponent {
context.add(background
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
.opacity(context.component.backgroundVisible ? 1 : 0)
.animation(key: "opacity")
)
var centerLeftInset = sideInset
@ -1660,7 +1786,7 @@ private final class OriginInfoComponent: CombinedComponent {
component: ParticipantsComponent(
count: context.component.participantsCount,
showsSubtitle: true,
fontSize: 24
fontSize: 18.0
),
availableSize: CGSize(width: context.availableSize.width, height: context.availableSize.height),
transition: context.transition
@ -1671,7 +1797,7 @@ private final class OriginInfoComponent: CombinedComponent {
size.height = min(size.height, context.availableSize.height)
context.add(viewerCounter
.position(CGPoint(x: size.width / 2.0, y: viewerCounter.size.height / 2.0))
.position(CGPoint(x: size.width / 2.0, y: (context.availableSize.height - viewerCounter.size.height) / 2.0))
)
return size

View File

@ -120,7 +120,9 @@ final class MediaStreamVideoComponent: Component {
private var noSignalTimer: Foundation.Timer?
private var noSignalTimeout: Bool = false
private let maskGradientLayer = CAGradientLayer()
private let videoBlurGradientMask = CAGradientLayer()
private let videoBlurSolidMask = CALayer()
private var wasVisible = true
private var borderShimmer = StandaloneShimmerEffect()
private let shimmerBorderLayer = CALayer()
@ -168,6 +170,8 @@ final class MediaStreamVideoComponent: Component {
deinit {
avatarDisposable?.dispose()
frameInputDisposable?.dispose()
self.x?.invalidate()
self.x = nil
}
public func matches(tag: Any) -> Bool {
@ -316,10 +320,14 @@ final class MediaStreamVideoComponent: Component {
UIView.animate(withDuration: 0.3) {
videoBlurView.alpha = 1
}
self.maskGradientLayer.type = .radial
self.maskGradientLayer.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor]
self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
self.maskGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
self.videoBlurGradientMask.type = .radial
self.videoBlurGradientMask.colors = [UIColor(rgb: 0x000000, alpha: 0.5).cgColor, UIColor(rgb: 0xffffff, alpha: 0.0).cgColor]
self.videoBlurGradientMask.startPoint = CGPoint(x: 0.5, y: 0.5)
self.videoBlurGradientMask.endPoint = CGPoint(x: 1.0, y: 1.0)
self.videoBlurSolidMask.backgroundColor = UIColor.black.cgColor
self.videoBlurGradientMask.addSublayer(videoBlurSolidMask)
}
if let videoView = self.videoRenderingContext.makeView(input: input, blur: false, forceSampleBufferDisplayLayer: true) {
@ -431,6 +439,14 @@ final class MediaStreamVideoComponent: Component {
let videoSize: CGSize
let videoCornerRadius: CGFloat = component.isFullscreen ? 0 : 10
let videoFrameUpdateTransition: Transition
if self.wasFullscreen != component.isFullscreen {
videoFrameUpdateTransition = transition
} else {
videoFrameUpdateTransition = transition.withAnimation(.none)
}
if let videoView = self.videoView {
if videoView.bounds.size.width > 0,
videoView.alpha > 0,
@ -482,15 +498,10 @@ final class MediaStreamVideoComponent: Component {
videoView.clipsToBounds = true
videoView.layer.cornerRadius = videoCornerRadius
let videoFrameUpdateTransition: Transition
if self.wasFullscreen != component.isFullscreen {
videoFrameUpdateTransition = transition
} else {
videoFrameUpdateTransition = transition.withAnimation(.none)
}
self.wasFullscreen = component.isFullscreen
let newVideoFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
videoFrameUpdateTransition.setFrame(view: videoView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize), completion: nil)
videoFrameUpdateTransition.setFrame(view: videoView, frame: newVideoFrame, completion: nil)
if let videoBlurView = self.videoBlurView {
@ -505,27 +516,51 @@ final class MediaStreamVideoComponent: Component {
// videoBlurView.frame = videoView.frame.insetBy(dx: -69 * aspect, dy: -69)
}
if !component.isFullscreen {
videoBlurView.layer.mask = maskGradientLayer
} else {
videoBlurView.layer.mask = nil
}
videoBlurView.layer.mask = videoBlurGradientMask
self.maskGradientLayer.frame = videoBlurView.bounds
if !component.isFullscreen {
transition.setAlpha(layer: videoBlurSolidMask, alpha: 0)
// if videoBlurView.layer.mask !== videoBlurGradientMask {
// UIView.transition(with: videoBlurView, duration: transition.animation.isImmediate ? 0.0 : 0.3) { [self] in
// videoBlurView.layer.mask = videoBlurGradientMask
// }
// }
// videoBlurView.layer.mask = maskGradientLayer
} else {
transition.setAlpha(layer: videoBlurSolidMask, alpha: 1)
// if videoBlurView.layer.mask != nil {
// UIView.transition(with: videoBlurView, duration: transition.animation.isImmediate ? 0.0 : 0.3) {
// videoBlurView.layer.mask = nil
// }
// }
}
//
videoFrameUpdateTransition.setFrame(layer: self.videoBlurGradientMask, frame: videoBlurView.bounds)
videoFrameUpdateTransition.setFrame(layer: self.videoBlurSolidMask, frame: self.videoBlurGradientMask.bounds)
}
} else {
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 = videoCornerRadius
let loadingBlurViewFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
videoFrameUpdateTransition.setFrame(view: loadingBlurView, frame: loadingBlurViewFrame)
// loadingBlurView.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - videoSize.width) / 2.0), y: floor((availableSize.height - videoSize.height) / 2.0)), size: videoSize)
placeholderView.frame = loadingBlurView.frame
placeholderView.layer.cornerRadius = videoCornerRadius
videoFrameUpdateTransition.setCornerRadius(layer: loadingBlurView.layer, cornerRadius: videoCornerRadius)
// loadingBlurView.layer.cornerRadius = videoCornerRadius
videoFrameUpdateTransition.setFrame(view: placeholderView, frame: loadingBlurViewFrame)
// placeholderView.frame = loadingBlurView.frame
videoFrameUpdateTransition.setCornerRadius(layer: placeholderView.layer, cornerRadius: videoCornerRadius)
// placeholderView.layer.cornerRadius = videoCornerRadius
placeholderView.clipsToBounds = true
placeholderView.subviews.forEach { $0.frame = placeholderView.bounds }
placeholderView.subviews.forEach {
videoFrameUpdateTransition.setFrame(view: $0, frame: placeholderView.bounds)
// $0.frame = placeholderView.bounds
}
shimmerBorderLayer.frame = loadingBlurView.bounds
videoFrameUpdateTransition.setFrame(layer: shimmerBorderLayer, 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)
@ -607,26 +642,58 @@ final class MediaStreamVideoComponent: Component {
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
if let videoView = self.videoView, let presentation = videoView.snapshotView(afterScreenUpdates: false) {
self.addSubview(presentation)
presentation.frame = videoView.frame
let presentationParent = self.window ?? self
presentationParent.addSubview(presentation)
presentation.frame = presentationParent.convert(videoView.frame, from: self)
if let callId = self.component?.call.peerId.id.description {
lastFrame[callId] = presentation
}
videoView.alpha = 0
UIView.animate(withDuration: 0.07, delay: 0.07, animations: {
presentation.alpha = 0
}, completion: { _ in
presentation.removeFromSuperview()
})
lastPresentation?.removeFromSuperview()
lastPresentation = presentation
// UIView.animate(withDuration: 0.04, delay: 0.04, animations: {
// presentation.alpha = 0
// }, completion: { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
self.lastPresentation?.removeFromSuperview()
self.lastPresentation = nil
self.x?.invalidate()
self.x = nil
}
// })
}
UIView.animate(withDuration: 0.1) { [self] in
videoBlurView?.alpha = 0
}
// UIApplication.shared.windows.first?.windowLevel == .normal
// TODO: make safe
UIApplication.shared.windows.first?/*(where: { $0.layer !== (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.keyWindow?.layer })?*/.layer.cornerRadius = 10// (where: { !($0 is NativeWindow)*/ })
UIApplication.shared.windows.first?.layer.masksToBounds = true
// UIApplication.shared.windows.first?.subviews[0].subviews[0].subviews[1].subviews[0].subviews[0].backgroundColor = .red
// UIApplication.shared.windows.first?.subviews[0].subviews[0].subviews[1].subviews[0].subviews[0].setNeedsDisplay()
// UIApplication.shared.windows.first?.subviews[0].subviews[0].subviews[1].backgroundColor = .red
// UIApplication.shared.windows.first?.subviews[0].subviews[0].subviews[1].setNeedsDisplay()
self.x?.invalidate()
let x = CADisplayLink(target: self, selector: #selector(observePiPWindow))
x.add(to: .main, forMode: .default)
self.x = x
}
var lastPresentation: UIView?
var x: CADisplayLink?
@objc func observePiPWindow() {
let pipViewDidBecomeVisible = (UIApplication.shared.windows.first?.layer.animationKeys()?.count ?? 0) > 0
if pipViewDidBecomeVisible {
lastPresentation?.removeFromSuperview()
lastPresentation = nil
self.x?.invalidate()
self.x = nil
}
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {

View File

@ -20,6 +20,10 @@ final class StreamSheetComponent: CombinedComponent {
let deviceCornerRadius: CGFloat
let videoHeight: CGFloat
let isFullscreen: Bool
let fullscreenTopComponent: AnyComponent<Empty>
let fullscreenBottomComponent: AnyComponent<Empty>
init(
topComponent: AnyComponent<Empty>,
bottomButtonsRow: AnyComponent<Empty>,
@ -30,10 +34,13 @@ final class StreamSheetComponent: CombinedComponent {
participantsCount: Int,
isFullyExtended: Bool,
deviceCornerRadius: CGFloat,
videoHeight: CGFloat
videoHeight: CGFloat,
isFullscreen: Bool,
fullscreenTopComponent: AnyComponent<Empty>,
fullscreenBottomComponent: AnyComponent<Empty>
) {
self.topComponent = topComponent
self.bottomButtonsRow = bottomButtonsRow
self.topComponent = nil // topComponent
self.bottomButtonsRow = nil // bottomButtonsRow
self.topOffset = topOffset
self.sheetHeight = sheetHeight
self.backgroundColor = backgroundColor
@ -42,6 +49,10 @@ final class StreamSheetComponent: CombinedComponent {
self.isFullyExtended = isFullyExtended
self.deviceCornerRadius = deviceCornerRadius
self.videoHeight = videoHeight
self.isFullscreen = isFullscreen
self.fullscreenTopComponent = fullscreenTopComponent
self.fullscreenBottomComponent = fullscreenBottomComponent
}
static func ==(lhs: StreamSheetComponent, rhs: StreamSheetComponent) -> Bool {
@ -75,6 +86,19 @@ final class StreamSheetComponent: CombinedComponent {
if lhs.videoHeight != rhs.videoHeight {
return false
}
if lhs.isFullscreen != rhs.isFullscreen {
return false
}
if lhs.fullscreenTopComponent != rhs.fullscreenTopComponent {
return false
}
if lhs.fullscreenBottomComponent != rhs.fullscreenBottomComponent {
return false
}
return true
}
@ -173,7 +197,7 @@ final class StreamSheetComponent: CombinedComponent {
)
}
// TODO: replace
let isFullscreen = context.component.participantsCount == -1
let isFullscreen = context.component.isFullscreen // context.component.participantsCount == -1
context.add(background
.position(CGPoint(x: size.width / 2.0, y: topOffset + context.component.sheetHeight / 2))