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

How to implement 3-way conference call video chat with WebRTC Native Code for Android?

我正在尝试使用客户端应用程序中的 WebRTC Native Code package for Android (i.e. not using a WebView). I've written a signalling server using node.js and used the Gottox socket.io java client 库在 Android 应用程序中实现 3 向视频聊天,以连接到服务器、交换 SDP 数据包并建立 2-方式视频聊天连接。

但是现在我遇到了超出 3 向调用的问题。 WebRTC 本机代码包附带的 AppRTCDemo 应用仅演示双向调用(如果第 3 方尝试加入房间,则返回 "room full" 消息)。

根据 this answer(具体与 Android 无关),我应该通过创建多个 PeerConnections 来做到这一点,因此每个聊天参与者都将连接到其他 2 个参与者.

但是,当我创建多个PeerConnectionClient时(一个Java class包装了一个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(VideoCapturerAndroid.java:520)
E/VideoCapturerAndroid(21170):  at org.webrtc.VideoCapturerAndroid.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 共享摄像机,以便我可以将同一视频发送给多个对等方?

我的一个想法是实现某种单例相机 class 来替换可以在多个连接之间共享的 VideoCapturerAndroid,但我什至不确定这是否可行,我在我开始在图书馆内部闲逛之前,我想知道是否有办法使用 API 进行 3 向调用。

这可能吗?如果可能,怎么做?

更新:

我尝试在多个 PeerConnectionClient 之间共享一个 VideoCapturerAndroid object,仅为第一个连接创建它并将其传递给后续连接的初始化函数,但这导致了这个 "Capturer can only be taken once!" 从 VideoCapturer object 为第二个对等连接创建第二个 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(PeerConnectionClient.java:433)
E/AndroidRuntime(18956):    at com.example.rtcapp.PeerConnectionClient.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)

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

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.

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

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

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

private PeerConnection peerConnection;

如果你多看看周围,你会发现它变得有点复杂。

createPeerConnectionInternal 中的 mediaStream 逻辑只应执行一次,您需要像这样在 PeerConnection 对象之间共享流:

peerConnection.addStream(mediaStream);

可以参考WebRTC spec or take a look at this Whosebug question to confirm that the PeerConnection type was designed to only handle one peer. It is also somewhat vaguely implied here.

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

private MediaStream mediaStream;

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

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

private VideoTrack remoteVideoTrack;

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

我希望这些信息足以让您重回正轨。

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

大部分更改需要在 PeerConnectionClient 中进行,但也需要在使用 PeerConnectionClient 的 class 中进行,这是您与信令服务器通信并设置连接。

PeerConnectionClient里面,需要存储如下成员变量per-connection:

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 方聊天),所以我只存储了每个连接的数组,但你可以将它们全部放在 object 中并有一个数组objects.

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];

我在 PCObserverSDPObserver class 中添加了一个 connectionId 字段,并且在 PeerConnectionClient 构造函数中我分配了观察者 objects 并将每个观察者 object 的 connectionId 字段设置为其在数组中的索引。引用上面列出的成员变量的 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)

与观察者 classes 中的方法一样,所有这些函数都需要更改为使用 connectionId 索引到 per-connection [=94= 的适当数组中]s,而不是引用以前的单个 objects。回调函数的任何调用也需要更改以将 connectionId 传回。

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

我遇到的一个复杂情况是当应用程序关闭并且 PeerConnection 实例关闭并且 MediaStream 被处置时。在示例代码中,使用 addStream(mediaStream)mediaStream 添加到 PeerConnection,但从未调用相应的 removeStream(mediaStream) 函数(而是调用 dispose())。然而,当有多个 PeerConnection 共享 MediaStream object 时,这会产生问题(本机代码中 MediaStreamInterface 中的引用计数断言),因为 dispose() 最终确定了 MediaStream,这应该只在最后一个 PeerConnection 关闭时发生。调用 removeStream()close() 也是不够的,因为它没有完全关闭 PeerConnection 并且这会导致在处理 PeerConnectionFactory [=94= 时断言崩溃].我能找到的唯一修复方法是将以下代码添加到 PeerConnection class:

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 套接字 ID 和连接 ID 之间的散列来做到这一点。当新客户加入房间时,每个现有成员都会向新客户发送报价并依次收到答复。还需要初始化多个 VideoRenderer.Callbacks objects,将它们传递给 PeerConnectionClient 实例,并根据需要划分屏幕以进行电话会议。