Android WebRTC开发:从环境搭建到音视频通话实现
1. WebRTC-Android开发环境搭建在开始构建音视频通话应用之前我们需要先搭建好开发环境。Android平台上的WebRTC开发需要一些特定的配置和依赖项。1.1 项目基础配置首先在Android Studio中创建一个新项目然后在app模块的build.gradle文件中添加以下关键配置android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation org.webrtc:google-webrtc:1.0.32006 implementation org.java-websocket:Java-WebSocket:1.5.1 implementation com.squareup.okhttp3:okhttp:4.9.0 }注意WebRTC库需要Java 8支持所以必须配置compileOptions。最新版本的WebRTC库可能会有所变化建议查看官方文档获取最新版本号。1.2 权限配置在AndroidManifest.xml中添加必要的权限uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.RECORD_AUDIO / uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.MODIFY_AUDIO_SETTINGS /对于Android 6.0及以上版本还需要在运行时请求这些权限。这里我推荐使用Google的EasyPermissions库来简化权限请求流程。2. WebRTC核心组件初始化2.1 PeerConnectionFactory初始化PeerConnectionFactory是WebRTC的核心工厂类负责创建各种WebRTC对象。正确的初始化方式如下private fun initializePeerConnectionFactory(context: Context) { val initializationOptions PeerConnectionFactory.InitializationOptions .builder(context) .setEnableVideoHwAcceleration(true) .createInitializationOptions() PeerConnectionFactory.initialize(initializationOptions) val options PeerConnectionFactory.Options() val defaultVideoEncoderFactory DefaultVideoEncoderFactory( rootEglBase.eglBaseContext, true, // enableIntelVp8Encoder true // enableH264HighProfile ) val defaultVideoDecoderFactory DefaultVideoDecoderFactory(rootEglBase.eglBaseContext) peerConnectionFactory PeerConnectionFactory( options, defaultVideoEncoderFactory, defaultVideoDecoderFactory ) }在实际项目中我发现启用硬件加速(true)可以显著提升视频编码/解码性能但某些低端设备可能会出现兼容性问题。如果遇到问题可以尝试关闭硬件加速。2.2 视频采集与渲染视频采集需要创建VideoCapturer实例private fun createVideoCapturer(): VideoCapturer? { val enumerator Camera2Enumerator(context) val deviceNames enumerator.deviceNames for (deviceName in deviceNames) { if (enumerator.isFrontFacing(deviceName)) { return enumerator.createCapturer(deviceName, null) } } return null }视频渲染使用SurfaceViewRendererprivate fun setupVideoViews() { localVideoView.init(rootEglBase.eglBaseContext, null) remoteVideoView.init(rootEglBase.eglBaseContext, null) localVideoView.setZOrderMediaOverlay(true) remoteVideoView.setZOrderMediaOverlay(true) localVideoView.setMirror(true) // 本地预览需要镜像 remoteVideoView.setMirror(false) // 远程视频不需要镜像 }3. 信令服务器实现3.1 信令服务器选择WebRTC本身不包含信令机制需要开发者自行实现。常见的选择有WebSocket服务器如Node.js ws库Socket.IOFirebase实时数据库自定义HTTP信令这里我推荐使用WebSocket因为它简单高效。下面是一个基于Ktor的WebSocket信令服务器实现示例embeddedServer(Netty, port 8080) { install(WebSockets) routing { webSocket(/signal) { val sessionId call.parameters[sessionId] ?: returnwebSocket sessions[sessionId] this try { for (frame in incoming) { if (frame is Frame.Text) { val text frame.readText() // 处理信令消息 handleSignalingMessage(sessionId, text) } } } finally { sessions.remove(sessionId) } } } }.start(wait true)3.2 Android端信令客户端Android端需要实现WebSocket客户端class SignalingClient(private val listener: SignalingListener) { private var webSocket: WebSocket? null fun connect(sessionId: String) { val request Request.Builder() .url(ws://your-server-address:8080/signal?sessionId$sessionId) .build() val client OkHttpClient() webSocket client.newWebSocket(request, object : WebSocketListener() { override fun onMessage(webSocket: WebSocket, text: String) { listener.onMessageReceived(text) } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { listener.onConnectionError(t.message) } }) } fun sendMessage(message: String) { webSocket?.send(message) } fun disconnect() { webSocket?.close(1000, User disconnect) } }4. 建立PeerConnection4.1 ICE服务器配置WebRTC需要使用ICE服务器进行NAT穿透。可以使用免费的STUN服务器和付费的TURN服务器private val iceServers listOf( PeerConnection.IceServer.builder(stun:stun.l.google.com:19302).createIceServer(), PeerConnection.IceServer.builder(turn:your-turn-server.com) .setUsername(username) .setPassword(password) .createIceServer() )在实际项目中我建议将ICE服务器配置放在远程配置中这样可以在不更新应用的情况下调整服务器设置。4.2 创建PeerConnectionprivate fun createPeerConnection(): PeerConnection { val rtcConfig PeerConnection.RTCConfiguration(iceServers) rtcConfig.tcpCandidatePolicy PeerConnection.TcpCandidatePolicy.DISABLED rtcConfig.bundlePolicy PeerConnection.BundlePolicy.MAXBUNDLE rtcConfig.rtcpMuxPolicy PeerConnection.RtcpMuxPolicy.REQUIRE rtcConfig.continualGatheringPolicy PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY return peerConnectionFactory.createPeerConnection(rtcConfig, object : PeerConnection.Observer() { override fun onIceCandidate(iceCandidate: IceCandidate) { // 通过信令服务器发送ICE候选 signalingClient.sendIceCandidate(iceCandidate) } override fun onAddStream(mediaStream: MediaStream) { // 处理远程流 runOnUiThread { handleRemoteStream(mediaStream) } } }) ?: throw IllegalStateException(Failed to create PeerConnection) }5. 媒体协商流程5.1 发起呼叫Offer当用户发起呼叫时需要创建Offer并设置本地描述private fun startCall() { val constraints MediaConstraints().apply { mandatory.add(MediaConstraints.KeyValuePair(OfferToReceiveVideo, true)) mandatory.add(MediaConstraints.KeyValuePair(OfferToReceiveAudio, true)) } peerConnection.createOffer(object : SdpObserver { override fun onCreateSuccess(sessionDescription: SessionDescription) { peerConnection.setLocalDescription(object : SdpObserver { override fun onSetSuccess() { // 通过信令服务器发送Offer signalingClient.sendSessionDescription(sessionDescription) } override fun onSetFailure(error: String) { Log.e(TAG, Failed to set local description: $error) } }, sessionDescription) } override fun onCreateFailure(error: String) { Log.e(TAG, Failed to create offer: $error) } }, constraints) }5.2 应答呼叫Answer当接收方收到Offer后需要创建Answerprivate fun handleOffer(offer: SessionDescription) { peerConnection.setRemoteDescription(object : SdpObserver { override fun onSetSuccess() { val constraints MediaConstraints().apply { mandatory.add(MediaConstraints.KeyValuePair(OfferToReceiveVideo, true)) mandatory.add(MediaConstraints.KeyValuePair(OfferToReceiveAudio, true)) } peerConnection.createAnswer(object : SdpObserver { override fun onCreateSuccess(sessionDescription: SessionDescription) { peerConnection.setLocalDescription(object : SdpObserver { override fun onSetSuccess() { signalingClient.sendSessionDescription(sessionDescription) } override fun onSetFailure(error: String) { Log.e(TAG, Failed to set local description: $error) } }, sessionDescription) } override fun onCreateFailure(error: String) { Log.e(TAG, Failed to create answer: $error) } }, constraints) } override fun onSetFailure(error: String) { Log.e(TAG, Failed to set remote description: $error) } }, offer) }6. 媒体流处理6.1 添加本地流在创建PeerConnection后需要添加本地媒体流private fun addLocalStream() { val localStream peerConnectionFactory.createLocalMediaStream(local_stream) // 添加音频轨道 val audioSource peerConnectionFactory.createAudioSource(MediaConstraints()) val localAudioTrack peerConnectionFactory.createAudioTrack(audio_track, audioSource) localStream.addTrack(localAudioTrack) // 添加视频轨道 val videoCapturer createVideoCapturer() val videoSource peerConnectionFactory.createVideoSource(videoCapturer) val localVideoTrack peerConnectionFactory.createVideoTrack(video_track, videoSource) localStream.addTrack(localVideoTrack) // 渲染本地视频 localVideoTrack.addSink(localVideoView) peerConnection.addStream(localStream) }6.2 处理远程流当收到远程流时需要将其渲染到远程视图private fun handleRemoteStream(mediaStream: MediaStream) { if (mediaStream.videoTracks.isNotEmpty()) { val remoteVideoTrack mediaStream.videoTracks[0] remoteVideoTrack.addSink(remoteVideoView) } if (mediaStream.audioTracks.isNotEmpty()) { // 音频会自动播放 } }7. 连接状态管理7.1 ICE连接状态监控override fun onIceConnectionChange(iceConnectionState: PeerConnection.IceConnectionState) { runOnUiThread { when (iceConnectionState) { PeerConnection.IceConnectionState.CONNECTED - { Log.d(TAG, ICE connected) // 更新UI显示连接成功 } PeerConnection.IceConnectionState.DISCONNECTED - { Log.d(TAG, ICE disconnected) // 尝试重新连接或显示错误 } PeerConnection.IceConnectionState.FAILED - { Log.e(TAG, ICE connection failed) // 处理连接失败 } else - {} } } }7.2 通话结束处理private fun endCall() { peerConnection.close() signalingClient.disconnect() runOnUiThread { localVideoView.clearImage() remoteVideoView.clearImage() } }8. 实际开发中的经验与技巧8.1 常见问题解决黑屏问题确保SurfaceViewRenderer已经初始化并且设置了正确的EGL上下文。没有声音检查是否获取了录音权限音频轨道是否正确添加。连接失败检查ICE服务器配置确保STUN/TURN服务器可访问。高CPU使用率考虑降低视频分辨率或帧率使用硬件加速编码。8.2 性能优化建议根据网络条件动态调整视频分辨率val videoConstraints MediaConstraints().apply { mandatory.add(MediaConstraints.KeyValuePair(maxWidth, 640)) mandatory.add(MediaConstraints.KeyValuePair(maxHeight, 480)) mandatory.add(MediaConstraints.KeyValuePair(maxFrameRate, 30)) }使用带宽估计自动调整比特率peerConnection.setBitrate(500, 2000, 300) // min, max, current (kbps)实现ICE重启机制处理网络切换peerConnection.restartIce()8.3 高级功能扩展屏幕共享Android 10支持MediaProjection API实现屏幕共享。数据通道使用DataChannel实现文字聊天或文件传输val dataChannel peerConnection.createDataChannel(chat, DataChannel.Init())多路通话通过创建多个PeerConnection实例实现多方通话。录制功能使用MediaRecorder API录制音视频流。9. 完整项目结构建议一个结构良好的WebRTC Android项目可以这样组织app/ ├── src/ │ ├── main/ │ │ ├── java/com/example/webrtc/ │ │ │ ├── signaling/ │ │ │ │ ├── SignalingClient.kt │ │ │ │ └── SignalingListener.kt │ │ │ ├── webrtc/ │ │ │ │ ├── PeerConnectionManager.kt │ │ │ │ └── WebRTCListener.kt │ │ │ ├── ui/ │ │ │ │ ├── CallActivity.kt │ │ │ │ └── MainActivity.kt │ │ │ └── utils/ │ │ │ ├── PermissionHelper.kt │ │ │ └── IceServerConfig.kt │ │ └── res/ │ └── test/ └── build.gradle这种结构将信令逻辑、WebRTC核心逻辑和UI逻辑分离便于维护和测试。