mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-02 13:48:26 +00:00
wip temp
This commit is contained in:
@@ -69,17 +69,31 @@ SUMMARY_SERVICE_WEBHOOK_API_TOKEN=webhook-password
|
||||
RECORDING_DOWNLOAD_BASE_URL=http://localhost:3000/recording
|
||||
|
||||
# Recording encoding (LiveKit Egress advanced options).
|
||||
# When RECORDING_ENCODING_ENABLED is False (default), LiveKit uses its built-in
|
||||
# H264_720P_30 preset (1280x720, 30fps, 3000 kbps). Enable and tune to reduce
|
||||
# file size and CPU load on the egress worker.
|
||||
# RECORDING_ENCODING_ENABLED=False
|
||||
# RECORDING_ENCODING_WIDTH=1280
|
||||
# RECORDING_ENCODING_HEIGHT=720
|
||||
# RECORDING_ENCODING_FRAMERATE=30
|
||||
# RECORDING_ENCODING_VIDEO_BITRATE_KBPS=3000
|
||||
# Encoding is described by a named resolution (width/height) and a named profile
|
||||
# (framerate + video bitrate per resolution) instead of raw encoder values. The
|
||||
# start-recording API accepts a pair per recording, e.g.
|
||||
# options.encoding={"resolution": "720p", "profile": "talking_heads"}; only keys
|
||||
# declared in the two maps below are accepted, "profile" is optional.
|
||||
# Both maps are read as a one-line Python dict literal (ast.literal_eval): double
|
||||
# quoted keys, no outer quotes, no trailing comma. Every profile must define a
|
||||
# kbps entry for exactly the resolutions of the resolutions map, or startup fails.
|
||||
# RECORDING_ENCODING_AVAILABLE_RESOLUTIONS={"540p": {"width": 960, "height": 540}, "720p": {"width": 1280, "height": 720}, "1080p": {"width": 1920, "height": 1080}}
|
||||
# RECORDING_ENCODING_AVAILABLE_PROFILES={"talking_heads": {"fps": 15, "kbps": {"540p": 400, "720p": 700, "1080p": 1200}}, "text": {"fps": 15, "kbps": {"540p": 600, "720p": 1000, "1080p": 1800}}, "mixed": {"fps": 20, "kbps": {"540p": 900, "720p": 1500, "1080p": 2500}}, "full": {"fps": 30, "kbps": {"540p": 2000, "720p": 3000, "1080p": 4500}}}
|
||||
|
||||
# Defaults for recordings that don't carry an encoding. Must be keys of the maps
|
||||
# above. Declared but not yet read by the recording code at this commit.
|
||||
# RECORDING_ENCODING_DEFAULT_RESOLUTION=720p
|
||||
# RECORDING_ENCODING_DEFAULT_PROFILE=full
|
||||
|
||||
# Applied to every resolved encoding, independent of resolution and profile.
|
||||
# RECORDING_ENCODING_AUDIO_BITRATE_KBPS=128
|
||||
# RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=4.0
|
||||
|
||||
# Server-wide encoding used when a recording carries no encoding of its own.
|
||||
# Keep False: when True, the worker factory still reads RECORDING_ENCODING_WIDTH,
|
||||
# _HEIGHT, _FRAMERATE and _VIDEO_BITRATE_KBPS, which no longer exist.
|
||||
# RECORDING_ENCODING_ENABLED=False
|
||||
|
||||
# Telephony
|
||||
ROOM_TELEPHONY_ENABLED=True
|
||||
|
||||
|
||||
@@ -22,6 +22,37 @@ _RECORDING_AUDIO_CODEC = livekit_api.AudioCodec.AAC
|
||||
_RECORDING_AUDIO_FREQUENCY_HZ = 48000
|
||||
|
||||
|
||||
def _build_default_encoding_options() -> Optional[Dict[str, Any]]:
|
||||
"""Build the server-wide EncodingOptions kwargs, or None to keep LiveKit's preset.
|
||||
|
||||
Operator-tunable values live in Django settings; the default resolution gives
|
||||
width / height, the default profile gives framerate and video bitrate, while
|
||||
codec and frequency are pinned constants. Either default left empty means we
|
||||
use the livekit defaults.
|
||||
"""
|
||||
|
||||
resolution = settings.RECORDING_ENCODING_DEFAULT_RESOLUTION
|
||||
profile = settings.RECORDING_ENCODING_DEFAULT_PROFILE
|
||||
|
||||
if not resolution or not profile:
|
||||
return None
|
||||
|
||||
dimensions = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
||||
profile_spec = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
|
||||
|
||||
return {
|
||||
"width": dimensions["width"],
|
||||
"height": dimensions["height"],
|
||||
"framerate": profile_spec["fps"],
|
||||
"video_bitrate": profile_spec["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,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkerServiceConfig:
|
||||
"""Declare Worker Service common configurations"""
|
||||
@@ -38,22 +69,14 @@ class WorkerServiceConfig:
|
||||
|
||||
logger.debug("Loading WorkerServiceConfig from settings.")
|
||||
|
||||
encoding_options: Optional[Dict[str, Any]] = None
|
||||
if settings.RECORDING_ENCODING_ENABLED:
|
||||
# Single source of truth for the EncodingOptions kwargs:
|
||||
# operator-tunable values live in Django settings, codec / frequency
|
||||
# are pinned constants. The services layer only unpacks this dict.
|
||||
encoding_options = {
|
||||
"width": settings.RECORDING_ENCODING_WIDTH,
|
||||
"height": settings.RECORDING_ENCODING_HEIGHT,
|
||||
"framerate": settings.RECORDING_ENCODING_FRAMERATE,
|
||||
"video_bitrate": settings.RECORDING_ENCODING_VIDEO_BITRATE_KBPS,
|
||||
"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,
|
||||
}
|
||||
# Single source of truth for the EncodingOptions kwargs; the services
|
||||
# layer only unpacks this dict. Recordings carrying their own encoding
|
||||
# resolve it per request and bypass this default.
|
||||
encoding_options: Optional[Dict[str, Any]] = (
|
||||
_build_default_encoding_options()
|
||||
if settings.RECORDING_ENCODING_ENABLED
|
||||
else None
|
||||
)
|
||||
|
||||
return cls(
|
||||
output_folder=settings.RECORDING_OUTPUT_FOLDER,
|
||||
|
||||
@@ -36,16 +36,14 @@ def resolve_encoding_config(encoding_config):
|
||||
}
|
||||
|
||||
if resolution:
|
||||
width, height = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
||||
resolved["width"] = width
|
||||
resolved["height"] = height
|
||||
dimensions = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
||||
resolved["width"] = dimensions["width"]
|
||||
resolved["height"] = dimensions["height"]
|
||||
|
||||
if resolution and profile:
|
||||
fps, kbps_by_resolution = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[
|
||||
profile
|
||||
]
|
||||
resolved["framerate"] = fps
|
||||
resolved["video_bitrate"] = kbps_by_resolution[resolution]
|
||||
profile_spec = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
|
||||
resolved["framerate"] = profile_spec["fps"]
|
||||
resolved["video_bitrate"] = profile_spec["kbps"][resolution]
|
||||
|
||||
return resolved
|
||||
|
||||
|
||||
@@ -85,23 +85,18 @@ def test_resolve_options_returns_none_when_empty(service, encoding_options):
|
||||
)
|
||||
def test_resolve_profile_resolution_combinations(service, profile, resolution):
|
||||
"""Every (profile, resolution) pair should resolve to the values from settings."""
|
||||
expected_width, expected_height = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[
|
||||
resolution
|
||||
]
|
||||
expected_fps, kbps_by_resolution = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[
|
||||
profile
|
||||
]
|
||||
expected_bitrate = kbps_by_resolution[resolution]
|
||||
dimensions = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
||||
profile_spec = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
|
||||
|
||||
resolved = resolve_encoding_config(
|
||||
EncodingConfig(resolution=resolution, profile=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
|
||||
assert result.width == dimensions["width"]
|
||||
assert result.height == dimensions["height"]
|
||||
assert result.framerate == profile_spec["fps"]
|
||||
assert result.video_bitrate == profile_spec["kbps"][resolution]
|
||||
|
||||
|
||||
def test_resolve_options_none_profile_uses_livekit_defaults(service):
|
||||
|
||||
@@ -85,18 +85,22 @@ def test_config_immutability(default_config):
|
||||
AWS_S3_REGION_NAME="test-region",
|
||||
AWS_STORAGE_BUCKET_NAME="test-bucket",
|
||||
RECORDING_ENCODING_ENABLED=True,
|
||||
RECORDING_ENCODING_WIDTH=1280,
|
||||
RECORDING_ENCODING_HEIGHT=720,
|
||||
RECORDING_ENCODING_FRAMERATE=15,
|
||||
RECORDING_ENCODING_VIDEO_BITRATE_KBPS=600,
|
||||
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS={
|
||||
"720p": {"width": 1280, "height": 720},
|
||||
},
|
||||
RECORDING_ENCODING_AVAILABLE_PROFILES={
|
||||
"talking_heads": {"fps": 15, "kbps": {"720p": 600}},
|
||||
},
|
||||
RECORDING_ENCODING_DEFAULT_RESOLUTION="720p",
|
||||
RECORDING_ENCODING_DEFAULT_PROFILE="talking_heads",
|
||||
RECORDING_ENCODING_AUDIO_BITRATE_KBPS=64,
|
||||
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=10.0,
|
||||
)
|
||||
def test_config_encoding_options_enabled():
|
||||
"""When RECORDING_ENCODING_ENABLED is True, encoding options are populated.
|
||||
|
||||
The dict mixes operator-tunable values from settings with pinned codec /
|
||||
frequency constants, so the services layer can simply unpack it.
|
||||
The dict mixes values resolved from the default resolution / profile with
|
||||
pinned codec / frequency constants, so the services layer can simply unpack it.
|
||||
"""
|
||||
|
||||
WorkerServiceConfig.from_settings.cache_clear()
|
||||
@@ -115,6 +119,27 @@ def test_config_encoding_options_enabled():
|
||||
}
|
||||
|
||||
|
||||
@override_settings(
|
||||
RECORDING_OUTPUT_FOLDER="/test/output",
|
||||
LIVEKIT_CONFIGURATION={"server": "test.example.com"},
|
||||
AWS_S3_ENDPOINT_URL="https://s3.test.com",
|
||||
AWS_S3_ACCESS_KEY_ID="test_key",
|
||||
AWS_S3_SECRET_ACCESS_KEY="test_secret",
|
||||
AWS_S3_REGION_NAME="test-region",
|
||||
AWS_STORAGE_BUCKET_NAME="test-bucket",
|
||||
RECORDING_ENCODING_ENABLED=True,
|
||||
RECORDING_ENCODING_DEFAULT_RESOLUTION="",
|
||||
RECORDING_ENCODING_DEFAULT_PROFILE="",
|
||||
)
|
||||
def test_config_encoding_options_without_defaults():
|
||||
"""An empty default resolution or profile leaves the encoding to LiveKit."""
|
||||
|
||||
WorkerServiceConfig.from_settings.cache_clear()
|
||||
config = WorkerServiceConfig.from_settings()
|
||||
|
||||
assert config.encoding_options is None
|
||||
|
||||
|
||||
@override_settings(
|
||||
RECORDING_OUTPUT_FOLDER="/test/output",
|
||||
LIVEKIT_CONFIGURATION={"server": "test.example.com"},
|
||||
|
||||
@@ -755,7 +755,7 @@ class Base(Configuration):
|
||||
False, environ_name="RECORDING_ENCODING_ENABLED", environ_prefix=None
|
||||
)
|
||||
|
||||
# Map resolution string -> (width, height) in pixels.
|
||||
# Map resolution name -> {"width": ..., "height": ...} in pixels.
|
||||
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS = values.DictValue(
|
||||
{
|
||||
"540p": {"width": 960, "height": 540},
|
||||
@@ -1211,16 +1211,21 @@ class Base(Configuration):
|
||||
def _check_recording_encoding_maps(cls):
|
||||
"""Ensure the per-recording encoding maps are mutually consistent.
|
||||
|
||||
Every profile in RECORDING_ENCODING_AVAILABLE_PROFILES must define a bitrate for
|
||||
each resolution declared in RECORDING_ENCODING_AVAILABLE_RESOLUTIONS.
|
||||
Every profile in RECORDING_ENCODING_AVAILABLE_PROFILES must declare an fps and
|
||||
a bitrate for each resolution declared in RECORDING_ENCODING_AVAILABLE_RESOLUTIONS,
|
||||
and each non-empty default must name an entry of its map.
|
||||
"""
|
||||
# DictValue resolves to a dict at runtime; pylint sees the descriptor.
|
||||
# pylint: disable=no-member
|
||||
resolutions = set(cls.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS)
|
||||
for profile, (
|
||||
_fps,
|
||||
kbps_by_resolution,
|
||||
# DictValue resolves to a dict at runtime; pylint sees the descriptor.
|
||||
) in cls.RECORDING_ENCODING_AVAILABLE_PROFILES.items(): # pylint: disable=no-member
|
||||
profile_resolutions = set(kbps_by_resolution)
|
||||
for profile, spec in cls.RECORDING_ENCODING_AVAILABLE_PROFILES.items():
|
||||
if "fps" not in spec or "kbps" not in spec:
|
||||
raise ValueError(
|
||||
f"Profile '{profile}' in RECORDING_ENCODING_AVAILABLE_PROFILES must "
|
||||
"define both 'fps' and 'kbps'."
|
||||
)
|
||||
|
||||
profile_resolutions = set(spec["kbps"])
|
||||
if profile_resolutions != resolutions:
|
||||
raise ValueError(
|
||||
f"Profile '{profile}' in RECORDING_ENCODING_AVAILABLE_PROFILES must "
|
||||
@@ -1229,6 +1234,22 @@ class Base(Configuration):
|
||||
f"{resolutions ^ profile_resolutions}"
|
||||
)
|
||||
|
||||
default_resolution = cls.RECORDING_ENCODING_DEFAULT_RESOLUTION
|
||||
if default_resolution and default_resolution not in resolutions:
|
||||
raise ValueError(
|
||||
f"RECORDING_ENCODING_DEFAULT_RESOLUTION '{default_resolution}' is not a "
|
||||
"key of RECORDING_ENCODING_AVAILABLE_RESOLUTIONS, choose from "
|
||||
f"{sorted(resolutions)}."
|
||||
)
|
||||
|
||||
profiles = set(cls.RECORDING_ENCODING_AVAILABLE_PROFILES)
|
||||
default_profile = cls.RECORDING_ENCODING_DEFAULT_PROFILE
|
||||
if default_profile and default_profile not in profiles:
|
||||
raise ValueError(
|
||||
f"RECORDING_ENCODING_DEFAULT_PROFILE '{default_profile}' is not a key of "
|
||||
f"RECORDING_ENCODING_AVAILABLE_PROFILES, choose from {sorted(profiles)}."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def post_setup(cls):
|
||||
"""Post setup configuration.
|
||||
|
||||
Reference in New Issue
Block a user