@capgo/capacitor-stream-call 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Package.swift +31 -0
- package/README.md +340 -0
- package/StreamCall.podspec +19 -0
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CallOverlayView.kt +281 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +142 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +147 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/RingtonePlayer.kt +164 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +1014 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/TouchInterceptWrapper.kt +31 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/UserRepository.kt +111 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/values/strings.xml +7 -0
- package/dist/docs.json +533 -0
- package/dist/esm/definitions.d.ts +169 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +32 -0
- package/dist/esm/web.js +323 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +337 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +339 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/StreamCallPlugin/CallOverlayView.swift +147 -0
- package/ios/Sources/StreamCallPlugin/CustomCallParticipantImageView.swift +60 -0
- package/ios/Sources/StreamCallPlugin/CustomCallView.swift +257 -0
- package/ios/Sources/StreamCallPlugin/CustomVideoParticipantsView.swift +107 -0
- package/ios/Sources/StreamCallPlugin/ParticipantsView.swift +206 -0
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +722 -0
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +177 -0
- package/ios/Sources/StreamCallPlugin/UserRepository.swift +96 -0
- package/ios/Sources/StreamCallPlugin/WebviewNavigationDelegate.swift +68 -0
- package/ios/Tests/StreamCallPluginTests/StreamCallPluginTests.swift +15 -0
- package/package.json +96 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright © 2025 Stream.io Inc. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import StreamVideo
|
|
6
|
+
import StreamWebRTC
|
|
7
|
+
import SwiftUI
|
|
8
|
+
import StreamVideoSwiftUI
|
|
9
|
+
|
|
10
|
+
// Custom class to hold bindable settings
|
|
11
|
+
class BindableCallSettings: ObservableObject {
|
|
12
|
+
@Published var settings: CallSettings
|
|
13
|
+
|
|
14
|
+
init(settings: CallSettings) {
|
|
15
|
+
self.settings = settings
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public struct CustomCallView<Factory: ViewFactory>: View {
|
|
20
|
+
|
|
21
|
+
@Injected(\.streamVideo) var streamVideo
|
|
22
|
+
@Injected(\.images) var images
|
|
23
|
+
@Injected(\.colors) var colors
|
|
24
|
+
|
|
25
|
+
var viewFactory: Factory
|
|
26
|
+
@ObservedObject var viewModel: CallViewModel
|
|
27
|
+
@StateObject private var bindableSettings: BindableCallSettings
|
|
28
|
+
|
|
29
|
+
public init(
|
|
30
|
+
viewFactory: Factory = DefaultViewFactory.shared,
|
|
31
|
+
viewModel: CallViewModel
|
|
32
|
+
) {
|
|
33
|
+
self.viewFactory = viewFactory
|
|
34
|
+
self.viewModel = viewModel
|
|
35
|
+
self._bindableSettings = StateObject(wrappedValue: BindableCallSettings(settings: viewModel.callSettings))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public var body: some View {
|
|
39
|
+
VStack {
|
|
40
|
+
GeometryReader { videoFeedProxy in
|
|
41
|
+
ZStack {
|
|
42
|
+
contentView(videoFeedProxy.frame(in: .global))
|
|
43
|
+
|
|
44
|
+
cornerDraggableView(videoFeedProxy)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
.padding([.leading, .trailing], 8)
|
|
48
|
+
}
|
|
49
|
+
.background(Color(colors.callBackground).edgesIgnoringSafeArea(.all))
|
|
50
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
51
|
+
.onAppear {
|
|
52
|
+
UIApplication.shared.isIdleTimerDisabled = true
|
|
53
|
+
}
|
|
54
|
+
.onDisappear {
|
|
55
|
+
UIApplication.shared.isIdleTimerDisabled = false
|
|
56
|
+
}
|
|
57
|
+
.enablePictureInPicture(viewModel.isPictureInPictureEnabled)
|
|
58
|
+
.presentParticipantListView(viewModel: viewModel, viewFactory: viewFactory)
|
|
59
|
+
.onChange(of: viewModel.callSettings) { newSettings in
|
|
60
|
+
bindableSettings.settings = newSettings
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@ViewBuilder
|
|
65
|
+
private func contentView(_ availableFrame: CGRect) -> some View {
|
|
66
|
+
if viewModel.localVideoPrimary, viewModel.participantsLayout == .grid {
|
|
67
|
+
localVideoView(bounds: availableFrame)
|
|
68
|
+
.accessibility(identifier: "localVideoView")
|
|
69
|
+
} else if
|
|
70
|
+
let screenSharingSession = viewModel.call?.state.screenSharingSession,
|
|
71
|
+
viewModel.call?.state.isCurrentUserScreensharing == false {
|
|
72
|
+
viewFactory.makeScreenSharingView(
|
|
73
|
+
viewModel: viewModel,
|
|
74
|
+
screensharingSession: screenSharingSession,
|
|
75
|
+
availableFrame: availableFrame
|
|
76
|
+
)
|
|
77
|
+
} else {
|
|
78
|
+
participantsView(bounds: availableFrame)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private var shouldShowDraggableView: Bool {
|
|
83
|
+
let participantsCount = viewModel.participants.count
|
|
84
|
+
return (viewModel.call?.state.screenSharingSession == nil || viewModel.call?.state.isCurrentUserScreensharing == true)
|
|
85
|
+
&& viewModel.participantsLayout == .grid
|
|
86
|
+
&& participantsCount > 0
|
|
87
|
+
&& participantsCount <= 3
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@ViewBuilder
|
|
91
|
+
private func cornerDraggableView(_ proxy: GeometryProxy) -> some View {
|
|
92
|
+
if shouldShowDraggableView {
|
|
93
|
+
CornerDraggableView(
|
|
94
|
+
content: { cornerDraggableViewContent($0) },
|
|
95
|
+
proxy: proxy,
|
|
96
|
+
onTap: {
|
|
97
|
+
withAnimation {
|
|
98
|
+
if participants.count == 1 {
|
|
99
|
+
viewModel.localVideoPrimary.toggle()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
.accessibility(identifier: "cornerDraggableView")
|
|
105
|
+
.opacity(viewModel.hideUIElements ? 0 : 1)
|
|
106
|
+
.padding()
|
|
107
|
+
} else {
|
|
108
|
+
EmptyView()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@ViewBuilder
|
|
113
|
+
private func cornerDraggableViewContent(_ bounds: CGRect) -> some View {
|
|
114
|
+
if viewModel.localVideoPrimary {
|
|
115
|
+
minimizedView(bounds: bounds)
|
|
116
|
+
} else {
|
|
117
|
+
localVideoView(bounds: bounds)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@ViewBuilder
|
|
122
|
+
private func minimizedView(bounds: CGRect) -> some View {
|
|
123
|
+
if let firstParticipant = viewModel.participants.first {
|
|
124
|
+
viewFactory.makeVideoParticipantView(
|
|
125
|
+
participant: firstParticipant,
|
|
126
|
+
id: firstParticipant.id,
|
|
127
|
+
availableFrame: bounds,
|
|
128
|
+
contentMode: .scaleAspectFit,
|
|
129
|
+
customData: [:],
|
|
130
|
+
call: viewModel.call
|
|
131
|
+
)
|
|
132
|
+
.modifier(
|
|
133
|
+
viewFactory.makeVideoCallParticipantModifier(
|
|
134
|
+
participant: firstParticipant,
|
|
135
|
+
call: viewModel.call,
|
|
136
|
+
availableFrame: bounds,
|
|
137
|
+
ratio: bounds.width / bounds.height,
|
|
138
|
+
showAllInfo: true
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
.accessibility(identifier: "minimizedParticipantView")
|
|
142
|
+
} else {
|
|
143
|
+
EmptyView()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@ViewBuilder
|
|
148
|
+
private func localVideoView(bounds: CGRect) -> some View {
|
|
149
|
+
if let localParticipant = viewModel.localParticipant {
|
|
150
|
+
CustomLocalVideoView(
|
|
151
|
+
viewFactory: viewFactory,
|
|
152
|
+
participant: localParticipant,
|
|
153
|
+
callSettings: bindableSettings.settings,
|
|
154
|
+
call: viewModel.call,
|
|
155
|
+
availableFrame: bounds
|
|
156
|
+
)
|
|
157
|
+
.modifier(viewFactory.makeLocalParticipantViewModifier(
|
|
158
|
+
localParticipant: localParticipant,
|
|
159
|
+
callSettings: $bindableSettings.settings,
|
|
160
|
+
call: viewModel.call
|
|
161
|
+
))
|
|
162
|
+
} else {
|
|
163
|
+
EmptyView()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func participantsView(bounds: CGRect) -> some View {
|
|
168
|
+
viewFactory.makeVideoParticipantsView(
|
|
169
|
+
viewModel: viewModel,
|
|
170
|
+
availableFrame: bounds,
|
|
171
|
+
onChangeTrackVisibility: viewModel.changeTrackVisibility(for:isVisible:)
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public func makeVideoParticipantsView(
|
|
176
|
+
viewModel: CallViewModel,
|
|
177
|
+
availableFrame: CGRect,
|
|
178
|
+
onChangeTrackVisibility: @escaping @MainActor(CallParticipant, Bool) -> Void
|
|
179
|
+
) -> some View {
|
|
180
|
+
VideoParticipantsView(
|
|
181
|
+
viewFactory: self.viewFactory,
|
|
182
|
+
viewModel: viewModel,
|
|
183
|
+
availableFrame: availableFrame,
|
|
184
|
+
onChangeTrackVisibility: onChangeTrackVisibility
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private var participants: [CallParticipant] {
|
|
189
|
+
viewModel.participants
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Custom modifier that doesn't require binding
|
|
194
|
+
struct CustomLocalParticipantViewModifier: ViewModifier {
|
|
195
|
+
let localParticipant: CallParticipant
|
|
196
|
+
let callSettings: CallSettings
|
|
197
|
+
let call: Call?
|
|
198
|
+
|
|
199
|
+
func body(content: Content) -> some View {
|
|
200
|
+
content
|
|
201
|
+
.overlay(
|
|
202
|
+
VStack {
|
|
203
|
+
HStack {
|
|
204
|
+
Text(localParticipant.name)
|
|
205
|
+
.foregroundColor(.white)
|
|
206
|
+
.padding(.horizontal, 8)
|
|
207
|
+
.padding(.vertical, 4)
|
|
208
|
+
.background(Color.black.opacity(0.5))
|
|
209
|
+
.cornerRadius(8)
|
|
210
|
+
Spacer()
|
|
211
|
+
}
|
|
212
|
+
Spacer()
|
|
213
|
+
}
|
|
214
|
+
.padding(8)
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public struct CustomLocalVideoView<Factory: ViewFactory>: View {
|
|
220
|
+
|
|
221
|
+
@Injected(\.streamVideo) var streamVideo
|
|
222
|
+
|
|
223
|
+
private let callSettings: CallSettings
|
|
224
|
+
private var viewFactory: Factory
|
|
225
|
+
private var participant: CallParticipant
|
|
226
|
+
private var idSuffix: String
|
|
227
|
+
private var call: Call?
|
|
228
|
+
private var availableFrame: CGRect
|
|
229
|
+
|
|
230
|
+
public init(
|
|
231
|
+
viewFactory: Factory = DefaultViewFactory.shared,
|
|
232
|
+
participant: CallParticipant,
|
|
233
|
+
idSuffix: String = "local",
|
|
234
|
+
callSettings: CallSettings,
|
|
235
|
+
call: Call?,
|
|
236
|
+
availableFrame: CGRect
|
|
237
|
+
) {
|
|
238
|
+
self.viewFactory = viewFactory
|
|
239
|
+
self.participant = participant
|
|
240
|
+
self.idSuffix = idSuffix
|
|
241
|
+
self.callSettings = callSettings
|
|
242
|
+
self.call = call
|
|
243
|
+
self.availableFrame = availableFrame
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public var body: some View {
|
|
247
|
+
viewFactory.makeVideoParticipantView(
|
|
248
|
+
participant: participant,
|
|
249
|
+
id: "\(streamVideo.user.id)-\(idSuffix)",
|
|
250
|
+
availableFrame: availableFrame,
|
|
251
|
+
contentMode: .scaleAspectFit,
|
|
252
|
+
customData: ["videoOn": .bool(callSettings.videoOn)],
|
|
253
|
+
call: call
|
|
254
|
+
)
|
|
255
|
+
.adjustVideoFrame(to: availableFrame.width, ratio: availableFrame.width / availableFrame.height)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import StreamVideo
|
|
2
|
+
import StreamVideoSwiftUI
|
|
3
|
+
import StreamWebRTC
|
|
4
|
+
import SwiftUI
|
|
5
|
+
|
|
6
|
+
public struct CustomVideoCallParticipantView<Factory: ViewFactory>: View {
|
|
7
|
+
|
|
8
|
+
@Injected(\.images) var images
|
|
9
|
+
@Injected(\.streamVideo) var streamVideo
|
|
10
|
+
|
|
11
|
+
var viewFactory: Factory
|
|
12
|
+
let participant: CallParticipant
|
|
13
|
+
var id: String
|
|
14
|
+
var availableFrame: CGRect
|
|
15
|
+
var contentMode: UIView.ContentMode
|
|
16
|
+
var edgesIgnoringSafeArea: Edge.Set
|
|
17
|
+
var customData: [String: RawJSON]
|
|
18
|
+
var call: Call?
|
|
19
|
+
|
|
20
|
+
@State private var isUsingFrontCameraForLocalUser: Bool = false
|
|
21
|
+
|
|
22
|
+
public init(
|
|
23
|
+
viewFactory: Factory = DefaultViewFactory.shared,
|
|
24
|
+
participant: CallParticipant,
|
|
25
|
+
id: String? = nil,
|
|
26
|
+
availableFrame: CGRect,
|
|
27
|
+
contentMode: UIView.ContentMode,
|
|
28
|
+
edgesIgnoringSafeArea: Edge.Set = .all,
|
|
29
|
+
customData: [String: RawJSON],
|
|
30
|
+
call: Call?
|
|
31
|
+
) {
|
|
32
|
+
print("size: \(availableFrame.size)")
|
|
33
|
+
self.viewFactory = viewFactory
|
|
34
|
+
self.participant = participant
|
|
35
|
+
self.id = id ?? participant.id
|
|
36
|
+
self.availableFrame = availableFrame
|
|
37
|
+
self.contentMode = contentMode
|
|
38
|
+
self.edgesIgnoringSafeArea = edgesIgnoringSafeArea
|
|
39
|
+
self.customData = customData
|
|
40
|
+
self.call = call
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public var body: some View {
|
|
44
|
+
withCallSettingsObservation {
|
|
45
|
+
VideoRendererView(
|
|
46
|
+
id: id,
|
|
47
|
+
size: availableFrame.size,
|
|
48
|
+
contentMode: contentMode,
|
|
49
|
+
showVideo: showVideo,
|
|
50
|
+
handleRendering: { [weak call, participant] view in
|
|
51
|
+
guard call != nil else { return }
|
|
52
|
+
view.handleViewRendering(for: participant) { [weak call] size, participant in
|
|
53
|
+
Task { [weak call] in
|
|
54
|
+
await call?.updateTrackSize(size, for: participant)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
.opacity(showVideo ? 1 : 0)
|
|
61
|
+
.edgesIgnoringSafeArea(edgesIgnoringSafeArea)
|
|
62
|
+
.accessibility(identifier: "callParticipantView")
|
|
63
|
+
.streamAccessibility(value: showVideo ? "1" : "0")
|
|
64
|
+
.overlay(
|
|
65
|
+
// CustomCallParticipantImageView(
|
|
66
|
+
// viewFactory: viewFactory,
|
|
67
|
+
// id: participant.id,
|
|
68
|
+
// name: participant.name,
|
|
69
|
+
// imageURL: participant.profileImageURL
|
|
70
|
+
// // frame: availableFrame.size
|
|
71
|
+
// )
|
|
72
|
+
Color.green
|
|
73
|
+
.frame(width: availableFrame.width, height: availableFrame.height)
|
|
74
|
+
.opacity(showVideo ? 0 : 1)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private var showVideo: Bool {
|
|
79
|
+
participant.shouldDisplayTrack || customData["videoOn"]?.boolValue == true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@MainActor
|
|
83
|
+
@ViewBuilder
|
|
84
|
+
private func withCallSettingsObservation(
|
|
85
|
+
@ViewBuilder _ content: () -> some View
|
|
86
|
+
) -> some View {
|
|
87
|
+
if participant.id == streamVideo.state.activeCall?.state.localParticipant?.id {
|
|
88
|
+
content()
|
|
89
|
+
.rotation3DEffect(.degrees(180), axis: (x: 0, y: 1, z: 0))
|
|
90
|
+
.onReceive(call?.state.$callSettings) { self.isUsingFrontCameraForLocalUser = $0.cameraPosition == .front }
|
|
91
|
+
} else {
|
|
92
|
+
content()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
extension Color {
|
|
98
|
+
init(hex: UInt, alpha: Double = 1) {
|
|
99
|
+
self.init(
|
|
100
|
+
.sRGB,
|
|
101
|
+
red: Double((hex >> 16) & 0xff) / 255,
|
|
102
|
+
green: Double((hex >> 8) & 0xff) / 255,
|
|
103
|
+
blue: Double(hex & 0xff) / 255,
|
|
104
|
+
opacity: alpha
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
//
|
|
2
|
+
// ParticipantsView.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Michał Tremblay on 05/02/2025.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import SwiftUI
|
|
9
|
+
import StreamVideo
|
|
10
|
+
import StreamVideoSwiftUI
|
|
11
|
+
|
|
12
|
+
// Data structure to hold a label and a frame.
|
|
13
|
+
struct ViewFramePreferenceData: Equatable {
|
|
14
|
+
let label: String
|
|
15
|
+
let frame: CGRect
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// PreferenceKey to collect frames.
|
|
19
|
+
struct ViewFramePreferenceKey: PreferenceKey {
|
|
20
|
+
static var defaultValue: [ViewFramePreferenceData] = []
|
|
21
|
+
|
|
22
|
+
static func reduce(value: inout [ViewFramePreferenceData], nextValue: () -> [ViewFramePreferenceData]) {
|
|
23
|
+
value.append(contentsOf: nextValue())
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// An extension to attach a label and record the view's frame.
|
|
28
|
+
extension View {
|
|
29
|
+
func labelFrame(_ label: String) -> some View {
|
|
30
|
+
self.background(
|
|
31
|
+
GeometryReader { geo in
|
|
32
|
+
Color.clear
|
|
33
|
+
.preference(key: ViewFramePreferenceKey.self,
|
|
34
|
+
value: [ViewFramePreferenceData(label: label,
|
|
35
|
+
frame: geo.frame(in: .global))])
|
|
36
|
+
.onAppear {
|
|
37
|
+
print("ParticipantsView - Collecting frame for label: \(label)")
|
|
38
|
+
print("Frame: \(geo.frame(in: .global))")
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
struct ParticipantsView: View {
|
|
46
|
+
|
|
47
|
+
var call: Call
|
|
48
|
+
var participants: [CallParticipant]
|
|
49
|
+
var onChangeTrackVisibility: (CallParticipant?, Bool) -> Void
|
|
50
|
+
var localParticipant: CallParticipant
|
|
51
|
+
@State private var labeledFrames: [ViewFramePreferenceData] = []
|
|
52
|
+
|
|
53
|
+
private func findTouchInterceptView() -> TouchInterceptView? {
|
|
54
|
+
// Find the TouchInterceptView by traversing up the view hierarchy
|
|
55
|
+
var currentView = UIApplication.shared.windows.first?.rootViewController?.view
|
|
56
|
+
while let view = currentView {
|
|
57
|
+
if let touchInterceptView = view as? TouchInterceptView {
|
|
58
|
+
return touchInterceptView
|
|
59
|
+
}
|
|
60
|
+
currentView = view.superview
|
|
61
|
+
}
|
|
62
|
+
return nil
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
var body: some View {
|
|
66
|
+
GeometryReader { proxy in
|
|
67
|
+
if !participants.isEmpty {
|
|
68
|
+
ZStack {
|
|
69
|
+
if participants.count >= 5 && participants.count <= 6 {
|
|
70
|
+
let nonLocalParticipants = participants.filter { $0.userId != localParticipant.userId }
|
|
71
|
+
let hStackSpacing: CGFloat = 8
|
|
72
|
+
let vStackSpacing: CGFloat = 8
|
|
73
|
+
let columnWidth = (proxy.size.width - hStackSpacing) / 2 // Account for spacing between columns
|
|
74
|
+
|
|
75
|
+
HStack(spacing: hStackSpacing) {
|
|
76
|
+
VStack(spacing: vStackSpacing) {
|
|
77
|
+
ForEach(nonLocalParticipants.prefix(3)) { participant in
|
|
78
|
+
let frame = CGRect(
|
|
79
|
+
x: proxy.frame(in: .global).origin.x,
|
|
80
|
+
y: proxy.frame(in: .global).origin.y,
|
|
81
|
+
width: columnWidth,
|
|
82
|
+
height: (proxy.size.height - (vStackSpacing * 2)) / 3 // Account for 2 spaces between 3 rows
|
|
83
|
+
)
|
|
84
|
+
makeCallParticipantView(participant, frame: frame)
|
|
85
|
+
.frame(width: frame.width, height: frame.height)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
.frame(width: columnWidth, height: proxy.size.height)
|
|
89
|
+
|
|
90
|
+
VStack(spacing: vStackSpacing) {
|
|
91
|
+
ForEach(nonLocalParticipants.dropFirst(3)) { participant in
|
|
92
|
+
let frame = CGRect(
|
|
93
|
+
x: proxy.frame(in: .global).origin.x + columnWidth + hStackSpacing,
|
|
94
|
+
y: proxy.frame(in: .global).origin.y,
|
|
95
|
+
width: columnWidth,
|
|
96
|
+
height: (proxy.size.height - (vStackSpacing * 2)) / 3 // Account for 2 spaces between 3 rows
|
|
97
|
+
)
|
|
98
|
+
makeCallParticipantView(participant, frame: frame)
|
|
99
|
+
.frame(width: frame.width, height: frame.height)
|
|
100
|
+
}
|
|
101
|
+
let localFrame = CGRect(
|
|
102
|
+
x: proxy.frame(in: .global).origin.x + columnWidth + hStackSpacing,
|
|
103
|
+
y: proxy.frame(in: .global).origin.y,
|
|
104
|
+
width: columnWidth,
|
|
105
|
+
height: (proxy.size.height - (vStackSpacing * 2)) / 3 // Account for 2 spaces between 3 rows
|
|
106
|
+
)
|
|
107
|
+
makeCallParticipantView(localParticipant, frame: localFrame)
|
|
108
|
+
.frame(width: localFrame.width, height: localFrame.height)
|
|
109
|
+
}
|
|
110
|
+
.frame(width: columnWidth, height: proxy.size.height)
|
|
111
|
+
}
|
|
112
|
+
.frame(width: proxy.size.width, height: proxy.size.height)
|
|
113
|
+
.padding(4)
|
|
114
|
+
} else {
|
|
115
|
+
ScrollView {
|
|
116
|
+
LazyVStack {
|
|
117
|
+
if participants.count == 1, let participant = participants.first {
|
|
118
|
+
let frame = CGRect(
|
|
119
|
+
x: proxy.frame(in: .global).origin.x,
|
|
120
|
+
y: proxy.frame(in: .global).origin.y,
|
|
121
|
+
width: proxy.size.width,
|
|
122
|
+
height: proxy.size.height
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
makeCallParticipantView(participant, frame: frame)
|
|
126
|
+
.frame(width: frame.width, height: frame.height)
|
|
127
|
+
} else {
|
|
128
|
+
ForEach(participants.filter { $0.userId != localParticipant.userId }) { participant in
|
|
129
|
+
let frame = CGRect(
|
|
130
|
+
x: proxy.frame(in: .global).origin.x,
|
|
131
|
+
y: proxy.frame(in: .global).origin.y,
|
|
132
|
+
width: proxy.size.width,
|
|
133
|
+
height: participants.count == 4 ? proxy.size.height / 3 : (participants.count == 2 ? proxy.size.height : proxy.size.height / 2)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
makeCallParticipantView(participant, frame: frame)
|
|
137
|
+
.frame(width: frame.width, height: frame.height)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if participants.count >= 2 && participants.count <= 4 {
|
|
144
|
+
CornerDraggableView(
|
|
145
|
+
content: { availableFrame in
|
|
146
|
+
LocalVideoView(
|
|
147
|
+
viewFactory: DefaultViewFactory.shared,
|
|
148
|
+
participant: localParticipant,
|
|
149
|
+
callSettings: call.state.callSettings,
|
|
150
|
+
call: call,
|
|
151
|
+
availableFrame: CGRect(
|
|
152
|
+
x: availableFrame.origin.x,
|
|
153
|
+
y: availableFrame.origin.y,
|
|
154
|
+
width: availableFrame.width,
|
|
155
|
+
height: availableFrame.height * 0.8
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
.frame(width: availableFrame.width, height: availableFrame.height * 0.8)
|
|
159
|
+
.cornerRadius(12)
|
|
160
|
+
.labelFrame("abc")
|
|
161
|
+
},
|
|
162
|
+
proxy: proxy
|
|
163
|
+
) {
|
|
164
|
+
withAnimation {
|
|
165
|
+
if participants.count == 1 {
|
|
166
|
+
// call.state.callSettings.localVideoPrimary.toggle()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
.onPreferenceChange(ViewFramePreferenceKey.self) { frames in
|
|
174
|
+
print("ParticipantsView - Received frame updates:")
|
|
175
|
+
print("Number of frames: \(frames.count)")
|
|
176
|
+
frames.forEach { frame in
|
|
177
|
+
print("Label: \(frame.label), Frame: \(frame.frame)")
|
|
178
|
+
}
|
|
179
|
+
self.labeledFrames = frames
|
|
180
|
+
if let touchInterceptView = findTouchInterceptView() {
|
|
181
|
+
print("ParticipantsView - Found TouchInterceptView, updating frames")
|
|
182
|
+
touchInterceptView.updateLabeledFrames(frames)
|
|
183
|
+
} else {
|
|
184
|
+
print("ParticipantsView - Failed to find TouchInterceptView!")
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
Color.gray
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
.edgesIgnoringSafeArea(.all)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@ViewBuilder
|
|
195
|
+
private func makeCallParticipantView(_ participant: CallParticipant, frame: CGRect) -> some View {
|
|
196
|
+
CustomVideoCallParticipantView(
|
|
197
|
+
participant: participant,
|
|
198
|
+
availableFrame: frame,
|
|
199
|
+
contentMode: .scaleAspectFit,
|
|
200
|
+
customData: [:],
|
|
201
|
+
call: call
|
|
202
|
+
)
|
|
203
|
+
// .onAppear { onChangeTrackVisibility(participant, true) }
|
|
204
|
+
// .onDisappear{ onChangeTrackVisibility(participant, false) }
|
|
205
|
+
}
|
|
206
|
+
}
|