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没报红,恭喜,库已经进来了。

小提示: 如果你在模拟器上跑,记得给模拟器分配至少2GB内存,否则WebRTC初始化时容易OOM。我踩过这个坑,模拟器直接闪退,查了半天日志才发现是内存不够。

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候选。我画了一张图,帮你理清整个流程:

Android WebRTC P2P连接流程 发起方(Caller) 接收方(Callee) 1. 创建PeerConnection 2. 创建Offer SDP 3. setLocalDescription Offer SDP 4. setRemoteDescription 5. 创建Answer SDP 6. setLocalDescription Answer SDP 7. setRemoteDescription 8. ICE候选交换 ICE候选 9. 连接建立 9. 连接建立 P2P 音视频通话

流程清楚了,咱们写代码。先创建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())
注意: 信令服务器不是WebRTC标准的一部分,你需要自己实现。可以用WebSocket、MQTT甚至HTTP轮询。我个人推荐用WebSocket,延迟低,双向通信方便。千万别用HTTP长轮询,延迟高得离谱,我在一个早期项目里吃过亏。

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