WebRTC 系列(一、简介) 一、整体流程 有了上一篇 WEBRTC 简介的基础,我们知道了 WebRTC 的工作流程,接下来就是需要用代码去实现这个流程了。对于不同端,实现起来的难易程度可能略微不同(实践中我感觉 iOS
有了上一篇 WEBRTC 简介的基础,我们知道了 WebRTC 的工作流程,接下来就是需要用代码去实现这个流程了。对于不同端,实现起来的难易程度可能略微不同(实践中我感觉 iOS 端是资料最少的),但是整体流程是一样的。
问:要把大象装冰箱,总共分几步?答:三步。
这些步骤,不同端不一定都需要,有的端的 WebRTC 依赖库中已经做了一些,H5 的初始化操作是最少的。有的步骤的顺序可能会略有不同,Android 和 iOS 就有区别,需要注意的是初始化不当可能导致画面渲染不出来,而且还不易排查,后面我再说明我踩过哪些坑。
这期间,PeerConnection 的 onIceCandidate() 这个回调方法会被调用,我们需要将生成的 IceCandidate 对象传递给对方,收到 IceCandidate 的时候我们需要通过 PeerConnection 的 addIceCandidate() 方法添加。
这期间,PeerConnection 的 onAddStream() 这个回调也会被调用,我们需要将 MediaStream 添加到远端控件渲染控件中进行渲染。
释放资源主要是为了能够正常的再次呼叫。
整理完整体流程后,我们可以先来实现一个本地 demo,我是创建了两个 PeerConnection,一个本地 PeerConnection 作为用户 A,一个远端 PeerConnection 作为用户 B,由于大部分 Android 设备和 iOS 设备同时只能开启一个摄像头,所以远端 PeerConnection 不会采集视频数据,只会渲染远端画面。
上一篇博客已经说到,WebRTC 已经成为 H5 的标准之一,所以 H5 实现它很容易,并不需要额外引入依赖。
H5 的实现相对简单,很多初始化的操作我们不需要去做,浏览器已经帮我们做好了,整体代码如下:
Local Demo
运行起来后点击呼叫,在右上角的远端控件就能收到本地视频了:
Android 端流程是最多的,但是由于我是写 Android 的,所以觉得它的流程是最严谨的,也可能是因为 WebRTC 是 Google 开源的缘故,所以 Android 端这方面的资料还是挺好找的。
对于 Android 来说,需要在 app 的 build.gradle 中引入依赖:
// WebRTCimplementation 'org.webrtc:google-webrtc:1.0.32006'
由于国内众所周知的原因,下载依赖比较慢甚至会失败,所以可能需要使用阿里云 Maven 镜像。
gradle 7.0 之前在项目根目录的 build.gradle 中添加阿里云 maven 镜像:
...allprojects { repositories { ... // 阿里云 maven 镜像 maven { url 'Http://maven.aliyun.com/nexus/content/groups/public/' } }}...
gradle 7.0 及之后则是在项目根目录的 setting.gradle 中添加阿里云 maven 镜像:
...dependencyResolutionManagement { ... repositories { ... // 阿里云 maven 镜像 maven { allowInsecureProtocol = true url 'http://maven.aliyun.com/nexus/content/groups/public/' } }}...
除了依赖之外,Android 还需要配置网络权限、录音权限、摄像头权限、获取网络状态权限、获取 wifi 状态权限,Android 6.0 之后录音权限、摄像头权限需要动态申请,这些不是 WebRTC 的重点,只要不是 Android 新手应该都知道怎么去做,这里就不赘述了。
package com.qinshou.webrtcdemo_android;import android.content.Context;import android.os.Bundle;import android.view.View;import androidx.appcompat.app.AppCompatActivity;import org.webrtc.AudioSource;import org.webrtc.AudioTrack;import org.webrtc.Camera2Capturer;import org.webrtc.Camera2Enumerator;import org.webrtc.CameraEnumerator;import org.webrtc.DataChannel;import org.webrtc.DefaultVideoDecoderFactory;import org.webrtc.DefaultVideoEncoderFactory;import org.webrtc.EglBase;import org.webrtc.IceCandidate;import org.webrtc.MediaConstraints;import org.webrtc.MediaStream;import org.webrtc.PeerConnection;import org.webrtc.PeerConnectionFactory;import org.webrtc.RtpReceiver;import org.webrtc.SessionDescription;import org.webrtc.SurfaceTextureHelper;import org.webrtc.SurfaceViewRenderer;import org.webrtc.VideoCapturer;import org.webrtc.VideoDecoderFactory;import org.webrtc.VideoEncoderFactory;import org.webrtc.VideoSource;import org.webrtc.VideoTrack;import java.util.ArrayList;import java.util.List;public class LocalDemoActivity extends AppCompatActivity { private static final String TAG = LocalDemoActivity.class.getSimpleName(); private static final String AUDIO_TRACK_ID = "ARDAMSa0"; private static final String VIDEO_TRACK_ID = "ARDAMSv0"; private static final List STREAM_IDS = new ArrayList() {{ add("ARDAMS"); }}; private static final String SURFACE_TEXTURE_HELPER_THREAD_NAME = "SurfaceTextureHelperThread"; private static final int WIDTH = 1280; private static final int HEIGHT = 720; private static final int FPS = 30; private EglBase mEglBase; private PeerConnectionFactory mPeerConnectionFactory; private VideoCapturer mVideoCapturer; private AudioTrack mAudioTrack; private VideoTrack mVideoTrack; private PeerConnection mLocalPeerConnection; private PeerConnection mRemotePeerConnection; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_local_demo); findViewById(R.id.btn_call).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { call(); } }); findViewById(R.id.btn_hang_up).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { hangUp(); } }); // 初始化 PeerConnectionFactory initPeerConnectionFactory(LocalDemoActivity.this); // 创建 EglBase mEglBase = EglBase.create(); // 创建 PeerConnectionFactory mPeerConnectionFactory = createPeerConnectionFactory(mEglBase); // 创建音轨 mAudioTrack = createAudioTrack(mPeerConnectionFactory); // 创建视轨 mVideoCapturer = createVideoCapturer(); VideoSource videoSource = createVideoSource(mPeerConnectionFactory, mVideoCapturer); mVideoTrack = createVideoTrack(mPeerConnectionFactory, videoSource); // 初始化本地视频渲染控件,这个方法非常重要,不初始化会黑屏 SurfaceViewRenderer svrLocal = findViewById(R.id.svr_local); svrLocal.init(mEglBase.getEglBaseContext(), null); mVideoTrack.addSink(svrLocal); // 初始化远端视频渲染控件,这个方法非常重要,不初始化会黑屏 SurfaceViewRenderer svrRemote = findViewById(R.id.svr_remote); svrRemote.init(mEglBase.getEglBaseContext(), null); // 开始本地渲染 // 创建 SurfaceTextureHelper,用来表示 camera 初始化的线程 SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create(SURFACE_TEXTURE_HELPER_THREAD_NAME, mEglBase.getEglBaseContext()); // 初始化视频采集器 mVideoCapturer.initialize(surfaceTextureHelper, LocalDemoActivity.this, videoSource.getCapturerObserver()); mVideoCapturer.startCapture(WIDTH, HEIGHT, FPS); } @Override protected void onDestroy() { super.onDestroy(); if (mEglBase != null) { mEglBase.release(); mEglBase = null; } if (mVideoCapturer != null) { try { mVideoCapturer.stopCapture(); } catch (InterruptedException e) { e.printStackTrace(); } mVideoCapturer.dispose(); mVideoCapturer = null; } if (mAudioTrack != null) { mAudioTrack.dispose(); mAudioTrack = null; } if (mVideoTrack != null) { mVideoTrack.dispose(); mVideoTrack = null; } if (mLocalPeerConnection != null) { mLocalPeerConnection.close(); mLocalPeerConnection = null; } if (mRemotePeerConnection != null) { mRemotePeerConnection.close(); mRemotePeerConnection = null; } SurfaceViewRenderer svrLocal = findViewById(R.id.svr_local); svrLocal.release(); SurfaceViewRenderer svrRemote = findViewById(R.id.svr_remote); svrRemote.release(); } private void initPeerConnectionFactory(Context context) { PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()); } private PeerConnectionFactory createPeerConnectionFactory(EglBase eglBase) { VideoEncoderFactory videoEncoderFactory = new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true); VideoDecoderFactory videoDecoderFactory = new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()); return PeerConnectionFactory.builder().setVideoEncoderFactory(videoEncoderFactory).setVideoDecoderFactory(videoDecoderFactory).createPeerConnectionFactory(); } private AudioTrack createAudioTrack(PeerConnectionFactory peerConnectionFactory) { AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints()); AudioTrack audioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, audioSource); audioTrack.setEnabled(true); return audioTrack; } private VideoCapturer createVideoCapturer() { VideoCapturer videoCapturer = null; CameraEnumerator cameraEnumerator = new Camera2Enumerator(LocalDemoActivity.this); for (String deviceName : cameraEnumerator.getDeviceNames()) { // 前摄像头 if (cameraEnumerator.isFrontFacing(deviceName)) { videoCapturer = new Camera2Capturer(LocalDemoActivity.this, deviceName, null); } } return videoCapturer; } private VideoSource createVideoSource(PeerConnectionFactory peerConnectionFactory, VideoCapturer videoCapturer) { // 创建视频源 VideoSource videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast()); return videoSource; } private VideoTrack createVideoTrack(PeerConnectionFactory peerConnectionFactory, VideoSource videoSource) { // 创建视轨 VideoTrack videoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource); videoTrack.setEnabled(true); return videoTrack; } private void call() { // 创建 PeerConnection PeerConnection.RTCConfiguration rtcConfiguration = new PeerConnection.RTCConfiguration(new ArrayList<>()); mLocalPeerConnection = mPeerConnectionFactory.createPeerConnection(rtcConfiguration, new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { } @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { } @Override public void onIceConnectionReceivingChange(boolean b) { } @Override public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { } @Override public void onIceCandidate(IceCandidate iceCandidate) { ShowLogUtil.verbose("onIceCandidate--->" + iceCandidate); sendIceCandidate(mLocalPeerConnection, iceCandidate); } @Override public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { } @Override public void onAddStream(MediaStream mediaStream) { } @Override public void onRemoveStream(MediaStream mediaStream) { } @Override public void onDataChannel(DataChannel dataChannel) { } @Override public void onRenegotiationNeeded() { } @Override public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { } }); // 为 PeerConnection 添加音轨、视轨 mLocalPeerConnection.addTrack(mAudioTrack, STREAM_IDS); mLocalPeerConnection.addTrack(mVideoTrack, STREAM_IDS); // 通过 PeerConnection 创建 offer,获取 sdp MediaConstraints mediaConstraints = new MediaConstraints(); mLocalPeerConnection.createOffer(new MySdpObserver() { @Override public void onCreateSuccess(SessionDescription sessionDescription) { ShowLogUtil.verbose("create offer success."); // 将 offer sdp 作为参数 setLocalDescription mLocalPeerConnection.setLocalDescription(new MySdpObserver() { @Override public void onCreateSuccess(SessionDescription sessionDescription) { } @Override public void onSetSuccess() { ShowLogUtil.verbose("set local sdp success."); // 发送 offer sdp sendOffer(sessionDescription); } }, sessionDescription); } @Override public void onSetSuccess() { } }, mediaConstraints); } private void sendOffer(SessionDescription offer) { receivedOffer(offer); } private void receivedOffer(SessionDescription offer) { // 创建 PeerConnection PeerConnection.RTCConfiguration rtcConfiguration = new PeerConnection.RTCConfiguration(new ArrayList<>()); mRemotePeerConnection = mPeerConnectionFactory.createPeerConnection(rtcConfiguration, new PeerConnection.Observer() { @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { } @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { } @Override public void onIceConnectionReceivingChange(boolean b) { } @Override public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { } @Override public void onIceCandidate(IceCandidate iceCandidate) { ShowLogUtil.verbose("onIceCandidate--->" + iceCandidate); sendIceCandidate(mRemotePeerConnection, iceCandidate); } @Override public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { } @Override public void onAddStream(MediaStream mediaStream) { ShowLogUtil.verbose("onAddStream--->" + mediaStream); if (mediaStream == null || mediaStream.videoTracks == null || mediaStream.videoTracks.isEmpty()) { return; } runOnUiThread(new Runnable() { @Override public void run() { SurfaceViewRenderer svrRemote = findViewById(R.id.svr_remote); mediaStream.videoTracks.get(0).addSink(svrRemote); } }); } @Override public void onRemoveStream(MediaStream mediaStream) { } @Override public void onDataChannel(DataChannel dataChannel) { } @Override public void onRenegotiationNeeded() { } @Override public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { } }); // 将 offer sdp 作为参数 setRemoteDescription mRemotePeerConnection.setRemoteDescription(new MySdpObserver() { @Override public void onCreateSuccess(SessionDescription sessionDescription) { } @Override public void onSetSuccess() { ShowLogUtil.verbose("set remote sdp success."); // 通过 PeerConnection 创建 answer,获取 sdp MediaConstraints mediaConstraints = new MediaConstraints(); mRemotePeerConnection.createAnswer(new MySdpObserver() { @Override public void onCreateSuccess(SessionDescription sessionDescription) { ShowLogUtil.verbose("create answer success."); // 将 answer sdp 作为参数 setLocalDescription mRemotePeerConnection.setLocalDescription(new MySdpObserver() {@Overridepublic void onCreateSuccess(SessionDescription sessionDescription) {}@Overridepublic void onSetSuccess() { ShowLogUtil.verbose("set local sdp success."); // 发送 answer sdp sendAnswer(sessionDescription);} }, sessionDescription); } @Override public void onSetSuccess() { } }, mediaConstraints); } }, offer); } private void sendAnswer(SessionDescription answer) { receivedAnswer(answer); } private void receivedAnswer(SessionDescription answer) { // 收到 answer sdp,将 answer sdp 作为参数 setRemoteDescription mLocalPeerConnection.setRemoteDescription(new MySdpObserver() { @Override public void onCreateSuccess(SessionDescription sessionDescription) { } @Override public void onSetSuccess() { ShowLogUtil.verbose("set remote sdp success."); } }, answer); } private void sendIceCandidate(PeerConnection peerConnection, IceCandidate iceCandidate) { receivedCandidate(peerConnection, iceCandidate); } private void receivedCandidate(PeerConnection peerConnection, IceCandidate iceCandidate) { if (peerConnection == mLocalPeerConnection) { mRemotePeerConnection.addIceCandidate(iceCandidate); } else { mLocalPeerConnection.addIceCandidate(iceCandidate); } } private void hangUp() { // 关闭 PeerConnection if (mLocalPeerConnection != null) { mLocalPeerConnection.close(); mLocalPeerConnection.dispose(); mLocalPeerConnection = null; } if (mRemotePeerConnection != null) { mRemotePeerConnection.close(); mRemotePeerConnection.dispose(); mRemotePeerConnection = null; } // 释放远端视频渲染控件 SurfaceViewRenderer svrRemote = findViewById(R.id.svr_remote); svrRemote.clearImage(); }}
其中 MySdpObserver 只是 SdpObserver 的一个自定义子接口,默认实现了失败回调:
package com.qinshou.webrtcdemo_android;import org.webrtc.SdpObserver;public interface MySdpObserver extends SdpObserver { @Override default void onCreateFailure(String s) { } @Override default void onSetFailure(String s) { }}
运行起来后点击呼叫,在右上角的远端控件就能收到本地视频了:
相较于 Android,iOS 端的资料相对较少,且资料大同小异,我也是踩了好多坑才跑通了 demo。
对于 iOS 来说,也需要额外引入依赖,由于我是做 Android 的,对 iOS 的其他引入依赖方式不太了解,下面是使用 Cocopods 引入 WebRTC 依赖:
...target 'WebRTCDemo-iOS' do ... pod 'GoogleWebRTC','~>1.1.31999'end...
除了依赖之外,iOS 还需要配置录音权限、摄像头权限,也需要动态申请,这些不是 WebRTC 的重点,只要不是 iOS 新手应该都知道怎么去做,这里就不赘述了。
NSMicrophoneUsageDescription 允许程序使用麦克风的权限 NSCameraUsageDescription 允许程序使用摄像头的权限
//// LocalDemoViewController.swift// WebRTCDemo//// Created by 覃浩 on 2023/3/21.//import UIKitimport WebRTCimport SnapKitclass LocalDemoViewController: UIViewController { private static let AUDIO_TRACK_ID = "ARDAMSa0" private static let VIDEO_TRACK_ID = "ARDAMSv0" private static let STREAM_IDS = ["ARDAMS"] private static let WIDTH = 1280 private static let HEIGHT = 720 private static let FPS = 30 private var localView: RTCEAGLVideoView! private var remoteView: RTCEAGLVideoView! private var peerConnectionFactory: RTCPeerConnectionFactory! private var audioTrack: RTCAudioTrack? private var videoTrack: RTCVideoTrack? private var videoCapturer: RTCVideoCapturer? private var remoteStream: RTCMediaStream? private var localPeerConnection: RTCPeerConnection? private var remotePeerConnection: RTCPeerConnection? override func viewDidLoad() { super.viewDidLoad() // 表明 View 不要扩展到整个屏幕,而是在 NavigationBar 下的区域 edgesForExtendedLayout = UIRectEdge() let btnCall = UIButton() btnCall.backgroundColor = UIColor.lightGray btnCall.setTitle("呼叫", for: .normal) btnCall.setTitleColor(UIColor.black, for: .normal) btnCall.addTarget(self, action: #selector(call), for: .touchUpInside) self.view.addSubview(btnCall) btnCall.snp.makeConstraints({ maker in maker.left.equalToSuperview().offset(30) maker.width.equalTo(60) maker.height.equalTo(40) }) let btnHangUp = UIButton() btnHangUp.backgroundColor = UIColor.lightGray btnHangUp.setTitle("挂断", for: .normal) btnHangUp.setTitleColor(UIColor.black, for: .normal) btnHangUp.addTarget(self, action: #selector(hangUp), for: .touchUpInside) self.view.addSubview(btnHangUp) btnHangUp.snp.makeConstraints({ maker in maker.left.equalToSuperview().offset(30) maker.width.equalTo(60) maker.height.equalTo(40) maker.top.equalTo(btnCall.snp.bottom).offset(10) }) // 初始化 PeerConnectionFactory initPeerConnectionFactory() // 创建 EglBase // 创建 PeerConnectionFactory peerConnectionFactory = createPeerConnectionFactory() // 创建音轨 audioTrack = createAudioTrack(peerConnectionFactory: peerConnectionFactory) // 创建视轨 videoTrack = createVideoTrack(peerConnectionFactory: peerConnectionFactory) let tuple = createVideoCapturer(videoSource: videoTrack!.source) let captureDevice = tuple.captureDevice videoCapturer = tuple.videoCapture // 初始化本地视频渲染控件 localView = RTCEAGLVideoView() localView.delegate = self self.view.insertSubview(localView,at: 0) localView.snp.makeConstraints({ maker in maker.width.equalToSuperview() maker.height.equalTo(localView.snp.width).multipliedBy(16.0/9.0) maker.centerY.equalToSuperview() }) videoTrack?.add(localView!) // 初始化远端视频渲染控件 remoteView = RTCEAGLVideoView() remoteView.delegate = self self.view.insertSubview(remoteView, aboveSubview: localView) remoteView.snp.makeConstraints({ maker in maker.width.equalTo(90) maker.height.equalTo(160) maker.top.equalToSuperview().offset(30) maker.right.equalToSuperview().offset(-30) }) // 开始本地渲染 (videoCapturer as? RTCCameraVideoCapturer)?.startCapture(with: captureDevice!, format: captureDevice!.activeFormat, fps: LocalDemoViewController.FPS) } override func viewDidDisappear(_ animated: Bool) { (videoCapturer as? RTCCameraVideoCapturer)?.stopCapture() videoCapturer = nil localPeerConnection?.close() localPeerConnection = nil remotePeerConnection?.close() remotePeerConnection = nil } private func initPeerConnectionFactory() { RTCPeerConnectionFactory.initialize() } private func createPeerConnectionFactory() -> RTCPeerConnectionFactory { var videoEncoderFactory = RTCDefaultVideoEncoderFactory() var videoDecoderFactory = RTCDefaultVideoDecoderFactory() if TARGET_OS_SIMULATOR != 0 { videoEncoderFactory = RTCSimluatorVideoEncoderFactory() videoDecoderFactory = RTCSimulatorVideoDecoderFactory() } return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory) } private func createAudioTrack(peerConnectionFactory: RTCPeerConnectionFactory) -> RTCAudioTrack { let mandatoryConstraints : [String : String] = [:] let optionalConstraints : [String : String] = [:] let audioSource = peerConnectionFactory.audioSource(with: RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints)) let audioTrack = peerConnectionFactory.audioTrack(with: audioSource, trackId: LocalDemoViewController.AUDIO_TRACK_ID) audioTrack.isEnabled = true return audioTrack } private func createVideoTrack(peerConnectionFactory: RTCPeerConnectionFactory) -> RTCVideoTrack? { let videoSource = peerConnectionFactory.videoSource() let videoTrack = peerConnectionFactory.videoTrack(with: videoSource, trackId: LocalDemoViewController.VIDEO_TRACK_ID) videoTrack.isEnabled = true return videoTrack } private func createVideoCapturer(videoSource: RTCVideoSource) -> (captureDevice: AVCaptureDevice?, videoCapture: RTCVideoCapturer?) { let videoCapturer = RTCCameraVideoCapturer(delegate: videoSource) let captureDevices = RTCCameraVideoCapturer.captureDevices() if (captureDevices.count == 0) { return (nil, nil) } var captureDevice: AVCaptureDevice? for c in captureDevices { // 前摄像头 if (c.position == .front) { captureDevice = c break } } if (captureDevice == nil) { return (nil, nil) } return (captureDevice, videoCapturer) } @objc private func call() { // 创建 PeerConnection let rtcConfiguration = RTCConfiguration() var mandatoryConstraints : [String : String] = [:] var optionalConstraints : [String : String] = [:] var mediaConstraints = RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints) localPeerConnection = peerConnectionFactory.peerConnection(with: rtcConfiguration, constraints: mediaConstraints, delegate: self) // 为 PeerConnection 添加音轨、视轨 localPeerConnection?.add(audioTrack!, streamIds: LocalDemoViewController.STREAM_IDS) localPeerConnection?.add(videoTrack!, streamIds: LocalDemoViewController.STREAM_IDS) // 通过 PeerConnection 创建 offer,获取 sdp mandatoryConstraints = [:] optionalConstraints = [:] mediaConstraints = RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints) localPeerConnection?.offer(for: mediaConstraints, completionHandler: { sessionDescription, error in ShowLogUtil.verbose("create offer success.") // 将 offer sdp 作为参数 setLocalDescription self.localPeerConnection?.setLocalDescription(sessionDescription!, completionHandler: { _ in ShowLogUtil.verbose("set local sdp success.") // 发送 offer sdp self.sendOffer(offer: sessionDescription!) }) }) } private func sendOffer(offer: RTCSessionDescription) { receivedOffer(offer: offer) } private func receivedOffer(offer: RTCSessionDescription) { // 创建 PeerConnection let rtcConfiguration = RTCConfiguration() let mandatoryConstraints : [String : String] = [:] let optionalConstraints : [String : String] = [:] var mediaConstraints = RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints) remotePeerConnection = peerConnectionFactory.peerConnection(with: rtcConfiguration, constraints: mediaConstraints, delegate: self) // 将 offer sdp 作为参数 setRemoteDescription remotePeerConnection?.setRemoteDescription(offer, completionHandler: { _ in ShowLogUtil.verbose("set remote sdp success.") // 通过 PeerConnection 创建 answer,获取 sdp let mandatoryConstraints : [String : String] = [:] let optionalConstraints : [String : String] = [:] let mediaConstraints = RTCMediaConstraints(mandatoryConstraints: mandatoryConstraints, optionalConstraints: optionalConstraints) self.remotePeerConnection?.answer(for: mediaConstraints, completionHandler: { sessionDescription, error in ShowLogUtil.verbose("create answer success.") // 将 answer sdp 作为参数 setLocalDescription self.remotePeerConnection?.setLocalDescription(sessionDescription!, completionHandler: { _ in ShowLogUtil.verbose("set local sdp success.") // 发送 answer sdp self.sendAnswer(answer: sessionDescription!) }) }) }) } private func sendAnswer(answer: RTCSessionDescription) { receivedAnswer(answer: answer) } private func receivedAnswer(answer: RTCSessionDescription) { // 收到 answer sdp,将 answer sdp 作为参数 setRemoteDescription localPeerConnection?.setRemoteDescription(answer, completionHandler: { _ in ShowLogUtil.verbose("set remote sdp success.") }) } private func sendIceCandidate(peerConnection: RTCPeerConnection, iceCandidate: RTCIceCandidate) { receivedCandidate(peerConnection: peerConnection,iceCandidate: iceCandidate) } private func receivedCandidate(peerConnection: RTCPeerConnection, iceCandidate: RTCIceCandidate) { if (peerConnection == localPeerConnection) { remotePeerConnection?.add(iceCandidate) } else { localPeerConnection?.add(iceCandidate) } } @objc private func hangUp() { // 关闭 PeerConnection localPeerConnection?.close() localPeerConnection = nil remotePeerConnection?.close() remotePeerConnection = nil // 释放远端视频渲染控件 if let track = remoteStream?.videoTracks.first { track.remove(remoteView!) } }}// MARK: - RTCVideoViewDelegateextension LocalDemoViewController: RTCVideoViewDelegate { func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) { }}// MARK: - RTCPeerConnectionDelegateextension LocalDemoViewController: RTCPeerConnectionDelegate { func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { } func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { ShowLogUtil.verbose("peerConnection didAdd stream--->\(stream)") if (peerConnection == self.localPeerConnection) { } else if (peerConnection == self.remotePeerConnection) { self.remoteStream = stream if let track = stream.videoTracks.first { track.add(remoteView!) } if let audioTrack = stream.audioTracks.first{ audioTrack.source.volume = 8 } } } func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { } func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { } func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { } func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) { } func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { ShowLogUtil.verbose("didGenerate candidate--->\(candidate)") self.sendIceCandidate(peerConnection: peerConnection, iceCandidate: candidate) } func peerConnection(_ peerConnection: RtcpeerConnection, didRemove candidates: [RTCIceCandidate]) { } func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { }}
运行起来后点击呼叫,在右上角的远端控件就能收到本地视频了:
Gif 的质量就包涵一下,压缩后失真比较严重,高清的可以看最后 demo 地址中的 imgs 目下的图。
大家可能会看到 iOS 中间会卡了一下,那其实不是卡了,因为 iOS 录屏录不了点击事件,实际上是跟 H5 和 Android 一样,我中间点击了一下挂断,但是 iOS 挂断后释放远端渲染控件时,控件会保留显示最后一帧的画面,所以大家如果在实际做的时候想要效果好一点的话,可以在渲染控件上再放一个黑色遮罩,挂断时显示遮罩。
踩过的坑,其实在代码中已经有注释了,大家留意一下。
Android 端的坑:
iOS 端的坑:
通过这三端的本地 demo,大家应该还是可以看到一定区别,H5 其实很少的代码量就能实现 WebRTC 功能,而 Android 相对来说需要初始化的地方更多,但可能因为是我的本行的缘故,所以我在实现 Android 端的时候并没有遇到什么坑。
其实一端与另一端通话是只需要创建一个 PeerConnection ,这一次本地 demo 中我们创建了两个 PeerConnection,这是为了不搭建信令服务器就能看到效果,所以创建了一个 RemotePeerConnection 模拟远端,下一次我们就会通过 websocket 搭建一个简单的信令服务器来实现点对点通话。
来源地址:https://blog.csdn.net/zgcqflqinhao/article/details/130030110
--结束END--
本文标题: WebRTC 系列(二、本地通话,H5、Android、iOS)
本文链接: https://lsjlt.com/news/404312.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-21
2023-10-28
2023-10-28
2023-10-27
2023-10-27
2023-10-27
2023-10-27
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0