בלוג

איך מטמיעים שידור חי ב-Next.js

· Andrij Tkachenko · שתי דקות קריאה

חלון דפדפן עם אדם בווידאו ואורח לצידו, שמשדר לארבעה חלונות קטנים יותר, באחד מישהו מרים יד ובאחר מישהו כותב

שידור חי נראה כמו תכונה אחת, ובאפליקציית Next.js הוא כמה תכונות. מישהו עולה לשידור מהדפדפן, או ממקודד כמו OBS. כל מי שמורשה לצפות רואה ושומע אותו באותו רגע, קרוב מספיק כדי לשאול שאלה ולקבל תשובה. הצופים כותבים בצ׳אט, מגיבים, וחלקם עולים לבמה עם המצלמה שלהם. ולפני כל זה, האפליקציה שלכם צריכה להחליט מי משדר, מי צופה ומי שומע ששידור התחיל.

הפוסט הזה עובר על כל חלק שאפליקציה אמיתית צריכה, עם דוגמה שלמה ועובדת: Live Lessons, אתר השיעורים החיים מהמדריך שלנו ל-Next.js, שבו מורים לבישול, מוזיקה וכושר עולים לשידור בשיעורים שלהם, והתלמידים צופים, שואלים ועולים לבמה. אפשר לנסות אותו בשידור חי: היו מיה ועלו לשידור מהסטודיו שלה, ואז פתחו את השיעור שלה בחלון פרטי בתור אווה, אחת התלמידות שלה, כדי לצפות ולהרים יד.

מה שידור חי צריך

  • התמונה פנימה. המצלמה והמיקרופון מהדפדפן, מסך משותף עם הקול שלו, או מקודד דרך WHIP לסצנות ולכמה מצלמות.
  • התמונה החוצה, להרבה אנשים, באותו רגע. שידור שהקהל שלו מגיב עובר ב-WebRTC, כמו שיחות, דרך שרתי מדיה שמעבירים אותו לכל צופה (איך שרתי מדיה עובדים). החיבורים שלו נשארים פתוחים לאורך כל השידור, ופונקציית serverless לא יכולה להחזיק את זה (למה, ומה עושים).
  • מי משדר ומי צופה. כלל בשרת שלכם, עם הנתונים שלכם: המורה משדר, תלמידי הקורס צופים, ושיעור פתוח כל אחד יכול לראות.
  • סטודיו ונגן. עלייה לשידור וסיום, המצלמה והמסך, הצ׳אט, תגובות וכמה אנשים צופים.
  • הבמה. צופה מרים יד, המורה מזמין אותו, וכולם רואים אותו לצד המורה.
  • לדעת מה בשידור. רשימות של השידורים החיים כרגע שמתעדכנות מעצמן, והודעה למי שרוצה לדעת.

אפשר לבנות כל אחד מהחלקים האלה בעצמכם, על WebRTC ושרתי מדיה משלכם. או לתת לענן זמן אמת להריץ את המדיה, הסטודיו והנגן, בזמן שהשרת שלכם שומר את החלק היחיד שבאמת שלכם: הכלל. זה מה ש-Live Lessons עושה, דרך WordPlus Cloud.

הדוגמה: שיעורים חיים

למורים יש קורסים, לכל קורס השיעורים שלו. שיעור פתוח לכולם, או רק לתלמידי הקורס שלו. מורה עולה לשידור בשיעור מהסטודיו שלו, והתלמידים צופים בדף השיעור. זו אפליקציית Next.js עם App Router, ‏Better Auth ו-Postgres, וחלק זמן האמת שלה הוא כמה קבצים. חמישה צעדים יוצרים את השידורים שלה.

1. מי רשאי לצפות

לכל שיעור יש שידור, שנקרא לפי המספר שלו: lesson:7. האתר מצהיר עליהם פעם אחת, לפי התבנית הזאת, עם הכלל:

lib/realtime.ts
realtime.streams(
    'lesson:{id}',
    async (user, { id }) => {
        const lesson = await lessonOf(id);
        if (!lesson) return false;
        const me = user === null ? null : String(user);
        if (me === lesson.teacher) return { role: 'host', quality: '1080p' };
        return (await mayWatch(me, lesson)) ? 'viewer' : false;
    },
    { stage: true, reactions: true },
);

המורה הוא המארח, עם תמונה עד 1080p. מי ש-mayWatch מכניס הוא צופה, ו-user הוא null למבקר שלא התחבר, כך ששיעור פתוח לא דורש חשבון. כל השאר מקבלים false. ההגדרות מאפשרות אורחים על הבמה ותגובות. דף השיעור שואל את אותו mayWatch, כך שהדף והשידור אף פעם לא חלוקים.

2. איפה הכלל רץ

כשדף מבקש לעלות לשידור, או לצפות, הוא שואל קודם את השרת של האתר עצמו, ב-route handler:

app/api/realtime/calls/route.ts
import { currentUserId } from '@/lib/auth';
import { realtime } from '@/lib/realtime';

export async function POST(request: Request) {
    // JSON only, which a page of another site can't send here.
    if (!request.headers.get('content-type')?.includes('application/json')) {
        return new Response(null, { status: 415 });
    }
    const ask = await request.json();
    const user = await currentUserId();
    const pass = await realtime.authorizeCall(ask, user);
    return Response.json(pass);
}

זה אותו נתיב שדרכו עוברות שיחות הווידאו של אתר ההיכרויות (איך מטמיעים שיחות וידאו ב-Next.js). הכלל רץ עם הסשן של האתר עצמו, והדף מקבל אישור לשידור, חתום בסוד של האפליקציה, או את הסיבה שלא. הנתיב עונה בבקשה קצרה אחת, בדיוק הדבר שאירוח serverless טוב בו.

3. הסטודיו של המורה

מורה עולה לשידור מדף משלו, שבתוכו הסטודיו של WordPlus Cloud:

app/(site)/studio/[id]/page.tsx
<LiveStream
    name={roomOf(lesson.id)}
    view="studio-page"
    title={`${lesson.position}. ${lesson.title}`}
    url={`/lessons/${lesson.id}`}
    backUrl="/studio"
/>

הסטודיו ממלא את הגובה שהדף נותן לו: התמונה גדולה ככל שנכנס, ומתחתיה המצלמה, המסך, OBS ו-Go live, ולצידה, בלשוניות, הצ׳אט, האורחים עם הידיים המורמות, והשידור עם הקישור לשיעור לשיתוף. קודם לכן הדף בודק שמי שמבקש מלמד את השיעור, ושולח כל אחד אחר לשיעור. בהתחברות, מורה מגיע לרשימת השיעורים שלו, כל אחד במרחק לחיצה מעלייה לשידור.

4. דף השיעור

התלמידים צופים במקום שבו נמצא השיעור:

app/(site)/lessons/[id]/page.tsx
<LiveStream
    name={roomOf(lesson.id)}
    title={lesson.title}
    loginUrl="/signin"
    studioUrl={teaches ? `/studio/${lesson.id}` : undefined}
/>

אלמנט אחד מצייר את הנגן ואת הצ׳אט שלו. לפני שהשיעור מתחיל הוא אומר זאת, והוא מתחיל לנגן ברגע שהמורה עולה לשידור, בלי לטעון מחדש. תלמיד מחובר יכול לכתוב, להגיב וללחוץ על Ask to join on stage. מבקר צופה וקורא, עם הדרך להתחבר. המורה רואה מעל הנגן את הדרך לסטודיו שלו.

5. מה בשידור, תמיד מעודכן

WordPlus Cloud יודע אילו שידורים חיים וכמה צופים בכל אחד, כך שהאתר לא מחזיק לזה שום טבלה:

lib/realtime.ts
export async function liveNow(): Promise<Map<string, number>> {
    const live = new Map<string, number>();
    for (const stream of await realtime.listStreams()) {
        const lesson = lessonIn(stream.name);
        if (lesson) live.set(lesson, stream.viewers);
    }
    return live;
}

דף הבית מציג מזה את השיעורים שבשידור עכשיו. כדי שהרשימה הזאת תישאר מעודכנת גם בדפים שכבר פתוחים, WordPlus Cloud שולח לאתר webhook כששידור מתחיל וכשהוא נגמר, והאתר מעביר אותו לחדר שכל דף מצטרף אליו, גם הדף של מבקר:

lib/realtime.ts
realtime.room('live', { public: true });

realtime.on('stream.ended', async ({ stream }) => {
    if (lessonIn(stream)) await realtime.to('live').emit('changed');
});
app/(site)/live.tsx
export function LiveNow() {
    const router = useRouter();
    useRoom('live', { changed: () => router.refresh() });
    return null;
}

LiveNow נמצא ב-layout, כך שכל דף מבקש שוב מהשרת את הרשימות שלו, בלי לטעון מחדש. ה-webhook ‏stream.live עושה אותו דבר, ובנוסף מודיע לתלמידי הקורס, ובשיעור פתוח גם לעוקבים של המורה, בכל דף שבו הם נמצאים: „Mia is live: Your first tune”.

הבמה, בלי קוד שלכם

stage: true בכלל הוא כל מה שהבמה צריכה. בזמן ששיעור בשידור, תלמיד מרים יד מהנגן, המורה רואה את זה תחת Guests ולוחץ על Invite, וברגע שהתלמיד מקבל, כולם רואים אותו לצד המורה, עם מצלמה ומיקרופון. המורה יכול להשתיק אותו או להוריד אותו מהבמה, והתלמיד יכול לרדת ממנה בעצמו ולחזור לצפות בלי לטעון מחדש.

מה מגיע עם זה

  • מהדפדפן או עם OBS. הסטודיו עולה לשידור עם המצלמה, עם מסך והקול שלו, או עם שניהם, או מקבל OBS או כל מקודד אחר דרך WHIP.
  • באותו רגע. השידור עובר ב-WebRTC, כך ששאלה בצ׳אט מגיעה למורה כשהוא עדיין באותו נושא.
  • צ׳אט מסודר. מנחים מצמידים, מוחקים, משתיקים לזמן מה, מוציאים וחוסמים, והכלל יכול להפוך עוזר של המורה למנחה: { role: 'viewer', mod: true }.
  • שלילת גישה. כשתלמיד עוזב קורס, האתר מוציא אותו מהשיעורים החיים של אותו קורס עם realtime.stream(name).kick(), ומאז הכלל אומר לא.

נסו, ואז בנו משלכם

בהדגמה החיה, המורים והתלמידים שייכים רק לכם למשך יום, כך שמה שלא תשדרו או תכתבו שם, אף אחד אחר לא רואה. בתחתית כל דף, How this page works מראה את הקוד שמפעיל אותו, צעד אחר צעד, עם הערכים של הדף שפתוח אצלכם.

המדריך ל-Live Lessons בונה את כל האתר, עם כל קובץ של צד זמן האמת שלו במלואו, ו-שידורים חיים ב-Next.js מציג כל דרך להכניס שידור לאפליקציה שלכם, כולל ממשק משלכם.

WordPlus Cloud for Apps מתומחר לפי חיבורים בו-זמניים, החל מ-1,000, בחיוב חודשי או שנתי. עם 1,000 חיבורים, שידורים חיים אינם נמדדים עד 50 צופים בו-זמנית, וכל 1,000 חיבורים נוספים מוסיפים 20. מעבר לזה, כל צופה נוסף צורך קרדיט לדקה, שנספר רק כשהשידור באוויר. ראו שידור חי לאפליקציות, או את המחירים לאפליקציות. שאלות על האינטגרציה שלכם? כתבו לנו.