https://a.storyblok.com/f/270183/40020/f95c36bb00/video-express-ruby_part_2.png

Vonage Video Express com Ruby on Rails – Parte 2

Publicado em August 10, 2022

Tempo de leitura: 10 minutos

Esta é a segunda parte de uma série de duas partes sobre a criação de um aplicativo para sessões coletivas de exibição de vídeos usando Ruby on Rails com a Video API da Vonage e a biblioteca Video Express.

Na Parte 1, percorremos as etapas de criação do aplicativo Rails, mostramos como usar alguns componentes do Vivid e colocamos o chat de vídeo do Video Express para funcionar. Se você ainda não leu esse post, seria um bom ponto de partida.

Quando terminarmos, teremos um aplicativo para assistir a eventos em grupo, que poderemos usar para conversar com nossos amigos e assistir a esportes ou Videos juntos!

O que o aplicativo fará

Só para relembrar: estamos desenvolvendo um aplicativo de videoconferência que oferece aos usuários uma barra de ferramentas com diversos controles de áudio e vídeo. Além disso, o aplicativo permite que o moderador configure o “Watch Party” em diferentes modos de exibição.

Neste momento, já temos um Video Express em funcionamento Roomem funcionamento. Esse objeto nos permite chamar diferentes funções que executam as ações em nossa barra de ferramentas. Queremos oferecer ao usuário uma maneira de acionar essa funcionalidade, e faremos isso com os componentes do Vivid. Organizaremos tanto nosso HTML quanto nosso JS em componentes. Com o Webpack, vamos então import nossos módulos e require nossos componentes em application.js , o que exporá nosso JavaScript ao lado do cliente.

Criação de componentes auxiliares

O restante deste tutorial será dedicado à criação dos componentes que permitem aos usuários controlar sua sala do Video Express. Cada componente seguirá uma estrutura semelhante: HTML com componentes do Vivid e JavaScript para acionar as funções do Video Express.

Organizando o HTML

Vamos criar nossos partials, onde o HTML ficará. A partir da raiz do seu aplicativo, usando a linha de comando, execute:

mkdir app/views/components

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

E atualize o party.html.erb arquivo para renderizar os partials:

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

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

Organizando o JavaScript

Assim como temos uma pasta “components” em nossa pasta “Views”, vamos criar uma pasta “components” em nossa pasta “Javascript” para abrigar a lógica dos componentes correspondentes.

mkdir app/javascript/components

Aqui, vamos adicionar nossos arquivos de componentes: touch app/javascript/components/header.js touch app/javascript/components/toolbar.js

Para incluí-las no nosso código do lado do cliente por meio do Webpack, adicione as seguintes linhas no arquivo Application.js, logo abaixo das importações dos módulos.

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

Como nosso JavaScript responderá às ações do usuário no DOM, queremos garantir que ele seja carregado pelo Rails após o carregamento do DOM. Por isso, precisamos fazer uma pequena alteração e adicionar defer:true ao javascript_pack_tag em application.html.erb:

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

Agora estamos prontos para desenvolver nossos componentes.

Criação do cabeçalho

Criação do HTML

Um lembrete sobre o cabeçalho que queremos criar:

The Header in Moderator ViewThe Header in Moderator View

Uma ótima notícia é que a Vivid tem exatamente o que precisamos: uma Barra de Aplicativos Superior . A barra superior de aplicativos vem com algumas opções de slots, mas duas delas são as que nos interessam: title e actionItems. O título é ótimo para um logotipo ou, no nosso caso, para o título. E o actionitems pode ser usado como o conteúdo da barra de aplicativos. É aqui que vamos adicionar o botão de alternância para que o moderador mude entre os modos “chill” e “festa”. Podemos fazer isso com o vwc-switch componente.

Por dentro app/view/watch_party/_header.html.erb ficará assim:

<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>

Criação do JavaScript do cabeçalho

Dentro do components/header.js arquivo, teremos três partes essenciais: monitorar a ativação/desativação, ativar/desativar o compartilhamento de tela e ativar/desativar os efeitos visuais associados.

Observação importante sobre o Video Express

No Video Express, você tem dois layouts predefinidos: “Grid” e “Active Speaker”. O Video Express oferece uma videochamada padronizada e rápida! Mas, se quiser se afastar do comportamento padrão, tenha cuidado; provavelmente será melhor usar a Video API completa.

Neste exemplo, o moderador é o único a compartilhar a tela. Pode-se pensar que o layout “Orador Ativo” nos permitiria tornar a tela compartilhada a tela dominante. No entanto, em videoconferências padronizadas, o comportamento esperado é que quem compartilha a tela faça a apresentação a partir de outra aba. Portanto, por padrão, o Video Express não torna a janela da tela compartilhada a visualização dominante.

No nosso caso de uso, o moderador não precisa controlar a tela e quer ver o compartilhamento de tela em tamanho grande, assim como todos os outros participantes. Esse não é o comportamento padrão. Precisaremos implementar isso por conta própria. Felizmente, o Video Express oferece a [screenSharingContainer](https://tokbox.com/developer/video-express/reference/room.html) opção que nos permitirá implementar o que queremos.

Pela documentação, podemos ver que basta adicionar um DIV vazio com o id screenSharingContainer. Mas precisamos deixar a aparência bonita e ainda poder aproveitar o VideoExpress LayoutManager para garantir a responsividade. Além disso, queremos que isso seja aplicado apenas ao moderador no modo de compartilhamento de tela. Portanto, nossa solução é anexar o screenSharingContainer ao lado do layoutContainer e escrever um pouco de CSS para deixar a visualização do moderador o mais parecida possível com o que todos os outros veem!

Primeiro, vamos criar a lógica básica do ouvinte para o botão de alternância:

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");
      }
  });
}

Agora, antes de ativarmos o compartilhamento de tela no Video Express, precisamos preparar a visualização do moderador para que o estilo personalizado, que afetará o layoutContainerWrapper e layoutContainer não afete a visualização de todos os demais participantes. Vamos chamar essa função de 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 essa função faz duas coisas: atualiza o rótulo do botão de alternância e adiciona um id de screenSharingContainer. Esse id adicionado nos ajuda a restringir a aplicação do CSS apenas ao Moderador.

Quando o screenShare for encerrado, precisaremos remover o estilo personalizado para que a visualização do moderador não fique distorcida. Por isso, temos uma função removeModeratorCustomStyles para desfazer tudo o que foi feito anteriormente:

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);
}

Agora, nosso cabeçalho com botões de alternância está praticamente pronto. Só precisamos selecionar nossos elementos e chamar as funções de compartilhamento de tela do Video Express. O cabeçalho completo fica assim:

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");
    }
  });
}

Criação da barra de ferramentas em HTML

Agora podemos adicionar o último e mais complicado componente que temos: a barra de ferramentas. Mas não vai ser tão difícil assim: basta criar o HTML com os componentes do Vivid e, em seguida, adicionar um pouco de JavaScript para acionar as funções do Video Express. Você já sabe como funciona!

Um lembrete sobre a barra de ferramentas que queremos criar:

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

Criação dos botões de alternância

Podemos ver na barra de ferramentas que há três grupos de botões que ativam ou desativam alguns recursos da sala: silenciar/ativar o som de todos, desativar/ativar o microfone e desativar/ativar a câmera de Video. Para todos os três, usaremos dois botões do Vivid e, em seguida, utilizaremos JavaScript para ocultar o botão inativo.

<!-- 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>

Criação de menus suspensos

Podemos ver, porém, que o segundo e o terceiro conjuntos de botões são um pouco diferentes. Eles também devem vir acompanhados de um menu suspenso que permita ao usuário selecionar a entrada associada: microfone ou câmera. Isso é possível com o <vwc-action-group elemento. O lado esquerdo do grupo de ação vem diretamente da documentação, com um botão e um separador. No lado direito, vamos utilizar o componente vwc-select para gerar um select elemento que podemos associar às diferentes opções que recebemos do VideoExpress. Também passamos as vwc-select duas opções: selected e disabled, para instruí-lo a exibir e desativar o vwc-list-item , que funcionará como um rótulo.

Nossos dois grupos de ação com botões agora têm a seguinte aparência:

<!-- 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 temos um terceiro grupo de ações: as entradas de áudio. Podemos usar a mesma estrutura:

<!-- 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>

Criação do HTML das dicas de ferramenta

Vamos dar aos nossos usuários um pouco mais de informação sobre esses componentes. Podemos fazer isso de maneira elegante usando as dicas de ferramenta do Vivid.

Há três partes nas dicas de ferramenta que nos interessam: a anchor, o cornere o id. A âncora indica à dica de ferramenta a qual elemento HTML ela deve se vincular. O canto indica à dica de ferramenta em que direção ela deve ser exibida, em relação à âncora. E o id é algo que usaremos no JavaScript para acionar a exibição da dica de ferramenta quando um usuário passar o mouse sobre a âncora.

Com as dicas de ferramenta adicionadas, nossa página completa _toolbar.html.erb fica assim:

<!-- 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>

Estilo da barra de ferramentas

Antes de desenvolvermos o JavaScript para os componentes, vamos fazer com que a barra de ferramentas fique igual ao nosso mockup:

// 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;
}

Criação da barra de ferramentas em JavaScript

Da esquerda para a direita, o primeiro componente que precisamos criar é o botão “Silenciar tudo”. Queremos que ele seja um botão de alternância que ative e desative o áudio de todos os outros participantes. Mas como podemos criar um botão de alternância? Na verdade, precisamos de dois botões: um “silenciar todos” e um “ativar som de todos”. Esse padrão será repetido várias vezes na barra de ferramentas, então vamos escrever uma função auxiliar para isso.

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

Criando o botão “Silenciar os outros”

Para silenciar todos os participantes, precisamos detectar quando o usuário acionar a ação e, em seguida, percorrer todos os participantes na instância da sala desse usuário. É importante observar que a sala é local para cada usuário.

Portanto, para desativar o áudio, precisamos percorrer todos os participantes da sala e usar a .camera.disableAudio() função. Você vai perceber que chamamos isso em participant[1] porque o objeto participant retorna uma matriz: [id, participantObject].

// 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);

Como ativar os botões “Silenciar” e “Desativar a câmera”

Os botões “Silenciar/Ativar som” e “Desativar/Ativar câmera” seguem a mesma lógica do botão “Silenciar tudo”. Eles aguardam uma ação do usuário, verificam um estado e chamam uma ação no VideoExpress. No entanto, são muito mais simples, pois o VideoExpress nos oferece funções para verificar o estado. As duas funções são room.camera.isVideoEnabled e room.camera.isAudioEnabled. Além disso, precisamos apenas acionar a ação para um único usuário.

Podemos criar essa função toggleInputButton que aceitará uma condição — o valor booleano que recebemos das nossas funções do Video Express — e, em seguida, chamará a ação correspondente do Video Express. Ela também atualizará a visualização com 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)
    })
}

Precisaremos dos elementos DOM específicos para monitorar e passar listenForToggle; por isso, criamos alguns seletores 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');

Por fim, todos juntos podemos chamar nosso código, passando as condições, os botões e as ações:

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);

Criação das entradas do dispositivo de seleção

Nossos menus suspensos de seleção para entradas de áudio e vídeo estão vazios no momento. Vamos preenchê-los com os dispositivos de cada usuário. O Video Express já oferece essa funcionalidade pronta para uso com VideoExpress.getDevices(). Isso retorna uma matriz contendo as entradas de áudio e vídeo para a instância local do VideoExpress.

Precisaremos recuperar essa lista e, em seguida, classificá-la para que possamos adicionar dispositivos de áudio à entrada do microfone e dispositivos de vídeo à entrada da câmera. Fazemos isso com esta função assíncrona, que aguarda o retorno da Promise após a consulta ao VideoExpress e, em seguida, itera sobre os dispositivos e os anexa ao 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");
      }
    })
  })
}

Também precisaremos atualizar a sala quando um usuário alterar sua seleção. Podemos acionar isso monitorando as alterações nos menus de seleção e, em seguida, chamando room.camera.setAudioDevice() ou 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");
    }
  })
}

Resumindo, a chamada às nossas funções fica assim:

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

Configurando a saída de áudio selecionada

A recuperação da lista de saídas de áudio do VideoExpress será praticamente idêntica à das entradas. No entanto, como não temos nenhum dispositivo para saída de vídeo, nosso código é praticamente o mesmo, mas sem a filtragem. Assim como fizemos VideoExpress.getDevices(), agora temos VideoExpress.getAudioOutputDevices(). E, da mesma forma que room.camera.setAudioDevice() ou room.camera.setVideoDevice(), agora temos VideoExpress.setAudioOutputDevice().

Como estamos monitorando apenas uma única alteração, podemos organizar tudo de forma bem organizada em uma única função:

// 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);
  });
}

E não se esqueça de chamá-la no seu código!

audioOutputs();

Criação das dicas de ferramenta

Nosso último passo na barra de ferramentas é adicionar o comportamento de exibição e ocultação das dicas de ferramenta. Fazemos isso com o vanilla.js, selecionando a âncora que chamaremos de target e a dica de ferramenta associada, que chamaremos de targertToolTip. O mouseover e mouseout nos permitem detectar quando um usuário passa o mouse sobre uma âncora e quando ele para.

O nosso addToolTipListeners fica assim:

// 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);
  })
}

Em seguida, definiremos as dicas de ferramenta que vamos monitorar:

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"},
]

E, por fim, chame nosso código.

addToolTipListeners(toolTipsToListen);

Nosso código final para toolbar.js fica assim:

// 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);

Final

E... pronto! Com um pouquinho de Ruby e um pouco mais de manipulação de JavaScript, você tem um aplicativo completo de videoconferência.

Podemos abrir o aplicativo e testá-lo com alguns amigos!

Executando o ngrok

A maneira mais fácil é usando o ngrok. Instale o ngrok se você ainda não o tiver.

Em seguida, use o ngrok para executar um servidor acessível ao público. Na linha de comando, digite: ngrok http 3000

No seu terminal, você verá uma janela parecida com esta:

ngrok server screenshotngrok server screenshot

Você deve copiar a linha que termina em ngrok.io. Essa é a URL temporariamente acessível para a qual o ngrok redirecionará seu servidor Rails. Precisamos que o Rails conceda permissão ao ngrok para atuar como host. Em nosso config/environments/development.rb arquivo, dentro do Rails.application.configure do , precisamos adicionar a seguinte linha:

config.hosts << "[ngrok url]"

Por exemplo, no caso acima, ao executar o ngrok, eu acrescentaria:

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

Agora podemos executar nosso servidor Rails:

rails s

E agora nosso aplicativo já está funcionando, e você pode convidar amigos para participar da sua sessão de exibição, enviando a eles a URL do ngrok. E a senha!

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

Entre em contato

O que você achou deste tutorial? Tem alguma dúvida ou comentário sobre o Video Express ou o Vivid? Junte-se a nós no Slack dos desenvolvedores da Vonage. E siga-nos no Twitter para ficar por dentro das últimas atualizações do Vonage Developer.

Compartilhar:

https://a.storyblok.com/f/270183/384x384/e4e7d1452e/benjamin-aronov.png
Benjamin AronovDeveloper Advocate

Benjamin Aronov is a developer advocate at Vonage. He is a proven community builder with a background in Ruby on Rails. Benjamin enjoys the beaches of Tel Aviv which he calls home. His Tel Aviv base allows him to meet and learn from some of the world's best startup founders. Outside of tech, Benjamin loves traveling the world in search of the perfect pain au chocolat.