Android实现WebRTC的android端互连(附带源码)_android webrtc
一、项目介绍
1. 背景与意义
随着移动端实时通讯(RTC)需求的爆炸式增长,基于 WebRTC 的点对点(P2P)视频和音频通话已成为几乎所有现代社交、直播、远程协作应用的核心功能。在 Android 平台上直接使用 WebRTC 原生 API 能最大化定制性和性能,但学习曲线陡峭、配置复杂。本项目通过手把手示例,教你在 Android 上从零开始:
-
集成 Google WebRTC 原生库
-
建立信令通道(使用 WebSocket)
-
完成两台 Android 设备之间互连
-
传输并渲染实时视频流与音频流
-
管理 ICE 候选、SDP 协商、网络变化
-
处理会话断开与重连
-
封装复用模块,支持多房间、多对多通话
全文超过一万字,所有代码整合到一个代码块中,用注释区分不同文件,便于复制。
2. 功能需求
-
双端互连:两部 Android 设备加入同一房间,相互看到对方摄像头画面并听到对方音频
-
信令服务:基于 WebSocket 进行 SDP 与 ICE 候选交换
-
视频渲染:SurfaceViewRenderer 渲染本地与远端流
-
网络自适应:处理网络切换与重连
-
UI 交互:开始/结束通话、切换前后摄、静音、视频开关
-
日志与统计:展示通话时长、丢包、码率等信息
二、相关知识
-
WebRTC 核心组件
-
PeerConnectionFactory
:工厂,用于创建音视频捕获器、编码器与PeerConnection
-
PeerConnection
:负责底层 ICE 协商、NAT 穿透与媒体传输 -
VideoCapturer
+VideoTrack
:采集摄像头并编码发送 -
SurfaceViewRenderer
:渲染远端与本地视频 -
AudioTrack
:采集与播放音频
-
-
信令协议
-
WebRTC 本身不包含信令,需要自定义。常用 WebSocket、Socket.IO、RESTful+Long Polling 等
-
-
ICE 与 STUN/TURN
-
IceServer
列表包含 STUN(候选发现)与 TURN(中继)服务器 -
根据环境添加公有或自建 STUN/TURN
-
-
SDP 协商流程
-
Caller 创建 Offer,Caller SetLocalDescription → 发送 Offer → Callee SetRemoteDescription → Callee CreateAnswer → Callee SetLocalDescription → 发送 Answer → Caller SetRemoteDescription
-
双方收集与交换 ICE 候选
-
-
Android 与 WebRTC 打包
-
可以从 Google Maven(
org.webrtc:google-webrtc:VERSION
)拉取,也可用官方 C++ 源码自行编译
-
-
多线程与 Looper
-
WebRTC 在内部使用
EglBase.Context
、SurfaceViewRenderer
要在 UI 线程操作,协商和 I/O 可在后台线程
-
-
性能与电量
-
建议使用硬件编码(默认),并在低带宽场景下动态适应分辨率和帧率
-
三、环境与依赖
// app/build.gradleplugins { id \'com.android.application\' id \'kotlin-android\'}android { compileSdk 34 defaultConfig { applicationId \"com.example.webrtcdemo\" minSdk 21 targetSdk 34 } packagingOptions { pickFirst \'lib/armeabi-v7a/libjingle_peerconnection_so.so\' pickFirst \'lib/arm64-v8a/libjingle_peerconnection_so.so\' }}dependencies { implementation \"androidx.appcompat:appcompat:1.6.1\" implementation \"androidx.constraintlayout:constraintlayout:2.1.4\" implementation \"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4\" // WebRTC 原生库 implementation \'org.webrtc:google-webrtc:1.0.32006\' // WebSocket 客户端 implementation \'com.squareup.okhttp3:okhttp:4.10.0\'}
四、实现思路与架构
-
信令模块
SignalClient
-
WebSocket 连接服务器,封装
sendOffer()
,sendAnswer()
,sendIceCandidate()
-
注册回调收到远端 SDP/ICE 后调用
onRemoteSessionReceived()
-
-
PeerConnection 管理
RTCClient
-
初始化
PeerConnectionFactory
、创建本地流(摄像头+麦克风) -
创建
PeerConnection
,添加本地MediaStream
,设置PeerConnection.Observer
-
startCall()
→createOffer()
→ 发送 Offer -
接收到 Offer →
onRemoteSessionReceived()
→createAnswer()
→ 发送 Answer -
交换 ICE 候选
-
-
UI 层
MainActivity
-
持有两个
SurfaceViewRenderer
:本地与远端 -
按钮触发呼叫/挂断、静音、摄像头切换
-
显示通话时长与网络状态
-
-
生命周期管理
-
在
onCreate()
初始化,onDestroy()
释放资源 -
退出通话时关闭 WebSocket 与
PeerConnection
-
-
网络重连
-
SignalClient
在 WebSocket 断开时自动重试 -
PeerConnection
在 ICE 失败时尝试重连接
-
五、整合代码
// =======================================================// 文件:AndroidManifest.xml// 描述:申请网络与摄像头权限// ======================================================= // =======================================================// 文件:res/layout/activity_main.xml// 描述:双 SurfaceViewRenderer 与控制按钮// ======================================================= // =======================================================// 文件:SignalClient.kt// 描述:WebSocket 信令客户端,使用 OkHttp// =======================================================package com.example.webrtcdemo.signalimport okhttp3.*import okio.ByteStringimport org.json.JSONObjectimport java.util.concurrent.TimeUnitclass SignalClient( private val serverUrl: String, private val listener: Listener) { interface Listener { fun onOffer(sdp: String) fun onAnswer(sdp: String) fun onIceCandidate(sdpMid: String, sdpMLineIndex: Int, candidate: String) } private val client = OkHttpClient.Builder() .readTimeout(0, TimeUnit.MILLISECONDS) .build() private var ws: WebSocket? = null fun connect() { val req = Request.Builder().url(serverUrl).build() ws = client.newWebSocket(req, object : WebSocketListener() { override fun onMessage(webSocket: WebSocket, text: String) { val json = JSONObject(text) when (json.getString(\"type\")) { \"offer\" -> listener.onOffer(json.getString(\"sdp\")) \"answer\"-> listener.onAnswer(json.getString(\"sdp\")) \"candidate\"-> listener.onIceCandidate( json.getString(\"sdpMid\"), json.getInt(\"sdpMLineIndex\"), json.getString(\"candidate\") ) } } }) } fun sendOffer(sdp: String) { ws?.send(JSONObject().apply { put(\"type\",\"offer\"); put(\"sdp\",sdp) }.toString()) } fun sendAnswer(sdp: String) { ws?.send(JSONObject().apply { put(\"type\",\"answer\"); put(\"sdp\",sdp) }.toString()) } fun sendIceCandidate(sdpMid: String, sdpMLineIndex: Int, candidate: String) { ws?.send(JSONObject().apply { put(\"type\",\"candidate\") put(\"sdpMid\",sdpMid) put(\"sdpMLineIndex\",sdpMLineIndex) put(\"candidate\",candidate) }.toString()) } fun close() { ws?.close(1000, null) }}// =======================================================// 文件:RTCClient.kt// 描述:WebRTC 管理类,封装 PeerConnection 与流// =======================================================package com.example.webrtcdemo.rtcimport android.content.Contextimport org.webrtc.*class RTCClient( private val context: Context, private val eglBase: EglBase, private val signalClient: com.example.webrtcdemo.signal.SignalClient) : PeerConnection.Observer, PeerConnection.IceServer { private val factory: PeerConnectionFactory private val peerConnection: PeerConnection private val localVideoTrack: VideoTrack private val localAudioTrack: AudioTrack init { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions() ) factory = PeerConnectionFactory.builder() .setVideoEncoderFactory(DefaultVideoEncoderFactory(eglBase.eglBaseContext,true,true)) .setVideoDecoderFactory(DefaultVideoDecoderFactory(eglBase.eglBaseContext)) .createPeerConnectionFactory() // 本地视频采集 val videoCapturer = createCameraCapturer() val videoSource = factory.createVideoSource(videoCapturer.isScreencast) videoCapturer.initialize( SurfaceTextureHelper.create(\"CaptureThread\", eglBase.eglBaseContext), context, videoSource.capturerObserver ) videoCapturer.startCapture(640,480,30) localVideoTrack = factory.createVideoTrack(\"VIDEOTRACK\", videoSource) // 本地音频 val audioSource = factory.createAudioSource(MediaConstraints()) localAudioTrack = factory.createAudioTrack(\"AUDIOTRACK\", audioSource) // PeerConnection val iceServers = listOf( PeerConnection.IceServer.builder(\"stun:stun.l.google.com:19302\").createIceServer() ) peerConnection = factory.createPeerConnection( iceServers, this )!! // 添加本地流 val stream = factory.createLocalMediaStream(\"LOCALSTREAM\") stream.addTrack(localVideoTrack) stream.addTrack(localAudioTrack) peerConnection.addStream(stream) // 信令回调 signalClient.connect() } fun startCall() { peerConnection.createOffer(object : SdpObserver { override fun onCreateSuccess(desc: SessionDescription) { peerConnection.setLocalDescription(this, desc) signalClient.sendOffer(desc.description) } // 省略 onSetSuccess/onCreateFailure/onSetFailure }, MediaConstraints()) } fun onRemoteOffer(sdp: String) { val desc = SessionDescription(SessionDescription.Type.OFFER, sdp) peerConnection.setRemoteDescription(object : SdpObserver{ override fun onSetSuccess() { peerConnection.createAnswer(object:SdpObserver{ override fun onCreateSuccess(answer: SessionDescription){ peerConnection.setLocalDescription(this, answer) signalClient.sendAnswer(answer.description) } // 省略其余 }, MediaConstraints()) } // 省略其余 }, desc) } fun onRemoteAnswer(sdp: String) { val desc = SessionDescription(SessionDescription.Type.ANSWER, sdp) peerConnection.setRemoteDescription(object : SdpObserver{ override fun onSetSuccess(){} // ignore // ... }, desc) } fun onRemoteIceCandidate(sdpMid:String, sdpMLineIndex:Int, candidate:String){ peerConnection.addIceCandidate(IceCandidate(sdpMid,sdpMLineIndex,candidate)) } // PeerConnection.Observer 回调 override fun onIceCandidate(c: IceCandidate) { signalClient.sendIceCandidate(c.sdpMid!!, c.sdpMLineIndex, c.sdp) } override fun onAddStream(stream: MediaStream) { // 提取远端视频轨道 val remoteVideoTrack = stream.videoTracks[0] // 由 Activity 注册并渲染 onRemoteStream?.invoke(remoteVideoTrack) } // 省略其他 Observer 方法空实现... var onRemoteStream: ((VideoTrack)->Unit)? = null private fun createCameraCapturer(): VideoCapturer { val enumerator = Camera2Enumerator(context) return enumerator.deviceNames .firstNotNullOf { name -> if (enumerator.isFrontFacing(name)) enumerator.createCapturer(name, null) else null } }}// =======================================================// 文件:MainActivity.kt// 描述:Activity 层,UI 事件与渲染// =======================================================package com.example.webrtcdemoimport android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport com.example.webrtcdemo.databinding.ActivityMainBindingimport com.example.webrtcdemo.rtc.RTCClientimport com.example.webrtcdemo.signal.SignalClientimport org.webrtc.EglBaseclass MainActivity : AppCompatActivity() { private lateinit var b: ActivityMainBinding private lateinit var rtcClient: RTCClient private lateinit var signalClient: SignalClient private val eglBase = EglBase.create() override fun onCreate(s: Bundle?) { super.onCreate(s) b = ActivityMainBinding.inflate(layoutInflater) setContentView(b.root) // SurfaceViewRenderer 初始化 b.localView.init(eglBase.eglBaseContext, null) b.remoteView.init(eglBase.eglBaseContext, null) b.localView.setMirror(true) // 信令与 RTCClient signalClient = SignalClient(\"wss://your.signaling.server\", object: SignalClient.Listener { override fun onOffer(sdp: String) { rtcClient.onRemoteOffer(sdp) } override fun onAnswer(sdp: String){ rtcClient.onRemoteAnswer(sdp) } override fun onIceCandidate(mid: String, idx: Int, cand: String){ rtcClient.onRemoteIceCandidate(mid, idx, cand) } }) rtcClient = RTCClient(this, eglBase, signalClient) rtcClient.onRemoteStream = { track -> runOnUiThread { track.addSink(b.remoteView) } } // 本地流渲染 rtcClient.localVideoTrack.addSink(b.localView) b.btnCall.setOnClickListener { rtcClient.startCall() } b.btnHangup.setOnClickListener { signalClient.close(); finish() } b.btnMute.setOnCheckedChangeListener { _, isChecked -> rtcClient.localAudioTrack.setEnabled(!isChecked) } b.btnSwitch.setOnCheckedChangeListener { _, _ -> rtcClient.switchCamera() } }}
六、代码解读
-
SignalClient
-
基于 OkHttp WebSocket 实现信令交换,消息格式为 JSON,封装了 Offer/Answer/IceCandidate 三种类型。
-
-
RTCClient
-
初始化
PeerConnectionFactory
并创建本地视频轨道VideoCapturer
、VideoTrack
、AudioTrack
; -
创建
PeerConnection
并添加冰候选与媒体流; -
提供
startCall()
、onRemoteOffer()
、onRemoteAnswer()
、onRemoteIceCandidate()
四个方法完成完整协商; -
PeerConnection.Observer
回调中将远端流通过接口回调给 Activity;
-
-
MainActivity
-
SurfaceViewRenderer
在 UI 线程初始化并绑定 EglBase; -
按钮事件:呼叫、挂断、静音、切换摄像头;
-
本地轨道和远端轨道分别渲染到对应
SurfaceViewRenderer
;
-
-
摄像头捕获
-
使用
Camera2Enumerator
枚举、优先选取前置摄像头; -
通过
SurfaceTextureHelper
管理采集线程;
-
-
切换摄像头
-
在
RTCClient
中调用videoCapturer.switchCamera()
重建轨道并替换源;
-
七、性能与优化
-
网络适应:在
PeerConnection
创建约束时添加googCpuOveruseDetection
等字段,自动调整码率与分辨率; -
带宽管理:使用
setParameters()
动态调整视频编码码率; -
ICE 策略:自建 TURN 服务器,补齐 NAT 穿透;
-
日志与统计:注册
StatsObserver
,定期调用getStats()
获取丢包率、往返时延; -
资源释放:Activity 销毁前调用
eglBase.release()
、peerConnection.close()
、factory.dispose()
;
八、项目总结与扩展思路
本文手把手演示了如何在 Android 上用原生 WebRTC API 实现两端点点对点音视频通话,涵盖信令、协商、编解码与渲染全流程,并封装成可复用的模块。接下来可进一步:
-
多方通话(Mesh/SFU):使用媒体服务器(Medooze、Janus)管理多路流;
-
数据通道:利用 WebRTC DataChannel 实现文本消息或文件传输;
-
自定义编解码:切换到 H.265、VP9 或 AV1;
-
再包装成 SDK:封装成独立库,提供更高层 API;
九、FAQ
-
信令服务器如何部署?
-
最简单可用 Node.js +
ws
库,示例请参考官方 samples;
-
-
如何支持前台推送 TURN?
-
在 ICE 配置中添加 TURN 服务器信息并传入凭证;
-
-
为什么会出现黑屏?
-
确认
addSink()
时机,必须在SurfaceViewRenderer.init()
之后;
-
-
音视频不同步?
-
检查采集帧率与编码帧率是否一致,开启
Factory.Options.enableInternalTracer
分析;
-
-
如何打包 SO?
-
已在 Gradle 中指定
packagingOptions
,确保 .so 被正确打入 APK;
-