https://d226lax1qjow5r.cloudfront.net/blog/blogposts/vonage-video-express-with-ruby-on-rails-part-2/video-express-ruby_part_2.png

Vonage Video Express con Ruby on Rails Parte 2

Publicado el August 10, 2022

Tiempo de lectura: 10 minutos

Esta es la segunda parte de una serie de dos partes sobre la creación de una aplicación de visionado de vídeo utilizando Ruby on Rails con la Video API de Vonage y la biblioteca Video Express.

En la Parte 1repasamos los pasos necesarios para crear la aplicación Rails, mostramos cómo utilizar algunos componentes de Vivid y conseguimos ejecutar el videochat de Video Express. Si aún no has leído ese post, sería un buen lugar para empezar.

Una vez que hayamos terminado, ¡tendremos una aplicación que podremos utilizar para chatear con nuestros amigos y ver deportes o vídeos juntos!

Qué hará la aplicación

Estamos construyendo una aplicación de videoconferencia que ofrece a los usuarios una barra de herramientas para diferentes controles de audio/vídeo. Además, la aplicación ofrece al moderador la posibilidad de enviar al Watch Party a diferentes modos de visualización.

En este punto, tenemos un Video Express que funciona Sala. Este objeto nos da la capacidad de llamar a diferentes funciones que realizan las acciones en nuestra barra de herramientas. Queremos dar al usuario una manera de desencadenar esta funcionalidad, lo haremos con componentes Vivid. Organizaremos nuestro HTML y JS en componentes. Con Webpack, entonces import nuestros módulos y require nuestros componentes en application.js que expondrán nuestro Javascript al lado del cliente.

Creación de componentes auxiliares

El resto de este tutorial será la construcción de los componentes que permiten a los usuarios controlar su sala de Video Express. Cada componente seguirá una estructura similar: HTML con componentes Vivid y Javascript para activar las funciones de Video Express.

Organizar el HTML

Vamos a construir nuestros parciales donde vivirá el HTML. Desde la raíz de su aplicación, utilizando la línea de comandos, ejecute:

mkdir app/views/components

touch app/views/components/_header.html.erb touch app/views/components/_toolbox.html.erb

Y actualizar el archivo party.html.erb para renderizar los parciales:

<header>
  <%= render partial: 'components/header' %>
</header>

<main class="app">
  <div id="roomContainer"></div>
  <toolbar>
    <%= render partial: 'components/toolbar' %>
  </toolbar>

Organizar el Javascript

Al igual que tenemos una carpeta components en nuestras Vistas, vamos a crear una carpeta components en nuestra carpeta Javascript para alojar nuestra lógica de componentes correspondiente.

mkdir app/javascript/components

Aquí añadiremos los archivos de nuestros componentes: touch app/javascript/components/header.js touch app/javascript/components/toolbar.js

Para requerirlos para nuestro lado cliente a través de Webpack, añade las siguientes líneas en Application.js debajo de nuestro módulo imports.

require("components/header");
require("components/toolbar");

Como nuestro Javascript responderá a las acciones del usuario en el DOM, queremos asegurarnos de que Rails carga el Javascript después de que se haya cargado el DOM. Así que tenemos que hacer una pequeña adición y añadir defer:true a la directiva javascript_pack_tag en application.html.erb:

<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload', defer: true %>

Ahora estamos listos para construir nuestros componentes.

Construcción de la cabecera

Construir el HTML

Un recordatorio de la cabecera que queremos construir:

The Header in Moderator ViewThe Header in Moderator View

Una gran noticia es que Vivid tiene exactamente lo que necesitamos, una Top App Bar superior. La barra superior de aplicaciones viene con unas cuantas opciones de ranura, pero hay dos que nos importan: title y actionItems. El título es ideal para un logotipo o en nuestro caso título. Y el actionitems puede usarse como contenido de la barra de aplicaciones. Aquí es donde añadiremos el toggler para que el moderador cambie de modo entre modo chill y modo fiesta. Podemos lograr esto con el componente vwc-switch componente.

Dentro de app/view/watch_party/_header.html.erb tendrá este aspecto:

<vwc-top-app-bar-fixed alternate="true">
  <span slot="title" id="title">Big Game Chill Zone</span>
  <% if @name == @moderator_name  %>
    <span slot="actionItems" id="mode-name">Chill Mode</span>
    <vwc-switch slot="actionItems"></vwc-switch>
  <% else %>
    <span slot="actionItems"><%= @moderator_name %> is the host</span>
  <% end %>
</vwc-top-app-bar-fixed>

Construcción de la cabecera Javascript

Dentro del archivo components/header.js tendremos 3 partes esenciales: la escucha de un toggle, el toggle de la pantalla compartida y el toggle de los visuales que la acompañan.

Nota importante sobre Video Express

En Video Express tienes dos configuraciones: "Cuadrícula" y "Altavoz activo". Video Express te ofrece una videollamada estandarizada que funciona rápidamente. Pero si quieres desviarte del comportamiento por defecto, ten cuidado y probablemente será mejor que utilices la Video API completa.

En este ejemplo, el moderador es el único que comparte pantalla. Podríamos pensar que la disposición "Orador activo" nos permitiría hacer de la pantalla compartida la pantalla dominante. Sin embargo, en las videoconferencias estandarizadas, el comportamiento esperado es que el compartidor de pantalla presente desde otra pestaña. Así que por defecto, Video Express no hace que su ventana de pantalla compartida sea la vista dominante.

En nuestro caso de uso, el moderador no necesita controlar la pantalla y quiere ver su screenshare en grande, como todos los demás. Este no es el comportamiento por defecto. Tendremos que construirlo nosotros mismos. Afortunadamente, Video Express tiene la opción [screenSharingContainer](https://tokbox.com/developer/video-express/reference/room.html) que nos permitirá construir lo que queremos.

Podemos ver en la documentación que sólo tenemos que añadir un DIV vacío con id de screenSharingContainer. Pero tenemos que hacer que se vea bien y todavía ser capaz de tomar ventaja de VideoExpress LayoutManager para la capacidad de respuesta. Y, sólo queremos que esto se aplique para el moderador en el modo de compartir pantalla. Así que nuestra solución es añadir el screenSharingContainer al lado del layoutContainer y escribir algo de CSS para que la vista del moderador sea lo más parecida posible a lo que ven los demás.

Primero vamos a crear la lógica base del listener para el toggle:

const switch_btn = document.querySelector('vwc-switch');

if (switch_btn !== null){
  switch_btn.addEventListener('change', (event) => {
    if (event.target.checked){
      <!-- Custom Styles To Scope Moderator View --->
      <!-- Start Screen Share -->
    }
    else if (!event.target.checked){
      <!-- Stop Screen Share -->
      <!-- Remove Custome Styles From Moderator --->
    } else{
        console.log("Error in Switch Button Listener");
      }
  });
}

Ahora, antes de que podamos activar la captura de pantalla en Video Express, tenemos que preparar la vista del Moderador para que el estilo personalizado, que tocará el botón layoutContainerWrapper y layoutContainer no afecte a la vista de los demás. Llamaremos a esta función addModeratorCustomStyles.

let addModeratorCustomStyles = () => {
  mode_name.innerHTML = "Watch Mode"
  layoutContainerWrapper.firstElementChild.classList.add("moderator-screenshare");
  layoutContainerWrapper.classList.add("moderator-screenshare");
  screenShare.setAttribute("id", "screenSharingContainer");
  layoutContainer.appendChild(screenShare);
}

Podemos ver que esta función hace dos cosas: actualiza la etiqueta del toggler y añade un id de screenSharingContainer. Este id añadido nos ayuda a delimitar el CSS para que sólo se aplique al Moderador.

Cuando el screenShare se detenga, tendremos que eliminar el estilo personalizado para que la vista del Moderador no se desordene. Así que tenemos una función removeModeratorCustomStyles para deshacer todo lo anterior:

let removeModeratorCustomStyles = () => {
  mode_name.innerHTML = "Chill Mode";
  layoutContainerWrapper.firstElementChild.classList.remove("moderator-screenshare");
  layoutContainerWrapper.classList.remove("moderator-screenshare");
  screenShare.removeAttribute("id");
  layoutContainer.removeChild(layoutContainer.lastChild);
}

Ahora nuestra cabecera está básicamente completa. Sólo tenemos que consultar nuestros elementos y llamar a las funciones de compartición de pantalla de Video Express. La cabecera completa tiene este aspecto:

const switch_btn = document.querySelector('vwc-switch');
const layoutContainer = document.querySelector('#layoutContainerWrapper');
const screenShare = document.createElement('div');
const layoutContainerWrapper = document.querySelector('#layoutContainerWrapper');
const mode_name = document.querySelector('#mode-name');

let addModeratorCustomStyles = (layoutContainer, screenShare, layoutContainerWrapper, mode_name) => {
  mode_name.innerHTML = "Watch Mode";
  layoutContainerWrapper.firstElementChild.classList.add("moderator-screenshare");
  layoutContainerWrapper.classList.add("moderator-screenshare");
  screenShare.setAttribute("id", "screenSharingContainer");
  layoutContainer.appendChild(screenShare);
}

let removeModeratorCustomStyles = (layoutContainer, screenShare, layoutContainerWrapper, mode_name) => {
  mode_name.innerHTML = "Chill Mode";
  layoutContainerWrapper.firstElementChild.classList.remove("moderator-screenshare");
  layoutContainerWrapper.classList.remove("moderator-screenshare");
  screenShare.removeAttribute("id");
  layoutContainer.removeChild(layoutContainer.lastChild);
}

if (switch_btn !== null){
  switch_btn.addEventListener('change', (event) => {
    if (event.target.checked){
      addModeratorCustomStyles(layoutContainer, screenShare, layoutContainerWrapper, mode_name);
      room.startScreensharing('screenSharingContainer');
    } else if (!event.target.checked){
        room.stopScreensharing('screenSharingContainer');
        removeModeratorCustomStyles(layoutContainer, screenShare, layoutContainerWrapper, mode_name);
    } else{
        console.log("Error in Switch Button Listener");
    }
  });
}

Creación de la barra de herramientas HTML

Ahora podemos añadir el último y más complicado componente que tenemos: la barra de herramientas. Pero no será tan malo, sólo construir el HTML con componentes Vivid y luego añadir algo de Javascript para activar las funciones de Video Express. Ya sabes cómo funciona.

Un recordatorio de la barra de herramientas que queremos construir:

The Toolbar To Control The Video CallThe Toolbar To Control The Video Call

Construcción de los botones basculantes

Podemos ver en la barra de herramientas que hay 3 grupos de botones que activarán/desactivarán algunas características de la sala: silenciar/desactivar todo, desactivar/activar micrófono y desactivar/activar cámara de Video. Para los tres usaremos dos botones de Vivid y luego usaremos Javascript para ocultar el botón inactivo.

<!-- Mute all / Unmute all -->
<vwc-icon-button icon="audio-max-solid" shape="circled" layout="filled" id="mute-all" class="white-border"></vwc-icon-button>
<vwc-icon-button icon="audio-off-solid" shape="circled" layout="filled" id="unmute-all" class="hidden white-border"></vwc-icon-button>
<!-- Mute self / Unmute self -->
<vwc-icon-button icon="mic-mute-solid" shape="circled" layout="ghost" id="mute-self" class="vvd-scheme-alternate" ></vwc-icon-button>
<vwc-icon-button icon="microphone-2-solid" shape="circled" layout="ghost" id="unmute-self" class="hidden vvd-scheme-alternate"></vwc-icon-button>
<!-- Disable camera / Enable camera -->
<vwc-icon-button icon="video-off-solid" shape="circled" layout="ghost" id="hide-self" class="vvd-scheme-alternate" ></vwc-icon-button>
<vwc-icon-button icon="video-solid" shape="circled" layout="ghost" id="unhide-self" class="hidden vvd-scheme-alternate"></vwc-icon-button>

Crear selecciones desplegables

Sin embargo, podemos ver que el segundo y el tercer grupo de botones son un poco diferentes. También deberían ir acompañados de un desplegable que permita al usuario seleccionar la entrada asociada: micrófono o cámara. Esto es posible con el elemento <vwc-action-group elemento. El lado izquierdo del grupo de acciones viene directamente de la documentación con un botón y un separador. En el lado derecho, haremos uso del componente Vivid vwc-select para generar un elemento select al que podemos apuntar con las diferentes opciones que recibimos de VideoExpress. También pasamos las vwc-select dos opciones: seleccionado y desactivado para indicarle que muestre y desactive el predeterminado vwc-list-item que actuará como etiqueta.

Nuestros dos grupos de acciones con botones tienen ahora este aspecto:

<!-- Mute Self / Unmute Self -->
<!-- Select Mic Input -->
<vwc-action-group layout="outlined" shape="pill" class="vvd-scheme-alternate">
  <vwc-icon-button icon="mic-mute-solid" shape="circled" layout="ghost" id="mute-self" class="vvd-scheme-alternate" ></vwc-icon-button>
  <vwc-icon-button icon="microphone-2-solid" shape="circled" layout="ghost" id="unmute-self" class="hidden vvd-scheme-alternate"></vwc-icon-button>
  <span role="separator"></span>
  <vwc-select appearance="ghost" id="audio-input" class="select-max-width">
    <vwc-list-item
      disabled
      selected
    >
    Mic
    </vwc-list-item>
  </vwc-select>
</vwc-action-group>
<!-- Disable Camera / Enable Camera -->
<!-- Select Camera Input -->
<vwc-action-group layout="outlined" shape="pill" class="vvd-scheme-alternate">
  <vwc-icon-button icon="video-off-solid" shape="circled" layout="ghost" id="hide-self" class="vvd-scheme-alternate" ></vwc-icon-button>
  <vwc-icon-button icon="video-solid" shape="circled" layout="ghost" id="unhide-self" class="hidden vvd-scheme-alternate"></vwc-icon-button>
  <span role="separator"></span>
  <vwc-select appearance="ghost" class="select-max-width" id="video-input">
    <vwc-list-item
      disabled
      selected
    >
    Camera
    </vwc-list-item>
  </vwc-select>
</vwc-action-group>

Podemos ver que tenemos un tercer grupo de acciones: las entradas de audio. Podemos utilizar la misma estructura:

<!-- Select Audio Output -->
<vwc-action-group layout="outlined" shape="pill" class="vvd-scheme-alternate" id="audio-output-target">
  <vwc-icon-button icon="headset-solid" shape="circled" layout="ghost" class="vvd-scheme-alternate"></vwc-icon-button>
  <span role="separator"></span>
  <vwc-select appearance="ghost" id="audio-output" class="select-max-width">
    <vwc-list-item
      disabled
      selected
    >
    Audio
    </vwc-list-item>
  </vwc-select>
</vwc-action-group>

Construir el HTML de ToolTips

Demos a nuestros usuarios un poco más de información sobre estos componentes. Podemos hacerlo con elegancia utilizando tooltips de Vivid.

Los tooltips que nos interesan constan de tres partes: el anchor, el cornery la id. El ancla indica al tooltip a qué elemento HTML debe engancharse. La esquina esquina indica a la información emergente en qué dirección debe mostrarse, en relación con el ancla. Y la esquina id es algo que utilizaremos en Javascript para activar la información sobre herramientas cuando el usuario pase el ratón por encima del ancla.

Con los Tooltips añadidos, nuestro _toolbar.html.erb tiene este aspecto:

<!-- Mute all / Unmute all -->
<vwc-icon-button icon="audio-max-solid" shape="circled" layout="filled" id="mute-all" class="white-border"></vwc-icon-button>
<vwc-icon-button icon="audio-off-solid" shape="circled" layout="filled" id="unmute-all" class="hidden white-border"></vwc-icon-button>
<vwc-tooltip anchor="mute-all" text="Mute All" corner="top" id="mute-all-tooltip"></vwc-tooltip>
<vwc-tooltip anchor="unmute-all" text="Un-Mute All" corner="top" id="unmute-all-tooltip"></vwc-tooltip>

<!-- Mute Self / Unmute Self -->
<!-- Select Mic Input -->
<vwc-action-group layout="outlined" shape="pill" class="vvd-scheme-alternate">
  <vwc-icon-button icon="mic-mute-solid" shape="circled" layout="ghost" id="mute-self" class="vvd-scheme-alternate" ></vwc-icon-button>
  <vwc-icon-button icon="microphone-2-solid" shape="circled" layout="ghost" id="unmute-self" class="hidden vvd-scheme-alternate"></vwc-icon-button>
  <vwc-tooltip anchor="mute-self" text="Disable Mic" corner="top" id="mute-self-tooltip"></vwc-tooltip>
  <vwc-tooltip anchor="unmute-self" text="Enable Mic" corner="top" id="unmute-self-tooltip"></vwc-tooltip>
  <span role="separator"></span>
  <vwc-select appearance="ghost" id="audio-input" class="select-max-width">
    <vwc-list-item
      disabled
      selected
    >
    Mic
    </vwc-list-item>
  </vwc-select>
</vwc-action-group>



<!-- Disable Camera / Enable Camera -->
<!-- Select Camera Input -->
<vwc-action-group layout="outlined" shape="pill" class="vvd-scheme-alternate">
  <vwc-icon-button icon="video-off-solid" shape="circled" layout="ghost" id="hide-self" class="vvd-scheme-alternate" ></vwc-icon-button>
  <vwc-icon-button icon="video-solid" shape="circled" layout="ghost" id="unhide-self" class="hidden vvd-scheme-alternate"></vwc-icon-button>
  <vwc-tooltip anchor="hide-self" text="Disable Camera" corner="top" id="hide-self-tooltip"></vwc-tooltip>
  <vwc-tooltip anchor="unhide-self" text="Enable Camera" corner="top" id="unhide-self-tooltip"></vwc-tooltip>
  <span role="separator"></span>
  <vwc-select appearance="ghost" class="select-max-width" id="video-input">
    <vwc-list-item
      disabled
      selected
    >
    Camera
    </vwc-list-item>
  </vwc-select>
</vwc-action-group>

<!-- Select Audio Output -->
<vwc-action-group layout="outlined" shape="pill" class="vvd-scheme-alternate" id="audio-output-target">
  <vwc-tooltip anchor="audio-output-target" text="Select Audio Output" corner="top" id="audio-output-tooltip"></vwc-tooltip>
  <vwc-icon-button icon="headset-solid" shape="circled" layout="ghost" class="vvd-scheme-alternate"></vwc-icon-button>
  <span role="separator"></span>
  <vwc-select appearance="ghost" id="audio-output" class="select-max-width">
    <vwc-list-item
      disabled
      selected
    >
    Audio
    </vwc-list-item>
  </vwc-select>
</vwc-action-group>

Estilizar la barra de herramientas

Antes de construir el Javascript para los componentes, vamos a hacer que la barra de herramientas se parezca a nuestra maqueta:

// toolbar styles
toolbar {
  display: flex;
  justify-content: space-around;
  margin: 0 auto;
  width: 650px;
  background-color: var(--vvd-color-primary);
  padding: 10px;
  border-radius: 8px 8px 0 0;
}

.hidden {
  display: none;
}

.white-border {
  border: 2px solid white;
  border-radius: 50%;
}

.select-max-width {
  max-width: 130px;
}

vwc-tooltip {
  --tooltip-inline-size: 100px;
  text-align: center;
}

Creación de la barra de herramientas Javascript

De izquierda a derecha, el primer componente que tenemos que construir es el botón "Silenciar todo". Queremos que sea un conmutador que active y desactive el audio de todos los demás participantes. Pero, ¿cómo podemos tener un botón de alternancia? En realidad necesitamos dos botones: uno para "Silenciar todo" y otro para "Desactivar todo". Este patrón se repetirá varias veces en la barra de herramientas, así que vamos a escribir una función de ayuda para ello.

// toggle hide/display of buttons
let toggleButtonView = (buttonToHide, buttonToShow) => {
  buttonToHide.style.display = "none";
  buttonToShow.style.display = "block";
}

Construir el botón silenciar otros

Para silenciar a todos los participantes, tenemos que esperar a que el usuario active la acción y, a continuación, recorrer todos los participantes en la instancia de la sala de este usuario. Es importante tener en cuenta que la sala es local para cada usuario.

Así que para desactivar el audio, tenemos que iterar a través de todos los participantes en la sala y utilizar la función .camera.disableAudio() . Notarás que la llamamos en participant[1] porque el objeto participant devuelve un array: [idparticipantObject].

// toggle Mute All / Unmute All
let toggleMuteAllButton = (button, state, participants) =>{
  button.addEventListener("click", function(){
    Object.entries(participants).forEach(participant => {
      if (state === "mute"){
        toggleButtonView(mute_all_btn, unmute_all_btn)
        participant[1].camera.disableAudio();
      } else if (state === "unmute") {
        toggleButtonView(unmute_all_btn, mute_all_btn)
        participant[1].camera.enableAudio();
      } else {
        console.log("Error in toggleMuteAll")
      }
    })
  })
}
const mute_all_btn = document.querySelector('#mute-all');
const unmute_all_btn = document.querySelector('#unmute-all');
toggleMuteAllButton(mute_all_btn, "mute", room.participants);
toggleMuteAllButton(unmute_all_btn, "unmute", room.participants);

Construir los botones de silenciar y desactivar la cámara

Los botones Mute/Unmute Self y Disable/Enable Camera siguen la misma lógica que el botón MuteAll. Escuchan una acción del usuario, comprueban un estado y llaman a una acción en VideoExpress. Sin embargo son mucho mas simples porque VideoExpress nos da funciones para chequear el estado. Las dos funciones son room.camera.isVideoEnabled y room.camera.isAudioEnabled. Además sólo necesitamos activar la acción en un único usuario.

Podemos crear esta función toggleInputButton que aceptará una condición, el booleano que recibimos de nuestras funciones de Video Express, y luego llamará a la acción correspondiente de Video Express. También actualizará la vista con toggleButtonView.

// toggle button (display and functionality) of any audio and video input devices
let toggleInputButton = (condition, defaultBtn, altBtn, action) => {
  if (condition()){
    toggleButtonView(defaultBtn, altBtn);
    action();
  } else if (!condition()){
    toggleButtonView(altBtn, defaultBtn);
    action();
  } else {
    console.log(`Error in toggleInputButton. Condition: ${condition}`);
  }
}

The toggle will need to be triggered on a user click, so we can wrap this in the `listenForToggle` function.

// listen for clicks to trigger toggle of input buttons
let listenForToggle = (condition, defaultBtn, altBtn, defaultAction, altAction) => {
    defaultBtn.addEventListener("click", function(){
      console.log("inside listenfortoggle listener")
      toggleInputButton(condition, defaultBtn, altBtn, defaultAction)
    })
    altBtn.addEventListener("click", function(){
      toggleInputButton(condition, defaultBtn, altBtn, altAction)
    })
}

Necesitaremos los elementos DOM específicos para escuchar y pasar listenForToggleasí que crearemos algunos selectores de consulta.

const mute_self_btn = document.querySelector('#mute-self');
const unmute_self_btn = document.querySelector('#unmute-self');

const hide_self_btn = document.querySelector('#hide-self');
const unhide_self_btn = document.querySelector('#unhide-self');

Finalmente, todos juntos podemos llamar a nuestro código pasando las condiciones, los botones y las acciones:

listenForToggle(room.camera.isVideoEnabled, hide_self_btn, unhide_self_btn, room.camera.disableVideo, room.camera.enableVideo);
listenForToggle(room.camera.isAudioEnabled, mute_self_btn, unmute_self_btn, room.camera.disableAudio, room.camera.enableAudio);

Construcción de las entradas del dispositivo Select

Nuestros desplegables de selección de entradas de audio y Video están actualmente vacíos. Vamos a llenarlos con los dispositivos de cada usuario. Video Express nos da esta funcionalidad fuera de la caja con VideoExpress.getDevices(). Esto devuelve un array de entradas de audio y video para la instancia local de VideoExpress.

Tendremos que recuperar esta lista y ordenarla para poder añadir dispositivos de audio a la entrada de micrófono y dispositivos de vídeo a la entrada de cámara. Lo hacemos con esta función asíncrona, que espera a que la Promise regrese de la consulta VideoExpress y luego itera sobre los dispositivos y los añade al elemento select.

// Retrieve available input devices from VideoExpress
// add retrieved input devices to select options
async function getDeviceInputs(audioTarget, videoTarget){
  const audio = document.querySelector(`${audioTarget}`);
  const video = document.querySelector(`${videoTarget}`);
  const availableDevices = VideoExpress.getDevices();
  availableDevices.then(devices=> {
    devices.forEach(device => {
      if (device.kind === "audioInput"){
        let opt = document.createElement('vwc-list-item');
        opt.value = device.deviceId;
        opt.innerHTML = device.label;
        audio.appendChild(opt);
      }
      else if (device.kind === "videoInput"){
        let opt = document.createElement('vwc-list-item');
        opt.value = device.deviceId;
        opt.innerHTML = device.label;
        video.appendChild(opt);
      }
      else{
        console.log("Error in retrieveDevices");
      }
    })
  })
}

También querremos actualizar la sala cuando un usuario cambie su selección. Podemos hacer esto escuchando los cambios en los menús de selección y luego llamando a room.camera.setAudioDevice() o room.camera.setVideoDevice().

// // listen for changes to selected audio/video inputs
// // update room when inputs are changed
let listenInputChange = (target) => {
  const targetSelect = document.querySelector(`${target}`);
  targetSelect.addEventListener('change', (inputOption) => {
    if (target === "vwc-select#audio-input"){
      room.camera.setAudioDevice(inputOption.target.value);
    }
    else if (target === "vwc-select#video-input"){
      room.camera.setVideoDevice(inputOption.target.value);
    }
    else{
      console.log("Error in listenInputChange");
    }
  })
}

Poniéndolo todo junto, llamar a nuestras funciones se parece a esto:

getDeviceInputs("vwc-select#audio-input", "vwc-select#video-input");
listenInputChange("vwc-select#audio-input");
listenInputChange("vwc-select#video-input");

Construir la salida de audio Select

Recuperar la lista de Salidas de Audio desde VideoExpress se verá casi idéntico a las entradas. Sin embargo, aquí no tenemos ningún dispositivo para Video Output por lo que nuestro código es casi idéntico excepto sin el filtrado. Al igual que teníamos VideoExpress.getDevices()ahora tenemos VideoExpress.getAudioOutputDevices(). Y similar a room.camera.setAudioDevice() o room.camera.setVideoDevice()ahora tenemos VideoExpress.setAudioOutputDevice().

Como sólo estamos atentos a un único cambio, podemos agruparlo todo en una única función:

// Retrieve lists of auidoOutput
// add audioOutputs to select menu
// On user select new option, update audio input
async function audioOutputs() {
  var audioOutputs = await VideoExpress.getAudioOutputDevices();
  const audioOutputSelect = document.querySelector('vwc-select#audio-output');
  audioOutputs.forEach(output => {
    let opt = document.createElement('vwc-list-item');
    opt.value = output.deviceId;
    opt.innerHTML = output.label;
    audioOutputSelect.appendChild(opt);
  })

  audioOutputSelect.addEventListener('change', (audioOutputOption) => {
    VideoExpress.setAudioOutputDevice(audioOutputOption.target.value);
  });
}

Y no olvides llamarlo en tu código.

audioOutputs();

Creación de la información sobre herramientas

Nuestro último paso en la barra de herramientas es añadir el comportamiento de los tooltips para que aparezcan y desaparezcan. Esto lo hacemos con vanilla.js apuntando al ancla que llamaremos target y el tooltip asociado que llamaremos targertToolTip. En mouseover y mouseout nos permiten escuchar cuando un usuario se desplaza sobre un ancla y cuando se detiene.

Nuestro addToolTipListeners tiene este aspecto:

// toggle tooltips on hover
let addToolTipListeners = (toolTipsToListen) => {
  toolTipsToListen.forEach(toolTipToListen => {
    const target = document.querySelector(`#${toolTipToListen.targetId}`);
    const targetToolTip = document.querySelector(`#${toolTipToListen.toolTipId}`);
    target.addEventListener('mouseover', (event) => targetToolTip.open = !targetToolTip.open);
    target.addEventListener('mouseout', (event) => targetToolTip.open = !targetToolTip.open);
  })
}

A continuación, vamos a definir nuestros tooltips para escuchar:

const toolTipsToListen = [
  {targetId: "hide-self", toolTipId:"hide-self-tooltip"},
  {targetId: "unhide-self", toolTipId: "unhide-self-tooltip"},
  {targetId: "mute-self", toolTipId: "mute-self-tooltip"},
  {targetId: "unmute-self", toolTipId: "unmute-self-tooltip"},
  {targetId: "mute-all", toolTipId: "mute-all-tooltip"},
  {targetId: "unmute-all", toolTipId: "unmute-all-tooltip"},
  {targetId: "audio-output-target", toolTipId: "audio-output-tooltip"},
]

Y por último, llamar a nuestro código.

addToolTipListeners(toolTipsToListen);

Nuestro código final para toolbar.js tiene este aspecto:

// Start of Toolbar Code

// toggle hide/display of buttons
let toggleButtonView = (buttonToHide, buttonToShow) => {
  buttonToHide.style.display = "none";
  buttonToShow.style.display = "block";
}

// toggle Mute All / Unmute All
let toggleMuteAllButton = (button, state, participants) =>{
  button.addEventListener("click", function(){
    Object.entries(participants).forEach(participant => {
      if (state === "mute"){
        toggleButtonView(mute_all_btn, unmute_all_btn)
        // why need both here???
        participant[1].camera.disableAudio();
      } else if (state === "unmute") {
        toggleButtonView(unmute_all_btn, mute_all_btn)
        participant[1].camera.enableAudio();
      } else {
        console.log("Error in toggleMuteAll")
      }
    })
  })
}



// toggle button (display and functionality) of any audio and video input devices
let toggleInputButton = (condition, defaultBtn, altBtn, action) => {
  if (condition()){
    toggleButtonView(defaultBtn, altBtn);
    action();
  } else if (!condition()){
    toggleButtonView(altBtn, defaultBtn);
    action();
  } else {
    console.log(`Error in toggleInputButton. Condition: ${condition}`);
  }
}

// listen for clicks to trigger toggle of input buttons
let listenForToggle = (condition, defaultBtn, altBtn, defaultAction, altAction) => {
    defaultBtn.addEventListener("click", function(){
      toggleInputButton(condition, defaultBtn, altBtn, defaultAction)
    })
    altBtn.addEventListener("click", function(){
      toggleInputButton(condition, defaultBtn, altBtn, altAction)
    })
}

// Retrieve available input devices from VideoExpress
// Add retrieved input devices to select options
async function getDeviceInputs(audioTarget, videoTarget){
  const audio = document.querySelector(`${audioTarget}`);
  const video = document.querySelector(`${videoTarget}`);
  const availableDevices = VideoExpress.getDevices();
  availableDevices.then(devices=> {
    devices.forEach(device => {
      if (device.kind === "audioInput"){
        let opt = document.createElement('vwc-list-item');
        opt.value = device.deviceId;
        opt.innerHTML = device.label;
        audio.appendChild(opt);
      }
      else if (device.kind === "videoInput"){
        let opt = document.createElement('vwc-list-item');
        opt.value = device.deviceId;
        opt.innerHTML = device.label;
        video.appendChild(opt);
      }
      else{
        console.log("Error in retrieveDevices");
      }
    })
  })
}


// listen for changes to selected audio/video inputs
// update room when inputs are changed
let listenInputChange = (target) => {
  const targetSelect = document.querySelector(`${target}`);
  targetSelect.addEventListener('change', (inputOption) => {
    if (target === "vwc-select#audio-input"){
      room.camera.setAudioDevice(inputOption.target.value);
    }
    else if (target === "vwc-select#video-input"){
      room.camera.setVideoDevice(inputOption.target.value);
    }
    else{
      console.log("Error in listenInputChange");
    }
  })
}


// Retrieve lists of auidoOutput
// Add audioOutputs to select menu
// On user select new option, update audio input
async function audioOutputs() {
  var audioOutputs = await VideoExpress.getAudioOutputDevices();
  const audioOutputSelect = document.querySelector('vwc-select#audio-output');
  audioOutputs.forEach(output => {
    let opt = document.createElement('vwc-list-item');
    opt.value = output.deviceId;
    opt.innerHTML = output.label;
    audioOutputSelect.appendChild(opt);
  })

  audioOutputSelect.addEventListener('change', (audioOutputOption) => {
    VideoExpress.setAudioOutputDevice(audioOutputOption.target.value);
  });
}


// toggle tooltips on hover
let addToolTipListeners = (toolTipsToListen) => {
  toolTipsToListen.forEach(toolTipToListen => {
    const target = document.querySelector(`#${toolTipToListen.targetId}`);
    const targetToolTip = document.querySelector(`#${toolTipToListen.toolTipId}`);
    target.addEventListener('mouseover', (event) => targetToolTip.open = !targetToolTip.open);
    target.addEventListener('mouseout', (event) => targetToolTip.open = !targetToolTip.open);
  })
}


// define all our DOM elements which we'll want to listen to
const mute_all_btn = document.querySelector('#mute-all');
const unmute_all_btn = document.querySelector('#unmute-all');

const mute_self_btn = document.querySelector('#mute-self');
const unmute_self_btn = document.querySelector('#unmute-self');

const hide_self_btn = document.querySelector('#hide-self');
const unhide_self_btn = document.querySelector('#unhide-self');

const toolTipsToListen = [
  {targetId: "hide-self", toolTipId:"hide-self-tooltip"},
  {targetId: "unhide-self", toolTipId: "unhide-self-tooltip"},
  {targetId: "mute-self", toolTipId: "mute-self-tooltip"},
  {targetId: "unmute-self", toolTipId: "unmute-self-tooltip"},
  {targetId: "mute-all", toolTipId: "mute-all-tooltip"},
  {targetId: "unmute-all", toolTipId: "unmute-all-tooltip"},
  {targetId: "audio-output-target", toolTipId: "audio-output-tooltip"},
]


// Call all of our functions!
toggleMuteAllButton(mute_all_btn, "mute", room.participants);
toggleMuteAllButton(unmute_all_btn, "unmute", room.participants);
listenForToggle(room.camera.isVideoEnabled, hide_self_btn, unhide_self_btn, room.camera.disableVideo, room.camera.enableVideo);
listenForToggle(room.camera.isAudioEnabled, mute_self_btn, unmute_self_btn, room.camera.disableAudio, room.camera.enableAudio);
getDeviceInputs("vwc-select#audio-input", "vwc-select#video-input");
listenInputChange("vwc-select#audio-input");
listenInputChange("vwc-select#video-input");
audioOutputs();
addToolTipListeners(toolTipsToListen);

Finale

Y .... ¡eso es todo! Con un poco de Ruby y un poco más de manipulación de Javascript, tienes una aplicación completa de videoconferencia.

Podemos ejecutar la aplicación y probarla con algunos amigos.

Correr ngrok

La forma más sencilla es con ngrok. Instale ngrok si no lo tienes.

A continuación, utilice ngrok para ejecutar un servidor de acceso público. Desde la línea de comandos ejecute: ngrok http 3000

En tu terminal verás una ventana parecida a ésta:

ngrok server screenshotngrok server screenshot

Querrás copiar la línea que termina en ngrok.io. Esta es la URL accesible temporalmente a la que ngrok reenviará tu servidor Rails. Necesitamos que Rails dé permiso a ngrok para ser un host. En nuestro archivo config/environments/development.rb dentro del archivo Rails.application.configure do tenemos que añadir la siguiente línea:

config.hosts << "[ngrok url]"

Por ejemplo, en el caso anterior de ejecutar ngrok añadiría:

config.hosts << "c3b9-146-185-57-50.ngrok.io"

Ahora podemos ejecutar nuestro servidor rails:

rails s

Y ahora nuestra aplicación está en marcha y puedes invitar a tus amigos a unirse a tu fiesta del reloj enviándoles la URL de ngrok. ¡Y la contraseña!

GIF Preview Of Finished Video Conferencing App Built With Vonage Video ExpressGIF Preview Of Finished Video Conferencing App Built With Vonage Video Express

Póngase en contacto

¿Qué te ha parecido este tutorial? ¿Tienes alguna pregunta o comentario sobre Video Express o Vivid? Únete a nosotros en Slack para desarrolladores de Vonage. Y síguenos en Twitter para estar al tanto de las últimas actualizaciones de Vonage Developer.

Compartir:

https://a.storyblok.com/f/270183/384x384/e4e7d1452e/benjamin-aronov.png
Benjamin AronovDefensor del Desarrollador

Benjamin Aronov es desarrollador de Vonage. Es un constructor de comunidades con experiencia en Ruby on Rails. Benjamin disfruta de las playas de Tel Aviv, a la que llama hogar. Su base en Tel Aviv le permite conocer y aprender de algunos de los mejores fundadores de startups del mundo. Fuera de la tecnología, a Benjamin le encanta viajar por el mundo en busca del perfecto pain au chocolat.