WebRTC Android Application Overview
Created on: October 30, 2024
Created on: October 30, 2024
class ClientActivity4 : ComponentActivity() {
private lateinit var socket: Socket
private var localPeer: PeerConnection? = null
private var localView: SurfaceViewRenderer? = null
private var remoteView: SurfaceViewRenderer? = null
private var localEglBase: EglBase? = null
private var remoteEglBase: EglBase? = null
private val pendingIceCandidates = mutableListOf<IceCandidate>()
private var currentRoom: String? = null
private val localPeerLiveData = MutableLiveData<PeerConnection?>(null)
private val TAG: String = "WebRTC-Client"
textoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 设置角色和房间 currentRoom = intent.getStringExtra("room") ?: "default-room" Log.d(TAG, "onCreate: role = Client, room = $currentRoom") initializeSocketIO() setContent { WebRTCComposeLayout() } requestPermissionsIfNeeded() } private fun initializeSocketIO() { val options = IO.Options().apply { transports = arrayOf("websocket") secure = true path = "/socket.io/" } try { socket = IO.socket("https://wstszx.us.kg", options) socket.on(Socket.EVENT_CONNECT) { Log.d(TAG, "Socket connected") socket.emit("join", currentRoom) Log.d(TAG, "Joined room: $currentRoom") initiateCall() } socket.on(Socket.EVENT_CONNECT_ERROR) { args -> if (args.isNotEmpty()) { val error = args[0] Log.e(TAG, "Socket connect error: $error") } } socket.on(Socket.EVENT_DISCONNECT) { args -> if (args.isNotEmpty()) { val reason = args[0] Log.d(TAG, "Socket disconnected: $reason") } } socket.on("signal") { args -> Log.d(TAG, "Received signal: ${args[0]}") if (args.isNotEmpty() && args[0] is JSONObject) { val data = args[0] as JSONObject handleSignalingData(data) } } socket.connect() Log.d(TAG, "Connecting to socket...") } catch (e: Exception) { Log.e(TAG, "Error connecting to socket: ${e.message}") } } private fun requestPermissionsIfNeeded() { if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED ) { requestPermissions( arrayOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE ), 1 ) } } @Composable fun WebRTCComposeLayout() { val context = LocalContext.current var peerConnectionFactory: PeerConnectionFactory? = null var localVideoTrack: VideoTrack? = null Surface(color = Color.Black) { Box(modifier = Modifier.fillMaxSize()) { AndroidView( factory = { remoteView = SurfaceViewRenderer(it).apply { setZOrderMediaOverlay(false) } remoteView!! }, modifier = Modifier.fillMaxSize(), update = { if (remoteEglBase?.eglBaseContext == null) { remoteEglBase = EglBase.create() it.init(remoteEglBase!!.eglBaseContext, null) it.setMirror(false) } } ) AndroidView( factory = { localView = SurfaceViewRenderer(it).apply { setZOrderMediaOverlay(true) } localView!! }, modifier = Modifier .size(100.dp, 150.dp) .align(Alignment.BottomEnd) .padding(16.dp), update = { if (localEglBase == null) { localEglBase = EglBase.create() it.init(localEglBase!!.eglBaseContext, null) it.setMirror(true) } } ) LaunchedEffect(Unit) { val options = PeerConnectionFactory.InitializationOptions.builder(context) .createInitializationOptions() PeerConnectionFactory.initialize(options) val encoderFactory = DefaultVideoEncoderFactory( localEglBase!!.eglBaseContext, true, true ) val decoderFactory = DefaultVideoDecoderFactory(remoteEglBase!!.eglBaseContext) peerConnectionFactory = PeerConnectionFactory.builder() .setVideoEncoderFactory(encoderFactory) .setVideoDecoderFactory(decoderFactory) .createPeerConnectionFactory() initLocalVideo(context, localView, peerConnectionFactory!!, localEglBase!!) { localVideoTrack = it } createPeerConnection( context, peerConnectionFactory, localVideoTrack, remoteView!! ) { localPeer = it localPeerLiveData.postValue(it) } } } } } private fun initLocalVideo( context: Context, localView: SurfaceViewRenderer?, peerConnectionFactory: PeerConnectionFactory, eglBase: EglBase, onLocalVideoTrack: (VideoTrack) -> Unit ) { val videoCapturer = createCameraCapturer(context) val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.eglBaseContext) val videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast) videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver) videoCapturer.startCapture(1280, 720, 30) val localVideoTrack = peerConnectionFactory.createVideoTrack("video_track", videoSource) localVideoTrack.addSink(localView) val audioSource = peerConnectionFactory.createAudioSource(MediaConstraints()) val localAudioTrack = peerConnectionFactory.createAudioTrack("audio_track", audioSource) // 添加音视频轨道到本地流 val mediaStream = peerConnectionFactory.createLocalMediaStream("local_stream") mediaStream.addTrack(localAudioTrack) mediaStream.addTrack(localVideoTrack) onLocalVideoTrack(localVideoTrack) } private fun createCameraCapturer(context: Context): CameraVideoCapturer { val camera2Enumerator = Camera2Enumerator(context) val deviceNames = camera2Enumerator.deviceNames for (deviceName in deviceNames) { if (camera2Enumerator.isFrontFacing(deviceName)) { return camera2Enumerator.createCapturer(deviceName, null) ?: throw IllegalStateException("无法创建前置摄像头捕获器") } } // 如果没有前置摄像头,则使用第一个摄像头 return camera2Enumerator.createCapturer(deviceNames[0], null) ?: throw IllegalStateException("无法创建摄像头捕获器") } private fun createPeerConnection( context: Context, peerConnectionFactory: PeerConnectionFactory, localVideoTrack: VideoTrack?, remoteView: SurfaceViewRenderer, onLocalPeerCreated: (PeerConnection) -> Unit ) { val turnServer = PeerConnection.IceServer.builder("turns:turn.wstszx.us.kg:5349?transport=tcp") .setUsername("wstszx") .setPassword("930379") .createIceServer() val turnServerUdp = PeerConnection.IceServer.builder("turn:turn.wstszx.us.kg:3478?transport=udp") .setUsername("wstszx") .setPassword("930379") .createIceServer() val turnServerTcp = PeerConnection.IceServer.builder("turn:turn.wstszx.us.kg:3478?transport=tcp") .setUsername("wstszx") .setPassword("930379") .createIceServer() val stunServer = PeerConnection.IceServer.builder("stun:stun.wstszx.us.kg:3478") .createIceServer() val iceServers = listOf(stunServer, turnServerUdp, turnServerTcp, turnServer) val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply { bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN } val localPeer = peerConnectionFactory.createPeerConnection(rtcConfig, object : PeerConnection.Observer { override fun onIceCandidate(iceCandidate: IceCandidate?) { iceCandidate?.let { Log.d(TAG, "ICE candidate: $it") val signalData = JSONObject().apply { put("type", "ice") put("candidate", JSONObject().apply { put("sdpMid", it.sdpMid) put("sdpMLineIndex", it.sdpMLineIndex) put("candidate", it.sdp) }) put("room", currentRoom) } socket.emit("signal", signalData) } } override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>?) { Log.d(TAG, "ICE candidates removed") } override fun onSignalingChange(newState: PeerConnection.SignalingState?) { Log.d(TAG, "Signaling state changed to: $newState") } override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { Log.d(TAG, "ICE connection state changed to: $newState") } override fun onIceConnectionReceivingChange(receiving: Boolean) { Log.d(TAG, "ICE connection receiving change: $receiving") } override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) { Log.d(TAG, "ICE gathering state changed to: $newState") } override fun onAddStream(stream: MediaStream?) { Log.d(TAG, "Stream added") } override fun onRemoveStream(stream: MediaStream?) { Log.d(TAG, "Stream removed") } override fun onDataChannel(dataChannel: DataChannel?) { Log.d(TAG, "Data channel created") } override fun onRenegotiationNeeded() { Log.d(TAG, "Renegotiation needed") } override fun onAddTrack(receiver: RtpReceiver?, streams: Array<out MediaStream>?) { Log.d(TAG, "Track added") receiver?.track()?.let { track -> if (track is VideoTrack) { track.addSink(remoteView) } } } override fun onTrack(transceiver: RtpTransceiver?) { Log.d(TAG, "onTrack called") transceiver?.receiver?.track()?.let { track -> if (track is VideoTrack) { track.addSink(remoteView) } } } override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) { Log.d(TAG, "Connection state changed to: $newState") } }) localVideoTrack?.let { localPeer?.addTrack(it, listOf("local_stream")) } val audioSource = peerConnectionFactory.createAudioSource(MediaConstraints()) val localAudioTrack = peerConnectionFactory.createAudioTrack("audio_track", audioSource) localPeer?.addTrack(localAudioTrack, listOf("local_stream")) onLocalPeerCreated(localPeer!!) } private fun initiateCall() { Log.d(TAG, "Initiating call...") val constraints = MediaConstraints().apply { mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true")) } localPeer?.createOffer(object : SdpObserver { override fun onCreateSuccess(sessionDescription: SessionDescription?) { sessionDescription?.let { sdp -> localPeer?.setLocalDescription(object : SdpObserver { override fun onSetSuccess() { val signalData = JSONObject().apply { put("type", "offer") put("sdp", JSONObject().put("sdp", sdp.description)) put("room", currentRoom) } socket.emit("signal", signalData) } override fun onSetFailure(error: String?) { Log.e(TAG, "Set local description error: $error") } override fun onCreateSuccess(p0: SessionDescription?) {} override fun onCreateFailure(p0: String?) {} }, sdp) } } override fun onSetSuccess() {} override fun onCreateFailure(error: String?) { Log.e(TAG, "Create offer error: $error") } override fun onSetFailure(error: String?) {} }, constraints) } private fun handleSignalingData(data: JSONObject) { Log.d(TAG, "Handling signaling data: $data") when (data.getString("type")) { "answer" -> { Log.d(TAG, "Received answer") val sdp = SessionDescription( SessionDescription.Type.ANSWER, data.getJSONObject("sdp").getString("sdp") ) localPeer?.setRemoteDescription(object : SdpObserver { override fun onSetSuccess() { pendingIceCandidates.forEach { candidate -> localPeer?.addIceCandidate(candidate) } pendingIceCandidates.clear() Log.d(TAG, "Set remote description (answer) success") } override fun onSetFailure(error: String?) { Log.e(TAG, "Set remote description error: $error") } override fun onCreateSuccess(p0: SessionDescription?) {} override fun onCreateFailure(p0: String?) {} }, sdp) } "ice" -> { Log.d(TAG, "Received ICE candidate") val candidateData = data.getJSONObject("candidate") val candidate = IceCandidate( candidateData.getString("sdpMid"), candidateData.getInt("sdpMLineIndex"), candidateData.getString("candidate") ) if (localPeer?.remoteDescription != null) { localPeer?.addIceCandidate(candidate) } else { pendingIceCandidates.add(candidate) } } else -> { Log.e(TAG, "Unknown signaling type: ${data.getString("type")}") } } } override fun onDestroy() { super.onDestroy() socket.disconnect() localPeer?.dispose() localView?.release() remoteView?.release() }
}
class ServerActivity4 : ComponentActivity() {
private lateinit var socket: Socket
private var localPeer: PeerConnection? = null
private var localView: SurfaceViewRenderer? = null
private var remoteView: SurfaceViewRenderer? = null
private var localEglBase: EglBase? = null
private var remoteEglBase: EglBase? = null
private val pendingIceCandidates = mutableListOf<IceCandidate>()
private var currentRoom: String? = null
private val localPeerLiveData = MutableLiveData<PeerConnection?>(null)
private val TAG: String = "WebRTC-Server"
textoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 设置角色和房间 currentRoom = intent.getStringExtra("room") ?: "default-room" Log.d(TAG, "onCreate: role = Server, room = $currentRoom") initializeSocketIO() setContent { WebRTCComposeLayout() } requestPermissionsIfNeeded() } private fun initializeSocketIO() { val options = IO.Options().apply { transports = arrayOf("websocket") secure = true path = "/socket.io/" } try { socket = IO.socket("https://wstszx.us.kg", options) socket.on(Socket.EVENT_CONNECT) { Log.d(TAG, "Socket connected") socket.emit("join", currentRoom) Log.d(TAG, "Joined room: $currentRoom") // 服务器端通常等待客户端发起呼叫,因此不主动发起呼叫 } socket.on(Socket.EVENT_CONNECT_ERROR) { args -> if (args.isNotEmpty()) { val error = args[0] Log.e(TAG, "Socket connect error: $error") } } socket.on(Socket.EVENT_DISCONNECT) { args -> if (args.isNotEmpty()) { val reason = args[0] Log.d(TAG, "Socket disconnected: $reason") } } socket.on("signal") { args -> Log.d(TAG, "Received signal: ${args[0]}") if (args.isNotEmpty() && args[0] is JSONObject) { val data = args[0] as JSONObject handleSignalingData(data) } } socket.connect() Log.d(TAG, "Connecting to socket...") } catch (e: Exception) { Log.e(TAG, "Error connecting to socket: ${e.message}") } } private fun requestPermissionsIfNeeded() { if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED || checkSelfPermission(Manifest.permission.ACCESS_NETWORK_STATE) != PackageManager.PERMISSION_GRANTED ) { requestPermissions( arrayOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO, Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE ), 1 ) } } @Composable fun WebRTCComposeLayout() { val context = LocalContext.current var peerConnectionFactory: PeerConnectionFactory? = null var localVideoTrack: VideoTrack? = null Surface(color = Color.Black) { Box(modifier = Modifier.fillMaxSize()) { AndroidView( factory = { remoteView = SurfaceViewRenderer(it).apply { setZOrderMediaOverlay(false) } remoteView!! }, modifier = Modifier.fillMaxSize(), update = { if (remoteEglBase?.eglBaseContext == null) { remoteEglBase = EglBase.create() it.init(remoteEglBase!!.eglBaseContext, null) it.setMirror(false) } } ) AndroidView( factory = { localView = SurfaceViewRenderer(it).apply { setZOrderMediaOverlay(true) } localView!! }, modifier = Modifier .size(100.dp, 150.dp) .align(Alignment.BottomEnd) .padding(16.dp), update = { if (localEglBase == null) { localEglBase = EglBase.create() it.init(localEglBase!!.eglBaseContext, null) it.setMirror(true) } } ) LaunchedEffect(Unit) { val options = PeerConnectionFactory.InitializationOptions.builder(context) .createInitializationOptions() PeerConnectionFactory.initialize(options) val encoderFactory = DefaultVideoEncoderFactory( localEglBase!!.eglBaseContext, true, true ) val decoderFactory = DefaultVideoDecoderFactory(remoteEglBase!!.eglBaseContext) peerConnectionFactory = PeerConnectionFactory.builder() .setVideoEncoderFactory(encoderFactory) .setVideoDecoderFactory(decoderFactory) .createPeerConnectionFactory() initLocalVideo(context, localView, peerConnectionFactory!!, localEglBase!!) { localVideoTrack = it } createPeerConnection( context, peerConnectionFactory, localVideoTrack, remoteView!! ) { localPeer = it localPeerLiveData.postValue(it) } } } } } private fun initLocalVideo( context: Context, localView: SurfaceViewRenderer?, peerConnectionFactory: PeerConnectionFactory, eglBase: EglBase, onLocalVideoTrack: (VideoTrack) -> Unit ) { val videoCapturer = createCameraCapturer(context) val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.eglBaseContext) val videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast) videoCapturer.initialize(surfaceTextureHelper, context, videoSource.capturerObserver) videoCapturer.startCapture(1280, 720, 30) val localVideoTrack = peerConnectionFactory.createVideoTrack("video_track", videoSource) localVideoTrack.addSink(localView) val audioSource = peerConnectionFactory.createAudioSource(MediaConstraints()) val localAudioTrack = peerConnectionFactory.createAudioTrack("audio_track", audioSource) // 添加音视频轨道到本地流 val mediaStream = peerConnectionFactory.createLocalMediaStream("local_stream") mediaStream.addTrack(localAudioTrack) mediaStream.addTrack(localVideoTrack) onLocalVideoTrack(localVideoTrack) } private fun createCameraCapturer(context: Context): CameraVideoCapturer { val camera2Enumerator = Camera2Enumerator(context) val deviceNames = camera2Enumerator.deviceNames for (deviceName in deviceNames) { if (camera2Enumerator.isFrontFacing(deviceName)) { return camera2Enumerator.createCapturer(deviceName, null) ?: throw IllegalStateException("无法创建前置摄像头捕获器") } } // 如果没有前置摄像头,则使用第一个摄像头 return camera2Enumerator.createCapturer(deviceNames[0], null) ?: throw IllegalStateException("无法创建摄像头捕获器") } private fun createPeerConnection( context: Context, peerConnectionFactory: PeerConnectionFactory, localVideoTrack: VideoTrack?, remoteView: SurfaceViewRenderer, onLocalPeerCreated: (PeerConnection) -> Unit ) { val turnServer = PeerConnection.IceServer.builder("turns:turn.wstszx.us.kg:5349?transport=tcp") .setUsername("wstszx") .setPassword("930379") .createIceServer() val turnServerUdp = PeerConnection.IceServer.builder("turn:turn.wstszx.us.kg:3478?transport=udp") .setUsername("wstszx") .setPassword("930379") .createIceServer() val turnServerTcp = PeerConnection.IceServer.builder("turn:turn.wstszx.us.kg:3478?transport=tcp") .setUsername("wstszx") .setPassword("930379") .createIceServer() val stunServer = PeerConnection.IceServer.builder("stun:stun.wstszx.us.kg:3478") .createIceServer() val iceServers = listOf(stunServer, turnServerUdp, turnServerTcp, turnServer) val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply { bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN } val localPeer = peerConnectionFactory.createPeerConnection(rtcConfig, object : PeerConnection.Observer { override fun onIceCandidate(iceCandidate: IceCandidate?) { iceCandidate?.let { Log.d(TAG, "ICE candidate: $it") val signalData = JSONObject().apply { put("type", "ice") put("candidate", JSONObject().apply { put("sdpMid", it.sdpMid) put("sdpMLineIndex", it.sdpMLineIndex) put("candidate", it.sdp) }) put("room", currentRoom) } socket.emit("signal", signalData) } } override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>?) { Log.d(TAG, "ICE candidates removed") } override fun onSignalingChange(newState: PeerConnection.SignalingState?) { Log.d(TAG, "Signaling state changed to: $newState") } override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { Log.d(TAG, "ICE connection state changed to: $newState") } override fun onIceConnectionReceivingChange(receiving: Boolean) { Log.d(TAG, "ICE connection receiving change: $receiving") } override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) { Log.d(TAG, "ICE gathering state changed to: $newState") } override fun onAddStream(stream: MediaStream?) { Log.d(TAG, "Stream added") } override fun onRemoveStream(stream: MediaStream?) { Log.d(TAG, "Stream removed") } override fun onDataChannel(dataChannel: DataChannel?) { Log.d(TAG, "Data channel created") } override fun onRenegotiationNeeded() { Log.d(TAG, "Renegotiation needed") } override fun onAddTrack(receiver: RtpReceiver?, streams: Array<out MediaStream>?) { Log.d(TAG, "Track added") receiver?.track()?.let { track -> if (track is VideoTrack) { track.addSink(remoteView) } } } override fun onTrack(transceiver: RtpTransceiver?) { Log.d(TAG, "onTrack called") transceiver?.receiver?.track()?.let { track -> if (track is VideoTrack) { track.addSink(remoteView) } } } override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) { Log.d(TAG, "Connection state changed to: $newState") } }) localVideoTrack?.let { localPeer?.addTrack(it, listOf("local_stream")) } val audioSource = peerConnectionFactory.createAudioSource(MediaConstraints()) val localAudioTrack = peerConnectionFactory.createAudioTrack("audio_track", audioSource) localPeer?.addTrack(localAudioTrack, listOf("local_stream")) onLocalPeerCreated(localPeer!!) } private fun createAnswer(peerConnection: PeerConnection, onAnswerCreated: (String) -> Unit) { Log.d(TAG, "Creating answer...") 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?) { sessionDescription?.let { sdp -> peerConnection.setLocalDescription(object : SdpObserver { override fun onSetSuccess() { Log.d(TAG, "SetLocalDescription onSetSuccess") onAnswerCreated(sdp.description) } override fun onSetFailure(error: String?) { Log.e(TAG, "SetLocalDescription onSetFailure: $error") } override fun onCreateSuccess(p0: SessionDescription?) {} override fun onCreateFailure(p0: String?) {} }, sdp) } } override fun onSetSuccess() { Log.d(TAG, "createAnswer onSetSuccess") } override fun onCreateFailure(error: String?) { Log.e(TAG, "createAnswer onCreateFailure: $error") } override fun onSetFailure(error: String?) {} }, constraints) } private fun handleSignalingData(data: JSONObject) { Log.d(TAG, "Handling signaling data: $data") when (data.getString("type")) { "offer" -> { Log.d(TAG, "Received offer") val sdp = SessionDescription( SessionDescription.Type.OFFER, data.getJSONObject("sdp").getString("sdp") ) localPeer?.setRemoteDescription(object : SdpObserver { override fun onSetSuccess() { Log.d(TAG, "Set remote description (offer) success") createAnswer(localPeer!!) { answer -> val signalData = JSONObject().apply { put("type", "answer") put("sdp", JSONObject().put("sdp", answer)) put("room", currentRoom) } socket.emit("signal", signalData) pendingIceCandidates.forEach { candidate -> localPeer?.addIceCandidate(candidate) } pendingIceCandidates.clear() } } override fun onSetFailure(error: String?) { Log.e(TAG, "Set remote description (offer) error: $error") } override fun onCreateSuccess(p0: SessionDescription?) {} override fun onCreateFailure(p0: String?) {} }, sdp) } "ice" -> { Log.d(TAG, "Received ICE candidate") val candidateData = data.getJSONObject("candidate") val candidate = IceCandidate( candidateData.getString("sdpMid"), candidateData.getInt("sdpMLineIndex"), candidateData.getString("candidate") ) if (localPeer?.remoteDescription != null) { localPeer?.addIceCandidate(candidate) } else { pendingIceCandidates.add(candidate) } } else -> { Log.e(TAG, "Unknown signaling type: ${data.getString("type")}") } } } override fun onDestroy() { super.onDestroy() socket.disconnect() localPeer?.dispose() localView?.release() remoteView?.release() }
}
分析我提供的代码,给我写一份方案,主要需要讲清楚目前实现原理,为什么这样做,为什么局域网可以直连,不是局域网就需要用到stun和turn服务,移动网络使用stun是否有用,是否只能走中转,返回给我一份专业的方案说明
The provided Kotlin code comprises two Android ComponentActivity
classes: ClientActivity4
and ServerActivity4
. Both classes facilitate real-time video and audio communication using WebRTC, with signaling managed via Socket.IO. The application supports roles designated as Client and Server, enabling peer-to-peer (P2P) connections for media streaming.
Initialization:
https://wstszx.us.kg
with specified options.UI Composition:
localView
and remoteView
).WebRTC Setup:
Signaling:
offer
, answer
, ice
) to manage the WebRTC negotiation process.Lifecycle Management:
offer
to establish a connection.offer
and responds with an answer
, thus facilitating the connection establishment process.The application employs Socket.IO for the signaling phase, which is critical in exchanging session descriptions (SDP) and ICE candidates between peers. This exchange is necessary for peers to discover each other and negotiate connection parameters.
Both ClientActivity4
and ServerActivity4
define multiple ICE servers:
stun:stun.wstszx.us.kg:3478
turns:turn.wstszx.us.kg:5349?transport=tcp
turn:turn.wstszx.us.kg:3478?transport=udp
turn:turn.wstszx.us.kg:3478?transport=tcp
These servers are integrated into the PeerConnection.RTCConfiguration
to aid in NAT traversal.
STUN Servers:
TURN Servers:
Mobile Networks:
Wi-Fi (LAN):
TURN becomes essential when:
By incorporating both STUN and TURN servers, the application ensures robust connectivity across diverse network environments, enhancing reliability and user experience.
Differentiating between Client and Server roles allows for a clear initiation and response mechanism, simplifying the signaling and negotiation process.
Leveraging WebRTC and Socket.IO, both well-supported and widely adopted technologies, ensures the application benefits from community support, security updates, and performance optimizations.
Proper initialization and cleanup of resources (e.g., socket connections, media renderers) prevent memory leaks and ensure the application's stability.
The provided implementation effectively leverages WebRTC and Socket.IO to facilitate real-time audio and video communication between peers in Android. By incorporating both STUN and TURN servers, the application ensures robust connectivity across various network environments, including LAN and mobile networks. The role-based separation and comprehensive signaling mechanism underpin a reliable and scalable communication framework. Adhering to best practices in security, error handling, and performance optimization will further enhance the application's reliability and user experience.
用中文写一份
本文档对提供的Kotlin代码进行深入分析,并提出一份专业的实现方案。该代码包含两个Android ComponentActivity
类:ClientActivity4
和 ServerActivity4
,用于通过WebRTC实现实时音视频通信,信令部分采用Socket.IO进行管理。本文将详细阐述当前的实现原理、设计选择的原因,以及在不同网络环境下(如局域网和广域网)使用STUN和TURN服务器的必要性。
初始化阶段:
https://wstszx.us.kg
),配置传输选项。用户界面构建:
localView
和 remoteView
)。WebRTC配置:
信令处理:
offer
、answer
、ice
),管理WebRTC的协商过程。生命周期管理:
offer
,以建立连接。offer
,然后回应answer
,完成连接建立。应用程序使用Socket.IO进行信令通信,这是建立WebRTC连接的关键步骤。信令过程包括交换会话描述(SDP)和ICE候选,帮助双方发现对方并协商连接参数。
在ClientActivity4
和ServerActivity4
中,均配置了多个ICE服务器:
stun:stun.wstszx.us.kg:3478
turns:turn.wstszx.us.kg:5349?transport=tcp
turn:turn.wstszx.us.kg:3478?transport=udp
turn:turn.wstszx.us.kg:3478?transport=tcp
这些服务器被整合到PeerConnection.RTCConfiguration
中,辅助NAT穿透。
STUN服务器:
TURN服务器:
移动网络:
Wi-Fi(局域网):
在以下情况下,TURN服务器变得不可或缺:
通过集成STUN和TURN服务器,应用程序能够在各种网络环境下实现可靠的连接,提升用户体验。
区分客户端和服务器角色,明确发起和响应机制,简化信令和协商过程。
利用WebRTC和Socket.IO等成熟、广泛采用的技术,确保应用具备良好的社区支持、安全性和性能优化。
正确初始化和释放资源(如Socket连接、媒体渲染器),防止内存泄漏,保证应用稳定性。
提供的实现方案有效地利用了WebRTC和Socket.IO技术,在Android平台上实现了实时音视频通信。通过集成STUN和TURN服务器,应用能够在不同网络环境下保持稳定的连接,尤其在局域网和移动网络中展现出良好的适应性。角色分离设计和全面的信令机制为可靠和可扩展的通信框架奠定了基础。遵循最佳实践,如加强安全性、优化性能和提升可扩展性,将进一步提升应用的稳定性和用户体验。
文案中不要提及我的代码,重点是要说明这种方案的合理性,为什么在移动网络中无法实现直连,在除了局域网的情况下,什么情况下才可以实现直连,重新给我生成方案内容
本方案旨在通过WebRTC技术实现移动端设备间的实时音视频通信。为确保在各种网络环境下的稳定连接,特别是在复杂的移动网络条件下,本方案结合使用了STUN和TURN服务器,以解决NAT穿透和防火墙限制带来的挑战,从而实现高效、可靠的点对点(P2P)通信。
WebRTC(Web Real-Time Communication)是一项支持浏览器和移动应用进行实时音视频通信的开源项目。它无需中间插件,通过建立直接的P2P连接,实现低延迟、高质量的音视频传输。
在WebRTC中,信令机制用于交换建立P2P连接所需的元数据(如会话描述协议SDP和ICE候选)。信令本身并不依赖于特定的协议,可以通过WebSocket、Socket.IO等多种方式实现。
在局域网环境中,移动设备通常拥有私有IP地址,且处于同一网络内。此时,设备之间可以直接通过本地IP地址进行通信,无需复杂的NAT穿透机制。
在广域网环境中,设备往往位于不同的网络,且通常通过NAT(网络地址转换)或防火墙进行连接。NAT改变了设备的私有IP地址,使得直接P2P连接变得复杂。
移动设备在使用蜂窝数据网络时,通常面临以下挑战:
尽管在广域网环境中直接P2P连接具有挑战性,但在以下情况下仍有可能实现:
然而,由于上述条件在实际应用中难以保证,依赖中继服务器成为必要的解决方案。
功能:
工作原理: 设备向STUN服务器发送请求,服务器返回设备的公共IP和端口信息,设备利用这些信息尝试与对端建立直接连接。
适用场景:
功能:
工作原理: TURN服务器接收来自一方的媒体流,并将其转发给另一方,充当中间人确保通信的顺利进行。
适用场景:
在实际应用中,通常会配置多个STUN和TURN服务器,以提高连接的成功率和稳定性。STUN服务器作为首选,TURN服务器作为备选,确保在各种网络条件下都能实现通信。
许多移动运营商使用对称NAT,这种NAT类型为每个外部连接分配不同的公共端口,使得设备无法通过固定的公共地址与其他设备建立直接连接。
移动设备的IP地址经常变化,且可能在不同网络之间切换(如从Wi-Fi到蜂窝数据),导致连接的不稳定性和中断,增加了直接连接的难度。
移动网络中的防火墙通常严格控制入站和出站流量,阻止未经授权的连接请求,进一步限制了直接P2P连接的可能性。
在非局域网环境下,实现直连的前提条件主要包括:
如果双方设备位于支持端口转发的NAT环境中,并且能够手动或自动配置端口转发规则,直接P2P连接则有可能实现。
防火墙需允许WebRTC所需的协议(如UDP)和端口,通过允许这些流量,设备间可以尝试建立直接连接。
在STUN服务器帮助下,设备能够准确获取公共IP和端口信息,增加了直接连接的成功率。
尽管满足上述条件有助于实现直连,但由于网络环境的多样性和复杂性,依赖于STUN和TURN服务器仍然是确保通信稳定性的必要手段。
通过结合使用STUN和TURN服务器,方案能够适应各种网络环境,包括局域网、家庭网络、企业网络和移动网络,确保在不同条件下都能实现稳定的音视频通信。
利用STUN服务器实现直接连接,节省了中继服务器的资源和带宽成本;在无法直接连接时,TURN服务器提供必要的连接保障,确保通信的可靠性。
采用标准的WebRTC技术和广泛支持的STUN/TURN服务器,方案具备良好的扩展性和可维护性,易于集成到现有系统中,并能随着需求增长进行扩展。
通过尽可能建立直接连接,减少了延迟和带宽消耗,提升了用户的实时通信体验;在无法直接连接时,TURN服务器的支持确保了通信的连续性和稳定性,避免了因连接失败导致的服务中断。
基于WebRTC的实时通信方案,通过结合STUN和TURN服务器,成功解决了移动网络环境下的P2P连接挑战。该方案在局域网中能够实现高效的直接连接,在广域网和复杂网络环境中,通过STUN和TURN的辅助,确保了通信的可靠性和稳定性。此方案不仅具备高度的适应性和扩展性,还能在不同网络条件下优化用户体验,适用于各种实时音视频通信需求。
第4点和第7点有重复的部分,给我重新生成不重复的文案
本方案旨在通过WebRTC技术实现移动端设备间的实时音视频通信。为确保在各种网络环境下的稳定连接,尤其是在复杂的移动网络条件下,方案结合使用了STUN和TURN服务器,以解决NAT穿透和防火墙限制带来的挑战,从而实现高效、可靠的点对点(P2P)通信。
WebRTC(Web Real-Time Communication)是一项支持浏览器和移动应用进行实时音视频通信的开源项目。它无需中间插件,通过建立直接的P2P连接,实现低延迟、高质量的音视频传输。
在WebRTC中,信令机制用于交换建立P2P连接所需的元数据(如会话描述协议SDP和ICE候选)。信令本身并不依赖于特定的协议,可以通过WebSocket、Socket.IO等多种方式实现。
在局域网环境中,移动设备通常拥有私有IP地址,且处于同一网络内。此时,设备之间可以直接通过本地IP地址进行通信,无需复杂的NAT穿透机制。
在广域网环境中,设备往往位于不同的网络,且通常通过NAT(网络地址转换)或防火墙进行连接。NAT改变了设备的私有IP地址,使得直接P2P连接变得复杂。
移动设备在使用蜂窝数据网络时,通常面临以下挑战:
尽管在广域网环境中直接P2P连接具有挑战性,但以下因素可能影响连接的成功与否:
功能:
工作原理: 设备向STUN服务器发送请求,服务器返回设备的公共IP和端口信息,设备利用这些信息尝试与对端建立直接连接。
适用场景:
功能:
工作原理: TURN服务器接收来自一方的媒体流,并将其转发给另一方,充当中间人确保通信的顺利进行。
适用场景:
在实际应用中,通常会配置多个STUN和TURN服务器,以提高连接的成功率和稳定性。STUN服务器作为首选,TURN服务器作为备选,确保在各种网络条件下都能实现通信。
如果双方设备位于支持端口转发的NAT环境中,并且能够手动或自动配置端口转发规则,直接P2P连接则有可能实现。
防火墙需允许WebRTC所需的协议(如UDP)和端口,通过允许这些流量,设备间可以尝试建立直接连接。
在STUN服务器帮助下,设备能够准确获取公共IP和端口信息,增加了直接连接的成功率。
注意:尽管满足上述条件有助于实现直连,但由于网络环境的多样性和复杂性,依赖于STUN和TURN服务器仍然是确保通信稳定性的必要手段。
通过结合使用STUN和TURN服务器,方案能够适应各种网络环境,包括局域网、家庭网络、企业网络和移动网络,确保在不同条件下都能实现稳定的音视频通信。
利用STUN服务器实现直接连接,节省了中继服务器的资源和带宽成本;在无法直接连接时,TURN服务器提供必要的连接保障,确保通信的可靠性。
采用标准的WebRTC技术和广泛支持的STUN/TURN服务器,方案具备良好的扩展性和可维护性,易于集成到现有系统中,并能随着需求增长进行扩展。
通过尽可能建立直接连接,减少了延迟和带宽消耗,提升了用户的实时通信体验;在无法直接连接时,TURN服务器的支持确保了通信的连续性和稳定性,避免了因连接失败导致的服务中断。
基于WebRTC的实时通信方案,通过结合STUN和TURN服务器,成功解决了移动网络环境下的P2P连接挑战。该方案在局域网中能够实现高效的直接连接,在广域网和复杂网络环境中,通过STUN和TURN的辅助,确保了通信的可靠性和稳定性。此方案不仅具备高度的适应性和扩展性,还能在不同网络条件下优化用户体验,适用于各种实时音视频通信需求。