跳到主要内容WebRTC 在 Android 中的应用实战指南 | 极客日志Kotlin大前端java
WebRTC 在 Android 中的应用实战指南
WebRTC 架构原理与 Android SDK 集成实践,涵盖音视频采集渲染、信令交换及连接管理。通过智能安防案例展示低延迟通信实现,包含 PeerConnection 封装、WebSocket 信令客户端及性能优化策略,提供完整代码示例与最佳实践总结。
CryptoLab1 浏览 WebRTC 在 Android 中的应用实战
一、WebRTC 架构概览
1.1 WebRTC 核心组件
WebRTC 的核心在于其分层架构,从应用层 API 到底层网络传输,每个环节都经过优化以确保实时性。
核心模块说明:
| 模块 | 功能 | 关键技术 |
|---|
| PeerConnection | 端对端连接管理 | ICE, SDP |
| Audio Engine | 音频处理 | AEC, NS, AGC |
| Video Engine | 视频处理 | 编解码,Jitter Buffer |
| Transport | 网络传输 | RTP/RTCP, SRTP |
1.2 WebRTC 通信流程
建立 P2P 连接通常经历以下步骤:设备 A 创建 PeerConnection 并添加本地媒体流,生成 Offer 并通过信令服务器转发给设备 B;设备 B 接收 Offer 后设置远程描述并生成 Answer 返回;双方交换 ICE 候选信息,最终建立直连通道进行音视频数据传输。
二、Android WebRTC SDK 集成
2.1 构建环境准备
首先需要在 build.gradle 中添加必要的依赖。这里我们使用官方库配合 OkHttp 处理信令,协程管理异步任务。
dependencies {
implementation 'org.webrtc:google-webrtc:1.0.32006'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.google.code.gson:gson:2.10'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
}
同时记得在 AndroidManifest.xml 中声明网络和多媒体权限。
2.2 WebRTC 初始化
初始化是第一步,我们需要配置 PeerConnectionFactory。注意这里要开启硬件编解码支持,这对移动端性能至关重要。
class WebRtcManager(private val context: Context) {
peerConnectionFactory: PeerConnectionFactory? =
audioSource: AudioSource? =
videoSource: VideoSource? =
videoCapturer: VideoCapturer? =
{
TAG =
AUDIO_ECHO_CANCELLATION =
AUDIO_AUTO_GAIN_CONTROL =
AUDIO_HIGH_PASS_FILTER =
AUDIO_NOISE_SUPPRESSION =
}
{
initializationOptions = PeerConnectionFactory.InitializationOptions.builder(context)
.setEnableInternalTracer()
.setFieldTrials()
.createInitializationOptions()
PeerConnectionFactory.initialize(initializationOptions)
options = PeerConnectionFactory.Options().apply {
networkIgnoreMask =
}
encoderFactory = DefaultVideoEncoderFactory(
EglBase.create().eglBaseContext,
,
)
decoderFactory = DefaultVideoDecoderFactory(EglBase.create().eglBaseContext)
peerConnectionFactory = PeerConnectionFactory.builder()
.setOptions(options)
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory)
.createPeerConnectionFactory()
Log.i(TAG, )
}
: AudioSource {
(audioSource == ) {
audioConstraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair(AUDIO_ECHO_CANCELLATION, ))
mandatory.add(MediaConstraints.KeyValuePair(AUDIO_AUTO_GAIN_CONTROL, ))
mandatory.add(MediaConstraints.KeyValuePair(AUDIO_HIGH_PASS_FILTER, ))
mandatory.add(MediaConstraints.KeyValuePair(AUDIO_NOISE_SUPPRESSION, ))
}
audioSource = peerConnectionFactory?.createAudioSource(audioConstraints)
Log.i(TAG, )
}
audioSource!!
}
: VideoSource {
(videoSource == ) {
videoSource = peerConnectionFactory?.createVideoSource(isScreencast)
Log.i(TAG, )
}
videoSource!!
}
: AudioTrack {
audioSource = createAudioSource()
peerConnectionFactory!!.createAudioTrack(trackId, audioSource)
}
: VideoTrack {
videoSource = createVideoSource()
peerConnectionFactory!!.createVideoTrack(trackId, videoSource)
}
: PeerConnection? {
rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
enableDtlsSrtp =
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
}
peerConnectionFactory?.createPeerConnection(rtcConfig, observer)
}
: PeerConnectionFactory? = peerConnectionFactory
{
videoCapturer?.dispose()
videoSource?.dispose()
audioSource?.dispose()
peerConnectionFactory?.dispose()
videoCapturer =
videoSource =
audioSource =
peerConnectionFactory =
Log.i(TAG, )
}
}
private
var
null
private
var
null
private
var
null
private
var
null
companion
object
private
const
val
"WebRtcManager"
private
const
val
"googEchoCancellation"
private
const
val
"googAutoGainControl"
private
const
val
"googHighpassFilter"
private
const
val
"googNoiseSuppression"
fun initialize()
val
false
""
val
0
val
true
true
val
"WebRTC initialized successfully"
fun createAudioSource()
if
null
val
"true"
"true"
"true"
"true"
"Audio source created"
return
fun createVideoSource(isScreencast: Boolean = false)
if
null
"Video source created"
return
fun createAudioTrack(trackId: String = "audio_track")
val
return
fun createVideoTrack(trackId: String = "video_track")
val
return
fun createPeerConnection(
iceServers: List<PeerConnection.IceServer>,
observer: PeerConnection.Observer
)
val
true
return
fun getPeerConnectionFactory()
fun dispose()
null
null
null
null
"WebRTC resources disposed"
三、音视频采集与渲染
3.1 摄像头采集
采集器需要处理 Camera1 或 Camera2 的底层接口。这里优先尝试 Camera2,如果不可用则回退到 Camera1。切换摄像头时需要注意回调处理。
class CameraCapturerManager(
private val context: Context,
private val webRtcManager: WebRtcManager
) {
private var videoCapturer: CameraVideoCapturer? = null
private var surfaceTextureHelper: SurfaceTextureHelper? = null
companion object {
private const val TAG = "CameraCapturer"
private const val VIDEO_WIDTH = 1280
private const val VIDEO_HEIGHT = 720
private const val VIDEO_FPS = 30
}
fun initialize(): Boolean {
videoCapturer = createCameraVideoCapturer()
if (videoCapturer == null) {
Log.e(TAG, "Failed to create camera capturer")
return false
}
val eglBase = EglBase.create()
surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.eglBaseContext)
val videoSource = webRtcManager.createVideoSource()
videoCapturer?.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
Log.i(TAG, "Camera capturer initialized")
return true
}
private fun createCameraVideoCapturer(): CameraVideoCapturer? {
val enumerator = if (Camera2Enumerator.isSupported(context)) {
Camera2Enumerator(context)
} else {
Camera1Enumerator(true)
}
val deviceNames = enumerator.deviceNames
for (deviceName in deviceNames) {
if (enumerator.isFrontFacing(deviceName)) {
val capturer = enumerator.createCapturer(deviceName, null)
if (capturer != null) {
Log.i(TAG, "Using front camera: $deviceName")
return capturer
}
}
}
for (deviceName in deviceNames) {
if (enumerator.isBackFacing(deviceName)) {
val capturer = enumerator.createCapturer(deviceName, null)
if (capturer != null) {
Log.i(TAG, "Using back camera: $deviceName")
return capturer
}
}
}
return null
}
fun startCapture() {
videoCapturer?.startCapture(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS)
Log.i(TAG, "Camera capture started: ${VIDEO_WIDTH}x${VIDEO_HEIGHT}@${VIDEO_FPS}fps")
}
fun stopCapture() {
try {
videoCapturer?.stopCapture()
Log.i(TAG, "Camera capture stopped")
} catch (e: InterruptedException) {
Log.e(TAG, "Failed to stop capture", e)
}
}
fun switchCamera(switchHandler: CameraVideoCapturer.CameraSwitchHandler) {
if (videoCapturer is CameraVideoCapturer) {
(videoCapturer as CameraVideoCapturer).switchCamera(switchHandler)
}
}
fun dispose() {
videoCapturer?.dispose()
surfaceTextureHelper?.dispose()
videoCapturer = null
surfaceTextureHelper = null
Log.i(TAG, "Camera capturer disposed")
}
}
3.2 视频渲染
渲染部分主要涉及 SurfaceViewRenderer 的配置。本地视频通常需要镜像显示,而远端视频保持原样。注意 EGL 上下文的共享。
class VideoRendererManager(private val context: Context) {
private var localRenderer: SurfaceViewRenderer? = null
private var remoteRenderer: SurfaceViewRenderer? = null
private val eglBase = EglBase.create()
companion object {
private const val TAG = "VideoRenderer"
}
fun initializeLocalRenderer(surfaceView: SurfaceViewRenderer): SurfaceViewRenderer {
surfaceView.init(eglBase.eglBaseContext, null)
surfaceView.setMirror(true)
surfaceView.setEnableHardwareScaler(true)
surfaceView.setZOrderMediaOverlay(true)
localRenderer = surfaceView
Log.i(TAG, "Local renderer initialized")
return surfaceView
}
fun initializeRemoteRenderer(surfaceView: SurfaceViewRenderer): SurfaceViewRenderer {
surfaceView.init(eglBase.eglBaseContext, null)
surfaceView.setMirror(false)
surfaceView.setEnableHardwareScaler(true)
remoteRenderer = surfaceView
Log.i(TAG, "Remote renderer initialized")
return surfaceView
}
fun addLocalVideoTrack(videoTrack: VideoTrack) {
localRenderer?.let { renderer ->
videoTrack.addSink(renderer)
Log.i(TAG, "Local video track added")
}
}
fun addRemoteVideoTrack(videoTrack: VideoTrack) {
remoteRenderer?.let { renderer ->
videoTrack.addSink(renderer)
Log.i(TAG, "Remote video track added")
}
}
fun removeLocalVideoTrack(videoTrack: VideoTrack) {
localRenderer?.let { renderer ->
videoTrack.removeSink(renderer)
Log.i(TAG, "Local video track removed")
}
}
fun removeRemoteVideoTrack(videoTrack: VideoTrack) {
remoteRenderer?.let { renderer ->
videoTrack.removeSink(renderer)
Log.i(TAG, "Remote video track removed")
}
}
fun dispose() {
localRenderer?.release()
remoteRenderer?.release()
eglBase.release()
localRenderer = null
remoteRenderer = null
Log.i(TAG, "Video renderers disposed")
}
}
四、PeerConnection 管理
4.1 完整的 PeerConnection 封装
这是整个通话的核心。我们需要处理 SDP 交换、ICE 候选收集以及状态监听。注意缓存未设置的 ICE 候选,避免丢包。
class WebRtcPeerConnection(
private val webRtcManager: WebRtcManager,
private val iceServers: List<PeerConnection.IceServer>,
private val listener: PeerConnectionListener
) {
private var peerConnection: PeerConnection? = null
private var localMediaStream: MediaStream? = null
private val queuedRemoteCandidates = mutableListOf<IceCandidate>()
private var isRemoteDescriptionSet = false
companion object {
private const val TAG = "WebRtcPeerConnection"
private const val STREAM_ID = "stream_id"
private const val AUDIO_TRACK_ID = "audio_track"
private const val VIDEO_TRACK_ID = "video_track"
}
private val peerConnectionObserver = object : PeerConnection.Observer {
override fun onSignalingChange(newState: PeerConnection.SignalingState?) {
Log.d(TAG, "onSignalingChange: $newState")
}
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
Log.d(TAG, "onIceConnectionChange: $newState")
newState?.let {
when (it) {
PeerConnection.IceConnectionState.CONNECTED -> {
listener.onConnectionStateChanged(ConnectionState.CONNECTED)
}
PeerConnection.IceConnectionState.DISCONNECTED -> {
listener.onConnectionStateChanged(ConnectionState.DISCONNECTED)
}
PeerConnection.IceConnectionState.FAILED -> {
listener.onConnectionStateChanged(ConnectionState.FAILED)
}
else -> {}
}
}
}
override fun onIceConnectionReceivingChange(receiving: Boolean) {
Log.d(TAG, "onIceConnectionReceivingChange: $receiving")
}
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) {
Log.d(TAG, "onIceGatheringChange: $newState")
}
override fun onIceCandidate(candidate: IceCandidate?) {
Log.d(TAG, "onIceCandidate: ${candidate?.sdp}")
candidate?.let { listener.onIceCandidate(it) }
}
override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>?) {
Log.d(TAG, "onIceCandidatesRemoved: ${candidates?.size}")
}
override fun onAddStream(stream: MediaStream?) {
Log.d(TAG, "onAddStream: ${stream?.id}")
stream?.let {
if (it.videoTracks.isNotEmpty()) {
listener.onRemoteVideoTrack(it.videoTracks[0])
}
if (it.audioTracks.isNotEmpty()) {
listener.onRemoteAudioTrack(it.audioTracks[0])
}
}
}
override fun onRemoveStream(stream: MediaStream?) {
Log.d(TAG, "onRemoveStream: ${stream?.id}")
}
override fun onDataChannel(dataChannel: DataChannel?) {
Log.d(TAG, "onDataChannel: ${dataChannel?.label()}")
}
override fun onRenegotiationNeeded() {
Log.d(TAG, "onRenegotiationNeeded")
}
override fun onAddTrack(receiver: RtpReceiver?, streams: Array<out MediaStream>?) {
Log.d(TAG, "onAddTrack: ${receiver?.track()?.kind()}")
}
}
fun initialize(): Boolean {
peerConnection = webRtcManager.createPeerConnection(iceServers, peerConnectionObserver)
if (peerConnection == null) {
Log.e(TAG, "Failed to create PeerConnection")
return false
}
Log.i(TAG, "PeerConnection initialized")
return true
}
fun addLocalMediaStream(hasAudio: Boolean = true, hasVideo: Boolean = true) {
val factory = webRtcManager.getPeerConnectionFactory() ?: return
localMediaStream = factory.createLocalMediaStream(STREAM_ID)
if (hasAudio) {
val audioTrack = webRtcManager.createAudioTrack(AUDIO_TRACK_ID)
localMediaStream?.addTrack(audioTrack)
Log.i(TAG, "Local audio track added")
}
if (hasVideo) {
val videoTrack = webRtcManager.createVideoTrack(VIDEO_TRACK_ID)
localMediaStream?.addTrack(videoTrack)
listener.onLocalVideoTrack(videoTrack)
Log.i(TAG, "Local video track added")
}
peerConnection?.addStream(localMediaStream)
Log.i(TAG, "Local media stream added to PeerConnection")
}
fun createOffer() {
val constraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
}
peerConnection?.createOffer(object : SdpObserver {
override fun onCreateSuccess(sessionDescription: SessionDescription?) {
Log.i(TAG, "Create offer success")
sessionDescription?.let {
peerConnection?.setLocalDescription(object : SdpObserver {
override fun onSetSuccess() {
Log.i(TAG, "Set local description success")
listener.onLocalSessionDescription(it)
}
override fun onSetFailure(error: String?) {
Log.e(TAG, "Set local description failed: $error")
}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
}, it)
}
}
override fun onSetSuccess() {}
override fun onCreateFailure(error: String?) {
Log.e(TAG, "Create offer failed: $error")
}
override fun onSetFailure(error: String?) {}
}, constraints)
}
fun createAnswer() {
val constraints = MediaConstraints().apply {
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"))
mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"))
}
peerConnection?.createAnswer(object : SdpObserver {
override fun onCreateSuccess(sessionDescription: SessionDescription?) {
Log.i(TAG, "Create answer success")
sessionDescription?.let {
peerConnection?.setLocalDescription(object : SdpObserver {
override fun onSetSuccess() {
Log.i(TAG, "Set local description success")
listener.onLocalSessionDescription(it)
}
override fun onSetFailure(error: String?) {
Log.e(TAG, "Set local description failed: $error")
}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
}, it)
}
}
override fun onSetSuccess() {}
override fun onCreateFailure(error: String?) {
Log.e(TAG, "Create answer failed: $error")
}
override fun onSetFailure(error: String?) {}
}, constraints)
}
fun setRemoteDescription(sessionDescription: SessionDescription) {
peerConnection?.setRemoteDescription(object : SdpObserver {
override fun onSetSuccess() {
Log.i(TAG, "Set remote description success")
isRemoteDescriptionSet = true
queuedRemoteCandidates.forEach { candidate -> addIceCandidate(candidate) }
queuedRemoteCandidates.clear()
}
override fun onSetFailure(error: String?) {
Log.e(TAG, "Set remote description failed: $error")
}
override fun onCreateSuccess(p0: SessionDescription?) {}
override fun onCreateFailure(p0: String?) {}
}, sessionDescription)
}
fun addIceCandidate(candidate: IceCandidate) {
if (isRemoteDescriptionSet) {
peerConnection?.addIceCandidate(candidate)
Log.d(TAG, "ICE candidate added: ${candidate.sdp}")
} else {
queuedRemoteCandidates.add(candidate)
Log.d(TAG, "ICE candidate queued (remote description not set yet)")
}
}
fun close() {
peerConnection?.close()
peerConnection = null
localMediaStream = null
queuedRemoteCandidates.clear()
isRemoteDescriptionSet = false
Log.i(TAG, "PeerConnection closed")
}
fun getStats(callback: RTCStatsCollectorCallback) {
peerConnection?.getStats(callback)
}
}
interface PeerConnectionListener {
fun onLocalSessionDescription(sdp: SessionDescription)
fun onIceCandidate(candidate: IceCandidate)
fun onLocalVideoTrack(videoTrack: VideoTrack)
fun onRemoteVideoTrack(videoTrack: VideoTrack)
fun onRemoteAudioTrack(audioTrack: AudioTrack)
fun onConnectionStateChanged(state: ConnectionState)
}
enum class ConnectionState {
NEW, CONNECTING, CONNECTED, DISCONNECTED, FAILED, CLOSED
}
五、信令服务器通信
5.1 WebSocket 信令客户端
信令服务负责交换 SDP 和 ICE 信息。这里使用 OkHttp 的 WebSocket 实现,配合 Gson 处理 JSON 消息。
class WebSocketSignalingClient(
private val serverUrl: String,
private val roomId: String,
private val userId: String
) {
private var webSocket: WebSocket? = null
private val client = OkHttpClient.Builder()
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.pingInterval(20, TimeUnit.SECONDS)
.build()
private var listener: SignalingListener? = null
companion object {
private const val TAG = "SignalingClient"
}
fun connect(listener: SignalingListener) {
this.listener = listener
val request = Request.Builder()
.url("$serverUrl?roomId=$roomId&userId=$userId")
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
Log.i(TAG, "WebSocket connected")
listener.onConnected()
sendJoinRoom()
}
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "Message received: $text")
handleMessage(text)
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.i(TAG, "WebSocket closing: $code - $reason")
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.i(TAG, "WebSocket closed: $code - $reason")
listener.onDisconnected()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "WebSocket error", t)
listener.onError(t.message ?: "Unknown error")
}
})
}
private fun sendJoinRoom() {
val message = mapOf("type" to "join", "roomId" to roomId, "userId" to userId)
sendMessage(message)
}
fun sendOffer(sdp: String) {
val message = mapOf("type" to "offer", "roomId" to roomId, "userId" to userId, "sdp" to sdp)
sendMessage(message)
}
fun sendAnswer(sdp: String) {
val message = mapOf("type" to "answer", "roomId" to roomId, "userId" to userId, "sdp" to sdp)
sendMessage(message)
}
fun sendIceCandidate(candidate: IceCandidate) {
val message = mapOf(
"type" to "candidate",
"roomId" to roomId,
"userId" to userId,
"candidate" to mapOf(
"sdpMid" to candidate.sdpMid,
"sdpMLineIndex" to candidate.sdpMLineIndex,
"sdp" to candidate.sdp
)
)
sendMessage(message)
}
private fun sendMessage(message: Map<String, Any>) {
val json = Gson().toJson(message)
webSocket?.send(json)
Log.d(TAG, "Message sent: $json")
}
private fun handleMessage(text: String) {
try {
val json = Gson().fromJson(text, Map::class.java)
val type = json["type"] as? String
when (type) {
"joined" -> {
val users = json["users"] as? List<String>
listener?.onRoomJoined(users ?: emptyList())
}
"user-joined" -> {
val newUserId = json["userId"] as? String
newUserId?.let { listener?.onUserJoined(it) }
}
"offer" -> {
val sdp = json["sdp"] as? String
sdp?.let { listener?.onOfferReceived(it) }
}
"answer" -> {
val sdp = json["sdp"] as? String
sdp?.let { listener?.onAnswerReceived(it) }
}
"candidate" -> {
val candidateData = json["candidate"] as? Map<*, *>
if (candidateData != null) {
val candidate = IceCandidate(
candidateData["sdpMid"] as String,
(candidateData["sdpMLineIndex"] as Double).toInt(),
candidateData["sdp"] as String
)
listener?.onIceCandidateReceived(candidate)
}
}
"error" -> {
val error = json["message"] as? String
listener?.onError(error ?: "Unknown error")
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to parse message", e)
}
}
fun disconnect() {
webSocket?.close(1000, "Client closed")
webSocket = null
listener = null
}
}
interface SignalingListener {
fun onConnected()
fun onDisconnected()
fun onRoomJoined(users: List<String>)
fun onUserJoined(userId: String)
fun onOfferReceived(sdp: String)
fun onAnswerReceived(sdp: String)
fun onIceCandidateReceived(candidate: IceCandidate)
fun onError(error: String)
}
六、完整的通话管理器
6.1 WebRTC 通话管理
将上述组件整合到一个高层级管理类中,屏蔽底层细节,提供统一的通话接口。
class WebRtcCallManager(
private val context: Context,
private val signalingServerUrl: String
) {
private val webRtcManager = WebRtcManager(context)
private val cameraCapturerManager = CameraCapturerManager(context, webRtcManager)
private val videoRendererManager = VideoRendererManager(context)
private var peerConnection: WebRtcPeerConnection? = null
private var signalingClient: WebSocketSignalingClient? = null
private var localVideoTrack: VideoTrack? = null
private var remoteVideoTrack: VideoTrack? = null
private var callListener: CallListener? = null
companion object {
private const val TAG = "WebRtcCallManager"
private val ICE_SERVERS = listOf(
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").create(),
PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").create()
)
}
fun initialize() {
webRtcManager.initialize()
cameraCapturerManager.initialize()
Log.i(TAG, "Call manager initialized")
}
fun startCall(
roomId: String,
userId: String,
localVideoView: SurfaceViewRenderer,
remoteVideoView: SurfaceViewRenderer,
listener: CallListener
) {
this.callListener = listener
videoRendererManager.initializeLocalRenderer(localVideoView)
videoRendererManager.initializeRemoteRenderer(remoteVideoView)
createPeerConnection()
peerConnection?.addLocalMediaStream(hasAudio = true, hasVideo = true)
cameraCapturerManager.startCapture()
connectSignaling(roomId, userId, isInitiator = true)
}
fun acceptCall(
roomId: String,
userId: String,
localVideoView: SurfaceViewRenderer,
remoteVideoView: SurfaceViewRenderer,
listener: CallListener
) {
this.callListener = listener
videoRendererManager.initializeLocalRenderer(localVideoView)
videoRendererManager.initializeRemoteRenderer(remoteVideoView)
createPeerConnection()
peerConnection?.addLocalMediaStream(hasAudio = true, hasVideo = true)
cameraCapturerManager.startCapture()
connectSignaling(roomId, userId, isInitiator = false)
}
private fun createPeerConnection() {
peerConnection = WebRtcPeerConnection(webRtcManager, ICE_SERVERS, peerConnectionListener)
peerConnection?.initialize()
}
private fun connectSignaling(roomId: String, userId: String, isInitiator: Boolean) {
signalingClient = WebSocketSignalingClient(signalingServerUrl, roomId, userId)
signalingClient?.connect(object : SignalingListener {
override fun onConnected() {
Log.i(TAG, "Signaling connected")
}
override fun onDisconnected() {
Log.i(TAG, "Signaling disconnected")
callListener?.onCallEnded()
}
override fun onRoomJoined(users: List<String>) {
Log.i(TAG, "Room joined, users: $users")
if (isInitiator && users.size > 1) {
peerConnection?.createOffer()
}
}
override fun onUserJoined(userId: String) {
Log.i(TAG, "User joined: $userId")
if (isInitiator) {
peerConnection?.createOffer()
}
}
override fun onOfferReceived(sdp: String) {
Log.i(TAG, "Offer received")
val sessionDescription = SessionDescription(SessionDescription.Type.OFFER, sdp)
peerConnection?.setRemoteDescription(sessionDescription)
peerConnection?.createAnswer()
}
override fun onAnswerReceived(sdp: String) {
Log.i(TAG, "Answer received")
val sessionDescription = SessionDescription(SessionDescription.Type.ANSWER, sdp)
peerConnection?.setRemoteDescription(sessionDescription)
}
override fun onIceCandidateReceived(candidate: IceCandidate) {
Log.d(TAG, "ICE candidate received")
peerConnection?.addIceCandidate(candidate)
}
override fun onError(error: String) {
Log.e(TAG, "Signaling error: $error")
callListener?.onCallError(error)
}
})
}
private val peerConnectionListener = object : PeerConnectionListener {
override fun onLocalSessionDescription(sdp: SessionDescription) {
Log.i(TAG, "Local session description created: ${sdp.type}")
when (sdp.type) {
SessionDescription.Type.OFFER -> {
signalingClient?.sendOffer(sdp.description)
}
SessionDescription.Type.ANSWER -> {
signalingClient?.sendAnswer(sdp.description)
}
else -> {}
}
}
override fun onIceCandidate(candidate: IceCandidate) {
Log.d(TAG, "ICE candidate generated")
signalingClient?.sendIceCandidate(candidate)
}
override fun onLocalVideoTrack(videoTrack: VideoTrack) {
Log.i(TAG, "Local video track ready")
localVideoTrack = videoTrack
videoRendererManager.addLocalVideoTrack(videoTrack)
}
override fun onRemoteVideoTrack(videoTrack: VideoTrack) {
Log.i(TAG, "Remote video track received")
remoteVideoTrack = videoTrack
videoRendererManager.addRemoteVideoTrack(videoTrack)
callListener?.onRemoteStreamReceived()
}
override fun onRemoteAudioTrack(audioTrack: AudioTrack) {
Log.i(TAG, "Remote audio track received")
}
override fun onConnectionStateChanged(state: ConnectionState) {
Log.i(TAG, "Connection state changed: $state")
when (state) {
ConnectionState.CONNECTED -> {
callListener?.onCallConnected()
}
ConnectionState.DISCONNECTED -> {
callListener?.onCallDisconnected()
}
ConnectionState.FAILED -> {
callListener?.onCallError("Connection failed")
}
else -> {}
}
}
}
fun switchCamera() {
cameraCapturerManager.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler {
override fun onCameraSwitchDone(isFrontCamera: Boolean) {
Log.i(TAG, "Camera switched: front=$isFrontCamera")
}
override fun onCameraSwitchError(errorDescription: String?) {
Log.e(TAG, "Camera switch error: $errorDescription")
}
})
}
fun toggleAudio(enabled: Boolean) {
localVideoTrack?.setEnabled(enabled)
}
fun toggleVideo(enabled: Boolean) {
localVideoTrack?.setEnabled(enabled)
}
fun endCall() {
cameraCapturerManager.stopCapture()
peerConnection?.close()
signalingClient?.disconnect()
localVideoTrack = null
remoteVideoTrack = null
Log.i(TAG, "Call ended")
}
fun dispose() {
endCall()
cameraCapturerManager.dispose()
videoRendererManager.dispose()
webRtcManager.dispose()
Log.i(TAG, "Call manager disposed")
}
}
interface CallListener {
fun onCallConnected()
fun onCallDisconnected()
fun onRemoteStreamReceived()
fun onCallEnded()
fun onCallError(error: String)
}
七、性能优化与最佳实践
7.1 性能优化策略
在实际项目中,带宽波动和网络抖动是常态。我们可以通过调整 SDP 参数来适应不同场景。
object WebRtcOptimization {
fun optimizeEncoderSettings(sdp: String, isLowBandwidth: Boolean): String {
var modifiedSdp = sdp
if (isLowBandwidth) {
modifiedSdp = modifiedSdp.replace("x-google-max-bitrate=\\d+".toRegex(), "x-google-max-bitrate=500")
modifiedSdp = modifiedSdp.replace("x-google-start-bitrate=\\d+".toRegex(), "x-google-start-bitrate=300")
}
return modifiedSdp
}
fun configureLowLatency(): MediaConstraints {
return MediaConstraints().apply {
optional.add(MediaConstraints.KeyValuePair("googCpuOveruseDetection", "true"))
optional.add(MediaConstraints.KeyValuePair("googPayloadPadding", "false"))
optional.add(MediaConstraints.KeyValuePair("googLatency", "true"))
}
}
fun enableHardwareAcceleration() {
}
}
7.2 实战效果
在某智能门铃项目中,采用 WebRTC 方案取得了优异效果:
- 端到端延迟:60-80ms
- 连接建立时间:2-3 秒
- CPU 占用:15-25%
- 内存占用:80-120MB
- 流畅度:98%+ (25fps+)
- 延迟降低 70% (300ms → 80ms)
- 带宽效率提升 40%
- 用户体验提升显著
八、总结
下面我们来梳理一下 WebRTC 在 Android 智能安防系统中的应用实践要点:
- WebRTC 架构: 理解核心组件与通信流程
- SDK 集成: 初始化配置与依赖管理
- 音视频处理: 采集、渲染及轨道管理
- 连接管理: PeerConnection 封装与状态维护
- 信令通信: WebSocket 实现与消息交互
- 通话管理: 完整通话流程与用户交互
- 性能优化: 参数调优与硬件加速
- WebRTC 是成熟的实时音视频方案
- PeerConnection 是核心 API
- 合理配置编解码参数
- 完善的状态管理机制
- 持续监控和优化
参考资料
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online