Skip to content

Commit

Permalink
[Feature]Extract video mirroring and callSettings observation to view…
Browse files Browse the repository at this point in the history
…Modifiers

Allowing integrators to override their usage by using directly the VideoCallParticipantView without applying the modifiers or by overriding the ViewFactory method.
  • Loading branch information
ipavlidakis committed Oct 9, 2024
1 parent 3feeca9 commit 999851f
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 17 deletions.
6 changes: 4 additions & 2 deletions Sources/StreamVideoSwiftUI/CallView/MinimizedCallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
28 changes: 13 additions & 15 deletions Sources/StreamVideoSwiftUI/CallView/VideoParticipantsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
)
}
}
Original file line number Diff line number Diff line change
@@ -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
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/StreamVideoSwiftUI/ViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ extension ViewFactory {
customData: customData,
call: call
)
.localParticipantMirroring(participant: participant)
.frontCameraUsageObservation(call: call, participant: participant)
}

public func makeVideoCallParticipantModifier(
Expand Down
8 changes: 8 additions & 0 deletions StreamVideo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1495,6 +1497,8 @@
4049CE812BBBF74C003D07D2 /* LegacyAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyAsyncImage.swift; sourceTree = "<group>"; };
4049CE832BBBF8EF003D07D2 /* StreamAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamAsyncImage.swift; sourceTree = "<group>"; };
404A5CFA2AD5648100EF1C62 /* DemoChatModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoChatModifier.swift; sourceTree = "<group>"; };
404C6ECF2CB67D4D0063EDB6 /* LocalParticipantMirroringViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalParticipantMirroringViewModifier.swift; sourceTree = "<group>"; };
404C6ED12CB67E5A0063EDB6 /* FromCameraUsageObservationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FromCameraUsageObservationViewModifier.swift; sourceTree = "<group>"; };
4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DemoChatViewModel+Injection.swift"; sourceTree = "<group>"; };
4063033E2AD847EC0091AE77 /* CallState_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallState_Tests.swift; sourceTree = "<group>"; };
406303412AD848000091AE77 /* CallParticipant_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipant_Mock.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3697,6 +3701,8 @@
40C7B82A2B612D5100FB9DB2 /* ViewModifiers */ = {
isa = PBXGroup;
children = (
404C6ED12CB67E5A0063EDB6 /* FromCameraUsageObservationViewModifier.swift */,
404C6ECF2CB67D4D0063EDB6 /* LocalParticipantMirroringViewModifier.swift */,
403EFC9E2BDBFE050057C248 /* CallEndedViewModifier.swift */,
408D29A02B6D208700885473 /* Snapshot */,
409145E92B68FDD2007F3C17 /* ReadableContentGuide */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down

0 comments on commit 999851f

Please sign in to comment.