9、Android端实战:集成WebRTC库、建立P2P连接、音视频通话
好,咱们终于到了动手环节。前面讲了那么多理论,什么信令、ICE、SDP,说实话,光看概念容易晕。我自己当年学WebRTC的时候,也是看了好几遍流程图,脑子里还是一团浆糊。直到真正在Android Studio里敲下第一行代码,看到两个手机的画面通了,那种感觉才叫「哦,原来是这样」。
这一章,我们就来点实在的。我会带你一步步把WebRTC库集成到Android项目里,然后建立P2P连接,最后跑通音视频通话。嗯,这里要注意,我用的Android Studio版本是Hedgehog,Kotlin 1.9,Gradle 8.2。版本不同可能会有细微差异,但核心逻辑不变。
9.1 集成WebRTC库
集成这一步,说白了就是往项目里塞依赖。WebRTC官方其实没有直接给Android的Maven包,但Google维护了一个第三方镜像,或者你也可以自己编译源码。我个人习惯用Maven直接拉,省事。
在build.gradle(Module级别)的dependencies块里加上:
dependencies {
implementation 'io.getstream:stream-webrtc-android:1.1.0'
}
等等,别急着Sync。这里有个坑——我之前在项目里遇到过,如果项目里同时用了其他版本的org.webrtc包,会冲突。解决办法是加个exclude:
implementation ('io.getstream:stream-webrtc-android:1.1.0') {
exclude group: 'org.webrtc', module: 'webrtc'
}
Sync完之后,检查一下能不能正常导入:
import org.webrtc.*
如果IDE没报红,恭喜,库已经进来了。
9.2 权限与初始化
WebRTC需要摄像头和麦克风权限。Android 6.0以上要动态申请。我一般这样写:
private val PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
)
private fun checkPermissions() {
if (PERMISSIONS.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED }) {
initWebRTC()
} else {
ActivityCompat.requestPermissions(this, PERMISSIONS, REQUEST_CODE)
}
}
权限拿到后,初始化PeerConnectionFactory。这是WebRTC的核心工厂类,所有连接都靠它创建。
private fun initWebRTC() {
val options = PeerConnectionFactory.InitializationOptions.builder(this)
.setFieldTrials("")
.createInitializationOptions()
PeerConnectionFactory.initialize(options)
val encoderFactory = DefaultVideoEncoderFactory(rootEglBase.eglBaseContext, true, true)
val decoderFactory = DefaultVideoDecoderFactory(rootEglBase.eglBaseContext)
peerConnectionFactory = PeerConnectionFactory.builder()
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory)
.createPeerConnectionFactory()
}
这里有个细节:rootEglBase是什么?它是用来管理OpenGL上下文的。没有它,视频渲染会黑屏。我习惯在Activity的onCreate里初始化:
rootEglBase = EglBase.create()
9.3 建立P2P连接
好,现在工厂有了,我们来创建连接。P2P的核心是交换SDP和ICE候选。我画了一张图,帮你理清整个流程:
流程清楚了,咱们写代码。先创建PeerConnection:
val iceServers = listOf(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
)
val config = PeerConnection.RTCConfiguration(iceServers).apply {
sdpSemantics = SdpSemantics.UNIFIED_PLAN
}
peerConnection = peerConnectionFactory.createPeerConnection(config, object : PeerConnection.Observer {
override fun onIceCandidate(candidate: IceCandidate?) {
// 通过信令服务器发送给远端
signalingClient.sendIceCandidate(candidate)
}
override fun onAddStream(stream: MediaStream?) {
// 收到远端视频流,渲染到SurfaceView
stream?.videoTracks?.first()?.addSink(surfaceView)
}
override fun onIceConnectionState(state: IceConnectionState?) {
Log.d("WebRTC", "连接状态: $state")
}
// 其他回调省略...
})
发起方创建Offer:
val mediaConstraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
}
peerConnection?.createOffer(object : SdpObserver {
override fun onCreateSuccess(sdp: SessionDescription?) {
peerConnection?.setLocalDescription(SdpObserver, sdp)
// 发送SDP给远端
signalingClient.sendSdp(sdp)
}
override fun onCreateFailure(error: String?) {
Log.e("WebRTC", "创建Offer失败: $error")
}
}, mediaConstraints)
接收方收到Offer后,创建Answer:
peerConnection?.setRemoteDescription(SdpObserver, offerSdp)
peerConnection?.createAnswer(object : SdpObserver {
override fun onCreateSuccess(sdp: SessionDescription?) {
peerConnection?.setLocalDescription(SdpObserver, sdp)
signalingClient.sendSdp(sdp)
}
}, MediaConstraints())
9.4 音视频通话
连接建立后,就该采集本地音视频了。先获取摄像头和麦克风:
// 视频源
val videoSource = peerConnectionFactory.createVideoSource(false)
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase.eglBaseContext)
val cameraCapturer = Camera2Capturer(context, cameraEnumerator, surfaceTextureHelper)
cameraCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
cameraCapturer.startCapture(1280, 720, 30)
// 音频源
val audioSource = peerConnectionFactory.createAudioSource(MediaConstraints())
val audioTrack = peerConnectionFactory.createAudioTrack("audio_track", audioSource)
然后把本地流添加到PeerConnection:
val videoTrack = peerConnectionFactory.createVideoTrack("video_track", videoSource)
val localStream = peerConnectionFactory.createLocalMediaStream("local_stream")
localStream.addTrack(videoTrack)
localStream.addTrack(audioTrack)
peerConnection?.addStream(localStream)
渲染远端视频,我用的是SurfaceViewRenderer:
<org.webrtc.SurfaceViewRenderer
android:id="@+id/remote_surface"
android:layout_width="match_parent"
android:layout_height="match_parent" />
在代码里初始化:
remoteSurface.init(rootEglBase.eglBaseContext, null)
remoteSurface.setMirror(false)
remoteSurface.setEnableHardwareScaler(true)
当onAddStream回调触发时,把远端视频流绑定到SurfaceView:
override fun onAddStream(stream: MediaStream?) {
stream?.videoTracks?.first()?.addSink(remoteSurface)
}
- PeerConnectionFactory是单例,整个App只初始化一次
- ICE服务器建议至少配一个STUN和一个TURN,TURN可以用coturn自建
- SurfaceViewRenderer必须在主线程初始化,否则渲染会卡死
- 通话结束后记得调用
peerConnection?.close()释放资源
9.5 避坑指南
最后分享几个我踩过的坑,希望能帮你省点时间:
- 摄像头权限拒绝后不重新请求:Android 11以上,用户如果点了「拒绝且不再询问」,
requestPermissions不会再弹窗。你需要引导用户去设置里手动开启。我当时的处理方式是弹一个Dialog,点击后跳转Settings.ACTION_APPLICATION_DETAILS_SETTINGS。 - SDP交换顺序搞反:必须先
setLocalDescription再发送SDP,否则远端收到的SDP里没有ICE候选信息。我曾经调试了一下午,发现SDP里candidate字段全是空的,就是因为顺序错了。 - 模拟器摄像头黑屏:Android模拟器对Camera2的支持很差。如果你用模拟器调试,建议用
Camera1Enumerator代替Camera2Enumerator。或者干脆用真机,省心。 - 音频回声:如果通话时对方听到自己的回声,多半是没开回声消除。在
MediaConstraints里加上:mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true"))。
好了,到这里你已经完成了Android端WebRTC的集成、P2P连接建立和音视频通话。说实话,第一次跑通的时候,那种成就感还是很爽的。你可以在两个手机上装同一个App,一个点「发起呼叫」,另一个点「接听」,看看画面和声音是不是都通了。
如果遇到问题,先看Logcat里有没有WebRTC标签的日志。大部分问题都能从日志里找到线索。嗯,今天就到这里,动手试试吧。
公众号:蓝海资料掘金营,微信deep3321