Compare commits

..

No commits in common. "a02b76c360744bc0b3f5bfbf5697eba7b4adc013" and "5dde87dc133911acae6bbe9fd493799cc3a19a8f" have entirely different histories.

3 changed files with 156 additions and 201 deletions

View file

@ -223,7 +223,7 @@ class LinphoneBridge(
core.uploadBandwidth = 512 core.uploadBandwidth = 512
core.downloadBandwidth = 1500 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") val preferredVideo = listOf("h264", "vp8")
core.audioPayloadTypes.forEach { pt: PayloadType -> core.audioPayloadTypes.forEach { pt: PayloadType ->

View file

@ -80,12 +80,18 @@ public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method { switch call.method {
case "checkPermissions": case "checkPermissions":
result(linphoneBridge.checkPermissions()) let hasPermissions = linphoneBridge.checkPermissions()
result(hasPermissions)
case "initialize": case "initialize":
do {
linphoneBridge = LinphoneBridge( linphoneBridge = LinphoneBridge(
remoteViewAcquisitor: { [weak self] in self?.acquireRemoteView() }, remoteViewAcquisitor: { [weak self] in
localViewAcquisitor: { [weak self] in self?.acquireLocalView() }, return self?.acquireRemoteView()
},
localViewAcquisitor: { [weak self] in
return self?.acquireLocalView()
},
onRegistrationStateChanged: { [weak self] state in onRegistrationStateChanged: { [weak self] state in
self?.registrationEventsChannel?(state) self?.registrationEventsChannel?(state)
}, },
@ -95,6 +101,14 @@ public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
) )
linphoneBridge.initializeLinphone() linphoneBridge.initializeLinphone()
result(true) result(true)
} catch {
print("[\(LiblinphoneFlutterPlugin.TAG)] initialize error: \(error.localizedDescription)")
result(FlutterError(
code: "ERROR",
message: error.localizedDescription,
details: nil
))
}
case "register": case "register":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
@ -102,11 +116,30 @@ public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
let password = args["password"] as? String, let password = args["password"] as? String,
let serverIp = args["serverIp"] as? String, let serverIp = args["serverIp"] as? String,
let serverPort = args["serverPort"] as? Int else { 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 return
} }
linphoneBridge.register(username: username, password: password, serverIp: serverIp, serverPort: serverPort)
do {
linphoneBridge.register(
username: username,
password: password,
serverIp: serverIp,
serverPort: serverPort
)
result(true) result(true)
} catch {
print("[\(LiblinphoneFlutterPlugin.TAG)] register error: \(error.localizedDescription)")
result(FlutterError(
code: "ERROR",
message: error.localizedDescription,
details: nil
))
}
case "unregister": case "unregister":
linphoneBridge.unregister() linphoneBridge.unregister()
@ -116,37 +149,58 @@ public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let callTo = args["callTo"] as? String, let callTo = args["callTo"] as? String,
let isVideoEnabled = args["isVideoEnabled"] as? Bool else { 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 return
} }
do { do {
try linphoneBridge.makeCall(callTo: callTo, isVideoEnabled: isVideoEnabled) try linphoneBridge.makeCall(callTo: callTo, isVideoEnabled: isVideoEnabled)
result(true) result(true)
} catch { } 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": case "answerCall":
result(linphoneBridge.answerCall()) let success = linphoneBridge.answerCall()
result(success)
case "hangupCall": case "hangupCall":
result(linphoneBridge.hangupCall()) let success = linphoneBridge.hangupCall()
result(success)
case "inCall": case "inCall":
result(linphoneBridge.inCall()) let inCall = linphoneBridge.inCall()
result(inCall)
case "callType": case "callType":
switch linphoneBridge.callType() { let callType = linphoneBridge.callType()
case .audio: result(0) let ordinal: Int
case .video: result(1) switch callType {
case .unknown: result(2) case .audio:
ordinal = 0
case .video:
ordinal = 1
case .unknown:
ordinal = 2
} }
result(ordinal)
case "toggleVideo": case "toggleVideo":
result(linphoneBridge.toggleVideo()) let enabled = linphoneBridge.toggleVideo()
result(enabled)
case "toggleMicrophone": case "toggleMicrophone":
result(linphoneBridge.toggleMicrophone()) let enabled = linphoneBridge.toggleMicrophone()
result(enabled)
case "stop": case "stop":
linphoneBridge.stop() linphoneBridge.stop()
@ -159,71 +213,54 @@ public class LiblinphoneFlutterPlugin: NSObject, FlutterPlugin {
case "sendDtmf": case "sendDtmf":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let tone = args["tone"] as? String else { 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 return
} }
result(linphoneBridge.sendDtmf(tone: tone))
let success = linphoneBridge.sendDtmf(tone: tone)
result(success)
case "stopCallService": case "stopCallService":
// Android-only; no-op on iOS
result(true) result(true)
case "setMicGain": case "setMicGain":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let levelStr = args["level"] as? String, let level = args["level"] as? String else {
let level = Float(levelStr) else { result(FlutterError(
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing or invalid 'level'", details: nil)) code: "INVALID_ARGUMENTS",
message: "Missing required arguments",
details: nil
))
return return
} }
linphoneBridge.setMicGain(level: level) linphoneBridge.setMicGain(level: level)
result(true) result(success)
case "getMicGain": case "getMicGain":
result(String(linphoneBridge.getMicGain())) result(linphoneBridge.getMicGain())
case "setPlaybackGain": case "setPlaybackGain":
guard let args = call.arguments as? [String: Any], guard let args = call.arguments as? [String: Any],
let levelStr = args["level"] as? String, let level = args["level"] as? String else {
let level = Float(levelStr) else { result(FlutterError(
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing or invalid 'level'", details: nil)) code: "INVALID_ARGUMENTS",
message: "Missing required arguments",
details: nil
))
return return
} }
linphoneBridge.setPlaybackGain(level: level) linphoneBridge.setPlaybackGain(level: level)
result(true) result(success)
case "getPlaybackGain": case "getPlaybackGain":
result(linphoneBridge.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: default:
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
} }

View file

@ -45,6 +45,7 @@ class LinphoneBridge {
let videoStatus = AVCaptureDevice.authorizationStatus(for: .video) let videoStatus = AVCaptureDevice.authorizationStatus(for: .video)
if audioStatus != .authorized || videoStatus != .authorized { if audioStatus != .authorized || videoStatus != .authorized {
// Request permissions
AVCaptureDevice.requestAccess(for: .audio) { _ in } AVCaptureDevice.requestAccess(for: .audio) { _ in }
AVCaptureDevice.requestAccess(for: .video) { _ in } AVCaptureDevice.requestAccess(for: .video) { _ in }
return false return false
@ -57,10 +58,13 @@ class LinphoneBridge {
do { do {
let factory = Factory.Instance let factory = Factory.Instance
core = try factory.createCore(configPath: nil, factoryConfigPath: nil, systemContext: nil) core = try factory.createCore(configPath: nil, factoryConfigPath: nil, systemContext: nil)
core.addDelegate(delegate: self)
core.ipv6Enabled = false 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 // Enable video
core.videoCaptureEnabled = true core.videoCaptureEnabled = true
@ -80,7 +84,7 @@ class LinphoneBridge {
core.downloadBandwidth = 1500 core.downloadBandwidth = 1500
// Configure audio codecs // Configure audio codecs
let preferredAudio = ["g729"] let preferredAudio = ["opus", "pcmu", "pcma"]
for pt in core.audioPayloadTypes { for pt in core.audioPayloadTypes {
let mime = pt.mimeType.lowercased() let mime = pt.mimeType.lowercased()
let enabled = preferredAudio.contains(mime) let enabled = preferredAudio.contains(mime)
@ -98,9 +102,9 @@ class LinphoneBridge {
} }
// Set bitrates // Set bitrates
// core.getPayloadType(type: "opus", rate: -1, channels: 0)?.normalBitrate = 32 core.getPayloadType(type: "opus", rate: -1, channels: 0)?.normalBitrate = 32
// core.getPayloadType(type: "h264", rate: -1, channels: 0)?.normalBitrate = 600 core.getPayloadType(type: "h264", rate: -1, channels: 0)?.normalBitrate = 600
// core.getPayloadType(type: "vp8", rate: -1, channels: 0)?.normalBitrate = 600 core.getPayloadType(type: "vp8", rate: -1, channels: 0)?.normalBitrate = 600
// Video settings // Video settings
let preferredVidDef = try factory.createVideoDefinition(width: 720, height: 1280) let preferredVidDef = try factory.createVideoDefinition(width: 720, height: 1280)
@ -152,6 +156,7 @@ class LinphoneBridge {
throw NSError(domain: "LinphoneBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Not registered"]) throw NSError(domain: "LinphoneBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Not registered"])
} }
do {
let factory = Factory.Instance let factory = Factory.Instance
guard let remoteAddress = try? factory.createAddress(addr: callTo) else { guard let remoteAddress = try? factory.createAddress(addr: callTo) else {
throw NSError(domain: "LinphoneBridge", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to create remote address"]) throw NSError(domain: "LinphoneBridge", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to create remote address"])
@ -165,10 +170,15 @@ class LinphoneBridge {
callParams.videoDirection = isVideoEnabled ? .SendRecv : .Inactive callParams.videoDirection = isVideoEnabled ? .SendRecv : .Inactive
currentCall = try core.inviteAddressWithParams(addr: remoteAddress, params: callParams) currentCall = try core.inviteAddressWithParams(addr: remoteAddress, params: callParams)
} catch {
throw error
}
} }
func answerCall() -> Bool { func answerCall() -> Bool {
guard let call = currentCall else { return false } guard let call = currentCall else {
return false
}
do { do {
let callParams = try core.createCallParams(call: call) let callParams = try core.createCallParams(call: call)
@ -193,7 +203,9 @@ class LinphoneBridge {
} }
func toggleVideo() -> Bool { func toggleVideo() -> Bool {
guard let call = currentCall else { return false } guard let call = currentCall else {
return false
}
do { do {
let params = try core.createCallParams(call: call) let params = try core.createCallParams(call: call)
@ -226,12 +238,25 @@ class LinphoneBridge {
} }
func callType() -> CallType { func callType() -> CallType {
guard let params = currentCall?.currentParams else { return .unknown } guard let params = currentCall?.currentParams else {
return params.videoEnabled ? .video : .audio return .unknown
}
if params.videoEnabled {
return .video
} else {
return .audio
}
} }
func sendDtmf(tone: String) -> Bool { 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! let dtmfChar = tone.first!
do { do {
@ -248,8 +273,6 @@ class LinphoneBridge {
onCallStateChanged(callState.rawValue) onCallStateChanged(callState.rawValue)
} }
// MARK: - Gain
func setMicGain(level: Float) { func setMicGain(level: Float) {
core.micGainDb = level core.micGainDb = level
} }
@ -265,111 +288,6 @@ class LinphoneBridge {
func getPlaybackGain() -> Float { func getPlaybackGain() -> Float {
return core.playbackGainDb 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 // MARK: - CoreDelegate