Browse Source
making backup values for multilanguage fields, md file made for egress architecture
master
making backup values for multilanguage fields, md file made for egress architecture
master
3 changed files with 454 additions and 7 deletions
@ -0,0 +1,424 @@ |
|||||
|
# Web Egress Recording Architecture |
||||
|
|
||||
|
This document describes how online-class recording works after replacing the old PlugNMeet recorder with LiveKit Web Egress. |
||||
|
|
||||
|
## Goal |
||||
|
|
||||
|
We no longer rely on the built-in PlugNMeet recorder for class recordings. |
||||
|
|
||||
|
Instead: |
||||
|
|
||||
|
- PlugNMeet is still the meeting server and source of classroom events |
||||
|
- LiveKit Egress is responsible for recording the rendered meeting page |
||||
|
- Django remains the source of truth for sessions, recordings, lessons, and attendance-related data |
||||
|
- The chat/FastAPI service acts as the bridge between Django and LiveKit Egress |
||||
|
|
||||
|
## Main Services |
||||
|
|
||||
|
### 1. `conference_client` |
||||
|
|
||||
|
Path: |
||||
|
|
||||
|
- `Online Class/conference_client/` |
||||
|
|
||||
|
Role: |
||||
|
|
||||
|
- Renders the online classroom UI |
||||
|
- Provides the `Rec` button for professor recording control |
||||
|
- Hosts the same meeting page used by normal users and the recorder bot |
||||
|
- Applies recorder-specific UI rules when the access token belongs to the recorder |
||||
|
|
||||
|
Important points: |
||||
|
|
||||
|
- The recorder joins this same frontend using a special access token |
||||
|
- Some UI branches are customized for `isRecorder` / `web_egress=1` |
||||
|
- The recorder should bypass landing/prejoin steps and go directly into the meeting |
||||
|
|
||||
|
### 2. `conference_server` / PlugNMeet |
||||
|
|
||||
|
Path: |
||||
|
|
||||
|
- `conference_server/conference_server/` |
||||
|
|
||||
|
Role: |
||||
|
|
||||
|
- Creates and runs meeting rooms |
||||
|
- Issues meeting join tokens |
||||
|
- Sends meeting lifecycle webhooks |
||||
|
- Continues to provide room state, participant lifecycle, and room end events |
||||
|
|
||||
|
Important points: |
||||
|
|
||||
|
- PlugNMeet still owns the real classroom |
||||
|
- Attendance and automatic session closing logic still depend on PlugNMeet room/webhook behavior |
||||
|
- Only the recording backend changed, not the meeting backend itself |
||||
|
|
||||
|
### 3. Django backend |
||||
|
|
||||
|
Path: |
||||
|
|
||||
|
- `backend/apps/course/` |
||||
|
|
||||
|
Role: |
||||
|
|
||||
|
- Manages `CourseLiveSession` |
||||
|
- Generates join URLs and meeting access tokens |
||||
|
- Tracks `LiveSessionUser` |
||||
|
- Stores `LiveSessionRecording` |
||||
|
- Creates course lessons automatically from saved recordings |
||||
|
- Controls recording start/stop through backend APIs |
||||
|
|
||||
|
Important files: |
||||
|
|
||||
|
- [live_session.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/live_session.py) |
||||
|
- [course.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/course.py) |
||||
|
- [webhook.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/webhook.py) |
||||
|
- [signals.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/signals.py) |
||||
|
- [web_egress.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/services/web_egress.py) |
||||
|
|
||||
|
### 4. Chat / FastAPI bridge service |
||||
|
|
||||
|
Path: |
||||
|
|
||||
|
- `chat/chat/` |
||||
|
|
||||
|
Role: |
||||
|
|
||||
|
- Receives recording start/stop requests from Django |
||||
|
- Talks to LiveKit Egress |
||||
|
- Tracks the current egress job state |
||||
|
- Receives LiveKit webhook callbacks |
||||
|
- Uploads the final recorded file back to Django |
||||
|
|
||||
|
Important files: |
||||
|
|
||||
|
- [web_egress_apis.py](/F:/WORK/CODE/WORK/imam-javad/chat/chat/web_egress_apis.py) |
||||
|
- [main.py](/F:/WORK/CODE/WORK/imam-javad/chat/chat/main.py) |
||||
|
- [settings.py](/F:/WORK/CODE/WORK/imam-javad/chat/chat/settings.py) |
||||
|
|
||||
|
### 5. LiveKit + Egress stack |
||||
|
|
||||
|
External/runtime project on server: |
||||
|
|
||||
|
- LiveKit server |
||||
|
- Redis |
||||
|
- Caddy/L4 proxy |
||||
|
- LiveKit Egress |
||||
|
|
||||
|
Role: |
||||
|
|
||||
|
- Accepts recording jobs |
||||
|
- Opens the target classroom URL in a browser-like environment |
||||
|
- Captures the rendered meeting page |
||||
|
- Saves the output file to shared storage |
||||
|
- Emits webhook events back to the chat/FastAPI bridge |
||||
|
|
||||
|
## High-Level Flow |
||||
|
|
||||
|
```mermaid |
||||
|
flowchart LR |
||||
|
A["Professor clicks Rec in conference_client"] --> B["Django recording API"] |
||||
|
B --> C["WebEgressClient in Django"] |
||||
|
C --> D["Chat/FastAPI /api/web-egress/start/"] |
||||
|
D --> E["LiveKit Egress job created"] |
||||
|
E --> F["Recorder bot opens conference_client join URL"] |
||||
|
F --> G["Rendered meeting page is recorded"] |
||||
|
G --> H["Egress stores output file"] |
||||
|
H --> I["LiveKit webhook -> chat/FastAPI"] |
||||
|
I --> J["chat/FastAPI uploads file to Django callback"] |
||||
|
J --> K["Django creates LiveSessionRecording"] |
||||
|
K --> L["Django signal creates CourseLesson"] |
||||
|
``` |
||||
|
|
||||
|
## Recording Start Flow |
||||
|
|
||||
|
### Frontend |
||||
|
|
||||
|
Professor presses the recording button in: |
||||
|
|
||||
|
- [useCloudRecording.tsx](/F:/WORK/CODE/WORK/imam-javad/Online%20Class/conference_client/src/components/footer/icons/recording/useCloudRecording.tsx) |
||||
|
|
||||
|
That calls: |
||||
|
|
||||
|
- `POST /api/courses/online/room/recording/` |
||||
|
|
||||
|
Payload: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"action": "start", |
||||
|
"room_id": "room-16-1781432247" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Django |
||||
|
|
||||
|
Handled in: |
||||
|
|
||||
|
- `CourseLiveSessionRecordingAPIView.post()` |
||||
|
|
||||
|
What Django does: |
||||
|
|
||||
|
1. Resolves the current live session from the meeting token |
||||
|
2. Checks that the caller is a moderator |
||||
|
3. Marks session status as `starting` |
||||
|
4. Sends a start request to the Web Egress bridge service |
||||
|
5. Stores returned `egress_id` |
||||
|
6. Marks the session as `recording` |
||||
|
|
||||
|
Important session fields used: |
||||
|
|
||||
|
- `web_egress_id` |
||||
|
- `web_egress_status` |
||||
|
- `web_egress_started_at` |
||||
|
- `web_egress_stopped_at` |
||||
|
- `web_egress_error` |
||||
|
- `recording_title` |
||||
|
|
||||
|
### Chat / FastAPI bridge |
||||
|
|
||||
|
Handled in: |
||||
|
|
||||
|
- `POST /api/web-egress/start/` |
||||
|
|
||||
|
What it does: |
||||
|
|
||||
|
1. Registers a job for the given `session_id` and `room_id` |
||||
|
2. Builds the join URL for the recorder bot |
||||
|
3. Calls LiveKit Egress to start a browser-based recording |
||||
|
4. Stores current job state |
||||
|
|
||||
|
### Recorder Bot |
||||
|
|
||||
|
The recorder joins the classroom using a special access token and URL generated by Django: |
||||
|
|
||||
|
- `?access_token=...&web_egress=1&skip_landing=1&room_id=...` |
||||
|
|
||||
|
This is important because: |
||||
|
|
||||
|
- the recorder uses the real classroom UI |
||||
|
- it must bypass landing/prejoin checks |
||||
|
- it must render the same content users see |
||||
|
|
||||
|
## Recording Stop Flow |
||||
|
|
||||
|
### Manual stop |
||||
|
|
||||
|
Professor presses `Rec` again: |
||||
|
|
||||
|
- frontend calls `POST /api/courses/online/room/recording/` with `"action": "stop"` |
||||
|
|
||||
|
Django: |
||||
|
|
||||
|
1. verifies the session |
||||
|
2. sends stop request to Web Egress bridge |
||||
|
3. marks session as `stopping` |
||||
|
|
||||
|
### Automatic stop on session end |
||||
|
|
||||
|
This is critical. |
||||
|
|
||||
|
If the professor ends the meeting while recording is still active, Django must still stop Web Egress before the session is fully closed. |
||||
|
|
||||
|
This is now handled centrally by: |
||||
|
|
||||
|
- `stop_session_web_egress_if_active(...)` |
||||
|
|
||||
|
Used from: |
||||
|
|
||||
|
- [course.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/course.py) |
||||
|
- [webhook.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/webhook.py) |
||||
|
- [admin.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/admin.py) |
||||
|
- [live_session.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/views/live_session.py) |
||||
|
|
||||
|
This ensures the final active recording is not lost when the class is closed. |
||||
|
|
||||
|
## Finalization Flow |
||||
|
|
||||
|
After stop: |
||||
|
|
||||
|
1. LiveKit Egress finishes processing the output file |
||||
|
2. LiveKit sends webhook events to the chat/FastAPI bridge |
||||
|
3. The bridge locates the output file in shared storage |
||||
|
4. The bridge uploads the file to Django callback endpoint |
||||
|
5. Django creates `LiveSessionRecording` |
||||
|
6. Django updates `CourseLiveSession.web_egress_status` |
||||
|
|
||||
|
Important Django callback endpoint: |
||||
|
|
||||
|
- `POST /api/courses/online/room/recording/web-egress/callback/` |
||||
|
|
||||
|
Handled in: |
||||
|
|
||||
|
- `CourseLiveSessionWebEgressCallbackAPIView` |
||||
|
|
||||
|
## Shared Storage |
||||
|
|
||||
|
The recording file is not sent directly from LiveKit to Django first. |
||||
|
|
||||
|
Current pattern: |
||||
|
|
||||
|
1. Egress writes file to shared disk |
||||
|
2. Chat/FastAPI reads the file |
||||
|
3. Chat/FastAPI uploads file to Django |
||||
|
|
||||
|
Example shared path on server: |
||||
|
|
||||
|
- `/najm/HabibMeetSocket/shared/recordings/imamjavad/web-egress/<room-id>/` |
||||
|
|
||||
|
This means: |
||||
|
|
||||
|
- LiveKit Egress and chat/FastAPI must both be able to access the same recording storage |
||||
|
- file permissions and mount paths are important |
||||
|
|
||||
|
## Session and Recording Data Model |
||||
|
|
||||
|
### `CourseLiveSession` |
||||
|
|
||||
|
Represents the live class session. |
||||
|
|
||||
|
Important responsibilities: |
||||
|
|
||||
|
- room identity |
||||
|
- live/ended status |
||||
|
- moderator auto-close timing |
||||
|
- current Web Egress recording state |
||||
|
|
||||
|
### `LiveSessionUser` |
||||
|
|
||||
|
Represents user presence inside a live session. |
||||
|
|
||||
|
Used for: |
||||
|
|
||||
|
- attendance-like tracking |
||||
|
- determining who is online |
||||
|
- moderator-exit auto-close logic |
||||
|
|
||||
|
Important note: |
||||
|
|
||||
|
Even with webhooks, Django also performs fail-safe registration when direct join-token flows are used, so presence data is not lost when some event paths are bypassed. |
||||
|
|
||||
|
### `LiveSessionRecording` |
||||
|
|
||||
|
Represents a final recorded file stored in Django. |
||||
|
|
||||
|
Used for: |
||||
|
|
||||
|
- listing recordings |
||||
|
- generating lessons |
||||
|
- attaching final media files to courses |
||||
|
|
||||
|
## Lesson Creation Flow |
||||
|
|
||||
|
When a `LiveSessionRecording` is created, Django signal logic automatically creates a lesson. |
||||
|
|
||||
|
Handled in: |
||||
|
|
||||
|
- [signals.py](/F:/WORK/CODE/WORK/imam-javad/backend/apps/course/signals.py) |
||||
|
|
||||
|
What it does: |
||||
|
|
||||
|
1. Creates or reuses a chapter for the recording date |
||||
|
2. Creates `Lesson` |
||||
|
3. Creates `CourseLesson` |
||||
|
4. Uses session title or subject as lesson title |
||||
|
5. Adds `part 1`, `part 2`, etc. when multiple recordings belong to the same session |
||||
|
|
||||
|
### Late title update behavior |
||||
|
|
||||
|
If professor enters a custom recording title near session end, old recordings and already-created lessons must also be renamed. |
||||
|
|
||||
|
This is now handled by syncing titles after `recording_title` is updated, so early-created lessons do not keep stale names. |
||||
|
|
||||
|
## Why We Kept PlugNMeet Events |
||||
|
|
||||
|
The recorder changed, but PlugNMeet room lifecycle still matters. |
||||
|
|
||||
|
Things that should still come from PlugNMeet or session logic around it: |
||||
|
|
||||
|
- room end state |
||||
|
- participant join/leave tracking |
||||
|
- presence rows in `LiveSessionUser` |
||||
|
- moderator exit detection |
||||
|
- automatic session closure |
||||
|
|
||||
|
So the architecture is intentionally split: |
||||
|
|
||||
|
- PlugNMeet remains the meeting authority |
||||
|
- Web Egress becomes only the recording engine |
||||
|
- Django links both worlds together |
||||
|
|
||||
|
## Important Endpoints |
||||
|
|
||||
|
### Django |
||||
|
|
||||
|
- `POST /api/courses/online/room/recording/` |
||||
|
- start or stop recording |
||||
|
- `GET /api/courses/online/room/recording/?room_id=...` |
||||
|
- read recording status |
||||
|
- `POST /api/courses/online/room/recording/web-egress/callback/` |
||||
|
- receive final uploaded recording from bridge service |
||||
|
- `POST /api/courses/online/room/set-recording-title/` |
||||
|
- set custom recording title for active session |
||||
|
- `POST /api/courses/plugnmeet/webhook/` |
||||
|
- receive room and participant lifecycle events from PlugNMeet |
||||
|
|
||||
|
### Chat / FastAPI bridge |
||||
|
|
||||
|
- `POST /api/web-egress/start/` |
||||
|
- `POST /api/web-egress/stop/` |
||||
|
- `GET /api/web-egress/status/` |
||||
|
- `POST /api/web-egress/livekit-webhook/` |
||||
|
- `GET /api/web-egress/health/` |
||||
|
|
||||
|
## Environment and Runtime Dependencies |
||||
|
|
||||
|
### Django |
||||
|
|
||||
|
Relevant envs: |
||||
|
|
||||
|
- `ONLINE_CLASS_WEB_EGRESS_ENABLED` |
||||
|
- `ONLINE_CLASS_WEB_EGRESS_SERVICE_URL` |
||||
|
- `ONLINE_CLASS_WEB_EGRESS_SERVICE_TOKEN` |
||||
|
- `ONLINE_CLASS_WEB_EGRESS_CALLBACK_TOKEN` |
||||
|
- `ONLINE_CLASS_WEB_EGRESS_TIMEOUT` |
||||
|
|
||||
|
### Chat / FastAPI bridge |
||||
|
|
||||
|
Must know: |
||||
|
|
||||
|
- Django upload/callback target |
||||
|
- callback token |
||||
|
- shared recordings root |
||||
|
- LiveKit/Egress credentials |
||||
|
|
||||
|
### LiveKit/Egress |
||||
|
|
||||
|
Must be configured with: |
||||
|
|
||||
|
- correct output path |
||||
|
- webhook target to chat/FastAPI |
||||
|
- access to the shared recordings directory |
||||
|
|
||||
|
## Current Operational Notes |
||||
|
|
||||
|
- The recorder is a hidden meeting participant named `Session Recorder` |
||||
|
- The recorder uses the real meeting frontend, not a separate rendering app |
||||
|
- The frontend has recorder-specific behavior to skip entry/landing flow |
||||
|
- Presence and auto-close logic must continue to work independently of recording |
||||
|
- Recording stop should be triggered both manually and during session close |
||||
|
|
||||
|
## Summary |
||||
|
|
||||
|
The current architecture is a three-layer recording pipeline: |
||||
|
|
||||
|
1. PlugNMeet hosts the class |
||||
|
2. Django decides when recording starts/stops and stores final course data |
||||
|
3. Chat/FastAPI bridges Django to LiveKit Egress |
||||
|
|
||||
|
LiveKit Egress records the rendered classroom page, but PlugNMeet remains the source of meeting truth. |
||||
|
|
||||
|
That split is the key design decision: |
||||
|
|
||||
|
- do not move classroom lifecycle into Egress |
||||
|
- only move the recording responsibility into Egress |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue