如何实现3路电话会议视频聊天与WebRTC Native Code for Android?

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

但是,现在我遇到了问题,超出了3路通话的范围。WebRTC 本机代码包附带的 AppRTCDemo 应用仅演示双向调用(如果第三方尝试加入聊天室,则会返回“聊天室已满”消息)。

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

但是,当我创建多个 PeerConnectionClient(一个 Java 类,它包装了一个 PeerConection,在 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 或任何信令服务器无关。

如何获取多个对等连接来共享摄像头,以便我可以将同一视频发送给多个对等方?

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

是否可能,如果是,如何实现?

更新:

我尝试在多个PeerConnectionClients之间共享一个VideoCapturerAndroid对象,仅为第一个连接创建它,然后将其传递到后续连接的初始化函数中,但是当从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 中不会出现任何错误消息。


答案 1

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

我注意到这个问题没有得到解答,所以我想看看我是否可以帮忙。我查看了源代码,PeerConnectionClient对于单个远程对等节点进行了非常硬编码。您需要创建 PeerConnection 对象的集合,而不是以下行:

private PeerConnection peerConnection;

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

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

peerConnection.addStream(mediaStream);

您可以查阅WebRTC规范或查看此堆栈溢出问题,以确认PeerConnection类型被设计为仅处理一个对等节点。这里也有些含糊地暗示了这一点

因此,您只维护一个 mediaStream 对象:

private MediaStream mediaStream;

因此,主要思想是一个MediaStream对象和尽可能多的PeerConnection对象,只要您有要连接到的对等体。因此,您不会使用多个 PeerConnectionClient 对象,而是修改单个 PeerConnectionClient 以封装多客户端处理。如果您出于某种原因确实想要设计多个PeerConnectionClient对象,则只需从中抽象出媒体流逻辑(以及任何应仅创建一次的支持类型)。

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

private VideoTrack remoteVideoTrack;

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

我希望这些信息足以让你回到正轨。


答案 2

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

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

在 内部,每个连接需要存储以下成员变量: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];

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

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)

与观察者类中的方法一样,需要将所有这些函数更改为使用 to 索引到每个连接对象的相应数组中,而不是引用它们以前的单个对象。回调函数的任何调用也需要更改以传递回回。connectionIdconnectionId

我用一个名为 的新函数替换,该函数传递一个用于显示远程视频流的对象数组,而不是单个对象。该函数为每个 s 调用一次,从 循环从 到 。该对象仅在第一次调用 时创建,只需将初始化代码包装在 check 中即可。createPeerConnectioncreateMultiPeerConnectionVideoRenderer.CallbackscreateMediaConstraintsInternal()createPeerConnectionInternal()PeerConnection0MAX_CONNECTIONS - 1mediaStreamcreatePeerConnectionInternal()if(mediaStream == null)

我遇到的一个复杂情况是,当应用程序关闭并且实例被关闭并处置时。在示例代码中,被添加到 using 中,但相应的函数从不被调用 (而是被调用)。但是,当有多个共享一个对象时,这会产生问题(在本机代码中的 MediaStreamInterface 中的 ref 计数断言),因为最终确定 ,这应该只发生在最后一个关闭时。调用 和 也是不够的,因为它不会完全关闭 ,这会导致在释放对象时断言崩溃。我能找到的唯一修复方法是将以下代码添加到类中:PeerConnectionMediaStreammediaStreamPeerConnectionaddStream(mediaStream)removeStream(mediaStream)dispose()PeerConnectionMediaStreamdispose()MediaStreamPeerConnectionremoveStream()close()PeerConnectionPeerConnectionFactoryPeerConnection

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;

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


推荐