mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-02 21:58:29 +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_DOWNLOAD_BASE_URL=http://localhost:3000/recording
|
||||||
|
|
||||||
# Recording encoding (LiveKit Egress advanced options).
|
# Recording encoding (LiveKit Egress advanced options).
|
||||||
# When RECORDING_ENCODING_ENABLED is False (default), LiveKit uses its built-in
|
# Encoding is described by a named resolution (width/height) and a named profile
|
||||||
# H264_720P_30 preset (1280x720, 30fps, 3000 kbps). Enable and tune to reduce
|
# (framerate + video bitrate per resolution) instead of raw encoder values. The
|
||||||
# file size and CPU load on the egress worker.
|
# start-recording API accepts a pair per recording, e.g.
|
||||||
# RECORDING_ENCODING_ENABLED=False
|
# options.encoding={"resolution": "720p", "profile": "talking_heads"}; only keys
|
||||||
# RECORDING_ENCODING_WIDTH=1280
|
# declared in the two maps below are accepted, "profile" is optional.
|
||||||
# RECORDING_ENCODING_HEIGHT=720
|
# Both maps are read as a one-line Python dict literal (ast.literal_eval): double
|
||||||
# RECORDING_ENCODING_FRAMERATE=30
|
# quoted keys, no outer quotes, no trailing comma. Every profile must define a
|
||||||
# RECORDING_ENCODING_VIDEO_BITRATE_KBPS=3000
|
# 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_AUDIO_BITRATE_KBPS=128
|
||||||
# RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=4.0
|
# 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
|
# Telephony
|
||||||
ROOM_TELEPHONY_ENABLED=True
|
ROOM_TELEPHONY_ENABLED=True
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,37 @@ _RECORDING_AUDIO_CODEC = livekit_api.AudioCodec.AAC
|
|||||||
_RECORDING_AUDIO_FREQUENCY_HZ = 48000
|
_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)
|
@dataclass(frozen=True)
|
||||||
class WorkerServiceConfig:
|
class WorkerServiceConfig:
|
||||||
"""Declare Worker Service common configurations"""
|
"""Declare Worker Service common configurations"""
|
||||||
@@ -38,22 +69,14 @@ class WorkerServiceConfig:
|
|||||||
|
|
||||||
logger.debug("Loading WorkerServiceConfig from settings.")
|
logger.debug("Loading WorkerServiceConfig from settings.")
|
||||||
|
|
||||||
encoding_options: Optional[Dict[str, Any]] = None
|
# Single source of truth for the EncodingOptions kwargs; the services
|
||||||
if settings.RECORDING_ENCODING_ENABLED:
|
# layer only unpacks this dict. Recordings carrying their own encoding
|
||||||
# Single source of truth for the EncodingOptions kwargs:
|
# resolve it per request and bypass this default.
|
||||||
# operator-tunable values live in Django settings, codec / frequency
|
encoding_options: Optional[Dict[str, Any]] = (
|
||||||
# are pinned constants. The services layer only unpacks this dict.
|
_build_default_encoding_options()
|
||||||
encoding_options = {
|
if settings.RECORDING_ENCODING_ENABLED
|
||||||
"width": settings.RECORDING_ENCODING_WIDTH,
|
else None
|
||||||
"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,
|
|
||||||
}
|
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
output_folder=settings.RECORDING_OUTPUT_FOLDER,
|
output_folder=settings.RECORDING_OUTPUT_FOLDER,
|
||||||
|
|||||||
@@ -36,16 +36,14 @@ def resolve_encoding_config(encoding_config):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resolution:
|
if resolution:
|
||||||
width, height = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
dimensions = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
||||||
resolved["width"] = width
|
resolved["width"] = dimensions["width"]
|
||||||
resolved["height"] = height
|
resolved["height"] = dimensions["height"]
|
||||||
|
|
||||||
if resolution and profile:
|
if resolution and profile:
|
||||||
fps, kbps_by_resolution = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[
|
profile_spec = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
|
||||||
profile
|
resolved["framerate"] = profile_spec["fps"]
|
||||||
]
|
resolved["video_bitrate"] = profile_spec["kbps"][resolution]
|
||||||
resolved["framerate"] = fps
|
|
||||||
resolved["video_bitrate"] = kbps_by_resolution[resolution]
|
|
||||||
|
|
||||||
return resolved
|
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):
|
def test_resolve_profile_resolution_combinations(service, profile, resolution):
|
||||||
"""Every (profile, resolution) pair should resolve to the values from settings."""
|
"""Every (profile, resolution) pair should resolve to the values from settings."""
|
||||||
expected_width, expected_height = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[
|
dimensions = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
||||||
resolution
|
profile_spec = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
|
||||||
]
|
|
||||||
expected_fps, kbps_by_resolution = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[
|
|
||||||
profile
|
|
||||||
]
|
|
||||||
expected_bitrate = kbps_by_resolution[resolution]
|
|
||||||
|
|
||||||
resolved = resolve_encoding_config(
|
resolved = resolve_encoding_config(
|
||||||
EncodingConfig(resolution=resolution, profile=profile)
|
EncodingConfig(resolution=resolution, profile=profile)
|
||||||
)
|
)
|
||||||
result = service._resolve_encoding_options(resolved)
|
result = service._resolve_encoding_options(resolved)
|
||||||
|
|
||||||
assert result.width == expected_width
|
assert result.width == dimensions["width"]
|
||||||
assert result.height == expected_height
|
assert result.height == dimensions["height"]
|
||||||
assert result.framerate == expected_fps
|
assert result.framerate == profile_spec["fps"]
|
||||||
assert result.video_bitrate == expected_bitrate
|
assert result.video_bitrate == profile_spec["kbps"][resolution]
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_options_none_profile_uses_livekit_defaults(service):
|
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_S3_REGION_NAME="test-region",
|
||||||
AWS_STORAGE_BUCKET_NAME="test-bucket",
|
AWS_STORAGE_BUCKET_NAME="test-bucket",
|
||||||
RECORDING_ENCODING_ENABLED=True,
|
RECORDING_ENCODING_ENABLED=True,
|
||||||
RECORDING_ENCODING_WIDTH=1280,
|
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS={
|
||||||
RECORDING_ENCODING_HEIGHT=720,
|
"720p": {"width": 1280, "height": 720},
|
||||||
RECORDING_ENCODING_FRAMERATE=15,
|
},
|
||||||
RECORDING_ENCODING_VIDEO_BITRATE_KBPS=600,
|
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_AUDIO_BITRATE_KBPS=64,
|
||||||
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=10.0,
|
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=10.0,
|
||||||
)
|
)
|
||||||
def test_config_encoding_options_enabled():
|
def test_config_encoding_options_enabled():
|
||||||
"""When RECORDING_ENCODING_ENABLED is True, encoding options are populated.
|
"""When RECORDING_ENCODING_ENABLED is True, encoding options are populated.
|
||||||
|
|
||||||
The dict mixes operator-tunable values from settings with pinned codec /
|
The dict mixes values resolved from the default resolution / profile with
|
||||||
frequency constants, so the services layer can simply unpack it.
|
pinned codec / frequency constants, so the services layer can simply unpack it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
WorkerServiceConfig.from_settings.cache_clear()
|
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(
|
@override_settings(
|
||||||
RECORDING_OUTPUT_FOLDER="/test/output",
|
RECORDING_OUTPUT_FOLDER="/test/output",
|
||||||
LIVEKIT_CONFIGURATION={"server": "test.example.com"},
|
LIVEKIT_CONFIGURATION={"server": "test.example.com"},
|
||||||
|
|||||||
@@ -755,7 +755,7 @@ class Base(Configuration):
|
|||||||
False, environ_name="RECORDING_ENCODING_ENABLED", environ_prefix=None
|
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(
|
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS = values.DictValue(
|
||||||
{
|
{
|
||||||
"540p": {"width": 960, "height": 540},
|
"540p": {"width": 960, "height": 540},
|
||||||
@@ -1211,16 +1211,21 @@ class Base(Configuration):
|
|||||||
def _check_recording_encoding_maps(cls):
|
def _check_recording_encoding_maps(cls):
|
||||||
"""Ensure the per-recording encoding maps are mutually consistent.
|
"""Ensure the per-recording encoding maps are mutually consistent.
|
||||||
|
|
||||||
Every profile in RECORDING_ENCODING_AVAILABLE_PROFILES must define a bitrate for
|
Every profile in RECORDING_ENCODING_AVAILABLE_PROFILES must declare an fps and
|
||||||
each resolution declared in RECORDING_ENCODING_AVAILABLE_RESOLUTIONS.
|
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)
|
resolutions = set(cls.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS)
|
||||||
for profile, (
|
for profile, spec in cls.RECORDING_ENCODING_AVAILABLE_PROFILES.items():
|
||||||
_fps,
|
if "fps" not in spec or "kbps" not in spec:
|
||||||
kbps_by_resolution,
|
raise ValueError(
|
||||||
# DictValue resolves to a dict at runtime; pylint sees the descriptor.
|
f"Profile '{profile}' in RECORDING_ENCODING_AVAILABLE_PROFILES must "
|
||||||
) in cls.RECORDING_ENCODING_AVAILABLE_PROFILES.items(): # pylint: disable=no-member
|
"define both 'fps' and 'kbps'."
|
||||||
profile_resolutions = set(kbps_by_resolution)
|
)
|
||||||
|
|
||||||
|
profile_resolutions = set(spec["kbps"])
|
||||||
if profile_resolutions != resolutions:
|
if profile_resolutions != resolutions:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Profile '{profile}' in RECORDING_ENCODING_AVAILABLE_PROFILES must "
|
f"Profile '{profile}' in RECORDING_ENCODING_AVAILABLE_PROFILES must "
|
||||||
@@ -1229,6 +1234,22 @@ class Base(Configuration):
|
|||||||
f"{resolutions ^ profile_resolutions}"
|
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
|
@classmethod
|
||||||
def post_setup(cls):
|
def post_setup(cls):
|
||||||
"""Post setup configuration.
|
"""Post setup configuration.
|
||||||
|
|||||||
Reference in New Issue
Block a user