import Foundation import UIKit import linphonesw import AVFoundation class LinphoneBridge { private var core: Core! private var isRegistered = false private var currentCall: Call? private let activity: UIViewController private let remoteViewAcquisitor: () -> UIView? private let localViewAcquisitor: () -> UIView? private let onRegistrationStateChanged: (Int) -> Void private let onCallStateChanged: (Int) -> Void private var registrationState: RegistrationState = .None { didSet { print("registrationState delegate: oldValue: \(oldValue), newValue: \(registrationState)") onRegistrationStateChanged(registrationState.rawValue) } } private var callState: Call.State = .Idle { didSet { print("callState delegate: oldValue: \(oldValue), newValue: \(callState)") onCallStateChanged(callState.rawValue) } } private static let TAG = "LinphoneBridge" init( activity: UIViewController, remoteViewAcquisitor: @escaping () -> UIView?, localViewAcquisitor: @escaping () -> UIView?, onRegistrationStateChanged: @escaping (Int) -> Void, onCallStateChanged: @escaping (Int) -> Void ) { self.activity = activity self.remoteViewAcquisitor = remoteViewAcquisitor self.localViewAcquisitor = localViewAcquisitor self.onRegistrationStateChanged = onRegistrationStateChanged self.onCallStateChanged = onCallStateChanged } func checkPermissions() -> Bool { let audioStatus = AVCaptureDevice.authorizationStatus(for: .audio) let videoStatus = AVCaptureDevice.authorizationStatus(for: .video) if audioStatus != .authorized || videoStatus != .authorized { // Request permissions AVCaptureDevice.requestAccess(for: .audio) { _ in } AVCaptureDevice.requestAccess(for: .video) { _ in } return false } return true } func initializeLinphone() { do { let factory = Factory.Instance core = try factory.createCore(configPath: nil, factoryConfigPath: nil, systemContext: nil) // Add core listener core.addDelegate(delegate: self) // Enable video core.videoCaptureEnabled = true core.videoDisplayEnabled = true core.videoActivationPolicy?.automaticallyInitiate = true core.videoActivationPolicy?.automaticallyAccept = true // DSCP settings core.sipDscp = 26 // AF31 core.audioDscp = 46 // EF core.videoDscp = 36 // AF42 core.videoAdaptiveJittcompEnabled = true // Bandwidth settings core.uploadBandwidth = 512 core.downloadBandwidth = 1500 // Configure audio codecs let preferredAudio = ["opus", "pcmu", "pcma"] for pt in core.audioPayloadTypes { let mime = pt.mimeType.lowercased() let enabled = preferredAudio.contains(mime) let ok = pt.enable(enabled: enabled) == 0 print("\(LinphoneBridge.TAG) Change state of \(mime) to \(enabled) = \(ok)") } // Configure video codecs let preferredVideo = ["h264", "vp8"] for pt in core.videoPayloadTypes { let mime = pt.mimeType.lowercased() let enabled = preferredVideo.contains(mime) let ok = pt.enable(enabled: enabled) == 0 print("\(LinphoneBridge.TAG) Change state of \(mime) to \(enabled) = \(ok)") } // Set bitrates core.getPayloadType(type: "opus", rate: -1, channels: 0)?.normalBitrate = 32 core.getPayloadType(type: "h264", rate: -1, channels: 0)?.normalBitrate = 600 core.getPayloadType(type: "vp8", rate: -1, channels: 0)?.normalBitrate = 600 // Video settings let preferredVidDef = try factory.createVideoDefinition(width: 720, height: 1280) core.preferredVideoDefinition = preferredVidDef core.preferredFramerate = 30 try core.start() } catch { print("Error initializing Linphone: \(error)") } } func register(username: String, password: String, serverIp: String, serverPort: Int) { do { let factory = Factory.Instance let authInfo = try factory.createAuthInfo( username: username, userid: nil, passwd: password, ha1: nil, realm: nil, domain: serverIp ) core.addAuthInfo(info: authInfo) let identity = try factory.createAddress(addr: "sip:\(username)@\(serverIp)") let server = try factory.createAddress(addr: "sip:\(serverIp):\(serverPort)") let accountParams = try core.createAccountParams() try accountParams.setIdentityaddress(newValue: identity) try accountParams.setServeraddress(newValue: server) accountParams.registerEnabled = true let account = try core.createAccount(params: accountParams) try core.addAccount(account: account) core.defaultAccount = account } catch { print("Error registering: \(error)") } } func unregister() { core.clearAccounts() core.clearAllAuthInfo() } func makeCall(callTo: String, isVideoEnabled: Bool) throws { guard isRegistered else { throw NSError(domain: "LinphoneBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Not registered"]) } do { let factory = Factory.Instance guard let remoteAddress = try? factory.createAddress(addr: callTo) else { throw NSError(domain: "LinphoneBridge", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to create remote address"]) } guard let callParams = try? core.createCallParams(call: nil) else { throw NSError(domain: "LinphoneBridge", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to create call params"]) } callParams.videoEnabled = isVideoEnabled callParams.videoDirection = isVideoEnabled ? .SendRecv : .Inactive currentCall = try core.inviteAddressWithParams(addr: remoteAddress, params: callParams) } catch { throw error } } func answerCall() -> Bool { guard let call = currentCall else { return false } do { let callParams = try core.createCallParams(call: call) callParams.videoEnabled = true callParams.videoDirection = .SendRecv try call.acceptWithParams(params: callParams) return true } catch { print("Error answering call: \(error)") return false } } func hangupCall() -> Bool { do { try currentCall?.terminate() return true } catch { print("Error hanging up: \(error)") return false } } func toggleVideo() -> Bool { guard let call = currentCall else { return false } do { let params = try core.createCallParams(call: call) params.videoEnabled = !(call.currentParams?.videoEnabled ?? false) try call.update(params: params) return call.currentParams?.videoEnabled ?? false } catch { print("Error toggling video: \(error)") return false } } func toggleMicrophone() -> Bool { core.micEnabled = !core.micEnabled return core.micEnabled } func stop() { core.stop() } func inCall() -> Bool { return currentCall != nil } enum CallType { case audio case video case unknown } func callType() -> CallType { guard let params = currentCall?.currentParams else { return .unknown } if params.videoEnabled { return .video } else { return .audio } } } // MARK: - CoreDelegate extension LinphoneBridge: CoreDelegate { func onAccountRegistrationStateChanged(core: Core, account: Account, state: RegistrationState, message: String) { registrationState = state switch state { case .Ok: isRegistered = true case .Failed, .None, .Progress: isRegistered = false default: break } } func onCallStateChanged(core: Core, call: Call, state: Call.State, message: String) { callState = state switch state { case .IncomingReceived: currentCall = call print("onCallStateChanged: INCOMING_RECEIVED \(call)") case .Connected: print("onCallStateChanged: CONNECTED \(call)") if let remoteView = remoteViewAcquisitor() { call.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(remoteView).toOpaque()) } else { print("onCallStateChanged: CONNECTED remoteViewAcquisitor found nothing") } if let localView = localViewAcquisitor() { core.nativePreviewWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(localView).toOpaque()) } else { print("onCallStateChanged: CONNECTED localViewAcquisitor found nothing") } case .StreamsRunning: print("onCallStateChanged: STREAMS_RUNNING \(call)") if let remoteView = remoteViewAcquisitor() { call.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(remoteView).toOpaque()) } else { print("onCallStateChanged: STREAMS_RUNNING remoteViewAcquisitor found nothing") } if let localView = localViewAcquisitor() { core.nativePreviewWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(localView).toOpaque()) } else { print("onCallStateChanged: STREAMS_RUNNING localViewAcquisitor found nothing") } case .End, .Error, .Released: currentCall = nil default: break } } }