Blog

Come implementare le videoconferenze in Next.js

· Andrij Tkachenko · 8 min di lettura

Una finestra del browser con una riunione in video, cinque persone e uno schermo condiviso in una griglia con i controlli della chiamata sotto, accanto a un elenco di colleghi, tre dei quali segnati come online

Una riunione video sembra una sola funzione, e in un’app Next.js sono diverse. Un team apre la sua sala riunioni e i suoi membri si vedono e si sentono, che siano pochi o qualche decina, con i loro schermi condivisi. Qualcuno la conduce e può silenziare un microfono rumoroso o far uscire qualcuno. Un collega che apre la pagina mentre la riunione è in corso dovrebbe entrarci subito, e il resto del team dovrebbe venire a sapere che è iniziata, su qualunque pagina si trovi.

Questo articolo passa in rassegna ogni parte che serve a un’app vera, con un esempio completo e funzionante: Team Space, l’intranet per team della nostra guida Next.js, dove ogni team ha la sua sala riunioni e i suoi canali, e i colleghi vedono chi è online e si chiamano tra loro. Puoi provarlo dal vivo: sii Mia, a capo di Design, e avvia la sua riunione, poi apri la pagina di Design in una finestra privata come Ava, anche lei di Design, ed eccola nella riunione non appena la pagina si apre.

Cosa serve a una videoconferenza

  • Molte fotocamere insieme. Tra due persone, l’immagine può andare direttamente da un browser all’altro. In una riunione, ogni browser manda la sua fotocamera una sola volta ai server media, che la inoltrano a tutti gli altri (come funzionano i server media). Le loro connessioni restano aperte per tutta la riunione, cosa che una funzione serverless non può tenere (perché, e cosa fare).
  • Chi può entrare, e chi conduce. Una regola sul tuo server, con i tuoi dati: i membri del team entrano, il suo responsabile fa da host, e nessun altro può entrare.
  • Una sala riunioni nella pagina. Le fotocamere e gli schermi di tutti, i controlli, e una riunione che continua mentre le persone si spostano tra le pagine della tua app.
  • Entrare. Un pulsante per avviare la riunione o per unirsi, e una pagina che ti porta subito dentro mentre la riunione è in corso.
  • I controlli dell’host. Silenziare qualcuno, o farlo uscire.
  • Sapere che è iniziata. Un avviso al team, ed elenchi delle riunioni in corso che si aggiornano da soli.

Puoi costruire ciascuna di queste parti da solo, su WebRTC e server media tuoi. Oppure lasci che un cloud realtime gestisca i media e la schermata della riunione, mentre il tuo server tiene l’unica parte davvero tua: la regola. È ciò che fa Team Space, con WordPlus Cloud.

L’esempio: un’intranet per team

I colleghi lavorano in team, e ogni team ha un responsabile. Un team ha una pagina tutta sua: la sala riunioni, i canali, e chi fa parte del team, prima quelli online. È un’app Next.js con App Router, Better Auth e Postgres, e la sua parte in tempo reale sta in pochi file. Cinque passi fanno le sue riunioni.

1. Chi può entrare, e chi fa da host

Ogni team ha una sala riunioni, una chiamata di gruppo con il nome del numero del team: meeting:3. Il sito le dichiara una volta sola, con quello schema, insieme alla regola:

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

Un membro del team entra, il suo responsabile con admin: true, che lo rende host. Tutti gli altri ricevono false, e la pagina del team chiede allo stesso isMember, quindi la pagina e la riunione non sono mai in disaccordo. Quando una pagina chiede di entrare, chiede prima al server del sito, in /api/realtime/calls: lo stesso route handler da cui passano le chiamate del sito di incontri (come implementare le videochiamate in Next.js). La regola gira con la sessione del sito, e la pagina riceve un pass per la riunione, firmato con il segreto dell’app, o il motivo per cui no.

2. La sala riunioni nella pagina

La riunione compare nella pagina stessa del team, nel punto in cui la pagina la colloca, con dentro il pulsante per avviarla:

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

Prima della riunione, CallSpot mostra ciò che contiene: il nome della sala, chi è nella riunione in quel momento, e il pulsante. Appena questo collega entra, la riunione prende il suo posto: le fotocamere di tutti in una griglia, uno schermo condiviso in grande, e i controlli per il microfono, la fotocamera, lo schermo e per uscire. Su un’altra pagina dell’app, la riunione continua in una finestra nell’angolo, e torna qui quando si riapre la pagina del team. Lasciare il sito, o ricaricare la pagina, chiede prima conferma.

3. Dentro subito, all’apertura della pagina

Un collega che apre la pagina mentre la riunione è in corso non dovrebbe dover premere nulla. La pagina sa dal suo server se c’è qualcuno nella riunione (passo 4), e solo in quel caso passa join al componente:

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

Con join, il componente entra nella riunione all’apertura della pagina, come farebbe il pulsante, e la regola decide come per un clic. Si decide una volta sola, all’apertura della pagina, e resta nello stato: chi lascia la riunione, o chi il responsabile fa uscire, resta fuori finché la pagina è aperta. Se nella riunione non c’è nessuno, la pagina non entra in nulla, e il primo che arriva la avvia con il pulsante.

Il collega entra con fotocamera e microfono spenti: vede e sente subito la riunione, e il browser non chiede l’accesso a nessuno dei due finché non ne accende uno dai controlli, così aprire una pagina non avvia mai una fotocamera di sorpresa. Dove le persone devono poter parlare appena entrano, joinWith="microphone" le fa entrare con il microfono già acceso, e "microphone camera" con entrambi.

4. Chi c’è dentro

WordPlus Cloud sa chi c’è in ogni riunione, quindi il sito non tiene alcuna tabella:

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)] : []));
}

La pagina del team lo chiede mentre si disegna, e mostra i volti di chi è nella riunione con «Mia and Omar are in the meeting». L’elenco dei team chiede allo stesso modo a realtime.listCalls() ogni riunione in corso e quante persone ci sono in ciascuna.

5. Una riunione che inizia, e chi lo viene a sapere

Quando qualcuno entra in una riunione, WordPlus Cloud manda al sito un webhook. Il sito dice a ogni pagina che le riunioni sono cambiate, e quando è il primo a entrare, lo dice al resto del 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() raggiunge ogni pagina che ciascuno di loro ha aperto, su qualsiasi dispositivo. Nel layout, le pagine ascoltano entrambi:

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

Il primo mostra un avviso che apre la pagina del team: «Leo started the Engineering meeting». Il secondo fa sì che ogni pagina richieda di nuovo i suoi elenchi al server, senza ricaricare, così chi c’è in ogni riunione corrisponde sempre alla realtà. Anche il webhook group.left manda changed, quando qualcuno esce.

L’host, senza codice tuo

admin: true nella regola è tutto ciò che serve all’host. Sullo schermo del responsabile, ogni riquadro degli altri ha i comandi per silenziare e rimuovere, e la schermata della chiamata li chiede a WordPlus Cloud attraverso la stessa route, che li accetta solo da un host. Un collega silenziato può riaccendere il microfono, e chi viene rimosso è fuori subito. Per tenerlo fuori, la regola dice di no.

Cosa è incluso

  • La condivisione dello schermo, con il suo audio, dai controlli.
  • Anche le chiamate a due. In Team Space, una conversazione con un collega ha la sua chiamata, che compare accanto ai messaggi, come quelle del sito di incontri.
  • Nomi e volti dalla tua app. Ogni riquadro mostra il nome e la foto di un collega così come li dà il profile del sito, senza alcun elenco di utenti tenuto in WordPlus Cloud.
  • Togliere l’accesso. Quando qualcuno lascia un team, il sito lo fa uscire subito dalla sua riunione con realtime.call(name).remove(user), e da lì in poi la regola dice di no.

Provalo, poi costruisci il tuo

Nella demo dal vivo, i colleghi e i team sono solo tuoi per un giorno, quindi qualunque cosa tu scriva e chiunque tu incontri, nessun altro lo vede, e una riunione finisce da sola dopo 10 minuti. Sotto ogni pagina, How this page works mostra il codice che la fa funzionare, un passo alla volta, con i valori della pagina che hai aperto.

La guida all’intranet per team costruisce l’intero sito, con ogni file della sua parte in tempo reale per intero, e le chiamate in Next.js raccoglie tutti i modi per mettere una chiamata o una riunione nella tua app.

WordPlus Cloud for Apps ha un prezzo in base alle connessioni simultanee, a partire da 1.000, con fatturazione mensile o annuale. Con 1.000 connessioni, le chiamate di gruppo sono senza conteggio fino a 32 persone contemporanee in video, dove una persona in audio conta la metà, e ogni 1.000 connessioni in più ne aggiungono 12. Oltre, ogni persona in più in video consuma un credito al minuto, e le chiamate tra due persone non ne consumano. Scopri le videochiamate per le app, o i prezzi per le app. Domande sulla tua integrazione? Scrivici.