From 999851f85f60a0d94eb3ba42bceafb47459ec869 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Wed, 9 Oct 2024 12:13:43 +0300 Subject: [PATCH] [Feature]Extract video mirroring and callSettings observation to viewModifiers Allowing integrators to override their usage by using directly the VideoCallParticipantView without applying the modifiers or by overriding the ViewFactory method. --- .../CallView/MinimizedCallView.swift | 6 +- .../CallView/VideoParticipantsView.swift | 28 ++++----- ...omCameraUsageObservationViewModifier.swift | 59 +++++++++++++++++++ ...ocalParticipantMirroringViewModifier.swift | 59 +++++++++++++++++++ .../Livestreaming/LivestreamPlayer.swift | 2 + Sources/StreamVideoSwiftUI/ViewFactory.swift | 2 + StreamVideo.xcodeproj/project.pbxproj | 8 +++ 7 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 Sources/StreamVideoSwiftUI/CallView/ViewModifiers/FromCameraUsageObservationViewModifier.swift create mode 100644 Sources/StreamVideoSwiftUI/CallView/ViewModifiers/LocalParticipantMirroringViewModifier.swift diff --git a/Sources/StreamVideoSwiftUI/CallView/MinimizedCallView.swift b/Sources/StreamVideoSwiftUI/CallView/MinimizedCallView.swift index 39ed138b5..3c40ef1c0 100644 --- a/Sources/StreamVideoSwiftUI/CallView/MinimizedCallView.swift +++ b/Sources/StreamVideoSwiftUI/CallView/MinimizedCallView.swift @@ -30,14 +30,16 @@ public struct MinimizedCallView: View { func content(for availableFrame: CGRect) -> some View { Group { - if !viewModel.participants.isEmpty { + if let participant = viewModel.participants.first { VideoCallParticipantView( - participant: viewModel.participants[0], + participant: participant, availableFrame: availableFrame, contentMode: .scaleAspectFill, customData: [:], call: viewModel.call ) + .localParticipantMirroring(participant: participant) + .frontCameraUsageObservation(call: viewModel.call, participant: participant) } else { EmptyView() } diff --git a/Sources/StreamVideoSwiftUI/CallView/VideoParticipantsView.swift b/Sources/StreamVideoSwiftUI/CallView/VideoParticipantsView.swift index e5b00e95e..29279cebb 100644 --- a/Sources/StreamVideoSwiftUI/CallView/VideoParticipantsView.swift +++ b/Sources/StreamVideoSwiftUI/CallView/VideoParticipantsView.swift @@ -345,22 +345,20 @@ public struct VideoCallParticipantView: View { } public var body: some View { - withCallSettingsObservartion { - VideoRendererView( - id: id, - size: availableFrame.size, - contentMode: contentMode, - showVideo: showVideo, - handleRendering: { [weak call, participant] view in - guard call != nil else { return } - view.handleViewRendering(for: participant) { [weak call] size, participant in - Task { [weak call] in - await call?.updateTrackSize(size, for: participant) - } + VideoRendererView( + id: id, + size: availableFrame.size, + contentMode: contentMode, + showVideo: showVideo, + handleRendering: { [weak call, participant] view in + guard call != nil else { return } + view.handleViewRendering(for: participant) { [weak call] size, participant in + Task { [weak call] in + await call?.updateTrackSize(size, for: participant) } } - ) - } + } + ) .opacity(showVideo ? 1 : 0) .edgesIgnoringSafeArea(edgesIgnoringSafeArea) .accessibility(identifier: "callParticipantView") @@ -381,7 +379,7 @@ public struct VideoCallParticipantView: View { @MainActor @ViewBuilder - private func withCallSettingsObservartion( + private func withCallSettingsObservation( @ViewBuilder _ content: () -> some View ) -> some View { if participant.id == streamVideo.state.activeCall?.state.localParticipant?.id { diff --git a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/FromCameraUsageObservationViewModifier.swift b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/FromCameraUsageObservationViewModifier.swift new file mode 100644 index 000000000..fac76418e --- /dev/null +++ b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/FromCameraUsageObservationViewModifier.swift @@ -0,0 +1,59 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import StreamVideo +import SwiftUI + +private struct FromCameraUsageObservationViewModifier: ViewModifier { + + /// Injects the StreamVideo instance from the environment to access the current active call and its state. + @Injected(\.streamVideo) private var streamVideo + + /// Tracks whether the front camera is being used by the local user. + @State var isUsingFrontCameraForLocalUser: Bool = false + + /// The current call whose state is being observed for camera settings. + var call: Call? + + /// The participant whose view might need to observe front camera usage. + var participant: CallParticipant + + /// Defines the body of the view modifier. + /// - Parameter content: The content view that is being modified. + /// - Returns: A view that updates its state based on the camera position of the local user. + func body(content: Content) -> some View { + if participant.id == streamVideo.state.activeCall?.state.localParticipant?.id { + content + // Observes changes to the call's settings and updates the `isUsingFrontCameraForLocalUser` state accordingly. + .onReceive(call?.state.$callSettings) { + self.isUsingFrontCameraForLocalUser = $0.cameraPosition == .front + } + } else { + content + } + } +} + +extension View { + + /// Observes whether the front camera is being used by the local user during a call. + /// This modifier listens for updates to the camera settings in the call and updates the view. + /// - Parameters: + /// - call: The `Call` instance whose camera settings are being observed. + /// - participant: The `CallParticipant` that might need to observe from camera usage. + /// - Returns: A view that reflects changes based on the front camera usage. + @ViewBuilder + public func frontCameraUsageObservation( + call: Call?, + participant: CallParticipant + ) -> some View { + // Applies the FromCameraUsageObservationViewModifier to observe the camera usage. + modifier( + FromCameraUsageObservationViewModifier( + call: call, + participant: participant + ) + ) + } +} diff --git a/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/LocalParticipantMirroringViewModifier.swift b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/LocalParticipantMirroringViewModifier.swift new file mode 100644 index 000000000..17aecab93 --- /dev/null +++ b/Sources/StreamVideoSwiftUI/CallView/ViewModifiers/LocalParticipantMirroringViewModifier.swift @@ -0,0 +1,59 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import StreamVideo +import SwiftUI + +private struct LocalParticipantMirroringViewModifier: ViewModifier { + + /// Injects the StreamVideo instance from the environment to access the current active call and its state. + @Injected(\.streamVideo) private var streamVideo + + /// The participant whose view might need to be mirrored. + var participant: CallParticipant + + /// The angle by which the content should be rotated if mirroring is applied. + var angle: Angle + + /// The axis around which the content will be rotated in 3D space. + var axis: (x: CGFloat, y: CGFloat, z: CGFloat) + + /// Defines the body of the view modifier. + /// - Parameter content: The content view that is being modified. + /// - Returns: A view that is conditionally mirrored if the participant is the local participant. + func body(content: Content) -> some View { + // If the participant is the local participant, apply a 3D rotation to mirror their view. + if participant.id == streamVideo.state.activeCall?.state.localParticipant?.id { + content.rotation3DEffect(angle, axis: axis) + } else { + // Otherwise, display the content without modification. + content + } + } +} + +extension View { + + /// Applies a mirroring effect to the view if the provided participant is the local participant. + /// This is useful for displaying the local participant's video feed with a mirror effect in the UI. + /// - Parameters: + /// - participant: The `CallParticipant` that might need to be mirrored. + /// - angle: The rotation angle (default is 180 degrees). + /// - axis: The axis of rotation (default is around the Y-axis). + /// - Returns: A view that conditionally applies the mirroring effect. + public func localParticipantMirroring( + participant: CallParticipant, + angle: Angle = .degrees(180), + axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (x: 0, y: 1, z: 0) + ) -> some View { + // Apply the LocalParticipantMirroringViewModifier with the specified participant, angle, and axis. + modifier( + LocalParticipantMirroringViewModifier( + participant: participant, + angle: angle, + axis: axis + ) + ) + } +} diff --git a/Sources/StreamVideoSwiftUI/Livestreaming/LivestreamPlayer.swift b/Sources/StreamVideoSwiftUI/Livestreaming/LivestreamPlayer.swift index 2e45de962..4313ab748 100644 --- a/Sources/StreamVideoSwiftUI/Livestreaming/LivestreamPlayer.swift +++ b/Sources/StreamVideoSwiftUI/Livestreaming/LivestreamPlayer.swift @@ -52,6 +52,8 @@ public struct LivestreamPlayer: View { customData: [:], call: viewModel.call ) + .localParticipantMirroring(participant: participant) + .frontCameraUsageObservation(call: viewModel.call, participant: participant) .onTapGesture { viewModel.update(controlsShown: true) } diff --git a/Sources/StreamVideoSwiftUI/ViewFactory.swift b/Sources/StreamVideoSwiftUI/ViewFactory.swift index bc76c07f5..9b3a5273c 100644 --- a/Sources/StreamVideoSwiftUI/ViewFactory.swift +++ b/Sources/StreamVideoSwiftUI/ViewFactory.swift @@ -225,6 +225,8 @@ extension ViewFactory { customData: customData, call: call ) + .localParticipantMirroring(participant: participant) + .frontCameraUsageObservation(call: call, participant: participant) } public func makeVideoCallParticipantModifier( diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 339364355..fe965b1c1 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -165,6 +165,8 @@ 404A5CFB2AD5648100EF1C62 /* DemoChatModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404A5CFA2AD5648100EF1C62 /* DemoChatModifier.swift */; }; 404C27CB2BF2552800DF2937 /* XCTestCase+PredicateFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409CA7982BEE21720045F7AA /* XCTestCase+PredicateFulfillment.swift */; }; 404C27CC2BF2552900DF2937 /* XCTestCase+PredicateFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409CA7982BEE21720045F7AA /* XCTestCase+PredicateFulfillment.swift */; }; + 404C6ED02CB67D4D0063EDB6 /* LocalParticipantMirroringViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404C6ECF2CB67D4D0063EDB6 /* LocalParticipantMirroringViewModifier.swift */; }; + 404C6ED22CB67E5A0063EDB6 /* FromCameraUsageObservationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404C6ED12CB67E5A0063EDB6 /* FromCameraUsageObservationViewModifier.swift */; }; 404CAEE72B8F48F6007087BC /* DemoBackgroundEffectSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A0E95F2B88ABC80089E8D3 /* DemoBackgroundEffectSelector.swift */; }; 4059C3422AAF0CE40006928E /* DemoChatViewModel+Injection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */; }; 4063033F2AD847EC0091AE77 /* CallState_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4063033E2AD847EC0091AE77 /* CallState_Tests.swift */; }; @@ -1495,6 +1497,8 @@ 4049CE812BBBF74C003D07D2 /* LegacyAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyAsyncImage.swift; sourceTree = ""; }; 4049CE832BBBF8EF003D07D2 /* StreamAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamAsyncImage.swift; sourceTree = ""; }; 404A5CFA2AD5648100EF1C62 /* DemoChatModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoChatModifier.swift; sourceTree = ""; }; + 404C6ECF2CB67D4D0063EDB6 /* LocalParticipantMirroringViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalParticipantMirroringViewModifier.swift; sourceTree = ""; }; + 404C6ED12CB67E5A0063EDB6 /* FromCameraUsageObservationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FromCameraUsageObservationViewModifier.swift; sourceTree = ""; }; 4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DemoChatViewModel+Injection.swift"; sourceTree = ""; }; 4063033E2AD847EC0091AE77 /* CallState_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallState_Tests.swift; sourceTree = ""; }; 406303412AD848000091AE77 /* CallParticipant_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipant_Mock.swift; sourceTree = ""; }; @@ -3697,6 +3701,8 @@ 40C7B82A2B612D5100FB9DB2 /* ViewModifiers */ = { isa = PBXGroup; children = ( + 404C6ED12CB67E5A0063EDB6 /* FromCameraUsageObservationViewModifier.swift */, + 404C6ECF2CB67D4D0063EDB6 /* LocalParticipantMirroringViewModifier.swift */, 403EFC9E2BDBFE050057C248 /* CallEndedViewModifier.swift */, 408D29A02B6D208700885473 /* Snapshot */, 409145E92B68FDD2007F3C17 /* ReadableContentGuide */, @@ -6774,6 +6780,7 @@ 40A941762B4D9F16006D6965 /* StreamPictureInPictureView.swift in Sources */, 848A73C229269E7D0089AA6E /* CornerDraggableView.swift in Sources */, 84DC382D29A8B9EC00946713 /* CallParticipantMenuAction.swift in Sources */, + 404C6ED22CB67E5A0063EDB6 /* FromCameraUsageObservationViewModifier.swift in Sources */, 8490032B29D4769700AD9BB4 /* CallConnectingView.swift in Sources */, 84F3B0E528917C620088751D /* Modifiers.swift in Sources */, 40245F3C2BE26FBF00FCF075 /* StatelessSpeakerIconView.swift in Sources */, @@ -6850,6 +6857,7 @@ 84B57D37297F406400E4E709 /* MicrophoneCheckView.swift in Sources */, 842C7EBC28A2A86700C2AB7F /* CallingParticipantView.swift in Sources */, 8469593E29BF214700134EA0 /* ViewExtensions.swift in Sources */, + 404C6ED02CB67D4D0063EDB6 /* LocalParticipantMirroringViewModifier.swift in Sources */, 4049CE822BBBF74C003D07D2 /* LegacyAsyncImage.swift in Sources */, 40F0C3A72BC7FAA400AB75AD /* VideoRendererPool.swift in Sources */, 4049CE842BBBF8EF003D07D2 /* StreamAsyncImage.swift in Sources */,