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

Vonage Video Express mit Ruby on Rails Teil 2

Zuletzt aktualisiert am August 10, 2022

Lesedauer: 10 Minuten

Dies ist der zweite Teil einer zweiteiligen Serie über die Erstellung einer Video-Watch-Party-Anwendung mit Ruby on Rails unter Verwendung der Vonage Video API und der Video Express Bibliothek.

Im Teil 1sind wir die Schritte zur Erstellung der Rails-Anwendung durchgegangen, haben gezeigt, wie einige Vivid-Komponenten verwendet werden, und haben den Video-Express-Videochat zum Laufen gebracht. Wenn Sie diesen Beitrag noch nicht gelesen haben, wäre das ein guter Anfang.

Wenn wir fertig sind, haben wir eine App, mit der wir mit unseren Freunden chatten und gemeinsam Sport oder Videos anschauen können!

Was die App macht

Zur Erinnerung: Wir entwickeln eine Videokonferenzanwendung, die den Benutzern eine Symbolleiste für verschiedene Audio-/Videosteuerungen bietet. Außerdem gibt die Anwendung dem Moderator die Möglichkeit, die Watch Party in verschiedene Betrachtungsmodi zu versetzen.

Zu diesem Zeitpunkt haben wir ein funktionierendes Video Express Raum. Dieses Objekt gibt uns die Möglichkeit, verschiedene Funktionen aufzurufen, die die Aktionen in unserer Symbolleiste ausführen. Wir wollen dem Benutzer eine Möglichkeit geben, diese Funktionen auszulösen, und das werden wir mit Vivid-Komponenten tun. Wir werden sowohl unser HTML als auch unser JS in Komponenten organisieren. Mit Webpack werden wir dann import unsere Module und require unsere Komponenten in application.js die unser Javascript auf der Client-Seite freilegen werden.

Aufbau von Hilfskomponenten

Im weiteren Verlauf dieses Tutorials werden die Komponenten erstellt, mit denen die Benutzer ihren Video Express-Raum steuern können. Jede Komponente folgt einer ähnlichen Struktur: HTML mit Vivid-Komponenten und Javascript, um Video Express-Funktionen auszulösen.

Organisieren des HTML

Lassen Sie uns unsere Teilbereiche erstellen, in denen der HTML-Code gespeichert werden soll. Führen Sie im Stammverzeichnis Ihrer Anwendung über die Befehlszeile aus:

mkdir app/views/components

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

Und aktualisieren Sie die party.html.erb Datei, um die Teilbilder zu rendern:

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

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

Die Organisation des Javascript

So wie wir einen Komponentenordner in unseren Views haben, erstellen wir einen Komponentenordner in unserem Javascript-Ordner, um unsere entsprechende Komponentenlogik unterzubringen.

mkdir app/javascript/components

Hier fügen wir unsere Komponentendateien hinzu: touch app/javascript/components/header.js touch app/javascript/components/toolbar.js

Um sie für unsere Clientseite über Webpack zu benötigen, fügen Sie die folgenden Zeilen in Application.js unterhalb unserer Modulimporte hinzu.

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

Da unser Javascript auf Benutzeraktionen im DOM reagieren wird, wollen wir sicherstellen, dass das Javascript von Rails geladen wird, nachdem das DOM geladen ist. Also müssen wir einen kleinen Zusatz machen und defer:true zu der javascript_pack_tag in application.html.erb:

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

Jetzt sind wir bereit, unsere Komponenten aufzubauen.

Aufbau der Kopfzeile

Erstellung des HTML

Eine Erinnerung an den Header, den wir bauen wollen:

The Header in Moderator ViewThe Header in Moderator View

Eine gute Nachricht ist, dass Vivid genau das hat, was wir brauchen: eine Top-App-Leiste Komponente. Die Top-App-Leiste kommt mit ein paar Slot-Optionen, aber zwei, die uns interessieren: title und . actionItems. Der Titel eignet sich hervorragend für ein Logo oder in unserem Fall einen Titel. Und die actionitems kann für den Inhalt der App-Leiste verwendet werden. Hier fügen wir den Toggler für den Moderator ein, um zwischen dem Chill- und dem Partymodus zu wechseln. Wir können dies mit der vwc-switch Komponente.

Innerhalb von app/view/watch_party/_header.html.erb wird wie folgt aussehen:

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

Aufbau des Header-Javascript

Innerhalb der components/header.js Datei gibt es 3 wesentliche Teile: Abhören eines Umschalters, Umschalten des Freigabebildschirms und Umschalten des begleitenden Bildmaterials.

Wichtiger Hinweis zu Video Express

In Video Express stehen Ihnen zwei Layouts zur Verfügung: "Raster" und "Active Speaker". Video Express bietet Ihnen einen standardisierten Videoanruf, der schnell läuft! Wenn Sie jedoch vom Standardverhalten abweichen möchten, sollten Sie vorsichtig sein und besser die vollständige Video API verwenden.

In diesem Beispiel ist der Moderator der einzige Bildschirmteilnehmer. Man könnte meinen, dass das Layout "Aktiver Sprecher" es uns ermöglichen würde, den gemeinsam genutzten Bildschirm zum dominanten Bildschirm zu machen. Bei standardisierten Videokonferenzen wird jedoch erwartet, dass der Teilnehmer, der den Bildschirm gemeinsam nutzt, von einer anderen Registerkarte aus präsentiert. Video Express macht also standardmäßig das Fenster des gemeinsam genutzten Bildschirms nicht zur dominanten Ansicht.

In unserem Anwendungsfall braucht der Moderator den Bildschirm nicht zu kontrollieren und möchte seine Bildschirmfreigabe groß sehen, genau wie alle anderen. Dies ist nicht das Standardverhalten. Wir müssen es selbst entwickeln. Zum Glück bietet Video Express die Option [screenSharingContainer](https://tokbox.com/developer/video-express/reference/room.html) Option, die es uns ermöglicht, das zu erstellen, was wir wollen.

Aus der Dokumentation können wir ersehen, dass wir nur ein leeres DIV mit der id von hinzufügen müssen screenSharingContainer. Aber wir müssen dafür sorgen, dass es schön aussieht und trotzdem die Vorteile von VideoExpress LayoutManager für die Reaktionsfähigkeit nutzen kann. Und wir wollen dies nur für den Moderator im Screensharing-Modus anwenden. Unsere Lösung ist also das Anhängen der screenSharingContainer neben dem layoutContainer hinzuzufügen und etwas CSS zu schreiben, um die Ansicht des Moderators so nah wie möglich an das anzupassen, was alle anderen sehen!

Erstellen wir zunächst die Grundlogik des Zuhörers für die Umschaltfunktion:

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

Bevor wir nun die Bildschirmfreigabe in Video Express auslösen können, müssen wir die Ansicht des Moderators so vorbereiten, dass das benutzerdefinierte Styling, das die layoutContainerWrapper und layoutContainer sich nicht auf die Ansicht der anderen Teilnehmer auswirkt. Wir rufen diese Funktion auf 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);
}

Wir sehen, dass diese Funktion zwei Dinge tut: Sie aktualisiert die Beschriftung des Togglers und fügt eine id von screenSharingContainer. Diese hinzugefügte ID hilft uns, CSS nur auf den Moderator anzuwenden.

Wenn die Bildschirmfreigabe endet, müssen wir das benutzerdefinierte Styling entfernen, damit die Ansicht des Moderators nicht durcheinander gebracht wird. Wir haben also eine Funktion removeModeratorCustomStyles um alles von vorher rückgängig zu machen:

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

Jetzt ist unser Toggle-Header im Grunde fertig. Wir müssen nur noch unsere Elemente abfragen und die Screensharing-Funktionen von Video Express aufrufen. Der vollständige Header sieht wie folgt aus:

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

Aufbau der Symbolleiste HTML

Jetzt können wir die letzte und komplizierteste Komponente hinzufügen, die wir haben: die Symbolleiste. Aber das ist gar nicht so schlimm, denn wir müssen nur den HTML-Code mit Vivid-Komponenten erstellen und dann Javascript hinzufügen, um die Funktionen von Video Express auszulösen. Sie wissen, wie es geht!

Eine Erinnerung an die Symbolleiste, die wir bauen wollen:

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

Bau der Umschalttasten

In der Symbolleiste sehen wir, dass es drei Gruppen von Schaltflächen gibt, mit denen einige Funktionen im Raum ein- und ausgeschaltet werden können: Alle stummschalten/aufheben, Mikrofon deaktivieren/aktivieren und Videokamera deaktivieren/aktivieren. Für alle drei werden wir zwei Schaltflächen von Vivid verwenden und dann Javascript einsetzen, um die inaktive Schaltfläche auszublenden.

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

Wir können sehen, dass die zweite und dritte Gruppe von Schaltflächen ein wenig anders sind. Sie sollten auch von einem Dropdown-Menü begleitet werden, mit dem der Benutzer den zugehörigen Eingang auswählen kann: Mikrofon oder Kamera. Dies ist möglich mit dem Vivid <vwc-action-group Element. Die linke Seite der Aktionsgruppe kommt direkt aus der Dokumentation mit einer Schaltfläche und einem Trennzeichen. Auf der rechten Seite verwenden wir die Vivid vwc-select Komponente zur Erzeugung eines select Element, das wir mit den verschiedenen Optionen, die wir von VideoExpress erhalten, ansteuern können. Wir übergeben auch die vwc-select zwei Optionen: ausgewählt und deaktiviert, um den Standardwert anzuzeigen und zu deaktivieren vwc-list-item die als Beschriftung dienen wird.

Unsere beiden Aktionsgruppen mit Schaltflächen sehen nun wie folgt aus:

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

Wir sehen, dass wir eine dritte Aktionsgruppe haben: die Audioeingänge. Wir können die gleiche Struktur verwenden:

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

Aufbau der ToolTips HTML

Lassen Sie uns unseren Benutzern ein wenig mehr Informationen über diese Komponenten geben. Dies können wir auf elegante Weise tun, indem wir Tooltips von Vivid.

Es gibt drei Teile der Tooltips, die uns interessieren: die anchor, die corner, und die id. Der Anker teilt der QuickInfo mit, an welchem HTML-Element sie sich festhalten soll. Die Ecke sagt der QuickInfo, in welcher Richtung sie in Bezug auf den Anker angezeigt werden soll. Und die id ist etwas, das wir im Javascript verwenden werden, um die Anzeige des Tooltips auszulösen, wenn ein Benutzer den Mauszeiger über den Anker bewegt.

Mit den hinzugefügten Tooltips sieht unsere vollständige _toolbar.html.erb wie folgt aus:

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

Gestalten der Symbolleiste

Bevor wir das Javascript für die Komponenten entwickeln, sollten wir die Symbolleiste wie unser Mockup aussehen lassen:

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

Aufbau der Symbolleiste Javascript

Von links nach rechts ist die erste Komponente, die wir bauen müssen, die Schaltfläche "Alle stummschalten". Dies soll ein Umschalter sein, der das Audio aller anderen Teilnehmer ein- und ausschaltet. Aber wie können wir eine Umschalttaste haben? Wir brauchen eigentlich zwei Schaltflächen: eine Schaltfläche "Alle stummschalten" und eine Schaltfläche "Alle aufheben". Dieses Muster wird sich in der Symbolleiste mehrmals wiederholen, also lassen Sie uns eine Hilfsfunktion dafür schreiben.

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

Die Taste "Andere stummschalten" bauen

Um alle Teilnehmer stumm zu schalten, müssen wir darauf warten, dass der Benutzer die Aktion auslöst und dann alle Teilnehmer in der Instanz des Raums dieses Benutzers durchlaufen. Es ist wichtig zu beachten, dass der Raum für jeden Benutzer lokal ist.

Um also den Ton zu deaktivieren, müssen wir alle Teilnehmer im Raum durchgehen und die .camera.disableAudio() Funktion verwenden. Sie werden feststellen, dass wir diese Funktion für Teilnehmer[1] aufrufen, weil das Teilnehmerobjekt ein Array zurückgibt: [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);

Bau der Tasten zum Stummschalten von sich selbst und zum Deaktivieren der Kamera

Die Schaltflächen Mute/Unmute Self und Disable/Enable Camera folgen der gleichen Logik wie die Schaltfläche MuteAll. Sie warten auf eine Benutzeraktion, prüfen einen Status und rufen eine Aktion in VideoExpress auf. Sie sind jedoch viel einfacher, da VideoExpress uns Funktionen zur Verfügung stellt, um den Status zu überprüfen. Die beiden Funktionen sind room.camera.isVideoEnabled und room.camera.isAudioEnabled. Außerdem müssen wir die Aktion nur bei einem einzigen Benutzer auslösen.

Wir können diese Funktion erstellen toggleInputButton erstellen, die eine Bedingung akzeptiert, den booleschen Wert, den wir von unseren Video Express-Funktionen erhalten, und dann die entsprechende Video Express-Aktion aufrufen. Sie aktualisiert auch die Ansicht mit 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)
    })
}

Wir brauchen die spezifischen DOM-Elemente, auf die wir achten und die wir übergeben müssen listenForToggleübergeben werden sollen, also erstellen wir einige Abfrage-Selektoren.

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

Schließlich können wir alle zusammen unseren Code aufrufen, indem wir die Bedingungen, die Schaltflächen und die Aktionen übergeben:

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

Aufbau der Select-Device-Eingänge

Unsere Dropdowns für die Audio- und Videoeingänge sind derzeit leer. Füllen wir sie mit den Geräten der einzelnen Benutzer. Video Express bietet uns diese Funktionalität von Haus aus mit VideoExpress.getDevices(). Dies gibt ein Array von Audio- und Videoeingängen für die lokale VideoExpress-Instanz zurück.

Wir müssen diese Liste abrufen und dann sortieren, damit wir Audiogeräte zum Mikrofoneingang und Videogeräte zum Kameraeingang hinzufügen können. Dies geschieht mit dieser asynchronen Funktion, die darauf wartet, dass das Promise von der Abfrage von VideoExpress zurückkehrt, dann die Geräte durchläuft und sie an das Select-Element anhängt.

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

Wir wollen den Raum auch aktualisieren, wenn ein Benutzer seine Auswahl ändert. Wir können dies auslösen, indem wir auf Änderungen in den Auswahlmenüs achten und dann entweder room.camera.setAudioDevice() oder 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");
    }
  })
}

Zusammengefasst sieht der Aufruf unserer Funktionen wie folgt aus:

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

Aufbau des ausgewählten Audioausgangs

Das Abrufen der Liste der Audioausgänge von VideoExpress sieht fast genauso aus wie die Eingaben. Allerdings haben wir hier keine Geräte für die Video-Ausgabe, so dass unser Code fast identisch ist, nur ohne die Filterung. Genau wie wir VideoExpress.getDevices()hatten, haben wir jetzt VideoExpress.getAudioOutputDevices(). Und ähnlich wie bei room.camera.setAudioDevice() oder room.camera.setVideoDevice(), haben wir jetzt VideoExpress.setAudioOutputDevice().

Da wir nur auf eine einzige Änderung achten, können wir das Ganze in einer einzigen Funktion zusammenfassen:

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

Und vergessen Sie nicht, sie in Ihrem Code aufzurufen!

audioOutputs();

Erstellung der Tooltips

Unser allerletzter Schritt in der Symbolleiste ist das Hinzufügen des Verhaltens von Tooltips, die erscheinen und verschwinden. Wir tun dies mit vanilla.js, indem wir auf den Anker, den wir als target und den zugehörigen Tooltip, den wir targertToolTip. Die mouseover und mouseout ermöglichen es uns, zu hören, wenn ein Benutzer über einen Anker schwebt und wenn er aufhört.

Unser addToolTipListeners sieht so aus:

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

Dann definieren wir unsere Tooltips, auf die wir hören:

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

Und zum Schluss rufen Sie unseren Code auf.

addToolTipListeners(toolTipsToListen);

Unser endgültiger Code für toolbar.js sieht wie folgt aus:

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

Und....das war's! Mit ein bisschen Ruby und ein bisschen mehr Javascript-Manipulation haben Sie eine vollständige Videokonferenz-Anwendung.

Wir können die App ausführen und sie mit einigen Freunden ausprobieren!

Laufende ngrok

Der einfachste Weg ist mit ngrok. Installieren Sie ngrok wenn Sie es noch nicht haben.

Verwenden Sie dann ngrok, um einen öffentlich zugänglichen Server zu betreiben. Führen Sie in der Befehlszeile aus: ngrok http 3000

In Ihrem Terminal sehen Sie ein Fenster, das wie folgt aussieht:

ngrok server screenshotngrok server screenshot

Sie müssen die Zeile kopieren, die mit ngrok.io endet. Dies ist die temporär zugängliche URL, an die ngrok Ihren Rails-Server weiterleiten wird. Rails muss ngrok die Erlaubnis erteilen, ein Host zu sein. In unserer config/environments/development.rb Datei, innerhalb der Rails.application.configure do müssen wir die folgende Zeile hinzufügen:

config.hosts << "[ngrok url]"

Zum Beispiel würde ich in dem obigen Beispiel der Ausführung von ngrok hinzufügen:

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

Jetzt können wir unseren Rails-Server starten:

rails s

Und jetzt ist unsere App einsatzbereit und Sie können Freunde einladen, sich Ihrer Überwachungsparty anzuschließen, indem Sie ihnen die ngrok-URL schicken. Und das Passwort!

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

Kontakt aufnehmen

Wie hat Ihnen dieser Lehrgang gefallen? Haben Sie Fragen oder Anregungen zu Video Express oder Vivid? Schließen Sie sich uns auf dem Vonage Entwickler Slack. Und folgen Sie uns auf Twitter um über die neuesten Vonage Developer-Updates auf dem Laufenden zu bleiben.

Teilen Sie:

https://a.storyblok.com/f/270183/384x384/e4e7d1452e/benjamin-aronov.png
Benjamin AronovAdvokat für Entwickler

Benjamin Aronov ist ein Entwickler-Befürworter bei Vonage. Er ist ein bewährter Community Builder mit einem Hintergrund in Ruby on Rails. Benjamin genießt die Strände von Tel Aviv, das er sein Zuhause nennt. Von Tel Aviv aus kann er einige der besten Startup-Gründer der Welt treffen und von ihnen lernen. Außerhalb der Tech-Branche reist Benjamin gerne um die Welt auf der Suche nach dem perfekten Pain au Chocolat.