如何使用适用于 Android 的 WebRTC Native Code 实现 3 路电话会议视频聊天?

Posted

技术标签:

【中文标题】如何使用适用于 Android 的 WebRTC Native Code 实现 3 路电话会议视频聊天?【英文标题】:How to implement 3-way conference call video chat with WebRTC Native Code for Android? 【发布时间】:2015-12-31 22:41:05 【问题描述】:

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

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

根据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 进行 3 向调用。

有可能吗?如果可以,怎么做?

更新:

我尝试在多个 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 中不会出现任何错误消息。

【问题讨论】:

请问“3路视频”是什么意思? @SilentKnight 3 人视频电话会议 @Samgak 嗨。你能分享完整的解决方案吗?我无法连接多个音频。 【参考方案1】:

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

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

private PeerConnection peerConnection;

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

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

peerConnection.addStream(mediaStream);

您可以咨询WebRTC spec 或查看此*** 问题以确认PeerConnection 类型旨在仅处理一个对等点。也有些含糊的暗示here。

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

private MediaStream mediaStream;

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

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

private VideoTrack remoteVideoTrack;

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

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

【讨论】:

感谢您的回答。我已经尝试在多个 PeerConnectionClients 之间共享一个 MediaStream 对象但没有成功,我会尝试你的建议,即使用单个 PeerConnectionClient 和多个 PeerConnections 您收到的异常正是来自于此。使用多个 PeerConnectionClients。这将尝试使用相机两次。我意识到你说过你试图重构那部分。我只是假设重构中一定遗漏了一些东西,因为您需要移动大量的逻辑。你只有一个流吗?因为这将使用 videoCapturer mediaStream.addTrack(createVideoTrack(videoCapturer)); 是的,我只有一个流,我尝试在 PeerConnectionClients 之间共享它,只需一个接一个地创建它们并将第一个创建的 MediaStream 传递到其余部分(在这种情况下,代码行在您的评论中未执行)。这并不优雅,但我只是想弄清楚哪些东西需要共享,哪些是每个连接的,并让它进入可以无错误初始化 PeerConnectionClients 的阶段(在实际连接到任何对等点之前)。 明白了。我发表评论后不久就意识到了这一点。 :P 这听起来确实像是在途中遗漏了一些东西。 EGL 上下文和 localRender 对象也需要是唯一的。在不相关的注释中,您收到的房间已满消息可能是服务器端的。 再次感谢您的帮助,我得到了它的工作,我已经在自我回答中更详细地记录了这个过程。【参考方案2】:

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

我在PCObserverSDPObserver 类中添加了一个connectionId 字段,在PeerConnectionClient 构造函数中,我在数组中分配了观察者对象,并将每个观察者对象的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)

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

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

我遇到的一个复杂情况是当应用程序关闭并且PeerConnection 实例被关闭并且MediaStream 被处置时。在示例代码中,mediaStream 使用addStream(mediaStream) 添加到PeerConnection,但从未调用过相应的removeStream(mediaStream) 函数(而是调用dispose())。然而,当有多个PeerConnection 共享MediaStream 对象时,这会产生问题(本机代码中MediaStreamInterface 中的引用计数断言),因为dispose() 最终确定了MediaStream,这应该只在最后一个@987654362 时发生@ 已经关闭。调用removeStream()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 套接字 ID 和连接 ID 之间的哈希来做到这一点。当新客户加入房间时,每个现有成员都会向新客户发送报价并依次收到答复。还需要初始化多个VideoRenderer.Callbacks 对象,将它们传递给PeerConnectionClient 实例,然后根据需要划分屏幕以进行电话会议。

【讨论】:

很高兴能提供帮助,更高兴看到您的详细回答解决了您的问题! @samgak 干得好,这个。请问如何才能到达 freePeerConnection() 和 freeObserver()?这些函数是私有的,PeerConnection 没有公共构造函数,因为它是由 PeerConnectionFactory 创建的。所以我想我不能扩展它,我看到的唯一方法是复制整个库并修改它。 @OliverHausler 是的,不幸的是我必须这样做。如果不修改库中的 PeerConnection 类,我找不到关闭 PeerConnections 而不发生崩溃的方法。 @RamIyer 这是我修改后的 PeerConnectionClient 类:pastebin.com/c0YCHS6g @RamIyer 我从来不用处理超过 3 个连接,因为它是一个最多有 4 个玩家的游戏应用程序。我也可以假设有线互联网连接(尽管我确实使用移动设备进行了测试)。每台设备的带宽与连接数成线性关系,因此添加的连接越多,性能就会越差,至少有一个连接失败的可能性也是如此。我有一个分屏 UI,始终显示所有流,但如果一次只显示一个,则只能在不可见的流上传输音频。我没有任何使用 SFU 的经验。

以上是关于如何使用适用于 Android 的 WebRTC Native Code 实现 3 路电话会议视频聊天?的主要内容,如果未能解决你的问题,请参考以下文章

适用于 android 和 ios 的 twilio webrtc mediastream contentHint

Android无法在通信过程中从相机切换到屏幕共享webrtc

适用于 Android 的 XMPP/Jingle 语音库

WebRTC connectionState 停留在“new” - 仅适用于 Safari,适用于 Chrome 和 FF

webRTC 原生 Android 应用程序是不是需要特定的线程模型

没有独立应用程序的移动浏览器上的 Webrtc?