MediaEngine 使用手册 - housekeeper-software/tech GitHub Wiki
简介
MediaEngine包含SIP信令和媒体传输两个部分,二者之间完全独立,可以单独使用。 对于信令,可以替换为其他的模式,比如websocket/mqtt/tcp/udp等 媒体传输部分只识别SDP,信令不管哪种方式,但是与媒体部分交互要使用sdp协议。 媒体部分不包含H264编解码,需要外部实现,通过接口与底层交互。
包含文件
MediaEngine SDK包含文件有:
libvoip.so,libmediaengine.so
java文件:
org下面的所有包
信令
包名: org.mediaengine.sdk.signaling.sip.SipEndpoint
实现的是SIP协议。支持多个账号注册,也支持点对点SIP协议。
包含的能力:
1.注册
2.发起会话
3.接收会话
4.SIP INFO支持,底层只获取信息,具体内容要根据ContentType来解析,可用于传输DTMF或者请求关键帧。SIP INFO需要在通话时使用。
5.SIP Message支持,不依赖通话,随时可以使用。同样需要根据ContentType来判断消息类型。一般常用的是 text/plain,可用于聊天,还有更高级的 Message/CPIM
6.SIP PING,探测对方是否在线,200毫秒之内返回,在线返回200,否则返回404等
7.支持自定义SIP header字段,可用于特殊目的
媒体传输
MediaEngine类
1.初始化,这个放在程序启动完成之后,native将创建一个独立线程
有几个与通道无关的接口:
(1) setVideoEncoderConfig,设置视频编码参数,底层会在 onInitVideoEncoder返回此参数
(2) setAudioOptions,设置webrtc 音频处理参数,一般不需要设置,但是如果与默认值不同,则需要单独设置。比如包含硬件回音消除,则需要屏蔽软件消除
(3) setAudioVolume,设置音频播放软音量(0-100),这个与AudioManager无关。也可以不使用。直接用系统控制音量。
2.当需要发起呼叫或者接收呼叫时:
(1) createChannel,给一个不重复的id,唯一标识这个通道,可以用int递增即可。
(2) createOffer,创建offer,
(3) createAnswer
(3) onChannelInitCompleted回调中接收到我方的SDP,通过信令发送给对方即可
H264编解码
解码
EncodedVideoFrameListener.onEncodedVideoFrame(int channelId, EncodedVideoFrame frame)
EncodedVideoFrame 在native层创建,不要在这个回调中解码,底层只有一个工作线程,不能阻塞这个调用。
可以直接投递给解码线程。记得用完之后一定要调用 relase()方法。
EncodedVideoFrame其中的ByteBuffer内存是映射的native内存,不是java管理的,需要手工释放
每帧解码成功之后,需要调用 videoFrameDecoded,将frameId传给底层。其作用有两个:
1)当收到解码成功通知之后,底层不在发送请求关键帧的RTCP消息,这个为了本端尽快解码成功。
2)底层会根据是否解码成功,处理Nack请求。
当收到onInitVideoDecoder,启动解码器
onReleaseVideoDecoder,销毁解码器
编码
当收到:onInitVideoEncoder回调之后,启动编码线程
编码的数据通过onEncodedImage传给底层,并通过网络发送给对方
当收到:onReleaseVideoEncoder,关闭编码器即可
当收到:onVideoEncoderRequestKeyFrame(),尽快让编码器编码一帧关键帧,说明对方丢包,无法解码,需要关键帧。
NegoCallConfig说明
1.重要的是音视频的媒体格式要传给底层
2.媒体传输的端口范围,默认即可
3.enableDTMF:如果呼叫telephone,需要使能,其他场景我们应该用不到,能不开就不开,消耗性能
4.isP2pcall,是否是点对点呼叫,如果是的话,则需要设置 localAddress, peerAdress,local可以不设置,但至少要设置peeraddrss,这关乎到sdp
5.如果需要支持ICE协议,则初始化 IceConfig
H264 ProfileLevel
createH264()需要三个参数
1.打包模式,NonInterleaved即可
2.profile_level_id,主要根据编码器设置设置
具体可以看以下两个网页:
https://github.com/DrKLO/Telegram/blob/master/TMessagesProj/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
https://webrtc.googlesource.com/src/+/refs/heads/lkgr/api/video_codecs/h264_profile_level_id.cc
3.sprop_parameter_sets:一般不用设置,就是通过sdp将编码的sps和pps告诉对方,我们一般包含在rtp包关键帧里
室外机呼叫策略
服务器首先要定义呼叫序列,其中可能包含如下序列的一种或者多种:
(1)p2p呼叫
(2)server呼叫
(3)移动端呼叫
(4)移动电话呼叫
每个序列都包含一个delay的时间(毫秒单位),表示这个序列延迟多久才会真正工作。
比如,{p2p:0, server:1000,mobile:30000,telephone:60000},这个序列表示,p2p无需等待,服务器通道需等待1秒,移动端呼叫需等待30秒,电话呼叫需等待60秒。
不过,具体执行时会由调度逻辑来控制,前一个结束,下一个会立即执行,无需死等,从而加快呼叫进程。
以上(1),(2)是针对室内机的,可以同时提供p2p和server呼叫两个通道,也可以只配置一个,(3)和(4)可选,根据项目需求而定
如果要实现p2p呼叫,室外机和室内机需在同一个网段,如果是云端方案,则不要配置p2p序列。
室外机的处理逻辑:
1)针对每户(可能包含多个室内分机)的每个室内分机,将p2p和server两个通道组合成一个多通道的呼叫方案。具体的策略是:
按照序列的延时分别建立不同通道的呼叫,一般p2p的延迟最小,所以p2p会优先发生,如果p2p通道收到对方的 100 trying消息,则不在呼叫其他通道,即使已经呼叫也会立即终止。
如果p2p通道没有收到100 trying,而服务器通道已经收到180或者183,基本说明p2p通道不通,我们结束掉其他通道,专注与这个收到振铃消息的通道。
2)如果室内机终止会话,并且状态码是 Decline(603),则室外机呼叫结束。如果是200,或者 486(busy),则室外机继续尝试呼叫其他通道
3)所以,只有用户在屏幕上点击结束会话,才会发送 603,其他情况不可以回复603.
4)当某个序列提前结束,下一步延时最短的序列将被提前呼叫。
SIP信令中包含一些特殊信息用于处理重复呼叫
1.同一个室外机,每个通道上,sip账号可能不通,我们不能通过sip账号来判断是否是同一个来源,但是,display name是相同的,我们都使用房号标识。
2.我们在SIP invite的头域增加一个扩展: Session-Key:后面跟着一个guid,针对同一个设备的同一个批次呼叫,这个值是相同的。
Linphone SDK 需要注意以下几个细节
每个呼叫,需要知道对方的sip URI,并能解析出其中的各个部件,比如display name, scheme,host,port, tag等。
可以拿到 invite的头域信息,以便可以知道室外机定义的扩展头域字段。
Terminate一个会话,要可以提供status code,比如 486(busy),603(decline)
补充说明
信令层支持呼叫转移,reinvite,呼叫保持等功能,但媒体层没有支持那么多内容。
对ICE,只在接收端支持 RFC5245(中间有一次reinvite过程)---SipIncomingCall中有处理
呼叫转移需要java端自行实现。其方法是当发现呼叫转移时,新建一个会话,把原会话结束掉即可。
媒体层,不需要提供会话方向,因为如果有reinvite,会话方向会发生变化。代之以createOffer表示生成offer,createAnswer表示生成应答
不再使用restcomm的SDP解析,因为其遇到不认识的直接抛出异常,这个有问题。另外,它的代码已经是6年前的,有些新的属性不支持。
改成由底层解析成json格式。
SIP信令方法变更,移除ringing,pickup,全部以 sendCallAnswer替代。这样更加灵活。
将原先的SipConfig分解为 SipConfig ,SipProfile.
如果是 p2p,也需要SipProfile,只需要设置 identity即可。
sipConfig增加 gateway,如果让sip绑定到某个接口上。多网卡的时候有用。
restcom改了一个bug