[WIP] Call UI

This commit is contained in:
Isaac 2023-12-08 22:33:07 +04:00
parent a6b140d599
commit d1e9e04dc1
27 changed files with 847 additions and 170 deletions

View File

@ -443,6 +443,7 @@ official_apple_pay_merchants = [
"merchant.org.telegram.billinenet.test",
"merchant.org.telegram.billinenet.prod",
"merchant.org.telegram.portmone.test",
"merchant.org.telegram.portmone.prod",
"merchant.org.telegram.ecommpay.test",
]

View File

@ -29,7 +29,8 @@ public final class ViewController: UIViewController {
shortName: "Emma",
avatarImage: UIImage(named: "test"),
audioOutput: .internalSpeaker,
isMicrophoneMuted: false,
isLocalAudioMuted: false,
isRemoteAudioMuted: false,
localVideo: nil,
remoteVideo: nil,
isRemoteBatteryLow: false
@ -142,7 +143,7 @@ public final class ViewController: UIViewController {
self.callState.lifecycleState = .terminated(PrivateCallScreen.State.TerminatedState(duration: 82.0))
self.callState.remoteVideo = nil
self.callState.localVideo = nil
self.callState.isMicrophoneMuted = false
self.callState.isLocalAudioMuted = false
self.callState.isRemoteBatteryLow = false
self.update(transition: .spring(duration: 0.4))
}
@ -150,8 +151,9 @@ public final class ViewController: UIViewController {
guard let self else {
return
}
self.callState.isMicrophoneMuted = !self.callState.isMicrophoneMuted
self.update(transition: .spring(duration: 0.4))
//self.callState.isLocalAudioMuted = !self.callState.isLocalAudioMuted
//self.update(transition: .spring(duration: 0.4))
self.callScreenView?.beginPictureInPicture()
}
callScreenView.closeAction = { [weak self] in
guard let self else {
@ -163,17 +165,17 @@ public final class ViewController: UIViewController {
private func update(transition: Transition) {
if let (size, insets) = self.currentLayout {
self.update(size: size, insets: insets, transition: transition)
self.update(size: size, insets: insets, interfaceOrientation: self.interfaceOrientation, transition: transition)
}
}
private func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
private func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, transition: Transition) {
guard let callScreenView = self.callScreenView else {
return
}
transition.setFrame(view: callScreenView, frame: CGRect(origin: CGPoint(), size: size))
callScreenView.update(size: size, insets: insets, screenCornerRadius: UIScreen.main.displayCornerRadius, state: self.callState, transition: transition)
callScreenView.update(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: UIScreen.main.displayCornerRadius, state: self.callState, transition: transition)
}
override public func viewWillLayoutSubviews() {
@ -190,7 +192,7 @@ public final class ViewController: UIViewController {
if let currentLayout = self.currentLayout, currentLayout == (size, insets) {
} else {
self.currentLayout = (size, insets)
self.update(size: size, insets: insets, transition: transition)
self.update(size: size, insets: insets, interfaceOrientation: self.interfaceOrientation, transition: transition)
}
}

View File

@ -507,7 +507,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
guard let presentationInterfaceState = self.presentationInterfaceState else {
return 0.0
}
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false)
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
public func setCaption(_ caption: NSAttributedString?) {

View File

@ -49,6 +49,8 @@ private extension Transition.Animation.Curve {
switch self {
case .easeInOut:
return CAMediaTimingFunction(name: .easeInEaseOut)
case .linear:
return CAMediaTimingFunction(name: .linear)
case let .custom(a, b, c, d):
return CAMediaTimingFunction(controlPoints: a, b, c, d)
case .spring:
@ -72,6 +74,7 @@ public struct Transition {
public enum Curve {
case easeInOut
case spring
case linear
case custom(Float, Float, Float, Float)
public func solve(at offset: CGFloat) -> CGFloat {
@ -80,6 +83,8 @@ public struct Transition {
return listViewAnimationCurveEaseInOut(offset)
case .spring:
return listViewAnimationCurveSystem(offset)
case .linear:
return offset
case let .custom(c1x, c1y, c2x, c2y):
return bezierPoint(CGFloat(c1x), CGFloat(c1y), CGFloat(c2x), CGFloat(c2y), offset)
}

View File

@ -7,7 +7,7 @@ public extension Transition.Animation.Curve {
init(_ curve: ContainedViewLayoutTransitionCurve) {
switch curve {
case .linear:
self = .easeInOut
self = .linear
case .easeInOut:
self = .easeInOut
case let .custom(a, b, c, d):
@ -21,6 +21,8 @@ public extension Transition.Animation.Curve {
var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve {
switch self {
case .linear:
return .linear
case .easeInOut:
return .easeInOut
case .spring:

View File

@ -1776,7 +1776,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega
}
contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight))
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
contentUnscaledSize = preferredSize
}
} else {
@ -1786,7 +1786,7 @@ final class ContextControllerNode: ViewControllerTracingNode, UIScrollViewDelega
let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset
contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight))
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
contentUnscaledSize = preferredSize
}
}

View File

@ -183,7 +183,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
self.controller.containerLayoutUpdated(
ContainerViewLayout(
size: size,
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact),
metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil),
deviceMetrics: parentLayout.deviceMetrics,
intrinsicInsets: UIEdgeInsets(),
safeInsets: UIEdgeInsets(),
@ -765,7 +765,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
defaultContentSize.height = min(defaultContentSize.height, 460.0)
let contentSize: CGSize
if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) {
contentSize = preferredSize
} else if let storedContentHeight = contentNode.storedContentHeight {
contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight)

View File

@ -69,13 +69,15 @@ public func childWindowHostView(parent: UIView) -> WindowHostView {
let hostView = WindowHostView(containerView: view, eventView: view, isRotating: {
return false
}, systemUserInterfaceStyle: .single(.light), updateSupportedInterfaceOrientations: { orientations in
}, systemUserInterfaceStyle: .single(.light), currentInterfaceOrientation: {
return .portrait
}, updateSupportedInterfaceOrientations: { orientations in
}, updateDeferScreenEdgeGestures: { edges in
}, updatePrefersOnScreenNavigationHidden: { value in
})
view.updateSize = { [weak hostView] size in
hostView?.updateSize?(size, 0.0)
hostView?.updateSize?(size, 0.0, .portrait)
}
view.layoutSubviewsEvent = { [weak hostView] in

View File

@ -23,15 +23,18 @@ public enum ContainerViewLayoutSizeClass {
public struct LayoutMetrics: Equatable {
public let widthClass: ContainerViewLayoutSizeClass
public let heightClass: ContainerViewLayoutSizeClass
public let orientation: UIInterfaceOrientation?
public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass) {
public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass, orientation: UIInterfaceOrientation?) {
self.widthClass = widthClass
self.heightClass = heightClass
self.orientation = orientation
}
public init() {
self.widthClass = .compact
self.heightClass = .compact
self.orientation = nil
}
}

View File

@ -22,7 +22,7 @@ public final class ContextContentContainerNode: ASDisplayNode {
transition.updateBounds(node: controller, bounds: CGRect(origin: CGPoint(), size: size))
transition.updateTransformScale(node: controller, scale: scaledSize.width / size.width)
controller.updateLayout(size: size, transition: transition)
controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition)
controller.controller.containerLayoutUpdated(ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: transition)
}
}
}

View File

@ -12,6 +12,24 @@ private let defaultOrientations: UIInterfaceOrientationMask = {
}
}()
func getCurrentViewInterfaceOrientation(view: UIView) -> UIInterfaceOrientation {
var orientation: UIInterfaceOrientation = .portrait
if #available(iOS 13.0, *) {
if let window = view as? UIWindow {
if let windowScene = window.windowScene {
orientation = windowScene.interfaceOrientation
}
} else {
if let windowScene = view.window?.windowScene {
orientation = windowScene.interfaceOrientation
}
}
} else {
orientation = UIApplication.shared.statusBarOrientation
}
return orientation
}
public enum WindowUserInterfaceStyle {
case light
case dark
@ -74,7 +92,7 @@ private final class WindowRootViewController: UIViewController, UIViewController
private var registeredForPreviewing = false
var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)?
var transitionToSize: ((CGSize, Double) -> Void)?
var transitionToSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)?
private var _systemUserInterfaceStyle = ValuePromise<WindowUserInterfaceStyle>(ignoreRepeated: true)
var systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError> {
@ -182,8 +200,10 @@ private final class WindowRootViewController: UIViewController, UIViewController
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let orientation = getCurrentViewInterfaceOrientation(view: self.view)
UIView.performWithoutAnimation {
self.transitionToSize?(size, coordinator.transitionDuration)
self.transitionToSize?(size, coordinator.transitionDuration, orientation)
}
}
@ -372,18 +392,29 @@ public func nativeWindowHostView() -> (UIWindow & WindowHost, WindowHostView) {
rootViewController.view.frame = CGRect(origin: CGPoint(), size: window.bounds.size)
rootViewController.viewDidAppear(false)
let hostView = WindowHostView(containerView: rootViewController.view, eventView: window, isRotating: {
return window.isRotating()
}, systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle, updateSupportedInterfaceOrientations: { orientations in
rootViewController.orientations = orientations
}, updateDeferScreenEdgeGestures: { edges in
rootViewController.gestureEdges = edges
}, updatePrefersOnScreenNavigationHidden: { value in
rootViewController.prefersOnScreenNavigationHidden = value
})
let hostView = WindowHostView(
containerView: rootViewController.view,
eventView: window,
isRotating: {
return window.isRotating()
},
systemUserInterfaceStyle: rootViewController.systemUserInterfaceStyle,
currentInterfaceOrientation: {
return getCurrentViewInterfaceOrientation(view: window)
},
updateSupportedInterfaceOrientations: { orientations in
rootViewController.orientations = orientations
},
updateDeferScreenEdgeGestures: { edges in
rootViewController.gestureEdges = edges
},
updatePrefersOnScreenNavigationHidden: { value in
rootViewController.prefersOnScreenNavigationHidden = value
}
)
rootViewController.transitionToSize = { [weak hostView] size, duration in
hostView?.updateSize?(size, duration)
rootViewController.transitionToSize = { [weak hostView] size, duration, orientation in
hostView?.updateSize?(size, duration, orientation)
}
window.updateSize = { _ in

View File

@ -156,6 +156,7 @@ public final class WindowHostView {
public let eventView: UIView
public let isRotating: () -> Bool
public let systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>
public let currentInterfaceOrientation: () -> UIInterfaceOrientation
let updateSupportedInterfaceOrientations: (UIInterfaceOrientationMask) -> Void
let updateDeferScreenEdgeGestures: (UIRectEdge) -> Void
@ -166,7 +167,7 @@ public final class WindowHostView {
var addGlobalPortalHostViewImpl: ((PortalSourceView) -> Void)?
var presentNative: ((UIViewController) -> Void)?
var nativeController: (() -> UIViewController?)?
var updateSize: ((CGSize, Double) -> Void)?
var updateSize: ((CGSize, Double, UIInterfaceOrientation) -> Void)?
var layoutSubviews: (() -> Void)?
var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)?
var isUpdatingOrientationLayout = false
@ -178,11 +179,12 @@ public final class WindowHostView {
var forEachController: (((ContainableController) -> Void) -> Void)?
var getAccessibilityElements: (() -> [Any]?)?
init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) {
init(containerView: UIView, eventView: UIView, isRotating: @escaping () -> Bool, systemUserInterfaceStyle: Signal<WindowUserInterfaceStyle, NoError>, currentInterfaceOrientation: @escaping () -> UIInterfaceOrientation, updateSupportedInterfaceOrientations: @escaping (UIInterfaceOrientationMask) -> Void, updateDeferScreenEdgeGestures: @escaping (UIRectEdge) -> Void, updatePrefersOnScreenNavigationHidden: @escaping (Bool) -> Void) {
self.containerView = containerView
self.eventView = eventView
self.isRotating = isRotating
self.systemUserInterfaceStyle = systemUserInterfaceStyle
self.currentInterfaceOrientation = currentInterfaceOrientation
self.updateSupportedInterfaceOrientations = updateSupportedInterfaceOrientations
self.updateDeferScreenEdgeGestures = updateDeferScreenEdgeGestures
self.updatePrefersOnScreenNavigationHidden = updatePrefersOnScreenNavigationHidden
@ -220,11 +222,11 @@ public extension UIView {
}
}
private func layoutMetricsForScreenSize(_ size: CGSize) -> LayoutMetrics {
private func layoutMetricsForScreenSize(size: CGSize, orientation: UIInterfaceOrientation?) -> LayoutMetrics {
if size.width > 690.0 && size.height > 650.0 {
return LayoutMetrics(widthClass: .regular, heightClass: .regular)
return LayoutMetrics(widthClass: .regular, heightClass: .regular, orientation: orientation)
} else {
return LayoutMetrics(widthClass: .compact, heightClass: .compact)
return LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: orientation)
}
}
@ -350,11 +352,13 @@ public class Window1 {
self.keyboardViewManager = nil
}
let isLandscape = boundsSize.width > boundsSize.height
let isLandscape = boundsSize.width > boundsSize.height
let safeInsets = self.deviceMetrics.safeInsets(inLandscape: isLandscape)
let onScreenNavigationHeight = self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight)
self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(boundsSize), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning)
let orientation: UIInterfaceOrientation = self.hostView.currentInterfaceOrientation()
self.windowLayout = WindowLayout(size: boundsSize, metrics: layoutMetricsForScreenSize(size: boundsSize, orientation: orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: self.forceInCallStatusBarText, inputHeight: 0.0, safeInsets: safeInsets, onScreenNavigationHeight: onScreenNavigationHeight, upperKeyboardInputPositionBound: nil, inVoiceOver: UIAccessibility.isVoiceOverRunning)
self.updatingLayout = UpdatingLayout(layout: self.windowLayout, transition: .immediate)
self.presentationContext = PresentationContext()
self.overlayPresentationContext = GlobalOverlayPresentationContext(statusBarHost: statusBarHost, parentView: self.hostView.containerView)
@ -406,8 +410,8 @@ public class Window1 {
self?.presentNative(controller)
}
self.hostView.updateSize = { [weak self] size, duration in
self?.updateSize(size, duration: duration)
self.hostView.updateSize = { [weak self] size, duration, orientation in
self?.updateSize(size, duration: duration, orientation: orientation)
}
self.hostView.layoutSubviews = { [weak self] in
@ -807,14 +811,14 @@ public class Window1 {
return self.viewController?.view.hitTest(point, with: event)
}
func updateSize(_ value: CGSize, duration: Double) {
func updateSize(_ value: CGSize, duration: Double, orientation: UIInterfaceOrientation) {
let transition: ContainedViewLayoutTransition
if !duration.isZero {
transition = .animated(duration: duration, curve: .easeInOut)
} else {
transition = .immediate
}
self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(value), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) }
self.updateLayout { $0.update(size: value, metrics: layoutMetricsForScreenSize(size: value, orientation: orientation), safeInsets: self.deviceMetrics.safeInsets(inLandscape: value.width > value.height), forceInCallStatusBarText: self.forceInCallStatusBarText, transition: transition, overrideTransition: true) }
if let statusBarHost = self.statusBarHost, !statusBarHost.isApplicationInForeground {
self.layoutSubviews(force: true)
}
@ -1117,7 +1121,7 @@ public class Window1 {
}
let previousInputOffset = inputHeightOffsetForLayout(self.windowLayout)
self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(updatingLayout.layout.size), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver)
self.windowLayout = WindowLayout(size: updatingLayout.layout.size, metrics: layoutMetricsForScreenSize(size: updatingLayout.layout.size, orientation: updatingLayout.layout.metrics.orientation), statusBarHeight: statusBarHeight, forceInCallStatusBarText: updatingLayout.layout.forceInCallStatusBarText, inputHeight: updatingLayout.layout.inputHeight, safeInsets: updatingLayout.layout.safeInsets, onScreenNavigationHeight: self.deviceMetrics.onScreenNavigationHeight(inLandscape: isLandscape, systemOnScreenNavigationHeight: self.hostView.onScreenNavigationHeight), upperKeyboardInputPositionBound: updatingLayout.layout.upperKeyboardInputPositionBound, inVoiceOver: updatingLayout.layout.inVoiceOver)
let childLayout = containedLayoutForWindowLayout(self.windowLayout, deviceMetrics: self.deviceMetrics)
let childLayoutUpdated = self.updatedContainerLayout != childLayout

View File

@ -2925,7 +2925,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
public func adapterContainerLayoutUpdatedSize(_ size: CGSize, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat, inputHeight: CGFloat, orientation: UIInterfaceOrientation, isRegular: Bool, animated: Bool) {
let layout = ContainerViewLayout(
size: size,
metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact),
metrics: LayoutMetrics(widthClass: isRegular ? .regular : .compact, heightClass: isRegular ? .regular : .compact, orientation: nil),
deviceMetrics: DeviceMetrics(screenSize: size, scale: UIScreen.main.scale, statusBarHeight: statusBarHeight, onScreenNavigationHeight: nil),
intrinsicInsets: intrinsicInsets,
safeInsets: safeInsets,

View File

@ -73,6 +73,8 @@ public final class CallController: ViewController {
private let idleTimerExtensionDisposable = MetaDisposable()
public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)?
public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) {
self.sharedContext = sharedContext
self.account = account
@ -136,7 +138,16 @@ public final class CallController: ViewController {
override public func loadDisplayNode() {
if self.sharedContext.immediateExperimentalUISettings.callUIV2 {
self.displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), easyDebugAccess: self.easyDebugAccess, call: self.call)
let displayNode = CallControllerNodeV2(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), easyDebugAccess: self.easyDebugAccess, call: self.call)
self.displayNode = displayNode
displayNode.restoreUIForPictureInPicture = { [weak self] completion in
guard let self, let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture else {
completion(false)
return
}
restoreUIForPictureInPicture(completion)
}
} else {
self.displayNode = CallControllerNode(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess, call: self.call)
}

View File

@ -17,6 +17,7 @@ import ImageBlur
import TelegramVoip
import MetalEngine
import DeviceAccess
import LibYuvBinding
final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol {
private let sharedContext: SharedAccountContext
@ -45,6 +46,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
var callEnded: ((Bool) -> Void)?
var dismissedInteractively: (() -> Void)?
var dismissAllTooltips: (() -> Void)?
var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)?
private var emojiKey: (data: Data, resolvedKey: [String])?
private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat)?
@ -117,6 +119,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
return
}
self.back?()
self.callScreen.beginPictureInPictureIfPossible()
}
self.callScreen.closeAction = { [weak self] in
guard let self else {
@ -124,6 +127,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
}
self.dismissedInteractively?()
}
self.callScreen.restoreUIForPictureInPicture = { [weak self] completion in
guard let self else {
completion(false)
return
}
self.restoreUIForPictureInPicture?(completion)
}
self.callScreenState = PrivateCallScreen.State(
lifecycleState: .connecting,
@ -312,6 +322,9 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
case let .active(startTime, signalQuality, keyData):
self.callStartTimestamp = startTime
var signalQuality = signalQuality
signalQuality = 4
let _ = keyData
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
@ -322,7 +335,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
let _ = keyData
mappedLifecycleState = .active(PrivateCallScreen.State.ActiveState(
startTime: startTime + kCFAbsoluteTimeIntervalSince1970,
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 0.0),
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
emojiKey: self.resolvedEmojiKey(data: keyData)
))
case .terminating, .terminated:
@ -518,6 +531,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.callScreen.update(
size: layout.size,
insets: layout.insets(options: [.statusBar]),
interfaceOrientation: layout.metrics.orientation ?? .portrait,
screenCornerRadius: layout.deviceMetrics.screenCornerRadius,
state: callScreenState,
transition: Transition(transition)
@ -526,7 +540,100 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
}
}
private func copyI420BufferToNV12Buffer(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer, pixelBuffer: CVPixelBuffer) -> Bool {
guard CVPixelBufferGetPixelFormatType(pixelBuffer) == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange else {
return false
}
guard CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) == buffer.width else {
return false
}
guard CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) == buffer.height else {
return false
}
let cvRet = CVPixelBufferLockBaseAddress(pixelBuffer, [])
if cvRet != kCVReturnSuccess {
return false
}
defer {
CVPixelBufferUnlockBaseAddress(pixelBuffer, [])
}
guard let dstY = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0) else {
return false
}
let dstStrideY = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
guard let dstUV = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else {
return false
}
let dstStrideUV = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1)
buffer.y.withUnsafeBytes { srcYBuffer in
guard let srcY = srcYBuffer.baseAddress else {
return
}
buffer.u.withUnsafeBytes { srcUBuffer in
guard let srcU = srcUBuffer.baseAddress else {
return
}
buffer.v.withUnsafeBytes { srcVBuffer in
guard let srcV = srcVBuffer.baseAddress else {
return
}
libyuv_I420ToNV12(
srcY.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideY),
srcU.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideU),
srcV.assumingMemoryBound(to: UInt8.self),
Int32(buffer.strideV),
dstY.assumingMemoryBound(to: UInt8.self),
Int32(dstStrideY),
dstUV.assumingMemoryBound(to: UInt8.self),
Int32(dstStrideUV),
Int32(buffer.width),
Int32(buffer.height)
)
}
}
}
return true
}
private final class AdaptedCallVideoSource: VideoSource {
final class I420DataBuffer: Output.DataBuffer {
private let buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer
override var pixelBuffer: CVPixelBuffer? {
let ioSurfaceProperties = NSMutableDictionary()
let options = NSMutableDictionary()
options.setObject(ioSurfaceProperties, forKey: kCVPixelBufferIOSurfacePropertiesKey as NSString)
var pixelBuffer: CVPixelBuffer?
CVPixelBufferCreate(
kCFAllocatorDefault,
self.buffer.width,
self.buffer.height,
kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
options,
&pixelBuffer
)
if let pixelBuffer, copyI420BufferToNV12Buffer(buffer: buffer, pixelBuffer: pixelBuffer) {
return pixelBuffer
} else {
return nil
}
}
init(buffer: OngoingGroupCallContext.VideoFrameData.I420Buffer) {
self.buffer = buffer
super.init()
}
}
private static let queue = Queue(name: "AdaptedCallVideoSource")
private var onUpdatedListeners = Bag<() -> Void>()
private(set) var currentOutput: Output?
@ -555,6 +662,8 @@ private final class AdaptedCallVideoSource: VideoSource {
rotationAngle = Float.pi * 3.0 / 2.0
}
let followsDeviceOrientation = videoFrameData.deviceRelativeOrientation != nil
var mirrorDirection: Output.MirrorDirection = []
var sourceId: Int = 0
@ -616,12 +725,45 @@ private final class AdaptedCallVideoSource: VideoSource {
output = Output(
resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)),
y: yTexture,
uv: uvTexture,
textureLayout: .biPlanar(Output.BiPlanarTextureLayout(
y: yTexture,
uv: uvTexture
)),
dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer),
rotationAngle: rotationAngle,
followsDeviceOrientation: followsDeviceOrientation,
mirrorDirection: mirrorDirection,
sourceId: sourceId
)
case let .i420(i420Buffer):
let width = i420Buffer.width
let height = i420Buffer.height
let _ = width
let _ = height
return
/*var cvMetalTextureY: CVMetalTexture?
var status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .r8Unorm, width, height, 0, &cvMetalTextureY)
guard status == kCVReturnSuccess, let yTexture = CVMetalTextureGetTexture(cvMetalTextureY!) else {
return
}
var cvMetalTextureUV: CVMetalTexture?
status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, nativeBuffer.pixelBuffer, nil, .rg8Unorm, width / 2, height / 2, 1, &cvMetalTextureUV)
guard status == kCVReturnSuccess, let uvTexture = CVMetalTextureGetTexture(cvMetalTextureUV!) else {
return
}
output = Output(
resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)),
y: yTexture,
uv: uvTexture,
dataBuffer: Output.NativeDataBuffer(pixelBuffer: nativeBuffer.pixelBuffer),
rotationAngle: rotationAngle,
followsDeviceOrientation: followsDeviceOrientation,
mirrorDirection: mirrorDirection,
sourceId: sourceId
)*/
default:
return
}

View File

@ -342,6 +342,32 @@ public final class PresentationCallImpl: PresentationCall {
}
private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?, audioSessionControl: ManagedAudioSessionControl?) {
self.reception = reception
if let ongoingContext = self.ongoingContext {
if self.receptionDisposable == nil, case .active = sessionState.state {
self.reception = 4
var canUpdate = false
self.receptionDisposable = (ongoingContext.reception
|> delay(1.0, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] reception in
if let strongSelf = self {
if let sessionState = strongSelf.sessionState {
if canUpdate {
strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf.reception = reception
}
} else {
strongSelf.reception = reception
}
}
})
canUpdate = true
}
}
if case .video = sessionState.type {
self.isVideo = true
}
@ -349,9 +375,10 @@ public final class PresentationCallImpl: PresentationCall {
let previousControl = self.audioSessionControl
self.sessionState = sessionState
self.callContextState = callContextState
self.reception = reception
self.audioSessionControl = audioSessionControl
let reception = self.reception
if previousControl != nil && audioSessionControl == nil {
print("updateSessionState \(sessionState.state) \(audioSessionControl != nil)")
}
@ -559,17 +586,6 @@ public final class PresentationCallImpl: PresentationCall {
}
})
self.receptionDisposable = (ongoingContext.reception
|> deliverOnMainQueue).start(next: { [weak self] reception in
if let strongSelf = self {
if let sessionState = strongSelf.sessionState {
strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception, audioSessionControl: strongSelf.audioSessionControl)
} else {
strongSelf.reception = reception
}
}
})
self.audioLevelDisposable = (ongoingContext.audioLevel
|> deliverOnMainQueue).start(next: { [weak self] level in
if let strongSelf = self {

View File

@ -236,7 +236,7 @@ fragment half4 callBlobFragment(
return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha);
}
kernel void videoYUVToRGBA(
kernel void videoBiPlanarToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureUV [[ texture(1) ]],
texture2d<half, access::write> outTexture [[ texture(2) ]],
@ -249,6 +249,22 @@ kernel void videoYUVToRGBA(
outTexture.write(color, threadPosition);
}
kernel void videoTriPlanarToRGBA(
texture2d<half, access::read> inTextureY [[ texture(0) ]],
texture2d<half, access::read> inTextureU [[ texture(1) ]],
texture2d<half, access::read> inTextureV [[ texture(2) ]],
texture2d<half, access::write> outTexture [[ texture(3) ]],
uint2 threadPosition [[ thread_position_in_grid ]]
) {
half y = inTextureY.read(threadPosition).r;
uint2 uvPosition = uint2(threadPosition.x / 2, threadPosition.y / 2);
half2 inUV = (inTextureU.read(uvPosition).r, inTextureV.read(uvPosition).r);
half2 uv = inUV - half2(0.5, 0.5);
half4 color(y + 1.403 * uv.y, y - 0.344 * uv.x - 0.714 * uv.y, y + 1.770 * uv.x, 1.0);
outTexture.write(color, threadPosition);
}
vertex QuadVertexOut mainVideoVertex(
const device Rectangle &rect [[ buffer(0) ]],
const device uint2 &mirror [[ buffer(1) ]],

View File

@ -0,0 +1,248 @@
import Foundation
import AVKit
import AVFoundation
import CoreMedia
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
private func sampleBufferFromPixelBuffer(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {
var maybeFormat: CMVideoFormatDescription?
let status = CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &maybeFormat)
if status != noErr {
return nil
}
guard let format = maybeFormat else {
return nil
}
var timingInfo = CMSampleTimingInfo(
duration: CMTimeMake(value: 1, timescale: 30),
presentationTimeStamp: CMTimeMake(value: 0, timescale: 30),
decodeTimeStamp: CMTimeMake(value: 0, timescale: 30)
)
var maybeSampleBuffer: CMSampleBuffer?
let bufferStatus = CMSampleBufferCreateReadyWithImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: format, sampleTiming: &timingInfo, sampleBufferOut: &maybeSampleBuffer)
if (bufferStatus != noErr) {
return nil
}
guard let sampleBuffer = maybeSampleBuffer else {
return nil
}
let attachments: NSArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true)! as NSArray
let dict: NSMutableDictionary = attachments[0] as! NSMutableDictionary
dict[kCMSampleAttachmentKey_DisplayImmediately as NSString] = true as NSNumber
return sampleBuffer
}
final class PrivateCallPictureInPictureView: UIView {
private final class SampleBufferView: UIView {
override static var layerClass: AnyClass {
return AVSampleBufferDisplayLayer.self
}
}
private final class AnimationTrackingLayer: SimpleLayer {
var onAnimation: ((CAAnimation) -> Void)?
override func add(_ anim: CAAnimation, forKey key: String?) {
super.add(anim, forKey: key)
if key == "bounds" {
self.onAnimation?(anim)
}
}
}
private final class AnimationTrackingView: UIView {
override static var layerClass: AnyClass {
return AnimationTrackingLayer.self
}
var onAnimation: ((CAAnimation) -> Void)? {
didSet {
(self.layer as? AnimationTrackingLayer)?.onAnimation = self.onAnimation
}
}
}
private let animationTrackingView: AnimationTrackingView
private let videoContainerView: UIView
private let sampleBufferView: SampleBufferView
private var videoMetrics: VideoContainerView.VideoMetrics?
private var videoDisposable: Disposable?
var isRenderingEnabled: Bool = false {
didSet {
if self.isRenderingEnabled != oldValue {
self.updateContents()
}
}
}
var video: VideoSource? {
didSet {
if self.video !== oldValue {
self.videoDisposable?.dispose()
if let video = self.video {
self.videoDisposable = video.addOnUpdated({ [weak self] in
guard let self else {
return
}
if self.isRenderingEnabled {
self.updateContents()
}
})
}
}
}
}
override static var layerClass: AnyClass {
return AVSampleBufferDisplayLayer.self
}
override init(frame: CGRect) {
self.animationTrackingView = AnimationTrackingView()
self.videoContainerView = UIView()
self.sampleBufferView = SampleBufferView()
super.init(frame: frame)
self.addSubview(self.animationTrackingView)
self.backgroundColor = .black
self.videoContainerView.addSubview(self.sampleBufferView)
self.addSubview(self.videoContainerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateContents() {
guard let video = self.video, let currentOutput = video.currentOutput else {
return
}
guard let pixelBuffer = currentOutput.dataBuffer.pixelBuffer else {
return
}
let videoMetrics = VideoContainerView.VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId)
if self.videoMetrics != videoMetrics {
self.videoMetrics = videoMetrics
self.setNeedsLayout()
}
if let sampleBuffer = sampleBufferFromPixelBuffer(pixelBuffer: pixelBuffer) {
(self.sampleBufferView.layer as? AVSampleBufferDisplayLayer)?.enqueue(sampleBuffer)
}
}
override func layoutSubviews() {
super.layoutSubviews()
let size = self.bounds.size
if size.width.isZero || size.height.isZero {
return
}
var animationTemplate: CAAnimation?
self.animationTrackingView.onAnimation = { animation in
animationTemplate = animation
}
self.animationTrackingView.frame = CGRect(origin: CGPoint(), size: size)
self.animationTrackingView.onAnimation = nil
let _ = animationTemplate
let animationDuration = CATransaction.animationDuration()
let timingFunction = CATransaction.animationTimingFunction()
let mappedTransition: Transition
if self.sampleBufferView.bounds.isEmpty {
mappedTransition = .immediate
} else if animationDuration > 0.0 && !CATransaction.disableActions() {
let mappedCurve: Transition.Animation.Curve
if let timingFunction {
var controlPoint0: [Float] = [0.0, 0.0]
var controlPoint1: [Float] = [0.0, 0.0]
timingFunction.getControlPoint(at: 1, values: &controlPoint0)
timingFunction.getControlPoint(at: 2, values: &controlPoint1)
mappedCurve = .custom(controlPoint0[0], controlPoint0[1], controlPoint1[0], controlPoint1[1])
} else if animationDuration >= 0.5 {
mappedCurve = .spring
} else {
mappedCurve = .easeInOut
}
mappedTransition = Transition(animation: .curve(
duration: animationDuration,
curve: mappedCurve
))
} else {
mappedTransition = .immediate
}
if let videoMetrics = self.videoMetrics {
let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: UIApplication.shared.statusBarOrientation)
var rotatedResolution = videoMetrics.resolution
var videoIsRotated = false
if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 {
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
videoIsRotated = true
}
var videoSize = rotatedResolution.aspectFitted(size)
let boundingAspectRatio = size.width / size.height
let videoAspectRatio = videoSize.width / videoSize.height
let isFillingBounds = abs(boundingAspectRatio - videoAspectRatio) < 0.15
if isFillingBounds {
videoSize = rotatedResolution.aspectFilled(size)
}
let rotatedBoundingSize = videoIsRotated ? CGSize(width: size.height, height: size.width) : size
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
let videoFrame = rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5))
let apply: () -> Void = {
self.videoContainerView.center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
self.videoContainerView.bounds = CGRect(origin: CGPoint(), size: rotatedBoundingSize)
self.videoContainerView.transform = CGAffineTransformMakeRotation(CGFloat(resolvedRotationAngle))
self.sampleBufferView.center = videoFrame.center
self.sampleBufferView.bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
if let sublayers = self.sampleBufferView.layer.sublayers {
if sublayers.count > 1, !sublayers[0].bounds.isEmpty {
sublayers[0].position = CGPoint(x: videoFrame.width * 0.5, y: videoFrame.height * 0.5)
sublayers[0].bounds = CGRect(origin: CGPoint(), size: videoFrame.size)
}
}
}
if !mappedTransition.animation.isImmediate {
apply()
} else {
UIView.performWithoutAnimation {
apply()
}
}
}
}
}
@available(iOS 15.0, *)
final class PrivateCallPictureInPictureController: AVPictureInPictureVideoCallViewController {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
}
}

View File

@ -11,7 +11,8 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
let blurredLayer: MetalEngineSubjectLayer
final class BlurState: ComputeState {
let computePipelineStateYUVToRGBA: MTLComputePipelineState
let computePipelineStateYUVBiPlanarToRGBA: MTLComputePipelineState
let computePipelineStateYUVTriPlanarToRGBA: MTLComputePipelineState
let computePipelineStateHorizontal: MTLComputePipelineState
let computePipelineStateVertical: MTLComputePipelineState
let downscaleKernel: MPSImageBilinearScale
@ -20,13 +21,22 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let functionVideoYUVToRGBA = library.makeFunction(name: "videoYUVToRGBA") else {
guard let functionVideoBiPlanarToRGBA = library.makeFunction(name: "videoBiPlanarToRGBA") else {
return nil
}
guard let computePipelineStateYUVToRGBA = try? device.makeComputePipelineState(function: functionVideoYUVToRGBA) else {
guard let computePipelineStateYUVBiPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoBiPlanarToRGBA) else {
return nil
}
self.computePipelineStateYUVToRGBA = computePipelineStateYUVToRGBA
self.computePipelineStateYUVBiPlanarToRGBA = computePipelineStateYUVBiPlanarToRGBA
guard let functionVideoTriPlanarToRGBA = library.makeFunction(name: "videoTriPlanarToRGBA") else {
return nil
}
guard let computePipelineStateYUVTriPlanarToRGBA = try? device.makeComputePipelineState(function: functionVideoTriPlanarToRGBA) else {
return nil
}
self.computePipelineStateYUVTriPlanarToRGBA = computePipelineStateYUVTriPlanarToRGBA
guard let gaussianBlurHorizontal = library.makeFunction(name: "gaussianBlurHorizontal"), let gaussianBlurVertical = library.makeFunction(name: "gaussianBlurVertical") else {
return nil
@ -107,7 +117,7 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
return
}
let rgbaTextureSpec = TextureSpec(width: videoTextures.y.width, height: videoTextures.y.height, pixelFormat: .rgba8UnsignedNormalized)
let rgbaTextureSpec = TextureSpec(width: Int(videoTextures.resolution.width), height: Int(videoTextures.resolution.height), pixelFormat: .rgba8UnsignedNormalized)
if self.rgbaTexture == nil || self.rgbaTexture?.spec != rgbaTextureSpec {
self.rgbaTexture = MetalEngine.shared.pooledTexture(spec: rgbaTextureSpec)
}
@ -136,10 +146,19 @@ final class PrivateCallVideoLayer: MetalEngineSubjectLayer, MetalEngineSubject {
let threadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
let threadgroupCount = MTLSize(width: (rgbaTexture.width + threadgroupSize.width - 1) / threadgroupSize.width, height: (rgbaTexture.height + threadgroupSize.height - 1) / threadgroupSize.height, depth: 1)
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVToRGBA)
computeEncoder.setTexture(videoTextures.y, index: 0)
computeEncoder.setTexture(videoTextures.uv, index: 1)
computeEncoder.setTexture(rgbaTexture, index: 2)
switch videoTextures.textureLayout {
case let .biPlanar(biPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA)
computeEncoder.setTexture(biPlanar.y, index: 0)
computeEncoder.setTexture(biPlanar.uv, index: 1)
computeEncoder.setTexture(rgbaTexture, index: 2)
case let .triPlanar(triPlanar):
computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVTriPlanarToRGBA)
computeEncoder.setTexture(triPlanar.y, index: 0)
computeEncoder.setTexture(triPlanar.u, index: 1)
computeEncoder.setTexture(triPlanar.u, index: 2)
computeEncoder.setTexture(rgbaTexture, index: 3)
}
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
computeEncoder.endEncoding()

View File

@ -25,22 +25,32 @@ final class RoundedCornersView: UIImageView {
guard let cornerRadius = self.currentCornerRadius else {
return
}
if self.smoothCorners {
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
if cornerRadius == 0.0 {
if let cornerImage = self.cornerImage, cornerImage.size.width == 1.0 {
} else {
self.cornerImage = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
self.cornerImage = generateImage(CGSize(width: 1.0, height: 1.0), rotatedContext: { size, context in
context.setFillColor(self.color.cgColor)
context.fillPath()
context.fill(CGRect(origin: CGPoint(), size: size))
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5)
}
} else {
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
if self.smoothCorners {
let size = CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius * 2.0 + 10.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
context.setFillColor(self.color.cgColor)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5)
}
} else {
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
if let cornerImage = self.cornerImage, cornerImage.size == size {
} else {
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
}
}
}
self.image = self.cornerImage

View File

@ -9,6 +9,26 @@ private let shadowImage: UIImage? = {
UIImage(named: "Call/VideoGradient")?.precomposed()
}()
func resolveVideoRotationAngle(angle: Float, followsDeviceOrientation: Bool, interfaceOrientation: UIInterfaceOrientation) -> Float {
if !followsDeviceOrientation {
return angle
}
let interfaceAngle: Float
switch interfaceOrientation {
case .portrait, .unknown:
interfaceAngle = 0.0
case .landscapeLeft:
interfaceAngle = Float.pi * 0.5
case .landscapeRight:
interfaceAngle = Float.pi * 3.0 / 2.0
case .portraitUpsideDown:
interfaceAngle = Float.pi
@unknown default:
interfaceAngle = 0.0
}
return (angle + interfaceAngle).truncatingRemainder(dividingBy: Float.pi * 2.0)
}
private final class VideoContainerLayer: SimpleLayer {
let contentsLayer: SimpleLayer
@ -44,14 +64,16 @@ final class VideoContainerView: HighlightTrackingButton {
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
var interfaceOrientation: UIInterfaceOrientation
var cornerRadius: CGFloat
var controlsHidden: Bool
var isMinimized: Bool
var isAnimatedOut: Bool
init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) {
init(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) {
self.size = size
self.insets = insets
self.interfaceOrientation = interfaceOrientation
self.cornerRadius = cornerRadius
self.controlsHidden = controlsHidden
self.isMinimized = isMinimized
@ -59,14 +81,16 @@ final class VideoContainerView: HighlightTrackingButton {
}
}
private struct VideoMetrics: Equatable {
struct VideoMetrics: Equatable {
var resolution: CGSize
var rotationAngle: Float
var followsDeviceOrientation: Bool
var sourceId: Int
init(resolution: CGSize, rotationAngle: Float, sourceId: Int) {
init(resolution: CGSize, rotationAngle: Float, followsDeviceOrientation: Bool, sourceId: Int) {
self.resolution = resolution
self.rotationAngle = rotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
self.sourceId = sourceId
}
}
@ -74,10 +98,12 @@ final class VideoContainerView: HighlightTrackingButton {
private final class FlipAnimationInfo {
let isForward: Bool
let previousRotationAngle: Float
let followsDeviceOrientation: Bool
init(isForward: Bool, previousRotationAngle: Float) {
init(isForward: Bool, previousRotationAngle: Float, followsDeviceOrientation: Bool) {
self.isForward = isForward
self.previousRotationAngle = previousRotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
}
}
@ -141,11 +167,11 @@ final class VideoContainerView: HighlightTrackingButton {
var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput {
if let previousVideo = self.videoLayer.video, previousVideo.sourceId != currentOutput.sourceId {
self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId, previousRotationAngle: previousVideo.rotationAngle))
self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId, previousRotationAngle: previousVideo.rotationAngle, followsDeviceOrientation: previousVideo.followsDeviceOrientation))
}
self.videoLayer.video = currentOutput
videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId)
} else {
self.videoLayer.video = nil
}
@ -164,7 +190,7 @@ final class VideoContainerView: HighlightTrackingButton {
var videoMetrics: VideoMetrics?
if let currentOutput = self.video?.currentOutput {
self.videoLayer.video = currentOutput
videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, followsDeviceOrientation: currentOutput.followsDeviceOrientation, sourceId: currentOutput.sourceId)
} else {
self.videoLayer.video = nil
}
@ -382,7 +408,7 @@ final class VideoContainerView: HighlightTrackingButton {
self.dragPositionAnimatorLink = nil
return
}
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: false)
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, resolvedRotationAngle: resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: false)
let targetPosition = videoLayout.rotatedVideoFrame.center
self.dragVelocity = self.updateVelocityUsingSpring(
@ -443,8 +469,8 @@ final class VideoContainerView: HighlightTrackingButton {
self.update(previousParams: params, params: params, transition: transition)
}
func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) {
let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut)
func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) {
let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut)
if self.params == params {
return
}
@ -469,10 +495,10 @@ final class VideoContainerView: HighlightTrackingButton {
var effectiveVideoFrame: CGRect
}
private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, applyDragPosition: Bool) -> MinimizedLayout {
private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, resolvedRotationAngle: Float, applyDragPosition: Bool) -> MinimizedLayout {
var rotatedResolution = videoMetrics.resolution
var videoIsRotated = false
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 {
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
videoIsRotated = true
}
@ -505,7 +531,7 @@ final class VideoContainerView: HighlightTrackingButton {
var videoTransform = CATransform3DIdentity
videoTransform.m34 = 1.0 / 600.0
videoTransform = CATransform3DRotate(videoTransform, CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)
videoTransform = CATransform3DRotate(videoTransform, CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0)
if params.isAnimatedOut {
videoTransform = CATransform3DScale(videoTransform, 0.6, 0.6, 1.0)
}
@ -530,10 +556,12 @@ final class VideoContainerView: HighlightTrackingButton {
}
self.appliedVideoMetrics = videoMetrics
let resolvedRotationAngle = resolveVideoRotationAngle(angle: videoMetrics.rotationAngle, followsDeviceOrientation: videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation)
if params.isMinimized {
self.isFillingBounds = false
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: true)
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, resolvedRotationAngle: resolvedRotationAngle, applyDragPosition: true)
transition.setPosition(layer: self.videoContainerLayer, position: videoLayout.rotatedVideoFrame.center)
@ -558,23 +586,25 @@ final class VideoContainerView: HighlightTrackingButton {
if let disappearingVideoLayer = self.disappearingVideoLayer {
self.disappearingVideoLayer = nil
let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, applyDragPosition: true)
let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, resolvedRotationAngle: resolveVideoRotationAngle(angle: disappearingVideoLayer.videoMetrics.rotationAngle, followsDeviceOrientation: disappearingVideoLayer.videoMetrics.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation), applyDragPosition: true)
let initialDisapparingVideoSize = disappearingVideoLayout.rotatedVideoSize
if !disappearingVideoLayer.isAlphaAnimationInitiated {
disappearingVideoLayer.isAlphaAnimationInitiated = true
if let flipAnimationInfo = disappearingVideoLayer.flipAnimationInfo {
let resolvedPreviousRotationAngle = resolveVideoRotationAngle(angle: flipAnimationInfo.previousRotationAngle, followsDeviceOrientation: flipAnimationInfo.followsDeviceOrientation, interfaceOrientation: params.interfaceOrientation)
var videoTransform = self.videoContainerLayer.transform
var axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0.0, 0.0, 0.0)
let previousVideoScale: CGPoint
if flipAnimationInfo.previousRotationAngle == Float.pi * 0.5 {
if resolvedPreviousRotationAngle == Float.pi * 0.5 {
axis.x = -1.0
previousVideoScale = CGPoint(x: 1.0, y: -1.0)
} else if flipAnimationInfo.previousRotationAngle == Float.pi {
} else if resolvedPreviousRotationAngle == Float.pi {
axis.y = -1.0
previousVideoScale = CGPoint(x: -1.0, y: -1.0)
} else if flipAnimationInfo.previousRotationAngle == Float.pi * 3.0 / 2.0 {
} else if resolvedPreviousRotationAngle == Float.pi * 3.0 / 2.0 {
axis.x = 1.0
previousVideoScale = CGPoint(x: 1.0, y: 1.0)
} else {
@ -652,7 +682,7 @@ final class VideoContainerView: HighlightTrackingButton {
} else {
var rotatedResolution = videoMetrics.resolution
var videoIsRotated = false
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 {
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
videoIsRotated = true
}
@ -728,13 +758,13 @@ final class VideoContainerView: HighlightTrackingButton {
}
}
transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeRotation(CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0))
videoTransition.setFrame(layer: self.videoLayer, frame: rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5)))
videoTransition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
videoTransition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
videoTransition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 1.0)
videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(resolvedRotationAngle), 0.0, 0.0, 1.0))
if !params.isAnimatedOut {
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)

View File

@ -16,18 +16,67 @@ public final class VideoSourceOutput {
public static let vertical = MirrorDirection(rawValue: 1 << 1)
}
open class DataBuffer {
open var pixelBuffer: CVPixelBuffer? {
return nil
}
public init() {
}
}
public final class BiPlanarTextureLayout {
public let y: MTLTexture
public let uv: MTLTexture
public init(y: MTLTexture, uv: MTLTexture) {
self.y = y
self.uv = uv
}
}
public final class TriPlanarTextureLayout {
public let y: MTLTexture
public let u: MTLTexture
public let v: MTLTexture
public init(y: MTLTexture, u: MTLTexture, v: MTLTexture) {
self.y = y
self.u = u
self.v = v
}
}
public enum TextureLayout {
case biPlanar(BiPlanarTextureLayout)
case triPlanar(TriPlanarTextureLayout)
}
public final class NativeDataBuffer: DataBuffer {
private let pixelBufferValue: CVPixelBuffer
override public var pixelBuffer: CVPixelBuffer? {
return self.pixelBufferValue
}
public init(pixelBuffer: CVPixelBuffer) {
self.pixelBufferValue = pixelBuffer
}
}
public let resolution: CGSize
public let y: MTLTexture
public let uv: MTLTexture
public let textureLayout: TextureLayout
public let dataBuffer: DataBuffer
public let rotationAngle: Float
public let followsDeviceOrientation: Bool
public let mirrorDirection: MirrorDirection
public let sourceId: Int
public init(resolution: CGSize, y: MTLTexture, uv: MTLTexture, rotationAngle: Float, mirrorDirection: MirrorDirection, sourceId: Int) {
public init(resolution: CGSize, textureLayout: TextureLayout, dataBuffer: DataBuffer, rotationAngle: Float, followsDeviceOrientation: Bool, mirrorDirection: MirrorDirection, sourceId: Int) {
self.resolution = resolution
self.y = y
self.uv = uv
self.textureLayout = textureLayout
self.dataBuffer = dataBuffer
self.rotationAngle = rotationAngle
self.followsDeviceOrientation = followsDeviceOrientation
self.mirrorDirection = mirrorDirection
self.sourceId = sourceId
}
@ -161,7 +210,18 @@ public final class FileVideoSource: VideoSource {
resolution.width = floor(resolution.width * self.sizeMultiplicator.x)
resolution.height = floor(resolution.height * self.sizeMultiplicator.y)
self.currentOutput = Output(resolution: resolution, y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, mirrorDirection: [], sourceId: self.sourceId)
self.currentOutput = Output(
resolution: resolution,
textureLayout: .biPlanar(Output.BiPlanarTextureLayout(
y: yTexture,
uv: uvTexture
)),
dataBuffer: Output.NativeDataBuffer(pixelBuffer: buffer),
rotationAngle: rotationAngle,
followsDeviceOrientation: false,
mirrorDirection: [],
sourceId: self.sourceId
)
return true
}
}

View File

@ -1,58 +1,14 @@
import Foundation
import AVFoundation
import AVKit
import UIKit
import Display
import MetalEngine
import ComponentFlow
import SwiftSignalKit
import UIKitRuntimeUtils
/*private final class EdgeTestLayer: MetalEngineSubjectLayer, MetalEngineSubject {
final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "edgeTestVertex"), let fragmentFunction = library.makeFunction(name: "edgeTestFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
var internalData: MetalEngineSubjectInternalData?
func update(context: MetalEngineSubjectContext) {
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: 300, height: 300), edgeInset: 100), state: RenderState.self, layer: self, commands: { encoder, placement in
let effectiveRect = placement.effectiveRect
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width * 0.5), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
var color = SIMD4<Float>(1.0, 0.0, 0.0, 1.0)
encoder.setFragmentBytes(&color, length: 4 * 4, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
})
}
}*/
public final class PrivateCallScreen: OverlayMaskContainerView {
public final class PrivateCallScreen: OverlayMaskContainerView, AVPictureInPictureControllerDelegate {
public struct State: Equatable {
public struct SignalInfo: Equatable {
public var quality: Double
@ -168,12 +124,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
private struct Params: Equatable {
var size: CGSize
var insets: UIEdgeInsets
var interfaceOrientation: UIInterfaceOrientation
var screenCornerRadius: CGFloat
var state: State
init(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State) {
init(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State) {
self.size = size
self.insets = insets
self.interfaceOrientation = interfaceOrientation
self.screenCornerRadius = screenCornerRadius
self.state = state
}
@ -210,11 +168,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
private var activeLocalVideoSource: VideoSource?
private var waitingForFirstLocalVideoFrameDisposable: Disposable?
private var isUpdating: Bool = false
private var canAnimateAudioLevel: Bool = false
private var displayEmojiTooltip: Bool = false
private var isEmojiKeyExpanded: Bool = false
private var areControlsHidden: Bool = false
private var swapLocalAndRemoteVideo: Bool = false
private var isPictureInPictureActive: Bool = false
private var processedInitialAudioLevelBump: Bool = false
private var audioLevelBump: Float = 0.0
@ -231,6 +192,12 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
public var endCallAction: (() -> Void)?
public var backAction: (() -> Void)?
public var closeAction: (() -> Void)?
public var restoreUIForPictureInPicture: ((@escaping (Bool) -> Void) -> Void)?
private let pipView: PrivateCallPictureInPictureView
private var pipContentSource: AnyObject?
private var pipVideoCallViewController: UIViewController?
private var pipController: AVPictureInPictureController?
public override init(frame: CGRect) {
self.overlayContentsView = UIView()
@ -256,6 +223,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
self.backButtonView = BackButtonView(text: "Back")
self.pipView = PrivateCallPictureInPictureView(frame: CGRect(origin: CGPoint(), size: CGSize()))
super.init(frame: frame)
self.clipsToBounds = true
@ -320,6 +289,20 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
self.closeAction?()
}
if #available(iOS 16.0, *) {
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.view.addSubview(self.pipView)
self.pipView.frame = pipVideoCallViewController.view.bounds
self.pipView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.pipView.translatesAutoresizingMaskIntoConstraints = true
self.pipVideoCallViewController = pipVideoCallViewController
}
if let blurFilter = makeBlurFilter() {
blurFilter.setValue(10.0 as NSNumber, forKey: "inputRadius")
self.overlayContentsView.layer.filters = [blurFilter]
}
}
public required init?(coder: NSCoder) {
@ -345,6 +328,39 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
return result
}
public func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.isPictureInPictureActive = true
if !self.isUpdating {
self.update(transition: .easeInOut(duration: 0.2))
}
}
public func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
self.isPictureInPictureActive = false
if !self.isUpdating {
let wereControlsHidden = self.areControlsHidden
self.areControlsHidden = true
self.update(transition: .immediate)
if !wereControlsHidden {
self.areControlsHidden = false
self.update(transition: .spring(duration: 0.4))
}
}
}
public func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
if self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil {
if let restoreUIForPictureInPicture = self.restoreUIForPictureInPicture {
restoreUIForPictureInPicture(completionHandler)
} else {
completionHandler(false)
}
} else {
completionHandler(false)
}
}
public func addIncomingAudioLevel(value: Float) {
if self.canAnimateAudioLevel {
self.targetAudioLevel = value
@ -395,8 +411,14 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
}
public func update(size: CGSize, insets: UIEdgeInsets, screenCornerRadius: CGFloat, state: State, transition: Transition) {
let params = Params(size: size, insets: insets, screenCornerRadius: screenCornerRadius, state: state)
public func beginPictureInPictureIfPossible() {
if let pipController = self.pipController, (self.activeLocalVideoSource != nil || self.activeRemoteVideoSource != nil) {
pipController.startPictureInPicture()
}
}
public func update(size: CGSize, insets: UIEdgeInsets, interfaceOrientation: UIInterfaceOrientation, screenCornerRadius: CGFloat, state: State, transition: Transition) {
let params = Params(size: size, insets: insets, interfaceOrientation: interfaceOrientation, screenCornerRadius: screenCornerRadius, state: state)
if self.params == params {
return
}
@ -497,6 +519,11 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
private func updateInternal(params: Params, transition: Transition) {
self.isUpdating = true
defer {
self.isUpdating = false
}
let genericAlphaTransition: Transition
switch transition.animation {
case .none:
@ -532,6 +559,41 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
let havePrimaryVideo = !activeVideoSources.isEmpty
if #available(iOS 16.0, *) {
if havePrimaryVideo, let pipVideoCallViewController = self.pipVideoCallViewController as? AVPictureInPictureVideoCallViewController {
if self.pipController == nil {
let pipContentSource = AVPictureInPictureController.ContentSource(activeVideoCallSourceView: self, contentViewController: pipVideoCallViewController)
let pipController = AVPictureInPictureController(contentSource: pipContentSource)
self.pipController = pipController
pipController.canStartPictureInPictureAutomaticallyFromInline = true
pipController.delegate = self
}
} else if let pipController = self.pipController {
self.pipController = nil
if pipController.isPictureInPictureActive {
pipController.stopPictureInPicture()
}
if self.isPictureInPictureActive {
self.isPictureInPictureActive = false
}
}
}
self.pipView.isRenderingEnabled = self.isPictureInPictureActive
self.pipView.video = self.activeRemoteVideoSource ?? self.activeLocalVideoSource
if let pipVideoCallViewController = self.pipVideoCallViewController {
if let video = self.pipView.video, let currentOutput = video.currentOutput {
var rotatedResolution = currentOutput.resolution
let resolvedRotationAngle = currentOutput.rotationAngle
if resolvedRotationAngle == Float.pi * 0.5 || resolvedRotationAngle == Float.pi * 3.0 / 2.0 {
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
}
pipVideoCallViewController.preferredContentSize = rotatedResolution
}
}
let currentAreControlsHidden = havePrimaryVideo && self.areControlsHidden
let backgroundAspect: CGFloat = params.size.width / params.size.height
@ -605,13 +667,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "Your microphone is turned off"))
}
if params.state.isRemoteAudioMuted {
notices.append(ButtonGroupView.Notice(id: AnyHashable(0 as Int), text: "\(params.state.shortName)'s microphone is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "\(params.state.shortName)'s microphone is turned off"))
}
if params.state.remoteVideo != nil && params.state.localVideo == nil {
notices.append(ButtonGroupView.Notice(id: AnyHashable(1 as Int), text: "Your camera is turned off"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "Your camera is turned off"))
}
if params.state.isRemoteBatteryLow {
notices.append(ButtonGroupView.Notice(id: AnyHashable(2 as Int), text: "\(params.state.shortName)'s battery is low"))
notices.append(ButtonGroupView.Notice(id: AnyHashable(3 as Int), text: "\(params.state.shortName)'s battery is low"))
}
var displayClose = false
@ -879,7 +941,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
videoContainerView.blurredContainerLayer.position = self.avatarTransformLayer.position
videoContainerView.blurredContainerLayer.bounds = self.avatarTransformLayer.bounds
videoContainerView.blurredContainerLayer.opacity = 0.0
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate)
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate)
Transition.immediate.setScale(view: videoContainerView, scale: self.currentAvatarAudioScale)
Transition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale)
} else {
@ -889,7 +951,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center
videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size)
videoContainerView.blurredContainerLayer.opacity = 0.0
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate)
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate)
}
}
@ -899,7 +961,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
videoContainerTransition.setPosition(layer: videoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
videoContainerTransition.setScale(layer: videoContainerView.blurredContainerLayer, scale: 1.0)
videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition)
videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition)
let alphaTransition: Transition
switch transition.animation {
@ -921,8 +983,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
}
}
alphaTransition.setAlpha(view: videoContainerView, alpha: 1.0)
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0)
let videoAlpha: CGFloat = self.isPictureInPictureActive ? 0.0 : 1.0
alphaTransition.setAlpha(view: videoContainerView, alpha: videoAlpha)
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: videoAlpha)
}
var removedVideoContainerIndices: [Int] = []
@ -934,7 +997,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
if self.videoContainerViews.count == 1 || (i == 0 && !havePrimaryVideo) {
let alphaTransition: Transition = genericAlphaTransition
videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition)
videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition)
transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
@ -973,7 +1036,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
})
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
videoContainerView.update(size: params.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition)
videoContainerView.update(size: params.size, insets: minimizedVideoInsets, interfaceOrientation: params.interfaceOrientation, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition)
}
}
}

View File

@ -71,6 +71,8 @@ private final class AnimatableProperty<T: Interpolatable> {
let timeFromStart = timestamp - animation.startTimestamp
var t = max(0.0, timeFromStart / duration)
switch curve {
case .linear:
break
case .easeInOut:
t = listViewAnimationCurveEaseInOut(t)
case .spring:

View File

@ -2455,7 +2455,7 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi
inputPanelHeight: 0.0,
transition: .immediate,
interfaceState: presentationInterfaceState,
layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact),
layoutMetrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil),
deviceMetrics: DeviceMetrics.iPhone12,
isVisible: true,
isExpanded: false

View File

@ -70,7 +70,7 @@ public class LegacyMessageInputPanelNode: ASDisplayNode, TGCaptionPanelView {
public var timerUpdated: ((NSNumber?) -> Void)?
public func updateLayoutSize(_ size: CGSize, keyboardHeight: CGFloat, sideInset: CGFloat, animated: Bool) -> CGFloat {
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact), isMediaInputExpanded: false)
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, keyboardHeight: keyboardHeight, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), isMediaInputExpanded: false)
}
public func caption() -> NSAttributedString {

View File

@ -1054,6 +1054,16 @@ public final class SharedAccountContextImpl: SharedAccountContext {
self.mainWindow?.hostView.containerView.endEditing(true)
let callController = CallController(sharedContext: self, account: call.context.account, call: call, easyDebugAccess: !GlobalExperimentalSettings.isAppStoreBuild)
self.callController = callController
callController.restoreUIForPictureInPicture = { [weak self, weak callController] completion in
guard let self, let callController else {
completion(false)
return
}
if callController.window == nil {
self.mainWindow?.present(callController, on: .calls)
}
completion(true)
}
self.mainWindow?.present(callController, on: .calls)
}