mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Call UI
This commit is contained in:
parent
a6b140d599
commit
d1e9e04dc1
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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?) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) ]],
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user