Erstellen eines Videochats mit React-Hooks

October 09, 2019
Autor:in:
Phil Nash
Twilion

Erstellen eines Videochats mit React-Hooks


Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Build a Video Chat with React Hooks. Während wir unsere Übersetzungsprozesse verbessern, würden wir uns über Dein Feedback an help@twilio.com freuen, solltest Du etwas bemerken, was falsch übersetzt wurde. Wir bedanken uns für hilfreiche Beiträge mit Twilio Swag :)

Wir haben im Rahmen dieses Blogs bereits einen Videochat in React erstellt, aber mittlerweile wurden mit der Version 16.8 von React Hooks eingeführt. Mit Hooks können wir Zustands- oder andere React-Funktionen innerhalb von Funktionskomponenten nutzen, ohne dass wir eine Klasse schreiben müssen.

In diesem Post werden wir eine Videochat-Anwendung mit Twilio Video und React erstellen, die nur Funktionskomponenten enthält. Dazu nutzen wir die Hooks useStateuseCallbackuseEffect und useRef.

Das brauchen wir

Um diese Videochat-Anwendung zu erstellen, brauchen wir Folgendes:

Sobald wir das alles haben, können wir unsere Entwicklungsumgebung vorbereiten.

Los geht's

Damit wir direkt die React-Anwendung aufrufen können, beginnen wir mit der React- und Express-Starter-Appdie ich erstellt habe. Wir laden den „twilio“-Zweig aus der Starter-App herunter oder klonen ihn, rufen das neue Verzeichnis auf und installieren die Abhängigkeiten:

git clone https://github.com/philnash/twilio-video-react-hooks.git
cd twilio-video-react-hooks
npm install

Wir kopieren die Datei .env.example in .env.

cp .env.example .env

Wir führen die Anwendung aus, um sicherzustellen, dass alles erwartungsgemäß funktioniert:

npm run dev

Wir sollten die folgende Seite in unserem Browser sehen:

Die Startseite zeigt das React-Logo und ein Formular an.

Vorbereiten der Twilio-Anmeldedaten

Damit wir eine Verbindung zu Twilio Video herstellen können, benötigen wir Anmeldedaten. Wir kopieren aus der Twilio console unsere Konto-SID und geben diese in die Datei .env als TWILIO_ACCOUNT_SID ein.

Außerdem brauchen wir einen API-Schlüssel und ein API-Geheimnis. Diese können wir mit den Programmable Video-Tools in der Konsole erstellen. Wir erstellen ein Schlüsselpaar und fügen die SID und das Geheimnis als TWILIO_API_KEY bzw. TWILIO_API_SECRET in die Datei .env ein.

Hinzufügen eines Stylesheets

In diesem Beitrag gehen wir zwar nicht näher auf CSS ein, aber damit das Ergebnis nicht ganz so schlimm aussieht, fügen wir ein Stylesheet hinzu. Wir holen uns das CSS von dieser URL und ersetzen den Inhalt unter src/App.css durch dieses neue Stylesheet.

Jetzt haben wir alles, um mit dem Erstellen zu beginnen.

Planen von Komponenten

Am Anfang steht die App-Komponente, in der wir einen Header und Footer für die App sowie eine VideoChat-Komponente festlegen können. Innerhalb der VideoChat-Komponente möchten wir eine Lobby-Komponente anzeigen, in die der Benutzer seinen Namen und den Raum eingeben kann, dem er beitreten möchte. Nachdem der Benutzer diese Informationen eingegeben hat, ersetzen wir die Lobby-Komponente durch eine Room-Komponente. Diese stellt dann eine Verbindung zum Raum her und zeigt die Teilnehmer des Videochats an. Schließlich rendern wir für jeden Teilnehmer im Raum eine Participant-Komponente, die dafür verantwortlich ist, die Medien der Teilnehmer anzuzeigen.

Erstellen von Komponenten

Die App-Komponente

Wir öffnen jetzt src/App.js. Hier steht noch viel Code von der ursprünglichen Beispiel-App, den wir entfernen können. Außerdem müssen wir beachten, dass es sich bei der App-Komponente um eine klassenbasierte Komponente handelt. Wir haben zu Beginn erwähnt, dass wir die gesamte App mit Funktionskomponenten erstellen, deshalb müssen wir das ändern.

Wir entfernen aus den Importen Component sowie die importierte Datei „logo.svg“. Wir ersetzen die gesamte App-Klasse durch eine Funktion, die das Gerüst unserer Anwendung rendert. Die ganze Datei sollte in etwa so aussehen:

import React from 'react';
import './App.css';

const App = () => {
  return (
    <div className="app">
      <header>
        <h1>Video Chat with Hooks</h1>
      </header>
      <main>
        <p>VideoChat goes here.</p>
      </main>
      <footer>
        <p>
          Made with{' '}
          <span role="img" aria-label="React">
            ⚛
          </span>{' '}
          by <a href="https://twitter.com/philnash">philnash</a>
        </p>
      </footer>
    </div>
  );
};

export default App;

Die VideoChat-Komponente

Diese Komponente zeigt, je nachdem, ob der Benutzer einen Benutzernamen und Raumnamen eingegeben hat, entweder einen Wartebereich oder einen Raum an. Wir erstellen eine neue Komponentendatei src/VideoChat.js und fügen an ihren Anfang den folgenden Textbaustein ein:

import React from 'react';

const VideoChat = () => {
  return <div></div> // we'll build up our response later
};

export default VideoChat;

Die VideoChat-Komponente ist die Komponente auf oberster Ebene, die die Daten zum Chat verarbeitet. Wir müssen außerdem einen Benutzernamen für den Benutzer, der dem Chat beitritt, einen Raumnamen für den Raum, zu dem er eine Verbindung herstellt, und ein Zugriffstoken speichern, sobald dieses vom Server abgerufen wurde. In der nächsten Komponente erstellen wir ein Formular, in das einige dieser Daten eingegeben werden.

Zum Speichern dieser Daten verwenden wir von den React-Hooks den useState Hook.

useState

useState ist eine Funktion, die ein Argument (Anfangszustand) benötigt und ein Array zurückgibt, das den aktuellen Zustand und eine Funktion zum Aktualisieren dieses Zustands enthält. Wir brechen die Struktur dieses Arrays auf. Dadurch erhalten wir zwei verschiedene Variablen, z. B. state und setState. Mit setState verfolgen wir den Benutzernamen, den Raumnamen und das Token in unserer Komponente.

Wir importieren zuerst useState aus React und richten dann die Zustände für den Benutzernamen, den Raumnamen und das Token ein:

import React, { useState } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  return <div></div> // we'll build up our response later
};

Als Nächstes benötigen wir zwei Funktionen, die den username und roomName aktualisieren, wenn der Benutzer diese Informationen in die entsprechenden Eingabeelemente eingibt.

import React, { useState } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  const handleUsernameChange = event => {
    setUsername(event.target.value);
  };

  const handleRoomNameChange = event => {
    setRoomName(event.target.value);
  };

  return <div></div> // we'll build up our response later
};

Das funktioniert zwar, aber wir können unsere Komponente optimieren, wenn wir hier einen weiteren React-Hook verwenden: useCallback

useCallback

Bei jedem Aufrufen dieser Funktionskomponente werden die handleXXX-Funktionen neu definiert. Diese müssen Teil der Komponente sein, da sie von den Funktionen setUsername und setRoomName abhängen. Diese bleiben aber unverändert. useCallback ist ein React-Hook, mit dem wir die Funktionen memoisieren können. Das heißt, wenn sie sich zwischen Funktionsaufrufen nicht geändert haben, werden sie nicht neu definiert.

useCallback benötigt zwei Argumente: die Funktion, die memoisiert werden soll, und ein Array der Abhängigkeiten der Funktion. Wenn sich eine der Abhängigkeiten der Funktion ändert, deutet das darauf hin, dass die memoisierte Funktion nicht mehr aktuell ist. Die Funktion wird dann neu definiert und erneut memoisiert.

In diesem Fall gibt es für diese zwei Funktionen keine Abhängigkeiten, deshalb genügt ein leeres Array (die setState-Funktionen des useState-Hooks gelten als konstant innerhalb der Funktion). Um diese Funktion neu zu schreiben, müssen wir useCallback dem Import am Anfang der Datei hinzufügen und dann jede einzelne dieser Funktionen umschließen.

import React, { useState, useCallback } from 'react';

const VideoChat = () => {
  const [username, setUsername] = useState('');
  const [roomName, setRoomName] = useState('');
  const [token, setToken] = useState(null);

  const handleUsernameChange = useCallback(event => {
    setUsername(event.target.value);
  }, []);

  const handleRoomNameChange = useCallback(event => {
    setRoomName(event.target.value);
  }, []);

  return <div></div> // we'll build up our response later
};

Wenn der Benutzer das Formular übermittelt, sollen der Benutzername und Raumname an den Server gesendet und gegen ein Zugriffstoken ausgetauscht werden. Mit diesem Zugriffstoken können wir dem Raum beitreten. Diese Funktion werden wir ebenfalls in dieser Komponente erstellen.

Mithilfe der Fetch-API senden wir die Daten im JSON-Format an den Endpunkt, erhalten eine Antwort, die analysiert wird, und speichern dann mithilfe von setToken das Token in unserem Zustand. Diese Funktion umschließen wir ebenfalls mit useCallback, aber in diesem Fall ist die Funktion abhängig von username und roomName. Deshalb fügen wir diese beiden Elemente useCallback als Abhängigkeiten hinzu.

  const handleRoomNameChange = useCallback(event => {
    setRoomName(event.target.value);
  }, []);

  const handleSubmit = useCallback(async event => {
    event.preventDefault();
    const data = await fetch('/video/token', {
      method: 'POST',
      body: JSON.stringify({
        identity: username,
        room: roomName
      }),
      headers: {
        'Content-Type': 'application/json'
      }
    }).then(res => res.json());
    setToken(data.token);
  }, [username, roomName]);

  return <div></div> // we'll build up our response later
};

Als letzte Funktion fügen wir dieser Komponente eine Abmeldefunktion hinzu. Dadurch wird der Benutzer von einem Raum abgemeldet, und er kehrt in den Wartebereich zurück. Hierzu setzen wir das Token auf den Wert null. Das Ganze wird wiederum mit useCallback ohne Abhängigkeiten umschlossen.

  const handleLogout = useCallback(event => {
    setToken(null);
  }, []);

  return <div></div> // we'll build up our response later
};

Diese Komponente orchestriert vorwiegend die Komponenten, die sich unterhalb befinden, deshalb gibt es erst etwas zu rendern, wenn wir diese Komponenten erstellt haben. Wir erstellen als Nächstes die Lobby-Komponente, mit der das Formular für die Eingabe von Benutzernamen und Raumnamen gerendert wird.

Die Lobby-Komponente

Wir erstellen unter src/Lobby.js eine neue Datei. In dieser Komponente werden keine Daten gespeichert, da sie alle Ereignisse an die übergeordnete VideoChat-Komponente übergibt. Wenn die Komponente gerendert wird, werden an sie username und roomName sowie die Funktionen zur Verarbeitung von Änderungen übergeben. Die Komponente ist außerdem für das Senden des Formulars verantwortlich. Wir können die Struktur dieser Eigenschaften aufbrechen, um die spätere Verwendung zu vereinfachen.

Die wichtigste Aufgabe der Lobby-Komponente ist das Rendern des Formulars anhand dieser Eigenschaften. Das sieht in etwa so aus:

import React from 'react';

const Lobby = ({
  username,
  handleUsernameChange,
  roomName,
  handleRoomNameChange,
  handleSubmit
}) => {
  return (
    <form onSubmit={handleSubmit}>
      <h2>Enter a room</h2>
      <div>
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="field"
          value={username}
          onChange={handleUsernameChange}
          required
        />
      </div>

      <div>
        <label htmlFor="room">Room name:</label>
        <input
          type="text"
          id="room"
          value={roomName}
          onChange={handleRoomNameChange}
          required
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};

export default Lobby;

Wir aktualisieren die VideoChat-Komponente, um die Lobby zu rendern, wenn wir kein token haben. Ansonsten rendern wir usernameroomName und token. Wir müssen die Lobby-Komponente am Anfang der Datei importieren und am Ende der Komponentenfunktion JSX rendern:

import React, { useState, useCallback } from 'react';
import Lobby from './Lobby';

const VideoChat = () => {
  // ...

  const handleLogout = useCallback(event => {
    setToken(null);
  }, []);
  
  let render;
  if (token) {
    render = (
      <div>
        <p>Username: {username}</p>
        <p>Room name: {roomName}</p>
        <p>Token: {token}</p>
      </div>
    );
  } else {
    render = (
      <Lobby
         username={username}
         roomName={roomName}
         handleUsernameChange={handleUsernameChange}
         handleRoomNameChange={handleRoomNameChange}
         handleSubmit={handleSubmit}
      />
    );
  }
  return render;
};

Damit das auch auf der Seite angezeigt wird, müssen wir außerdem die VideoChat-Komponente in die App-Komponente importieren und rendern. Wir öffnen erneut die Datei src/App.js und nehmen darin folgende Änderungen vor:

import React from 'react';
import './App.css';
import VideoChat from './VideoChat';

const App = () => {
  return (
    <div className="app">
      <header>
        <h1>Video Chat with Hooks</h1>
      </header>
      <main>
        <VideoChat />
      </main>
      <footer>
        <p>
          Made with{' '}
          <span role="img" aria-label="React">
            ⚛️
          </span>{' '}
          by <a href="https://twitter.com/philnash">philnash</a>
        </p>
      </footer>
    </div>
  );
};

export default App;

Wir müssen sicherstellen, dass die App immer noch ausgeführt wird (oder wir starten sie mit npm run dev neu), und öffnen sie im Browser. Wir sollten ein Formular sehen. Wir geben einen Benutzernamen und Raumnamen ein und senden das Formular. Die Ansicht ändert sich und wir sehen die von uns ausgewählten Namen sowie das Token, das vom Server abgerufen wurde.

Wenn wir das Formular ausfüllen und abschicken, sehen wir den Benutzernamen, den Raumnamen und das Token auf der Seite.

Die Room-Komponente

Nachdem wir der Anwendung jetzt einen Benutzernamen und Raumnamen hinzugefügt haben, können wir mit diesen Namen einem Twilio Video-Chatroom beitreten. Für die Arbeit mit dem Twilio Video-Dienst benötigen wir das JS-SDK. Für dieses Beispiel arbeiten wir mit Twilio Video Version 2.2.0. Wir installieren die Version mit diesem Befehl:

npm install twilio-video@^2.2.0 --save

Wir erstellen eine neue Datei im src-Verzeichnis mit dem Namen Room.js. Wir beginnen die Datei mit dem folgenden Textbaustein. In dieser Komponente verwenden wir das Twilio Video-SDK sowie die Hooks useState und useEffect. Außerdem rufen wir roomNametoken und handleLogout als Eigenschaften von der übergeordneten VideoChat-Komponente ab:

import React, { useState, useEffect } from 'react';
import Video from 'twilio-video';

const Room = ({ roomName, token, handleLogout }) => {

};

export default Room;

Die Komponente stellt als Erstes anhand des Tokens und des Raumnamens eine Verbindung zum Twilio Video-Dienst her. Sobald die Verbindung hergestellt ist, erhalten wir ein room-Objekt, das wir speichern. Der Raum enthält außerdem eine Liste mit Teilnehmern. Die Teilnehmer ändern sich im Lauf der Zeit. Deshalb speichern wir auch diese Liste. Dazu verwenden wir useState, wobei der Anfangswert für den Raum null ist und für die Teilnehmer ein leeres Array:

const Room = ({ roomName, token, handleLogout }) => {
  const [room, setRoom] = useState(null);
  const [participants, setParticipants] = useState([]);
});

Bevor wir schließlich dem Raum beitreten, rendern wir noch etwas für diese Komponente. Wir führen eine Zuordnung für das Teilnehmer-Array aus, um die Identität jedes Teilnehmers sowie die Identität des lokalen Teilnehmers im Raum anzuzeigen:

const Room = ({ roomName, token, handleLogout }) => {
  const [room, setRoom] = useState(null);
  const [participants, setParticipants] = useState([]);

  const remoteParticipants = participants.map(participant => (
    <p key={participant.sid}>{participant.identity}</p>
  ));

  return (
    <div className="room">
      <h2>Room: {roomName}</h2>
      <button onClick={handleLogout}>Log out</button>
      <div className="local-participant">
        {room ? (
          <p key={room.localParticipant.sid}>{room.localParticipant.identity}</p>
        ) : (
          ''
        )}
      </div>
      <h3>Remote Participants</h3>
      <div className="remote-participants">{remoteParticipants}</div>
    </div>
  );
});

Wir aktualisieren die VideoChat-Komponente, um diese Room-Komponente statt der zuvor verwendeten Platzhalterinformationen zu rendern.

import React, { useState, useCallback } from 'react';
import Lobby from './Lobby';
import Room from './Room';

const VideoChat = () => {
  // ...

  const handleLogout = useCallback(event => {
    setToken(null);
  }, []);
  
  let render;
  if (token) {
    render = (
      <Room roomName={roomName} token={token} handleLogout={handleLogout} />
    );
  } else {
    render = (
      <Lobby
         username={username}
         roomName={roomName}
         handleUsernameChange={handleUsernameChange}
         handleRoomNameChange={handleRoomNameChange}
         handleSubmit={handleSubmit}
      />
    );
  }
  return render;
};

Wenn wir das jetzt im Browser ausführen, werden der Raumname und die Schaltfläche „Abmelden“ angezeigt, aber keine Teilnehmeridentitäten, da wir noch keine Verbindung zum Raum hergestellt haben und ihm noch nicht beigetreten sind.

Wenn wir jetzt das Formular abschicken, sehen wir den Raumnamen – in diesem Fall „Awesome Room“ – und Platz für Remote-Teilnehmer.

Wir haben alle erforderlichen Informationen, um dem Raum beizutreten, deshalb sollten wir die Aktion zum Herstellen einer Verbindung beim ersten Rendern der Komponente auslösen. Und sobald die Komponente zerstört ist, möchten wir den Raum verlassen. (Es ergibt keinen Sinn, eine WebRTC-Verbindung im Hintergrund aufrechtzuerhalten.) Das sind beides Nebenwirkungen.

Bei klassenbasierten Komponenten würden wir hier die lifecycle-Methoden componentDidMount und componentWillUnmount verwenden. Bei React-Hooks verwenden wir den useEffect-Hook.

useEffect

useEffect ist eine Funktion, die eine Methode benötigt und diese ausführt, sobald die Komponente gerendert wurde. Wenn die Komponente geladen wird, möchten wir eine Verbindung zum Videodienst herstellen. Außerdem benötigen wir Funktionen, die wir immer dann ausführen können, wenn ein Teilnehmer dem Raum beitritt oder ihn wieder verlässt, um Teilnehmer dem Zustand hinzuzufügen bzw. sie aus dem Zustand zu entfernen.

Beginnen wir mit dem Erstellen des Hooks, indem wir den folgenden Code vor JSX in der Datei Room.js einfügen:

  useEffect(() => {
    const participantConnected = participant => {
      setParticipants(prevParticipants => [...prevParticipants, participant]);
    };
    const participantDisconnected = participant => {
      setParticipants(prevParticipants =>
        prevParticipants.filter(p => p !== participant)
      );
    };
    Video.connect(token, {
      name: roomName
    }).then(room => {
      setRoom(room);
      room.on('participantConnected', participantConnected);
      room.on('participantDisconnected', participantDisconnected);
      room.participants.forEach(participantConnected);
    });
  });

Dadurch werden token und roomName zum Herstellen einer Verbindung zum Twilio Video-Dienst verwendet. Wenn die Verbindung hergestellt wurde, legen wir den Zustand des Raums fest, richten einen Listener für weitere Teilnehmer ein, die eine Verbindung herstellen oder diese trennen, und führen eine Schleife aus, um vorhandene Teilnehmer dem Zustand des Teilnehmer-Arrays hinzuzufügen. Dazu bedienen wir uns der participantConnected-Funktion, die wir zuvor geschrieben haben.

Das ist zwar ein guter Anfang, aber wenn wir die Komponente entfernen, sind wir immer noch mit dem Raum verbunden. Wir müssen also hinter uns aufräumen.

Wenn wir vom Rückruf, den wir an useEffect übergeben, eine Funktion zurückgeben, wird diese ausgeführt, wenn die Bereitstellung der Komponente aufgehoben wird. Wenn eine Komponente, die useEffect verwendet, neu gerendert wird, wird auch diese Funktion aufgerufen, um den Effekt zu bereinigen, bevor die Komponente erneut ausgeführt wird.

Wir geben jetzt eine Funktion zurück, die alle Video- und Audiospuren des lokalen Teilnehmers beendet und dann die Verbindung zum Raum trennt, falls der lokale Teilnehmer verbunden ist:

    Video.connect(token, {
      name: roomName
    }).then(room => {
      setRoom(room);
      room.on('participantConnected', participantConnected);
      room.participants.forEach(participantConnected);
    });

    return () => {
      setRoom(currentRoom => {
        if (currentRoom && currentRoom.localParticipant.state === 'connected') {
          currentRoom.localParticipant.tracks.forEach(function(trackPublication) {
            trackPublication.track.stop();
          });
          currentRoom.disconnect();
          return null;
        } else {
          return currentRoom;
        }
      });
    };
  });

Wir müssen beachten, dass wir hier die Rückruf-Version der setRoom-Funktion verwenden, die wir zuvor von useState abgerufen haben. Wenn wir eine Funktion an setRoom übergeben, wird diese anhand des vorherigen Werts aufgerufen. In diesem Fall ist das der vorhandene Raum, den wir currentRoom nennen. Der Zustand wird auf das festgelegt, was wir zurückgeben.

Das ist aber noch nicht alles. Im aktuellen Zustand verlässt diese Komponente den Raum, dem wir beigetreten sind, und stellt jedes Mal erneut eine Verbindung mit ihm her, wenn die Komponente neu gerendert wird. Das ist nicht optimal, deshalb müssen wir der Komponente sagen, wann eine Bereinigung erforderlich ist und der Effekt erneut ausgeführt werden soll. Ähnlich wie bei useCallback übergeben wir in diesem Fall ein Array mit Variablen, von denen der Effekt abhängig ist. Wenn sich die Variablen geändert haben, möchten wir, dass zuerst eine Bereinigung und dann der Effekt erneut ausgeführt wird. Wenn sie sich nicht geändert haben, muss der Effekt nicht erneut ausgeführt werden.

Wenn wir die Funktion genauer betrachten, dann müssten wir erwarten, dass beim Ändern von roomName oder token eine Verbindung zu einem anderen Raum oder als anderer Benutzer hergestellt wird. Wir übergeben diese Variablen also auch als Array an useEffect:

    return () => {
      setRoom(currentRoom => {
        if (currentRoom && currentRoom.localParticipant.state === 'connected') {
          currentRoom.localParticipant.tracks.forEach(function(trackPublication) {
            trackPublication.track.stop();
          });
          currentRoom.disconnect();
          return null;
        } else {
          return currentRoom;
        }
      });
    };
  }, [roomName, token]);

Wir müssen beachten, dass in diesem Effekt zwei Rückruf-Funktionen definiert sind. Man könnte jetzt vielleicht denken, dass diese wie zuvor mit useCallback umschlossen werden sollten, das ist aber nicht der Fall. Da sie Teil des Effekts sind, werden sie nur ausgeführt, wenn die Abhängigkeiten aktualisiert werden. Außerdem können wir in Rückruf-Funktionen keine Hooks verwenden. Sie müssen direkt in Komponenten oder einem benutzerdefinierten Hook verwendet werden.

Wir sind jetzt fast durch mit dieser Komponente. Prüfen wir, ob sie soweit auch funktioniert. Wir laden die Anwendung neu und geben einen Benutzernamen und Raumnamen ein. Sobald wir dem Raum beitreten, sollten wir unsere Identität sehen. Wenn wir auf die Schaltfläche „Abmelden“ klicken, gelangen wir in den Wartebereich zurück.

Wenn wir jetzt das Formular abschicken, sehen wir alles wie vorher – und die eigene Identität.

Das letzte Puzzleteil ist das Rendern der Teilnehmer im Videoanruf, indem ihr Video und Audio auf der Webseite hinzugefügt werden.

Die Participant-Komponente

Wir erstellen eine neue Komponente unter src mit dem Namen Participant.js. Wir beginnen mit dem üblichen Textbaustein, aber in dieser Komponente verwenden wir drei Hooks: useState und useEffect, die uns bereits bekannt sind, und useRef. Außerdem übergeben wir ein participant-Objekt in den Eigenschaften und verfolgen die Video- und Audiospur des Teilnehmers mit useState:

import React, { useState, useEffect, useRef } from 'react';

const Participant = ({ participant }) => {
  const [videoTracks, setVideoTracks] = useState([]);
  const [audioTracks, setAudioTracks] = useState([]);
};

export default Participant;

Wenn wir einen Video- oder Audiodatenstrom von unserem Teilnehmer empfangen, fügen wir diesen einem <video>- oder <audio>-Element an. Da JSX deklarativ ist, haben wir keinen direkten Zugriff auf das Dokumentobjektmodell (DOM). Deshalb müssen wir uns auf anderem Weg eine Referenz zum HTML-Element besorgen.

React bietet Zugriff auf das DOM über Refs und den useRef-Hook. Wenn wir Refs verwenden möchten, deklarieren wir sie zuerst, und verweisen dann auf sie innerhalb von JSX. Wir erstellen unsere Refs mithilfe des useRef-Hooks, bevor wir uns ans Rendern machen:

const Participant = ({ participant }) => {
  const [videoTracks, setVideoTracks] = useState([]);
  const [audioTracks, setAudioTracks] = useState([]);

  const videoRef = useRef();
  const audioRef = useRef();
 });

Wir geben jetzt das gewünschte JSX-Element zurück. Zum Anfügen des JSX-Elements an die Ref verwenden wir das ref-Attribut.

const Participant = ({ participant }) => {
  const [videoTracks, setVideoTracks] = useState([]);
  const [audioTracks, setAudioTracks] = useState([]);

  const videoRef = useRef();
  const audioRef = useRef();

  return (
    <div className="participant">
      <h3>{participant.identity}</h3>
      <video ref={videoRef} autoPlay={true} />
      <audio ref={audioRef} autoPlay={true} muted={true} />
    </div>
  );
 });

Außerdem habe ich die Attribute der <video>- und <audio>-Tags auf automatische Wiedergabe eingestellt (damit diese wiedergegeben werden, sobald ein Mediadatenstrom empfangen wird) und stummgeschaltet (damit wir uns beim Testen durch Rückkopplungen keinen Gehörschaden zufügen; diesen Fehler mache ich bestimmt nicht noch einmal!).

Diese Komponente an sich hat noch keine Funktion, da wir zuerst noch ein paar Effekte anwenden müssen. Wir werden den useEffect-Hook sogar dreimal in dieser Komponente verwenden. Warum, das sehen wir gleich.

Der erste useEffect-Hook legt die Video- und Audiospur im Zustand fest und richtet Listeners für das Participant-Objekt ein, für den Fall, dass Video- und Audiospuren hinzugefügt oder entfernt werden. Wenn die Bereitstellung der Komponente aufgehoben wird, muss er außerdem diese Listeners bereinigen und entfernen und den Zustand leeren.

Unserem ersten useEffect-Hook fügen wir zwei Funktionen hinzu, die ausgeführt werden, wenn entweder eine Spur zum Teilnehmer hinzugefügt oder vom Teilnehmer entfernt wird. Beide Funktionen prüfen, ob es sich bei der Spur um eine Video- oder Audiospur handelt, und fügen sie dann dem Zustand hinzu oder entfernen sie aus dem Zustand mithilfe der entsprechenden Zustandsfunktion.

  const videoRef = useRef();
  const audioRef = useRef();

  useEffect(() => {
    const trackSubscribed = track => {
      if (track.kind === 'video') {
        setVideoTracks(videoTracks => [...videoTracks, track]);
      } else {
        setAudioTracks(audioTracks => [...audioTracks, track]);
      }
    };

    const trackUnsubscribed = track => {
      if (track.kind === 'video') {
        setVideoTracks(videoTracks => videoTracks.filter(v => v !== track));
      } else {
        setAudioTracks(audioTracks => audioTracks.filter(a => a !== track));
      }
    };

    // more to come

Als Nächstes verwenden wir das Participant-Objekt, um die Anfangswerte für die Audio- und Videospuren festzulegen. Teilnehmer verfügen über videoTracks- und audioTracks-Eigenschaften, die eine Zuordnung von TrackPublication-Objekten zurückgeben. Eine TrackPublication hat erst Zugriff auf ihr track-Objekt, wenn es abonniert wird. Deshalb müssen wir alle Spuren herausfiltern, die nicht vorhanden sind. Dazu bedienen wir uns einer Funktion, die eine Zuordnung zwischen TrackPublications und Tracks durchführt und alle Spuren herausfiltert, die den Wert null haben.

Anschließend richten wir mit den gerade eben geschriebenen Funktionen Listeners für trackSubscribed- und trackUnsubscribed-Ereignisse ein und führen eine Bereinigung mit der zurückgegebenen Funktion durch:

  const trackpubsToTracks = trackMap => Array.from(trackMap.values())
    .map(publication => publication.track)
    .filter(track => track !== null);

  useEffect(() => {
    const trackSubscribed = track => {
      // implementation
    };

    const trackUnsubscribed = track => {
      // implementation
    };

    setVideoTracks(trackpubsToTracks(participant.videoTracks));
    setAudioTracks(trackpubsToTracks(participant.audioTracks));

    participant.on('trackSubscribed', trackSubscribed);
    participant.on('trackUnsubscribed', trackUnsubscribed);

    return () => {
      setVideoTracks([]);
      setAudioTracks([]);
      participant.removeAllListeners();
    };
  }, [participant]);

  return (
    <div className="participant">

Wir müssen beachten, dass der Hook nur vom participant-Objekt abhängt und nur dann bereinigt und erneut ausgeführt wird, wenn sich der Teilnehmer ändert.

Außerdem benötigen wir einen useEffect-Hook, um die Video- und Audiospuren an das DOM anzufügen. Ich gehe hier nur auf die Videospur ein, aber der Vorgang ist der gleiche für Audio. Dabei muss nur Video durch Audio ersetzt werden. Der Hook ruft die erste Videospur vom Zustand ab und fügt sie, sofern vorhanden, an den DOM-Knoten an, den wir zuvor mit einer Ref erfasst haben. Wir können auf den aktuellen DOM-Knoten in der Ref mithilfe von videoRef.current verweisen. Wenn wir die Videospur anfügen, müssen wir außerdem eine Funktion zurückgeben, mit der die Spur bei der Bereinigung getrennt wird.

  }, [participant]);

  useEffect(() => {
    const videoTrack = videoTracks[0];
    if (videoTrack) {
      videoTrack.attach(videoRef.current);
      return () => {
        videoTrack.detach();
      };
    }
  }, [videoTracks]);

  return (
    <div className="participant">

Wir wiederholen diesen Hook für audioTracks, und dann sind wir auch schon bereit, unsere Participant-Komponente über die Room-Komponente zu rendern. Wir importieren die Participant-Komponente am Anfang der Datei und ersetzen die Absätze, die die Identität angezeigt haben, durch die Komponente selbst.

import React, { useState, useEffect } from 'react';
import Video from 'twilio-video';
import Participant from './Participant';

// hooks here

  const remoteParticipants = participants.map(participant => (
    <Participant key={participant.sid} participant={participant} />
  ));

  return (
    <div className="room">
      <h2>Room: {roomName}</h2>
      <button onClick={handleLogout}>Log out</button>
      <div className="local-participant">
        {room ? (
          <Participant
            key={room.localParticipant.sid}
            participant={room.localParticipant}
          />
        ) : (
          ''
        )}
      </div>
      <h3>Remote Participants</h3>
      <div className="remote-participants">{remoteParticipants}</div>
    </div>
  );
});

Wir laden die App jetzt neu, treten einem Raum bei und wir werden uns selbst auf dem Bildschirm sehen. Wir öffnen einen weiteren Browser, treten demselben Raum bei und wir werden uns zweimal sehen. Wenn wir auf die Schaltfläche „Abmelden“ klicken, gelangen wir in den Wartebereich zurück.

Ein voller Erfolg! Ihr solltet euch jetzt selbst in einem Videochat mit euch selbst sehen.

Fazit

Das Erstellen mit Twilio Video in React erfordert etwas Mehraufwand, da es eine Vielzahl von Nebenwirkungen gibt, die beachtet werden müssen. Von der Anforderung des Tokens über das Herstellen einer Verbindung zum Videodienst bis hin zum Bearbeiten des DOM, um <video>- und <audio>-Elemente zu verbinden: Es gibt einiges, auf das wir uns einstellen müssen. In diesem Blogbeitrag haben wir gesehen, wie useStateuseCallbackuseEffect und useRef verwendet werden, um diese Nebenwirkungen zu kontrollieren und unsere App nur mit Funktionskomponenten zu erstellen.

Ich hoffe, dass dieser Blogbeitrag zum besseren Verständnis von Twilio Video und React-Hooks beiträgt. Der gesamte Quellcode dieser Anwendung ist auf GitHub verfügbar. Du kannst ihn in seine Einzelteile zerlegen und wieder zusammensetzen.

Weitere Informationen zu React-Hooks findest du in der sehr detaillierten offiziellen Dokumentation sowie in dieser Veranschaulichung unter „Thinking in React Hooks“ und in Dan Abramovs Tiefenanalyse zu useEffect (ein langer Post, bei dem sich das Lesen lohnt, versprochen).

Wenn du mehr über das Entwickeln mit Twilio Video erfahren möchtest, dann sieh dir die folgenden Posts an: Kamerawechsel während eines Videochats mit Twilio Video und Hinzufügen der Bildschirmfreigabe zum Videochat.

Wenn du diese oder andere coole Videochat-Funktionen in React erstellst, möchte ich gern davon erfahren. Hinterlasse einfach hier oder auf Twitter einen Kommentar oder sende eine E-Mail an philnash@twilio.com.