9 changed files with 780 additions and 2 deletions
-
6.env.prod
-
78apps/course/migrations/0014_courselivesession_web_egress_fields.py
-
51apps/course/models/live_session.py
-
7apps/course/services/__init__.py
-
109apps/course/services/web_egress.py
-
2apps/course/urls.py
-
499apps/course/views/live_session.py
-
13apps/course/views/webhook.py
-
17config/settings/base.py
@ -0,0 +1,78 @@ |
|||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
("course", "0013_courselivesession_auto_close_fields"), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name="courselivesession", |
||||
|
name="web_egress_error", |
||||
|
field=models.TextField( |
||||
|
blank=True, |
||||
|
help_text="Last WebEgress error message, if any.", |
||||
|
null=True, |
||||
|
verbose_name="Web Egress Error", |
||||
|
), |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name="courselivesession", |
||||
|
name="web_egress_id", |
||||
|
field=models.CharField( |
||||
|
blank=True, |
||||
|
help_text="Active LiveKit WebEgress identifier for this session.", |
||||
|
max_length=255, |
||||
|
null=True, |
||||
|
verbose_name="Web Egress ID", |
||||
|
), |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name="courselivesession", |
||||
|
name="web_egress_started_at", |
||||
|
field=models.DateTimeField( |
||||
|
blank=True, |
||||
|
help_text="Timestamp when WebEgress recording started.", |
||||
|
null=True, |
||||
|
verbose_name="Web Egress Started At", |
||||
|
), |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name="courselivesession", |
||||
|
name="web_egress_status", |
||||
|
field=models.CharField( |
||||
|
choices=[ |
||||
|
("idle", "Idle"), |
||||
|
("starting", "Starting"), |
||||
|
("recording", "Recording"), |
||||
|
("stopping", "Stopping"), |
||||
|
("processing", "Processing"), |
||||
|
("completed", "Completed"), |
||||
|
("failed", "Failed"), |
||||
|
], |
||||
|
default="idle", |
||||
|
help_text="Current WebEgress recording status for this session.", |
||||
|
max_length=20, |
||||
|
verbose_name="Web Egress Status", |
||||
|
), |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name="courselivesession", |
||||
|
name="web_egress_stopped_at", |
||||
|
field=models.DateTimeField( |
||||
|
blank=True, |
||||
|
help_text="Timestamp when WebEgress recording stopped.", |
||||
|
null=True, |
||||
|
verbose_name="Web Egress Stopped At", |
||||
|
), |
||||
|
), |
||||
|
migrations.AddIndex( |
||||
|
model_name="courselivesession", |
||||
|
index=models.Index( |
||||
|
fields=["web_egress_status"], |
||||
|
name="course_cour_web_egr_5d55cb_idx", |
||||
|
), |
||||
|
), |
||||
|
] |
||||
@ -1,3 +1,8 @@ |
|||||
from .plugnmeet import PlugNMeetClient, PlugNMeetError |
from .plugnmeet import PlugNMeetClient, PlugNMeetError |
||||
|
|
||||
__all__ = ['PlugNMeetClient', 'PlugNMeetError'] |
|
||||
|
__all__ = [ |
||||
|
'PlugNMeetClient', |
||||
|
'PlugNMeetError', |
||||
|
'WebEgressClient', |
||||
|
'WebEgressError', |
||||
|
] |
||||
@ -0,0 +1,109 @@ |
|||||
|
from __future__ import annotations |
||||
|
|
||||
|
from typing import Any, Dict, Optional |
||||
|
|
||||
|
import requests |
||||
|
from django.conf import settings |
||||
|
from django.core.exceptions import ImproperlyConfigured |
||||
|
|
||||
|
|
||||
|
class WebEgressError(Exception): |
||||
|
def __init__( |
||||
|
self, |
||||
|
message: str, |
||||
|
*, |
||||
|
status_code: Optional[int] = None, |
||||
|
response_data: Optional[Dict[str, Any]] = None, |
||||
|
): |
||||
|
super().__init__(message) |
||||
|
self.status_code = status_code |
||||
|
self.response_data = response_data or {} |
||||
|
|
||||
|
|
||||
|
class WebEgressClient: |
||||
|
def __init__( |
||||
|
self, |
||||
|
*, |
||||
|
base_url: Optional[str] = None, |
||||
|
service_token: Optional[str] = None, |
||||
|
timeout: Optional[float] = None, |
||||
|
): |
||||
|
self.base_url = ( |
||||
|
base_url |
||||
|
or getattr(settings, "ONLINE_CLASS_WEB_EGRESS_SERVICE_URL", "") |
||||
|
).rstrip("/") |
||||
|
self.service_token = ( |
||||
|
service_token |
||||
|
or getattr(settings, "ONLINE_CLASS_WEB_EGRESS_SERVICE_TOKEN", "") |
||||
|
) |
||||
|
self.timeout = timeout or getattr( |
||||
|
settings, "ONLINE_CLASS_WEB_EGRESS_TIMEOUT", 20.0 |
||||
|
) |
||||
|
|
||||
|
if not self.base_url: |
||||
|
raise ImproperlyConfigured( |
||||
|
"ONLINE_CLASS_WEB_EGRESS_SERVICE_URL must be configured." |
||||
|
) |
||||
|
|
||||
|
def start_recording(self, payload: Dict[str, Any]) -> Dict[str, Any]: |
||||
|
return self._request("POST", "/api/web-egress/start/", json=payload) |
||||
|
|
||||
|
def stop_recording(self, payload: Dict[str, Any]) -> Dict[str, Any]: |
||||
|
return self._request("POST", "/api/web-egress/stop/", json=payload) |
||||
|
|
||||
|
def get_status(self, *, room_id: str, session_id: int) -> Dict[str, Any]: |
||||
|
return self._request( |
||||
|
"GET", |
||||
|
"/api/web-egress/status/", |
||||
|
params={"room_id": room_id, "session_id": session_id}, |
||||
|
) |
||||
|
|
||||
|
def _request(self, method: str, path: str, **kwargs) -> Dict[str, Any]: |
||||
|
headers = kwargs.pop("headers", {}) |
||||
|
headers["Content-Type"] = "application/json" |
||||
|
if self.service_token: |
||||
|
headers["Authorization"] = f"Bearer {self.service_token}" |
||||
|
|
||||
|
try: |
||||
|
response = requests.request( |
||||
|
method, |
||||
|
f"{self.base_url}{path}", |
||||
|
headers=headers, |
||||
|
timeout=self.timeout, |
||||
|
**kwargs, |
||||
|
) |
||||
|
except requests.RequestException as exc: |
||||
|
raise WebEgressError("Failed to reach WebEgress service.") from exc |
||||
|
|
||||
|
data = self._safe_json(response) |
||||
|
if response.status_code >= 400: |
||||
|
message = ( |
||||
|
(data or {}).get("message") |
||||
|
or (data or {}).get("detail") |
||||
|
or response.text |
||||
|
or "WebEgress service request failed." |
||||
|
) |
||||
|
raise WebEgressError( |
||||
|
message, |
||||
|
status_code=response.status_code, |
||||
|
response_data=data, |
||||
|
) |
||||
|
|
||||
|
if data is None: |
||||
|
raise WebEgressError("WebEgress service returned an invalid response.") |
||||
|
|
||||
|
if isinstance(data, dict) and data.get("status") is False: |
||||
|
raise WebEgressError( |
||||
|
data.get("message") or data.get("detail") or "WebEgress failed.", |
||||
|
status_code=response.status_code, |
||||
|
response_data=data, |
||||
|
) |
||||
|
|
||||
|
return data |
||||
|
|
||||
|
@staticmethod |
||||
|
def _safe_json(response: requests.Response) -> Optional[Dict[str, Any]]: |
||||
|
try: |
||||
|
return response.json() |
||||
|
except ValueError: |
||||
|
return None |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue