开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情
上一篇(Android:WebRtc实现多人音视频(上))中我们了解了基于WebRtc音视频的基础流程,这篇我们将用代码来进行详细的实战说明。
在app模块build.gradle中添加依赖:
implementation "org.webrtc:google-webrtc:1.0+"
Socket的使用相信大家都会,只是做一个信令转发这里就不作陈述,因为会涉及多人音视频,所以我们在最初的设计时就要考虑这点,避免之后重构的麻烦。首先初始化工厂,用于创建 PeerConnection
private fun createConnectionFactory(): PeerConnectionFactory {
// 1. 初始化的方法,必须在开始之前调用
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions
.builder(mContext)
.createInitializationOptions()
)
// 2. 设置编解码方式:默认方法
val encoderFactory = DefaultVideoEncoderFactory(mRootEglBase!!.eglBaseContext,true,true)
val decoderFactory = DefaultVideoDecoderFactory(mRootEglBase!!.eglBaseContext)
// 构造Factory
val audioDeviceModule = JavaAudioDeviceModule.builder(mContext).createAudioDeviceModule()
val options = PeerConnectionFactory.Options()
return PeerConnectionFactory.builder()
.setOptions(options)
.setAudioDeviceModule(audioDeviceModule)
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory)
.createPeerConnectionFactory()
}
创建好PeerConnectionFactory后其实就可以创建本地的音视频流了,也就页面已经具有显示本地视频的功能了。
private fun createLocalStream() {
localStream = factory?.createLocalMediaStream("ARDAMS")
// 音频
audioSource = factory?.createAudioSource(createAudioConstraints())
localAudioTrack = factory?.createAudioTrack(AUDIO_TRACK_ID, audioSource)
localStream?.addTrack(localAudioTrack)
// 视频
if (!mIsAudioOnly) {
captureAndroid = createVideoCapture()
surfaceTextureHelper =SurfaceTextureHelper.create("CaptureThread",mRootEglBase?.eglBaseContext)
videoSource = captureAndroid?.isScreencast?.let { factory?.createVideoSource(it) }
captureAndroid?.initialize(surfaceTextureHelper,mContext,videoSource?.capturerObserver)
captureAndroid?.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, FPS)
val localVideoTrack = factory?.createVideoTrack(VIDEO_TRACK_ID, videoSource)
localStream?.addTrack(localVideoTrack)
}
}
接下来我们需要定义一个包含PeerConnection的类,方便根据id找到对应PeerConnection。这里我们也实现了SdpObserver和PeerConnection.Observer接口,用于监听设置sdp是否成功和连接状态,其中onAddStream回调中会返回远端的音视频流信息。
class Peer(
private val mUserId: String,
private val pc: PeerConnection?
) : SdpObserver, PeerConnection.Observer {
//远端音视频流信息
override fun onAddStream(stream: MediaStream) {
_remoteStream = stream
}
......
}
这里我们使用SurfaceViewRenderer来接收,配合回调在UI线程中将其显示到页面。
renderer = SurfaceViewRenderer(context)
renderer?.init(mRootEglBase!!.eglBaseContext, object : RendererEvents {
override fun onFirstFrameRendered() {
Timber.e("socket createRender onFirstFrameRendered")
}
override fun onFrameResolutionChanged(
videoWidth: Int,
videoHeight: Int,
rotation: Int
) {
Timber.e("socket createRender onFrameResolutionChanged")
}
})
renderer?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
renderer?.setMirror(true)
renderer?.setZOrderMediaOverlay(isOverlay)
sink = ProxyVideoSink()
sink?.setTarget(renderer)
if (_remoteStream != null && _remoteStream!!.videoTracks.size > 0) {
_remoteStream!!.videoTracks[0].addSink(sink)
}
由于要在公网下实现音视频,PeerConnection的创建需要配置ice,服务端创建stun服务,移动端在这里配置。stun服务是否能够穿透可到官网进行测试。搭建stun/turn服务可参考链接。
val ice = IceServer.builder("stun服务地址")
.createIceServer()
iceServers.add(ice)
private fun createPeerConnection(): PeerConnection? {
val rtcConfig = RTCConfiguration(iceServers)
return mFactory?.createPeerConnection(rtcConfig, this)
}
到此,我们只需按照之前的先后顺序,通过Socket交换信令了。先从A端开始讲,
A端创建并发送offer
//创建offer
peerConnection.createOffer(this, offerOrAnswerConstraint())
//创建MediaConstraints 用于peer创建offer/answer入参
private fun offerOrAnswerConstraint(): MediaConstraints {
val mediaConstraints = MediaConstraints()
val keyValuePairs = ArrayList<MediaConstraints.KeyValuePair>()
keyValuePairs.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
keyValuePairs.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
mediaConstraints.mandatory.addAll(keyValuePairs)
mediaConstraints.optional.add(
MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")
)
return mediaConstraints
}
//在设置为本地sdp成功后发送offer
override fun onSetSuccess() {
socket.emit(Tags.REQUEST, jsonString)
}
A端收到B端Answer
//获取answer中用户id用于查找对应peerConnection,之后设置远端sdp
socket.on(Tags.RESPONSE) { args ->
if (args != null) {
val data = args[0].toString()
val bean = gson.fromJson(data, HandleBean::class.java)
val userId = bean.id
val sdp = bean.sdp
val peerConnection = peers[userId]
peerConnection?.let {
val sessionDescription = SessionDescription(SessionDescription.Type.ANSWER,sdp)
it.setRemoteDescription(sessionDescription)
}
}
}
//ice交换同时进行
socket.on(Tags.RESPONSE) { args ->
if (args != null) {
val data = args[0].toString()
val bean = gson.fromJson(data, HandleBean::class.java)
val userId = bean.id
val peerConnection = peers[userId]
peerConnection?.let {
val iceCandidate = IceCandidate(bean.iceId,bean.label,bean.candidate)
it.addRemoteIceCandidate(iceCandidate)
}
}
}
//如果信息无误,onAddStream方法则会触发,拿到远端视频流。
再来说说B端:
B端接收到A端offer,并创建answer
这一步和A端收到B端offer一样,都是将接收的sdp信息设置到远端,唯一不同的是增加了一步创建answer。
socket.on(Tags.RESPONSE) { args ->
if (args != null) {
val data = args[0].toString()
val bean = gson.fromJson(data, HandleBean::class.java)
val userId = bean.id
val sdp = bean.sdp
val peerConnection = peers[userId]
peerConnection?.let {
val sessionDescription = SessionDescription(SessionDescription.Type.ANSWER,sdp)
it.setRemoteDescription(sessionDescription)
//这步很关键
it.createAnswer(this, offerOrAnswerConstraint())
}
}
}
B端发送answer到A端
//将设置成功后的answer发送给A
override fun onSetSuccess() {
socket?.emit(Tags.REQUEST, jsonString)
}
//ice交换同时进行
socket.on(Tags.RESPONSE) { args ->
if (args != null) {
val data = args[0].toString()
val bean = gson.fromJson(data, HandleBean::class.java)
val userId = bean.id
val peerConnection = peers[userId]
peerConnection?.let {
val iceCandidate = IceCandidate(bean.iceId,bean.label,bean.candidate)
it.addRemoteIceCandidate(iceCandidate)
}
}
}
//如果信息无误,onAddStream方法则会触发,拿到远端视频流。
这里再将SessionDescription单独说明下,它内部是由type和description组成的,其中type其实就是offer和answer做区分,description则包含了所有音视频的一些信息。但其type是个枚举类型,传输的时候又是以json形式传输,所以前端那边传输的时候只需将SessionDescription中的sdp信息拿出来即可,移动端通过信令已经知道它的类型,拿到的时候重新组装成SessionDescription再设置到对应方法。否则会出现视频无法连接上的问题。
如何挂断?
//注意这里不能使用peerConnection.dispose(),会直接引起崩溃,具体原因不明。
renderer.release()
peerConnection.close()
总结
实现流程其实也并不复杂,主要是搞清逻辑顺序,先易后难。最开始还是得弄清楚一对一得流程,只要这个过程通了,在此基础上再管理一个peerConnection得管理类,每一个用户对应一个peerConnection建立连接。这篇就到这里,下篇我们再来一个分支,手机使用USB摄像头实现webrtc音视频功能。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 14 天,点击查看活动详情