Add CallMemberView and CallMemberComposableViews
This commit is contained in:
parent
69a948cbde
commit
fcc513d929
@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
E1E78CAC2B573B5800B6FC2D /* CallMemberChromeOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMemberChromeOverlayView.swift; sourceTree = "<group>"; };
|
||||
E1E78CAE2B573BD100B6FC2D /* CallMemberCameraOffView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMemberCameraOffView.swift; sourceTree = "<group>"; };
|
||||
E1E78CB32B575C2700B6FC2D /* CallMemberVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMemberVideoView.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
E44AD4E524E98F430035D7B8 /* PhotoCaptureDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureDismiss.swift; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
||||
124
Signal/src/Calls/UserInterface/CallMemberCameraOffView.swift
Normal file
124
Signal/src/Calls/UserInterface/CallMemberCameraOffView.swift
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
114
Signal/src/Calls/UserInterface/CallMemberVideoView.swift
Normal file
114
Signal/src/Calls/UserInterface/CallMemberVideoView.swift
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ class GroupCallMemberView: UIView {
|
||||
}
|
||||
|
||||
class GroupCallLocalMemberView: GroupCallMemberView {
|
||||
let videoView = LocalVideoView()
|
||||
let videoView = LocalVideoView(shouldUseAutolayout: false)
|
||||
|
||||
let videoOffIndicatorImage = UIImageView()
|
||||
|
||||
|
||||
@ -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
|
||||
}()
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user