使用Android实现实时视频通话(附源码)
作者:Katie。
一、项目介绍
在移动互联网时代,实时视频通话已成为社交、协作、教育、医疗等多种场景的标配功能。要实现一个高质量的 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
二、相关知识
1.WebRTC 概览
- PeerConnection:核心接口,负责 SDP 协商、ICE 连接、SRTP 加解密
- MediaStream:管理一组音视频轨道(VideoTrack、AudioTrack)
- SurfaceViewRenderer / GLSurfaceView:视频渲染控件
2.视频采集
- Camera2 API:支持高分辨率、手动对焦,但回调复杂
- WebRTC’s CameraCapturer:封装了旧 Camera API 与 Camera2,支持前后摄切换
3.音频处理
- WebRTC 内置 AEC、AGC、NS,无需额外集成
- 可通过 AudioProcessing 接口调节参数
4.信令与 NAT 穿透
- SDP Offer/Answer:描述音视频能力与网络参数
- ICE Candidate:传输候选地址,实现 P2P 连接
- STUN/TURN:开启 IceServer,解决私网直连问题
5.网络自适应
- 通过 BitrateObserver 与 ConnectionStateChange 回调监测网络状况
- 实时调整 VideoEncoder 的目标码率与分辨率
6.第三方 SDK 对比
- Agora/腾讯云/阿里云:提供更高层封装,内置信令与跨平台适配
- WebRTC 原生:免费、可深度定制,但需自行搭建信令与 TURN 服务
三、实现思路
1.集成 WebRTC Native
- 在 settings.gradle 中添加 webrtc 源码或使用编译好的 AAR
- 初始化 PeerConnectionFactory,启用硬件编码/解码
2.UI 设计
- 两个 SurfaceViewRenderer:本地预览与远端画面
- 控制按钮:发起呼叫、挂断、切换摄像头、静音、镜像开关
3.信令模块
- 使用 WebSocket 与信令服务器通信
- 定义简单协议:{"type":"offer","sdp":...}、{"type":"answer",...}、{"type":"candidate",...}
4.P2P 连接流程
- A 端点击“呼叫”→创建 offer → 发送给 B 端
- B 端收到 → 设置 remoteDesc → 创建 answer → 发送给 A
- 双方相互交换 ICE candidate → 触发 onIceConnectionChange = CONNECTED
5.音视频采集与渲染
- 使用 Camera2Enumerator 初始化 VideoCapturer,创建 VideoSource
- peerConnection.addTrack() 添加视频与音频轨道
- 远端轨道通过 RemoteVideoTrack.addSink(remoteRenderer) 渲染
6.网络优化
- 在 onAddTrack 中设置 AdaptiveVideoTrackSource 监听网络带宽
- 动态调用 peerConnection.getSenders().find { it.track is VideoTrack }.setParameters()
7.服务端搭建
- 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 // 描述: 摄像头与麦克风权限 // ======================================================= <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.videocall"> <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.INTERNET"/> <application> <activity android:name=".MainActivity" android:theme="@style/Theme.AppCompat.NoActionBar" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> // ======================================================= // 文件: res/layout/activity_main.xml // 描述: 本地与远端画面 + 控制按钮 // ======================================================= <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 远端画面 --> <org.webrtc.SurfaceViewRenderer android:id="@+id/remoteView" android:layout_width="match_parent" android:layout_height="match_parent"/> <!-- 本地预览(右上角小窗口) --> <org.webrtc.SurfaceViewRenderer android:id="@+id/localView" android:layout_width="120dp" android:layout_height="160dp" android:layout_margin="16dp" android:layout_gravity="top|end"/> <!-- 按钮栏 --> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom|center" android:gravity="center" android:padding="16dp"> <Button android:id="@+id/btnCall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="呼叫"/> <Button android:id="@+id/btnHangup" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="挂断" android:layout_marginStart="16dp"/> <Button android:id="@+id/btnSwitch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="切换摄像头" android:layout_marginStart="16dp"/> </LinearLayout> </FrameLayout> // ======================================================= // 文件: 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<out IceCandidate>?) {} 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?) {} }
六、代码解读
1.权限申请
动态获取摄像头与麦克风权限,授权后再初始化 WebRTC。
2.PeerConnectionFactory
- PeerConnectionFactory.initialize 配置全局环境;
- createPeerConnectionFactory 生成工厂,负责音视频源与底层网络栈。
3.本地采集与渲染
- 使用 Camera2Enumerator 建议先试旧 API 扩展兼容;
- SurfaceViewRenderer.init 必须在 EGLContext 已创建后执行;
- VideoCapturer.startCapture 启动实时采集并推送给 VideoSource。
4.信令交互
- 简单的 JSON 协议,WebSocket 单一通道,适合小规模 Demo;
- 生产环境推荐加入鉴权、重连、消息队列等稳定性设计。
5.P2P 与 NAT 穿透
- 仅 STUN 无法解决对等双方均在内网的场景,需要 TURN 服务器转发流量;
- rtcConfig 中可添加多个 IceServer。
6.通话控制
- “呼叫”建立 PeerConnection 并创建 Offer;
- “挂断”需同时关闭 PeerConnection、信令通道,并释放本地资源。
七、性能与优化
1.硬件编码/解码
WebRTC 默认开启硬编硬解,可在 PeerConnectionFactory 构建时通过选项调整。
2.自适应码率
监听 StatsObserver 中的 googAvailableSendBandwidth,动态调用
val parameters = sender.parameters parameters.encodings[0].maxBitrateBps = newRate sender.parameters = parameters
3.多路视频
可同时拉取多路流(如屏幕共享 + 摄像头),需创建多个 RtpSender。
4.回声消除与音量平衡
使用 WebRTC 默认 AEC、AGC;对特殊场景可开启软件回声消除器。
5.流量加密
SRTP 默认开启;如需更高安全,可在 UDP 之上再套 TLS 隧道。
八、项目总结与拓展
本文通过原生 WebRTC示例,完整演示了 Android 实现实时视频通话的全部流程:从权限、工厂初始化、摄像头采集、信令交互到 P2P 建连和动态 网络优化。你可以进一步扩展:
屏幕共享:通过 VideoCapturerAndroid.createScreenCapturer() 或 MediaProjection 接口,实现应用内屏幕推流
多人通话:引入多路混流或 SFU(如 Janus、Jitsi、MediaSoup)
可视化统计:UI 上展示丢包率、帧率、往返时延、码率曲线
第三方 SDK 对接:将 WebRTC 与 Agora/腾讯 TRTC 结合,支持更完善的商用功能
Compose 重构:将渲染视图和控件切换到 Jetpack Compose
九、常见问题
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 配置即可互通。
以上就是使用Android实现实时视频通话(附源码)的详细内容,更多关于Android视频通话的资料请关注脚本之家其它相关文章!