Blog

Zo implementeer je videovergaderingen in Next.js

· Andrij Tkachenko · 8 min leestijd

Een browservenster met een videovergadering, vijf mensen en een gedeeld scherm in een raster met de knoppen van het gesprek eronder, naast een lijst collega’s, drie ervan gemarkeerd als online

Een videovergadering lijkt één functie, maar in een Next.js-app zijn het er meerdere. Een team opent zijn vergaderruimte en de leden zien en horen elkaar, met een paar mensen of met tientallen, en delen hun scherm. Iemand leidt de vergadering en kan een lawaaiige microfoon dempen of iemand eruit halen. Een collega die de pagina opent terwijl de vergadering bezig is, moet er meteen in zitten, en de rest van het team moet horen dat de vergadering begonnen is, op welke pagina ze ook zitten.

Dit artikel loopt elk onderdeel langs dat een echte app nodig heeft, met een volledig werkend voorbeeld: Team Space, het teamintranet uit onze Next.js-handleiding, waar elk team een eigen vergaderruimte en eigen kanalen heeft, en collega’s zien wie er online is en elkaar bellen. Je kunt het live uitproberen: wees Mia, die Design leidt, en start de vergadering van haar team, open daarna de pagina van Design in een privévenster als Ava, ook van Design, en zij zit in de vergadering zodra de pagina opent.

Wat videovergaderingen nodig hebben

  • Veel camera’s tegelijk. Tussen twee mensen kan het beeld rechtstreeks van de ene browser naar de andere. In een vergadering stuurt elke browser het camerabeeld één keer naar mediaservers, die het doorsturen naar alle anderen (hoe mediaservers werken). De verbindingen blijven de hele vergadering open, en dat kan een serverless functie niet volhouden (waarom, en wat je eraan doet).
  • Wie mag deelnemen, en wie leidt. Een regel op je server, met je eigen gegevens: de leden van het team doen mee, de teamleider is de host, en verder komt niemand erin.
  • Een vergaderruimte in de pagina. Ieders camera en scherm, de knoppen, en een vergadering die doorgaat terwijl mensen tussen de pagina’s van je app wisselen.
  • Binnenkomen. Een knop om de vergadering te starten of eraan deel te nemen, en een pagina die je meteen binnenlaat terwijl de vergadering bezig is.
  • De knoppen van de host. Iemand dempen, of eruit halen.
  • Weten dat een vergadering begonnen is. Een melding aan het team, en lijsten van de vergaderingen die nu bezig zijn en zichzelf bijwerken.

Je kunt elk onderdeel zelf bouwen, op WebRTC en eigen mediaservers. Of je laat een realtime cloud de media en het vergaderscherm draaien, terwijl je server het ene deel houdt dat echt van jou is: de regel. Dat doet Team Space, met WordPlus Cloud.

Het voorbeeld: een teamintranet

Collega’s werken in teams, en elk team heeft een teamleider. Een team heeft een eigen pagina: de vergaderruimte, de kanalen en de leden van het team, met wie online is bovenaan. Het is een Next.js-app met de App Router, Better Auth en Postgres, en het realtime deel is een paar bestanden. Vijf stappen maken de vergaderingen.

1. Wie mag deelnemen, en wie de host is

Elk team heeft een vergaderruimte, een groepsgesprek genoemd naar het nummer van het team: meeting:3. De site declareert ze één keer, met dat patroon, samen met de regel:

lib/realtime.ts
realtime.groupCalls('meeting:{id}', async (user, { id }) => {
    if (!(await isMember(id, user))) return false;
    const team = await teamOf(id);
    return team?.lead === String(user) ? { admin: true } : true;
});

Een lid van het team komt erin, de teamleider met admin: true, en daarmee is die de host. Iedereen anders krijgt false, en de pagina van het team vraagt dezelfde isMember, dus de pagina en de vergadering zijn het altijd eens. Als een pagina wil deelnemen, vraagt ze het eerst aan de eigen server van de site, op /api/realtime/calls: dezelfde route handler waar de gesprekken van de datingsite doorheen gaan (zo implementeer je videogesprekken in Next.js). De regel draait met de eigen sessie van de site, en de pagina krijgt een pas voor de vergadering, ondertekend met het geheim van de app, of de reden waarom niet.

2. De vergaderruimte in de pagina

De vergadering verschijnt in de pagina van het team zelf, op de plek die de pagina ervoor kiest, met de knop om haar te starten erin:

app/(app)/teams/[id]/meeting.tsx
export function MeetingRoom({ room, team, people }: Props) {
    const rt = useRealtime();
    return (
        <CallSpot rt={rt} call={room} className="h-[32rem] rounded-2xl">
            <p>The {team} meeting room</p>
            <GroupCallButton rt={rt} room={room} type="video" title={`${team} meeting`}>
                {people.length > 0 ? 'Join the meeting' : 'Start the meeting'}
            </GroupCallButton>
        </CallSpot>
    );
}

Vóór de vergadering toont CallSpot wat erin staat: de naam van de ruimte, wie er nu in de vergadering zit, en de knop. Zodra deze collega erin zit, neemt de vergadering die plek in: ieders camera in een raster, een gedeeld scherm groot in beeld, en de knoppen voor de microfoon, de camera, het scherm en om te vertrekken. Op een andere pagina van de app gaat de vergadering door in een venster in de hoek, en ze komt hier terug als de pagina van het team weer opent. De site verlaten of de pagina herladen vraagt eerst om bevestiging.

3. Meteen erin, zodra de pagina opent

Een collega die de pagina opent terwijl de vergadering bezig is, hoeft nergens op te drukken. De pagina weet van haar server of er iemand in de vergadering zit (stap 4), en geeft het element alleen dan join mee:

app/(app)/teams/[id]/meeting.tsx
const [join] = useState(people.length > 0 ? ('video' as const) : undefined);

<CallSpot rt={rt} call={room} join={join} className="h-[32rem] rounded-2xl">

Met join neemt het element deel aan de vergadering zodra de pagina opent, zoals de knop het zou doen, en de regel beslist net als bij een klik. Dat wordt één keer beslist, bij het openen van de pagina, en bewaard in state: wie de vergadering verlaat, of door de teamleider eruit wordt gehaald, blijft eruit zolang de pagina open is. Zit er niemand in de vergadering, dan doet de pagina nergens aan mee, en wie als eerste binnenkomt, start haar met de knop.

De collega komt binnen met camera en microfoon uit: de vergadering is meteen te zien en te horen, en de browser vraagt pas om toegang tot een van beide als er een via de knoppen wordt aangezet, zodat het openen van een pagina nooit onverwacht een camera start. Moeten mensen meteen kunnen praten als ze binnenkomen, dan zet joinWith="microphone" de microfoon al aan bij het deelnemen, en "microphone camera" allebei.

4. Wie erin zit

WordPlus Cloud weet wie er in elke vergadering zit, dus de site houdt er geen tabel voor bij:

lib/realtime.ts
export async function inMeeting(team: string): Promise<string[]> {
    const { list } = await realtime.call(meetingOf(team)).get();
    return list.flatMap((person) => (person.user ? [String(person.user)] : []));
}

De pagina van het team vraagt dat op tijdens het renderen, en toont de gezichten van wie in de vergadering zit, met „Mia and Omar are in the meeting”. De lijst met teams vraagt op dezelfde manier realtime.listCalls() op, voor elke vergadering die nu bezig is en hoeveel mensen er in elk ervan zitten.

5. Een vergadering begint, en wie het hoort

Als iemand een vergadering binnenkomt, stuurt WordPlus Cloud de site een webhook. De site laat elke pagina weten dat de vergaderingen zijn veranderd, en is die persoon de eerste, dan meldt de site het aan de rest van het team:

lib/realtime.ts
realtime.on('group.joined', async ({ call, user }) => {
    const team = teamIn(call);
    if (!team) return;
    await realtime.to('meetings').emit('changed');
    // The first one in starts it.
    const { people } = await realtime.call(call).get();
    if (people > 1) return;
    const others = (await peopleIn(team)).map((person) => person.id).filter((id) => id !== String(user));
    await realtime.toUser(others).emit('meeting', {
        team,
        name: (await teamOf(team))?.name,
        by: await nameOf(String(user)),
    });
});

toUser() bereikt elke pagina die elk van hen open heeft, op elk apparaat. In de layout luisteren de pagina’s naar allebei:

app/(app)/live.tsx
useUserEvent('meeting', (data: MeetingStarted) => {
    if (path === `/teams/${data.team}`) return; // its page shows it already
    show(`${data.by} started the ${data.name} meeting`, `/teams/${data.team}`);
});

useRoom('meetings', { changed: () => router.refresh() });

De eerste toont een melding die de pagina van het team opent: „Leo started the Engineering meeting”. De tweede laat elke pagina de server opnieuw om haar lijsten vragen, zonder herladen, zodat altijd klopt wie er in elke vergadering zit. De webhook group.left stuurt ook changed, als iemand vertrekt.

De host, zonder eigen code

admin: true in de regel is alles wat de host nodig heeft. Op het scherm van de teamleider heeft elke tegel van de anderen een knop om te dempen en een om te verwijderen, en het belscherm vraagt dat aan WordPlus Cloud via dezelfde route, die het alleen van een host aanneemt. Een gedempte collega mag de microfoon weer aanzetten, en wie eruit is gehaald, is er meteen uit. Wil je iemand buiten houden, dan zegt de regel nee.

Wat erbij zit

  • Scherm delen, met geluid, via de knoppen.
  • Ook gesprekken tussen twee mensen. In Team Space heeft een chat met een collega een eigen gesprek, dat naast de berichten verschijnt, zoals op de datingsite.
  • Namen en gezichten uit je app. Elke tegel toont de naam en foto van een collega zoals profile van de site ze geeft, zonder dat WordPlus Cloud een lijst met gebruikers bijhoudt.
  • Toegang intrekken. Als iemand een team verlaat, haalt de site die persoon met realtime.call(name).remove(user) meteen uit de vergadering van het team, en vanaf dan zegt de regel nee.

Probeer het, en bouw dan je eigen

In de live demo zijn de collega’s en teams een dag lang alleen van jou, dus wat je er ook schrijft of met wie je er ook vergadert, niemand anders ziet het, en een vergadering eindigt na 10 minuten vanzelf. Onder elke pagina toont How this page works stap voor stap de code die de pagina maakt, met de waarden van de pagina die je open hebt.

De handleiding voor het teamintranet bouwt de hele site, met elk bestand van de realtime kant volledig uitgeschreven, en gesprekken in Next.js laat elke manier zien om een gesprek of een vergadering in je eigen app te zetten.

WordPlus Cloud for Apps is geprijsd op gelijktijdige verbindingen, vanaf 1.000, maandelijks of jaarlijks gefactureerd. Met 1.000 verbindingen worden groepsgesprekken niet gemeten tot 32 mensen tegelijk op video, waarbij iemand op audio voor de helft telt, en elke 1.000 verbindingen extra voegen er 12 toe. Daarboven kost elke extra persoon op video een credit per minuut, en gesprekken tussen twee mensen kosten geen credits. Bekijk videogesprekken voor apps, of de prijzen voor apps. Vragen over je integratie? Schrijf ons.