2025-03-06 01:59:23 +01:00
|
|
|
"""LiveKit Events Service"""
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
from enum import Enum
|
✨(backend) add telephony service for automatic SIP dispatch rules
Implemented a service that automatically creates a SIP dispatch rule when
the first WebRTC participant joins a room and removes it when the room
becomes empty.
Why? I don’t want a SIP participant to join an empty room.
The PIN code could be easily leaked, and there is currently no lobby
mechanism available for SIP participants.
A WebRTC participant is still required to create a room.
This behavior is inspired by a proprietary tool. The service uses LiveKit’s
webhook notification system to react to room lifecycle events. This is
a naive implementation that currently supports only a single SIP trunk and
will require refactoring to support multiple trunks. When no trunk is
specified, rules are created by default on a fallback trunk.
@rouja wrote a minimal Helm chart for LiveKit SIP with Asterisk, which
couldn’t be versioned yet due to embedded credentials. I deployed it
locally and successfully tested the integration with a remote
OVH SIP trunk.
One point to note: LiveKit lacks advanced filtering capabilities when
listing dispatch rules. Their recommendation is to fetch all rules and
filter them within your backend logic. I’ve opened a feature request asking
for at least the ability to filter dispatch rules by room, since filtering
by trunk is already supported, room-based filtering feels like a natural
addition.
Until there's an update, I prefer to keep the implementation simple.
It works well at our current scale, and can be refactored when higher load
or multi-trunk support becomes necessary.
While caching dispatch rule IDs could be a performance optimization,
I feel it would be premature and potentially error-prone due to the complexity
of invalidation. If performance becomes an issue, I’ll consider introducing
caching at that point. To handle the edge case where multiple dispatch rules
with different PIN codes are present, the service performs an extensive
cleanup during room creation to ensure SIP routing remains clean and
predictable. This edge case should not happen.
In the 'delete_dispatch_rule' if deleting one rule fails, method would exit
without deleting the other rules. It's okay IMO for a first iteration.
If multiple dispatch rules are often found for room, I would enhance this part.
2025-07-01 17:59:45 +02:00
|
|
|
from logging import getLogger
|
2025-03-06 01:59:23 +01:00
|
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
|
|
|
|
|
|
from livekit import api
|
|
|
|
|
|
✨(backend) add telephony service for automatic SIP dispatch rules
Implemented a service that automatically creates a SIP dispatch rule when
the first WebRTC participant joins a room and removes it when the room
becomes empty.
Why? I don’t want a SIP participant to join an empty room.
The PIN code could be easily leaked, and there is currently no lobby
mechanism available for SIP participants.
A WebRTC participant is still required to create a room.
This behavior is inspired by a proprietary tool. The service uses LiveKit’s
webhook notification system to react to room lifecycle events. This is
a naive implementation that currently supports only a single SIP trunk and
will require refactoring to support multiple trunks. When no trunk is
specified, rules are created by default on a fallback trunk.
@rouja wrote a minimal Helm chart for LiveKit SIP with Asterisk, which
couldn’t be versioned yet due to embedded credentials. I deployed it
locally and successfully tested the integration with a remote
OVH SIP trunk.
One point to note: LiveKit lacks advanced filtering capabilities when
listing dispatch rules. Their recommendation is to fetch all rules and
filter them within your backend logic. I’ve opened a feature request asking
for at least the ability to filter dispatch rules by room, since filtering
by trunk is already supported, room-based filtering feels like a natural
addition.
Until there's an update, I prefer to keep the implementation simple.
It works well at our current scale, and can be refactored when higher load
or multi-trunk support becomes necessary.
While caching dispatch rule IDs could be a performance optimization,
I feel it would be premature and potentially error-prone due to the complexity
of invalidation. If performance becomes an issue, I’ll consider introducing
caching at that point. To handle the edge case where multiple dispatch rules
with different PIN codes are present, the service performs an extensive
cleanup during room creation to ensure SIP routing remains clean and
predictable. This edge case should not happen.
In the 'delete_dispatch_rule' if deleting one rule fails, method would exit
without deleting the other rules. It's okay IMO for a first iteration.
If multiple dispatch rules are often found for room, I would enhance this part.
2025-07-01 17:59:45 +02:00
|
|
|
from core import models
|
|
|
|
|
|
2025-03-07 18:25:09 +01:00
|
|
|
from .lobby import LobbyService
|
✨(backend) add telephony service for automatic SIP dispatch rules
Implemented a service that automatically creates a SIP dispatch rule when
the first WebRTC participant joins a room and removes it when the room
becomes empty.
Why? I don’t want a SIP participant to join an empty room.
The PIN code could be easily leaked, and there is currently no lobby
mechanism available for SIP participants.
A WebRTC participant is still required to create a room.
This behavior is inspired by a proprietary tool. The service uses LiveKit’s
webhook notification system to react to room lifecycle events. This is
a naive implementation that currently supports only a single SIP trunk and
will require refactoring to support multiple trunks. When no trunk is
specified, rules are created by default on a fallback trunk.
@rouja wrote a minimal Helm chart for LiveKit SIP with Asterisk, which
couldn’t be versioned yet due to embedded credentials. I deployed it
locally and successfully tested the integration with a remote
OVH SIP trunk.
One point to note: LiveKit lacks advanced filtering capabilities when
listing dispatch rules. Their recommendation is to fetch all rules and
filter them within your backend logic. I’ve opened a feature request asking
for at least the ability to filter dispatch rules by room, since filtering
by trunk is already supported, room-based filtering feels like a natural
addition.
Until there's an update, I prefer to keep the implementation simple.
It works well at our current scale, and can be refactored when higher load
or multi-trunk support becomes necessary.
While caching dispatch rule IDs could be a performance optimization,
I feel it would be premature and potentially error-prone due to the complexity
of invalidation. If performance becomes an issue, I’ll consider introducing
caching at that point. To handle the edge case where multiple dispatch rules
with different PIN codes are present, the service performs an extensive
cleanup during room creation to ensure SIP routing remains clean and
predictable. This edge case should not happen.
In the 'delete_dispatch_rule' if deleting one rule fails, method would exit
without deleting the other rules. It's okay IMO for a first iteration.
If multiple dispatch rules are often found for room, I would enhance this part.
2025-07-01 17:59:45 +02:00
|
|
|
from .telephony import TelephonyException, TelephonyService
|
|
|
|
|
|
|
|
|
|
logger = getLogger(__name__)
|
2025-03-06 01:59:23 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class LiveKitWebhookError(Exception):
|
|
|
|
|
"""Base exception for LiveKit webhook processing errors."""
|
|
|
|
|
|
|
|
|
|
status_code = 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AuthenticationError(LiveKitWebhookError):
|
|
|
|
|
"""Authentication failed."""
|
|
|
|
|
|
|
|
|
|
status_code = 401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidPayloadError(LiveKitWebhookError):
|
|
|
|
|
"""Invalid webhook payload."""
|
|
|
|
|
|
|
|
|
|
status_code = 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class UnsupportedEventTypeError(LiveKitWebhookError):
|
|
|
|
|
"""Unsupported event type."""
|
|
|
|
|
|
|
|
|
|
status_code = 422
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ActionFailedError(LiveKitWebhookError):
|
|
|
|
|
"""Webhook action fails to process or complete."""
|
|
|
|
|
|
|
|
|
|
status_code = 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LiveKitWebhookEventType(Enum):
|
|
|
|
|
"""LiveKit webhook event types."""
|
|
|
|
|
|
|
|
|
|
# Room events
|
|
|
|
|
ROOM_STARTED = "room_started"
|
|
|
|
|
ROOM_FINISHED = "room_finished"
|
|
|
|
|
|
|
|
|
|
# Participant events
|
|
|
|
|
PARTICIPANT_JOINED = "participant_joined"
|
|
|
|
|
PARTICIPANT_LEFT = "participant_left"
|
|
|
|
|
|
|
|
|
|
# Track events
|
|
|
|
|
TRACK_PUBLISHED = "track_published"
|
|
|
|
|
TRACK_UNPUBLISHED = "track_unpublished"
|
|
|
|
|
|
|
|
|
|
# Egress events
|
|
|
|
|
EGRESS_STARTED = "egress_started"
|
|
|
|
|
EGRESS_UPDATED = "egress_updated"
|
|
|
|
|
EGRESS_ENDED = "egress_ended"
|
|
|
|
|
|
|
|
|
|
# Ingress events
|
|
|
|
|
INGRESS_STARTED = "ingress_started"
|
|
|
|
|
INGRESS_ENDED = "ingress_ended"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LiveKitEventsService:
|
|
|
|
|
"""Service for processing and handling LiveKit webhook events and notifications."""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
"""Initialize with required services."""
|
|
|
|
|
|
|
|
|
|
token_verifier = api.TokenVerifier(
|
|
|
|
|
settings.LIVEKIT_CONFIGURATION["api_key"],
|
|
|
|
|
settings.LIVEKIT_CONFIGURATION["api_secret"],
|
|
|
|
|
)
|
|
|
|
|
self.webhook_receiver = api.WebhookReceiver(token_verifier)
|
|
|
|
|
self.lobby_service = LobbyService()
|
✨(backend) add telephony service for automatic SIP dispatch rules
Implemented a service that automatically creates a SIP dispatch rule when
the first WebRTC participant joins a room and removes it when the room
becomes empty.
Why? I don’t want a SIP participant to join an empty room.
The PIN code could be easily leaked, and there is currently no lobby
mechanism available for SIP participants.
A WebRTC participant is still required to create a room.
This behavior is inspired by a proprietary tool. The service uses LiveKit’s
webhook notification system to react to room lifecycle events. This is
a naive implementation that currently supports only a single SIP trunk and
will require refactoring to support multiple trunks. When no trunk is
specified, rules are created by default on a fallback trunk.
@rouja wrote a minimal Helm chart for LiveKit SIP with Asterisk, which
couldn’t be versioned yet due to embedded credentials. I deployed it
locally and successfully tested the integration with a remote
OVH SIP trunk.
One point to note: LiveKit lacks advanced filtering capabilities when
listing dispatch rules. Their recommendation is to fetch all rules and
filter them within your backend logic. I’ve opened a feature request asking
for at least the ability to filter dispatch rules by room, since filtering
by trunk is already supported, room-based filtering feels like a natural
addition.
Until there's an update, I prefer to keep the implementation simple.
It works well at our current scale, and can be refactored when higher load
or multi-trunk support becomes necessary.
While caching dispatch rule IDs could be a performance optimization,
I feel it would be premature and potentially error-prone due to the complexity
of invalidation. If performance becomes an issue, I’ll consider introducing
caching at that point. To handle the edge case where multiple dispatch rules
with different PIN codes are present, the service performs an extensive
cleanup during room creation to ensure SIP routing remains clean and
predictable. This edge case should not happen.
In the 'delete_dispatch_rule' if deleting one rule fails, method would exit
without deleting the other rules. It's okay IMO for a first iteration.
If multiple dispatch rules are often found for room, I would enhance this part.
2025-07-01 17:59:45 +02:00
|
|
|
self.telephony_service = TelephonyService()
|
2025-03-06 01:59:23 +01:00
|
|
|
|
|
|
|
|
def receive(self, request):
|
|
|
|
|
"""Process webhook and route to appropriate handler."""
|
|
|
|
|
|
|
|
|
|
auth_token = request.headers.get("Authorization")
|
|
|
|
|
if not auth_token:
|
|
|
|
|
raise AuthenticationError("Authorization header missing")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
data = self.webhook_receiver.receive(
|
|
|
|
|
request.body.decode("utf-8"), auth_token
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
raise InvalidPayloadError("Invalid webhook payload") from e
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
webhook_type = LiveKitWebhookEventType(data.event)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise UnsupportedEventTypeError(
|
|
|
|
|
f"Unknown webhook type: {data.event}"
|
|
|
|
|
) from e
|
|
|
|
|
|
|
|
|
|
handler_name = f"_handle_{webhook_type.value}"
|
|
|
|
|
handler = getattr(self, handler_name, None)
|
|
|
|
|
|
|
|
|
|
if not handler or not callable(handler):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# pylint: disable=not-callable
|
|
|
|
|
handler(data)
|
|
|
|
|
|
✨(backend) add telephony service for automatic SIP dispatch rules
Implemented a service that automatically creates a SIP dispatch rule when
the first WebRTC participant joins a room and removes it when the room
becomes empty.
Why? I don’t want a SIP participant to join an empty room.
The PIN code could be easily leaked, and there is currently no lobby
mechanism available for SIP participants.
A WebRTC participant is still required to create a room.
This behavior is inspired by a proprietary tool. The service uses LiveKit’s
webhook notification system to react to room lifecycle events. This is
a naive implementation that currently supports only a single SIP trunk and
will require refactoring to support multiple trunks. When no trunk is
specified, rules are created by default on a fallback trunk.
@rouja wrote a minimal Helm chart for LiveKit SIP with Asterisk, which
couldn’t be versioned yet due to embedded credentials. I deployed it
locally and successfully tested the integration with a remote
OVH SIP trunk.
One point to note: LiveKit lacks advanced filtering capabilities when
listing dispatch rules. Their recommendation is to fetch all rules and
filter them within your backend logic. I’ve opened a feature request asking
for at least the ability to filter dispatch rules by room, since filtering
by trunk is already supported, room-based filtering feels like a natural
addition.
Until there's an update, I prefer to keep the implementation simple.
It works well at our current scale, and can be refactored when higher load
or multi-trunk support becomes necessary.
While caching dispatch rule IDs could be a performance optimization,
I feel it would be premature and potentially error-prone due to the complexity
of invalidation. If performance becomes an issue, I’ll consider introducing
caching at that point. To handle the edge case where multiple dispatch rules
with different PIN codes are present, the service performs an extensive
cleanup during room creation to ensure SIP routing remains clean and
predictable. This edge case should not happen.
In the 'delete_dispatch_rule' if deleting one rule fails, method would exit
without deleting the other rules. It's okay IMO for a first iteration.
If multiple dispatch rules are often found for room, I would enhance this part.
2025-07-01 17:59:45 +02:00
|
|
|
def _handle_room_started(self, data):
|
|
|
|
|
"""Handle 'room_started' event."""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
room_id = uuid.UUID(data.room.name)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
logger.warning(
|
|
|
|
|
"Ignoring room event: room name '%s' is not a valid UUID format.",
|
|
|
|
|
data.room.name,
|
|
|
|
|
)
|
|
|
|
|
raise ActionFailedError("Failed to process room started event") from e
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
room = models.Room.objects.get(id=room_id)
|
|
|
|
|
except models.Room.DoesNotExist as err:
|
|
|
|
|
raise ActionFailedError(f"Room with ID {room_id} does not exist") from err
|
|
|
|
|
|
|
|
|
|
if settings.ROOM_TELEPHONY_ENABLED:
|
|
|
|
|
try:
|
|
|
|
|
self.telephony_service.create_dispatch_rule(room)
|
|
|
|
|
except TelephonyException as e:
|
|
|
|
|
raise ActionFailedError(
|
|
|
|
|
f"Failed to create telephony dispatch rule for room {room_id}"
|
|
|
|
|
) from e
|
|
|
|
|
|
2025-03-06 01:59:23 +01:00
|
|
|
def _handle_room_finished(self, data):
|
|
|
|
|
"""Handle 'room_finished' event."""
|
✨(backend) add telephony service for automatic SIP dispatch rules
Implemented a service that automatically creates a SIP dispatch rule when
the first WebRTC participant joins a room and removes it when the room
becomes empty.
Why? I don’t want a SIP participant to join an empty room.
The PIN code could be easily leaked, and there is currently no lobby
mechanism available for SIP participants.
A WebRTC participant is still required to create a room.
This behavior is inspired by a proprietary tool. The service uses LiveKit’s
webhook notification system to react to room lifecycle events. This is
a naive implementation that currently supports only a single SIP trunk and
will require refactoring to support multiple trunks. When no trunk is
specified, rules are created by default on a fallback trunk.
@rouja wrote a minimal Helm chart for LiveKit SIP with Asterisk, which
couldn’t be versioned yet due to embedded credentials. I deployed it
locally and successfully tested the integration with a remote
OVH SIP trunk.
One point to note: LiveKit lacks advanced filtering capabilities when
listing dispatch rules. Their recommendation is to fetch all rules and
filter them within your backend logic. I’ve opened a feature request asking
for at least the ability to filter dispatch rules by room, since filtering
by trunk is already supported, room-based filtering feels like a natural
addition.
Until there's an update, I prefer to keep the implementation simple.
It works well at our current scale, and can be refactored when higher load
or multi-trunk support becomes necessary.
While caching dispatch rule IDs could be a performance optimization,
I feel it would be premature and potentially error-prone due to the complexity
of invalidation. If performance becomes an issue, I’ll consider introducing
caching at that point. To handle the edge case where multiple dispatch rules
with different PIN codes are present, the service performs an extensive
cleanup during room creation to ensure SIP routing remains clean and
predictable. This edge case should not happen.
In the 'delete_dispatch_rule' if deleting one rule fails, method would exit
without deleting the other rules. It's okay IMO for a first iteration.
If multiple dispatch rules are often found for room, I would enhance this part.
2025-07-01 17:59:45 +02:00
|
|
|
|
2025-03-06 01:59:23 +01:00
|
|
|
try:
|
|
|
|
|
room_id = uuid.UUID(data.room.name)
|
✨(backend) add telephony service for automatic SIP dispatch rules
Implemented a service that automatically creates a SIP dispatch rule when
the first WebRTC participant joins a room and removes it when the room
becomes empty.
Why? I don’t want a SIP participant to join an empty room.
The PIN code could be easily leaked, and there is currently no lobby
mechanism available for SIP participants.
A WebRTC participant is still required to create a room.
This behavior is inspired by a proprietary tool. The service uses LiveKit’s
webhook notification system to react to room lifecycle events. This is
a naive implementation that currently supports only a single SIP trunk and
will require refactoring to support multiple trunks. When no trunk is
specified, rules are created by default on a fallback trunk.
@rouja wrote a minimal Helm chart for LiveKit SIP with Asterisk, which
couldn’t be versioned yet due to embedded credentials. I deployed it
locally and successfully tested the integration with a remote
OVH SIP trunk.
One point to note: LiveKit lacks advanced filtering capabilities when
listing dispatch rules. Their recommendation is to fetch all rules and
filter them within your backend logic. I’ve opened a feature request asking
for at least the ability to filter dispatch rules by room, since filtering
by trunk is already supported, room-based filtering feels like a natural
addition.
Until there's an update, I prefer to keep the implementation simple.
It works well at our current scale, and can be refactored when higher load
or multi-trunk support becomes necessary.
While caching dispatch rule IDs could be a performance optimization,
I feel it would be premature and potentially error-prone due to the complexity
of invalidation. If performance becomes an issue, I’ll consider introducing
caching at that point. To handle the edge case where multiple dispatch rules
with different PIN codes are present, the service performs an extensive
cleanup during room creation to ensure SIP routing remains clean and
predictable. This edge case should not happen.
In the 'delete_dispatch_rule' if deleting one rule fails, method would exit
without deleting the other rules. It's okay IMO for a first iteration.
If multiple dispatch rules are often found for room, I would enhance this part.
2025-07-01 17:59:45 +02:00
|
|
|
except ValueError as e:
|
|
|
|
|
logger.warning(
|
|
|
|
|
"Ignoring room event: room name '%s' is not a valid UUID format.",
|
|
|
|
|
data.room.name,
|
|
|
|
|
)
|
|
|
|
|
raise ActionFailedError("Failed to process room finished event") from e
|
|
|
|
|
|
|
|
|
|
if settings.ROOM_TELEPHONY_ENABLED:
|
|
|
|
|
try:
|
|
|
|
|
self.telephony_service.delete_dispatch_rule(room_id)
|
|
|
|
|
except TelephonyException as e:
|
|
|
|
|
raise ActionFailedError(
|
|
|
|
|
f"Failed to delete telephony dispatch rule for room {room_id}"
|
|
|
|
|
) from e
|
|
|
|
|
|
|
|
|
|
try:
|
2025-03-06 01:59:23 +01:00
|
|
|
self.lobby_service.clear_room_cache(room_id)
|
|
|
|
|
except Exception as e:
|
✨(backend) add telephony service for automatic SIP dispatch rules
Implemented a service that automatically creates a SIP dispatch rule when
the first WebRTC participant joins a room and removes it when the room
becomes empty.
Why? I don’t want a SIP participant to join an empty room.
The PIN code could be easily leaked, and there is currently no lobby
mechanism available for SIP participants.
A WebRTC participant is still required to create a room.
This behavior is inspired by a proprietary tool. The service uses LiveKit’s
webhook notification system to react to room lifecycle events. This is
a naive implementation that currently supports only a single SIP trunk and
will require refactoring to support multiple trunks. When no trunk is
specified, rules are created by default on a fallback trunk.
@rouja wrote a minimal Helm chart for LiveKit SIP with Asterisk, which
couldn’t be versioned yet due to embedded credentials. I deployed it
locally and successfully tested the integration with a remote
OVH SIP trunk.
One point to note: LiveKit lacks advanced filtering capabilities when
listing dispatch rules. Their recommendation is to fetch all rules and
filter them within your backend logic. I’ve opened a feature request asking
for at least the ability to filter dispatch rules by room, since filtering
by trunk is already supported, room-based filtering feels like a natural
addition.
Until there's an update, I prefer to keep the implementation simple.
It works well at our current scale, and can be refactored when higher load
or multi-trunk support becomes necessary.
While caching dispatch rule IDs could be a performance optimization,
I feel it would be premature and potentially error-prone due to the complexity
of invalidation. If performance becomes an issue, I’ll consider introducing
caching at that point. To handle the edge case where multiple dispatch rules
with different PIN codes are present, the service performs an extensive
cleanup during room creation to ensure SIP routing remains clean and
predictable. This edge case should not happen.
In the 'delete_dispatch_rule' if deleting one rule fails, method would exit
without deleting the other rules. It's okay IMO for a first iteration.
If multiple dispatch rules are often found for room, I would enhance this part.
2025-07-01 17:59:45 +02:00
|
|
|
raise ActionFailedError(
|
|
|
|
|
f"Failed to clear room cache for room {room_id}"
|
|
|
|
|
) from e
|