diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt index 1319400..331fcb7 100644 --- a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt +++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt @@ -223,7 +223,7 @@ class LinphoneBridge( core.uploadBandwidth = 512 core.downloadBandwidth = 1500 - val preferredAudio = listOf("g729") // in order of preference + val preferredAudio = listOf("g729"/*, "opus", "speex", "pcmu", "pcma"*/) // in order of preference val preferredVideo = listOf("h264", "vp8") core.audioPayloadTypes.forEach { pt: PayloadType -> diff --git a/ios/Classes/LiblinphoneFlutterPlugin.swift b/ios/Classes/LiblinphoneFlutterPlugin.swift index bc4db2c..bb549b1 100644 --- a/ios/Classes/LiblinphoneFlutterPlugin.swift +++ b/ios/Classes/LiblinphoneFlutterPlugin.swift @@ -80,21 +80,35 @@ public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { case "checkPermissions": - result(linphoneBridge.checkPermissions()) + let hasPermissions = linphoneBridge.checkPermissions() + result(hasPermissions) case "initialize": - linphoneBridge = LinphoneBridge( - remoteViewAcquisitor: { [weak self] in self?.acquireRemoteView() }, - localViewAcquisitor: { [weak self] in self?.acquireLocalView() }, - onRegistrationStateChanged: { [weak self] state in - self?.registrationEventsChannel?(state) - }, - onCallStateChanged: { [weak self] state in - self?.callEventsChannel?(state) - } - ) - linphoneBridge.initializeLinphone() - result(true) + do { + linphoneBridge = LinphoneBridge( + remoteViewAcquisitor: { [weak self] in + return self?.acquireRemoteView() + }, + localViewAcquisitor: { [weak self] in + return self?.acquireLocalView() + }, + onRegistrationStateChanged: { [weak self] state in + self?.registrationEventsChannel?(state) + }, + onCallStateChanged: { [weak self] state in + self?.callEventsChannel?(state) + } + ) + linphoneBridge.initializeLinphone() + result(true) + } catch { + print("[\(LiblinphoneFlutterPlugin.TAG)] initialize error: \(error.localizedDescription)") + result(FlutterError( + code: "ERROR", + message: error.localizedDescription, + details: nil + )) + } case "register": guard let args = call.arguments as? [String: Any], @@ -102,11 +116,30 @@ public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin { let password = args["password"] as? String, let serverIp = args["serverIp"] as? String, let serverPort = args["serverPort"] as? Int else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing required arguments", details: nil)) + result(FlutterError( + code: "INVALID_ARGUMENTS", + message: "Missing required arguments", + details: nil + )) return } - linphoneBridge.register(username: username, password: password, serverIp: serverIp, serverPort: serverPort) - result(true) + + do { + linphoneBridge.register( + username: username, + password: password, + serverIp: serverIp, + serverPort: serverPort + ) + result(true) + } catch { + print("[\(LiblinphoneFlutterPlugin.TAG)] register error: \(error.localizedDescription)") + result(FlutterError( + code: "ERROR", + message: error.localizedDescription, + details: nil + )) + } case "unregister": linphoneBridge.unregister() @@ -116,114 +149,118 @@ public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin { guard let args = call.arguments as? [String: Any], let callTo = args["callTo"] as? String, let isVideoEnabled = args["isVideoEnabled"] as? Bool else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing required arguments", details: nil)) + result(FlutterError( + code: "INVALID_ARGUMENTS", + message: "Missing required arguments", + details: nil + )) return } + do { try linphoneBridge.makeCall(callTo: callTo, isVideoEnabled: isVideoEnabled) result(true) } catch { - result(FlutterError(code: "ERROR", message: error.localizedDescription, details: nil)) + print("[\(LiblinphoneFlutterPlugin.TAG)] makeCall error: \(error.localizedDescription)") + result(FlutterError( + code: "ERROR", + message: error.localizedDescription, + details: nil + )) } case "answerCall": - result(linphoneBridge.answerCall()) + let success = linphoneBridge.answerCall() + result(success) case "hangupCall": - result(linphoneBridge.hangupCall()) + let success = linphoneBridge.hangupCall() + result(success) case "inCall": - result(linphoneBridge.inCall()) + let inCall = linphoneBridge.inCall() + result(inCall) case "callType": - switch linphoneBridge.callType() { - case .audio: result(0) - case .video: result(1) - case .unknown: result(2) + let callType = linphoneBridge.callType() + let ordinal: Int + switch callType { + case .audio: + ordinal = 0 + case .video: + ordinal = 1 + case .unknown: + ordinal = 2 } + result(ordinal) case "toggleVideo": - result(linphoneBridge.toggleVideo()) + let enabled = linphoneBridge.toggleVideo() + result(enabled) case "toggleMicrophone": - result(linphoneBridge.toggleMicrophone()) + let enabled = linphoneBridge.toggleMicrophone() + result(enabled) case "stop": linphoneBridge.stop() result(true) - case "syncCurrentState": + case "syncCurrentState" : linphoneBridge.syncCurrentState() result(true) case "sendDtmf": guard let args = call.arguments as? [String: Any], let tone = args["tone"] as? String else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing required arguments", details: nil)) + result(FlutterError( + code: "INVALID_ARGUMENTS", + message: "Missing required arguments", + details: nil + )) return } - result(linphoneBridge.sendDtmf(tone: tone)) + + let success = linphoneBridge.sendDtmf(tone: tone) + result(success) case "stopCallService": - // Android-only; no-op on iOS result(true) case "setMicGain": guard let args = call.arguments as? [String: Any], - let levelStr = args["level"] as? String, - let level = Float(levelStr) else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing or invalid 'level'", details: nil)) + let level = args["level"] as? String else { + result(FlutterError( + code: "INVALID_ARGUMENTS", + message: "Missing required arguments", + details: nil + )) return } + linphoneBridge.setMicGain(level: level) - result(true) + result(success) case "getMicGain": - result(String(linphoneBridge.getMicGain())) + result(linphoneBridge.getMicGain()) case "setPlaybackGain": guard let args = call.arguments as? [String: Any], - let levelStr = args["level"] as? String, - let level = Float(levelStr) else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing or invalid 'level'", details: nil)) + let level = args["level"] as? String else { + result(FlutterError( + code: "INVALID_ARGUMENTS", + message: "Missing required arguments", + details: nil + )) return } + linphoneBridge.setPlaybackGain(level: level) - result(true) + result(success) case "getPlaybackGain": result(linphoneBridge.getPlaybackGain()) - case "setDscp": - guard let args = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing arguments", details: nil)) - return - } - // All three are optional — only present keys are applied - let sipDscp = args["sipDscp"] as? Int - let audioDscp = args["audioDscp"] as? Int - let videoDscp = args["videoDscp"] as? Int - linphoneBridge.setDscp(sipDscp: sipDscp, audioDscp: audioDscp, videoDscp: videoDscp) - result(true) - - case "getDscp": - result(linphoneBridge.getDscp()) - - case "getCurrentCallStats": - result(linphoneBridge.getCurrentCallStats()) - - case "getAvailableAudioCodecs": - result(linphoneBridge.getAvailableAudioCodecs()) - - case "setAudioCodec": - guard let args = call.arguments as? [String: Any], - let mime = args["mime"] as? String, - let clockRate = args["clockRate"] as? Int else { - result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing required arguments", details: nil)) - return - } - result(linphoneBridge.setAudioCodec(mime: mime, clockRate: clockRate)) - default: result(FlutterMethodNotImplemented) } diff --git a/ios/Classes/LinphoneBridge.swift b/ios/Classes/LinphoneBridge.swift index 157ca19..f1d61a4 100644 --- a/ios/Classes/LinphoneBridge.swift +++ b/ios/Classes/LinphoneBridge.swift @@ -45,6 +45,7 @@ class LinphoneBridge { 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 @@ -57,11 +58,14 @@ class LinphoneBridge { 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 + core.config?.setInt(section: "net", key: "ipv6", value: 0) + try? core.config?.sync() + // Add core listener + core.addDelegate(delegate: self) + // Enable video core.videoCaptureEnabled = true core.videoDisplayEnabled = true @@ -80,7 +84,7 @@ class LinphoneBridge { core.downloadBandwidth = 1500 // Configure audio codecs - let preferredAudio = ["g729"] + let preferredAudio = ["opus", "pcmu", "pcma"] for pt in core.audioPayloadTypes { let mime = pt.mimeType.lowercased() let enabled = preferredAudio.contains(mime) @@ -98,9 +102,9 @@ class LinphoneBridge { } // 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 + 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) @@ -152,23 +156,29 @@ class LinphoneBridge { 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"]) + 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 } - - 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 } + guard let call = currentCall else { + return false + } do { let callParams = try core.createCallParams(call: call) @@ -193,7 +203,9 @@ class LinphoneBridge { } func toggleVideo() -> Bool { - guard let call = currentCall else { return false } + guard let call = currentCall else { + return false + } do { let params = try core.createCallParams(call: call) @@ -226,12 +238,25 @@ class LinphoneBridge { } func callType() -> CallType { - guard let params = currentCall?.currentParams else { return .unknown } - return params.videoEnabled ? .video : .audio + guard let params = currentCall?.currentParams else { + return .unknown + } + + if params.videoEnabled { + return .video + } else { + return .audio + } } func sendDtmf(tone: String) -> Bool { - guard let call = currentCall, !tone.isEmpty else { return false } + guard let call = currentCall else { + return false + } + + guard !tone.isEmpty else { + return false + } let dtmfChar = tone.first! do { @@ -248,8 +273,6 @@ class LinphoneBridge { onCallStateChanged(callState.rawValue) } - // MARK: - Gain - func setMicGain(level: Float) { core.micGainDb = level } @@ -265,111 +288,6 @@ class LinphoneBridge { 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