Linphone 作为最常用的 SIP 音视频客户端,因其开源免费的特性,可方便的移植到移动端Android和IOS平台。国内很多人基于他继续二次开发。已成为行业事实标准。
我们可以基于 Linphone 完成很多功能,比如
设置音频编码类型 G729 PCMU PCMA 比特率限制 语音处理 低音增强 回音消除 麦克风扬声器响铃设备选择
设置视频编码 VP8 H264 H265 摄像头切换 默认通话方式 自动接听视频 分辨率尺寸 帧率 比特率 配置视频参数
以及速率控制 速度限制
支持 DTMF RFC2833 SIP INFO
支持 SIP注册多个账户
协议 UDP TCP TLS
可指定 SIP端口 RTP端口
SIP 客户端基本功能实现
注册 注销 呼入 呼出 保持 取消保持 录制 切换摄像头 关闭摄像头 切换音频 DTMF 通话质量监控 SIP MESSAGE
官方资料已经比较全了,推荐直接查阅。这里介绍的是 Linphone 安卓的 SDK 的一些常用接口。
一、官方资料
Java API文档
download.linphone.org/releases/do…
Maven仓库地址
download.linphone.org/releases/
集成说明
wiki.linphone.org/xwiki/wiki/…
示例代码
gitlab.linphone.org/BC/public/t…
二、使用流程
- 创建一个Core,通过Core对软电话进行设置,注册账号
- 注册回调,用于响应通话状态变化以及注册状态变化
三、主要类
Core
Core是SDK的主要对象。没有它你什么都做不了。为了创建核心,我们需要工厂的实例。
val factory = Factory.instance()
factory.setDebugMode(true, "Hello Linphone") // 启用调试日志
val core = factory.createCore(null, null, this) // 前两个参数是配置文件路径 最后一个参数传android Context
// do something
主要方法
version
clearProxyConfig() // 从配置中删除所有代理
getCallsNb() // 当前通话次数
addListener(CoreListener listener) // 添加监听器
void removeListener(CoreListener listener) // 移除监听器
start() // 在实例化之后启动一个 Core 对象
stop() // 在实例化并启动 Core 对象后停止该对象
clearProxyConfig() // 从配置中删除所有代理
createAccountCreator(String xmlrpcUrl) // 创建一个 AccountCreator 并设置 Linphone 请求回调
clearAllAuthInfo() // 清除所有身份验证信息
interpretUrl(String url) // 给定的字符串构造一个 Address。过时,请改用interpretUrl(String url, boolean applyInternationalPrefix)
createCallParams(Call call) // 创建呼叫参数 CallParams
enableVideoCapture(true) // 启用或禁用视频捕获
enableVideoDisplay(true) // 启用或禁用视频显示
inviteAddressWithParams(Address addr, CallParams params) // 发起呼叫
inviteAddress(Address addr) // 发起拨出呼叫
micEnabled // 麦克风是否已启用
getOutputAudioDevice() // 获取当前通话的输出音频设备
getCalls() // 获取当前的通话列表Call[]
getCurrentCall() // 获取当前的通话
nativeVideoWindowId
nativePreviewWindowId
videoDevice
videoDevicesList
// 两个TextureViews,一个用于预览,一个用于远端视频
nativeVideoWindowId // 远端视频
nativePreviewWindowId // 本地预览 CaptureTextureView可以获取视频比例
// 启用视频(非自动)
enableVideoCapture(true)
enableVideoDisplay(true)
videoActivationPolicy.automaticallyAccept // 自动配置创建的启用视频的呼叫参数
// 要求 CaptureTextureView 调整大小以匹配捕获视频的大小比例
core.config.setBool("video", "auto_resize_preview_to_keep_ratio", true)
CoreListener
监听注册的回调
// 注册回调 正常情况下状态为 Progress -> OK
onAccountRegistrationStateChanged(Core core, Account account, RegistrationState state, String message);
// 音频设备变更成功时会触发该回调
onAudioDeviceChanged(core: Core, audioDevice: AudioDevice);
// 当可用设备列表发生变化时,例如蓝牙耳机连接断开后,将触发此回调。
onAudioDevicesListUpdated(core: Core);
// 通话状态变更
onCallStateChanged(
core: Core,
call: Call,
state: Call.State?,
message: String
);
// 然后注册到core
core.addListener(coreListener)
RegistrationState
注册状态
None // 注册的初始状态
Progress // 注册正在进行中
Ok // 注册成功
Cleared // 注销成功
Failed // 注册失败
Refreshing // 注册刷新
Call.State
通话状态
Connected // 已连接 呼出4 呼出收到200
EarlyUpdatedByRemote // SIP UPDATE in early dialog received
EarlyUpdating // SIP UPDATE in early dialog sent
End // 通话结束
Error // 错误
Idle // 空闲状态
IncomingEarlyMedia // 发送 183
IncomingReceived // 接听
OutgoingEarlyMedia // 收到 183
OutgoingInit // 呼出初始化 呼出1
OutgoingProgress // 正在呼出 呼出2
OutgoingRinging // 呼出振铃 呼出收到180 呼出3
Paused // 发起暂停
PausedByRemote // 对方暂停
Pausing // 暂停中
PushIncomingReceived // 推送接听
Referred // Referred
Released // 呼出6 通话释放后
Resuming // Resuming
StreamsRunning // 呼出5 呼叫处于活动状态。通话中可能会多次达到此状态,例如在暂停恢复后或ICE协商完成后等待呼叫连接,然后允许呼叫更新
UpdatedByRemote // 当远程请求视频时,调用的参数会更新。
Updating // 当我们请求通话更新时,例如切换视频时
// 来电信息获取
call.remoteAddress.asStringUriOnly()
// 终止通话
core.currentCall?.terminate()
// 接听
core.currentCall?.accept()
// 打开或关闭麦克风
core.enableMic(!core.micEnabled())
// 切换声音输出示例
toggleSpeaker() {
// 获取当前使用的音频设备
val currentAudioDevice = core.currentCall?.outputAudioDevice
val speakerEnabled = currentAudioDevice?.type == AudioDevice.Type.Speaker
// 所有可用的音频设备的列表
// 一些设备可能没有听筒设备
for (audioDevice in core.audioDevices) {
if (speakerEnabled && audioDevice.type == AudioDevice.Type.Earpiece) { // 听筒
core.currentCall?.outputAudioDevice = audioDevice
return
} else if (!speakerEnabled && audioDevice.type == AudioDevice.Type.Speaker) { // 扬声器
core.currentCall?.outputAudioDevice = audioDevice
return
}/* 蓝牙
else if (audioDevice.type == AudioDevice.Type.Bluetooth) {
core.currentCall?.outputAudioDevice = audioDevice
}*/
}
}
// video大于2时启用视频切换,系统自带一个静态图片的假视频
findViewById<Button>(R.id.toggle_camera).isEnabled = core.videoDevicesList.size > 2 && call.currentParams.videoEnabled()
Account
要配置 SIP 帐户,我们需要一个 Account 对象和一个 AuthInfo 对象
Account是如何连接到代理服务器,AuthInfo是存储凭据
// arg1 用户名
// arg2 鉴权ID(默认使用null,和用户名相同)
// arg3 密码
val authInfo = Factory.instance().createAuthInfo(username, null, password, null, null, domain, null)
// AccountParams替换之前的ProxyConfig
val accountParams = core.createAccountParams()
val identity = Factory.instance().createAddress("sip:$username@$domain") // 身份地址
accountParams.identityAddress = identity
val address = Factory.instance().createAddress("sip:$domain") // 代理服务器
address?.transport = transportType // 传输协议
accountParams.serverAddress = address
accountParams.isRegisterEnabled = true
val account = core.createAccount(accountParams)
core.addAuthInfo(authInfo)
core.addAccount(account)
core.defaultAccount = account
core.addListener(coreListener) // 注册回调
// account也可以注册回调
account.addListener { _, state, message ->
// There is a Log helper in org.linphone.core.tools package
Log.i("[Account] Registration state changed: $state, $message")
}
core.start() // 启动才能注册
// 获取权限
if (packageManager.checkPermission(Manifest.permission.RECORD_AUDIO, packageName) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 0)
return
}
// 摄像头权限
if (packageManager.checkPermission(Manifest.permission.CAMERA, packageName) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.CAMERA), 0)
return
}
取消注册
private fun unregister() {
// Here we will disable the registration of our Account
val account = core.defaultAccount
account ?: return
val params = account.params
// Returned params object is const, so to make changes we first need to clone it
val clonedParams = params.clone()
// Now let's make our changes
clonedParams.registerEnabled = false
// And apply them
account.params = clonedParams
}
删除账号
private fun delete() {
// To completely remove an Account
val account = core.defaultAccount
account ?: return
core.removeAccount(account)
// To remove all accounts use
core.clearAccounts()
// Same for auth info
core.clearAllAuthInfo()
}
GlobalState
Core状态
Configuring // 如果存在远程预配 URI,则“启动”和“打开”之间的暂时性状态configured.配置
Off // 我们在 Core.stop() 之后所处的状态。
On // 指示 Core 已启动并已启动并正在运行。
Ready // 由linphone_factory_create_core创建Core状态,通常随后会调用Core.start()
Shutdown // 调用 Core.stop() 时的瞬态状态
Startup // 调用 Core.start() 时的瞬态状态
Call
我这里关注多路通话,查阅文档时,官方文档表示 Linphone 在任何给定时间最多只允许一个活动呼叫,并且状态为StreamsRunning。
This object represents a call issued or received by the Core.
Linphone only allows at most one active call at any given time and it will be in Call.State.StreamsRunning. However, if the core is locally hosting a Conference, you may have some or all the calls in the conference in Call.State.StreamsRunning as well as an additional active call outside of the conference in Call.State.StreamsRunning if the local participant of the Conference is not part of it. You can get the Call.State of the call using getState(), it's current CallParams with getCurrentParams() and the latest statistics by calling getAudioStats() or getVideoStats().
常用方法
acceptWithParams(CallParams params) // 接听
decline // 拒接一通待接电话,并说明原因。
terminate // 结束通话
setOutputAudioDevice(AudioDevice audioDevice) // 将给定的 AudioDevice 设置为仅此调用的输出
示例代码
private fun toggleVideo() {
if (core.callsNb == 0) return
val call = if (core.currentCall != null) core.currentCall else core.calls[0]
call ?: return
// We will need the CAMERA permission for video call
if (packageManager.checkPermission(Manifest.permission.CAMERA, packageName) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.CAMERA), 0)
return
}
// To update the call, we need to create a new call params, from the call object this time
val params = core.createCallParams(call)
// Here we toggle the video state (disable it if enabled, enable it if disabled)
// Note that we are using currentParams and not params or remoteParams
// params is the object you configured when the call was started
// remote params is the same but for the remote
// current params is the real params of the call, resulting of the mix of local & remote params
params?.enableVideo(!call.currentParams.videoEnabled())
// Finally we request the call update
call.update(params)
// Note that when toggling off the video, TextureViews will keep showing the latest frame displayed
}
private fun toggleCamera() {
// Currently used camera
val currentDevice = core.videoDevice
// Let's iterate over all camera available and choose another one
for (camera in core.videoDevicesList) {
// All devices will have a "Static picture" fake camera, and we don't want to use it
if (camera != currentDevice && camera != "StaticImage: Static picture") {
core.videoDevice = camera
break
}
}
}
private fun pauseOrResume() {
if (core.callsNb == 0) return
val call = if (core.currentCall != null) core.currentCall else core.calls[0]
call ?: return
if (call.state != Call.State.Paused && call.state != Call.State.Pausing) {
// If our call isn't paused, let's pause it
call.pause()
} else if (call.state != Call.State.Resuming) {
// Otherwise let's resume it
call.resume()
}
}
CallParams
包含Call的各种参数的对象。可以在接听来电时指定参数,或在发起拨出呼叫时。
enableLowBandwidth(true) // 开启低带宽模式 setLowBandwidthEnabled
enableVideo(true) // 启用视频流,直接开始视频通话
mediaEncryption // 加密方式 不加密MediaEncryption.None