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