java - simplewebrtc - webrtc chat example
¿Cómo implementar el chat de video por videoconferencia de 3 vías con el código nativo de WebRTC para Android? (2)
Estoy tratando de implementar un chat de video de 3 vías dentro de una aplicación de Android usando el paquete de código nativo de WebRTC para Android (es decir, no estoy usando un WebView). Escribí un servidor de señalización utilizando node.js y utilicé la biblioteca cliente Gottox socket.io java dentro de la aplicación cliente para conectarse al servidor, intercambiar paquetes SDP y establecer una conexión de chat de video bidireccional.
Sin embargo, ahora tengo problemas para ir más allá de eso a una llamada de tres vías. La aplicación AppRTCDemo que viene con el paquete de código nativo WebRTC muestra solo las llamadas de dos vías (si un tercero intenta unirse a una sala, se devuelve un mensaje de "sala llena").
De acuerdo con esta respuesta (que no se relaciona específicamente con Android), se supone que debo hacerlo creando múltiples Conexiones de Peer, para que cada participante del chat se conecte con los otros 2 participantes.
Sin embargo, cuando creo más de un PeerConnectionClient (una clase Java que envuelve un PeerConection, que se implementa en el lado nativo en libjingle_peerconnection_so.so), hay una excepción lanzada desde dentro de la biblioteca que resulta de un conflicto con ambos tratando de acceder a la cámara:
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)
Esto sucede al inicializar el cliente local incluso antes de intentar establecer una conexión, por lo que no está relacionado con node.js, socket.io o cualquiera de las cosas del servidor de señalización.
¿Cómo obtengo varias conexiones entre pares para compartir la cámara y poder enviar el mismo video a más de un par?
Una idea que tuve fue implementar algún tipo de clase de cámara singleton para reemplazar VideoCapturerAndroid que podría compartirse entre múltiples conexiones, pero ni siquiera estoy seguro de que funcionaría y me gustaría saber si hay una manera de hacerlo. Maneras de utilizar la API antes de que comience a piratear dentro de la biblioteca.
¿Es posible y si es así, cómo?
Actualizar:
Intenté compartir un objeto VideoCapturerAndroid entre varios PeerConnectionClients, creándolo solo para la primera conexión y pasándolo a la función de inicialización para los siguientes, pero eso dio lugar a que "Capturer solo se puede tomar una vez". excepción al crear un segundo VideoTrack desde el objeto VideoCapturer para la segunda conexión de igual:
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)
El intento de compartir el objeto VideoTrack entre PeerConnectionClients dio como resultado este error del código nativo:
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.
Compartir el MediaStream entre PeerConnectionClients hace que la aplicación se cierre repentinamente, sin que aparezca ningún mensaje de error en el Logcat.
Con la ayuda de la respuesta de Matthew Sanders, logré que funcionara, por lo que en esta respuesta describiré con más detalle una forma de adaptar el código de muestra para que sea compatible con las videoconferencias:
La mayoría de los cambios deben realizarse en PeerConnectionClient
, pero también en la clase que usa PeerConnectionClient
, que es donde se comunica con el servidor de señalización y configura las conexiones.
Dentro de PeerConnectionClient
, las siguientes variables miembro deben almacenarse por conexión:
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;
En mi aplicación necesitaba un máximo de 3 conexiones (para un chat de 4 vías), así que simplemente almacené una matriz de cada una, pero podría ponerlas todas dentro de un objeto y tener una matriz de objetos.
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];
PCObserver
un campo connectionId
a las clases PCObserver
y SDPObserver
, y dentro del constructor PeerConnectionClient
los objetos observadores en la matriz y configuré el campo connectionId
para cada objeto observador a su índice en la matriz. Todos los métodos de PCObserver
y SDPObserver
que hacen referencia a las variables miembro enumeradas anteriormente deben cambiarse para indexarse en la matriz apropiada utilizando el campo connectionId
.
Las devoluciones de llamada de PeerConnectionClient necesitan ser cambiadas:
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);
}
Y también los siguientes métodos de 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)
Al igual que con los métodos en las clases de observadores, todas estas funciones deben cambiarse para usar el connectionId
de connectionId
para indexar la matriz apropiada de objetos por conexión, en lugar de hacer referencia a los objetos individuales que eran anteriormente. Todas las invocaciones de las funciones de devolución de llamada también deben cambiarse para pasar el ID de connectionId
.
Reemplazé createPeerConnection
con una nueva función llamada createMultiPeerConnection
, a la que se le pasa una matriz de objetos VideoRenderer.Callbacks
para mostrar el flujo de video remoto, en lugar de uno solo. La función llama createMediaConstraintsInternal()
una vez y createPeerConnectionInternal()
para cada uno de los PeerConnection
s, pasando de 0
a MAX_CONNECTIONS - 1
. El objeto mediaStream
se crea solo en la primera llamada a createPeerConnectionInternal()
, simplemente envolviendo el código de inicialización en una comprobación if(mediaStream == null)
.
Una de las complicaciones que encontré fue cuando se cierra la aplicación y se PeerConnection
instancias de PeerConnection
y se PeerConnection
el MediaStream
. En el código de ejemplo, el mediaStream
se agrega a PeerConnection
mediante addStream(mediaStream)
, pero la función removeStream(mediaStream)
correspondiente nunca se llama ( dispose()
se llama en su lugar). Sin embargo, esto crea problemas (una afirmación de ref ref en MediaStreamInterface en el código nativo) cuando hay más de un PeerConnection
comparte un objeto MediaStream
porque dispose()
finaliza el MediaStream
, que solo debería ocurrir cuando se cierra el último PeerConnection
. Llamar a removeStream()
y close()
tampoco es suficiente, ya que no cierra completamente la PeerConnection
entre PeerConnection
y esto provoca una falla de PeerConnectionFactory
al desechar el objeto PeerConnectionFactory
. La única solución que pude encontrar fue agregar el siguiente código a la clase PeerConnection
:
public void freeConnection()
{
localStreams.clear();
freePeerConnection(nativePeerConnection);
freeObserver(nativeObserver);
}
Y luego llamar a estas funciones al finalizar cada PeerConnection
excepto el último:
peerConnections[connectionId].removeStream(mediaStream);
peerConnections[connectionId].close();
peerConnections[connectionId].freeConnection();
peerConnections[connectionId] = null;
y cerrando el último como este:
peerConnections[connectionId].dispose();
peerConnections[connectionId] = null;
Después de modificar PeerConnectionClient
, es necesario cambiar el código de señalización para configurar las conexiones en el orden correcto, pasar el índice de conexión correcto a cada una de las funciones y manejar las devoluciones de llamada de manera adecuada. Hice esto manteniendo un hash entre los identificadores de socket socket.io y un identificador de conexión. Cuando un nuevo cliente se une a la sala, cada uno de los miembros existentes envía una oferta al nuevo cliente y recibe una respuesta por turno. También es necesario inicializar varios objetos VideoRenderer.Callbacks
, pasarlos a la instancia de PeerConnectionClient
y dividir la pantalla como desee para una llamada de conferencia.
El problema que tiene es que PeerConnectionClient no es una envoltura alrededor de PeerConnection, ya que contiene una PeerConnection.
Noté que esta pregunta no fue respondida, así que quería ver si podía ayudar un poco. Busqué en el código fuente y PeerConnectionClient está muy codificado para un único par remoto. Necesitaría crear una colección de objetos PeerConnection en lugar de esta línea:
private PeerConnection peerConnection;
Si miras a tu alrededor un poco más, notarías que se vuelve un poco más complicado que eso.
La lógica de mediaStream en createPeerConnectionInternal solo debe hacerse una vez y usted necesita compartir la secuencia entre sus objetos de PeerConnection de esta manera:
peerConnection.addStream(mediaStream);
Puede consultar la especificación de WebRTC o echar un vistazo a esta pregunta de para confirmar que el tipo PeerConnection fue diseñado para manejar solo un igual. También está algo vagamente implícito here .
Entonces solo mantienes un objeto mediaStream:
private MediaStream mediaStream;
Entonces, nuevamente, la idea principal es un objeto MediaStream y tantos objetos PeerConnection como tenedores con los que quiera conectarse. Por lo tanto, no utilizará varios objetos PeerConnectionClient, sino que modificará el único PeerConnectionClient para encapsular el manejo de varios clientes. Si desea ir con un diseño de varios objetos PeerConnectionClient por cualquier motivo, solo tendría que abstraer la lógica de flujo de medios (y cualquier tipo de soporte que solo deba crearse una vez).
También deberá mantener varias pistas de video remotas en lugar de la existente:
private VideoTrack remoteVideoTrack;
Obviamente, solo le importaría renderizar una cámara local y crear múltiples renderizadores para las conexiones remotas.
Espero que esta información sea suficiente para volver a encaminarnos.