https://d226lax1qjow5r.cloudfront.net/blog/blogposts/waiting-room-and-pre-call-best-practices-with-vonage-video-api/waitingroom_videoapi_1200x600.png

Prácticas recomendadas para salas de espera y prellamadas con Video API de Vonage

Publicado el July 13, 2021

Tiempo de lectura: 7 minutos

Cuando desarrolle su propia solución de videoconferencia, es vital ofrecer una buena experiencia previa a la llamada. Asegúrese de que el usuario puede elegir los dispositivos de audio y vídeo que va a utilizar, compruebe que el micrófono detecta su voz y que la potencia de su red es suficientemente buena. Marcar todas estas casillas le ayudará a crear una aplicación más sólida y a detectar algunos problemas que, con suerte, reducirán algunas fricciones con los usuarios finales de su aplicación.

También es muy común en estos días tener diferentes roles en su aplicación; si usted opera en el espacio de la salud, la educación o webinar, es posible que desee tener un Moderador. En esta entrada del blog, cubriremos cómo hacer que el resto de los participantes esperen a que el Moderador se una a la sesión hasta que comiencen a publicar.

En resumen, esta aplicación de ejemplo implementa:

Moderación. Espere a que el Moderador/Host comience a publicar en la sesión. Selección de dispositivos. Buenas prácticas previas a la llamada. Esto incluye una prueba de conectividad y calidad previa a la llamada y otras buenas prácticas como indicadores de nivel de audio.

Si esto te parece un plan, quédate por aquí. Si te da un poco de pereza y quieres, puedes ver el repositorio terminado aquí.

Estructura del proyecto

Servidor

El archivo principal del servidor es un servidor Node.js express básico que sirve dos archivos HTML en función de la ruta elegida (/host o /participant). El servidor también se encarga de generar las credenciales para la sesión (token e identificadores de sesión).

El servidor generará un token de moderador para el anfitrión o un token de editor para un participante. Para más información sobre la creación de tokens, visite este enlace. El servidor almacenará un mapa de sesión y roomNames en la memoria. Para una aplicación de producción, necesitará almacenar estas sesiones en una base de datos o similar.

Cliente

La aplicación utiliza Webpack para agrupar todos los archivos JavaScript y hacer que la aplicación sea más escalable y fácil de entender. También utiliza Bootstrap para simplificar el proceso de diseño de la interfaz de usuario.

Todos los archivos JavaScript se encuentran en la carpeta carpeta src:

El punto de entrada principal es index.js en la carpeta src. Este archivo obtendrá el roomName de la URL y, dependiendo de la ruta visitada, creará una instancia de Host o Participant. A continuación, inicializará el proceso.

import { Host } from "./Host";
import { Participant } from "./Participant";

(() => {
  const urlParams = new URLSearchParams(window.location.search);
  const roomName = urlParams.get("room");
  if (window.location.pathname === "/host") {
    const host = new Host(roomName);
    host.init();
  } else if (window.location.pathname === "/participant") {
    const participant = new Participant(roomName);
    participant.init();
  }
})();

La lógica de la aplicación tiene lugar en la Clase Host y en la clase Clase Participante dependiendo del rol del usuario conectado. Estas clases aprovecharán archivos adicionales para mejorar la legibilidad del código. Puedes comprobar los diferentes ficheros que utiliza nuestra aplicación.

Selección de dispositivos

La selección del dispositivo se implementará en ambas vistas (Participante y Anfitrión). Como se explica en la referencia MediaDevices APInuestra aplicación debe ser lo suficientemente robusta como para gestionar la conexión/desconexión de dispositivos durante la llamada.

¿Qué ocurre si se desconecta un dispositivo durante la llamada o se conecta uno nuevo? Nuestra aplicación tiene que ser lo suficientemente inteligente como para detectar un cambio en la lista de dispositivos disponibles. Configuraremos un receptor de eventos para que, cuando se produzca un cambio en los dispositivos disponibles, active una llamada a una función para actualizar nuestra interfaz de usuario y mostrar los últimos dispositivos disponibles.

En primer lugar, activaremos la primera llamada para actualizar nuestra lista de dispositivos una vez que el usuario conceda permiso para la cámara y/o el micrófono. Podemos hacerlo aprovechando los eventos accessAllowed emitidos por el editor.

this.publisher.on("accessAllowed", () => {
  refreshDeviceList(this.publisher);
});

A continuación, configuraremos un receptor de eventos que recalculará los dispositivos disponibles si se produce una actualización de los dispositivos multimedia disponibles durante la llamada.

navigator.mediaDevices.ondevicechange = () => {
  refreshDeviceList(this.publisher);
};

La función refreshDeviceList se encarga de añadir la lista de dispositivos de audio y Video a un elemento DOM. En este caso, utilizaré un menú desplegable por simplicidad. Si quieres ver más detalles sobre esta función, no dudes en consultar el código fuente de implementación de la función fuera. También añadiremos una etiqueta HTML selected a las fuentes actuales de audio y Video devueltas por getAudioSource() y getVideoSource respectivamente.

Cuando se trata de manejar el cambio de dispositivo durante la llamada, aprovecharemos las funciones setVideoSource y setAudioSource respectivamente. Añadiré aquí el proceso de uno de ellos para que lo entiendas mejor.

const onVideoSourceChanged = async (event, publisher) => {
  const labelToFind = event.target.value;
  const videoDevices = await listVideoInputs();
  const deviceId = videoDevices.find((e) => e.label === labelToFind)?.deviceId;

  if (deviceId != null) {
    publisher.setVideoSource(deviceId);
  }
};

Estableceremos un receptor de eventos al cambiar nuestro menú desplegable que activará la función onVideoSourceChanged función. Esta función buscará el ID del dispositivo a cuya etiqueta nos dirigimos. A continuación, llamará al método setVideoSource del objeto editor para cambiar la fuente de vídeo.

document.getElementById("audioInputs").addEventListener("change", (e) => {
  onAudioSourceChanged(e, this.waitingRoompublisher);
});

Esperar al anfitrión

Nuestra aplicación necesita saber si el usuario que se une es un Anfitrión o un Participante. En este caso, estoy sirviendo un archivo HTML diferente desde el lado del servidor dependiendo del rol del usuario ya que nuestro Anfitrión podrá desconectar a todos los Participantes de la llamada. Nuestro punto de entrada instanciará un Anfitrión o un Participante dependiendo de la URL a la que naveguemos. Por favor, ten en cuenta que esto no es una aplicación lista para producción, y deberías implementar autenticación en las rutas.

Toda la lógica comienza con nuestra init función index.js en la carpeta /src que será ejecutada en una instancia Host o Participant dependiendo de donde naveguemos.

La función init llamará a nuestra función getCredentials función credenciales.js con un rol de administrador para el anfitrión o de participante para el participante.

const getCredentials = async (roomName, role) => {
  try {
    const url = `/api/room/${roomName}?role=${role}`;
    const config = {};
    const response = await fetch(`${url}`, config);
    const data = await response.json();
    if (data.apiKey && data.sessionId && data.token) {
      return Promise.resolve(data);
    }
    return Promise.reject(new Error("Credentials Not Valid"));
  } catch (error) {
    console.log(error.message);
    return Promise.reject(error);
  }
};

Nuestro servidor generará entonces un token de moderador para el administrador/anfitrión o un token de editor para el participante. Para obtener más información sobre la creación de tokens y funciones, consulte nuestra documentación sobre la creación de tokens.

Eche un vistazo a la generación de tokens en el servidor.

Una vez recibido el token en el lado cliente, podemos saber si un Host o un Participante se conecta a la sesión escuchando eventos de conexión enviados por nuestro SDK. El flujo de la aplicación es el siguiente:

Si un anfitrión se une a la convocatoria, se conectará a la sesión y empezará a publicar inmediatamente. Si un participante se une a la convocatoria, realizaremos una prueba previa a la convocatoria y, a continuación, el participante se conectará a la sesión. Si ya hay un anfitrión conectado a la sesión, el participante empezará a publicar. En caso contrario, el participante permanecerá conectado hasta que se conecte un anfitrión y sólo entonces empezará a publicar.

Participante

Este es el aspecto de la init de nuestro Participante. Primero obtenemos credenciales para un Participante (rol de editor). También solicitamos credenciales separadas para nuestra prueba de pre-llamada para evitar que la sesión principal sea contaminada por conexiones/flujos de la prueba de pre-llamada. A continuación, iniciamos la prueba previa a la llamada (explicaré cómo hacerlo en un minuto) y, una vez finalizada la prueba, nos conectamos a la sesión.

init() {
  getCredentials(this.roomName, 'participant')
    .then(data => {
      this.roomToken = data.token;
      this.initializeSession(data);
      getCredentials(`${this.roomName}-precall`, 'participant').then(
        precallCreds => {
          startTest(precallCreds)
            .then(results => {
              this.precallTestDone = true;
              this.connect();
            })
            .catch(e => console.log(e));
        }
      );
      this.registerEvents();
    })
    .catch(e => console.log(e));
}

La función conectar comprobará si ya hay un Host conectado a la sesión o no. Si ya hay un Host, comenzaremos a publicar, si no, permaneceremos conectados.

connect() {
  this.session.connect(this.roomToken, error => {
    if (error) {
      handleError(error);
    } else {
      if (isHostPresent()) {
        this.handlePublisher();
      }
      console.log('Session Connected');
    }
  });
}

La función isHostPresent devolverá true si hay un host conectado a la sesión y false en caso contrario.

const isHostPresent = () => {
  if (usersConnected.find((e) => e.data === "admin")) {
    return true;
  } else {
    return false;
  }
};

La matriz usersConnected llevará la cuenta de las conexiones de la sesión. Lo incrementaremos cuando se produzca un evento connectionCreated y lo decrementaremos ante un evento connectionDestroyed evento. Es importante tener en cuenta que esta variable se incrementará desde ambas clases (Host y Participant) cuando haya una nueva conexión. Por lo tanto necesitaremos que esta variable sea accesible por ambas Clases.

this.session.on("connectionCreated", (event) => {
  connectionCount += 1;
  console.log("[connectionCreated]", connectionCount);
  usersConnected.push(event.connection);
  console.log(usersConnected);
  if (event.connection.data === "admin") {
    this.handlePublisher();
  }
});
this.session.on("connectionDestroyed", (event) => {
  connectionCount -= 1;
  console.log("[connectionDestroyed]", connectionCount);
  usersConnected = usersConnected.filter((connection) => {
    return connection.id != event.connection.id;
  });
  connectionCount -= 1;
  console.log(usersConnected);
});

Si el Anfitrión no está presente cuando el Participante se conecta a la sesión, esperaremos hasta que un nuevo Anfitrión se una a la sesión y comenzaremos a publicar entonces.

Prueba previa a la llamada

Otro aspecto importante para ofrecer una buena experiencia al cliente es realizar una comprobación de la conectividad y la calidad para asegurarse de que las cosas pueden ir lo mejor posible. Si los participantes tienen que esperar a que se incorpore el moderador, ¿por qué no aprovechamos este tiempo precioso para realizar una prueba previa a la llamada?

Utilizaremos la prueba de red para verificar que el participante tenga conectividad con los servidores de registro, mensajería, medios y API de Video API de Vonage, así como para verificar la calidad esperada durante la llamada. Ten en cuenta que el comportamiento de la red es dinámico, lo que significa que tener un resultado positivo antes de la llamada no garantiza que tu ancho de banda disponible no cambiará durante la llamada.

Para simplificar, sólo ejecutaremos una prueba previa a la llamada en nuestros Participantes, pero no en el Host. Por supuesto, puedes realizarla en ambos.

He creado unos archivos para gestionar la respuesta de la prueba de conectividad y calidad y también una barra de progreso para indicar el estado de la prueba. Es una simple barra de progreso de Bootstrap que se llena después de 30 segundos, que es aproximadamente el tiempo que tarda en completarse la prueba. Puedes modificar esto estableciendo un valor de tiempo de espera al instanciar el NetworkTest. Sin embargo, cuanto más tiempo se ejecute la prueba, más precisos serán los resultados. Si la prueba falla, también eliminaremos el indicador de progreso.

const handleTestProgressIndicator = () => {
  const progressIndicator = setInterval(() => {
    let currentProgress = progressBar.value;
    progressBar.value += 3.3;
    if (currentProgress === 100) {
      clearInterval(progressIndicator);
      progressBar.value = 0;
      progressBar.style.display = "none";
    }
  }, 1000);
};
const removeProgressIndicator = () => {
  progressBar.style.display = "none";
};

Si quieres echar un vistazo a la implementación de la prueba de red, comprueba el archivo donde manejo la lógica de pre-llamada.

Los resultados de la prueba de preconvocatoria también proporcionan una resolución recomendada y una puntuación MOS de 0 a 4,5.

Dado que esto es un poco subjetivo, añadiremos la posibilidad de decidir si queremos mostrar la Resolución preferida y una etiqueta de resultado basada en la puntuación MOS, es decir (Bueno, Malo, Excelente..).

Puede decidir si desea incluir la resolución recomendada y la etiqueta de puntuación activando la variable addFeedback en /src/variables.js. También puede aprovechar el módulo ErrorNames del módulo npm para añadir tus propios errores en función del error arrojado y añadir algunas recomendaciones a los usuarios.

¿Y ahora qué?

El proyecto completo está disponible en GitHuby puedes obtener más información sobre la Video API de Vonage en nuestra documentación.

Compartir:

https://a.storyblok.com/f/270183/384x384/6007824739/javier-molina-sanz.png
Javier Molina Sanz

Javier studied Industrial Engineering back in Madrid where he's from. He is now one of our Solution Engineers, so if you get into trouble using our APIs he may be the one that gives you a hand. Out of work he loves playing football and travelling as much as he can.