Android WebRTC实战:从零搭建P2P视频通话(附完整Kotlin源码)

📅 发布时间:2026/7/5 3:02:49 👁️ 浏览次数:
Android WebRTC实战:从零搭建P2P视频通话(附完整Kotlin源码)
Android WebRTC实战构建高可靠P2P视频通话的Kotlin架构深度解析如果你是一名Android开发者正打算为你的社交应用、在线教育平台或远程协作工具嵌入实时音视频通话功能那么WebRTC几乎是你绕不开的技术选项。它开源、免费并且由Google主导在浏览器和移动端都拥有强大的原生支持。但当你真正打开官方文档面对PeerConnectionFactory、SDP、ICE候选这些术语以及需要自己搭建信令服务器的现实时那种从入门到放弃的冲动可能瞬间涌上心头。市面上很多教程要么过于理论化要么代码片段零散难以整合成一个可运行、可维护的完整项目。这篇文章的目的就是带你穿越这片迷雾。我们不只提供一段可以复制粘贴的代码更重要的是我会结合自己多次在商业项目中集成WebRTC的经验拆解其核心架构解释每一步背后的“为什么”并分享那些官方文档里不会写的“坑”和优化技巧。你将看到一个用Kotlin构建的、结构清晰的Android WebRTC客户端实现从权限申请、依赖引入到信令交换、媒体流管理再到UI渲染和异常处理形成一个完整的闭环。无论你是想快速实现一个Demo验证想法还是为成熟产品集成稳定的通话能力这里的内容都能提供扎实的起点和深入的见解。1. 项目基石环境搭建与核心依赖剖析在写下第一行业务代码之前搭建一个正确且高效的开发环境至关重要。WebRTC在Android上的集成有其特殊性我们需要在性能、包体积和兼容性之间做出权衡。1.1 依赖配置的艺术首先打开你的app/build.gradle.kts或build.gradle文件。WebRTC的依赖选择并非只有一种。plugins { id(com.android.application) id(org.jetbrains.kotlin.android) } android { compileSdk 34 defaultConfig { applicationId com.yourcompany.webrtcdemo minSdk 24 // 建议从API 24开始以获得更好的Camera2 API支持 targetSdk 34 } // 关键配置处理原生库冲突 packagingOptions { resources { // WebRTC库可能包含多个ABI版本的同名so文件此配置避免合并冲突 pickFirsts setOf( lib/armeabi-v7a/libjingle_peerconnection_so.so, lib/arm64-v8a/libjingle_peerconnection_so.so, lib/x86/libjingle_peerconnection_so.so, lib/x86_64/libjingle_peerconnection_so.so ) // 排除不必要的META-INF文件减小包体积 excludes setOf(/META-INF/**) } } } dependencies { implementation(androidx.core:core-ktx:1.12.0) implementation(androidx.appcompat:appcompat:1.6.1) implementation(com.google.android.material:material:1.11.0) implementation(androidx.constraintlayout:constraintlayout:2.1.4) // 核心WebRTC库 implementation(org.webrtc:google-webrtc:1.0.32006) // 替代方案如需特定版本或自定义编译可使用 // implementation files(libs/libwebrtc.aar) // 自行编译的AAR // 网络与协程 implementation(com.squareup.okhttp3:okhttp:4.12.0) // 用于WebSocket信令 implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3) implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3) // 工具类 implementation(com.google.code.gson:gson:2.10.1) // JSON解析 }注意WebRTC库的版本号更新较快。建议在Google Maven仓库查看最新稳定版。版本1.0.32006是一个经过广泛测试的版本平衡了功能与稳定性。这里有几个关键点minSdk版本虽然WebRTC支持到API 21但为了更稳定、功能更丰富的摄像头控制我强烈建议将minSdk设为24或以上以便使用Camera2 API。packagingOptions这是Android项目集成包含多个原生库.so文件的第三方库时的常见配置用于解决构建时因文件重复导致的冲突。依赖选择直接使用Google提供的预编译库是最快捷的方式。只有在需要深度定制编解码器、或进行大量剪裁以优化包体积时才需要考虑从源码编译。1.2 权限与硬件能力声明接下来在AndroidManifest.xml中声明必要的权限和硬件特性。这一步常被忽略细节导致运行时崩溃。?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.yourcompany.webrtcdemo !-- 网络权限 -- uses-permission android:nameandroid.permission.INTERNET / !-- 摄像头与音频权限 -- uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.RECORD_AUDIO / !-- 对于Android 6.0需要在运行时动态申请 -- uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / !-- 声明应用使用的硬件特性 -- uses-feature android:nameandroid.hardware.camera / uses-feature android:nameandroid.hardware.camera.autofocus android:requiredfalse / uses-feature android:nameandroid.hardware.microphone / application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name android:themestyle/Theme.WebRTCDemo activity android:name.MainActivity android:exportedtrue android:screenOrientationportrait !-- 锁定竖屏简化处理 -- intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifest提示android:requiredfalse表示该特性如自动对焦不是应用运行所必需的这样没有该特性的设备也能从Google Play商店安装应用。但代码中需要做兼容性判断。2. 架构核心分层设计与模块职责一个健壮的WebRTC应用不应该把所有代码都堆在Activity里。清晰的分层能极大提升代码的可读性、可测试性和可维护性。我推荐采用以下三层架构UI Layer (Activity/Fragment) ├── 处理用户交互点击呼叫、挂断 ├── 管理视频渲染视图SurfaceViewRenderer └── 协调业务逻辑层 Business Logic Layer (RTCClient, SignalClient) ├── RTCClient: WebRTC核心API的封装PeerConnection, 媒体流 ├── SignalClient: 信令服务器通信WebSocket └── 处理SDP交换、ICE候选传递等核心协商逻辑 Infrastructure Layer (EglBase, Threading) ├── 提供EGL上下文用于GPU加速渲染 ├── 管理后台线程避免阻塞UI └── 资源生命周期管理初始化、释放这种分离使得每一层职责单一。例如Activity只关心如何显示画面和响应按钮点击RTCClient不知道数据是通过WebSocket还是Socket.IO传输的SignalClient则完全专注于网络消息的收发。2.1 信令模块应用层的“电话接线员”WebRTC本身不负责发现和连接对等端这部分工作由信令服务器完成。你可以把它想象成电话系统中的接线员负责让双方交换“电话号码”网络地址信息和“通话意愿”媒体能力协商。下面是一个基于OkHttp WebSocket的SignalClient实现它足够轻量且稳定package com.yourcompany.webrtcdemo.signal import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import okhttp3.* import org.json.JSONObject import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton sealed class SignalingMessage { data class Offer(val sdp: String) : SignalingMessage() data class Answer(val sdp: String) : SignalingMessage() data class IceCandidate(val sdpMid: String, val sdpMLineIndex: Int, val candidate: String) : SignalingMessage() data class Error(val message: String) : SignalingMessage() object Connected : SignalingMessage() object Disconnected : SignalingMessage() } Singleton class SignalClient Inject constructor() { private val scope CoroutineScope(SupervisorJob() Dispatchers.IO) private var webSocket: WebSocket? null private val _signalingEvents MutableSharedFlowSignalingMessage() val signalingEvents: SharedFlowSignalingMessage _signalingEvents.asSharedFlow() private val okHttpClient OkHttpClient.Builder() .readTimeout(0, TimeUnit.MILLISECONDS) // WebSocket需要长连接读超时设为0 .pingInterval(20, TimeUnit.SECONDS) // 定期发送Ping保持连接活跃 .build() fun connect(serverUrl: String) { scope.launch { try { val request Request.Builder().url(serverUrl).build() webSocket okHttpClient.newWebSocket(request, object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { scope.launch { _signalingEvents.emit(SignalingMessage.Connected) } } override fun onMessage(webSocket: WebSocket, text: String) { parseAndEmitMessage(text) } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { scope.launch { _signalingEvents.emit(SignalingMessage.Error(Connection failed: ${t.message})) _signalingEvents.emit(SignalingMessage.Disconnected) } // 实现指数退避重连逻辑 scheduleReconnect() } override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { scope.launch { _signalingEvents.emit(SignalingMessage.Disconnected) } } }) } catch (e: Exception) { _signalingEvents.tryEmit(SignalingMessage.Error(Connect error: ${e.message})) } } } private fun parseAndEmitMessage(jsonText: String) { runCatching { val json JSONObject(jsonText) val type json.getString(type) val message when (type) { offer - SignalingMessage.Offer(json.getString(sdp)) answer - SignalingMessage.Answer(json.getString(sdp)) candidate - SignalingMessage.IceCandidate( json.getString(sdpMid), json.getInt(sdpMLineIndex), json.getString(candidate) ) else - returnrunCatching } scope.launch { _signalingEvents.emit(message) } }.onFailure { e - scope.launch { _signalingEvents.emit(SignalingMessage.Error(Parse message failed: ${e.message})) } } } fun sendOffer(sdp: String) { sendJsonMessage(offer, sdp) } fun sendAnswer(sdp: String) { sendJsonMessage(answer, sdp) } fun sendIceCandidate(sdpMid: String, sdpMLineIndex: Int, candidate: String) { val json JSONObject().apply { put(type, candidate) put(sdpMid, sdpMid) put(sdpMLineIndex, sdpMLineIndex) put(candidate, candidate) } webSocket?.send(json.toString()) } private fun sendJsonMessage(type: String, sdp: String) { val json JSONObject().apply { put(type, type) put(sdp, sdp) } webSocket?.send(json.toString()) } private fun scheduleReconnect() { // 实现重连逻辑例如延迟3秒后尝试重连 scope.launch { delay(3000L) webSocket?.cancel() // 重新连接这里需要保存之前的serverUrl实践中可通过构造函数或状态保存 } } fun disconnect() { webSocket?.close(1000, Normal closure) scope.cancel() } }这个实现有几个亮点使用Kotlin Flow处理异步事件将连接状态、收到的消息都封装成Flow业务层可以方便地收集并处理避免了回调地狱。心跳保活通过pingInterval设置定期发送Ping帧防止中间网络设备因长时间无数据而断开连接。错误处理与重连在onFailure中实现了简单的重连调度入口为网络不稳定的场景提供了恢复能力。密封类定义消息使用sealed class清晰地定义了所有可能的信令消息类型使模式匹配when语句更加安全和完善。3. WebRTC引擎RTCClient的深度封装这是整个应用最核心的部分负责与WebRTC原生库交互。我们的目标是封装一个RTCClient类对外提供简洁的API如startCall,endCall并内部处理所有复杂的初始化、媒体流管理和信令交互。3.1 初始化与资源管理WebRTC的初始化必须在应用生命周期早期进行且涉及线程和GPU上下文。package com.yourcompany.webrtcdemo.rtc import android.content.Context import android.util.Log import org.webrtc.* import javax.inject.Inject import javax.inject.Singleton Singleton class RTCClient Inject constructor( private val appContext: Context, private val eglBase: EglBase, private val signalClient: SignalClient ) { companion object { init { // 初始化PeerConnectionFactory必须在任何WebRTC操作之前调用 val initializationOptions PeerConnectionFactory.InitializationOptions.builder(appContext) .setEnableInternalTracer(true) // 开启内部跟踪便于调试 .setFieldTrials(WebRTC-H264HighProfile/Enabled/) // 可选的字段试验例如启用H.264 High Profile .createInitializationOptions() PeerConnectionFactory.initialize(initializationOptions) } } private val factory: PeerConnectionFactory private var peerConnection: PeerConnection? null private var localVideoTrack: VideoTrack? null private var localAudioTrack: AudioTrack? null private var videoCapturer: VideoCapturer? null private var videoSource: VideoSource? null init { // 1. 创建PeerConnectionFactory实例 factory PeerConnectionFactory.builder() .setVideoEncoderFactory(DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, true)) .setVideoDecoderFactory(DefaultVideoDecoderFactory(eglBase.eglBaseContext)) .createPeerConnectionFactory() // 2. 创建本地媒体流 createLocalMediaStream() // 3. 监听信令事件 observeSignaling() } private fun createLocalMediaStream() { // 创建视频轨道 videoCapturer createCameraCapturer() videoSource factory.createVideoSource(videoCapturer!!.isScreencast) val surfaceTextureHelper SurfaceTextureHelper.create(CaptureThread, eglBase.eglBaseContext) videoCapturer!!.initialize(surfaceTextureHelper, appContext, videoSource!!.capturerObserver) // 启动采集参数可根据网络状况动态调整 videoCapturer!!.startCapture(640, 480, 30) // 初始分辨率640x48030fps localVideoTrack factory.createVideoTrack(local_video, videoSource).apply { setEnabled(true) // 默认启用 } // 创建音频轨道 val audioConstraints MediaConstraints().apply { optional.add(MediaConstraints.KeyValuePair(googEchoCancellation, true)) optional.add(MediaConstraints.KeyValuePair(googAutoGainControl, true)) optional.add(MediaConstraints.KeyValuePair(googHighpassFilter, true)) optional.add(MediaConstraints.KeyValuePair(googNoiseSuppression, true)) } val audioSource factory.createAudioSource(audioConstraints) localAudioTrack factory.createAudioTrack(local_audio, audioSource).apply { setEnabled(true) } } private fun createCameraCapturer(): VideoCapturer { val enumerator Camera2Enumerator(appContext) val deviceNames enumerator.deviceNames // 优先选择后置摄像头因其通常画质更好 for (deviceName in deviceNames) { if (enumerator.isBackFacing(deviceName)) { enumerator.createCapturer(deviceName, null)?.let { return it } } } // 若无后置选择前置 for (deviceName in deviceNames) { if (enumerator.isFrontFacing(deviceName)) { enumerator.createCapturer(deviceName, null)?.let { return it } } } throw RuntimeException(No camera found on this device.) } }注意PeerConnectionFactory.initialize()是一个全局性的初始化操作通常建议在Application类中调用确保只执行一次。这里放在伴生对象初始化块中利用类加载机制保证线程安全且只执行一次。3.2 建立点对点连接与SDP交换创建本地流后下一步是建立PeerConnection并开始信令交互。这个过程遵循标准的Offer/Answer模型。class RTCClient { // ... 接上文代码 private fun createPeerConnection(): PeerConnection { // 配置ICE服务器STUN用于获取公网地址TURN用于在对称NAT后中继流量 val iceServers listOf( PeerConnection.IceServer.builder(stun:stun.l.google.com:19302).createIceServer(), // 在实际生产环境中你必须配置自己的TURN服务器 // PeerConnection.IceServer.builder(turn:your.turn.server:3478) // .setUsername(username) // .setPassword(password) // .createIceServer() ) val rtcConfig PeerConnection.RTCConfiguration(iceServers).apply { tcpCandidatePolicy PeerConnection.TcpCandidatePolicy.DISABLED // 通常禁用TCP候选性能较差 bundlePolicy PeerConnection.BundlePolicy.MAXBUNDLE // 聚合音视频流节省端口 rtcpMuxPolicy PeerConnection.RtcpMuxPolicy.REQUIRE // 要求RTCP复用简化NAT穿透 continualGatheringPolicy PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY // 持续收集ICE候选应对网络变化 // 配置更多高级选项... } val pcObserver object : PeerConnection.Observer { override fun onIceCandidate(candidate: IceCandidate) { // 当发现新的ICE候选网络路径时通过信令发送给对方 signalClient.sendIceCandidate(candidate.sdpMid, candidate.sdpMLineIndex, candidate.sdp) Log.d(RTCClient, Local ICE candidate: ${candidate.sdp}) } override fun onAddStream(stream: MediaStream) { // 当远端媒体流添加时触发 Log.d(RTCClient, Remote stream added, video tracks: ${stream.videoTracks.size}) if (stream.videoTracks.isNotEmpty()) { onRemoteVideoTrack?.invoke(stream.videoTracks[0]) } } override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) { Log.d(RTCClient, ICE connection state changed to: $newState) when (newState) { PeerConnection.IceConnectionState.CONNECTED - { // 连接成功 } PeerConnection.IceConnectionState.DISCONNECTED, PeerConnection.IceConnectionState.FAILED - { // 连接断开或失败可能需要尝试重启ICE或通知用户 } else - {} } } // ... 其他回调方法如onSignalingChange, onIceGatheringChange等 } return factory.createPeerConnection(rtcConfig, pcObserver) ?: throw IllegalStateException(Failed to create PeerConnection) } fun startCall() { val pc peerConnection ?: createPeerConnection().also { peerConnection it } // 将本地音视频轨道添加到PeerConnection val localStream factory.createLocalMediaStream(local_stream) localVideoTrack?.let { localStream.addTrack(it) } localAudioTrack?.let { localStream.addTrack(it) } pc.addStream(localStream) // 创建Offer包含本端的媒体能力描述SDP val constraints MediaConstraints().apply { optional.add(MediaConstraints.KeyValuePair(OfferToReceiveAudio, true)) optional.add(MediaConstraints.KeyValuePair(OfferToReceiveVideo, true)) } pc.createOffer(object : SdpObserver { override fun onCreateSuccess(desc: SessionDescription) { // 1. 设置本地描述 pc.setLocalDescription(object : SdpObserver { override fun onSetSuccess() { // 2. 通过信令服务器发送Offer给对端 signalClient.sendOffer(desc.description) Log.d(RTCClient, Offer created and sent: ${desc.type}) } override fun onSetFailure(error: String) { Log.e(RTCClient, Set local description failed: $error) } }, desc) } override fun onCreateFailure(error: String) { Log.e(RTCClient, Create offer failed: $error) } override fun onSetSuccess() {} override fun onSetFailure(error: String) {} }, constraints) } // 处理从信令收到的远端Offer fun handleRemoteOffer(sdp: String) { val pc peerConnection ?: createPeerConnection().also { peerConnection it } val remoteDesc SessionDescription(SessionDescription.Type.OFFER, sdp) pc.setRemoteDescription(object : SdpObserver { override fun onSetSuccess() { // 设置远端描述成功后创建Answer val constraints MediaConstraints() pc.createAnswer(object : SdpObserver { override fun onCreateSuccess(desc: SessionDescription) { pc.setLocalDescription(object : SdpObserver { override fun onSetSuccess() { // 发送Answer给对端 signalClient.sendAnswer(desc.description) } override fun onSetFailure(error: String) { Log.e(RTCClient, Set local description (answer) failed: $error) } }, desc) } override fun onCreateFailure(error: String) { Log.e(RTCClient, Create answer failed: $error) } override fun onSetSuccess() {} override fun onSetFailure(error: String) {} }, constraints) } override fun onSetFailure(error: String) { Log.e(RTCClient, Set remote description (offer) failed: $error) } }, remoteDesc) } // 处理从信令收到的远端Answer fun handleRemoteAnswer(sdp: String) { peerConnection?.let { pc - val remoteDesc SessionDescription(SessionDescription.Type.ANSWER, sdp) pc.setRemoteDescription(object : SdpObserver { override fun onSetSuccess() { Log.d(RTCClient, Remote answer set successfully) } override fun onSetFailure(error: String) { Log.e(RTCClient, Set remote description (answer) failed: $error) } override fun onCreateSuccess(desc: SessionDescription) {} override fun onCreateFailure(error: String) {} }, remoteDesc) } } // 处理从信令收到的远端ICE候选 fun addRemoteIceCandidate(sdpMid: String, sdpMLineIndex: Int, candidate: String) { peerConnection?.addIceCandidate(IceCandidate(sdpMid, sdpMLineIndex, candidate)) } var onRemoteVideoTrack: ((VideoTrack) - Unit)? null }这个模块清晰地展示了WebRTC建立连接的核心流程创建PeerConnection - 交换SDP (Offer/Answer) - 交换ICE候选。每一步都是异步的并通过回调处理结果。4. UI整合与实战优化技巧最后我们需要将RTCClient和SignalClient与Android的UI层连接起来并处理一些实际开发中必然会遇到的问题。4.1 Activity中的生命周期协调MainActivity需要妥善管理WebRTC组件的生命周期确保资源及时释放避免内存泄漏。package com.yourcompany.webrtcdemo import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.yourcompany.webrtcdemo.databinding.ActivityCallBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.webrtc.EglBase import javax.inject.Inject AndroidEntryPoint // 假设使用Hilt进行依赖注入 class MainActivity : AppCompatActivity() { Inject lateinit var rtcClient: RTCClient Inject lateinit var signalClient: SignalClient private lateinit var binding: ActivityCallBinding private val eglBase EglBase.create() // EglBase应在UI线程创建 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding ActivityCallBinding.inflate(layoutInflater) setContentView(binding.root) // 1. 初始化视频渲染视图 binding.localVideoView.init(eglBase.eglBaseContext, null) binding.remoteVideoView.init(eglBase.eglBaseContext, null) binding.localVideoView.setMirror(true) // 本地预览镜像符合用户习惯 // 2. 将本地视频轨道渲染到预览视图 rtcClient.localVideoTrack?.addSink(binding.localVideoView) // 3. 订阅远端视频轨道 rtcClient.onRemoteVideoTrack { remoteTrack - runOnUiThread { remoteTrack.addSink(binding.remoteVideoView) binding.statusText.text 已连接 } } // 4. 监听信令事件 lifecycleScope.launch { signalClient.signalingEvents.collect { message - when (message) { is SignalingMessage.Offer - { runOnUiThread { binding.statusText.text 收到呼叫请求... } rtcClient.handleRemoteOffer(message.sdp) } is SignalingMessage.Answer - { rtcClient.handleRemoteAnswer(message.sdp) } is SignalingMessage.IceCandidate - { rtcClient.addRemoteIceCandidate(message.sdpMid, message.sdpMLineIndex, message.candidate) } is SignalingMessage.Connected - { runOnUiThread { binding.statusText.text 信令服务器已连接 } } is SignalingMessage.Error - { runOnUiThread { binding.statusText.text 错误: ${message.message} } } else - {} } } } // 5. 连接信令服务器这里假设服务器地址已配置 signalClient.connect(wss://your-signaling-server.com) // 6. 绑定按钮事件 binding.btnCall.setOnClickListener { rtcClient.startCall() binding.statusText.text 呼叫中... } binding.btnHangup.setOnClickListener { endCall() } binding.btnToggleMute.setOnClickListener { val isMuted rtcClient.toggleAudioMute() binding.btnToggleMute.text if (isMuted) 取消静音 else 静音 } binding.btnSwitchCamera.setOnClickListener { rtcClient.switchCamera() } } private fun endCall() { rtcClient.endCall() signalClient.disconnect() binding.statusText.text 通话结束 binding.remoteVideoView.clearImage() // 清除远端画面 } override fun onDestroy() { // 关键按顺序释放资源避免崩溃 binding.localVideoView.release() binding.remoteVideoView.release() eglBase.release() super.onDestroy() } }4.2 常见问题与进阶优化在实际项目中仅仅实现基础功能是不够的。以下是一些提升体验和稳定性的关键点1. 摄像头切换的实现fun switchCamera() { videoCapturer?.switchCamera(null /* 切换回调 */) // 注意某些设备上switchCamera可能无效需要重新创建VideoCapturer }2. 网络自适应与统计信息WebRTC提供了丰富的统计信息可用于监控通话质量和实现自适应码率。fun gatherStats() { peerConnection?.getStats { reports - reports.forEach { report - if (report.type ssrc report.values.containsKey(bytesSent)) { val bytesSent report.values[bytesSent]?.toLong() ?: 0 val bitrate (bytesSent * 8 / 1000).toInt() // 估算码率 (kbps) Log.d(Stats, 当前发送码率: ${bitrate}kbps) // 根据码率动态调整视频分辨率或帧率 if (bitrate 300) { // 切换到低分辨率 videoCapturer?.changeCaptureFormat(320, 240, 15) } } } } }3. 处理ICE失败与重连在PeerConnection.Observer的onIceConnectionChange回调中如果状态变为FAILED或DISCONNECTED可以尝试触发重新协商ICE Restart。override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState) { when (newState) { PeerConnection.IceConnectionState.FAILED - { Log.w(RTCClient, ICE连接失败尝试重启...) // 可以在这里尝试重新创建Offer进行ICE重启 lifecycleScope.launch { delay(2000) if (isCallActive) { peerConnection?.restartIce() // 或者更彻底地endCall() 然后 startCall() } } } // ... 其他状态处理 } }4. 前后台处理与省电当应用退到后台时应暂停视频采集以节省电量回到前台时恢复。override fun onPause() { super.onPause() videoCapturer?.stopCapture() } override fun onResume() { super.onResume() videoCapturer?.startCapture(640, 480, 30) }集成WebRTC到Android应用是一个涉及音视频处理、网络通信和复杂状态管理的综合工程。从我的经验来看最大的挑战往往不是API调用本身而是异常情况下的稳定性处理、不同设备和网络环境下的兼容性以及如何提供流畅的用户体验。本文提供的架构和代码示例是一个坚实的起点但每个实际项目都可能需要根据具体需求进行调整例如集成更复杂的信令协议如Socket.IO、SignalR、加入房间管理逻辑、或者对接SFU服务器以实现多人会议。最重要的是理解其核心原理信令交换SDP、ICE建立连接、媒体流传输。掌握了这些你就能从容应对大部分实时通信场景的开发需求了。