最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

使用Android實(shí)現(xiàn)實(shí)時(shí)視頻通話(附源碼)

 更新時(shí)間:2025年04月29日 09:02:53   作者:Katie。  
在移動(dòng)互聯(lián)網(wǎng)時(shí)代,實(shí)時(shí)視頻通話已成為社交,協(xié)作,教育,醫(yī)療等多種場(chǎng)景的標(biāo)配功能,本項(xiàng)目將從零搭建一個(gè)基于?WebRTC?的?Android?視頻通話程序,希望對(duì)大家有一定的幫助

一、項(xiàng)目介紹

在移動(dòng)互聯(lián)網(wǎng)時(shí)代,實(shí)時(shí)視頻通話已成為社交、協(xié)作、教育、醫(yī)療等多種場(chǎng)景的標(biāo)配功能。要實(shí)現(xiàn)一個(gè)高質(zhì)量的 Android 視頻通話功能,需要解決視頻采集、編解碼、網(wǎng)絡(luò)傳輸、信令協(xié)商、回聲消除、網(wǎng)絡(luò)抖動(dòng)控制等多方面難點(diǎn)。本項(xiàng)目將從零搭建一個(gè)基于 WebRTC 的 Android 視頻通話示例,具備以下能力:

  • 雙端互通:Android ↔ Android、Android ↔ Web(或 iOS)
  • 視頻采集與渲染:使用 Camera2 API + OpenGL 渲染本地圖像
  • 音頻處理:自動(dòng)回聲消除(AEC)、自動(dòng)增益控制(AGC)、噪聲抑制(NS)
  • 網(wǎng)絡(luò)傳輸:基于 UDP 的 SRTP 加密通道,支持 STUN/TURN 穿透
  • 信令交換:WebSocket 實(shí)現(xiàn) SDP 協(xié)商與 ICE 候選交換
  • 自適應(yīng)網(wǎng)絡(luò):實(shí)時(shí)監(jiān)測(cè)丟包率、往返時(shí)延,動(dòng)態(tài)調(diào)整發(fā)送分辨率與碼率
  • 可選第三方集成:對(duì)接 Agora、騰訊云 TRTC、阿里云 RTC 等商用 SDK

二、相關(guān)知識(shí)

1.WebRTC 概覽

  • PeerConnection:核心接口,負(fù)責(zé) SDP 協(xié)商、ICE 連接、SRTP 加解密
  • MediaStream:管理一組音視頻軌道(VideoTrack、AudioTrack)
  • SurfaceViewRenderer / GLSurfaceView:視頻渲染控件

2.視頻采集

  • Camera2 API:支持高分辨率、手動(dòng)對(duì)焦,但回調(diào)復(fù)雜
  • WebRTC’s CameraCapturer:封裝了舊 Camera API 與 Camera2,支持前后攝切換

3.音頻處理

  • WebRTC 內(nèi)置 AEC、AGC、NS,無(wú)需額外集成
  • 可通過(guò) AudioProcessing 接口調(diào)節(jié)參數(shù)

4.信令與 NAT 穿透

  • SDP Offer/Answer:描述音視頻能力與網(wǎng)絡(luò)參數(shù)
  • ICE Candidate:傳輸候選地址,實(shí)現(xiàn) P2P 連接
  • STUN/TURN:開(kāi)啟 IceServer,解決私網(wǎng)直連問(wèn)題

5.網(wǎng)絡(luò)自適應(yīng)

  • 通過(guò) BitrateObserver 與 ConnectionStateChange 回調(diào)監(jiān)測(cè)網(wǎng)絡(luò)狀況
  • 實(shí)時(shí)調(diào)整 VideoEncoder 的目標(biāo)碼率與分辨率

6.第三方 SDK 對(duì)比

  • Agora/騰訊云/阿里云:提供更高層封裝,內(nèi)置信令與跨平臺(tái)適配
  • WebRTC 原生:免費(fèi)、可深度定制,但需自行搭建信令與 TURN 服務(wù)

三、實(shí)現(xiàn)思路

1.集成 WebRTC Native

  • 在 settings.gradle 中添加 webrtc 源碼或使用編譯好的 AAR
  • 初始化 PeerConnectionFactory,啟用硬件編碼/解碼

2.UI 設(shè)計(jì)

  • 兩個(gè) SurfaceViewRenderer:本地預(yù)覽與遠(yuǎn)端畫(huà)面
  • 控制按鈕:發(fā)起呼叫、掛斷、切換攝像頭、靜音、鏡像開(kāi)關(guān)

3.信令模塊

  • 使用 WebSocket 與信令服務(wù)器通信
  • 定義簡(jiǎn)單協(xié)議:{"type":"offer","sdp":...}、{"type":"answer",...}、{"type":"candidate",...}

4.P2P 連接流程

  • A 端點(diǎn)擊“呼叫”→創(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() 添加視頻與音頻軌道
  • 遠(yuǎn)端軌道通過(guò) RemoteVideoTrack.addSink(remoteRenderer) 渲染

6.網(wǎng)絡(luò)優(yōu)化

  • 在 onAddTrack 中設(shè)置 AdaptiveVideoTrackSource 監(jiān)聽(tīng)網(wǎng)絡(luò)帶寬
  • 動(dòng)態(tài)調(diào)用 peerConnection.getSenders().find { it.track is VideoTrack }.setParameters()

7.服務(wù)端搭建

  • Node.js + ws 庫(kù)實(shí)現(xiàn)信令轉(zhuǎn)發(fā)
  • STUN:stun:stun.l.google.com:19302;TURN:自行部署或租用

四、環(huán)境與依賴(lài)

// app/build.gradle
plugins {
  id 'com.android.application'
  id 'kotlin-android'
}
 
android {
  compileSdkVersion 34
  defaultConfig {
    applicationId "com.example.videocall"
    minSdkVersion 21
    targetSdkVersion 34
    // 需啟用對(duì)攝像頭、麥克風(fēng)權(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
// 描述: 攝像頭與麥克風(fēng)權(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
// 描述: 本地與遠(yuǎn)端畫(huà)面 + 控制按鈕
// =======================================================
<?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">
 
  <!-- 遠(yuǎn)端畫(huà)面 -->
  <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 信令客戶(hù)端
// =======================================================
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)限申請(qǐng)
    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
// 描述: 簡(jiǎn)化版 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)限申請(qǐng)

動(dòng)態(tài)獲取攝像頭與麥克風(fēng)權(quán)限,授權(quán)后再初始化 WebRTC。

2.PeerConnectionFactory

  • PeerConnectionFactory.initialize 配置全局環(huán)境;
  • createPeerConnectionFactory 生成工廠,負(fù)責(zé)音視頻源與底層網(wǎng)絡(luò)棧。

3.本地采集與渲染

  • 使用 Camera2Enumerator 建議先試舊 API 擴(kuò)展兼容;
  • SurfaceViewRenderer.init 必須在 EGLContext 已創(chuàng)建后執(zhí)行;
  • VideoCapturer.startCapture 啟動(dòng)實(shí)時(shí)采集并推送給 VideoSource。

4.信令交互

  • 簡(jiǎn)單的 JSON 協(xié)議,WebSocket 單一通道,適合小規(guī)模 Demo;
  • 生產(chǎn)環(huán)境推薦加入鑒權(quán)、重連、消息隊(duì)列等穩(wěn)定性設(shè)計(jì)。

5.P2P 與 NAT 穿透

  • 僅 STUN 無(wú)法解決對(duì)等雙方均在內(nèi)網(wǎng)的場(chǎng)景,需要 TURN 服務(wù)器轉(zhuǎn)發(fā)流量;
  • rtcConfig 中可添加多個(gè) IceServer。

6.通話控制

  • “呼叫”建立 PeerConnection 并創(chuàng)建 Offer;
  • “掛斷”需同時(shí)關(guān)閉 PeerConnection、信令通道,并釋放本地資源。

七、性能與優(yōu)化

1.硬件編碼/解碼

WebRTC 默認(rèn)開(kāi)啟硬編硬解,可在 PeerConnectionFactory 構(gòu)建時(shí)通過(guò)選項(xiàng)調(diào)整。

2.自適應(yīng)碼率

監(jiān)聽(tīng) StatsObserver 中的 googAvailableSendBandwidth,動(dòng)態(tài)調(diào)用

val parameters = sender.parameters
parameters.encodings[0].maxBitrateBps = newRate
sender.parameters = parameters

3.多路視頻

可同時(shí)拉取多路流(如屏幕共享 + 攝像頭),需創(chuàng)建多個(gè) RtpSender。

4.回聲消除與音量平衡

使用 WebRTC 默認(rèn) AEC、AGC;對(duì)特殊場(chǎng)景可開(kāi)啟軟件回聲消除器。

5.流量加密

SRTP 默認(rèn)開(kāi)啟;如需更高安全,可在 UDP 之上再套 TLS 隧道。

八、項(xiàng)目總結(jié)與拓展

本文通過(guò)原生 WebRTC示例,完整演示了 Android 實(shí)現(xiàn)實(shí)時(shí)視頻通話的全部流程:從權(quán)限、工廠初始化、攝像頭采集、信令交互到 P2P 建連和動(dòng)態(tài) 網(wǎng)絡(luò)優(yōu)化。你可以進(jìn)一步擴(kuò)展:

屏幕共享:通過(guò) VideoCapturerAndroid.createScreenCapturer() 或 MediaProjection 接口,實(shí)現(xiàn)應(yīng)用內(nèi)屏幕推流

多人通話:引入多路混流或 SFU(如 Janus、Jitsi、MediaSoup)

可視化統(tǒng)計(jì):UI 上展示丟包率、幀率、往返時(shí)延、碼率曲線

第三方 SDK 對(duì)接:將 WebRTC 與 Agora/騰訊 TRTC 結(jié)合,支持更完善的商用功能

Compose 重構(gòu):將渲染視圖和控件切換到 Jetpack Compose

九、常見(jiàn)問(wèn)題

Q1:WebRTC AAR 如何集成?

A1:直接在 Gradle 中添加 implementation 'org.webrtc:google-webrtc:1.0.32006',無(wú)需自行編譯。

Q2:信令服務(wù)器能否用 Socket.io?

A2:可以,用 socket.io-client 與 Node.js 服務(wù)端互通;注意跨域與二進(jìn)制消息格式。

Q3:如何避免攝像頭沖突?

A3:在開(kāi)始采集前檢查 videoCapturer != null,并在 onDestroy 中調(diào)用 stopCapture() 和 dispose()。

Q4:視頻通話質(zhì)量差怎么辦?

A4:開(kāi)啟自適應(yīng)碼率、調(diào)整編碼分辨率,或增加 TURN 服務(wù)器數(shù)量降低丟包。

Q5:如何實(shí)現(xiàn)跨平臺(tái)互通?

A5:Web 端可使用 adapter.js,iOS 使用 WebRTC.framework,統(tǒng)一信令與 ICE 配置即可互通。

以上就是使用Android實(shí)現(xiàn)實(shí)時(shí)視頻通話(附源碼)的詳細(xì)內(nèi)容,更多關(guān)于Android視頻通話的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論

东港市| 普定县| 元谋县| 东宁县| 青州市| 井冈山市| 吉首市| 遂宁市| 巴南区| 铜陵市| 罗甸县| 黄梅县| 钟祥市| 昌黎县| 韩城市| 会泽县| 诏安县| 黔西| 米脂县| 安宁市| 长葛市| 连云港市| 恭城| 玛多县| 临江市| 富顺县| 尉犁县| 曲阜市| 宜良县| 曲周县| 达拉特旗| 鹤庆县| 曲周县| 招远市| 曲松县| 新沂市| 新蔡县| 防城港市| 台南市| 寿阳县| 桐庐县|