使用Android實現(xiàn)實時視頻通話(附源碼)
一、項目介紹
在移動互聯(lián)網(wǎng)時代,實時視頻通話已成為社交、協(xié)作、教育、醫(yī)療等多種場景的標配功能。要實現(xiàn)一個高質(zhì)量的 Android 視頻通話功能,需要解決視頻采集、編解碼、網(wǎng)絡(luò)傳輸、信令協(xié)商、回聲消除、網(wǎng)絡(luò)抖動控制等多方面難點。本項目將從零搭建一個基于 WebRTC 的 Android 視頻通話示例,具備以下能力:
- 雙端互通:Android ↔ Android、Android ↔ Web(或 iOS)
- 視頻采集與渲染:使用 Camera2 API + OpenGL 渲染本地圖像
- 音頻處理:自動回聲消除(AEC)、自動增益控制(AGC)、噪聲抑制(NS)
- 網(wǎng)絡(luò)傳輸:基于 UDP 的 SRTP 加密通道,支持 STUN/TURN 穿透
- 信令交換:WebSocket 實現(xiàn) SDP 協(xié)商與 ICE 候選交換
- 自適應(yīng)網(wǎng)絡(luò):實時監(jiān)測丟包率、往返時延,動態(tài)調(diào)整發(fā)送分辨率與碼率
- 可選第三方集成:對接 Agora、騰訊云 TRTC、阿里云 RTC 等商用 SDK
二、相關(guān)知識
1.WebRTC 概覽
- PeerConnection:核心接口,負責 SDP 協(xié)商、ICE 連接、SRTP 加解密
- MediaStream:管理一組音視頻軌道(VideoTrack、AudioTrack)
- SurfaceViewRenderer / GLSurfaceView:視頻渲染控件
2.視頻采集
- Camera2 API:支持高分辨率、手動對焦,但回調(diào)復雜
- WebRTC’s CameraCapturer:封裝了舊 Camera API 與 Camera2,支持前后攝切換
3.音頻處理
- WebRTC 內(nèi)置 AEC、AGC、NS,無需額外集成
- 可通過 AudioProcessing 接口調(diào)節(jié)參數(shù)
4.信令與 NAT 穿透
- SDP Offer/Answer:描述音視頻能力與網(wǎng)絡(luò)參數(shù)
- ICE Candidate:傳輸候選地址,實現(xiàn) P2P 連接
- STUN/TURN:開啟 IceServer,解決私網(wǎng)直連問題
5.網(wǎng)絡(luò)自適應(yīng)
- 通過 BitrateObserver 與 ConnectionStateChange 回調(diào)監(jiān)測網(wǎng)絡(luò)狀況
- 實時調(diào)整 VideoEncoder 的目標碼率與分辨率
6.第三方 SDK 對比
- Agora/騰訊云/阿里云:提供更高層封裝,內(nèi)置信令與跨平臺適配
- WebRTC 原生:免費、可深度定制,但需自行搭建信令與 TURN 服務(wù)
三、實現(xiàn)思路
1.集成 WebRTC Native
- 在 settings.gradle 中添加 webrtc 源碼或使用編譯好的 AAR
- 初始化 PeerConnectionFactory,啟用硬件編碼/解碼
2.UI 設(shè)計
- 兩個 SurfaceViewRenderer:本地預(yù)覽與遠端畫面
- 控制按鈕:發(fā)起呼叫、掛斷、切換攝像頭、靜音、鏡像開關(guān)
3.信令模塊
- 使用 WebSocket 與信令服務(wù)器通信
- 定義簡單協(xié)議:{"type":"offer","sdp":...}、{"type":"answer",...}、{"type":"candidate",...}
4.P2P 連接流程
- A 端點擊“呼叫”→創(chuàng)建 offer → 發(fā)送給 B 端
- B 端收到 → 設(shè)置 remoteDesc → 創(chuàng)建 answer → 發(fā)送給 A
- 雙方相互交換 ICE candidate → 觸發(fā) onIceConnectionChange = CONNECTED
5.音視頻采集與渲染
- 使用 Camera2Enumerator 初始化 VideoCapturer,創(chuàng)建 VideoSource
- peerConnection.addTrack() 添加視頻與音頻軌道
- 遠端軌道通過 RemoteVideoTrack.addSink(remoteRenderer) 渲染
6.網(wǎng)絡(luò)優(yōu)化
- 在 onAddTrack 中設(shè)置 AdaptiveVideoTrackSource 監(jiān)聽網(wǎng)絡(luò)帶寬
- 動態(tài)調(diào)用 peerConnection.getSenders().find { it.track is VideoTrack }.setParameters()
7.服務(wù)端搭建
- Node.js + ws 庫實現(xiàn)信令轉(zhuǎn)發(fā)
- STUN:stun:stun.l.google.com:19302;TURN:自行部署或租用
四、環(huán)境與依賴
// app/build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.videocall"
minSdkVersion 21
targetSdkVersion 34
// 需啟用對攝像頭、麥克風權(quán)限
}
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
// 描述: 攝像頭與麥克風權(quán)限
// =======================================================
<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"/>
<!-- 本地預(yù)覽(右上角小窗口) -->
<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. 權(quán)限申請
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")
}
// 省略其他回調(diào)
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 回調(diào) =====
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.權(quán)限申請
動態(tài)獲取攝像頭與麥克風權(quán)限,授權(quán)后再初始化 WebRTC。
2.PeerConnectionFactory
- PeerConnectionFactory.initialize 配置全局環(huán)境;
- createPeerConnectionFactory 生成工廠,負責音視頻源與底層網(wǎng)絡(luò)棧。
3.本地采集與渲染
- 使用 Camera2Enumerator 建議先試舊 API 擴展兼容;
- SurfaceViewRenderer.init 必須在 EGLContext 已創(chuàng)建后執(zhí)行;
- VideoCapturer.startCapture 啟動實時采集并推送給 VideoSource。
4.信令交互
- 簡單的 JSON 協(xié)議,WebSocket 單一通道,適合小規(guī)模 Demo;
- 生產(chǎn)環(huán)境推薦加入鑒權(quán)、重連、消息隊列等穩(wěn)定性設(shè)計。
5.P2P 與 NAT 穿透
- 僅 STUN 無法解決對等雙方均在內(nèi)網(wǎng)的場景,需要 TURN 服務(wù)器轉(zhuǎn)發(fā)流量;
- rtcConfig 中可添加多個 IceServer。
6.通話控制
- “呼叫”建立 PeerConnection 并創(chuàng)建 Offer;
- “掛斷”需同時關(guān)閉 PeerConnection、信令通道,并釋放本地資源。
七、性能與優(yōu)化
1.硬件編碼/解碼
WebRTC 默認開啟硬編硬解,可在 PeerConnectionFactory 構(gòu)建時通過選項調(diào)整。
2.自適應(yīng)碼率
監(jiān)聽 StatsObserver 中的 googAvailableSendBandwidth,動態(tài)調(diào)用
val parameters = sender.parameters parameters.encodings[0].maxBitrateBps = newRate sender.parameters = parameters
3.多路視頻
可同時拉取多路流(如屏幕共享 + 攝像頭),需創(chuàng)建多個 RtpSender。
4.回聲消除與音量平衡
使用 WebRTC 默認 AEC、AGC;對特殊場景可開啟軟件回聲消除器。
5.流量加密
SRTP 默認開啟;如需更高安全,可在 UDP 之上再套 TLS 隧道。
八、項目總結(jié)與拓展
本文通過原生 WebRTC示例,完整演示了 Android 實現(xiàn)實時視頻通話的全部流程:從權(quán)限、工廠初始化、攝像頭采集、信令交互到 P2P 建連和動態(tài) 網(wǎng)絡(luò)優(yōu)化。你可以進一步擴展:
屏幕共享:通過 VideoCapturerAndroid.createScreenCapturer() 或 MediaProjection 接口,實現(xiàn)應(yīng)用內(nèi)屏幕推流
多人通話:引入多路混流或 SFU(如 Janus、Jitsi、MediaSoup)
可視化統(tǒng)計:UI 上展示丟包率、幀率、往返時延、碼率曲線
第三方 SDK 對接:將 WebRTC 與 Agora/騰訊 TRTC 結(jié)合,支持更完善的商用功能
Compose 重構(gòu):將渲染視圖和控件切換到 Jetpack Compose
九、常見問題
Q1:WebRTC AAR 如何集成?
A1:直接在 Gradle 中添加 implementation 'org.webrtc:google-webrtc:1.0.32006',無需自行編譯。
Q2:信令服務(wù)器能否用 Socket.io?
A2:可以,用 socket.io-client 與 Node.js 服務(wù)端互通;注意跨域與二進制消息格式。
Q3:如何避免攝像頭沖突?
A3:在開始采集前檢查 videoCapturer != null,并在 onDestroy 中調(diào)用 stopCapture() 和 dispose()。
Q4:視頻通話質(zhì)量差怎么辦?
A4:開啟自適應(yīng)碼率、調(diào)整編碼分辨率,或增加 TURN 服務(wù)器數(shù)量降低丟包。
Q5:如何實現(xiàn)跨平臺互通?
A5:Web 端可使用 adapter.js,iOS 使用 WebRTC.framework,統(tǒng)一信令與 ICE 配置即可互通。
以上就是使用Android實現(xiàn)實時視頻通話(附源碼)的詳細內(nèi)容,更多關(guān)于Android視頻通話的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 實現(xiàn)沉浸式狀態(tài)欄的方法
沉浸式狀態(tài)欄的來源就是很多手機用的是實體按鍵,沒有虛擬鍵,于是開了沉浸模式就只有狀態(tài)欄消失了。下面腳本之家小編給大家介紹Android 實現(xiàn)沉浸式狀態(tài)欄,需要的朋友可以參考下2015-09-09
android 仿微信demo——注冊功能實現(xiàn)(移動端)
本篇文章主要介紹了微信小程序-閱讀小程序?qū)嵗╠emo),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望能給你們提供幫助2021-06-06
Android編程之界面跳動提示動畫效果實現(xiàn)方法
這篇文章主要介紹了Android編程之界面跳動提示動畫效果實現(xiàn)方法,實例分析了Android動畫效果的布局及功能相關(guān)實現(xiàn)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-11-11
Android用于加載xml的LayoutInflater源碼超詳細分析
今天不想去聊一些Android的新功能,新特性之類的東西,特別想聊一聊這個老生常談的話題:LayoutInflater,感興趣的朋友來看看吧2022-08-08
Android Studio自定義萬能注釋模板與創(chuàng)建類,方法注釋模板操作
這篇文章主要介紹了Android Studio自定義萬能注釋模板與創(chuàng)建類,方法注釋模板操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03

