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

Я пытаюсь реализовать трехсторонний видеочат в приложении Android с помощью пакета собственного кода WebRTC для Android (т.е. без использования WebView). Я написал сервер сигнализации с использованием node.js и использовал java-клиент Gottox socket.io внутри клиентского приложения для подключения к серверу, обмена 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 для совместного использования камеры, чтобы я мог отправлять одно и то же видео более чем одному партнеру?

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

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

Обновление:

Я попытался поделиться объектом 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.


person samgak    schedule 04.10.2015    source источник
comment
Могу я спросить, что означает 3-стороннее видео?   -  person SilentKnight    schedule 10.10.2015
comment
@SilentKnight - видеоконференцсвязь с 3 участниками   -  person samgak    schedule 10.10.2015
comment
@Samgak Привет. Вы можете поделиться полным решением? У меня возникла проблема с подключением нескольких аудио.   -  person GensaGames    schedule 29.03.2016


Ответы (2)


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

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

private PeerConnection peerConnection;

Если вы посмотрите вокруг еще немного, то заметите, что все становится немного сложнее.

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

peerConnection.addStream(mediaStream);

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

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

private MediaStream mediaStream;

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

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

private VideoTrack remoteVideoTrack;

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

Я надеюсь, что этой информации достаточно, чтобы вы вернулись в нужное русло.

person Matthew Sanders    schedule 10.10.2015
comment
Спасибо за Ваш ответ. Я уже безуспешно пытался поделиться объектом MediaStream между несколькими PeerConnectionClients, я попробую ваше предложение об использовании одного PeerConnectionClient с несколькими PeerConnection - person samgak; 10.10.2015
comment
Полученные вами исключения относятся именно к этому. Использование нескольких клиентов PeerConnectionClients. Это будет пытаться использовать камеру дважды. Я понимаю, вы сказали, что пытались реорганизовать эту часть. Я просто предполагаю, что что-то должно быть упущено в рефакторинге, поскольку есть приличный объем логики, который вам нужно будет переместить. У вас был только один поток? Потому что это будет использовать videoCapturer mediaStream.addTrack (createVideoTrack (videoCapturer)); - person Matthew Sanders; 10.10.2015
comment
Да, у меня есть только один поток, я попытался разделить его между PeerConnectionClients, просто создав их один за другим и передав MediaStream, созданный первым потоком, в остальные (в этом случае строка кода в вашем комментарии не выполняется). Это не изящно, но я просто пытался выяснить, какими вещами нужно делиться, а какие - для каждого соединения, и довести его до этапа, на котором я могу без ошибок инициализировать PeerConnectionClients (перед фактическим подключением к любым одноранговым узлам). - person samgak; 10.10.2015
comment
Попался. Я понял это вскоре после того, как сделал свой комментарий. : P Похоже, что что-то упустили по пути. Контекст EGL и объект localRender также должны быть уникальными. На несвязанной ноте сообщение о заполнении комнаты, которое вы получили, скорее всего, будет на стороне сервера. - person Matthew Sanders; 10.10.2015
comment
Еще раз спасибо за вашу помощь, у меня все заработало, и я задокументировал этот процесс более подробно в самоответе. - person samgak; 17.10.2015

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

Большинство изменений необходимо внести в 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, циклически переходя от 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, и это приводит к сбою утверждения при удалении объекта 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 и разделить экран, как вы хотите для конференц-связи.

person samgak    schedule 17.10.2015
comment
Я рад, что смог помочь, и еще больше рад видеть ваш подробный ответ, который решил вашу проблему! - person Matthew Sanders; 17.10.2015
comment
@samgak Хорошая работа. Могу я спросить, как получить доступ к freePeerConnection () и freeObserver ()? Эти функции являются частными, и PeerConnection не имеет общедоступного конструктора, поскольку он создается PeerConnectionFactory. Поэтому я думаю, что не могу его расширить, и единственный способ, который я вижу, - это копирование всей библиотеки и ее изменение. - person Oliver Hausler; 13.11.2015
comment
@OliverHausler: да, к сожалению, мне пришлось это сделать. Я не мог найти способ закрыть PeerConnections без сбоев, не изменив класс PeerConnection в библиотеке. - person samgak; 16.11.2015
comment
@samgak спасибо за это. Теперь я могу иметь 2 или более одноранговых соединения одновременно. У меня остался вопрос. Предположим, A - клиент, B и C - одноранговые соединения. Как мне заставить B и C разговаривать друг с другом? Я разместил здесь вопрос stackoverflow.com/questions / 39579653 / Мы будем благодарны за любое направление. Спасибо! - person EdGs; 20.09.2016
comment
@samgak, Могу я взглянуть на класс PeerConnectionClient, где вы определили метод CreateMultiPeerConnection для моей справки? Я использую Xamarin и портировал AppRTC на Xamarin / .NET, и он работает так, как должен. Я собираюсь установить многостороннюю видео / аудиоконференцию и наткнулся на ваш пост. Интересно, что вы могли бы это сделать, и я тоже об этом думаю. Было бы неплохо, если бы я посмотрел и сохранил это для справки. Я создаю приложение в Xamarin для Android, и я использовал библиотеку привязки Xamarin Java для привязки библиотеки libjingle и ее зависимостей. - person Ram Iyer; 18.02.2017
comment
@RamIyer Вот класс PeerConnectionClient с моими изменениями: pastebin.com/c0YCHS6g - person samgak; 18.02.2017
comment
@samgak: Большое спасибо за сверхбыстрый ответ. Я посмотрю и доложу, если мне удастся добиться успеха. Кстати, а вы тоже делаете многостороннюю конференцию более 3-х подключений? Как вы думаете, каковы будут накладные расходы на производительность, если устройства будут сами управлять одноранговыми узлами? Я также думаю о модуле выборочной пересылки (SFU), таком как видеомост Jitsi, или модуле управления несколькими частями (MCU), таком как Licode, или медиа-сервере, таком как Kurento. - person Ram Iyer; 18.02.2017
comment
@RamIyer Мне никогда не приходилось иметь дело с более чем 3 подключениями, потому что это было игровое приложение с максимум 4 игроками. Я также мог предположить, что есть проводное интернет-соединение (хотя я тестировал с мобильными устройствами). Пропускная способность на устройство линейно зависит от количества подключений, поэтому чем больше подключений вы добавляете, тем хуже будет производительность, равно как и вероятность того, что хотя бы одно не сможет подключиться. У меня был пользовательский интерфейс с разделенным экраном, показывающий все потоки в любое время, но если вы показываете только один за раз, вы можете передавать звук только в невидимых потоках. У меня нет опыта использования SFU. - person samgak; 18.02.2017
comment
@RamIyer также, я использовал стороннюю службу STUN / TURN, избавил меня от огромной головной боли, поскольку запуск coturn работал только около 3/4 времени. - person samgak; 18.02.2017
comment
@samgak, Спасибо за комментарии. Я внес необходимые изменения в PeerConnectionClient. Можете ли вы также поделиться классом CallActivity? Я планирую установить максимальное количество подключений до 6 и проверить производительность. Я намереваюсь вызвать других одноранговых узлов в качестве отображения фрагмента HUD и дальнейшего расширения для отображения текущего говорящего в качестве основного дисплея (с использованием облачных голосовых / когнитивных сервисов - я не уверен, насколько это будет точно, особенно когда есть много трепа). Просто хочу взглянуть на ваш код для справки. - person Ram Iyer; 20.02.2017
comment
@samgak. Думаю, у вас есть возможность реализовать его для нескольких пользователей. Я с той же проблемой. Я подписался на этот пост, но не смог заставить его работать. Я продолжаю получать, что комната полна. Не могли бы вы передать код, чтобы помочь мне. :) - person Abdul Rauf; 12.06.2017
comment
@samgak, Вы должны написать об этом в блоге, это поможет многим разработчикам :) - person sid; 17.05.2018