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

时间:2015-10-04 00:30:08

标签: java android android-camera webrtc videochat

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

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

根据this answer(与Android无关),我应该通过创建多个PeerConnections来实现,因此每个聊天参与者将连接到其他2个参与者。

但是,当我创建多个PeerConnectionClient(一个包装PeerConection的Java类,它在libjingle_peerconnection_so.so中的本机端实现)时,由于与两者发生冲突而导致库内部抛出异常他们试图访问相机:

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.<init>(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共享相机,以便我可以将同一个视频发送给多个同行?

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

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

更新

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

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中不会显示任何错误消息。

2 个答案:

答案 0 :(得分:16)

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

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

private PeerConnection peerConnection;

如果你再多看一眼,你会发现它比那更复杂。

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

peerConnection.addStream(mediaStream);

您可以查阅WebRTC spec或查看此stackoverflow问题,确认PeerConnection类型仅用于处理一个对等方。它也有些模糊地隐含here

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

private MediaStream mediaStream;

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

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

private VideoTrack remoteVideoTrack;

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

我希望这足以让你重回正轨。

答案 1 :(得分:7)

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

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

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

private VideoRenderer.Callbacks remoteRender;
private final PCObserver pcObserver = new PCObserver();
private final SDPObserver sdpObserver = new SDPObserver();
private PeerConnection peerConnection;
private LinkedList<IceCandidate> 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<IceCandidate>[] 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];

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

需要更改PeerConnectionClient回调:

connectionId

还有以下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

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

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

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

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实例,然后根据需要划分屏幕以进行电话会议。