8.9 KiB
PlugNMeet Webhook Integration
Overview
This document describes the webhook integration between PlugNMeet and the Django backend to handle live session events.
Webhook Endpoint
URL: https://habibmeet.nwhco.ir/api/course/plugnmeet/webhook/
Method: POST
Content-Type: application/webhook+json
Authentication
The webhook endpoint is secured using SHA256 HMAC signature verification.
Headers
Hash-Token: SHA256 HMAC signature of the request body usingPLUGNMEET_API_SECRETContent-Type:application/webhook+jsonAuthorization: JWT token (optional, for additional verification)
Signature Verification
import hmac
import hashlib
# Calculate signature
signature = hmac.new(
PLUGNMEET_API_SECRET.encode('utf-8'),
request_body,
hashlib.sha256
).hexdigest()
# Compare with Hash-Token header
is_valid = hmac.compare_digest(hash_token_header, signature)
Supported Events
1. room_finished
Triggered when a live session room is closed.
Action: Closes the CourseLiveSession and marks all active LiveSessionUser entries as offline.
Payload:
{
"event": "room_finished",
"id": "550e8400-e29b-41d4-a716-446655440000",
"createdAt": 1697500800,
"room": {
"sid": "room-123456",
"identity": "algebra-1402",
"name": "کلاس جبر",
"maxParticipants": 100,
"creationTime": 1697497200,
"metadata": "{}",
"numParticipants": 0,
"duration": 3600
}
}
Response:
{
"status": "ok",
"message": "Room finished",
"session_id": 123,
"users_disconnected": 5
}
2. participant_joined
Triggered when a user joins the live session.
Action: Creates a new LiveSessionUser entry or reactivates an existing offline entry.
Payload:
{
"event": "participant_joined",
"id": "660e8400-e29b-41d4-a716-446655440001",
"createdAt": 1697497300,
"room": {
"sid": "room-123456",
"identity": "algebra-1402",
"name": "کلاس جبر"
},
"participant": {
"sid": "participant-user-27",
"identity": "27",
"state": "ACTIVE",
"name": "دانشجو نمونه",
"metadata": "{\"is_admin\": false}",
"permission": {
"canPublish": true,
"canPublishData": true,
"canSubscribe": true
},
"tracks": [],
"joinedAt": 1697497300
}
}
Response:
{
"status": "ok",
"message": "Participant joined",
"session_user_id": 456,
"created": true
}
3. participant_left
Triggered when a user leaves the live session.
Action: Marks the user's LiveSessionUser entry as offline and sets exited_at timestamp.
Payload:
{
"event": "participant_left",
"id": "770e8400-e29b-41d4-a716-446655440002",
"createdAt": 1697499000,
"room": {
"sid": "room-123456",
"identity": "algebra-1402",
"name": "کلاس جبر"
},
"participant": {
"sid": "participant-user-27",
"identity": "27",
"state": "DISCONNECTED",
"name": "دانشجو نمونه",
"metadata": "{\"is_admin\": false}",
"permission": {
"canPublish": true,
"canPublishData": true,
"canSubscribe": true
},
"tracks": [],
"joinedAt": 1697497300,
"duration": 1800
}
}
Response:
{
"status": "ok",
"message": "Participant left",
"updated": true
}
4. end_recording
Triggered when a recording finishes.
Action:
- Fetches recording info from PlugNMeet API
- Gets download token
- Downloads the recording file
- Saves to
LiveSessionRecordingmodel - Generates thumbnail for video recordings (requires
ffmpeg)
Payload:
{
"event": "end_recording",
"id": "880e8400-e29b-41d4-a716-446655440003",
"createdAt": 1697500800,
"room": {
"sid": "room-123456",
"identity": "algebra-1402",
"name": "کلاس جبر"
},
"recording_info": {
"recordingId": "rec-123456",
"roomId": "algebra-1402",
"recordingType": "COMPOSITE",
"fileName": "algebra-1402-20231016.mp4",
"duration": 3600,
"status": "FINISHED"
}
}
Response (Success):
{
"status": "ok",
"message": "Recording downloaded and saved successfully",
"recording_id": 123,
"file_name": "algebra-1402-20231016.mp4",
"file_size": 524288000,
"thumbnail_generated": true
}
Response (Error):
{
"status": "error",
"message": "Failed to get recording info",
"error": "Recording not found"
}
Requirements:
ffmpegmust be installed for thumbnail generation- Sufficient disk space for recording files
- Write permissions on media directories
Configuration
Add these settings to your Django settings file:
# PlugNMeet Integration
PLUGNMEET_SERVER_URL = "https://plugnmeet.example.com"
PLUGNMEET_API_KEY = "your-api-key"
PLUGNMEET_API_SECRET = "your-api-secret"
PLUGNMEET_TIMEOUT = 10.0
PlugNMeet Configuration
Configure the webhook URL in your PlugNMeet server settings:
webhooks:
- url: "https://habibmeet.nwhco.ir/api/course/plugnmeet/webhook/"
events:
- room_finished
- participant_joined
- participant_left
- end_recording
Error Handling
The webhook endpoint returns appropriate HTTP status codes:
200 OK: Event processed successfully400 Bad Request: Invalid payload or missing required fields403 Forbidden: Invalid webhook signature500 Internal Server Error: Server error during processing
All errors are logged with detailed information for debugging.
Testing
To test the webhook locally, you can use curl:
# Calculate signature
SECRET="your-api-secret"
PAYLOAD='{"event":"room_finished","room":{"identity":"test-room"}}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
# Send webhook request
curl -X POST https://habibmeet.nwhco.ir/api/course/plugnmeet/webhook/ \
-H "Content-Type: application/webhook+json" \
-H "Hash-Token: $SIGNATURE" \
-d "$PAYLOAD"
Migration from Polling
Previously, the system used a polling approach where it would check if a room was still active using the is_room_active API call. This has been deprecated in favor of the webhook approach:
Old (Deprecated):
# This code has been commented out
def _sync_room_status_with_plugnmeet(self, course: Course):
client = PlugNMeetClient()
response = client.is_room_active(active_session.room_id)
if not response.get('isActive', False):
self._close_live_session(active_session)
New (Webhook-based):
# Room status is automatically updated via webhooks
# No polling required
Benefits
- Real-time updates: No polling delay, events are processed immediately
- Reduced server load: No need for periodic API calls to check room status
- Accurate tracking: Precise participant join/leave timestamps
- Scalability: Webhook approach scales better than polling
- Lower latency: Users see status updates immediately
- Automatic recording management: Recordings are automatically downloaded and saved when ready
Recording Management
The webhook automatically handles recording downloads when the end_recording event is received:
Process Flow
- Webhook receives end_recording event
- Fetch recording info from PlugNMeet API (
/auth/recording/recordingInfo) - Get download token (
/auth/recording/getDownloadToken) - Download file to temporary location
- Determine recording type (video/voice) based on file extension
- Create database record (
LiveSessionRecording) - Generate thumbnail (for video files using ffmpeg)
- Clean up temporary files
File Storage
- Location: Configured by
MEDIA_ROOTin Django settings - Upload path:
recorded_sessions/(from model definition) - Thumbnails:
recording_thumbnails/(for video recordings)
Thumbnail Generation
For video recordings, a thumbnail is automatically generated using ffmpeg:
- Extracts frame at 1 second
- Scaled to width 640px (maintains aspect ratio)
- High quality JPEG (quality level 2)
- Saved to
recording.thumbnailfield
Note: ffmpeg must be installed on the server for thumbnail generation to work.
Error Handling
The recording download process includes comprehensive error handling:
- Missing recording: Returns appropriate error message
- Download failures: Logs error and returns failure status
- Thumbnail generation: Non-critical, failures are logged but don't stop the process
- Cleanup: Temporary files are always cleaned up, even on errors
Logging
All webhook events are logged with detailed information:
[PlugNMeet Webhook] Received webhook request
[PlugNMeet Webhook] Processing event=room_finished
[PlugNMeet Webhook] Session closed - session_id=123 room_id=algebra-1402
[PlugNMeet Webhook] User sessions closed - session_id=123 count=5
[PlugNMeet Webhook] Event processed successfully - event=room_finished
Check Django logs for webhook activity and debugging information.