fix bug linked to encoding preference not being passed custom profiles are used

This commit is contained in:
leo
2026-07-14 18:58:39 +02:00
parent f5d0458f29
commit 12c28ec37d
5 changed files with 77 additions and 74 deletions
+3 -3
View File
@@ -63,12 +63,12 @@ from core.recording.worker.exceptions import (
RecordingStopError,
)
from core.recording.worker.factories import (
build_encoding_options,
get_worker_service,
)
from core.recording.worker.mediator import (
WorkerServiceMediator,
)
from core.recording.worker.services import resolve_encoding_config
from core.services.invitation import InvitationService
from core.services.livekit_events import (
LiveKitEventsService,
@@ -402,8 +402,8 @@ class RoomViewSet(
if options is not None and options.encoding is not None:
# Persist the resolved encoding (concrete width/height/framerate/
# bitrate) alongside the requested resolution/profile for traceability.
options_data["encoding"]["resolved"] = resolve_encoding_config(
options.encoding
options_data["encoding"]["resolved"] = build_encoding_options(
options.encoding.resolution, options.encoding.profile
)
try:
+39 -15
View File
@@ -22,6 +22,44 @@ _RECORDING_AUDIO_CODEC = livekit_api.AudioCodec.AAC
_RECORDING_AUDIO_FREQUENCY_HZ = 48000
def build_encoding_options(resolution, profile):
"""Assemble the LiveKit ``EncodingOptions`` kwargs for a resolution/profile.
Single source of truth shared by the default encoding
(``WorkerServiceConfig.from_settings``) and the per-recording encoding
persisted by the start-recording API, so both paths always produce the
same shape.
The profile-independent fields (audio bitrate, keyframe interval and the
pinned codec / frequency constants) are always included.
The resolution-dependent fields are added only when they can be resolved:
width/height require a resolution; framerate/video_bitrate require both a
resolution and a profile (a resolution-only encoding leaves framerate and
bitrate to LiveKit's defaults).
"""
options: Dict[str, Any] = {
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
"video_codec": _RECORDING_VIDEO_CODEC,
"audio_codec": _RECORDING_AUDIO_CODEC,
"audio_frequency": _RECORDING_AUDIO_FREQUENCY_HZ,
}
if resolution:
resolution_config = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[
resolution
]
options["width"] = resolution_config["width"]
options["height"] = resolution_config["height"]
if resolution and profile:
profile_config = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
options["framerate"] = profile_config["fps"]
options["video_bitrate"] = profile_config["kbps"][resolution]
return options
@dataclass(frozen=True)
class WorkerServiceConfig:
"""Declare Worker Service common configurations"""
@@ -47,21 +85,7 @@ class WorkerServiceConfig:
encoding_options: Optional[Dict[str, Any]] = None
if resolution and profile:
resolution_config = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[
resolution
]
profile_config = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
encoding_options = {
"width": resolution_config["width"],
"height": resolution_config["height"],
"framerate": profile_config["fps"],
"video_bitrate": profile_config["kbps"][resolution],
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
"video_codec": _RECORDING_VIDEO_CODEC,
"audio_codec": _RECORDING_AUDIO_CODEC,
"audio_frequency": _RECORDING_AUDIO_FREQUENCY_HZ,
}
encoding_options = build_encoding_options(resolution, profile)
return cls(
output_folder=settings.RECORDING_OUTPUT_FOLDER,
@@ -2,8 +2,6 @@
# pylint: disable=no-member
from django.conf import settings
from asgiref.sync import async_to_sync
from livekit import api as livekit_api
@@ -13,43 +11,6 @@ from .exceptions import WorkerConnectionError, WorkerResponseError
from .factories import WorkerServiceConfig
def resolve_encoding_config(encoding_config):
"""Resolve a per-recording EncodingConfig to concrete encoding fields.
Returns a JSON-serializable dict of the LiveKit ``EncodingOptions`` kwargs
derived from the request's resolution / profile, or None when no
encoding_config is provided. This allows to derive width, height, fps, and
bitrate from (resolution, profile).
Only the fields that can actually be resolved are included: width/height
require a resolution, framerate/video_bitrate require both a resolution and a
profile.
"""
if encoding_config is None:
return None
resolution = encoding_config.resolution
profile = encoding_config.profile
resolved = {
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
}
if resolution:
resolution_config = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[
resolution
]
resolved["width"] = resolution_config["width"]
resolved["height"] = resolution_config["height"]
if resolution and profile:
profile_config = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
resolved["framerate"] = profile_config["fps"]
resolved["video_bitrate"] = profile_config["kbps"][resolution]
return resolved
class BaseEgressService:
"""Base egress defining common methods to manage and interact with LiveKit egress processes."""
@@ -7,13 +7,12 @@ from unittest.mock import Mock
from django.conf import settings
import pytest
from livekit import api as livekit_api
from pydantic import ValidationError as PydanticValidationError
from core.api.serializers import EncodingConfig
from core.recording.worker.services import (
VideoCompositeEgressService,
resolve_encoding_config,
)
from core.recording.worker.factories import build_encoding_options
from core.recording.worker.services import VideoCompositeEgressService
def make_config():
@@ -39,23 +38,28 @@ def service():
return svc
# --- resolve_encoding_config ---
# --- build_encoding_options ---
def test_resolve_config_returns_none_without_config():
"""Resolver should return None when no encoding config is provided."""
assert resolve_encoding_config(None) is None
def test_build_options_without_profile_omits_profile_fields():
"""A resolution-only config should resolve dimensions but no framerate/bitrate.
def test_resolve_config_without_profile_omits_profile_fields():
"""A resolution-only config should resolve dimensions but no framerate/bitrate."""
resolved = resolve_encoding_config(EncodingConfig(resolution="720p"))
The profile-independent fields (audio bitrate, keyframe interval, codec /
frequency pins) are always present, matching the default encoding.
"""
resolved = build_encoding_options("720p", None)
assert resolved == {
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
"video_codec": livekit_api.VideoCodec.H264_MAIN,
"audio_codec": livekit_api.AudioCodec.AAC,
"audio_frequency": 48000,
"width": 1280,
"height": 720,
}
assert "framerate" not in resolved
assert "video_bitrate" not in resolved
def test_encoding_config_requires_resolution():
@@ -92,22 +96,31 @@ def test_resolve_profile_resolution_combinations(service, profile, resolution):
expected_fps = profile_config["fps"]
expected_bitrate = profile_config["kbps"][resolution]
resolved = resolve_encoding_config(
EncodingConfig(resolution=resolution, profile=profile)
)
resolved = build_encoding_options(resolution, profile)
result = service._resolve_encoding_options(resolved)
assert result.width == expected_width
assert result.height == expected_height
assert result.framerate == expected_fps
assert result.video_bitrate == expected_bitrate
# Profile-independent fields match the default encoding, never dropped.
assert result.audio_bitrate == settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS
assert result.video_codec == livekit_api.VideoCodec.H264_MAIN
assert result.audio_codec == livekit_api.AudioCodec.AAC
assert result.audio_frequency == 48000
def test_resolve_options_none_profile_uses_livekit_defaults(service):
"""Missing profile should pass 0 fps/bitrate (LiveKit protobuf default)."""
resolved = resolve_encoding_config(EncodingConfig(resolution="720p"))
"""Missing profile should pass 0 fps/bitrate (LiveKit protobuf default).
The pinned codec / audio fields are still applied even without a profile.
"""
resolved = build_encoding_options("720p", None)
result = service._resolve_encoding_options(resolved)
assert result.width == 1280
assert result.height == 720
assert result.framerate == 0
assert result.video_bitrate == 0
assert result.audio_bitrate == settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS
assert result.video_codec == livekit_api.VideoCodec.H264_MAIN
assert result.audio_codec == livekit_api.AudioCodec.AAC
@@ -7,6 +7,7 @@ Test rooms API endpoints in the Meet core app: start recording.
from unittest import mock
import pytest
from livekit import api as livekit_api
from rest_framework.test import APIClient
from ...factories import RoomFactory, UserFactory
@@ -547,7 +548,11 @@ def test_start_recording_persists_resolved_encoding(
"resolution": "720p",
"profile": "talking_heads",
"resolved": {
"audio_bitrate": settings.RECORDING_ENCODING_AUDIO_BITRATE_KBPS,
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
"video_codec": livekit_api.VideoCodec.H264_MAIN,
"audio_codec": livekit_api.AudioCodec.AAC,
"audio_frequency": 48000,
"width": 1280,
"height": 720,
"framerate": 15,