import Foundation import UIKit import linphonesw import AVFoundation class LinphoneBridge { private var core: Core! private var isRegistered = false private var currentCall: Call? 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( remoteViewAcquisitor: @escaping () -> UIView?, localViewAcquisitor: @escaping () -> UIView?, onRegistrationStateChanged: @escaping (Int) -> Void, onCallStateChanged: @escaping (Int) -> Void ) { 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 { 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) core.addDelegate(delegate: self) core.ipv6Enabled = false core.agcEnabled = false // 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 = ["g729"] 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"]) } 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) } 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 } return params.videoEnabled ? .video : .audio } func sendDtmf(tone: String) -> Bool { guard let call = currentCall, !tone.isEmpty else { return false } let dtmfChar = tone.first! do { try call.sendDtmf(dtmf: CChar(dtmfChar.asciiValue!)) return true } catch { print("Error sending DTMF: \(error)") return false } } func syncCurrentState() { onRegistrationStateChanged(registrationState.rawValue) onCallStateChanged(callState.rawValue) } // MARK: - Gain func setMicGain(level: Float) { core.micGainDb = level } func getMicGain() -> Float { return core.micGainDb } func setPlaybackGain(level: Float) { core.playbackGainDb = level } func getPlaybackGain() -> Float { return core.playbackGainDb } // MARK: - DSCP func setDscp(sipDscp: Int?, audioDscp: Int?, videoDscp: Int?) { if let sip = sipDscp { core.sipDscp = sip } if let audio = audioDscp { core.audioDscp = audio } if let video = videoDscp { core.videoDscp = video } } func getDscp() -> [String: Any] { return [ "sipDscp": core.sipDscp, "audioDscp": core.audioDscp, "videoDscp": core.videoDscp, ] } // MARK: - Call Stats func getCurrentCallStats() -> [String: Any]? { guard let call = core.currentCall, let stats = call.getStats(type: .Audio) else { return nil } return [ "recordTs": Int64(Date().timeIntervalSince1970 * 1000), "remoteAddress": call.remoteAddress?.asString() ?? "", "currentQuality": call.currentQuality, "downloadBandwidth": stats.downloadBandwidth, "estimatedDownloadBandwidth": stats.estimatedDownloadBandwidth, "fecCumulativeLostPacketsNumber": stats.fecCumulativeLostPacketsNumber, "fecDownloadBandwidth": stats.fecDownloadBandwidth, "fecRepairedPacketsNumber": stats.fecRepairedPacketsNumber, "fecUploadBandwidth": stats.fecUploadBandwidth, "iceState": stats.iceState.rawValue, "jitterBufferSizeMs": stats.jitterBufferSizeMs, "latePacketsCumulativeNumber": stats.latePacketsCumulativeNumber, "localLateRate": stats.localLateRate, "localLossRate": stats.localLossRate, "receiverInterarrivalJitter": stats.receiverInterarrivalJitter, "receiverLossRate": stats.receiverLossRate, "roundTripDelay": stats.roundTripDelay, "rtcpDownloadBandwidth": stats.rtcpDownloadBandwidth, "rtcpUploadBandwidth": stats.rtcpUploadBandwidth, "rtpCumPacketLoss": stats.rtpCumPacketLoss, "rtpDiscarded": stats.rtpDiscarded, "rtpHwRecv": stats.rtpHwRecv, "rtpPacketRecv": stats.rtpPacketRecv, "rtpPacketSent": stats.rtpPacketSent, "rtpRecv": stats.rtpRecv, "rtpSent": stats.rtpSent, "senderInterarrivalJitter": stats.senderInterarrivalJitter, "senderLossRate": stats.senderLossRate, "srtpSource": stats.srtpSource.rawValue, "srtpSuite": stats.srtpSuite.rawValue, "uploadBandwidth": stats.uploadBandwidth, "upnpState": stats.upnpState.rawValue, "zrtpAuthTagAlgo": stats.zrtpAuthTagAlgo, "zrtpCipherAlgo": stats.zrtpCipherAlgo, "zrtpHashAlgo": stats.zrtpHashAlgo, "zrtpKeyAgreementAlgo": stats.zrtpKeyAgreementAlgo, "zrtpSasAlgo": stats.zrtpSasAlgo, "isZrtpKeyAgreementAlgoPostQuantum": stats.isZrtpKeyAgreementAlgoPostQuantum, ] } // MARK: - Codec management func getAvailableAudioCodecs() -> [[String: Any]] { return core.audioPayloadTypes.map { pt in [ "mimeType": pt.mimeType.lowercased(), "clockRate": pt.clockRate, "enabled": pt.enabled(), ] } } /// Enables the codec matching (mime, clockRate) and disables all others. /// Returns true if the target codec was found and enabled successfully. func setAudioCodec(mime: String, clockRate: Int) -> Bool { var found = false var ok = false for pt in core.audioPayloadTypes { let ptMime = pt.mimeType.lowercased() let ptClockRate = pt.clockRate let enable = ptMime == mime && ptClockRate == clockRate if enable { ok = pt.enable(enabled: true) == 0 found = true print("\(LinphoneBridge.TAG) setAudioCodec: preferred codec found (\(ptMime) @ \(ptClockRate)) switch: \(ok)") } else { let switchedOk = pt.enable(enabled: false) == 0 print("\(LinphoneBridge.TAG) setAudioCodec: \(ptMime) @ \(ptClockRate) switch: \(switchedOk)") } } if !found { print("\(LinphoneBridge.TAG) setAudioCodec: could not find codec with params \(mime) @ \(clockRate)") } return ok } } // 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 } } }