Android

关注公众号 jb51net

关闭
首页 > 软件编程 > Android > Android视频通话

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

作者:Katie。

在移动互联网时代,实时视频通话已成为社交,协作,教育,医疗等多种场景的标配功能,本项目将从零搭建一个基于 WebRTC 的 Android 视频通话程序,希望对大家有一定的帮助

一、项目介绍

在移动互联网时代,实时视频通话已成为社交、协作、教育、医疗等多种场景的标配功能。要实现一个高质量的 Android 视频通话功能,需要解决视频采集、编解码、网络传输、信令协商、回声消除、网络抖动控制等多方面难点。本项目将从零搭建一个基于 WebRTC 的 Android 视频通话示例,具备以下能力:

二、相关知识

1.WebRTC 概览

2.视频采集

3.音频处理

4.信令与 NAT 穿透

5.网络自适应

6.第三方 SDK 对比

三、实现思路

1.集成 WebRTC Native

2.UI 设计

3.信令模块

4.P2P 连接流程

5.音视频采集与渲染

6.网络优化

7.服务端搭建

四、环境与依赖

// 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

3.本地采集与渲染

4.信令交互

5.P2P 与 NAT 穿透

6.通话控制

七、性能与优化

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

您可能感兴趣的文章:
阅读全文