Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2020-07-08 16:47:52 +03:00
commit 465e8d96c8
23 changed files with 4946 additions and 4430 deletions

View File

@ -2426,6 +2426,8 @@ Unused sets are archived when you add more.";
"Calls.RatingFeedback" = "Write a comment...";
"Call.StatusIncoming" = "Telegram Audio...";
"Call.IncomingVoiceCall" = "Incoming Voice Call";
"Call.IncomingVideoCall" = "Incoming Video Call";
"Call.StatusRequesting" = "Contacting...";
"Call.StatusWaiting" = "Waiting...";
"Call.StatusRinging" = "Ringing...";

View File

@ -67,6 +67,19 @@ public struct PresentationCallState: Equatable {
}
}
public final class PresentationCallVideoView {
public let view: UIView
public let setOnFirstFrameReceived: ((() -> Void)?) -> Void
public init(
view: UIView,
setOnFirstFrameReceived: @escaping ((() -> Void)?) -> Void
) {
self.view = view
self.setOnFirstFrameReceived = setOnFirstFrameReceived
}
}
public protocol PresentationCall: class {
var account: Account { get }
var isIntegratedWithCallKit: Bool { get }
@ -96,8 +109,8 @@ public protocol PresentationCall: class {
func setCurrentAudioOutput(_ output: AudioSessionOutput)
func debugInfo() -> Signal<(String, String), NoError>
func makeIncomingVideoView(completion: @escaping (UIView?) -> Void)
func makeOutgoingVideoView(completion: @escaping (UIView?) -> Void)
func makeIncomingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void)
}
public protocol PresentationCallManager: class {

View File

@ -34,6 +34,7 @@ protocol CallControllerNodeProtocol: class {
func animateIn()
func animateOut(completion: @escaping () -> Void)
func expandFromPipIfPossible()
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition)
}
@ -320,7 +321,11 @@ public final class CallController: ViewController {
})
}
@objc func backPressed() {
@objc private func backPressed() {
self.dismiss()
}
public func expandFromPipIfPossible() {
self.controllerNode.expandFromPipIfPossible()
}
}

View File

@ -91,6 +91,8 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
func update(size: CGSize, content: Content, text: String, transition: ContainedViewLayoutTransition) {
let scaleFactor = size.width / self.largeButtonSize
let isSmall = self.largeButtonSize > size.width
self.effectView.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
self.contentNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
self.overlayHighlightNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: self.largeButtonSize, height: self.largeButtonSize))
@ -208,7 +210,7 @@ final class CallControllerButtonItemNode: HighlightTrackingButtonNode {
self.textNode.attributedText = NSAttributedString(string: text, font: labelFont, textColor: .white)
}
let textSize = self.textNode.updateLayout(CGSize(width: 150.0, height: 100.0))
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: size.height + 5.0), size: textSize)
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: size.height + (isSmall ? 5.0 : 8.0)), size: textSize)
if self.currentText.isEmpty {
self.textNode.frame = textFrame
if transition.isAnimated {

View File

@ -78,7 +78,7 @@ final class CallControllerButtonsNode: ASDisplayNode {
private var mode: CallControllerButtonsMode?
private var validLayout: CGFloat?
private var validLayout: (CGFloat, CGFloat)?
var isMuted = false
var isCameraPaused = false
@ -94,27 +94,21 @@ final class CallControllerButtonsNode: ASDisplayNode {
super.init()
}
func updateLayout(strings: PresentationStrings, constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = constrainedWidth
func updateLayout(strings: PresentationStrings, mode: CallControllerButtonsMode, constrainedWidth: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayout = (constrainedWidth, bottomInset)
self.mode = mode
if let mode = self.mode {
self.updateButtonsLayout(strings: strings, mode: mode, width: constrainedWidth, animated: transition.isAnimated)
}
}
func updateMode(strings: PresentationStrings, mode: CallControllerButtonsMode) {
if self.mode != mode {
let previousMode = self.mode
self.mode = mode
if let validLayout = self.validLayout {
self.updateButtonsLayout(strings: strings, mode: mode, width: validLayout, animated: previousMode != nil)
}
return self.updateButtonsLayout(strings: strings, mode: mode, width: constrainedWidth, bottomInset: bottomInset, animated: transition.isAnimated)
} else {
return 0.0
}
}
private var appliedMode: CallControllerButtonsMode?
private func updateButtonsLayout(strings: PresentationStrings, mode: CallControllerButtonsMode, width: CGFloat, animated: Bool) {
private func updateButtonsLayout(strings: PresentationStrings, mode: CallControllerButtonsMode, width: CGFloat, bottomInset: CGFloat, animated: Bool) -> CGFloat {
let transition: ContainedViewLayoutTransition
if animated {
transition = .animated(duration: 0.3, curve: .spring)
@ -151,6 +145,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
let frame: CGRect
}
let height: CGFloat
var buttons: [PlacedButton] = []
switch mode {
case .incoming(let speakerMode, let videoState), .outgoingRinging(let speakerMode, let videoState):
@ -205,6 +201,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: bottomButtonsLeftOffset, y: smallButtonSize + topBottomSpacing), size: CGSize(width: largeButtonSize, height: largeButtonSize))))
bottomButtonsLeftOffset += largeButtonSize + bottomButtonsSpacing
}
height = smallButtonSize + topBottomSpacing + largeButtonSize + max(bottomInset + 32.0, 46.0)
case let .active(speakerMode, videoState):
var topButtons: [ButtonDescription] = []
@ -238,9 +236,11 @@ final class CallControllerButtonsNode: ASDisplayNode {
let topButtonsWidth = CGFloat(topButtons.count) * smallButtonSize + CGFloat(topButtons.count - 1) * topButtonsSpacing
var topButtonsLeftOffset = floor((width - topButtonsWidth) / 2.0)
for button in topButtons {
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: topButtonsLeftOffset, y: smallButtonSize + topBottomSpacing), size: CGSize(width: smallButtonSize, height: smallButtonSize))))
buttons.append(PlacedButton(button: button, frame: CGRect(origin: CGPoint(x: topButtonsLeftOffset, y: 0.0), size: CGSize(width: smallButtonSize, height: smallButtonSize))))
topButtonsLeftOffset += smallButtonSize + topButtonsSpacing
}
height = smallButtonSize + max(bottomInset + 19.0, 46.0)
}
let delayIncrement = 0.015
@ -369,6 +369,8 @@ final class CallControllerButtonsNode: ASDisplayNode {
for key in removedKeys {
self.buttonNodes.removeValue(forKey: key)
}
return height
}
@objc func buttonPressed(_ button: CallControllerButtonItemNode) {

View File

@ -14,21 +14,63 @@ import LocalizedPeerData
import PhotoResources
import CallsEmoji
private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect {
return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t)))
}
private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat {
return (1.0 - value) * from + value * to
}
private final class IncomingVideoNode: ASDisplayNode {
private let videoView: UIView
private let videoView: PresentationCallVideoView
private var effectView: UIVisualEffectView?
private var isBlurred: Bool = false
init(videoView: UIView) {
private let isReadyUpdated: () -> Void
private(set) var isReady: Bool = false
private var isReadyTimer: SwiftSignalKit.Timer?
init(videoView: PresentationCallVideoView, isReadyUpdated: @escaping () -> Void) {
self.videoView = videoView
self.isReadyUpdated = isReadyUpdated
super.init()
self.view.addSubview(self.videoView)
self.view.addSubview(self.videoView.view)
self.isReadyTimer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
strongSelf.isReadyUpdated()
}
}, queue: .mainQueue())
self.isReadyTimer?.start()
videoView.setOnFirstFrameReceived { [weak self] in
Queue.mainQueue().async {
guard let strongSelf = self else {
return
}
if !strongSelf.isReady {
strongSelf.isReady = true
strongSelf.isReadyTimer?.invalidate()
strongSelf.isReadyUpdated()
}
}
}
}
deinit {
self.isReadyTimer?.invalidate()
}
func updateLayout(size: CGSize) {
self.videoView.frame = CGRect(origin: CGPoint(), size: size)
self.videoView.view.frame = CGRect(origin: CGPoint(), size: size)
}
func updateIsBlurred(isBlurred: Bool) {
@ -41,7 +83,7 @@ private final class IncomingVideoNode: ASDisplayNode {
if self.effectView == nil {
let effectView = UIVisualEffectView()
self.effectView = effectView
effectView.frame = self.videoView.frame
effectView.frame = self.videoView.view.frame
self.view.addSubview(effectView)
}
UIView.animate(withDuration: 0.3, animations: {
@ -57,26 +99,26 @@ private final class IncomingVideoNode: ASDisplayNode {
private final class OutgoingVideoNode: ASDisplayNode {
private let videoTransformContainer: ASDisplayNode
private let videoView: UIView
private let videoView: PresentationCallVideoView
private let buttonNode: HighlightTrackingButtonNode
private var effectView: UIVisualEffectView?
private var isBlurred: Bool = false
private var isExpanded: Bool = false
private var currentCornerRadius: CGFloat = 0.0
var tapped: (() -> Void)?
init(videoView: UIView) {
init(videoView: PresentationCallVideoView) {
self.videoTransformContainer = ASDisplayNode()
self.videoTransformContainer.clipsToBounds = true
self.videoView = videoView
self.videoView.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.videoView.view.layer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
self.buttonNode = HighlightTrackingButtonNode()
super.init()
self.videoTransformContainer.view.addSubview(self.videoView)
self.videoTransformContainer.view.addSubview(self.videoView.view)
self.addSubnode(self.videoTransformContainer)
//self.addSubnode(self.buttonNode)
@ -87,10 +129,10 @@ private final class OutgoingVideoNode: ASDisplayNode {
self.tapped?()
}
func updateLayout(size: CGSize, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
func updateLayout(size: CGSize, cornerRadius: CGFloat, transition: ContainedViewLayoutTransition) {
let videoFrame = CGRect(origin: CGPoint(), size: size)
self.buttonNode.frame = videoFrame
self.isExpanded = isExpanded
self.currentCornerRadius = cornerRadius
let previousVideoFrame = self.videoTransformContainer.frame
self.videoTransformContainer.frame = videoFrame
@ -99,11 +141,11 @@ private final class OutgoingVideoNode: ASDisplayNode {
transition.animateTransformScale(node: self.videoTransformContainer, from: previousVideoFrame.height / videoFrame.height)
}
self.videoView.frame = videoFrame
self.videoView.view.frame = videoFrame
transition.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: isExpanded ? 0.0 : 16.0)
transition.updateCornerRadius(layer: self.videoTransformContainer.layer, cornerRadius: self.currentCornerRadius)
if let effectView = self.effectView {
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: isExpanded ? 0.0 : 16.0)
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: self.currentCornerRadius)
}
}
@ -117,9 +159,9 @@ private final class OutgoingVideoNode: ASDisplayNode {
if self.effectView == nil {
let effectView = UIVisualEffectView()
effectView.clipsToBounds = true
effectView.layer.cornerRadius = self.isExpanded ? 0.0 : 16.0
effectView.layer.cornerRadius = self.currentCornerRadius
self.effectView = effectView
effectView.frame = self.videoView.frame
effectView.frame = self.videoView.view.frame
self.view.addSubview(effectView)
}
UIView.animate(withDuration: 0.3, animations: {
@ -133,7 +175,7 @@ private final class OutgoingVideoNode: ASDisplayNode {
}
}
final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
final class CallControllerNode: ViewControllerTracingNode, CallControllerNodeProtocol {
private enum VideoNodeCorner {
case topLeft
case topRight
@ -153,6 +195,7 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
private let easyDebugAccess: Bool
private let call: PresentationCall
private let containerTransformationNode: ASDisplayNode
private let containerNode: ASDisplayNode
private let imageNode: TransformImageNode
@ -202,9 +245,21 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
var callEnded: ((Bool) -> Void)?
var dismissedInteractively: (() -> Void)?
private var buttonsMode: CallControllerButtonsMode?
private var isUIHidden: Bool = false
private var isVideoPaused: Bool = false
private enum PictureInPictureGestureState {
case none
case collapsing(didSelectCorner: Bool)
case dragging(initialPosition: CGPoint, draggingPosition: CGPoint)
}
private var pictureInPictureGestureState: PictureInPictureGestureState = .none
private var pictureInPictureCorner: VideoNodeCorner = .topRight
private var pictureInPictureTransitionFraction: CGFloat = 0.0
init(sharedContext: SharedAccountContext, account: Account, presentationData: PresentationData, statusBar: StatusBar, debugInfo: Signal<(String, String), NoError>, shouldStayHiddenUntilConnection: Bool = false, easyDebugAccess: Bool, call: PresentationCall) {
self.sharedContext = sharedContext
self.account = account
@ -215,6 +270,9 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
self.easyDebugAccess = easyDebugAccess
self.call = call
self.containerTransformationNode = ASDisplayNode()
self.containerTransformationNode.clipsToBounds = true
self.containerNode = ASDisplayNode()
if self.shouldStayHiddenUntilConnection {
self.containerNode.alpha = 0.0
@ -242,13 +300,10 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
super.init()
self.setViewBlock({
return UITracingLayerView()
})
self.containerNode.backgroundColor = .black
self.addSubnode(self.containerNode)
self.addSubnode(self.containerTransformationNode)
self.containerTransformationNode.addSubnode(self.containerNode)
self.backButtonNode.setTitle(presentationData.strings.Common_Back, with: Font.regular(17.0), with: .white, for: [])
self.backButtonNode.hitTestSlop = UIEdgeInsets(top: -8.0, left: -20.0, bottom: -8.0, right: -8.0)
@ -377,7 +432,14 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
return
}
if let incomingVideoView = incomingVideoView {
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView)
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView, isReadyUpdated: {
guard let strongSelf = self else {
return
}
if let (layout, navigationBarHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.5, curve: .spring))
}
})
strongSelf.incomingVideoNode = incomingVideoNode
strongSelf.containerNode.insertSubnode(incomingVideoNode, aboveSubnode: strongSelf.dimNode)
if let (layout, navigationBarHeight) = strongSelf.validLayout {
@ -399,8 +461,8 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
return
}
if let outgoingVideoView = outgoingVideoView {
outgoingVideoView.backgroundColor = .black
outgoingVideoView.clipsToBounds = true
outgoingVideoView.view.backgroundColor = .black
outgoingVideoView.view.clipsToBounds = true
if let audioOutputState = strongSelf.audioOutputState, let currentOutput = audioOutputState.currentOutput {
switch currentOutput {
case .speaker, .builtin:
@ -436,6 +498,7 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
}
if let incomingVideoNode = self.incomingVideoNode {
incomingVideoNode.isHidden = !incomingVideoNode.isReady
let isActive: Bool
switch callState.remoteVideoState {
case .inactive:
@ -457,37 +520,42 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
switch callState.state {
case .waiting, .connecting:
statusValue = .text(self.presentationData.strings.Call_StatusConnecting)
statusValue = .text(string: self.presentationData.strings.Call_StatusConnecting, displayLogo: false)
case let .requesting(ringing):
if ringing {
statusValue = .text(self.presentationData.strings.Call_StatusRinging)
statusValue = .text(string: self.presentationData.strings.Call_StatusRinging, displayLogo: false)
} else {
statusValue = .text(self.presentationData.strings.Call_StatusRequesting)
statusValue = .text(string: self.presentationData.strings.Call_StatusRequesting, displayLogo: false)
}
case .terminating:
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
case let .terminated(_, reason, _):
if let reason = reason {
switch reason {
case let .ended(type):
switch type {
case .busy:
statusValue = .text(self.presentationData.strings.Call_StatusBusy)
statusValue = .text(string: self.presentationData.strings.Call_StatusBusy, displayLogo: false)
case .hungUp, .missed:
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
}
case .error:
statusValue = .text(self.presentationData.strings.Call_StatusFailed)
statusValue = .text(string: self.presentationData.strings.Call_StatusFailed, displayLogo: false)
}
} else {
statusValue = .text(self.presentationData.strings.Call_StatusEnded)
statusValue = .text(string: self.presentationData.strings.Call_StatusEnded, displayLogo: false)
}
case .ringing:
var text = self.presentationData.strings.Call_StatusIncoming
var text: String
if self.call.isVideo {
text = self.presentationData.strings.Call_IncomingVideoCall
} else {
text = self.presentationData.strings.Call_IncomingVoiceCall
}
if !self.statusNode.subtitle.isEmpty {
text += "\n\(self.statusNode.subtitle)"
}
statusValue = .text(text)
statusValue = .text(string: text, displayLogo: true)
case .active(let timestamp, let reception, let keyVisualHash), .reconnecting(let timestamp, let reception, let keyVisualHash):
let strings = self.presentationData.strings
var isReconnecting = false
@ -517,28 +585,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
}
statusReception = reception
}
switch callState.state {
case .terminated, .terminating:
if !self.statusNode.alpha.isEqual(to: 0.5) {
self.statusNode.alpha = 0.5
self.buttonsNode.alpha = 0.5
self.keyButtonNode.alpha = 0.5
self.backButtonArrowNode.alpha = 0.5
self.backButtonNode.alpha = 0.5
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
self.buttonsNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
self.keyButtonNode.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.25)
}
default:
if !self.statusNode.alpha.isEqual(to: 1.0) {
self.statusNode.alpha = 1.0
self.buttonsNode.alpha = 1.0
self.keyButtonNode.alpha = 1.0
self.backButtonArrowNode.alpha = 1.0
self.backButtonNode.alpha = 1.0
}
}
if self.shouldStayHiddenUntilConnection {
switch callState.state {
case .connecting, .active:
@ -550,6 +596,15 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
self.statusNode.status = statusValue
self.statusNode.reception = statusReception
if let callState = self.callState {
switch callState.state {
case .active, .connecting, .reconnecting:
break
default:
self.isUIHidden = false
}
}
self.updateButtonsMode()
if case let .terminated(id, _, reportRating) = callState.state, let callId = id {
@ -598,24 +653,27 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
switch callState.state {
case .ringing:
let buttonsMode: CallControllerButtonsMode = .incoming(speakerMode: mode, videoState: mappedVideoState)
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
self.buttonsMode = .incoming(speakerMode: mode, videoState: mappedVideoState)
self.buttonsTerminationMode = buttonsMode
case .waiting, .requesting:
let buttonsMode: CallControllerButtonsMode = .outgoingRinging(speakerMode: mode, videoState: mappedVideoState)
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
self.buttonsMode = .outgoingRinging(speakerMode: mode, videoState: mappedVideoState)
self.buttonsTerminationMode = buttonsMode
case .active, .connecting, .reconnecting:
let buttonsMode: CallControllerButtonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsMode)
self.buttonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
self.buttonsTerminationMode = buttonsMode
case .terminating, .terminated:
if let buttonsTerminationMode = self.buttonsTerminationMode {
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: buttonsTerminationMode)
self.buttonsMode = buttonsTerminationMode
} else {
self.buttonsNode.updateMode(strings: self.presentationData.strings, mode: .active(speakerMode: mode, videoState: mappedVideoState))
self.buttonsMode = .active(speakerMode: mode, videoState: mappedVideoState)
}
}
if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.3, curve: .spring))
}
}
func animateIn() {
@ -645,70 +703,110 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
}
}
func expandFromPipIfPossible() {
if self.pictureInPictureTransitionFraction.isEqual(to: 1.0), let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
}
private func calculatePreviewVideoRect(layout: ContainerViewLayout, navigationHeight: CGFloat) -> CGRect {
let buttonsHeight: CGFloat = 190.0
let buttonsOffset: CGFloat
if layout.size.width.isEqual(to: 320.0) {
if layout.size.height.isEqual(to: 480.0) {
buttonsOffset = 60.0
} else {
buttonsOffset = 73.0
}
} else {
buttonsOffset = 83.0
}
var uiDisplayTransition: CGFloat = self.isUIHidden ? 0.0 : 1.0
uiDisplayTransition *= 1.0 - self.pictureInPictureTransitionFraction
let buttonsOriginY: CGFloat
if self.isUIHidden {
buttonsOriginY = layout.size.height + 40.0 - 80.0
} else {
buttonsOriginY = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
}
let buttonsHeight: CGFloat = self.buttonsNode.bounds.height
let previewVideoSize = layout.size.aspectFitted(CGSize(width: 200.0, height: 200.0))
var insets = layout.insets(options: .statusBar)
insets.top += 44.0 + 8.0
insets.bottom = buttonsHeight + 27.0
insets.left = 20.0
insets.right = 20.0
let expandedInset: CGFloat = 16.0
insets.top = interpolate(from: expandedInset, to: insets.top, value: uiDisplayTransition)
insets.bottom = interpolate(from: expandedInset, to: insets.bottom, value: uiDisplayTransition)
insets.left = interpolate(from: expandedInset, to: insets.left, value: 1.0 - self.pictureInPictureTransitionFraction)
insets.right = interpolate(from: expandedInset, to: insets.right, value: 1.0 - self.pictureInPictureTransitionFraction)
let previewVideoSide = interpolate(from: 350.0, to: 200.0, value: 1.0 - self.pictureInPictureTransitionFraction)
let previewVideoSize = layout.size.aspectFitted(CGSize(width: previewVideoSide, height: previewVideoSide))
let previewVideoY: CGFloat
let previewVideoX: CGFloat
switch self.outgoingVideoNodeCorner {
case .topLeft:
previewVideoX = 20.0
if self.isUIHidden {
previewVideoY = layout.insets(options: .statusBar).top + 8.0
} else {
previewVideoY = layout.insets(options: .statusBar).top + 44.0 + 8.0
}
previewVideoX = insets.left
previewVideoY = insets.top
case .topRight:
previewVideoX = layout.size.width - previewVideoSize.width - 20.0
if self.isUIHidden {
previewVideoY = layout.insets(options: .statusBar).top + 8.0
} else {
previewVideoY = layout.insets(options: .statusBar).top + 44.0 + 8.0
}
previewVideoX = layout.size.width - previewVideoSize.width - insets.right
previewVideoY = insets.top
case .bottomLeft:
previewVideoX = 20.0
if self.isUIHidden {
previewVideoY = layout.size.height - layout.intrinsicInsets.bottom - 8.0 - previewVideoSize.height
} else {
previewVideoY = buttonsOriginY + 100.0 - previewVideoSize.height
}
previewVideoX = insets.left
previewVideoY = layout.size.height - insets.bottom - previewVideoSize.height
case .bottomRight:
previewVideoX = layout.size.width - previewVideoSize.width - 20.0
if self.isUIHidden {
previewVideoY = layout.size.height - layout.intrinsicInsets.bottom - 8.0 - previewVideoSize.height
} else {
previewVideoY = buttonsOriginY + 100.0 - previewVideoSize.height
}
previewVideoX = layout.size.width - previewVideoSize.width - insets.right
previewVideoY = layout.size.height - insets.bottom - previewVideoSize.height
}
return CGRect(origin: CGPoint(x: previewVideoX, y: previewVideoY), size: previewVideoSize)
}
private func calculatePictureInPictureContainerRect(layout: ContainerViewLayout, navigationHeight: CGFloat) -> CGRect {
let pictureInPictureTopInset: CGFloat = layout.insets(options: .statusBar).top + 44.0 + 8.0
let pictureInPictureSideInset: CGFloat = 8.0
let pictureInPictureSize = layout.size.fitted(CGSize(width: 240.0, height: 240.0))
let pictureInPictureBottomInset: CGFloat = layout.insets(options: .input).bottom + 44.0 + 8.0
let containerPictureInPictureFrame: CGRect
switch self.pictureInPictureCorner {
case .topLeft:
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: pictureInPictureSideInset, y: pictureInPictureTopInset), size: pictureInPictureSize)
case .topRight:
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: layout.size.width - pictureInPictureSideInset - pictureInPictureSize.width, y: pictureInPictureTopInset), size: pictureInPictureSize)
case .bottomLeft:
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: pictureInPictureSideInset, y: layout.size.height - pictureInPictureBottomInset - pictureInPictureSize.height), size: pictureInPictureSize)
case .bottomRight:
containerPictureInPictureFrame = CGRect(origin: CGPoint(x: layout.size.width - pictureInPictureSideInset - pictureInPictureSize.width, y: layout.size.height - pictureInPictureBottomInset - pictureInPictureSize.height), size: pictureInPictureSize)
}
return containerPictureInPictureFrame
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
let overlayAlpha: CGFloat = self.isUIHidden ? 0.0 : 1.0
var uiDisplayTransition: CGFloat = self.isUIHidden ? 0.0 : 1.0
uiDisplayTransition *= 1.0 - self.pictureInPictureTransitionFraction
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let buttonsHeight: CGFloat
if let buttonsMode = self.buttonsMode {
buttonsHeight = self.buttonsNode.updateLayout(strings: self.presentationData.strings, mode: buttonsMode, constrainedWidth: layout.size.width, bottomInset: layout.intrinsicInsets.bottom, transition: transition)
} else {
buttonsHeight = 0.0
}
let defaultButtonsOriginY = layout.size.height - buttonsHeight
let buttonsOriginY = interpolate(from: layout.size.height + 10.0, to: defaultButtonsOriginY, value: uiDisplayTransition)
var overlayAlpha: CGFloat = uiDisplayTransition
switch self.callState?.state {
case .terminated, .terminating:
overlayAlpha *= 0.5
default:
break
}
let containerFullScreenFrame = CGRect(origin: CGPoint(), size: layout.size)
let containerPictureInPictureFrame = self.calculatePictureInPictureContainerRect(layout: layout, navigationHeight: navigationBarHeight)
let containerFrame = interpolateFrame(from: containerFullScreenFrame, to: containerPictureInPictureFrame, t: self.pictureInPictureTransitionFraction)
transition.updateFrame(node: self.containerTransformationNode, frame: containerFrame)
transition.updateSublayerTransformScale(node: self.containerTransformationNode, scale: min(1.0, containerFrame.width / layout.size.width * 1.01))
transition.updateCornerRadius(layer: self.containerTransformationNode.layer, cornerRadius: self.pictureInPictureTransitionFraction * 10.0)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: (containerFrame.width - layout.size.width) / 2.0, y: floor(containerFrame.height - layout.size.height) / 2.0), size: layout.size))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
if let keyPreviewNode = self.keyPreviewNode {
@ -751,18 +849,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
statusOffset += layout.safeInsets.top
let buttonsHeight: CGFloat = 190.0
let buttonsOffset: CGFloat
if layout.size.width.isEqual(to: 320.0) {
if layout.size.height.isEqual(to: 480.0) {
buttonsOffset = 60.0
} else {
buttonsOffset = 73.0
}
} else {
buttonsOffset = 83.0
}
let statusHeight = self.statusNode.updateLayout(constrainedWidth: layout.size.width, transition: transition)
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: 0.0, y: statusOffset), size: CGSize(width: layout.size.width, height: statusHeight)))
transition.updateAlpha(node: self.statusNode, alpha: overlayAlpha)
@ -770,13 +856,6 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
let videoPausedSize = self.videoPausedNode.updateLayout(CGSize(width: layout.size.width - 16.0, height: 100.0))
transition.updateFrame(node: self.videoPausedNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - videoPausedSize.width) / 2.0), y: floor((layout.size.height - videoPausedSize.height) / 2.0)), size: videoPausedSize))
self.buttonsNode.updateLayout(strings: self.presentationData.strings, constrainedWidth: layout.size.width, transition: transition)
let buttonsOriginY: CGFloat
if self.isUIHidden {
buttonsOriginY = layout.size.height + 40.0 - 80.0
} else {
buttonsOriginY = layout.size.height - (buttonsOffset - 40.0) - buttonsHeight - layout.intrinsicInsets.bottom
}
transition.updateFrame(node: self.buttonsNode, frame: CGRect(origin: CGPoint(x: 0.0, y: buttonsOriginY), size: CGSize(width: layout.size.width, height: buttonsHeight)))
transition.updateAlpha(node: self.buttonsNode, alpha: overlayAlpha)
@ -801,18 +880,18 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
if outgoingVideoNode.frame.isEmpty {
outgoingVideoTransition = .immediate
}
if self.incomingVideoNode == nil {
outgoingVideoNode.frame = fullscreenVideoFrame
outgoingVideoNode.updateLayout(size: layout.size, isExpanded: true, transition: outgoingVideoTransition)
} else {
if let incomingVideoNode = self.incomingVideoNode, incomingVideoNode.isReady {
if self.minimizedVideoDraggingPosition == nil {
if self.outgoingVideoExplicitelyFullscreen {
outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: fullscreenVideoFrame)
} else {
outgoingVideoTransition.updateFrame(node: outgoingVideoNode, frame: previewVideoFrame)
}
outgoingVideoNode.updateLayout(size: outgoingVideoNode.frame.size, isExpanded: self.outgoingVideoExplicitelyFullscreen, transition: outgoingVideoTransition)
outgoingVideoNode.updateLayout(size: outgoingVideoNode.frame.size, cornerRadius: interpolate(from: self.outgoingVideoExplicitelyFullscreen ? 0.0 : 14.0, to: 24.0, value: self.pictureInPictureTransitionFraction), transition: outgoingVideoTransition)
}
} else {
outgoingVideoNode.frame = fullscreenVideoFrame
outgoingVideoNode.updateLayout(size: layout.size, cornerRadius: 0.0, transition: outgoingVideoTransition)
}
}
@ -861,12 +940,27 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
if let _ = self.keyPreviewNode {
if !self.pictureInPictureTransitionFraction.isZero {
if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
} else if let _ = self.keyPreviewNode {
self.backPressed()
} else {
if self.incomingVideoNode != nil || self.outgoingVideoNode != nil {
self.isUIHidden = !self.isUIHidden
if let (layout, navigationBarHeight) = self.validLayout {
var updated = false
if let callState = self.callState {
switch callState.state {
case .active, .connecting, .reconnecting:
self.isUIHidden = !self.isUIHidden
updated = true
default:
break
}
}
if updated, let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.3, curve: .easeInOut))
}
} else {
@ -1034,13 +1128,15 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
switch recognizer.state {
case .began:
let location = recognizer.location(in: self.view)
//let translation = recognizer.translation(in: self.view)
//location.x += translation.x
//location.y += translation.y
if let _ = self.incomingVideoNode, let outgoingVideoNode = self.outgoingVideoNode, outgoingVideoNode.frame.contains(location) {
if self.self.pictureInPictureTransitionFraction.isZero, let _ = self.incomingVideoNode, let outgoingVideoNode = self.outgoingVideoNode, outgoingVideoNode.frame.contains(location) {
self.minimizedVideoInitialPosition = outgoingVideoNode.position
} else {
self.minimizedVideoInitialPosition = nil
if !self.pictureInPictureTransitionFraction.isZero {
self.pictureInPictureGestureState = .dragging(initialPosition: self.containerTransformationNode.position, draggingPosition: self.containerTransformationNode.position)
} else {
self.pictureInPictureGestureState = .collapsing(didSelectCorner: false)
}
}
case .changed:
if let outgoingVideoNode = self.outgoingVideoNode, let minimizedVideoInitialPosition = self.minimizedVideoInitialPosition {
@ -1049,10 +1145,43 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
self.minimizedVideoDraggingPosition = minimizedVideoDraggingPosition
outgoingVideoNode.position = minimizedVideoDraggingPosition
} else {
let offset = recognizer.translation(in: self.view).y
var bounds = self.bounds
bounds.origin.y = -offset
self.bounds = bounds
switch self.pictureInPictureGestureState {
case .none:
let offset = recognizer.translation(in: self.view).y
var bounds = self.bounds
bounds.origin.y = -offset
self.bounds = bounds
case let .collapsing(didSelectCorner):
if let (layout, navigationHeight) = self.validLayout {
let offset = recognizer.translation(in: self.view)
if !didSelectCorner {
self.pictureInPictureGestureState = .collapsing(didSelectCorner: true)
if offset.x < 0.0 {
self.pictureInPictureCorner = .topLeft
} else {
self.pictureInPictureCorner = .topRight
}
}
let maxOffset: CGFloat = min(300.0, layout.size.height / 2.0)
let offsetTransition = max(0.0, min(1.0, abs(offset.y) / maxOffset))
self.pictureInPictureTransitionFraction = offsetTransition
switch self.pictureInPictureCorner {
case .topRight, .bottomRight:
self.pictureInPictureCorner = offset.y < 0.0 ? .topRight : .bottomRight
case .topLeft, .bottomLeft:
self.pictureInPictureCorner = offset.y < 0.0 ? .topLeft : .bottomLeft
}
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .immediate)
}
case .dragging(let initialPosition, var draggingPosition):
let translation = recognizer.translation(in: self.view)
draggingPosition.x = initialPosition.x + translation.x
draggingPosition.y = initialPosition.y + translation.y
self.pictureInPictureGestureState = .dragging(initialPosition: initialPosition, draggingPosition: draggingPosition)
self.containerTransformationNode.position = draggingPosition
}
}
case .cancelled, .ended:
if let outgoingVideoNode = self.outgoingVideoNode, let _ = self.minimizedVideoInitialPosition, let minimizedVideoDraggingPosition = self.minimizedVideoDraggingPosition {
@ -1067,25 +1196,62 @@ final class CallControllerNode: ASDisplayNode, CallControllerNodeProtocol {
outgoingVideoNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: minimizedVideoDraggingPosition.x - videoFrame.midX, y: minimizedVideoDraggingPosition.y - videoFrame.midY)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 110.0, removeOnCompletion: true, additive: true, completion: nil)
}
} else {
let velocity = recognizer.velocity(in: self.view).y
if abs(velocity) < 100.0 {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint()
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
} else {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
self?.dismissedInteractively?()
})
switch self.pictureInPictureGestureState {
case .none:
let velocity = recognizer.velocity(in: self.view).y
if abs(velocity) < 100.0 {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint()
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
} else {
var bounds = self.bounds
let previous = bounds
bounds.origin = CGPoint(x: 0.0, y: velocity > 0.0 ? -bounds.height: bounds.height)
self.bounds = bounds
self.layer.animateBounds(from: previous, to: bounds, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in
self?.dismissedInteractively?()
})
}
case .collapsing:
self.pictureInPictureGestureState = .none
let velocity = recognizer.velocity(in: self.view).y
if abs(velocity) < 100.0 {
if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 0.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
} else {
if let (layout, navigationHeight) = self.validLayout {
self.pictureInPictureTransitionFraction = 1.0
self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
}
case let .dragging(initialPosition, _):
self.pictureInPictureGestureState = .none
if let (layout, navigationHeight) = self.validLayout {
let translation = recognizer.translation(in: self.view)
let draggingPosition = CGPoint(x: initialPosition.x + translation.x, y: initialPosition.y + translation.y)
self.pictureInPictureCorner = self.nodeLocationForPosition(layout: layout, position: draggingPosition, velocity: recognizer.velocity(in: self.view))
let containerFrame = self.calculatePictureInPictureContainerRect(layout: layout, navigationHeight: navigationHeight)
self.containerTransformationNode.frame = containerFrame
containerTransformationNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: draggingPosition.x - containerFrame.midX, y: draggingPosition.y - containerFrame.midY)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: 0.0, initialVelocity: 0.0, damping: 110.0, removeOnCompletion: true, additive: true, completion: nil)
}
}
}
default:
break
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.containerTransformationNode.frame.contains(point) {
return self.containerTransformationNode.view.hitTest(self.view.convert(point, to: self.containerTransformationNode.view), with: event)
}
return nil
}
}

View File

@ -11,13 +11,13 @@ private let compactStatusFont = Font.regular(18.0)
private let regularStatusFont = Font.regular(18.0)
enum CallControllerStatusValue: Equatable {
case text(String)
case text(string: String, displayLogo: Bool)
case timer((String) -> String, Double)
static func ==(lhs: CallControllerStatusValue, rhs: CallControllerStatusValue) -> Bool {
switch lhs {
case let .text(text):
if case .text(text) = rhs {
case let .text(text, displayLogo):
if case .text(text, displayLogo) = rhs {
return true
} else {
return false
@ -37,10 +37,11 @@ final class CallControllerStatusNode: ASDisplayNode {
private let statusNode: TextNode
private let statusMeasureNode: TextNode
private let receptionNode: CallControllerReceptionNode
private let logoNode: ASImageNode
var title: String = ""
var subtitle: String = ""
var status: CallControllerStatusValue = .text("") {
var status: CallControllerStatusValue = .text(string: "", displayLogo: false) {
didSet {
if self.status != oldValue {
self.statusTimer?.invalidate()
@ -96,6 +97,10 @@ final class CallControllerStatusNode: ASDisplayNode {
self.receptionNode = CallControllerReceptionNode()
self.receptionNode.alpha = 0.0
self.logoNode = ASImageNode()
self.logoNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/CallTitleLogo"), color: .white)
self.logoNode.isHidden = true
super.init()
self.isUserInteractionEnabled = false
@ -103,6 +108,7 @@ final class CallControllerStatusNode: ASDisplayNode {
self.addSubnode(self.titleNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.receptionNode)
self.addSubnode(self.logoNode)
}
deinit {
@ -125,29 +131,34 @@ final class CallControllerStatusNode: ASDisplayNode {
var statusOffset: CGFloat = 0.0
let statusText: String
let statusMeasureText: String
var statusDisplayLogo: Bool = false
switch self.status {
case let .text(text):
statusText = text
statusMeasureText = text
case let .timer(format, referenceTime):
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
let durationString: String
let measureDurationString: String
if duration > 60 * 60 {
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
measureDurationString = "00:00:00"
} else {
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
measureDurationString = "00:00"
}
statusText = format(durationString)
statusMeasureText = format(measureDurationString)
if self.reception != nil {
statusOffset += 8.0
}
case let .text(text, displayLogo):
statusText = text
statusMeasureText = text
statusDisplayLogo = displayLogo
if displayLogo {
statusOffset += 10.0
}
case let .timer(format, referenceTime):
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
let durationString: String
let measureDurationString: String
if duration > 60 * 60 {
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
measureDurationString = "00:00:00"
} else {
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
measureDurationString = "00:00"
}
statusText = format(durationString)
statusMeasureText = format(measureDurationString)
if self.reception != nil {
statusOffset += 8.0
}
}
let spacing: CGFloat = 4.0
let spacing: CGFloat = 1.0
let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: nameFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
@ -159,6 +170,10 @@ final class CallControllerStatusNode: ASDisplayNode {
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size)
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0) + statusOffset, y: titleLayout.size.height + spacing), size: statusLayout.size)
self.receptionNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - receptionNodeSize.width, y: titleLayout.size.height + spacing + 9.0), size: receptionNodeSize)
self.logoNode.isHidden = !statusDisplayLogo
if let image = self.logoNode.image {
self.logoNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - image.size.width - 7.0, y: self.statusNode.frame.minY + 5.0), size: image.size)
}
return titleLayout.size.height + spacing + statusLayout.size.height
}

View File

@ -106,7 +106,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
private var outgoingVideoViewRequested: Bool = false
private let backButtonArrowNode: ASImageNode
private let backButtonNode: HighlightableButtonNode
private let statusNode: CallControllerStatusNode
private let statusNode: LegacyCallControllerStatusNode
private let videoPausedNode: ImmediateTextNode
private let buttonsNode: LegacyCallControllerButtonsNode
private var keyPreviewNode: CallControllerKeyPreviewNode?
@ -168,7 +168,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
self.backButtonArrowNode.image = NavigationBarTheme.generateBackArrowImage(color: .white)
self.backButtonNode = HighlightableButtonNode()
self.statusNode = CallControllerStatusNode()
self.statusNode = LegacyCallControllerStatusNode()
self.videoPausedNode = ImmediateTextNode()
self.videoPausedNode.alpha = 0.0
@ -291,7 +291,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
func updateCallState(_ callState: PresentationCallState) {
self.callState = callState
let statusValue: CallControllerStatusValue
let statusValue: LegacyCallControllerStatusValue
var statusReception: Int32?
switch callState.videoState {
@ -304,7 +304,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
}
if let incomingVideoView = incomingVideoView {
strongSelf.setCurrentAudioOutput?(.speaker)
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView)
let incomingVideoNode = IncomingVideoNode(videoView: incomingVideoView.view)
strongSelf.incomingVideoNode = incomingVideoNode
strongSelf.containerNode.insertSubnode(incomingVideoNode, aboveSubnode: strongSelf.dimNode)
strongSelf.statusNode.isHidden = true
@ -320,7 +320,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
guard let strongSelf = self else {
return
}
if let outgoingVideoView = outgoingVideoView {
if let outgoingVideoView = outgoingVideoView?.view {
outgoingVideoView.backgroundColor = .black
outgoingVideoView.clipsToBounds = true
strongSelf.setCurrentAudioOutput?(.speaker)
@ -349,7 +349,7 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
guard let strongSelf = self else {
return
}
if let outgoingVideoView = outgoingVideoView {
if let outgoingVideoView = outgoingVideoView?.view {
outgoingVideoView.backgroundColor = .black
outgoingVideoView.clipsToBounds = true
outgoingVideoView.layer.cornerRadius = 16.0
@ -569,6 +569,9 @@ final class LegacyCallControllerNode: ASDisplayNode, CallControllerNodeProtocol
}
}
func expandFromPipIfPossible() {
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)

View File

@ -0,0 +1,221 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
private let compactNameFont = Font.regular(28.0)
private let regularNameFont = Font.regular(36.0)
private let compactStatusFont = Font.regular(18.0)
private let regularStatusFont = Font.regular(18.0)
enum LegacyCallControllerStatusValue: Equatable {
case text(String)
case timer((String) -> String, Double)
static func ==(lhs: LegacyCallControllerStatusValue, rhs: LegacyCallControllerStatusValue) -> Bool {
switch lhs {
case let .text(text):
if case .text(text) = rhs {
return true
} else {
return false
}
case let .timer(_, referenceTime):
if case .timer(_, referenceTime) = rhs {
return true
} else {
return false
}
}
}
}
final class LegacyCallControllerStatusNode: ASDisplayNode {
private let titleNode: TextNode
private let statusNode: TextNode
private let statusMeasureNode: TextNode
private let receptionNode: LegacyCallControllerReceptionNode
var title: String = ""
var subtitle: String = ""
var status: LegacyCallControllerStatusValue = .text("") {
didSet {
if self.status != oldValue {
self.statusTimer?.invalidate()
if case .timer = self.status {
self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in
if let strongSelf = self, let validLayoutWidth = strongSelf.validLayoutWidth {
let _ = strongSelf.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
}
}, queue: Queue.mainQueue())
self.statusTimer?.start()
} else {
if let validLayoutWidth = self.validLayoutWidth {
let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
}
}
}
}
}
var reception: Int32? {
didSet {
if self.reception != oldValue {
if let reception = self.reception {
self.receptionNode.reception = reception
if oldValue == nil {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
transition.updateAlpha(node: self.receptionNode, alpha: 1.0)
}
} else if self.reception == nil, oldValue != nil {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
transition.updateAlpha(node: self.receptionNode, alpha: 0.0)
}
if (oldValue == nil) != (self.reception != nil) {
if let validLayoutWidth = self.validLayoutWidth {
let _ = self.updateLayout(constrainedWidth: validLayoutWidth, transition: .immediate)
}
}
}
}
}
private var statusTimer: SwiftSignalKit.Timer?
private var validLayoutWidth: CGFloat?
override init() {
self.titleNode = TextNode()
self.statusNode = TextNode()
self.statusNode.displaysAsynchronously = false
self.statusMeasureNode = TextNode()
self.receptionNode = LegacyCallControllerReceptionNode()
self.receptionNode.alpha = 0.0
super.init()
self.isUserInteractionEnabled = false
self.addSubnode(self.titleNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.receptionNode)
}
deinit {
self.statusTimer?.invalidate()
}
func updateLayout(constrainedWidth: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
self.validLayoutWidth = constrainedWidth
let nameFont: UIFont
let statusFont: UIFont
if constrainedWidth < 330.0 {
nameFont = compactNameFont
statusFont = compactStatusFont
} else {
nameFont = regularNameFont
statusFont = regularStatusFont
}
var statusOffset: CGFloat = 0.0
let statusText: String
let statusMeasureText: String
switch self.status {
case let .text(text):
statusText = text
statusMeasureText = text
case let .timer(format, referenceTime):
let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime)
let durationString: String
let measureDurationString: String
if duration > 60 * 60 {
durationString = String(format: "%02d:%02d:%02d", arguments: [duration / 3600, (duration / 60) % 60, duration % 60])
measureDurationString = "00:00:00"
} else {
durationString = String(format: "%02d:%02d", arguments: [(duration / 60) % 60, duration % 60])
measureDurationString = "00:00"
}
statusText = format(durationString)
statusMeasureText = format(measureDurationString)
if self.reception != nil {
statusOffset += 8.0
}
}
let spacing: CGFloat = 4.0
let (titleLayout, titleApply) = TextNode.asyncLayout(self.titleNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: self.title, font: nameFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
let (statusMeasureLayout, statusMeasureApply) = TextNode.asyncLayout(self.statusMeasureNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusMeasureText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
let (statusLayout, statusApply) = TextNode.asyncLayout(self.statusNode)(TextNodeLayoutArguments(attributedString: NSAttributedString(string: statusText, font: statusFont, textColor: .white), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedWidth - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)))
let _ = titleApply()
let _ = statusApply()
let _ = statusMeasureApply()
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - titleLayout.size.width) / 2.0), y: 0.0), size: titleLayout.size)
self.statusNode.frame = CGRect(origin: CGPoint(x: floor((constrainedWidth - statusMeasureLayout.size.width) / 2.0) + statusOffset, y: titleLayout.size.height + spacing), size: statusLayout.size)
self.receptionNode.frame = CGRect(origin: CGPoint(x: self.statusNode.frame.minX - receptionNodeSize.width, y: titleLayout.size.height + spacing + 9.0), size: receptionNodeSize)
return titleLayout.size.height + spacing + statusLayout.size.height
}
}
private final class CallControllerReceptionNodeParameters: NSObject {
let reception: Int32
init(reception: Int32) {
self.reception = reception
}
}
private let receptionNodeSize = CGSize(width: 24.0, height: 10.0)
final class LegacyCallControllerReceptionNode : ASDisplayNode {
var reception: Int32 = 4 {
didSet {
self.setNeedsDisplay()
}
}
override init() {
super.init()
self.isOpaque = false
self.isLayerBacked = true
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return CallControllerReceptionNodeParameters(reception: self.reception)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(UIColor.white.cgColor)
if let parameters = parameters as? CallControllerReceptionNodeParameters{
let width: CGFloat = 3.0
var spacing: CGFloat = 1.5
if UIScreenScale > 2 {
spacing = 4.0 / 3.0
}
for i in 0 ..< 4 {
let height = 4.0 + 2.0 * CGFloat(i)
let rect = CGRect(x: bounds.minX + CGFloat(i) * (width + spacing), y: receptionNodeSize.height - height, width: width, height: height)
if i >= parameters.reception {
context.setAlpha(0.4)
}
let path = UIBezierPath(roundedRect: rect, cornerRadius: 0.5)
context.addPath(path.cgPath)
context.fillPath()
}
}
}
}

View File

@ -758,12 +758,34 @@ public final class PresentationCallImpl: PresentationCall {
return self.debugInfoValue.get()
}
public func makeIncomingVideoView(completion: @escaping (UIView?) -> Void) {
self.ongoingContext?.makeIncomingVideoView(completion: completion)
public func makeIncomingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) {
self.ongoingContext?.makeIncomingVideoView(completion: { view in
if let view = view {
completion(PresentationCallVideoView(
view: view,
setOnFirstFrameReceived: { [weak view] f in
view?.setOnFirstFrameReceived(f)
}
))
} else {
completion(nil)
}
})
}
public func makeOutgoingVideoView(completion: @escaping (UIView?) -> Void) {
self.videoCapturer?.makeOutgoingVideoView(completion: completion)
public func makeOutgoingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) {
self.videoCapturer?.makeOutgoingVideoView(completion: { view in
if let view = view {
completion(PresentationCallVideoView(
view: view,
setOnFirstFrameReceived: { [weak view] f in
view?.setOnFirstFrameReceived(f)
}
))
} else {
completion(nil)
}
})
}
public func switchVideoCamera() {

View File

@ -676,9 +676,13 @@ public final class SharedAccountContextImpl: SharedAccountContext {
mainWindow.inCallNavigate = { [weak self] in
if let strongSelf = self, let callController = strongSelf.callController {
if callController.isNodeLoaded && callController.view.superview == nil {
if callController.isNodeLoaded {
mainWindow.hostView.containerView.endEditing(true)
mainWindow.present(callController, on: .calls)
if callController.view.superview == nil {
mainWindow.present(callController, on: .calls)
} else {
callController.expandFromPipIfPossible()
}
}
}
}

View File

@ -111,6 +111,12 @@ public final class TelegramRootController: NavigationController {
}
let accountSettingsController = PeerInfoScreen(context: self.context, peerId: self.context.account.peerId, avatarInitiallyExpanded: false, isOpenedFromChat: false, nearbyPeerDistance: nil, callMessages: [], isSettings: true)
accountSettingsController.tabBarItemDebugTapAction = { [weak self, weak accountSettingsController] in
guard let strongSelf = self, let accountSettingsController = accountSettingsController else {
return
}
accountSettingsController.push(debugController(sharedContext: strongSelf.context.sharedContext, context: strongSelf.context))
}
controllers.append(accountSettingsController)
tabBarController.setControllers(controllers, selectedIndex: restoreSettignsController != nil ? (controllers.count - 1) : (controllers.count - 2))

View File

@ -104,8 +104,16 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
private var initializedStatus = false
private let _status = Promise<MediaPlayerStatus>()
private let _thumbnailStatus = Promise<MediaPlayerStatus?>(nil)
var status: Signal<MediaPlayerStatus, NoError> {
return self._status.get()
return combineLatest(self._thumbnailStatus.get(), self._status.get())
|> map { thumbnailStatus, status in
if let thumbnailStatus = thumbnailStatus {
return thumbnailStatus
} else {
return status
}
}
}
private let _bufferingStatus = Promise<(IndexSet, Int)?>()
@ -183,7 +191,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
self.addSubnode(self.imageNode)
self.addSubnode(self.playerNode)
self._status.set(combineLatest(self.dimensionsPromise.get(), self.player.status)
|> map { [weak self] dimensions, status in
|> map { dimensions, status in
return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: dimensions, timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled)
})
@ -249,6 +257,11 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
self.thumbnailNode = thumbnailNode
thumbnailPlayer.attachPlayerNode(thumbnailNode)
self._thumbnailStatus.set(thumbnailPlayer.status
|> map { status in
return MediaPlayerStatus(generationTimestamp: status.generationTimestamp, duration: status.duration, dimensions: CGSize(), timestamp: status.timestamp, baseRate: status.baseRate, seekId: status.seekId, status: status.status, soundEnabled: status.soundEnabled)
})
self.addSubnode(thumbnailNode)
thumbnailNode.frame = self.playerNode.frame

View File

@ -302,7 +302,7 @@ public final class OngoingCallVideoCapturer {
self.impl.switchVideoCamera()
}
public func makeOutgoingVideoView(completion: @escaping (UIView?) -> Void) {
public func makeOutgoingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) {
self.impl.makeOutgoingVideoView(completion)
}
@ -418,6 +418,10 @@ private extension OngoingCallContextState.State {
}
}*/
public protocol OngoingCallContextPresentationCallVideoView: UIView {
func setOnFirstFrameReceived(_ onFirstFrameReceived: (() -> Void)?)
}
public final class OngoingCallContext {
public struct AuxiliaryServer {
public enum Connection {
@ -725,7 +729,7 @@ public final class OngoingCallContext {
return (poll |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue()))) |> restart
}
public func makeIncomingVideoView(completion: @escaping (UIView?) -> Void) {
public func makeIncomingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) {
self.withContext { context in
if let context = context as? OngoingCallThreadLocalContextWebrtc {
context.makeIncomingVideoView(completion)
@ -735,3 +739,6 @@ public final class OngoingCallContext {
}
}
}
extension OngoingCallThreadLocalContextWebrtcVideoView: OngoingCallContextPresentationCallVideoView {
}

View File

@ -195,11 +195,19 @@ std::unique_ptr<webrtc::VideoDecoderFactory> makeVideoDecoderFactory() {
}
bool supportsH265Encoding() {
#if TARGET_OS_IOS
if (@available(iOS 11.0, *)) {
return [[AVAssetExportSession allExportPresets] containsObject:AVAssetExportPresetHEVCHighestQuality];
} else {
return false;
}
#else
if (@available(macOS 10.13, *)) {
return [[AVAssetExportSession allExportPresets] containsObject:AVAssetExportPresetHEVCHighestQuality];
} else {
return false;
}
#endif
}
rtc::scoped_refptr<webrtc::VideoTrackSourceInterface> makeVideoSource(rtc::Thread *signalingThread, rtc::Thread *workerThread) {

View File

@ -5,12 +5,13 @@
#import <UIKit/UIKit.h>
#import "api/media_stream_interface.h"
#import <TgVoip/OngoingCallThreadLocalContext.h>
#include <memory>
@class RTCVideoFrame;
@interface VideoMetalView : UIView
@interface VideoMetalView : OngoingCallThreadLocalContextWebrtcVideoView
@property(nonatomic) UIViewContentMode videoContentMode;
@property(nonatomic, getter=isEnabled) BOOL enabled;

View File

@ -52,6 +52,9 @@ private:
CGSize _currentSize;
std::shared_ptr<VideoRendererAdapterImpl> _sink;
void (^_onFirstFrameReceived)();
bool _firstFrameReceivedReported;
}
@end
@ -264,6 +267,11 @@ private:
- (void)renderFrame:(nullable RTCVideoFrame *)frame {
assert([NSThread isMainThread]);
if (!_firstFrameReceivedReported && _onFirstFrameReceived) {
_firstFrameReceivedReported = true;
_onFirstFrameReceived();
}
if (!self.isEnabled) {
return;
@ -282,4 +290,9 @@ private:
return _sink;
}
- (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived {
_onFirstFrameReceived = [onFirstFrameReceived copy];
_firstFrameReceivedReported = false;
}
@end

View File

@ -78,6 +78,12 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
@end
@interface OngoingCallThreadLocalContextWebrtcVideoView : UIView
- (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived;
@end
@interface OngoingCallThreadLocalContextVideoCapturer : NSObject
- (instancetype _Nonnull)init;
@ -85,7 +91,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
- (void)switchVideoCamera;
- (void)setIsVideoEnabled:(bool)isVideoEnabled;
- (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion;
- (void)makeOutgoingVideoView:(void (^_Nonnull)(OngoingCallThreadLocalContextWebrtcVideoView * _Nullable))completion;
@end
@ -111,7 +117,7 @@ typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) {
- (void)setIsMuted:(bool)isMuted;
- (void)setVideoEnabled:(bool)videoEnabled;
- (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType;
- (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion;
- (void)makeIncomingVideoView:(void (^_Nonnull)(OngoingCallThreadLocalContextWebrtcVideoView * _Nullable))completion;
- (void)addSignalingData:(NSData * _Nonnull)data;
@end

View File

@ -49,7 +49,7 @@ using namespace TGVOIP_NAMESPACE;
return _interface;
}
- (void)makeOutgoingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion {
- (void)makeOutgoingVideoView:(void (^_Nonnull)(OngoingCallThreadLocalContextWebrtcVideoView * _Nullable))completion {
std::shared_ptr<TgVoipVideoCaptureInterface> interface = _interface;
dispatch_async(dispatch_get_main_queue(), ^{
VideoMetalView *remoteRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero];
@ -478,7 +478,7 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
}
}
- (void)makeIncomingVideoView:(void (^_Nonnull)(UIView * _Nullable))completion {
- (void)makeIncomingVideoView:(void (^_Nonnull)(OngoingCallThreadLocalContextWebrtcVideoView * _Nullable))completion {
if (_tgVoip) {
__weak OngoingCallThreadLocalContextWebrtc *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
@ -498,3 +498,9 @@ static void (*InternalVoipLoggingFunction)(NSString *) = NULL;
@end
@implementation OngoingCallThreadLocalContextWebrtcVideoView : UIView
- (void)setOnFirstFrameReceived:(void (^ _Nullable)())onFirstFrameReceived {
}
@end

View File

@ -449,12 +449,12 @@ public final class WalletStrings: Equatable {
public var Wallet_SecureStorageReset_Title: String { return self._s[219]! }
public var Wallet_Receive_CommentHeader: String { return self._s[220]! }
public var Wallet_Info_ReceiveGrams: String { return self._s[221]! }
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[0 * 6 + Int(form.rawValue)]!, stringValue)
}
public func Wallet_Updated_HoursAgo(_ value: Int32) -> String {
public func Wallet_Updated_MinutesAgo(_ value: Int32) -> String {
let form = getPluralizationForm(self.lc, value)
let stringValue = walletStringsFormattedNumber(value, self.groupingSeparator)
return String(format: self._ps[1 * 6 + Int(form.rawValue)]!, stringValue)