Blog

Come creare consulenze video private in Next.js

· Andrij Tkachenko · 9 min di lettura

Una finestra del browser con una consulenza video tra due persone, un lucchetto nella barra in alto e i controlli della chiamata sotto, accanto a una colonna di messaggi e a un file con una graffetta

Una consulenza video sembra una sola chiamata, e in un’app Next.js sono diverse funzionalità. Qualcuno prenota mezz’ora con una medica, un avvocato, un’insegnante privata o un consulente, e quell’orario sparisce da tutte le pagine nello stesso istante, così nessun altro lo prenota. All’ora fissata, chi ha prenotato attende, e chi offre la consulenza, su qualunque pagina si trovi, vede l’attesa e fa entrare. La visita è una videochiamata tra le due persone a cui nessun altro può unirsi, e che il servizio che la trasporta non può né vedere né sentire. Accanto, i messaggi e i file dell’appuntamento, conservati dal tuo server, e dopo, le note dello specialista.

Questo articolo percorre ogni parte che serve a un’app reale, con un esempio completo e funzionante: Consultations, il sito di consulenze private della nostra guida a Next.js, dove si prenotano specialisti, si attende in una sala d’attesa e ci si incontra in video. Puoi provarlo dal vivo: sii Ava, con una visita da Lena aperta adesso, e premi I'm ready. Poi apri il sito in una finestra privata come Lena, che vede Ava nella sala d’attesa, e fai entrare Ava.

Cosa serve a una consulenza video

  • Una prenotazione che regge. Gli orari disponibili su ogni pagina, e un orario che sparisce da tutte appena qualcuno lo prenota.
  • Una sala d’attesa. Chi ha prenotato dice di essere pronto, e lo specialista vede l’attesa da qualunque pagina, non solo da quella della visita.
  • Far entrare. Solo le due persone dell’appuntamento, solo intorno al suo orario, e chi ha prenotato solo quando lo specialista lo fa entrare.
  • Riservatezza. Videocamera e microfono cifrati end-to-end, così i server che trasportano la chiamata inoltrano ciò che non possono né vedere né sentire.
  • Messaggi e file accanto, conservati dove tieni i tuoi dati, e note per dopo.
  • Una fine. La visita si chiude alla fine della sua mezz’ora, e nessuno ci rientra.

Puoi costruire tutto questo da solo, su WebRTC, server multimediali e un server websocket tuo. Oppure lasci a un cloud in tempo reale i flussi audio e video, la sala d’attesa e la schermata di chiamata, mentre il tuo server tiene ciò che è davvero tuo: le prenotazioni, le regole e i file. È quello che fa Consultations, tramite WordPlus Cloud.

L’esempio: un sito di consulenze

Gli specialisti offrono mezz’ore: una medica di famiglia, un avvocato, un’insegnante privata, un consulente. Chi cerca un consiglio prenota un orario, oppure parla subito con uno specialista disponibile. Ogni appuntamento ha una pagina sua: la visita, i messaggi e, finita la visita, le note dello specialista. È un’app Next.js con l’App Router, Better Auth e Postgres, e la sua parte in tempo reale sta in pochi file. Cinque passi costruiscono le sue visite.

1. Orari che spariscono appena prenotati

Si prenota con una server action. Il server salva la prenotazione, e una mezz’ora già presa la rifiuta il database stesso. Poi lo comunica tramite WordPlus Cloud, due volte:

app/(app)/actions.ts
export async function bookVisit(expert: string, startsAt: number, topic: string): Promise<{ error: string }> {
    const me = await currentUserId();
    if (!me) return { error: 'Sign in first.' };
    const booked = await book(me, expert, startsAt, topic);
    if (typeof booked === 'string') return { error: booked };
    await realtime.toUser(expert).emit('booked', { appointment: booked.id, name: booked.clientName, startsAt: booked.startsAt });
    // Every page showing this expert's times takes this one away.
    await realtime.to('experts').emit('changed');
    redirect(`/appointments/${booked.id}`);
}

toUser() raggiunge ogni pagina che lo specialista ha aperta, su qualunque dispositivo, con un avviso della nuova visita. experts è una stanza in cui entra ogni pagina con accesso effettuato, e in cui parla solo il server. Ogni pagina sente changed e richiede se stessa al proprio server, senza ricaricare:

app/(app)/live.tsx
useRoom('experts', { changed: () => router.refresh() });

Chi stava guardando gli orari di quello specialista vede sparire la mezz’ora. La stessa stanza avvisa le pagine quando una visita inizia o finisce, così resta aggiornato anche chi è disponibile in questo momento. WordPlus Cloud sa quali visite sono in corso (realtime.listCalls()), e il sito non tiene nessuna tabella per questo.

2. La sala d’attesa

Ogni appuntamento ha una sala d’attesa, una stanza di presenza che porta il suo nome:

lib/realtime.ts
realtime.room('waiting:{id}', { presence: true }, async (user, { id }) => mayChat(user, id));

Entrano solo le due persone dell’appuntamento. Quando chi ha prenotato preme I'm ready, la sua pagina entra nella stanza e ci resta finché la pagina è aperta. La pagina dello specialista legge chi c’è dentro:

app/(app)/appointments/[id]/visit.tsx
function useWaiting(id: string, client: string): boolean {
    return usePresence(`waiting:${id}`).some((member) => String(member.id) === client);
}

La presenza segue la pagina: basta chiudere la scheda per uscire dalla sala d’attesa. E lo specialista non deve trovarsi sulla pagina della visita. Il server lo avvisa, con un evento waiting tramite realtime.toUser(), e su qualunque pagina compare un avviso, “Ava is in the waiting room”, che apre la visita.

3. Far entrare

La visita è una chiamata di gruppo con il nome dell’appuntamento, visit:81, dichiarata una volta, con quello schema, insieme alla sua regola:

lib/realtime.ts
realtime.groupCalls('visit:{id}', async (user, { id }) => mayJoinVisit(user, id), { encrypt: true });
lib/clinic.ts
export async function mayJoinVisit(user: string | number | null, id: string): Promise<boolean> {
    const appointment = await appointmentOf(id);
    if (!appointment || !isParty(appointment, user) || !visitOpen(appointment)) return false;
    return String(user) === appointment.expert || appointment.admittedAt !== null;
}

Le due persone dell’appuntamento e nessun altro, finché la visita è aperta: da dieci minuti prima del suo orario alla fine della sua mezz’ora, a meno che non sia finita prima. Lo specialista entra quando vuole mentre è aperta, chi ha prenotato solo dopo essere stato fatto entrare. Far entrare è una server action, che segna l’appuntamento e avvisa chi ha prenotato:

app/(app)/actions.ts
export async function letIn(id: string): Promise<boolean> {
    const me = await currentUserId();
    const appointment = me ? await admit(id, me) : null;
    if (!appointment) return false;
    await realtime.toUser(appointment.client).emit('admitted', { appointment: id, name: appointment.expertName });
    return true;
}

Da quel momento la regola dice sì, e la pagina in attesa entra nella visita da sola. 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 visita, firmato con il segreto dell’app, o il motivo per cui no.

4. La visita, cifrata end-to-end

{ encrypt: true } nella regola cifra la visita end-to-end. Ogni browser cifra videocamera e microfono con una chiave che crea il server del sito, e che ricevono solo quelli che la regola fa entrare. Le chiavi restano al sito e nei due browser, e i server multimediali di WordPlus Cloud inoltrano ciò che non possono né vedere né sentire (come funzionano i server multimediali).

La visita compare nella pagina stessa dell’appuntamento, dove la pagina la mette:

app/(app)/appointments/[id]/visit.tsx
<CallSpot rt={rt} call={`visit:${id}`} join={join} joinWith="microphone camera" className="h-[32rem] rounded-2xl">
    {/* The waiting room as each of the two sees it, until the page joins */}
</CallSpot>

Prima della visita, CallSpot mostra ciò che contiene: la sala d’attesa come la vede ciascuna delle due persone, con Let Ava in per lo specialista. join porta la pagina nella visita, appena lo specialista fa entrare o appena chi attende viene fatto entrare. joinWith="microphone camera" accende entrambi all’ingresso, perché in una visita ci si vuole vedere e sentire subito. Su un’altra pagina dell’app la visita continua in una finestra nell’angolo, e torna qui quando la pagina dell’appuntamento si riapre.

Una visita finisce anche. Lo specialista la chiude con un pulsante, e il sito la chiude alla fine della sua mezz’ora, con un timer che imposta quando la visita inizia:

lib/realtime.ts
export async function endVisit(appointment: Appointment) {
    await realtime.call(visitOf(appointment.id)).end().catch(() => {}); // not on, if nobody came in
    await markEnded(appointment.id);
    await realtime.toUser([appointment.expert, appointment.client]).emit('ended', { appointment: appointment.id });
    await realtime.to('experts').emit('changed');
}

end() fa uscire entrambi dalla chiamata, la regola, vedendo l’appuntamento chiuso, non fa più entrare nessuno, e le due pagine dicono che la visita è finita.

5. Messaggi e file, poi le note

Ogni appuntamento ha una conversazione, la stanza chat:{id}, solo per le sue due persone, prima della visita, durante e dopo. La Chat già pronta la mostra accanto alla visita, con chi sta scrivendo, le conferme di lettura e i file:

app/(app)/appointments/[id]/messages.tsx
<Chat
    rt={useRealtime()}
    room={room}
    messages={`/api/messages?room=${room}`}
    upload="/api/files"
    accept="image/*,.pdf,.doc,.docx,.txt"
    maxFileSize={10 * 1024 * 1024}
/>

Una foto, una scansione o un contratto va alla route del sito appena lo si sceglie, e la route chiede alla regola della stanza prima di conservarlo:

app/api/files/route.ts
if (!me || !appointment || !(file instanceof File) || !(await realtime.mayJoin(room, me))) {
    return Response.json({ error: 'This file is not for this conversation.' }, { status: 403 });
}

Il sito conserva il file nel proprio database e lo apre solo alle due persone. WordPlus Cloud trasporta soltanto il messaggio che dice dove si trova, sigillato con una chiave che tiene il sito, e mai il file. Il sito conserva anche ogni messaggio, dal webhook message, così la conversazione resta lì per entrambi dopo la visita. Finita la visita, lo specialista scrive le note, il sito le conserva, e la pagina di chi ha prenotato le mostra subito, ancora con toUser().

Cosa è incluso

  • Parla adesso. Chi cerca un consiglio chiede a uno specialista disponibile una sessione immediata. La pagina dello specialista mostra la richiesta, con un minuto per accettarla, ed entrambi entrano dritti in una nuova visita.
  • Chi è disponibile, aggiornato su ogni pagina. Uno specialista in visita risulta occupato, da realtime.listCalls(), senza nessuna tabella tua.
  • Online o no. La sala d’attesa dice se lo specialista è già online, con useOnline().
  • Nomi e volti dalla tua app. Ogni riquadro e ogni messaggio mostrano un nome e una foto come li dà il profile del sito, senza nessun elenco di utenti conservato in WordPlus Cloud.

Provalo, poi costruisci il tuo

Nella demo dal vivo, specialisti e clienti sono solo tuoi per un giorno, quindi quello che scrivi o chi incontri lì non lo vede nessun altro, e una visita 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 aperta.

La guida alle consulenze private costruisce l’intero sito, con ogni file della sua parte in tempo reale, le chiamate in Next.js raccolgono ogni modo di mettere una chiamata nella tua app, e la cifratura spiega cosa viaggia sigillato e chi tiene ogni chiave. Per la riunione di un intero team invece che di due persone, leggi come implementare le videoconferenze in Next.js.

WordPlus Cloud for Apps ha un prezzo in base alle connessioni simultanee, a partire da 1.000, con fatturazione mensile o annuale. Una visita è una chiamata di gruppo tra due persone. Con 1.000 connessioni, le chiamate di gruppo sono senza conteggio fino a 32 persone contemporanee in video, cioè 16 visite, 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.