Blog

Kako implementirati videokonferencije u Next.js-u

· Andrij Tkachenko · 8 min čitanja

Prozor preglednika sa sastankom na videu, pet osoba i dijeljeni zaslon u rešetki, ispod njih kontrole poziva, a pokraj popis kolega, od kojih je troje označeno kao na mreži

Videosastanak izgleda kao jedna značajka, a u Next.js aplikaciji riječ je o nekoliko njih. Tim otvori svoju sobu za sastanke i svi se međusobno vide i čuju, bilo ih nekoliko ili nekoliko desetaka, i dijele svoje zaslone. Netko vodi sastanak i može utišati bučan mikrofon ili nekoga ukloniti. Kolega koji otvori stranicu dok sastanak traje trebao bi odmah biti na njemu, a ostali u timu trebali bi saznati da je počeo, na kojoj god stranici bili.

Ovaj članak prolazi kroz svaki dio koji prava aplikacija treba, na potpunom primjeru koji radi: Team Space, timskom intranetu iz našeg vodiča za Next.js, u kojem svaki tim ima svoju sobu za sastanke i svoje kanale, a kolege vide tko je na mreži i zovu jedni druge. Možete ga isprobati uživo: budite Mia, koja vodi tim Design, i pokrenite njegov sastanak, a zatim otvorite stranicu tima Design u privatnom prozoru kao Ava, također članica tima Design, i ona je na sastanku čim se stranica otvori.

Što videokonferencija treba

  • Mnogo kamera odjednom. Između dviju osoba slika može ići izravno iz jednog preglednika u drugi. Na sastanku svaki preglednik svoju kameru šalje samo jednom, medijskim poslužiteljima, a oni je prosljeđuju svima ostalima (kako rade medijski poslužitelji). Te veze ostaju otvorene tijekom cijelog sastanka, a to serverless funkcija ne može održati (zašto i što učiniti).
  • Tko se smije pridružiti, a tko vodi. Pravilo na vašem poslužitelju, s vašim podacima: članovi tima se pridružuju, njegov voditelj je domaćin, a nitko drugi ne ulazi.
  • Soba za sastanke na stranici. Kamera i zaslon svakoga sudionika, kontrole i sastanak koji se nastavlja dok ljudi prelaze s jedne stranice vaše aplikacije na drugu.
  • Ulazak. Gumb kojim se sastanak pokreće ili mu se pridružuje te stranica koja vas, dok sastanak traje, odmah uvodi u njega.
  • Kontrole domaćina. Utišati nekoga ili ga ukloniti sa sastanka.
  • Znati da je počeo. Obavijest timu i popisi sastanaka koji upravo traju, a koji se sami ažuriraju.

Svaki od tih dijelova možete izgraditi sami, na WebRTC-u i vlastitim medijskim poslužiteljima. Ili pustite da oblak za rad u stvarnom vremenu vodi medije i zaslon sastanka, dok vaš poslužitelj zadržava jedini dio koji je stvarno vaš: pravilo. To radi Team Space, preko WordPlus Clouda.

Primjer: timski intranet

Kolege rade u timovima, a svaki tim ima voditelja. Tim ima vlastitu stranicu: svoju sobu za sastanke, svoje kanale i tko je u timu, najprije oni koji su na mreži. To je Next.js aplikacija s App Routerom, Better Authom i Postgresom, a njezin dio za rad u stvarnom vremenu stane u nekoliko datoteka. Pet koraka čini njezine sastanke.

1. Tko se smije pridružiti, a tko je domaćin

Svaki tim ima sobu za sastanke, grupni poziv nazvan po broju tima: meeting:3. Stranica ih deklarira jednom, po tom uzorku, s pravilom:

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

Član tima ulazi, a njegov voditelj s admin: true, što ga čini domaćinom. Svi ostali dobivaju false, a stranica tima pita isti isMember, pa se stranica i sastanak nikad ne razilaze. Kad se stranica želi pridružiti sastanku, najprije pita vlastiti poslužitelj web-stranice, na /api/realtime/calls: to je isti route handler kroz koji prolaze pozivi stranice za upoznavanje (kako implementirati videopozive u Next.js-u). Pravilo se izvodi sa sesijom web-stranice, a stranica dobiva propusnicu za sastanak, potpisanu tajnom aplikacije, ili razlog zašto ne.

2. Soba za sastanke na stranici

Sastanak se prikazuje na samoj stranici tima, ondje gdje ga stranica postavi, a unutra je gumb kojim se pokreće:

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

Prije sastanka CallSpot prikazuje svoj sadržaj: ime sobe, tko je trenutačno na sastanku i gumb. Čim taj kolega uđe, sastanak zauzima njegovo mjesto: kamere svih sudionika u rešetki, dijeljeni zaslon u velikom prikazu i kontrole za mikrofon, kameru, zaslon i izlazak. Na drugoj stranici aplikacije sastanak se nastavlja u prozoru u kutu i vraća se ovamo kad se stranica tima ponovno otvori. Napuštanje web-stranice ili ponovno učitavanje stranice najprije traži potvrdu.

3. Ravno unutra, čim se stranica otvori

Kolega koji otvori stranicu dok sastanak traje ne bi trebao ništa pritiskati. Stranica od svojeg poslužitelja zna je li itko na sastanku (4. korak) i samo tada mjestu za poziv daje join:

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

S join se mjesto za poziv pridružuje sastanku čim se stranica otvori, kao što bi to učinio gumb, a pravilo odlučuje kao i za klik. O tome se odlučuje jednom, pri otvaranju stranice, i to se čuva u stanju: tko napusti sastanak ili koga voditelj ukloni, ostaje vani dok je stranica otvorena. Kad na sastanku nema nikoga, stranica se ne pridružuje ničemu, a prvi koji uđe pokreće sastanak gumbom.

Kolega ulazi s isključenom kamerom i isključenim mikrofonom: odmah vidi i čuje sastanak, a preglednik ne traži dopuštenje ni za jedno ni za drugo dok sam ne uključi jedno od njih u kontrolama, pa otvaranje stranice nikad iznenada ne uključuje kameru. Kad ljudi trebaju govoriti čim uđu, joinWith="microphone" uključuje mikrofon već pri ulasku, a "microphone camera" i jedno i drugo.

4. Tko je na sastanku

WordPlus Cloud zna tko je na kojem sastanku, pa stranica za to ne vodi nikakvu tablicu:

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

Stranica tima poziva tu funkciju dok se iscrtava i prikazuje lica onih koji su na sastanku uz „Mia and Omar are in the meeting”. Popis timova na isti način poziva realtime.listCalls(), za svaki sastanak koji upravo traje i broj ljudi na svakome.

5. Početak sastanka i tko za njega sazna

Kad netko uđe na sastanak, WordPlus Cloud šalje web-stranici webhook. Ona javlja svakoj stranici da su se sastanci promijenili, a kad je netko ušao prvi, javlja to i ostatku tima:

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() stiže do svih stranica koje svaki od njih ima otvorene, na bilo kojem uređaju. U layoutu stranice primaju oba događaja:

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

Prvi prikazuje obavijest koja otvara stranicu tima: „Leo started the Engineering meeting”. Drugi navodi svaku stranicu da ponovno zatraži od poslužitelja svoje popise, bez ponovnog učitavanja, pa prikaz tko je na kojem sastanku uvijek odgovara stvarnosti. Webhook group.left također šalje changed, kad netko izađe.

Domaćin, bez vašeg koda

admin: true u pravilu je sve što domaćin treba. Na voditeljevu zaslonu svaka pločica ostalih sudionika ima gumbe za utišavanje i uklanjanje, a zaslon poziva te zahtjeve šalje WordPlus Cloudu preko iste rute, koji ih prihvaća samo od domaćina. Utišani kolega smije ponovno uključiti mikrofon, a tko je uklonjen, odmah je izvan sastanka. Da bi ga se zadržalo vani, pravilo za njega kaže ne.

Što dolazi uz to

  • Dijeljenje zaslona, s njegovim zvukom, iz kontrola.
  • I pozivi u dvoje. U aplikaciji Team Space razgovor s kolegom ima vlastiti poziv, koji se prikazuje pokraj poruka, kao i na stranici za upoznavanje.
  • Imena i lica iz vaše aplikacije. Svaka pločica prikazuje ime i fotografiju kolege onako kako ih daje profile web-stranice, a u WordPlus Cloudu se ne vodi nikakav popis korisnika.
  • Oduzimanje pristupa. Kad netko napusti tim, stranica ga s realtime.call(name).remove(user) odmah uklanja sa sastanka tog tima, a od tada pravilo kaže ne.

Isprobajte, pa izgradite svoju

U demu uživo kolege i timovi na jedan dan pripadaju samo vama, pa što god ondje napišete ili s kim god se sastanete, nitko drugi to ne vidi, a sastanak sam završava nakon 10 minuta. Ispod svake stranice ploča How this page works prikazuje kod koji je pokreće, korak po korak, s vrijednostima stranice koju imate otvorenu.

Vodič kroz timski intranet izgrađuje cijelu web-stranicu i u cijelosti donosi svaku datoteku njezina dijela za rad u stvarnom vremenu, a Pozivi u Next.js-u pokazuju svaki način da poziv ili sastanak stavite u vlastitu aplikaciju.

WordPlus Cloud for Apps ima cijenu prema istodobnim vezama, od 1.000 naviše, uz mjesečnu ili godišnju naplatu. S 1.000 veza grupni pozivi nisu obračunati do 32 osobe na videu istodobno, pri čemu se osoba samo sa zvukom računa kao pola, a svakih daljnjih 1.000 veza dodaje još 12. Iznad toga svaka dodatna osoba na videu troši kredit po minuti, a pozivi između dviju osoba ne troše ništa. Pogledajte videopozive za aplikacije ili cijene za aplikacije. Pitanja o vašoj integraciji? Pišite nam.