Подтвердить что ты не робот

Как реализовать трехсторонний конференц-звонок с чатом WebRTC для Android?

Я пытаюсь реализовать трехсторонний видеочат внутри приложения для Android, используя пакетный код для веб-сайтов для Android (т.е. не используя WebView). Я написал сервер сигнализации с помощью node.js и использовал библиотеку Gottox socket.io java client в клиентском приложении для подключения к сервер, обмениваться SDP-пакетами и устанавливать двухстороннее соединение видеочата.

Однако теперь у меня возникают проблемы, выходящие за рамки трехстороннего вызова. Приложение AppRTCDemo, которое поставляется вместе с внутренним кодом пакета WebRTC, демонстрирует только двухсторонние вызовы (если сторонняя сторона пытается присоединиться к комнате, возвращается сообщение "полная комната" ).

В соответствии с этим ответом (который не относится конкретно к Android), я должен сделать это, создав несколько PeerConnections, поэтому каждый участник чата подключится другим участникам.

Однако, когда я создаю более одного 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 или любым файлом сервера сигнализации.

Как получить несколько PeerConnections для совместного использования камеры, чтобы я мог отправлять одно и то же видео нескольким одноранговым узлам?

Одна из моих идей заключалась в том, чтобы реализовать какой-то класс камеры Singleton для замены VideoCapturerAndroid, который можно было бы использовать для нескольких соединений, но я даже не уверен, что это сработает, и я хотел бы знать, есть ли способ делать 3-сторонние вызовы с использованием API, прежде чем я начну взламывать внутри библиотеки.

Возможно ли, и если да, то как?

Update:

Я попробовал совместно использовать объект VideoCapturerAndroid между несколькими PeerConnectionClients, создав его только для первого подключения и передав его в функцию инициализации для последующих, но это привело к тому, что этот "Capturer можно взять только один раз!". исключение при создании второго VideoTrack из объекта VideoCapturer для второго однорангового соединения:

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)

Попытка совместного использования объекта VideoTrack между PeerConnectionClients привела к этой ошибке из собственного кода:

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.

Совместное использование MediaStream между PeerConnectionClients приводит к внезапному закрытию приложения без появления сообщения об ошибке в Logcat.

4b9b3361

Ответ 1

Проблема, с которой вы сталкиваетесь, заключается в том, что PeerConnectionClient не оболочка вокруг PeerConnection содержит PeerConnection.

Я заметил, что на этот вопрос не ответил, поэтому я хотел посмотреть, смогу ли я немного помочь. Я просмотрел исходный код, и PeerConnectionClient очень сильно закодирован для одного удаленного однорангового узла. Вам нужно будет создать коллекцию объектов PeerConnection, а не следующую строку:

private PeerConnection peerConnection;

Если вы посмотрите немного больше, вы заметите, что он становится немного более сложным, чем это.

Логика mediaStream в createPeerConnectionInternal должна выполняться только один раз, и вам необходимо разделить поток между вашими объектами PeerConnection следующим образом:

peerConnection.addStream(mediaStream);

Вы можете ознакомиться с спецификацией WebRTC или взглянуть на это fooobar.com/questions/240249/..., чтобы подтвердить, что тип PeerConnection предназначен для обработки только одного партнера. Это также несколько неопределенно подразумевается здесь.

Таким образом, вы поддерживаете только один объект mediaStream:

private MediaStream mediaStream;

Итак, главная идея - это один объект MediaStream и столько же объектов PeerConnection, сколько у вас есть одноранговые узлы, к которым вы хотите подключиться. Таким образом, вы не будете использовать несколько объектов PeerConnectionClient, а скорее измените единственный PeerConnectionClient, чтобы инкапсулировать обработку нескольких клиентов. Если вы хотите пойти с дизайном нескольких объектов PeerConnectionClient по какой-либо причине, вам просто нужно будет абстрагировать логику медиапотока (и любые типы поддержки, которые должны быть созданы только один раз) из нее.

Вам также потребуется поддерживать несколько удаленных видеодорожек, а не существующих:

private VideoTrack remoteVideoTrack;

Вы, очевидно, только хотели бы сделать одну локальную камеру и создать несколько рендерингов для удаленных подключений.

Надеюсь, этого достаточно, чтобы вернуть вас в нужное русло.

Ответ 2

С помощью ответа Мэтью Сандерса мне удалось заставить его работать, поэтому в этом ответе я расскажу более подробно об одном способе адаптации образца кода для поддержки вызова видеоконференции:

Большинство изменений необходимо сделать в 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];

Я добавил поле connectionId в классы PCObserver и SDPObserver, а внутри конструктора PeerConnectionClient я выделил объекты-наблюдатели в массиве и установил поле connectionId для каждого объекта-наблюдателя его индексу в массиве. Все методы PCObserver и SDPObserver, которые ссылаются на перечисленные выше переменные-члены, должны быть изменены на индекс в соответствующий массив, используя поле 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 назад.

Я заменил createPeerConnection на новую функцию с именем createMultiPeerConnection, которая передала массив объектов VideoRenderer.Callbacks для отображения удаленного видеопотока вместо одного. Функция вызывает createMediaConstraintsInternal() один раз и createPeerConnectionInternal() для каждого из PeerConnection s, с циклом от 0 до MAX_CONNECTIONS - 1. Объект mediaStream создается только при первом вызове createPeerConnectionInternal(), просто обернув код инициализации в тесте if(mediaStream == null).

Одна сложность, с которой я столкнулся, заключалась в том, когда приложение закрывается, а экземпляры PeerConnection закрываются и mediaStream удаляется. В примере кода mediaStream добавляется к PeerConnection с помощью addStream(mediaStream), но соответствующая функция removeStream(mediaStream) никогда не вызывается (вместо этого вызывается dispose()). Тем не менее это создает проблемы (подсчет числа ссылается в MediaStreamInterface в нативном коде), когда существует более одного PeerConnection совместного использования объекта mediaStream, поскольку dispose() завершает mediaStream, что должно произойти только тогда, когда последний PeerConnection замкнуто. Вызов removeStream() и close() также недостаточен, поскольку он не полностью отключает PeerConnection, и это приводит к сбою assert при утилизации объекта 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 и идентификатором соединения. Когда новый клиент присоединяется к комнате, каждый из существующих участников отправляет предложение новому клиенту и получает ответ по очереди. Также необходимо инициализировать несколько объектов VideoRenderer.Callbacks, передать их в экземпляр PeerConnectionClient и разделить экран, как вы хотите, на конференцию.