diff --git a/.DS_Store b/.DS_Store index 98f1654..3038961 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/VideoSDKSwiftUIExample.xcodeproj/project.pbxproj b/VideoSDKSwiftUIExample.xcodeproj/project.pbxproj index 2405a70..2400a95 100644 --- a/VideoSDKSwiftUIExample.xcodeproj/project.pbxproj +++ b/VideoSDKSwiftUIExample.xcodeproj/project.pbxproj @@ -7,7 +7,11 @@ objects = { /* Begin PBXBuildFile section */ - 6B537DBD2BDF9DA60074D762 /* VideoSDKRTCSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 6B537DBC2BDF9DA60074D762 /* VideoSDKRTCSwift */; }; + 2C61FD432D15324D007B6C62 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61FD422D15324D007B6C62 /* CustomTextField.swift */; }; + 2C61FD452D15325B007B6C62 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61FD442D15325B007B6C62 /* ActionButton.swift */; }; + 2C61FD472D153266007B6C62 /* CameraPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61FD462D153266007B6C62 /* CameraPreviewView.swift */; }; + 2C61FD492D153270007B6C62 /* CameraPreviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C61FD482D153270007B6C62 /* CameraPreviewModel.swift */; }; + 2C61FD502D153575007B6C62 /* VideoSDKRTCSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2C61FD4F2D153575007B6C62 /* VideoSDKRTCSwift */; }; 6B5693672BDFA1FB00CF3522 /* StartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5693662BDFA1FB00CF3522 /* StartView.swift */; }; 6BF0EBF32BD947420000DF8E /* RoomStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BF0EBF22BD947420000DF8E /* RoomStruct.swift */; }; 6BF0EBFB2BD94B7B0000DF8E /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BF0EBFA2BD94B7B0000DF8E /* ReplayKit.framework */; }; @@ -49,6 +53,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2C61FD422D15324D007B6C62 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = ""; }; + 2C61FD442D15325B007B6C62 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; + 2C61FD462D153266007B6C62 /* CameraPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreviewView.swift; sourceTree = ""; }; + 2C61FD482D153270007B6C62 /* CameraPreviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreviewModel.swift; sourceTree = ""; }; 6B5693662BDFA1FB00CF3522 /* StartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartView.swift; sourceTree = ""; }; 6BF0EBF22BD947420000DF8E /* RoomStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStruct.swift; sourceTree = ""; }; 6BF0EBF82BD94B7B0000DF8E /* Broadcast.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Broadcast.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -83,16 +91,36 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6B537DBD2BDF9DA60074D762 /* VideoSDKRTCSwift in Frameworks */, + 2C61FD502D153575007B6C62 /* VideoSDKRTCSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 2C61FD402D1531FB007B6C62 /* UIStruct */ = { + isa = PBXGroup; + children = ( + 2C61FD442D15325B007B6C62 /* ActionButton.swift */, + 2C61FD422D15324D007B6C62 /* CustomTextField.swift */, + ); + path = UIStruct; + sourceTree = ""; + }; + 2C61FD412D153205007B6C62 /* PrecallView */ = { + isa = PBXGroup; + children = ( + 2C61FD482D153270007B6C62 /* CameraPreviewModel.swift */, + 2C61FD462D153266007B6C62 /* CameraPreviewView.swift */, + ); + path = PrecallView; + sourceTree = ""; + }; 6B5693612BDFA1B400CF3522 /* Screens */ = { isa = PBXGroup; children = ( + 2C61FD402D1531FB007B6C62 /* UIStruct */, + 2C61FD412D153205007B6C62 /* PrecallView */, 6B5693652BDFA1DE00CF3522 /* MeetingView */, 6B5693622BDFA1BF00CF3522 /* StartView */, ); @@ -215,7 +243,7 @@ ); name = VideoSDKSwiftUIExample; packageProductDependencies = ( - 6B537DBC2BDF9DA60074D762 /* VideoSDKRTCSwift */, + 2C61FD4F2D153575007B6C62 /* VideoSDKRTCSwift */, ); productName = VideoSDKSwiftUIExample; productReference = B22C637329F1997900EEE43A /* VideoSDKSwiftUIExample.app */; @@ -249,7 +277,7 @@ ); mainGroup = B22C636A29F1997900EEE43A; packageReferences = ( - 6B537DBB2BDF9DA60074D762 /* XCRemoteSwiftPackageReference "videosdk-rtc-ios-spm" */, + 2C61FD4E2D153575007B6C62 /* XCRemoteSwiftPackageReference "videosdk-rtc-ios-spm" */, ); productRefGroup = B22C637429F1997900EEE43A /* Products */; projectDirPath = ""; @@ -297,11 +325,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2C61FD472D153266007B6C62 /* CameraPreviewView.swift in Sources */, 6BF0EBF32BD947420000DF8E /* RoomStruct.swift in Sources */, + 2C61FD492D153270007B6C62 /* CameraPreviewModel.swift in Sources */, B22C637729F1997900EEE43A /* VideoSDKSwiftUIExampleApp.swift in Sources */, + 2C61FD432D15324D007B6C62 /* CustomTextField.swift in Sources */, B22C638A29F1CD8400EEE43A /* MeetingViewController.swift in Sources */, B22C638829F19BC200EEE43A /* MeetingView.swift in Sources */, 6B5693672BDFA1FB00CF3522 /* StartView.swift in Sources */, + 2C61FD452D15325B007B6C62 /* ActionButton.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -594,9 +626,9 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 6B537DBB2BDF9DA60074D762 /* XCRemoteSwiftPackageReference "videosdk-rtc-ios-spm" */ = { + 2C61FD4E2D153575007B6C62 /* XCRemoteSwiftPackageReference "videosdk-rtc-ios-spm" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/videosdk-live/videosdk-rtc-ios-spm.git"; + repositoryURL = "https://github.com/videosdk-live/videosdk-rtc-ios-spm"; requirement = { branch = main; kind = branch; @@ -605,9 +637,9 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 6B537DBC2BDF9DA60074D762 /* VideoSDKRTCSwift */ = { + 2C61FD4F2D153575007B6C62 /* VideoSDKRTCSwift */ = { isa = XCSwiftPackageProductDependency; - package = 6B537DBB2BDF9DA60074D762 /* XCRemoteSwiftPackageReference "videosdk-rtc-ios-spm" */; + package = 2C61FD4E2D153575007B6C62 /* XCRemoteSwiftPackageReference "videosdk-rtc-ios-spm" */; productName = VideoSDKRTCSwift; }; /* End XCSwiftPackageProductDependency section */ diff --git a/VideoSDKSwiftUIExample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/VideoSDKSwiftUIExample.xcworkspace/xcshareddata/swiftpm/Package.resolved index 423aad0..62170b3 100644 --- a/VideoSDKSwiftUIExample.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/VideoSDKSwiftUIExample.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,14 +1,15 @@ { + "originHash" : "3bcce08c5dc6773fa23cbda274afbb9a7a53a250e8efbeb017aeed9ef36f18c9", "pins" : [ { "identity" : "videosdk-rtc-ios-spm", "kind" : "remoteSourceControl", - "location" : "https://github.com/videosdk-live/videosdk-rtc-ios-spm.git", + "location" : "https://github.com/videosdk-live/videosdk-rtc-ios-spm", "state" : { "branch" : "main", - "revision" : "c97e7971c7e7f597b5501f7642672439c0c531d6" + "revision" : "43c360e7395f5879da5c5b499591f8153ecfadb7" } } ], - "version" : 2 + "version" : 3 } diff --git a/VideoSDKSwiftUIExample.xcworkspace/xcuserdata/deepbhupatkar.xcuserdatad/UserInterfaceState.xcuserstate b/VideoSDKSwiftUIExample.xcworkspace/xcuserdata/deepbhupatkar.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..70bf32e Binary files /dev/null and b/VideoSDKSwiftUIExample.xcworkspace/xcuserdata/deepbhupatkar.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/VideoSDKSwiftUIExample.xcworkspace/xcuserdata/deepbhupatkar.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/VideoSDKSwiftUIExample.xcworkspace/xcuserdata/deepbhupatkar.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..3771f11 --- /dev/null +++ b/VideoSDKSwiftUIExample.xcworkspace/xcuserdata/deepbhupatkar.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + diff --git a/VideoSDKSwiftUIExample/Screens/MeetingView/MeetingView.swift b/VideoSDKSwiftUIExample/Screens/MeetingView/MeetingView.swift index dcccffb..9ca5cc7 100644 --- a/VideoSDKSwiftUIExample/Screens/MeetingView/MeetingView.swift +++ b/VideoSDKSwiftUIExample/Screens/MeetingView/MeetingView.swift @@ -9,244 +9,333 @@ import SwiftUI import VideoSDKRTC import WebRTC +struct ParticipantContainerView: View { + let participant: Participant + // Add state object for camera preview + @StateObject private var cameraPreview = CameraPreviewModel() -struct MeetingView: View{ + @ObservedObject var meetingViewController: MeetingViewController + + var body: some View { + ZStack { + // Main participant view + participantView(participant: participant, meetingViewController: meetingViewController) + + // Overlay for name and mic status + VStack { + Spacer() + HStack { + + // Participant name + Text(participant.displayName) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.black.opacity(0.5)) + .cornerRadius(4) + + // Mic status indicator + Image(systemName: meetingViewController.participantMicStatus[participant.id] ?? false ? "mic.fill" : "mic.slash.fill") + .foregroundColor(meetingViewController.participantMicStatus[participant.id] ?? false ? .green : .red) + .padding(4) + .background(Color.black.opacity(0.5)) + .clipShape(Circle()) + + + Spacer() + } + .padding(8) + } + } + // Add border, background, shadow, and rounded corners + .background(Color.black.opacity(0.9)) // Background color + .cornerRadius(10) // Rounded corners + .shadow(color: Color.gray.opacity(0.7), radius: 10, x: 0, y: 5) // Shadow effect + .overlay( + RoundedRectangle(cornerRadius: 10) // Rounded border + .stroke(Color.gray.opacity(0.9), lineWidth: 1) + ) + + } + + private func participantView(participant: Participant, meetingViewController: MeetingViewController) -> some View { + ZStack { + ParticipantView(participant: participant, meetingViewController: meetingViewController) + } + } +} + +struct MeetingView: View { @Environment(\.presentationMode) var presentationMode + @ObservedObject var meetingViewController: MeetingViewController - @ObservedObject var meetingViewController = MeetingViewController() - @State var meetingId: String? - @State var userName: String? - @State var isUnMute: Bool = true - @State var camEnabled: Bool = true - @State var isScreenShare: Bool = false + @State var meetingId: String? + @State var userName: String? + @State var isUnMute: Bool = true + @State var camEnabled: Bool = true + @State var isScreenShare: Bool = false var body: some View { - - VStack { if meetingViewController.participants.count == 0 { Text("Meeting Initializing") } else { VStack { - VStack(spacing: 20) { - Text("Meeting ID: \(meetingViewController.meetingID)") - .padding(.vertical) - - List { - ForEach(meetingViewController.participants.indices, id: \.self) { index in - Text("Participant Name: \(meetingViewController.participants[index].displayName)") - ZStack { - - ParticipantView(track: meetingViewController.participants[index].streams.first(where: { $1.kind == .state(value: .video) })?.value.track as? RTCVideoTrack).frame(height: 250) - if meetingViewController.participants[index].streams.first(where: { $1.kind == .state(value: .video) }) == nil { - Color.white.opacity(1.0).frame(width: UIScreen.main.bounds.width, height: 250) - Text("No media") + Text("Meeting ID: \(meetingViewController.meetingID)") + .padding(.vertical) + + GeometryReader { geometry in + VStack(spacing: 0) { + let totalParticipants = meetingViewController.participants.count + + switch totalParticipants { + case 1: + ParticipantContainerView( + participant: meetingViewController.participants[0], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width, height: geometry.size.height) + + case 2: + VStack(spacing: 0) { + ForEach(0..<2) { index in + ParticipantContainerView( + participant: meetingViewController.participants[index], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width, height: geometry.size.height / 2) + } + } + + case 3: + VStack(spacing: 0) { + HStack(spacing: 0) { + ForEach(0..<2) { index in + ParticipantContainerView( + participant: meetingViewController.participants[index], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width / 2, height: geometry.size.height / 2) + } + } + ParticipantContainerView( + participant: meetingViewController.participants[2], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width, height: geometry.size.height / 2) + } + + case 4: + VStack(spacing: 0) { + HStack(spacing: 0) { + ForEach(0..<2) { index in + ParticipantContainerView( + participant: meetingViewController.participants[index], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width / 2, height: geometry.size.height / 2) + } + } + HStack(spacing: 0) { + ForEach(2..<4) { index in + ParticipantContainerView( + participant: meetingViewController.participants[index], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width / 2, height: geometry.size.height / 2) + } + } + } + + case 5: + VStack(spacing: 0) { + HStack(spacing: 0) { + ForEach(0..<2) { index in + ParticipantContainerView( + participant: meetingViewController.participants[index], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width / 2, height: geometry.size.height / 3) + } + } + ParticipantContainerView( + participant: meetingViewController.participants[2], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width, height: geometry.size.height / 3) + HStack(spacing: 0) { + ForEach(3..<5) { index in + ParticipantContainerView( + participant: meetingViewController.participants[index], + meetingViewController: meetingViewController + ) + .frame(width: geometry.size.width / 2, height: geometry.size.height / 3) + } } } + default: + ScrollView { + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 0) { + ForEach(meetingViewController.participants.indices, id: \.self) { index in + ParticipantContainerView( + participant: meetingViewController.participants[index], + meetingViewController: meetingViewController + ) + .frame(height: geometry.size.height / 3) + } + } + } } } } + // Control buttons VStack { HStack(spacing: 15) { - // mic button Button { - if isUnMute { - isUnMute = false - meetingViewController.meeting?.muteMic() - - } - else { - isUnMute = true - meetingViewController.meeting?.unmuteMic() - } + isUnMute.toggle() + isUnMute ? meetingViewController.meeting?.unmuteMic(): meetingViewController.meeting?.muteMic() } label: { - Text("Toggle Mic") .foregroundStyle(Color.white) .font(.caption) .padding() - .background( - RoundedRectangle(cornerRadius: 25) - .fill(Color.blue)) - // - // Image(systemName: isUnMute ? "mic.fill" : "mic.slash") - // .resizable() - // .scaledToFit() - // .font(.headline) - // .frame(width: 40, height: 40) - // .padding(10) - // .foregroundColor(.orange) - // .shadow(radius: 3, x: 3) + .background(RoundedRectangle(cornerRadius: 25).fill(Color.blue)) } - // camera button Button { - if camEnabled { - camEnabled = false - meetingViewController.meeting?.disableWebcam() - } - else { - camEnabled = true - meetingViewController.meeting?.enableWebcam() - } + camEnabled.toggle() + camEnabled ? meetingViewController.meeting?.enableWebcam() : meetingViewController.meeting?.disableWebcam() } label: { - - Text("Toggle WebCam") .foregroundStyle(Color.white) .font(.caption) .padding() - .background( - RoundedRectangle(cornerRadius: 25) - .fill(Color.blue)) - - // Image(systemName: camEnabled ? "video.fill" : "video.slash") - // .resizable() - // .scaledToFit() - // .font(.headline) - // .frame(width: 40, height: 40) - // .padding(10) - // .foregroundColor(.green) - // .shadow(radius: 3, x: 3) + .background(RoundedRectangle(cornerRadius: 25).fill(Color.blue)) } } - HStack{ - // screenshare button + + HStack { Button { - if isScreenShare { - isScreenShare = false - Task { - await meetingViewController.meeting?.disableScreenShare() - } - } - else { - isScreenShare = true - Task { + isScreenShare.toggle() + Task { + if isScreenShare { await meetingViewController.meeting?.enableScreenShare() + } else { + await meetingViewController.meeting?.disableScreenShare() } } } label: { - - Text("Toggle ScreenShare") .foregroundStyle(Color.white) .font(.caption) .padding() - .background( - RoundedRectangle(cornerRadius: 25) - .fill(Color.blue)) - // Image(systemName: "rectangle.inset.filled.and.person.filled") - // .resizable() - // .scaledToFit() - // .font(.headline) - // .frame(width: 40, height: 40) - // .padding(10) - // .foregroundColor(.black) - // .shadow(radius: 3, x: 3) + .background(RoundedRectangle(cornerRadius: 25).fill(Color.blue)) } - // end meeting button Button { meetingViewController.meeting?.end() presentationMode.wrappedValue.dismiss() } label: { - - Text("End Call") .foregroundStyle(Color.white) .font(.caption) .padding() - .background( - RoundedRectangle(cornerRadius: 25) - .fill(Color.red)) - - // Image(systemName: "phone.down.fill") - // .resizable() - // .scaledToFit() - // .frame(width: 40, height: 40) - // .font(.headline) - // .padding(10) - // .foregroundColor(.red) - // .shadow(radius: 3, x: 3) - // }} + .background(RoundedRectangle(cornerRadius: 25).fill(Color.red)) } - + Button { + meetingViewController.meeting?.leave() + presentationMode.wrappedValue.dismiss() + } label: { + Text("leave Call") + .foregroundStyle(Color.white) + .font(.caption) + .padding() + .background(RoundedRectangle(cornerRadius: 25).fill(Color.indigo)) + } } .padding(.bottom) } } - } - }.onAppear() { - /// MARK :- configuring the videoSDK + } + .onAppear { VideoSDK.config(token: meetingViewController.token) - if meetingId?.isEmpty == false { - meetingViewController.joinMeeting(meetingId: meetingId!, userName: userName!) - } - else { + if let meetingId = meetingId, !meetingId.isEmpty { + meetingViewController.joinMeeting(meetingId: meetingId, userName: userName!) + } else { meetingViewController.joinRoom(userName: userName!) } - /// Initializing the meeting -// meetingViewController.initializeMeeting() } - } } /// VideoView for participant's video class VideoView: UIView { - var videoView: RTCMTLVideoView = { let view = RTCMTLVideoView() view.videoContentMode = .scaleAspectFill view.backgroundColor = UIColor.black view.clipsToBounds = true - view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 250) - + view.transform = CGAffineTransform(scaleX: 1, y: 1) + return view }() - init(track: RTCVideoTrack?) { - super.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 250)) + init(track: RTCVideoTrack?, frame: CGRect) { + super.init(frame: frame) backgroundColor = .clear + + // Set videoView frame to match parent view + videoView.frame = bounds + DispatchQueue.main.async { self.addSubview(self.videoView) self.bringSubviewToFront(self.videoView) track?.add(self.videoView) } - } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func layoutSubviews() { + super.layoutSubviews() + // Update videoView frame when parent view size changes + videoView.frame = bounds + } } /// ParticipantView for showing and hiding VideoView -struct ParticipantView: UIViewRepresentable { - var track: RTCVideoTrack? +struct ParticipantView: View { + let participant: Participant + @ObservedObject var meetingViewController: MeetingViewController + + var body: some View { + ZStack { + if let track = meetingViewController.participantVideoTracks[participant.id] { + VideoStreamView(track: track) + } else { + Color.white.opacity(1.0) + Text("No media") + } + } + } +} + +struct VideoStreamView: UIViewRepresentable { + let track: RTCVideoTrack func makeUIView(context: Context) -> VideoView { - let view = VideoView(track: track) - view.frame = CGRect(x: 0, y: 0, width: 250, height: 250) + let view = VideoView(track: track, frame: .zero) return view } func updateUIView(_ uiView: VideoView, context: Context) { - print("ui updated") - if track != nil { - track?.add(uiView.videoView) - } else { - track?.remove(uiView.videoView) - } + track.add(uiView.videoView) } } - - - - - diff --git a/VideoSDKSwiftUIExample/Screens/MeetingView/MeetingViewController.swift b/VideoSDKSwiftUIExample/Screens/MeetingView/MeetingViewController.swift index 0d33a7e..f498375 100644 --- a/VideoSDKSwiftUIExample/Screens/MeetingView/MeetingViewController.swift +++ b/VideoSDKSwiftUIExample/Screens/MeetingView/MeetingViewController.swift @@ -15,32 +15,63 @@ class MeetingViewController: ObservableObject { var token = "Your_Token" var meetingId: String = "" var name: String = "" - + var cameraMode: AVCaptureDevice.Position + var audioDevice: String? + @Published var meeting: Meeting? = nil @Published var localParticipantView: VideoView? = nil - @Published var videoTrack: RTCVideoTrack? @Published var participants: [Participant] = [] @Published var meetingID: String = "" - - + @Published var participantVideoTracks: [String: RTCVideoTrack] = [:] + @Published var participantMic : Bool = false + @Published var participantMicStatus: [String: Bool] = [:] + + init(cameraMode: AVCaptureDevice.Position , audioDevice: String?) { + self.cameraMode = cameraMode + self.audioDevice = audioDevice + } + + func initializeMeeting(meetingId: String, userName: String) { + print("Inside initializeMeeting Starting") + + // Initialize the meeting + var videoMediaTrack = try? VideoSDK.createCameraVideoTrack( + encoderConfig: .h720p_w1280p, + facingMode: cameraMode, + multiStream: false + ) meeting = VideoSDK.initMeeting( meetingId: meetingId, participantName: userName, micEnabled: true, - webcamEnabled: true + webcamEnabled: true, + customCameraVideoStream: videoMediaTrack +// multiStream: true + ) - + // Add event listeners and join the meeting meeting?.addEventListener(self) - - meeting?.join(cameraPosition: .front) + meeting?.join() + print("Inside initializeMeeting End ") + } + func forPrecallAudioDeviceSetup() + { + if let audio = audioDevice { + print("## audio \(audio)") + meeting?.changeMic(selectedDevice: audio) + } + } + } extension MeetingViewController: MeetingEventListener { func onMeetingJoined() { - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.forPrecallAudioDeviceSetup() + } guard let localParticipant = self.meeting?.localParticipant else { return } // add to list @@ -50,6 +81,7 @@ extension MeetingViewController: MeetingEventListener { localParticipant.addEventListener(self) localParticipant.setQuality(.high) + } func onParticipantJoined(_ participant: Participant) { @@ -70,8 +102,7 @@ extension MeetingViewController: MeetingEventListener { meeting?.localParticipant.removeEventListener(self) meeting?.removeEventListener(self) - - + participants.removeAll() } func onMeetingStateChanged(meetingState: MeetingState) { switch meetingState { @@ -87,37 +118,35 @@ extension MeetingViewController: MeetingEventListener { extension MeetingViewController: ParticipantEventListener { func onStreamEnabled(_ stream: MediaStream, forParticipant participant: Participant) { - - if participant.isLocal && stream.kind == .share { - return - } - - if participant.isLocal { - if let track = stream.track as? RTCVideoTrack { - DispatchQueue.main.async { - self.videoTrack = track - } - } - } else { - if let track = stream.track as? RTCVideoTrack { - DispatchQueue.main.async { - self.videoTrack = track + if let track = stream.track as? RTCVideoTrack { + DispatchQueue.main.async { + if case .state(let mediaKind) = stream.kind, mediaKind == .video { + print("WebCam Enable event") + + self.participantVideoTracks[participant.id] = track } } } + + if case .state(let mediaKind) = stream.kind, mediaKind == .audio { + print("Mic Enable event") + self.participantMicStatus[participant.id] = true // Mic enabled + } } - + func onStreamDisabled(_ stream: MediaStream, forParticipant participant: Participant) { - - if participant.isLocal { - - if let _ = stream.track as? RTCVideoTrack { - DispatchQueue.main.async { - self.videoTrack = nil - } + DispatchQueue.main.async { + if case .state(let mediaKind) = stream.kind, mediaKind == .video { + print("WebCam Disaled event") + + self.participantVideoTracks.removeValue(forKey: participant.id) } - } else { - self.videoTrack = nil + } + if case .state(let mediaKind) = stream.kind, mediaKind == .audio { + print("Mic Disaled event") + // Update microphone state for this participant + self.participantMicStatus[participant.id] = false // Mic disabled + } } } diff --git a/VideoSDKSwiftUIExample/Screens/PrecallView/CameraPreviewModel.swift b/VideoSDKSwiftUIExample/Screens/PrecallView/CameraPreviewModel.swift new file mode 100644 index 0000000..fa6f8d5 --- /dev/null +++ b/VideoSDKSwiftUIExample/Screens/PrecallView/CameraPreviewModel.swift @@ -0,0 +1,111 @@ +// +// CameraPreviewModel.swift +// VideoSDKSwiftUIExample +// +// Created by Deep Bhupatkar on 20/12/24. +// + + +import Foundation +import AVFoundation + +class CameraPreviewModel: ObservableObject { + @Published var session = AVCaptureSession() + private var videoDeviceInput: AVCaptureDeviceInput? + private var currentCameraPosition: AVCaptureDevice.Position = .front // Track the current camera position + + func checkPermissionsAndSetupSession() { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + setupCaptureSession() + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in + if granted { + DispatchQueue.main.async { + self?.setupCaptureSession() + } + } + } + default: + print("Camera access denied or restricted.") + } + } + + private func setupCaptureSession() { + session.beginConfiguration() + + // Video setup + let videoPosition: AVCaptureDevice.Position = currentCameraPosition + guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, + for: .video, + position: videoPosition) else { + print("Error: Unable to find video device.") + session.commitConfiguration() + return + } + + do { + let videoInput = try AVCaptureDeviceInput(device: videoDevice) + if session.canAddInput(videoInput) { + session.addInput(videoInput) + self.videoDeviceInput = videoInput + } else { + print("Error: Unable to add video input to the session.") + } + } catch { + print("Error setting up video input: \(error)") + } + + session.commitConfiguration() + } + + func flipCamera() { + guard let currentInput = videoDeviceInput else { return } + + // Toggle between front and back camera + currentCameraPosition = currentCameraPosition == .front ? .back : .front + + let newPosition: AVCaptureDevice.Position = currentCameraPosition + guard let newDevice = AVCaptureDevice.default(.builtInWideAngleCamera, + for: .video, + position: newPosition) else { + print("Error: Unable to find camera for position \(newPosition).") + return + } + + session.beginConfiguration() + session.removeInput(currentInput) + + do { + let newVideoInput = try AVCaptureDeviceInput(device: newDevice) + if session.canAddInput(newVideoInput) { + session.addInput(newVideoInput) + self.videoDeviceInput = newVideoInput + } else { + print("Error: Unable to add new video input to the session.") + } + } catch { + print("Error switching cameras: \(error)") + } + + session.commitConfiguration() + } + + func startSession() { + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.session.startRunning() + } + } + + func stopSession() { + DispatchQueue.main.async { [weak self] in + if let session = self?.session, session.isRunning { + session.stopRunning() + print("Session stopped.") + } else { + print("Session is already stopped or not initialized.") + } + } + } +} + diff --git a/VideoSDKSwiftUIExample/Screens/PrecallView/CameraPreviewView.swift b/VideoSDKSwiftUIExample/Screens/PrecallView/CameraPreviewView.swift new file mode 100644 index 0000000..01aa6a3 --- /dev/null +++ b/VideoSDKSwiftUIExample/Screens/PrecallView/CameraPreviewView.swift @@ -0,0 +1,33 @@ +// +// CameraPreviewView.swift +// VideoSDKSwiftUIExample +// +// Created by Deep Bhupatkar on 20/12/24. +// + + +import SwiftUI +import AVFoundation + +struct CameraPreviewView: UIViewRepresentable { + let session: AVCaptureSession + + func makeUIView(context: Context) -> VideoPreviewView { + let view = VideoPreviewView() + view.videoPreviewLayer.session = session + view.videoPreviewLayer.videoGravity = .resizeAspectFill + return view + } + + func updateUIView(_ uiView: VideoPreviewView, context: Context) {} +} + +class VideoPreviewView: UIView { + override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } + + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } +} diff --git a/VideoSDKSwiftUIExample/Screens/StartView/StartView.swift b/VideoSDKSwiftUIExample/Screens/StartView/StartView.swift index 79d52d6..0a6f526 100644 --- a/VideoSDKSwiftUIExample/Screens/StartView/StartView.swift +++ b/VideoSDKSwiftUIExample/Screens/StartView/StartView.swift @@ -6,113 +6,234 @@ // import SwiftUI +import Foundation +import VideoSDKRTC +import AVFoundation struct StartView: View { - @State var meetingId: String @State var name: String + @State private var isMicEnabled: Bool = true + @State private var isFrontCamera: Bool = true + @StateObject private var cameraPreview = CameraPreviewModel() + @State private var showActionSheet = false + @State private var audioDeviceList: [String] = [] + @State private var selectedCameraMode: AVCaptureDevice.Position = .front + @State private var selectedAudioDevice: String? + @State private var isNavigating = false + @State private var isCameraEnabled: Bool = true + + // Colors and constants + private let accentColor = Color(red: 0.2, green: 0.5, blue: 1.0) + private let backgroundColor = Color(red: 0.98, green: 0.98, blue: 0.98) var body: some View { - - NavigationView { + NavigationView { + ZStack { + // Background gradient + LinearGradient( + gradient: Gradient(colors: [backgroundColor, Color.white]), + startPoint: .top, + endPoint: .bottom + ).ignoresSafeArea() - VStack { - - Text("VideoSDK") - .font(.largeTitle) - .fontWeight(.bold) - Text("Swift UI Quickstart") - .font(.largeTitle) - .fontWeight(.semibold) - .padding(.bottom) - .padding(.bottom) - .padding(.bottom) - - TextField("Enter MeetingId", text: $meetingId) - .foregroundColor(Color.black) - .autocorrectionDisabled() - .font(.headline) - .overlay( - Image(systemName: "xmark.circle.fill") - .padding() - .offset(x: 10) - .foregroundColor(Color.gray) - .opacity(meetingId.isEmpty ? 0.0 : 1.0) - .onTapGesture { - UIApplication.shared.endEditing() - meetingId = "" + ScrollView { + VStack(spacing: 24) { + // Header + VStack(spacing: 8) { + Text("VideoSDK") + .font(.system(size: 32, weight: .bold)) + .foregroundColor(accentColor) + Text("Swift UI Quickstart") + .font(.system(size: 20, weight: .medium)) + .foregroundColor(.gray) + } + .padding(.top, 25) + + //Precall View and Controlls for that + ZStack { + VStack { + // Camera Preview + GeometryReader { geometry in + ZStack { + CameraPreviewView(session: cameraPreview.session) + .frame(height: 220) + .cornerRadius(16) + .overlay( + Group { + if !isCameraEnabled { + ZStack { + Color.black.opacity(2.0) + } + .cornerRadius(16) + } + } + ) + + VStack { + Spacer() + + // Bottom controls row + HStack { + // Microphone Button + Button(action: { + fetchAudioDevices() + showActionSheet.toggle() + }) { + Image(systemName: isMicEnabled ? "mic.fill" : "mic.slash.fill") + .foregroundColor(.white) + .padding(11) + .background(Circle().fill(Color.black.opacity(0.6))) + } + + Spacer() + if isCameraEnabled { + Button(action: { + isFrontCamera.toggle() + cameraPreview.flipCamera() + selectedCameraMode = (selectedCameraMode == .front) ? .back : .front + }) { + Image(systemName: "camera.rotate.fill") + .foregroundColor(.white) + .padding(11) + .background(Circle().fill(Color.black.opacity(0.6))) + } + } else { + Color.clear + .frame(width: 44, height: 44) + } + + Spacer() + + // Camera Toggle Button + Button(action: { + isCameraEnabled.toggle() + if isCameraEnabled { + cameraPreview.startSession() + } else { + cameraPreview.stopSession() + } + }) { + Image(systemName: isCameraEnabled ? "video.fill" : "video.slash.fill") + .foregroundColor(.white) + .padding(11) + .background(Circle().fill(Color.black.opacity(0.6))) + } + } + .padding(.horizontal) + .padding(.bottom, 5) + } + } + } } - , alignment: .trailing) - .padding() - .background( - RoundedRectangle(cornerRadius: 25) - .fill(Color.secondary.opacity(0.5)) - .shadow(color: Color.gray.opacity(0.10), radius: /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/)) - .padding(.leading) - .padding(.trailing) - - Text("Enter Meeting Id to join an existing meeting") - - TextField("Enter Your Name", text: $name) - .foregroundColor(Color.black) - .autocorrectionDisabled() - .font(.headline) - .overlay( - Image(systemName: "xmark.circle.fill") .padding() - .offset(x: 10) - .foregroundColor(Color.gray) - .opacity(name.isEmpty ? 0.0 : 1.0) - .onTapGesture { - UIApplication.shared.endEditing() - name = "" + } + .frame(width: 300, height: 250) // Set desired width here + .padding(.horizontal) + + // Input Fields + VStack(spacing: 16) { + // Meeting ID Field + CustomTextField( + text: $meetingId, + placeholder: "Enter Meeting ID", + systemImage: "number" + ) + + // Name Field + CustomTextField( + text: $name, + placeholder: "Enter Your Name", + systemImage: "person" + ) + } + .padding(.horizontal) + + // Action Buttons + VStack(spacing: 12) { + if !meetingId.isEmpty { + NavigationLink( + destination: MeetingView( + meetingViewController: MeetingViewController( + cameraMode: selectedCameraMode, + audioDevice: selectedAudioDevice + ), + meetingId: meetingId, + userName: name.isEmpty ? "Guest" : name + ) + .navigationBarBackButtonHidden(true), + isActive: $isNavigating + ) { + ActionButton(title: "Join Meeting", color: accentColor) + } } - , alignment: .trailing) - .padding() - .background( - RoundedRectangle(cornerRadius: 25) - .fill(Color.secondary.opacity(0.5)) - .shadow(color: Color.gray.opacity(0.10), radius: 10)) - .padding() - - if meetingId.isEmpty == false { - NavigationLink(destination:{ - if meetingId.isEmpty == false { - MeetingView(meetingId: self.meetingId, userName: name ?? "Guest") - .navigationBarBackButtonHidden(true) - } else { + + NavigationLink( + destination: MeetingView( + meetingViewController: MeetingViewController( + cameraMode: selectedCameraMode, + audioDevice: selectedAudioDevice + ), + userName: name.isEmpty ? "Guest" : name + ) + .navigationBarBackButtonHidden(true) + ) { + ActionButton(title: "Start Meeting", color: .indigo) } - }) { - Text("Join Meeting") - .foregroundColor(Color.white) - .padding() - .background( - RoundedRectangle(cornerRadius: 25.0) - .fill(Color.blue)) } + } + .padding(.horizontal) } + .padding(.bottom, 32) + } + } + .actionSheet(isPresented: $showActionSheet) { + ActionSheet( + title: Text("Select Audio Device"), + message: Text(selectedAudioDevice ?? "No device selected"), + buttons: buildDeviceButtons() + ) + } + .onAppear { + cameraPreview.checkPermissionsAndSetupSession() + isCameraEnabled = false + + + } + .onChange(of: isNavigating) { newValue in + if newValue { + cameraPreview.stopSession() - NavigationLink(destination: MeetingView(userName: name ?? "Guest") - .navigationBarBackButtonHidden(true)) { - Text("Start Meeting") - .foregroundColor(Color.white) - .padding() - .background( - RoundedRectangle(cornerRadius: 25.0) - .fill(Color.blue)) } } } - + .onDisappear { + cameraPreview.stopSession() } } + } + + private func fetchAudioDevices() { + audioDeviceList = VideoSDK.getAudioDevices() + } + private func buildDeviceButtons() -> [ActionSheet.Button] { + var buttons = audioDeviceList.map { device in + ActionSheet.Button.default( + Text("\(device)\(selectedAudioDevice == device ? " ✓" : "")") + ) { + selectedAudioDevice = device + } + } + buttons.append(.cancel(Text("Cancel"))) + return buttons + } +} #Preview { StartView(meetingId: "", name: "") } extension UIApplication { - func endEditing() { sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } diff --git a/VideoSDKSwiftUIExample/Screens/UIStruct/ActionButton.swift b/VideoSDKSwiftUIExample/Screens/UIStruct/ActionButton.swift new file mode 100644 index 0000000..ecf4cf6 --- /dev/null +++ b/VideoSDKSwiftUIExample/Screens/UIStruct/ActionButton.swift @@ -0,0 +1,29 @@ +// +// ActionButton.swift +// VideoSDKSwiftUIExample +// +// Created by Deep Bhupatkar on 20/12/24. +// + + +import SwiftUI +struct ActionButton: View { + let title: String + let color: Color + + var body: some View { + HStack { + Spacer() + Text(title) + .font(.system(size: 16, weight: .semibold)) + .foregroundColor(.white) + Spacer() + } + .padding() + .background( + RoundedRectangle(cornerRadius: 12) + .fill(color) + .shadow(color: color.opacity(0.3), radius: 5, x: 0, y: 2) + ) + } +} diff --git a/VideoSDKSwiftUIExample/Screens/UIStruct/CustomTextField.swift b/VideoSDKSwiftUIExample/Screens/UIStruct/CustomTextField.swift new file mode 100644 index 0000000..addd6d5 --- /dev/null +++ b/VideoSDKSwiftUIExample/Screens/UIStruct/CustomTextField.swift @@ -0,0 +1,39 @@ +// +// CustomTextField.swift +// VideoSDKSwiftUIExample +// +// Created by Deep Bhupatkar on 20/12/24. +// + + +import SwiftUI +struct CustomTextField: View { + @Binding var text: String + let placeholder: String + let systemImage: String + + var body: some View { + HStack { + Image(systemName: systemImage) + .foregroundColor(.gray) + .frame(width: 24) + + TextField(placeholder, text: $text) + .autocapitalization(.none) + .autocorrectionDisabled() + + if !text.isEmpty { + Button(action: { text = "" }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.gray) + } + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.white) + .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + ) + } +}