使用Android实现实时视频通话(附源码)

 更新时间:2025年04月29日 09:02:53   作者:Katie。  
在移动互联网时代,实时视频通话已成为社交,协作,教育,医疗等多种场景的标配功能,本项目将从零搭建一个基于 WebRTC 的 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

二、相关知识

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视频通话的资料请关注脚本之家其它相关文章!

相关文章

  • Android webview用法实例简析

    Android webview用法实例简析

    这篇文章主要介绍了Android webview用法,结合实例形式简单分析了Android中webview的功能、用法与相关注意事项,需要的朋友可以参考下
    2016-01-01
  • Android Studio将程序打包成APK的步骤详解

    Android Studio将程序打包成APK的步骤详解

    这篇文章主要介绍了Android Studio将程序打包成APK的步骤详解,本文分步骤给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-05-05
  • Android 实现沉浸式状态栏的方法

    Android 实现沉浸式状态栏的方法

    沉浸式状态栏的来源就是很多手机用的是实体按键,没有虚拟键,于是开了沉浸模式就只有状态栏消失了。下面脚本之家小编给大家介绍Android 实现沉浸式状态栏,需要的朋友可以参考下
    2015-09-09
  • android 仿微信demo——注册功能实现(移动端)

    android 仿微信demo——注册功能实现(移动端)

    本篇文章主要介绍了微信小程序-阅读小程序实例(demo),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧,希望能给你们提供帮助
    2021-06-06
  • Android滑动事件冲突详解(一)

    Android滑动事件冲突详解(一)

    这篇文章主要为大家详细介绍了Android滑动事件冲突,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-03-03
  • Android实现高亮搜索功能的示例

    Android实现高亮搜索功能的示例

    这篇文章主要介绍了Android实现高亮搜索功能的示例,帮助大家更好的理解和学习使用Android开发,感兴趣的朋友可以了解下
    2021-05-05
  • Android编程之界面跳动提示动画效果实现方法

    Android编程之界面跳动提示动画效果实现方法

    这篇文章主要介绍了Android编程之界面跳动提示动画效果实现方法,实例分析了Android动画效果的布局及功能相关实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-11-11
  • Android用于加载xml的LayoutInflater源码超详细分析

    Android用于加载xml的LayoutInflater源码超详细分析

    今天不想去聊一些Android的新功能,新特性之类的东西,特别想聊一聊这个老生常谈的话题:LayoutInflater,感兴趣的朋友来看看吧
    2022-08-08
  • Android Studio自定义万能注释模板与创建类,方法注释模板操作

    Android Studio自定义万能注释模板与创建类,方法注释模板操作

    这篇文章主要介绍了Android Studio自定义万能注释模板与创建类,方法注释模板操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-03-03
  • Android的activity学习笔记

    Android的activity学习笔记

    这篇文章主要整理了Android的activity学习笔记,总共有八大亮点,推荐给大家,需要的朋友可以参考下
    2015-09-09

最新评论