Android实现视频通话(附带源码)

一、项目介绍
在移动互联网时代,实时视频通话已成为社交、协作、教育、医疗等多种场景的标配功能。要实现一个高质量的 Android 视频通话功能,需要解决视频采集、编解码、网络传输、信令协商、回声消除、网络抖动控制等多方面难点。本项目将从零搭建一个基于 WebRTC 的 Android 视频通话示例,具备以下能力:
双端互通:Android ↔ Android、Android ↔ Web(或 iOS)
视频采集与渲染:使用 Camera2 API + OpenGL 渲染本地图像
音频处理:自动回声消除(AEC)、自动增益控制(AGC)、噪声抑制(NS)
网络传输:基于 UDP 的 SRTP 加密通道,支持 STUN/TURN 穿透
信令交换:WebSocket 实现 SDP 协商与 ICE 候选交换
自适应网络:实时监测丢包率、往返时延,动态调整发送分辨率与码率
可选第三方集成:对接 Agora、腾讯云 TRTC、阿里云 RTC 等商用 SDK
二、相关知识
WebRTC 概览
PeerConnection:核心接口,负责 SDP 协商、ICE 连接、SRTP 加解密
MediaStream:管理一组音视频轨道(VideoTrack、AudioTrack)
SurfaceViewRenderer / GLSurfaceView:视频渲染控件
视频采集
Camera2 API:支持高分辨率、手动对焦,但回调复杂
WebRTC’s CameraCapturer:封装了旧 Camera API 与 Camera2,支持前后摄切换
音频处理
WebRTC 内置 AEC、AGC、NS,无需额外集成
可通过 AudioProcessing 接口调节参数
信令与 NAT 穿透
SDP Offer/Answer:描述音视频能力与网络参数
ICE Candidate:传输候选地址,实现 P2P 连接
STUN/TURN:开启 IceServer,解决私网直连问题
网络自适应
通过 BitrateObserver 与 ConnectionStateChange 回调监测网络状况
实时调整 VideoEncoder 的目标码率与分辨率
第三方 SDK 对比
Agora/腾讯云/阿里云:提供更高层封装,内置信令与跨平台适配
WebRTC 原生:免费、可深度定制,但需自行搭建信令与 TURN 服务
三、实现思路
集成 WebRTC Native
在 settings.gradle 中添加 webrtc 源码或使用编译好的 AAR
初始化 PeerConnectionFactory,启用硬件编码/解码
UI 设计
两个 SurfaceViewRenderer:本地预览与远端画面
控制按钮:发起呼叫、挂断、切换摄像头、静音、镜像开关
信令模块
使用 WebSocket 与信令服务器通信
定义简单协议:{"type":"offer","sdp":...}、{"type":"answer",...}、{"type":"candidate",...}
P2P 连接流程
A 端点击“呼叫”→创建 offer → 发送给 B 端
B 端收到 → 设置 remoteDesc → 创建 answer → 发送给 A
双方相互交换 ICE candidate → 触发 onIceConnectionChange = CONNECTED
音视频采集与渲染
使用 Camera2Enumerator 初始化 VideoCapturer,创建 VideoSource
peerConnection.addTrack() 添加视频与音频轨道
远端轨道通过 RemoteVideoTrack.addSink(remoteRenderer) 渲染
网络优化
在 onAddTrack 中设置 AdaptiveVideoTrackSource 监听网络带宽
动态调用 peerConnection.getSenders().find { it.track is VideoTrack }.setParameters()
服务端搭建
Node.js + ws 库实现信令转发
STUN:stun:stun.l.google.com:19302;TURN:自行部署或租用
四、环境与依赖
// app/build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.videocall"
minSdkVersion 21
targetSdkVersion 34
// 需启用对摄像头、麦克风权限
}
buildFeatures { viewBinding true }
kotlinOptions { jvmTarget = "1.8" }
}
dependencies {
implementation 'org.webrtc:google-webrtc:1.0.32006' // 官方 AAR
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'com.squareup.okhttp3:okhttp:4.10.0' // WebSocket
}
五、整合代码
// =======================================================
// 文件: AndroidManifest.xml
// 描述: 摄像头与麦克风权限
// =======================================================
package="com.example.videocall"> android:theme="@style/Theme.AppCompat.NoActionBar" android:exported="true">
// =======================================================
// 文件: res/layout/activity_main.xml
// 描述: 本地与远端画面 + 控制按钮
// =======================================================
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> android:id="@+id/remoteView" android:layout_width="match_parent" android:layout_height="match_parent"/> android:id="@+id/localView" android:layout_width="120dp" android:layout_height="160dp" android:layout_margin="16dp" android:layout_gravity="top|end"/> android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom|center" android:gravity="center" android:padding="16dp">
// =======================================================
// 文件: SignalingClient.kt
// 描述: WebSocket 信令客户端
// =======================================================
package com.example.videocall
import kotlinx.coroutines.*
import okhttp3.*
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class SignalingClient(
private val serverUrl: String,
private val listener: Listener
) : WebSocketListener() {
interface Listener {
fun onOffer(sdp: String)
fun onAnswer(sdp: String)
fun onCandidate(sdpMid: String, sdpMLineIndex: Int, candidate: String)
}
private val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.build()
private var ws: WebSocket? = null
fun connect() {
val req = Request.Builder().url(serverUrl).build()
ws = client.newWebSocket(req, this)
}
fun close() { ws?.close(1000, "bye") }
fun sendOffer(sdp: String) {
val obj = JSONObject().apply {
put("type", "offer"); put("sdp", sdp)
}
ws?.send(obj.toString())
}
fun sendAnswer(sdp: String) {
val obj = JSONObject().apply {
put("type", "answer"); put("sdp", sdp)
}
ws?.send(obj.toString())
}
fun sendCandidate(c: PeerConnection.IceCandidate) {
val obj = JSONObject().apply {
put("type", "candidate")
put("sdpMid", c.sdpMid); put("sdpMLineIndex", c.sdpMLineIndex)
put("candidate", c.sdp)
}
ws?.send(obj.toString())
}
override fun onMessage(webSocket: WebSocket, text: String) {
val obj = JSONObject(text)
when (obj.getString("type")) {
"offer" -> listener.onOffer(obj.getString("sdp"))
"answer"-> listener.onAnswer(obj.getString("sdp"))
"candidate"-> listener.onCandidate(
obj.getString("sdpMid"), obj.getInt("sdpMLineIndex"),
obj.getString("candidate")
)
}
}
}
// =======================================================
// 文件: MainActivity.kt
// 描述: 核心视频通话逻辑
// =======================================================
package com.example.videocall
import android.Manifest
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import com.example.videocall.databinding.ActivityMainBinding
import kotlinx.coroutines.*
import org.webrtc.*
class MainActivity : AppCompatActivity(), SignalingClient.Listener {
private lateinit var binding: ActivityMainBinding
// WebRTC
private lateinit var peerFactory: PeerConnectionFactory
private var peerConnection: PeerConnection? = null
private lateinit var localVideoSource: VideoSource
private lateinit var localAudioSource: AudioSource
private lateinit var localVideoTrack: VideoTrack
private lateinit var localAudioTrack: AudioTrack
private lateinit var videoCapturer: VideoCapturer
private lateinit var signalingClient: SignalingClient
private val coroutineScope = CoroutineScope(Dispatchers.Main)
override fun onCreate(s: Bundle?) {
super.onCreate(s)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 1. 权限申请
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), 1)
// 2. 初始化 PeerConnectionFactory
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(this)
.createInitializationOptions()
)
peerFactory = PeerConnectionFactory.builder().createPeerConnectionFactory()
// 3. 初始化本地采集与渲染
initLocalMedia()
// 4. 初始化信令
signalingClient = SignalingClient("wss://your.signaling.server", this)
signalingClient.connect()
// 5. 按钮事件
binding.btnCall.setOnClickListener { startCall() }
binding.btnHangup.setOnClickListener { hangUp() }
binding.btnSwitch.setOnClickListener { switchCamera() }
}
private fun initLocalMedia() {
// SurfaceViewRenderer 初始化
binding.localView.init(EglBase.create().eglBaseContext, null)
binding.remoteView.init(EglBase.create().eglBaseContext, null)
// 摄像头捕获
val enumerator = Camera2Enumerator(this)
val camName = enumerator.deviceNames[0]
videoCapturer = enumerator.createCapturer(camName, null)
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread",
EglBase.create().eglBaseContext)
localVideoSource = peerFactory.createVideoSource(videoCapturer.isScreencast)
videoCapturer.initialize(surfaceTextureHelper, this, localVideoSource.capturerObserver)
videoCapturer.startCapture(1280, 720, 30)
localVideoTrack = peerFactory.createVideoTrack("ARDAMSv0", localVideoSource)
localVideoTrack.addSink(binding.localView)
localAudioSource = peerFactory.createAudioSource(MediaConstraints())
localAudioTrack = peerFactory.createAudioTrack("ARDAMSa0", localAudioSource)
}
private fun createPeerConnection() {
val iceServers = listOf(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
)
val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
}
peerConnection = peerFactory.createPeerConnection(rtcConfig, object : PeerConnection.Observer {
override fun onIceCandidate(c: IceCandidate) {
signalingClient.sendCandidate(c)
}
override fun onAddStream(stream: MediaStream) {
runOnUiThread {
stream.videoTracks[0].addSink(binding.remoteView)
}
}
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) {
Log.d("PC", "State = $newState")
}
// 省略其他回调
override fun onIceConnectionChange(state: PeerConnection.IceConnectionState) {}
override fun onIceGatheringChange(state: PeerConnection.IceGatheringState) {}
override fun onSignalingChange(state: PeerConnection.SignalingState) {}
override fun onIceCandidatesRemoved(candidates: Array
override fun onRemoveStream(stream: MediaStream?) {}
override fun onDataChannel(dc: DataChannel?) {}
override fun onRenegotiationNeeded() {}
override fun onTrack(transceiver: RtpTransceiver?) {}
})
// 添加音视频轨道
peerConnection?.addTrack(localVideoTrack)
peerConnection?.addTrack(localAudioTrack)
}
private fun startCall() {
createPeerConnection()
peerConnection?.createOffer(object : SdpObserver {
override fun onCreateSuccess(desc: SessionDescription) {
peerConnection?.setLocalDescription(this, desc)
signalingClient.sendOffer(desc.description)
}
override fun onSetSuccess() {}
override fun onCreateFailure(e: String) { }
override fun onSetFailure(e: String) { }
}, MediaConstraints())
}
private fun hangUp() {
peerConnection?.close(); peerConnection = null
signalingClient.close()
}
private fun switchCamera() {
(videoCapturer as CameraVideoCapturer).switchCamera(null)
}
// ===== SignalingClient.Listener 回调 =====
override fun onOffer(sdp: String) {
if (peerConnection == null) createPeerConnection()
val offer = SessionDescription(SessionDescription.Type.OFFER, sdp)
peerConnection?.setRemoteDescription(object: SdpObserver {
override fun onSetSuccess() {
peerConnection?.createAnswer(object : SdpObserver {
override fun onCreateSuccess(desc: SessionDescription) {
peerConnection?.setLocalDescription(this, desc)
signalingClient.sendAnswer(desc.description)
}
override fun onSetSuccess() {}
override fun onCreateFailure(e: String) {}
override fun onSetFailure(e: String) {}
}, MediaConstraints())
}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
override fun onSetFailure(p0: String?) {}
}, offer)
}
override fun onAnswer(sdp: String) {
val answer = SessionDescription(SessionDescription.Type.ANSWER, sdp)
peerConnection?.setRemoteDescription(object: SdpObserver {
override fun onSetSuccess() {}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
override fun onSetFailure(p0: String?) {}
}, answer)
}
override fun onCandidate(sdpMid: String, sdpMLineIndex: Int, cand: String) {
val candidate = IceCandidate(sdpMid, sdpMLineIndex, cand)
peerConnection?.addIceCandidate(candidate)
}
}
// =======================================================
// 文件: SdpObserver.kt
// 描述: 简化版 SdpObserver
// =======================================================
package com.example.videocall
import org.webrtc.SdpObserver
import org.webrtc.SessionDescription
abstract class SimpleSdpObserver : SdpObserver {
override fun onCreateSuccess(desc: SessionDescription?) {}
override fun onSetSuccess() {}
override fun onCreateFailure(error: String?) {}
override fun onSetFailure(error: String?) {}
}
六、代码解读
权限申请
动态获取摄像头与麦克风权限,授权后再初始化 WebRTC。
PeerConnectionFactory
PeerConnectionFactory.initialize 配置全局环境;
createPeerConnectionFactory 生成工厂,负责音视频源与底层网络栈。
本地采集与渲染
使用 Camera2Enumerator 建议先试旧 API 扩展兼容;
SurfaceViewRenderer.init 必须在 EGLContext 已创建后执行;
VideoCapturer.startCapture 启动实时采集并推送给 VideoSource。
信令交互
简单的 JSON 协议,WebSocket 单一通道,适合小规模 Demo;
生产环境推荐加入鉴权、重连、消息队列等稳定性设计。
P2P 与 NAT 穿透
仅 STUN 无法解决对等双方均在内网的场景,需要 TURN 服务器转发流量;
rtcConfig 中可添加多个 IceServer。
通话控制
“呼叫”建立 PeerConnection 并创建 Offer;
“挂断”需同时关闭 PeerConnection、信令通道,并释放本地资源。
七、性能与优化
硬件编码/解码
WebRTC 默认开启硬编硬解,可在 PeerConnectionFactory 构建时通过选项调整。
自适应码率
监听 StatsObserver 中的 googAvailableSendBandwidth,动态调用
val parameters = sender.parameters
parameters.encodings[0].maxBitrateBps = newRate
sender.parameters = parameters
多路视频
可同时拉取多路流(如屏幕共享 + 摄像头),需创建多个 RtpSender。
回声消除与音量平衡
使用 WebRTC 默认 AEC、AGC;对特殊场景可开启软件回声消除器。
流量加密
SRTP 默认开启;如需更高安全,可在 UDP 之上再套 TLS 隧道。
八、项目总结与拓展
本文通过原生 WebRTC示例,完整演示了 Android 实现实时视频通话的全部流程:从权限、工厂初始化、摄像头采集、信令交互到 P2P 建连和动态网络优化。你可以进一步扩展:
屏幕共享:通过 VideoCapturerAndroid.createScreenCapturer() 或 MediaProjection 接口,实现应用内屏幕推流
多人通话:引入多路混流或 SFU(如 Janus、Jitsi、MediaSoup)
可视化统计:UI 上展示丢包率、帧率、往返时延、码率曲线
第三方 SDK 对接:将 WebRTC 与 Agora/腾讯 TRTC 结合,支持更完善的商用功能
Compose 重构:将渲染视图和控件切换到 Jetpack Compose
九、FAQ
Q1:WebRTC AAR 如何集成?
A1:直接在 Gradle 中添加 implementation 'org.webrtc:google-webrtc:1.0.32006',无需自行编译。
Q2:信令服务器能否用 Socket.io?
A2:可以,用 socket.io-client 与 Node.js 服务端互通;注意跨域与二进制消息格式。
Q3:如何避免摄像头冲突?
A3:在开始采集前检查 videoCapturer != null,并在 onDestroy 中调用 stopCapture() 和 dispose()。
Q4:视频通话质量差怎么办?
A4:开启自适应码率、调整编码分辨率,或增加 TURN 服务器数量降低丢包。
Q5:如何实现跨平台互通?
A5:Web 端可使用 adapter.js,iOS 使用 WebRTC.framework,统一信令与 ICE 配置即可互通。