435 lines
15 KiB
Swift
435 lines
15 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|