@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.
Files changed (39) hide show
  1. package/Package.swift +31 -0
  2. package/README.md +340 -0
  3. package/StreamCall.podspec +19 -0
  4. package/android/build.gradle +74 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/ee/forgr/capacitor/streamcall/CallOverlayView.kt +281 -0
  7. package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +142 -0
  8. package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +147 -0
  9. package/android/src/main/java/ee/forgr/capacitor/streamcall/RingtonePlayer.kt +164 -0
  10. package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +1014 -0
  11. package/android/src/main/java/ee/forgr/capacitor/streamcall/TouchInterceptWrapper.kt +31 -0
  12. package/android/src/main/java/ee/forgr/capacitor/streamcall/UserRepository.kt +111 -0
  13. package/android/src/main/res/.gitkeep +0 -0
  14. package/android/src/main/res/values/strings.xml +7 -0
  15. package/dist/docs.json +533 -0
  16. package/dist/esm/definitions.d.ts +169 -0
  17. package/dist/esm/definitions.js +2 -0
  18. package/dist/esm/definitions.js.map +1 -0
  19. package/dist/esm/index.d.ts +4 -0
  20. package/dist/esm/index.js +7 -0
  21. package/dist/esm/index.js.map +1 -0
  22. package/dist/esm/web.d.ts +32 -0
  23. package/dist/esm/web.js +323 -0
  24. package/dist/esm/web.js.map +1 -0
  25. package/dist/plugin.cjs.js +337 -0
  26. package/dist/plugin.cjs.js.map +1 -0
  27. package/dist/plugin.js +339 -0
  28. package/dist/plugin.js.map +1 -0
  29. package/ios/Sources/StreamCallPlugin/CallOverlayView.swift +147 -0
  30. package/ios/Sources/StreamCallPlugin/CustomCallParticipantImageView.swift +60 -0
  31. package/ios/Sources/StreamCallPlugin/CustomCallView.swift +257 -0
  32. package/ios/Sources/StreamCallPlugin/CustomVideoParticipantsView.swift +107 -0
  33. package/ios/Sources/StreamCallPlugin/ParticipantsView.swift +206 -0
  34. package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +722 -0
  35. package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +177 -0
  36. package/ios/Sources/StreamCallPlugin/UserRepository.swift +96 -0
  37. package/ios/Sources/StreamCallPlugin/WebviewNavigationDelegate.swift +68 -0
  38. package/ios/Tests/StreamCallPluginTests/StreamCallPluginTests.swift +15 -0
  39. 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
+ }