diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 2637b7923e..77ec84be89 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1607,6 +1607,9 @@ E18C4A7729EF2ECC007534D4 /* SignalAccountTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18C4A7629EF2ECC007534D4 /* SignalAccountTest.swift */; }; E1A090382A4B909B00F2BE8B /* RecipientHidingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A090372A4B909B00F2BE8B /* RecipientHidingManager.swift */; }; E1C2A54B2A8FCB0D00AEC4DA /* DeleteSystemContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C2A54A2A8FCB0D00AEC4DA /* DeleteSystemContactViewController.swift */; }; + E1E78CAD2B573B5800B6FC2D /* CallMemberChromeOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E78CAC2B573B5800B6FC2D /* CallMemberChromeOverlayView.swift */; }; + E1E78CAF2B573BD100B6FC2D /* CallMemberCameraOffView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E78CAE2B573BD100B6FC2D /* CallMemberCameraOffView.swift */; }; + E1E78CB42B575C2700B6FC2D /* CallMemberVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E78CB32B575C2700B6FC2D /* CallMemberVideoView.swift */; }; E44AD4E624E98F440035D7B8 /* PhotoCaptureDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */; }; E75DD3E02810CDBD00E32C36 /* SubscriptionManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E75DD3DF2810CDBD00E32C36 /* SubscriptionManagerTest.swift */; }; E7D7C93F28B580AC003F043B /* Bundle+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7D7C93E28B580AC003F043B /* Bundle+OWS.swift */; }; @@ -4354,6 +4357,9 @@ E1A090372A4B909B00F2BE8B /* RecipientHidingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientHidingManager.swift; sourceTree = ""; }; E1A0AD8B16E13FDD0071E604 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; E1C2A54A2A8FCB0D00AEC4DA /* DeleteSystemContactViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteSystemContactViewController.swift; sourceTree = ""; }; + E1E78CAC2B573B5800B6FC2D /* CallMemberChromeOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMemberChromeOverlayView.swift; sourceTree = ""; }; + E1E78CAE2B573BD100B6FC2D /* CallMemberCameraOffView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMemberCameraOffView.swift; sourceTree = ""; }; + E1E78CB32B575C2700B6FC2D /* CallMemberVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMemberVideoView.swift; sourceTree = ""; }; E27EBB962FA58BAC64DD64A3 /* Pods-SignalUITests.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalUITests.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalUITests/Pods-SignalUITests.app store release.xcconfig"; sourceTree = ""; }; E434E65F6E6E989A106307BA /* Pods-SignalTests.profiling.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.profiling.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.profiling.xcconfig"; sourceTree = ""; }; E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureDismiss.swift; sourceTree = ""; }; @@ -8115,6 +8121,9 @@ 88588D1A252D3DFF00405414 /* CallControls.swift */, E17283DD2B2A713C00302DC7 /* CallControlsConfirmationToast.swift */, 88ABB8B42534070400229EAA /* CallHeader.swift */, + E1E78CAE2B573BD100B6FC2D /* CallMemberCameraOffView.swift */, + E1E78CAC2B573B5800B6FC2D /* CallMemberChromeOverlayView.swift */, + E1E78CB32B575C2700B6FC2D /* CallMemberVideoView.swift */, 88588D1C252D5D3D00405414 /* CallMemberView.swift */, 88D23D1C23CEC0C700B0E74B /* CallUIAdapter.swift */, 1700E33E28B856FB0073D949 /* IncomingCallControls.swift */, @@ -12100,6 +12109,9 @@ 88ABB8B52534070400229EAA /* CallHeader.swift in Sources */, 88D23D2423CEC0C700B0E74B /* CallKitCallManager.swift in Sources */, 88D23D2523CEC0C700B0E74B /* CallKitCallUIAdaptee.swift in Sources */, + E1E78CAF2B573BD100B6FC2D /* CallMemberCameraOffView.swift in Sources */, + E1E78CAD2B573B5800B6FC2D /* CallMemberChromeOverlayView.swift in Sources */, + E1E78CB42B575C2700B6FC2D /* CallMemberVideoView.swift in Sources */, 88588D1D252D5D3D00405414 /* CallMemberView.swift in Sources */, D93108092B337A23006A034E /* CallRecordLoader.swift in Sources */, 88588D26252E59CE00405414 /* CallService.swift in Sources */, diff --git a/Signal/src/Calls/UserInterface/CallMemberCameraOffView.swift b/Signal/src/Calls/UserInterface/CallMemberCameraOffView.swift new file mode 100644 index 0000000000..c0ff7a6919 --- /dev/null +++ b/Signal/src/Calls/UserInterface/CallMemberCameraOffView.swift @@ -0,0 +1,124 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation +import LibSignalClient +import SignalMessaging +import SignalRingRTC +import SignalUI + +// TODO: Before enabling FeatureFlags.useCallMemberComposableViewsForRemoteUsersIn[Group|Individual]Calls, +// show this view while waiting for remote video to be received and displayed. +class CallMemberCameraOffView: UIView, CallMemberComposableView { + private let backgroundAvatarView = UIImageView() + private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .dark)) + private let avatarView = ConversationAvatarView(localUserDisplayMode: .asUser, badged: false) + + private var avatarDiameter: UInt { + if width > 180 { + return 112 + } else if width > 102 { + return 96 + } else if width > 48 { + return UInt(width) - 36 + } else { + return 16 + } + } + + init() { + super.init(frame: .zero) + + let overlayView = UIView() + overlayView.backgroundColor = .ows_blackAlpha40 + self.addSubview(overlayView) + overlayView.autoPinEdgesToSuperviewEdges() + + backgroundAvatarView.contentMode = .scaleAspectFill + self.addSubview(backgroundAvatarView) + backgroundAvatarView.autoPinEdgesToSuperviewEdges() + + self.addSubview(blurView) + blurView.autoPinEdgesToSuperviewEdges() + + self.addSubview(avatarView) + avatarView.autoCenterInSuperview() + } + + func configure( + call: SignalCall, + isFullScreen: Bool = false, + memberType: CallMemberView.ConfigurationType + ) { + self.isHidden = !call.isOutgoingVideoMuted + self.avatarView.isHidden = isFullScreen + + let backgroundAvatarImage: UIImage? + let backgroundColor: UIColor? + switch memberType { + case .local: + let avatar = profileManager.localProfileAvatarImage() + backgroundAvatarImage = avatar + let localAddress: SignalServiceAddress? = databaseStorage.read { tx in + guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aciAddress else { + owsFailDebug("missing local address") + return nil + } + let updatedSize = avatarDiameter + avatarView.update(tx) { config in + config.dataSource = .address(localAddress) + config.sizeClass = .customDiameter(updatedSize) + } + return localAddress + } + if let localAddress { + backgroundColor = AvatarTheme.forAddress(localAddress).backgroundColor + } else { + backgroundColor = nil + } + case .remote(let remoteDeviceState, _): + let profileImage = databaseStorage.read { tx in + let updatedSize = avatarDiameter + avatarView.update(tx) { config in + config.dataSource = .address(remoteDeviceState.address) + config.sizeClass = .customDiameter(updatedSize) + } + + let profileImage = self.contactsManagerImpl.avatarImage( + forAddress: remoteDeviceState.address, + shouldValidate: true, + transaction: tx + ) + return profileImage + } + backgroundAvatarImage = profileImage + backgroundColor = AvatarTheme.forAddress(remoteDeviceState.address).backgroundColor + } + backgroundAvatarView.image = backgroundAvatarImage + self.backgroundColor = backgroundColor + } + + func updateDimensions() { + // Will this be a problem being called from layout subviews? + avatarView.updateWithSneakyTransactionIfNecessary { config in + config.sizeClass = .customDiameter(avatarDiameter) + } + } + + func rotateForPhoneOrientation(_ rotationAngle: CGFloat) { + /// TODO: Add support for rotating other elements too. + self.avatarView.transform = CGAffineTransform(rotationAngle: rotationAngle) + } + + func clearConfiguration() { + self.backgroundColor = .ows_black + backgroundAvatarView.image = nil + avatarView.reset() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Signal/src/Calls/UserInterface/CallMemberChromeOverlayView.swift b/Signal/src/Calls/UserInterface/CallMemberChromeOverlayView.swift new file mode 100644 index 0000000000..ecd4b80dda --- /dev/null +++ b/Signal/src/Calls/UserInterface/CallMemberChromeOverlayView.swift @@ -0,0 +1,80 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation + +class CallMemberChromeOverlayView: UIView, CallMemberComposableView { + private let muteIndicatorImage = UIImageView() + private var muteLeadingConstraint: NSLayoutConstraint? + private var muteBottomConstraint: NSLayoutConstraint? + private var muteHeightConstraint: NSLayoutConstraint? + + private var muteInsets: CGFloat { + return width > 102 ? 9 : 4 + } + + private var muteHeight: CGFloat { + return width > 200 && UIDevice.current.isIPad ? 20 : 16 + } + + init() { + super.init(frame: .zero) + muteIndicatorImage.isHidden = true + muteIndicatorImage.setTemplateImageName("mic-slash-fill-28", tintColor: .ows_white) + addSubview(muteIndicatorImage) + muteIndicatorImage.autoMatch(.width, to: .height, of: muteIndicatorImage) + createAndActivateMuteLayoutConstraints() + } + + private func createAndActivateMuteLayoutConstraints() { + let muteLeadingConstraint = muteIndicatorImage.autoPinEdge(toSuperviewEdge: .leading, withInset: muteInsets) + let muteBottomConstraint = muteIndicatorImage.autoPinEdge(toSuperviewEdge: .bottom, withInset: muteInsets) + let muteHeightConstraint = muteIndicatorImage.autoSetDimension(.height, toSize: muteHeight) + NSLayoutConstraint.activate([ + muteLeadingConstraint, + muteBottomConstraint, + muteHeightConstraint + ]) + self.muteLeadingConstraint = muteLeadingConstraint + self.muteBottomConstraint = muteBottomConstraint + self.muteHeightConstraint = muteHeightConstraint + } + + func rotateForPhoneOrientation(_ rotationAngle: CGFloat) { + /// TODO: Add support for rotating other elements too. + self.muteIndicatorImage.transform = CGAffineTransform(rotationAngle: rotationAngle) + } + + func configure( + call: SignalCall, + isFullScreen: Bool = false, + memberType: CallMemberView.ConfigurationType + ) { + switch memberType { + case .local: + muteIndicatorImage.isHidden = !call.isOutgoingAudioMuted || isFullScreen + case .remote(let remoteDeviceState, let context): + muteIndicatorImage.isHidden = context == .speaker || remoteDeviceState.audioMuted != true || isFullScreen + } + } + + func updateDimensions() { + let constraints = [ + muteLeadingConstraint, + muteBottomConstraint, + muteHeightConstraint + ].compactMap { $0 } + NSLayoutConstraint.deactivate(constraints) + createAndActivateMuteLayoutConstraints() + } + + func clearConfiguration() { + muteIndicatorImage.isHidden = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Signal/src/Calls/UserInterface/CallMemberVideoView.swift b/Signal/src/Calls/UserInterface/CallMemberVideoView.swift new file mode 100644 index 0000000000..1063f1e540 --- /dev/null +++ b/Signal/src/Calls/UserInterface/CallMemberVideoView.swift @@ -0,0 +1,114 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import AVFoundation +import Foundation +import SignalMessaging +import SignalRingRTC + +class CallMemberVideoView: UIView, CallMemberComposableView { + /// See note on ```CallMemberView.Configurationtype```. + enum MemberType { + case local(SignalCall) + case remote(isGroupCall: Bool) + } + + init(type: MemberType) { + super.init(frame: .zero) + switch type { + case .local(let call): + let localVideoView = LocalVideoView(shouldUseAutolayout: true) + localVideoView.captureSession = call.videoCaptureController.captureSession + self.addSubview(localVideoView) + localVideoView.contentMode = .scaleAspectFill + localVideoView.autoPinEdgesToSuperviewEdges() + self.callViewWrapper = .local(localVideoView) + case .remote(let isGroupCall): + if !isGroupCall { + let remoteVideoView = RemoteVideoView() + remoteVideoView.isGroupCall = false + remoteVideoView.isUserInteractionEnabled = false + self.addSubview(remoteVideoView) + remoteVideoView.autoPinEdgesToSuperviewEdges() + self.callViewWrapper = .remoteInIndividual(remoteVideoView) + } + // The view for group calls is set upon `configure`. + } + } + + private enum CallViewWrapper { + case local(LocalVideoView) + case remoteInIndividual(RemoteVideoView) + case remoteInGroup(GroupCallRemoteVideoView) + } + private var callViewWrapper: CallViewWrapper? + + func configure( + call: SignalCall, + isFullScreen: Bool = false, + memberType: CallMemberView.ConfigurationType + ) { + self.isHidden = call.isOutgoingVideoMuted + switch memberType { + case .local: + if case let .local(videoView) = callViewWrapper { + videoView.captureSession = call.videoCaptureController.captureSession + } else { + owsFailDebug("This should not be called when we're dealing with a remote video!") + } + case .remote(let remoteDeviceState, let context): + if remoteDeviceState.mediaKeysReceived, remoteDeviceState.videoTrack != nil { + self.isHidden = (remoteDeviceState.videoMuted == true) + } + if !self.isHidden { + configureRemoteVideo(device: remoteDeviceState, context: context) + } + } + } + + private func unwrapVideoView() -> UIView? { + switch self.callViewWrapper { + case .local(let localVideoView): + return localVideoView + case .remoteInIndividual(let remoteVideoView): + return remoteVideoView + case .remoteInGroup(let groupCallRemoteVideoView): + return groupCallRemoteVideoView + case .none: + return nil + } + } + + func configureRemoteVideo( + device: RemoteDeviceState, + context: CallMemberVisualContext + ) { + if case let .remoteInGroup(videoView) = callViewWrapper { + if videoView.superview == self { videoView.removeFromSuperview() } + } else { + owsFailDebug("Can only call configureRemoteVideo for groups!") + } + let remoteVideoView = callService.groupCallRemoteVideoManager.remoteVideoView(for: device, context: context) + self.addSubview(remoteVideoView) + self.callViewWrapper = .remoteInGroup(remoteVideoView) + remoteVideoView.autoPinEdgesToSuperviewEdges() + remoteVideoView.isScreenShare = device.sharingScreen == true + } + + func rotateForPhoneOrientation(_ rotationAngle: CGFloat) { + /// TODO: Add support for rotating. + } + + func updateDimensions() {} + + func clearConfiguration() { + if unwrapVideoView()?.superview === self { unwrapVideoView()?.removeFromSuperview() } + callViewWrapper = nil + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Signal/src/Calls/UserInterface/CallMemberView.swift b/Signal/src/Calls/UserInterface/CallMemberView.swift index b5a53efb64..af1e4c01b8 100644 --- a/Signal/src/Calls/UserInterface/CallMemberView.swift +++ b/Signal/src/Calls/UserInterface/CallMemberView.swift @@ -3,6 +3,156 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import Foundation +import LibSignalClient +import SignalMessaging +import SignalRingRTC +import SignalUI + enum CallMemberVisualContext: Equatable { case videoGrid, videoOverflow, speaker } + +protocol CallMemberComposableView: UIView { + func configure( + call: SignalCall, + isFullScreen: Bool, + memberType: CallMemberView.ConfigurationType + ) + func rotateForPhoneOrientation(_ rotationAngle: CGFloat) + func updateDimensions() + func clearConfiguration() +} + +class CallMemberView: UIView { + private let callMemberCameraOffView: CallMemberCameraOffView + private let callMemberVideoView: CallMemberVideoView + private let callMemberChromeOverlayView: CallMemberChromeOverlayView + + /// Must be specified with the lowest view first; views will be added + /// as subviews in the order they are given. + private var orderedComposableViews = [CallMemberComposableView]() + + override func layoutSubviews() { + super.layoutSubviews() + updateDimensions() + } + + private let type: CallMemberVideoView.MemberType + + init(type: CallMemberVideoView.MemberType) { + self.type = type + self.callMemberCameraOffView = CallMemberCameraOffView() + self.callMemberVideoView = CallMemberVideoView(type: type) + self.callMemberChromeOverlayView = CallMemberChromeOverlayView() + super.init(frame: .zero) + switch type { + case .local: + self.orderedComposableViews = [ + callMemberCameraOffView, + callMemberVideoView, + callMemberChromeOverlayView + ] + case .remote(_): + self.orderedComposableViews = [ + callMemberCameraOffView, + callMemberVideoView, + // TODO: Add waiting and error views + callMemberChromeOverlayView + ] + } + + backgroundColor = .ows_gray90 + clipsToBounds = true + + self.orderedComposableViews.forEach { view in + self.addSubview(view) + view.autoPinEdgesToSuperviewEdges() + } + + NotificationCenter.default.addObserver( + self, + selector: #selector(updateOrientationForPhone), + name: CallService.phoneOrientationDidChange, + object: nil + ) + } + + @objc + private func updateOrientationForPhone(_ notification: Notification) { + let rotationAngle = notification.object as! CGFloat + + if window == nil { + self.orderedComposableViews.forEach { view in + view.rotateForPhoneOrientation(rotationAngle) + } + } else { + UIView.animate(withDuration: 0.3) { + self.orderedComposableViews.forEach { view in + view.rotateForPhoneOrientation(rotationAngle) + } + } + } + } + + /// Not to be confused with ``CallMemberVideoView.MemberType``, which is used on + /// init. This is an imperfect solution to the issue of (group,individual)x(local,remote) + /// call member views having different needs for set up and configuration. The alternative + /// is having optional params on `init` and `configure`. + /// TODO: Eventually iterate to a point that the enum params can become non-optional + /// `configure` params. + enum ConfigurationType { + case local + case remote(RemoteDeviceState, CallMemberVisualContext) + } + + private var hasBeenConfigured = false + func configure( + call: SignalCall, + isFullScreen: Bool = false, + memberType: ConfigurationType + ) { + hasBeenConfigured = true + switch memberType { + case .local: + guard case .local = self.type else { + owsAssertBeta(false, "Member type on init must match member type on config!") + return + } + layer.shadowOffset = .zero + layer.shadowOpacity = 0.25 + layer.shadowRadius = 4 + layer.cornerRadius = isFullScreen ? 0 : 10 + case .remote(_, _): + guard case .remote = self.type else { + owsAssertBeta(false, "Member type on init must match member type on config!") + return + } + } + + self.orderedComposableViews.forEach { view in + view.configure( + call: call, + isFullScreen: isFullScreen, + memberType: memberType + ) + } + } + + private func updateDimensions() { + guard hasBeenConfigured else { return } + self.orderedComposableViews.forEach { view in + view.updateDimensions() + } + } + + func clearConfiguration() { + self.orderedComposableViews.forEach { view in + view.clearConfiguration() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift b/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift index 8569887cfd..460fda2556 100644 --- a/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift +++ b/Signal/src/Calls/UserInterface/Group/GroupCallMemberView.swift @@ -105,7 +105,7 @@ class GroupCallMemberView: UIView { } class GroupCallLocalMemberView: GroupCallMemberView { - let videoView = LocalVideoView() + let videoView = LocalVideoView(shouldUseAutolayout: false) let videoOffIndicatorImage = UIImageView() diff --git a/Signal/src/Calls/UserInterface/Individual/IndividualCallViewController.swift b/Signal/src/Calls/UserInterface/Individual/IndividualCallViewController.swift index 10112bcef8..59701cb186 100644 --- a/Signal/src/Calls/UserInterface/Individual/IndividualCallViewController.swift +++ b/Signal/src/Calls/UserInterface/Individual/IndividualCallViewController.swift @@ -130,7 +130,7 @@ class IndividualCallViewController: OWSViewController, CallObserver { private weak var remoteVideoTrack: RTCVideoTrack? private lazy var localVideoView: LocalVideoView = { - let localVideoView = LocalVideoView() + let localVideoView = LocalVideoView(shouldUseAutolayout: false) localVideoView.captureSession = call.videoCaptureController.captureSession return localVideoView }() diff --git a/Signal/src/Calls/UserInterface/LocalVideoView.swift b/Signal/src/Calls/UserInterface/LocalVideoView.swift index 2bfe8e0d2d..9b8a5c3b9a 100644 --- a/Signal/src/Calls/UserInterface/LocalVideoView.swift +++ b/Signal/src/Calls/UserInterface/LocalVideoView.swift @@ -10,6 +10,8 @@ import WebRTC class LocalVideoView: UIView { private let localVideoCapturePreview = RTCCameraPreviewView() + private let shouldUseAutolayout: Bool + var captureSession: AVCaptureSession? { get { localVideoCapturePreview.captureSession } set { localVideoCapturePreview.captureSession = newValue } @@ -19,10 +21,14 @@ class LocalVideoView: UIView { didSet { localVideoCapturePreview.contentMode = contentMode } } - override init(frame: CGRect) { + init(shouldUseAutolayout: Bool) { + self.shouldUseAutolayout = shouldUseAutolayout super.init(frame: .zero) addSubview(localVideoCapturePreview) + if shouldUseAutolayout { + localVideoCapturePreview.autoPinEdgesToSuperviewEdges() + } if Platform.isSimulator { backgroundColor = .brown @@ -41,12 +47,22 @@ class LocalVideoView: UIView { } override var frame: CGRect { - didSet { updateLocalVideoOrientation() } + didSet { + if !shouldUseAutolayout { + updateLocalVideoOrientation() + } + } } @objc private func updateLocalVideoOrientation() { - defer { localVideoCapturePreview.frame = bounds } + defer { + if shouldUseAutolayout { + setNeedsUpdateConstraints() + } else { + localVideoCapturePreview.frame = bounds + } + } // iPad supports rotating this view controller directly, // so we don't need to do anything here.