如何使用WebRTC Native Code for Android实现三方电话会议video聊天?

我正在尝试使用适用于Android的WebRTC本机代码包 (即不使用WebView)在Android应用内部实现3路video聊天。 我使用node.js编写了一个信令服务器,并使用客户端应用程序内的Gottox socket.io java客户端库连接到服务器,交换SDP数据包并建立双向video聊天连接。

但是现在我遇到的问题不仅仅是三方通话。 WebRTC本机代码包附带的AppRTCDemo应用程序仅演示双向呼叫(如果第三方尝试加入房间,则返回“房间已满”消息)。

根据这个答案 (与Android没有特别关系),我应该通过创建多个PeerConnections来实现,因此每个聊天参与者将连接到其他2个参与者。

但是,当我创建多个PeerConnectionClient(一个包装PeerConection的Java类,它在libjingle_peerconnection_so.so中的本机端实现)时,会从库内部抛出一个exception,因为它们都试图与访问相机:

E/VideoCapturerAndroid(21170): startCapture failed E/VideoCapturerAndroid(21170): java.lang.RuntimeException: Fail to connect to camera service E/VideoCapturerAndroid(21170): at android.hardware.Camera.native_setup(Native Method) E/VideoCapturerAndroid(21170): at android.hardware.Camera.(Camera.java:548) E/VideoCapturerAndroid(21170): at android.hardware.Camera.open(Camera.java:389) E/VideoCapturerAndroid(21170): at org.webrtc.VideoCapturerAndroid.startCaptureOnCameraThread(VideoCapturerAndroid.java:528) E/VideoCapturerAndroid(21170): at org.webrtc.VideoCapturerAndroid.access$11(VideoCapturerAndroid.java:520) E/VideoCapturerAndroid(21170): at org.webrtc.VideoCapturerAndroid$6.run(VideoCapturerAndroid.java:514) E/VideoCapturerAndroid(21170): at android.os.Handler.handleCallback(Handler.java:733) E/VideoCapturerAndroid(21170): at android.os.Handler.dispatchMessage(Handler.java:95) E/VideoCapturerAndroid(21170): at android.os.Looper.loop(Looper.java:136) E/VideoCapturerAndroid(21170): at org.webrtc.VideoCapturerAndroid$CameraThread.run(VideoCapturerAndroid.java:484) 

在尝试建立连接之前初始化本地客户端时会发生这种情况,因此它与node.js,socket.io或任何信令服务器内容无关。

如何让多个PeerConnections共享摄像头,以便我可以将同一video发送给多个同行?

我的一个想法是实现某种单独的相机类来取代可以在多个连接之间共享的VideoCapturerAndroid,但我甚至不确定它是否会起作用,我想知道是否有办法做3-在我开始在库内进行黑客攻击之前使用API​​进行调用。

是否可能,如果可能,怎么样?

更新:

我尝试在多个PeerConnectionClients之间共享一个VideoCapturerAndroid对象,仅为第一个连接创建它,并将其传递给后续的初始化函数,但这导致了“Capturer只能被捕获一次!” 从VideoCapturer对象为第二个对等连接创建第二个VideoTrack时出现exception:

 E/AndroidRuntime(18956): FATAL EXCEPTION: Thread-1397 E/AndroidRuntime(18956): java.lang.RuntimeException: Capturer can only be taken once! E/AndroidRuntime(18956): at org.webrtc.VideoCapturer.takeNativeVideoCapturer(VideoCapturer.java:52) E/AndroidRuntime(18956): at org.webrtc.PeerConnectionFactory.createVideoSource(PeerConnectionFactory.java:113) E/AndroidRuntime(18956): at com.example.rtcapp.PeerConnectionClient.createVideoTrack(PeerConnectionClient.java:720) E/AndroidRuntime(18956): at com.example.rtcapp.PeerConnectionClient.createPeerConnectionInternal(PeerConnectionClient.java:482) E/AndroidRuntime(18956): at com.example.rtcapp.PeerConnectionClient.access$20(PeerConnectionClient.java:433) E/AndroidRuntime(18956): at com.example.rtcapp.PeerConnectionClient$2.run(PeerConnectionClient.java:280) E/AndroidRuntime(18956): at android.os.Handler.handleCallback(Handler.java:733) E/AndroidRuntime(18956): at android.os.Handler.dispatchMessage(Handler.java:95) E/AndroidRuntime(18956): at android.os.Looper.loop(Looper.java:136) E/AndroidRuntime(18956): at com.example.rtcapp.LooperExecutor.run(LooperExecutor.java:56) 

尝试在PeerConnectionClients之间共享VideoTrack对象导致本机代码出现此错误:

 E/libjingle(19884): Local fingerprint does not match identity. E/libjingle(19884): P2PTransportChannel::Connect: The ice_ufrag_ and the ice_pwd_ are not set. E/libjingle(19884): Local fingerprint does not match identity. E/libjingle(19884): Failed to set local offer sdp: Failed to push down transport description: Local fingerprint does not match identity. 

在PeerConnectionClients之间共享MediaStream会导致应用程序突然关闭,而Logcat中不会显示任何错误消息。

Solutions Collecting From Web of "如何使用WebRTC Native Code for Android实现三方电话会议video聊天?"

您遇到的问题是PeerConnectionClient 不是 PeerConnection的包装器,它包含 PeerConnection。

我注意到这个问题没有回答,所以我想知道我是否可以提供一些帮助。 我查看了源代码,并且PeerConnectionClient对于单个远程对等体来说是非常硬编码的。 您需要创建PeerConnection对象的集合,而不是此行:

 private PeerConnection peerConnection; 

如果你环顾四周,你会发现它变得有点复杂。

createPeerConnectionInternal中的mediaStream逻辑应该只执行一次,您需要在PeerConnection对象之间共享流,如下所示:

 peerConnection.addStream(mediaStream); 

您可以查阅WebRTC规范或查看此stackoverflow问题以确认PeerConnectiontypes仅用于处理一个对等方。 这里也有些模糊。

所以你只维护一个mediaStream对象:

 private MediaStream mediaStream; 

所以主要的想法是一个MediaStream对象和你想要连接的对等体一样多的PeerConnection对象。 因此,您不会使用多个PeerConnectionClient对象,而是修改单个PeerConnectionClient以封装多客户端处理。 如果您确实想要使用多个PeerConnectionClient对象的设计,无论出于何种原因,您只需抽象出媒体流逻辑(以及应该只创建一次的任何支持types)。

您还需要维护多个远程video轨道,而不是现有的video轨道:

 private VideoTrack remoteVideoTrack; 

您显然只需要渲染一个本地摄像头并为远程连接创建多个渲染器。

我希望这是足够的信息让你回到正轨。

在Matthew Sanders的回答的帮助下,我设法让它工作,所以在这个答案中,我将更详细地描述一种调整示例代码以支持video会议呼叫的方法:

大多数更改需要在PeerConnectionClient ,但也需要在使用PeerConnectionClient的类中进行, PeerConnectionClient是您与信令服务器通信并建立连接的地方。

PeerConnectionClient内部,每个连接需要存储以下成员variables:

 private VideoRenderer.Callbacks remoteRender; private final PCObserver pcObserver = new PCObserver(); private final SDPObserver sdpObserver = new SDPObserver(); private PeerConnection peerConnection; private LinkedList queuedRemoteCandidates; private boolean isInitiator; private SessionDescription localSdp; private VideoTrack remoteVideoTrack; 

在我的应用程序中,我最多需要3个连接(用于4向聊天),所以我只存储了每个连接的数组,但是你可以将它们全部放在一个对象中并拥有一个对象数组。

 private static final int MAX_CONNECTIONS = 3; private VideoRenderer.Callbacks[] remoteRenders; private final PCObserver[] pcObservers = new PCObserver[MAX_CONNECTIONS]; private final SDPObserver[] sdpObservers = new SDPObserver[MAX_CONNECTIONS]; private PeerConnection[] peerConnections = new PeerConnection[MAX_CONNECTIONS]; private LinkedList[] queuedRemoteCandidateLists = new LinkedList[MAX_CONNECTIONS]; private boolean[] isConnectionInitiator = new boolean[MAX_CONNECTIONS]; private SessionDescription[] localSdps = new SessionDescription[MAX_CONNECTIONS]; private VideoTrack[] remoteVideoTracks = new VideoTrack[MAX_CONNECTIONS]; 

我在PCObserverSDPObserver类中添加了一个connectionId字段,在PeerConnectionClient构造函数中,我在数组中分配了观察者对象,并将每个观察者对象的connectionId字段设置为数组中的索引。 引用上面列出的成员variables的PCObserverSDPObserver所有方法都应该更改为使用connectionId字段索引到相应的数组中。

需要更改PeerConnectionClient回调:

 public static interface PeerConnectionEvents { public void onLocalDescription(final SessionDescription sdp, int connectionId); public void onIceCandidate(final IceCandidate candidate, int connectionId); public void onIceConnected(int connectionId); public void onIceDisconnected(int connectionId); public void onPeerConnectionClosed(int connectionId); public void onPeerConnectionStatsReady(final StatsReport[] reports); public void onPeerConnectionError(final String description); } 

以及以下PeerConnectionClient方法:

 private void createPeerConnectionInternal(int connectionId) private void closeConnectionInternal(int connectionId) private void getStats(int connectionId) public void createOffer(final int connectionId) public void createAnswer(final int connectionId) public void addRemoteIceCandidate(final IceCandidate candidate, final int connectionId) public void setRemoteDescription(final SessionDescription sdp, final int connectionId) private void drainCandidates(int connectionId) 

与observer类中的方法一样,需要更改所有这些函数以使用connectionId索引到相应的每个连接对象数组,而不是引用它们之前的单个对象。 还需要更改任何回调函数调用以返回connectionId

我用一个名为createMultiPeerConnection的新函数替换了createPeerConnection ,该函数传递了一个VideoRenderer.Callbacks对象数组,用于显示远程video流,而不是单个video流。 该函数调用createMediaConstraintsInternal()一次,并为每个PeerConnection调用createPeerConnectionInternal() ,循环从0MAX_CONNECTIONS - 1mediaStream对象仅在第一次调用createPeerConnectionInternal() ,只需将初始化代码包装在if(mediaStream == null)检查中即可。

我遇到的一个复杂问题是当应用程序关闭并且PeerConnection实例关闭并且MediaStream丢弃时。 在示例代码中, mediaStream使用addStream(mediaStream)添加到PeerConnection ,但是从不调用相应的removeStream(mediaStream)函数dispose()而是调用dispose() )。 然而,当有多个PeerConnection共享MediaStream对象时,这会产生问题(本机代码中的MediaStreamInterface中的ref计数断言),因为dispose()最终确定了MediaStream ,这应该仅在最后一个PeerConnection关闭时发生。 调用removeStream()close()也是不够的,因为它没有完全关闭PeerConnection ,这会在处理PeerConnectionFactory对象时导致断言崩溃。 我能find的唯一解决方法是将以下代码添加到PeerConnection类:

 public void freeConnection() { localStreams.clear(); freePeerConnection(nativePeerConnection); freeObserver(nativeObserver); } 

然后在完成除最后一个之外的每个PeerConnection时调用这些函数:

 peerConnections[connectionId].removeStream(mediaStream); peerConnections[connectionId].close(); peerConnections[connectionId].freeConnection(); peerConnections[connectionId] = null; 

并关闭最后一个像这样:

 peerConnections[connectionId].dispose(); peerConnections[connectionId] = null; 

修改PeerConnectionClient ,需要更改信令代码以正确的顺序设置连接,将正确的连接索引传递给每个函数并适当地处理回调。 我通过在socket.io socket id和连接id之间维护一个哈希来做到这一点。 当新客户加入房间时,每个现有成员都会向新客户发送要约并依次收到答复。 还需要初始化多个VideoRenderer.Callbacks对象,将它们传递到PeerConnectionClient实例,然后根据需要划分屏幕以进行电话会议。