mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-03 14:17:59 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21866a731f | |||
| 3a93826166 | |||
| 8473069670 | |||
| cf3960db95 | |||
| d80d31897c | |||
| 63a7751072 | |||
| 1ac1778521 | |||
| fcc58065d2 | |||
| 21c57bffb4 | |||
| bd81c99495 |
+11
-4
@@ -10,13 +10,19 @@ and this project adheres to
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- ✨(backend) add per-recording encoding quality presets to start-recording API
|
- ✨(frontend) add 1080p sending resolution option #1660
|
||||||
|
- ✨(backend) add Traefik support via configurable media-auth url header #1649
|
||||||
## [1.30.0] - 2026-09-01
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- ♿️(frontend) close side panel with Escape key #1507
|
- ⬆️(dev) pin LiveKit server to v1.13.6
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🐛(backend) allow any printable ASCII characters in user sub field #1673
|
||||||
|
- 🐛(frontend) keep the sending resolution picked while the camera is off #1667
|
||||||
|
|
||||||
|
## [1.30.0] - 2026-09-01
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@@ -33,6 +39,7 @@ and this project adheres to
|
|||||||
- ⬆️(frontend) upgrade @fontsource/opendyslexic from 5.2.5 to 5.3.0
|
- ⬆️(frontend) upgrade @fontsource/opendyslexic from 5.2.5 to 5.3.0
|
||||||
- ⬆️(addons) upgrade core-js from 3.49.0 to 3.50.0
|
- ⬆️(addons) upgrade core-js from 3.49.0 to 3.50.0
|
||||||
- ♻️(backend) factorize s3 client creation in utils
|
- ♻️(backend) factorize s3 client creation in utils
|
||||||
|
- ♿️(frontend) close side panel with Escape key #1507
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
-86
@@ -16,92 +16,6 @@ the following command inside your docker container:
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Recording encoding settings replaced by a resolution/profile model
|
|
||||||
|
|
||||||
The `RECORDING_ENCODING_*` settings introduced in v1.16.0 exposed raw encoder
|
|
||||||
values (width, height, framerate, bitrate). They are replaced by two named and configurable sets of
|
|
||||||
dimensions, a **resolution** (default: `540p`, `720p`, `1080p`) and a **profile**
|
|
||||||
(default: `talking_heads`, `text`, `mixed`, `full`), which are resolved to the width, height,
|
|
||||||
fps and video bitrate.
|
|
||||||
|
|
||||||
**The following environment variables are no longer read. If they are still set in
|
|
||||||
your deployment they are silently ignored, and your recordings will be encoded with
|
|
||||||
the new defaults instead of your tuned values.**
|
|
||||||
|
|
||||||
| Removed variable | Replaced by |
|
|
||||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `RECORDING_ENCODING_ENABLED` | Nothing. A default encoding is now always built (see below). **Not** `RECORDING_CUSTOM_ENCODING_ENABLED`, which gates a different feature. |
|
|
||||||
| `RECORDING_ENCODING_WIDTH` | The `width` of the entry selected by `RECORDING_ENCODING_DEFAULT_RESOLUTION` in `RECORDING_ENCODING_AVAILABLE_RESOLUTIONS`. |
|
|
||||||
| `RECORDING_ENCODING_HEIGHT` | The `height` of that same entry. |
|
|
||||||
| `RECORDING_ENCODING_FRAMERATE` | The `fps` of the profile selected by `RECORDING_ENCODING_DEFAULT_PROFILE` in `RECORDING_ENCODING_AVAILABLE_PROFILES`. |
|
|
||||||
| `RECORDING_ENCODING_VIDEO_BITRATE_KBPS` | That profile's `kbps`. |
|
|
||||||
|
|
||||||
`RECORDING_ENCODING_AUDIO_BITRATE_KBPS` and `RECORDING_ENCODING_KEY_FRAME_INTERVAL_S`
|
|
||||||
are unchanged and keep their values.
|
|
||||||
|
|
||||||
#### If you never set `RECORDING_ENCODING_ENABLED=True`
|
|
||||||
|
|
||||||
No action is required. The shipped defaults (`RECORDING_ENCODING_DEFAULT_PROFILE=full`,
|
|
||||||
`RECORDING_ENCODING_DEFAULT_RESOLUTION=720p`) match LiveKit's built-in
|
|
||||||
`H264_720P_30` preset: 1280×720, 30 fps, 3000 kbps H.264 MAIN, 128 kbps AAC.
|
|
||||||
|
|
||||||
Note that these values are now sent explicitly as advanced `EncodingOptions`
|
|
||||||
rather than relying on LiveKit's preset, so `RECORDING_ENCODING_AUDIO_BITRATE_KBPS`
|
|
||||||
and `RECORDING_ENCODING_KEY_FRAME_INTERVAL_S` now apply to every recording. They
|
|
||||||
previously applied only when `RECORDING_ENCODING_ENABLED` was `True`.
|
|
||||||
|
|
||||||
To keep letting LiveKit pick the encoding instead, set either default to an empty
|
|
||||||
value:
|
|
||||||
|
|
||||||
```
|
|
||||||
RECORDING_ENCODING_DEFAULT_RESOLUTION=
|
|
||||||
RECORDING_ENCODING_DEFAULT_PROFILE=
|
|
||||||
```
|
|
||||||
|
|
||||||
#### If you had tuned `RECORDING_ENCODING_*` values
|
|
||||||
|
|
||||||
Translate your old values into a default resolution and a default profile. Declare your own resolution and/or profile. Both maps are read from the
|
|
||||||
environment as a single-line Python/JSON dict literal (parsed with
|
|
||||||
`ast.literal_eval`, so use double-quoted keys and no trailing commas, and do not
|
|
||||||
add outer quotes in `.env`-style files):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS={"540p": {"width": 960, "height": 540}, "720p": {"width": 1280, "height": 720}, "1080p": {"width": 1920, "height": 1080}}
|
|
||||||
RECORDING_ENCODING_AVAILABLE_PROFILES={"my_old_profile": {"fps": 15, "kbps": {"540p": 350, "720p": 600, "1080p": 1100}}}
|
|
||||||
RECORDING_ENCODING_DEFAULT_RESOLUTION=720p
|
|
||||||
RECORDING_ENCODING_DEFAULT_PROFILE=my_old_profile
|
|
||||||
```
|
|
||||||
|
|
||||||
Two constraints are validated at startup and may raise a `ValueError`:
|
|
||||||
|
|
||||||
- every profile in `RECORDING_ENCODING_AVAILABLE_PROFILES` must define a `kbps`
|
|
||||||
entry for **exactly** the keys of `RECORDING_ENCODING_AVAILABLE_RESOLUTIONS`;
|
|
||||||
overriding one of the two maps usually means overriding both;
|
|
||||||
- `RECORDING_ENCODING_DEFAULT_RESOLUTION` and `RECORDING_ENCODING_DEFAULT_PROFILE`,
|
|
||||||
when non-empty, must be keys of their respective map.
|
|
||||||
|
|
||||||
#### Optional: per-recording encoding
|
|
||||||
|
|
||||||
`RECORDING_CUSTOM_ENCODING_ENABLED` (default `False`) toggles whether the
|
|
||||||
start-recording API accepts an `encoding` object
|
|
||||||
(`{"resolution": "720p", "profile": "talking_heads"}`, `profile` optional) that
|
|
||||||
overrides the default for a single recording. It does not enable or disable the
|
|
||||||
default encoding, which is built from the two `RECORDING_ENCODING_DEFAULT_*`
|
|
||||||
settings either way. Leaving it at `False` preserves the previous behaviour, where
|
|
||||||
every recording uses the server-side encoding: requests carrying
|
|
||||||
`options.encoding` are rejected with a `400` before the recording is created, so
|
|
||||||
nothing is persisted and no egress is started.
|
|
||||||
|
|
||||||
Before enabling it:
|
|
||||||
|
|
||||||
- clients can only pick keys you declared; there is no way to send a raw width or bitrate
|
|
||||||
- as of this implementation, the frontend never sends `encoding`
|
|
||||||
- `encoding` is accepted but ignored for `transcript` recordings, whose audio-only
|
|
||||||
egress has no video encoding to configure.
|
|
||||||
|
|
||||||
See [docs/features/recording.md](docs/features/recording.md#tuning-recording-encoding)
|
|
||||||
for the full setting reference, the shipped profile table and the tuning caveats.
|
|
||||||
|
|
||||||
## v1.30.0
|
## v1.30.0
|
||||||
|
|
||||||
### Removing S3 storage-event webhooks for recordings
|
### Removing S3 storage-event webhooks for recordings
|
||||||
|
|||||||
+3
-1
@@ -207,12 +207,14 @@ services:
|
|||||||
- kc_postgresql
|
- kc_postgresql
|
||||||
|
|
||||||
livekit:
|
livekit:
|
||||||
image: livekit/livekit-server
|
image: livekit/livekit-server:v1.13.6
|
||||||
entrypoint: /livekit-server --dev --bind 0.0.0.0 --config ./config.yaml
|
entrypoint: /livekit-server --dev --bind 0.0.0.0 --config ./config.yaml
|
||||||
ports:
|
ports:
|
||||||
- "7880:7880"
|
- "7880:7880"
|
||||||
- "7881:7881"
|
- "7881:7881"
|
||||||
- "7882:7882/udp"
|
- "7882:7882/udp"
|
||||||
|
- "3478:3478/udp"
|
||||||
|
- "30000-30100:30000-30100/udp"
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/livekit/config/livekit-server.yaml:/config.yaml
|
- ./docker/livekit/config/livekit-server.yaml:/config.yaml
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ RUN apk update && apk upgrade \
|
|||||||
musl \
|
musl \
|
||||||
musl-utils \
|
musl-utils \
|
||||||
zlib>=1.3.2-r0 \
|
zlib>=1.3.2-r0 \
|
||||||
|
libexpat>=2.8.4-r0 \
|
||||||
&& apk del curl
|
&& apk del curl
|
||||||
|
|
||||||
USER nginx
|
USER nginx
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM livekit/livekit-server:v1.9.4
|
FROM livekit/livekit-server:v1.13.6
|
||||||
|
|
||||||
# We inject the nip.io certificate manually because the livekit chart doesn't support volume mounting
|
# We inject the nip.io certificate manually because the livekit chart doesn't support volume mounting
|
||||||
COPY rootCA.pem /etc/ssl/certs/
|
COPY rootCA.pem /etc/ssl/certs/
|
||||||
|
|||||||
@@ -8,3 +8,16 @@ webhook:
|
|||||||
api_key: devkey
|
api_key: devkey
|
||||||
urls:
|
urls:
|
||||||
- http://app-dev:8000/api/v1.0/rooms/webhooks-livekit/
|
- http://app-dev:8000/api/v1.0/rooms/webhooks-livekit/
|
||||||
|
|
||||||
|
turn:
|
||||||
|
enabled: true
|
||||||
|
domain: turn.127.0.0.1.nip.io
|
||||||
|
udp_port: 3478
|
||||||
|
tls_port: 0
|
||||||
|
external_tls: false
|
||||||
|
relay_range_start: 30000
|
||||||
|
relay_range_end: 30100
|
||||||
|
allow_restricted_peer_cidrs:
|
||||||
|
- 192.168.0.0/16
|
||||||
|
- 172.16.0.0/12
|
||||||
|
|
||||||
|
|||||||
@@ -69,31 +69,17 @@ 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).
|
||||||
# Encoding is described by a named resolution (width/height) and a named profile
|
# When RECORDING_ENCODING_ENABLED is False (default), LiveKit uses its built-in
|
||||||
# (framerate + video bitrate per resolution) instead of raw encoder values. The
|
# H264_720P_30 preset (1280x720, 30fps, 3000 kbps). Enable and tune to reduce
|
||||||
# start-recording API accepts a pair per recording, e.g.
|
# file size and CPU load on the egress worker.
|
||||||
# options.encoding={"resolution": "720p", "profile": "talking_heads"}; only keys
|
# RECORDING_ENCODING_ENABLED=False
|
||||||
# declared in the two maps below are accepted, "profile" is optional.
|
# RECORDING_ENCODING_WIDTH=1280
|
||||||
# Both maps are read as a one-line Python dict literal (ast.literal_eval): double
|
# RECORDING_ENCODING_HEIGHT=720
|
||||||
# quoted keys, no outer quotes, no trailing comma. Every profile must define a
|
# RECORDING_ENCODING_FRAMERATE=30
|
||||||
# kbps entry for exactly the resolutions of the resolutions map, or startup fails.
|
# RECORDING_ENCODING_VIDEO_BITRATE_KBPS=3000
|
||||||
# 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
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ from django.core.exceptions import SuspiciousOperation
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from django_pydantic_field.rest_framework import SchemaField
|
from django_pydantic_field.rest_framework import SchemaField
|
||||||
from pydantic import (
|
from pydantic import BaseModel, Field, field_serializer
|
||||||
BaseModel,
|
|
||||||
Field,
|
|
||||||
field_serializer,
|
|
||||||
field_validator,
|
|
||||||
)
|
|
||||||
from pydantic import ValidationError as PydanticValidationError
|
from pydantic import ValidationError as PydanticValidationError
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
@@ -249,49 +244,6 @@ class BaseValidationOnlySerializer(serializers.Serializer):
|
|||||||
raise NotImplementedError(f"{self.__class__.__name__} is validation-only")
|
raise NotImplementedError(f"{self.__class__.__name__} is validation-only")
|
||||||
|
|
||||||
|
|
||||||
class EncodingConfig(BaseModel):
|
|
||||||
"""Configuration options for recording encoding.
|
|
||||||
|
|
||||||
The allowed `resolution` and `profile` values are derived at validation time
|
|
||||||
from ``settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS`` and
|
|
||||||
``settings.RECORDING_ENCODING_AVAILABLE_PROFILES``, so adding a resolution or profile
|
|
||||||
to those maps is enough to make it accepted here.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
resolution: Target video resolution.
|
|
||||||
profile: Encoding profile to balance quality and CPU usage. When `None`,
|
|
||||||
LiveKit default framerate/bitrate are used for the resolution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
resolution: str
|
|
||||||
profile: str | None = None
|
|
||||||
model_config = {"extra": "forbid"}
|
|
||||||
|
|
||||||
@field_validator("resolution")
|
|
||||||
@classmethod
|
|
||||||
def _validate_resolution(cls, value):
|
|
||||||
"""Reject resolutions absent from RECORDING_ENCODING_AVAILABLE_RESOLUTIONS."""
|
|
||||||
allowed = set(settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS)
|
|
||||||
if value not in allowed:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid resolution '{value}'. Choose from {sorted(allowed)}."
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
@field_validator("profile")
|
|
||||||
@classmethod
|
|
||||||
def _validate_profile(cls, value):
|
|
||||||
"""Reject profiles absent from RECORDING_ENCODING_AVAILABLE_PROFILES."""
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
allowed = set(settings.RECORDING_ENCODING_AVAILABLE_PROFILES)
|
|
||||||
if value not in allowed:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid profile '{value}'. Choose from {sorted(allowed)}."
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class RecordingOptions(BaseModel):
|
class RecordingOptions(BaseModel):
|
||||||
"""Configuration options for recording.
|
"""Configuration options for recording.
|
||||||
|
|
||||||
@@ -312,7 +264,7 @@ class RecordingOptions(BaseModel):
|
|||||||
transcribe: bool | None = None
|
transcribe: bool | None = None
|
||||||
collect_metadata: bool | None = None
|
collect_metadata: bool | None = None
|
||||||
original_mode: Literal["screen_recording", "transcript"] | None = None
|
original_mode: Literal["screen_recording", "transcript"] | None = None
|
||||||
encoding: EncodingConfig | None = None
|
|
||||||
model_config = {"extra": "forbid"}
|
model_config = {"extra": "forbid"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ from core.recording.worker.factories import (
|
|||||||
from core.recording.worker.mediator import (
|
from core.recording.worker.mediator import (
|
||||||
WorkerServiceMediator,
|
WorkerServiceMediator,
|
||||||
)
|
)
|
||||||
from core.recording.worker.services import resolve_encoding_config
|
|
||||||
from core.services.invitation import InvitationService
|
from core.services.invitation import InvitationService
|
||||||
from core.services.livekit_events import (
|
from core.services.livekit_events import (
|
||||||
LiveKitEventsService,
|
LiveKitEventsService,
|
||||||
@@ -401,20 +400,12 @@ class RoomViewSet(
|
|||||||
options = serializer.validated_data.get("options")
|
options = serializer.validated_data.get("options")
|
||||||
room = self.get_object()
|
room = self.get_object()
|
||||||
|
|
||||||
options_data = options.model_dump(exclude_none=True) if options else {}
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
recording = models.Recording.objects.create(
|
recording = models.Recording.objects.create(
|
||||||
room=room,
|
room=room,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
options=options_data,
|
options=options.model_dump(exclude_none=True) if options else {},
|
||||||
)
|
)
|
||||||
models.RecordingAccess.objects.create(
|
models.RecordingAccess.objects.create(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
@@ -1085,9 +1076,10 @@ class RecordingViewSet(
|
|||||||
|
|
||||||
def _auth_get_original_url(self, request):
|
def _auth_get_original_url(self, request):
|
||||||
"""
|
"""
|
||||||
Extracts and parses the original URL from the "HTTP_X_ORIGINAL_URL" header.
|
Extracts and parses the original URL from the configured header.
|
||||||
Raises PermissionDenied if the header is missing.
|
Raises PermissionDenied if the header is missing.
|
||||||
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
|
The original url is passed by the reverse proxy in the header named by the
|
||||||
|
MEDIA_AUTH_ORIGINAL_URL_HEADER setting, which defaults to "HTTP_X_ORIGINAL_URL".
|
||||||
See corresponding ingress configuration in Helm chart and read about the
|
See corresponding ingress configuration in Helm chart and read about the
|
||||||
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
||||||
is configured to do this.
|
is configured to do this.
|
||||||
@@ -1097,9 +1089,13 @@ class RecordingViewSet(
|
|||||||
reasons.
|
reasons.
|
||||||
"""
|
"""
|
||||||
# Extract the original URL from the request header
|
# Extract the original URL from the request header
|
||||||
original_url = request.META.get("HTTP_X_ORIGINAL_URL")
|
original_url = request.META.get(settings.MEDIA_AUTH_ORIGINAL_URL_HEADER)
|
||||||
if not original_url:
|
if not original_url:
|
||||||
logger.warning("Missing HTTP_X_ORIGINAL_URL header in subrequest")
|
logger.warning(
|
||||||
|
"Missing %s header in subrequest. Set MEDIA_AUTH_ORIGINAL_URL_HEADER "
|
||||||
|
"to the header your reverse proxy sends.",
|
||||||
|
settings.MEDIA_AUTH_ORIGINAL_URL_HEADER,
|
||||||
|
)
|
||||||
raise drf_exceptions.PermissionDenied()
|
raise drf_exceptions.PermissionDenied()
|
||||||
|
|
||||||
logger.debug("Original url: '%s'", original_url)
|
logger.debug("Original url: '%s'", original_url)
|
||||||
@@ -1424,7 +1420,8 @@ class FileViewSet(
|
|||||||
Authorize access based on the original URL of an Nginx subrequest
|
Authorize access based on the original URL of an Nginx subrequest
|
||||||
and user permissions. Returns a dictionary of URL parameters if authorized.
|
and user permissions. Returns a dictionary of URL parameters if authorized.
|
||||||
|
|
||||||
The original url is passed by nginx in the "HTTP_X_ORIGINAL_URL" header.
|
The original url is passed by the reverse proxy in the header named by the
|
||||||
|
MEDIA_AUTH_ORIGINAL_URL_HEADER setting, which defaults to "HTTP_X_ORIGINAL_URL".
|
||||||
See corresponding ingress configuration in Helm chart and read about the
|
See corresponding ingress configuration in Helm chart and read about the
|
||||||
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
||||||
is configured to do this.
|
is configured to do this.
|
||||||
@@ -1443,9 +1440,13 @@ class FileViewSet(
|
|||||||
- PermissionDenied if authorization fails.
|
- PermissionDenied if authorization fails.
|
||||||
"""
|
"""
|
||||||
# Extract the original URL from the request header
|
# Extract the original URL from the request header
|
||||||
original_url = request.META.get("HTTP_X_ORIGINAL_URL")
|
original_url = request.META.get(settings.MEDIA_AUTH_ORIGINAL_URL_HEADER)
|
||||||
if not original_url:
|
if not original_url:
|
||||||
logger.warning("Missing HTTP_X_ORIGINAL_URL header in subrequest")
|
logger.warning(
|
||||||
|
"Missing %s header in subrequest. Set MEDIA_AUTH_ORIGINAL_URL_HEADER "
|
||||||
|
"to the header your reverse proxy sends.",
|
||||||
|
settings.MEDIA_AUTH_ORIGINAL_URL_HEADER,
|
||||||
|
)
|
||||||
raise drf_exceptions.PermissionDenied()
|
raise drf_exceptions.PermissionDenied()
|
||||||
|
|
||||||
parsed_url = urlparse(original_url)
|
parsed_url = urlparse(original_url)
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
from django.core.exceptions import (
|
||||||
|
ImproperlyConfigured,
|
||||||
|
SuspiciousOperation,
|
||||||
|
ValidationError,
|
||||||
|
)
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from lasuite.oidc_login.backends import (
|
from lasuite.oidc_login.backends import (
|
||||||
@@ -17,6 +21,7 @@ from core.services.marketing import (
|
|||||||
ContactData,
|
ContactData,
|
||||||
get_marketing_service,
|
get_marketing_service,
|
||||||
)
|
)
|
||||||
|
from core.validators import sub_validator
|
||||||
|
|
||||||
|
|
||||||
class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
|
class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
|
||||||
@@ -84,6 +89,19 @@ class OIDCAuthenticationBackend(LaSuiteOIDCAuthenticationBackend):
|
|||||||
|
|
||||||
def get_existing_user(self, sub, email):
|
def get_existing_user(self, sub, email):
|
||||||
"""Fetch existing user by sub or email."""
|
"""Fetch existing user by sub or email."""
|
||||||
|
|
||||||
|
sub = str(sub)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sub_validator(sub)
|
||||||
|
except ValidationError as err:
|
||||||
|
raise SuspiciousOperation(
|
||||||
|
"User info contained an invalid sub claim"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
if len(sub) > 255:
|
||||||
|
raise SuspiciousOperation("User info contained an invalid sub claim")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return User.objects.get(sub=sub)
|
return User.objects.get(sub=sub)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import uuid
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import core.validators
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||||
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||||
('sub', models.CharField(blank=True, help_text='Optional for pending users; required upon account activation. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.', regex='^[\\w.@+-]+\\Z')], verbose_name='sub')),
|
('sub', models.CharField(blank=True, help_text='Optional for pending users; required upon account activation. 255 characters or fewer. Printable ASCII characters only.', max_length=255, null=True, unique=True, validators=[core.validators.sub_validator], verbose_name='sub')),
|
||||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='identity email address')),
|
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='identity email address')),
|
||||||
('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')),
|
('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')),
|
||||||
('language', models.CharField(choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')),
|
('language', models.CharField(choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE, help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from timezone_field import TimeZoneField
|
|||||||
|
|
||||||
from . import fields, utils
|
from . import fields, utils
|
||||||
from .recording.enums import FileExtension
|
from .recording.enums import FileExtension
|
||||||
|
from .validators import sub_validator
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
@@ -145,19 +146,11 @@ class BaseModel(models.Model):
|
|||||||
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||||
"""User model to work with OIDC only authentication."""
|
"""User model to work with OIDC only authentication."""
|
||||||
|
|
||||||
sub_validator = validators.RegexValidator(
|
|
||||||
regex=r"^[\w.@+-]+\Z",
|
|
||||||
message=_(
|
|
||||||
"Enter a valid sub. This value may contain only letters, "
|
|
||||||
"numbers, and @/./+/-/_ characters."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
sub = models.CharField(
|
sub = models.CharField(
|
||||||
_("sub"),
|
_("sub"),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Optional for pending users; required upon account activation. "
|
"Optional for pending users; required upon account activation. "
|
||||||
"255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
"255 characters or fewer. Printable ASCII characters only."
|
||||||
),
|
),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
|
|||||||
@@ -22,37 +22,6 @@ _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"""
|
||||||
@@ -69,14 +38,22 @@ class WorkerServiceConfig:
|
|||||||
|
|
||||||
logger.debug("Loading WorkerServiceConfig from settings.")
|
logger.debug("Loading WorkerServiceConfig from settings.")
|
||||||
|
|
||||||
# Single source of truth for the EncodingOptions kwargs; the services
|
encoding_options: Optional[Dict[str, Any]] = None
|
||||||
# layer only unpacks this dict. Recordings carrying their own encoding
|
if settings.RECORDING_ENCODING_ENABLED:
|
||||||
# resolve it per request and bypass this default.
|
# Single source of truth for the EncodingOptions kwargs:
|
||||||
encoding_options: Optional[Dict[str, Any]] = (
|
# operator-tunable values live in Django settings, codec / frequency
|
||||||
_build_default_encoding_options()
|
# are pinned constants. The services layer only unpacks this dict.
|
||||||
if settings.RECORDING_ENCODING_ENABLED
|
encoding_options = {
|
||||||
else None
|
"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,
|
||||||
|
}
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
output_folder=settings.RECORDING_OUTPUT_FOLDER,
|
output_folder=settings.RECORDING_OUTPUT_FOLDER,
|
||||||
@@ -101,12 +78,7 @@ class WorkerService(Protocol):
|
|||||||
def __init__(self, config: WorkerServiceConfig):
|
def __init__(self, config: WorkerServiceConfig):
|
||||||
"""Initialize the service with the given configuration."""
|
"""Initialize the service with the given configuration."""
|
||||||
|
|
||||||
def start(
|
def start(self, room_id: str, recording_id: str) -> str:
|
||||||
self,
|
|
||||||
room_id: str,
|
|
||||||
recording_id: str,
|
|
||||||
encoding_options: Optional[Dict[str, Any]] = None,
|
|
||||||
) -> str:
|
|
||||||
"""Start a recording for a specified room."""
|
"""Start a recording for a specified room."""
|
||||||
|
|
||||||
def stop(self, worker_id: str) -> str:
|
def stop(self, worker_id: str) -> str:
|
||||||
|
|||||||
@@ -51,11 +51,8 @@ class WorkerServiceMediator:
|
|||||||
raise RecordingStartError()
|
raise RecordingStartError()
|
||||||
|
|
||||||
room_name = str(recording.room.id)
|
room_name = str(recording.room.id)
|
||||||
encoding_options = (recording.options.get("encoding") or {}).get("resolved")
|
|
||||||
try:
|
try:
|
||||||
worker_id = self._worker_service.start(
|
worker_id = self._worker_service.start(room_name, recording.id)
|
||||||
room_name, recording.id, encoding_options=encoding_options
|
|
||||||
)
|
|
||||||
except (WorkerRequestError, WorkerConnectionError, WorkerResponseError) as e:
|
except (WorkerRequestError, WorkerConnectionError, WorkerResponseError) as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to start recording for room %s: %s", recording.room.slug, e
|
"Failed to start recording for room %s: %s", recording.room.slug, e
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from livekit import api as livekit_api
|
from livekit import api as livekit_api
|
||||||
|
|
||||||
@@ -13,41 +11,6 @@ from .exceptions import WorkerConnectionError, WorkerResponseError
|
|||||||
from .factories import WorkerServiceConfig
|
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:
|
|
||||||
dimensions = settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS[resolution]
|
|
||||||
resolved["width"] = dimensions["width"]
|
|
||||||
resolved["height"] = dimensions["height"]
|
|
||||||
|
|
||||||
if resolution and profile:
|
|
||||||
profile_spec = settings.RECORDING_ENCODING_AVAILABLE_PROFILES[profile]
|
|
||||||
resolved["framerate"] = profile_spec["fps"]
|
|
||||||
resolved["video_bitrate"] = profile_spec["kbps"][resolution]
|
|
||||||
|
|
||||||
return resolved
|
|
||||||
|
|
||||||
|
|
||||||
class BaseEgressService:
|
class BaseEgressService:
|
||||||
"""Base egress defining common methods to manage and interact with LiveKit egress processes."""
|
"""Base egress defining common methods to manage and interact with LiveKit egress processes."""
|
||||||
|
|
||||||
@@ -113,7 +76,7 @@ class BaseEgressService:
|
|||||||
|
|
||||||
return "FAILED_TO_STOP"
|
return "FAILED_TO_STOP"
|
||||||
|
|
||||||
def start(self, room_name, recording_id, encoding_options=None):
|
def start(self, room_name, recording_id):
|
||||||
"""Start the egress process for a recording (not implemented in the base class).
|
"""Start the egress process for a recording (not implemented in the base class).
|
||||||
Each derived class must implement this method, providing the necessary parameters for
|
Each derived class must implement this method, providing the necessary parameters for
|
||||||
its specific egress type (e.g. audio_only, streaming output).
|
its specific egress type (e.g. audio_only, streaming output).
|
||||||
@@ -136,24 +99,13 @@ class BaseEgressService:
|
|||||||
|
|
||||||
return livekit_api.EncodingOptions(**opts)
|
return livekit_api.EncodingOptions(**opts)
|
||||||
|
|
||||||
def _resolve_encoding_options(self, encoding_options):
|
|
||||||
"""Build LiveKit EncodingOptions from a resolved per-recording dict, or None.
|
|
||||||
|
|
||||||
``encoding_options`` is the dict persisted by the API in
|
|
||||||
``recording.options["encoding"]["resolved"]``.
|
|
||||||
"""
|
|
||||||
if not encoding_options:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return livekit_api.EncodingOptions(**encoding_options)
|
|
||||||
|
|
||||||
|
|
||||||
class VideoCompositeEgressService(BaseEgressService):
|
class VideoCompositeEgressService(BaseEgressService):
|
||||||
"""Record multiple participant video and audio tracks into a single output '.mp4' file."""
|
"""Record multiple participant video and audio tracks into a single output '.mp4' file."""
|
||||||
|
|
||||||
hrid = "video-recording-composite-livekit-egress"
|
hrid = "video-recording-composite-livekit-egress"
|
||||||
|
|
||||||
def start(self, room_name, recording_id, encoding_options=None):
|
def start(self, room_name, recording_id):
|
||||||
"""Start the video composite egress process for a recording."""
|
"""Start the video composite egress process for a recording."""
|
||||||
|
|
||||||
# Save room's recording as a mp4 video file.
|
# Save room's recording as a mp4 video file.
|
||||||
@@ -174,10 +126,7 @@ class VideoCompositeEgressService(BaseEgressService):
|
|||||||
"layout": "speaker-light",
|
"layout": "speaker-light",
|
||||||
}
|
}
|
||||||
|
|
||||||
advanced = (
|
advanced = self._build_encoding_options()
|
||||||
self._resolve_encoding_options(encoding_options)
|
|
||||||
or self._build_encoding_options()
|
|
||||||
)
|
|
||||||
if advanced is not None:
|
if advanced is not None:
|
||||||
request_kwargs["advanced"] = advanced
|
request_kwargs["advanced"] = advanced
|
||||||
|
|
||||||
@@ -196,13 +145,8 @@ class AudioCompositeEgressService(BaseEgressService):
|
|||||||
|
|
||||||
hrid = "audio-recording-composite-livekit-egress"
|
hrid = "audio-recording-composite-livekit-egress"
|
||||||
|
|
||||||
def start(self, room_name, recording_id, encoding_options=None):
|
def start(self, room_name, recording_id):
|
||||||
"""Start the audio composite egress process for a recording.
|
"""Start the audio composite egress process for a recording."""
|
||||||
|
|
||||||
``encoding_options`` is accepted for signature compatibility with the
|
|
||||||
WorkerService protocol but ignored: audio-only egress has no
|
|
||||||
encoding to configure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Save room's recording as an ogg audio file.
|
# Save room's recording as an ogg audio file.
|
||||||
file_type = livekit_api.EncodedFileType.OGG
|
file_type = livekit_api.EncodedFileType.OGG
|
||||||
|
|||||||
@@ -40,6 +40,111 @@ def test_authentication_getter_existing_user(monkeypatch):
|
|||||||
assert user == db_user
|
assert user == db_user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"sub",
|
||||||
|
[
|
||||||
|
# NUL (U+0000) passes str.isascii() but PostgreSQL text fields
|
||||||
|
# cannot store or compare it (DataError)
|
||||||
|
"auth0|abc\x00def",
|
||||||
|
# lone surrogates cannot be encoded to UTF-8 for the DB lookup
|
||||||
|
# (UnicodeEncodeError), which runs before any model validation
|
||||||
|
"bad\ud800sub",
|
||||||
|
# plainly invalid subs would otherwise escape as ValidationError
|
||||||
|
# on user creation, which mozilla-django-oidc does not catch
|
||||||
|
"\u00e9milie",
|
||||||
|
"a" * 256,
|
||||||
|
# ASCII control characters are rejected by policy
|
||||||
|
"tab\tsub",
|
||||||
|
"del\x7fsub",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_authentication_getter_invalid_sub_rejected_cleanly(monkeypatch, sub):
|
||||||
|
"""
|
||||||
|
Subs that can never be persisted should be rejected with
|
||||||
|
SuspiciousOperation (turned into a clean authentication failure by
|
||||||
|
mozilla-django-oidc) instead of leaking DataError, UnicodeEncodeError
|
||||||
|
or ValidationError as a server error.
|
||||||
|
"""
|
||||||
|
klass = OIDCAuthenticationBackend()
|
||||||
|
|
||||||
|
def get_userinfo_mocked(*args):
|
||||||
|
return {"sub": sub, "email": "john@example.com"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
SuspiciousOperation,
|
||||||
|
match="User info contained an invalid sub claim",
|
||||||
|
):
|
||||||
|
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||||
|
|
||||||
|
assert models.User.objects.exists() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_authentication_getter_numeric_sub(monkeypatch):
|
||||||
|
"""
|
||||||
|
Some providers serialize the sub as a JSON number. It should keep working
|
||||||
|
(CharField coerces it to a string on save) and must not crash the early
|
||||||
|
sub checks in get_existing_user.
|
||||||
|
"""
|
||||||
|
klass = OIDCAuthenticationBackend()
|
||||||
|
|
||||||
|
def get_userinfo_mocked(*args):
|
||||||
|
return {"sub": 12345, "email": "john@example.com"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||||
|
|
||||||
|
user = klass.get_or_create_user(
|
||||||
|
access_token="test-token", id_token=None, payload=None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user.sub == "12345"
|
||||||
|
assert models.User.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_authentication_getter_new_user_auth0_pipe_sub(monkeypatch):
|
||||||
|
"""
|
||||||
|
A first login with an Auth0-style sub containing a pipe ("provider|user-id")
|
||||||
|
should create the user instead of raising a ValidationError.
|
||||||
|
Regression test for https://github.com/suitenumerique/meet/issues/[XXX].
|
||||||
|
"""
|
||||||
|
klass = OIDCAuthenticationBackend()
|
||||||
|
|
||||||
|
def get_userinfo_mocked(*args):
|
||||||
|
return {"sub": "auth0|644c0bc8f1874ef6d339fb34", "email": "john@example.com"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||||
|
|
||||||
|
user = klass.get_or_create_user(
|
||||||
|
access_token="test-token", id_token=None, payload=None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user.sub == "auth0|644c0bc8f1874ef6d339fb34"
|
||||||
|
assert user.email == "john@example.com"
|
||||||
|
assert models.User.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_authentication_getter_existing_user_auth0_pipe_sub(monkeypatch):
|
||||||
|
"""
|
||||||
|
A returning user with an Auth0-style pipe sub should be matched by sub,
|
||||||
|
not duplicated or rejected.
|
||||||
|
"""
|
||||||
|
klass = OIDCAuthenticationBackend()
|
||||||
|
db_user = UserFactory(sub="auth0|644c0bc8f1874ef6d339fb34")
|
||||||
|
|
||||||
|
def get_userinfo_mocked(*args):
|
||||||
|
return {"sub": db_user.sub}
|
||||||
|
|
||||||
|
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||||
|
|
||||||
|
user = klass.get_or_create_user(
|
||||||
|
access_token="test-token", id_token=None, payload=None
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user == db_user
|
||||||
|
assert models.User.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
def test_authentication_getter_new_user_no_email(monkeypatch):
|
def test_authentication_getter_new_user_no_email(monkeypatch):
|
||||||
"""
|
"""
|
||||||
If no user matches, a user should be created.
|
If no user matches, a user should be created.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from urllib.parse import quote, urlparse
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -143,3 +144,59 @@ def test_api_files_media_auth_own_file_deleted():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(MEDIA_AUTH_ORIGINAL_URL_HEADER="HTTP_X_FORWARDED_URI")
|
||||||
|
def test_api_files_media_auth_custom_original_url_header():
|
||||||
|
"""
|
||||||
|
Authorization should honour the configured original-url header.
|
||||||
|
|
||||||
|
Covers the attachment subrequest path, which resolves the header separately
|
||||||
|
from the recording one. Reverse proxies other than nginx-ingress use
|
||||||
|
different headers: Traefik's ForwardAuth sends X-Forwarded-Uri and cannot
|
||||||
|
emit X-Original-URL at all.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
file = factories.FileFactory(
|
||||||
|
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||||
|
update_upload_state=models.FileUploadStateChoices.READY,
|
||||||
|
creator=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
default_storage.save(file.file_key, BytesIO(b"my prose"))
|
||||||
|
|
||||||
|
original_url = f"http://localhost/media/{file.file_key:s}"
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/files/media-auth/", HTTP_X_FORWARDED_URI=original_url
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "AWS4-HMAC-SHA256 Credential=" in response["Authorization"]
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(MEDIA_AUTH_ORIGINAL_URL_HEADER="HTTP_X_FORWARDED_URI")
|
||||||
|
def test_api_files_media_auth_default_header_ignored_when_reconfigured():
|
||||||
|
"""
|
||||||
|
Only the configured header should be honoured, never a hardcoded fallback.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
|
||||||
|
file = factories.FileFactory(
|
||||||
|
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||||
|
update_upload_state=models.FileUploadStateChoices.READY,
|
||||||
|
creator=user,
|
||||||
|
)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
original_url = f"http://localhost/media/{file.file_key:s}"
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/files/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
|
from django.test import override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -282,3 +283,63 @@ def test_api_recordings_media_auth_success_administrator(mode):
|
|||||||
timeout=1,
|
timeout=1,
|
||||||
)
|
)
|
||||||
assert response.content.decode("utf-8") == "my prose"
|
assert response.content.decode("utf-8") == "my prose"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_recordings_media_auth_missing_header():
|
||||||
|
"""
|
||||||
|
Test that a subrequest without the configured original-url header is rejected.
|
||||||
|
"""
|
||||||
|
user = UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.get("/api/v1.0/recordings/media-auth/")
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(MEDIA_AUTH_ORIGINAL_URL_HEADER="HTTP_X_FORWARDED_URI")
|
||||||
|
def test_api_recordings_media_auth_custom_original_url_header():
|
||||||
|
"""
|
||||||
|
Test that the header carrying the original URL can be configured.
|
||||||
|
|
||||||
|
Reverse proxies other than nginx-ingress use different headers: Traefik's
|
||||||
|
ForwardAuth sends X-Forwarded-Uri and cannot emit X-Original-URL at all.
|
||||||
|
"""
|
||||||
|
user = UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
original_url = f"http://localhost/media/recordings/{uuid4()!s}.mp4"
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/recordings/media-auth/", HTTP_X_FORWARDED_URI=original_url
|
||||||
|
)
|
||||||
|
|
||||||
|
# The header was read and parsed: we get as far as looking the recording up,
|
||||||
|
# rather than being rejected for a missing header.
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(MEDIA_AUTH_ORIGINAL_URL_HEADER="HTTP_X_FORWARDED_URI")
|
||||||
|
def test_api_recordings_media_auth_default_header_ignored_when_reconfigured():
|
||||||
|
"""
|
||||||
|
Test that only the configured header is honoured.
|
||||||
|
|
||||||
|
Guards against the header being read from a hardcoded name in parallel with
|
||||||
|
the setting.
|
||||||
|
"""
|
||||||
|
user = UserFactory()
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
original_url = f"http://localhost/media/recordings/{uuid4()!s}.mp4"
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1.0/recordings/media-auth/", HTTP_X_ORIGINAL_URL=original_url
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
"""Tests for the per-recording encoding resolution in BaseEgressService."""
|
|
||||||
|
|
||||||
# pylint: disable=protected-access,redefined-outer-name,unused-argument
|
|
||||||
|
|
||||||
from unittest.mock import Mock
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pydantic import ValidationError as PydanticValidationError
|
|
||||||
|
|
||||||
from core.api.serializers import EncodingConfig
|
|
||||||
from core.recording.worker.services import (
|
|
||||||
VideoCompositeEgressService,
|
|
||||||
resolve_encoding_config,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def make_config():
|
|
||||||
"""Build a minimal WorkerServiceConfig-like mock for service instantiation."""
|
|
||||||
config = Mock()
|
|
||||||
config.bucket_args = {
|
|
||||||
"endpoint": "https://s3.test.com",
|
|
||||||
"access_key": "test_key",
|
|
||||||
"secret": "test_secret",
|
|
||||||
"region": "test-region",
|
|
||||||
"bucket": "test-bucket",
|
|
||||||
"force_path_style": True,
|
|
||||||
}
|
|
||||||
config.encoding_options = None
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def service():
|
|
||||||
"""Return a VideoCompositeEgressService with mocked handle_request."""
|
|
||||||
svc = VideoCompositeEgressService(make_config())
|
|
||||||
svc._handle_request = Mock()
|
|
||||||
return svc
|
|
||||||
|
|
||||||
|
|
||||||
# --- resolve_encoding_config ---
|
|
||||||
|
|
||||||
|
|
||||||
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_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"))
|
|
||||||
|
|
||||||
assert resolved == {
|
|
||||||
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_encoding_config_requires_resolution():
|
|
||||||
"""A profile-only or empty encoding config should be rejected at validation."""
|
|
||||||
with pytest.raises(PydanticValidationError):
|
|
||||||
EncodingConfig(profile="mixed")
|
|
||||||
with pytest.raises(PydanticValidationError):
|
|
||||||
EncodingConfig()
|
|
||||||
|
|
||||||
|
|
||||||
# --- _resolve_encoding_options ---
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("encoding_options", [None, {}])
|
|
||||||
def test_resolve_options_returns_none_when_empty(service, encoding_options):
|
|
||||||
"""Resolver should return None when the resolved dict is empty or missing."""
|
|
||||||
assert service._resolve_encoding_options(encoding_options) is None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"resolution",
|
|
||||||
list(settings.RECORDING_ENCODING_AVAILABLE_RESOLUTIONS),
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"profile",
|
|
||||||
list(settings.RECORDING_ENCODING_AVAILABLE_PROFILES),
|
|
||||||
)
|
|
||||||
def test_resolve_profile_resolution_combinations(service, profile, resolution):
|
|
||||||
"""Every (profile, resolution) pair should resolve to the values from settings."""
|
|
||||||
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 == 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):
|
|
||||||
"""Missing profile should pass 0 fps/bitrate (LiveKit protobuf default)."""
|
|
||||||
resolved = resolve_encoding_config(EncodingConfig(resolution="720p"))
|
|
||||||
result = service._resolve_encoding_options(resolved)
|
|
||||||
assert result.width == 1280
|
|
||||||
assert result.height == 720
|
|
||||||
assert result.framerate == 0
|
|
||||||
assert result.video_bitrate == 0
|
|
||||||
@@ -85,22 +85,18 @@ 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_AVAILABLE_RESOLUTIONS={
|
RECORDING_ENCODING_WIDTH=1280,
|
||||||
"720p": {"width": 1280, "height": 720},
|
RECORDING_ENCODING_HEIGHT=720,
|
||||||
},
|
RECORDING_ENCODING_FRAMERATE=15,
|
||||||
RECORDING_ENCODING_AVAILABLE_PROFILES={
|
RECORDING_ENCODING_VIDEO_BITRATE_KBPS=600,
|
||||||
"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 values resolved from the default resolution / profile with
|
The dict mixes operator-tunable values from settings with pinned codec /
|
||||||
pinned codec / frequency constants, so the services layer can simply unpack it.
|
frequency constants, so the services layer can simply unpack it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
WorkerServiceConfig.from_settings.cache_clear()
|
WorkerServiceConfig.from_settings.cache_clear()
|
||||||
@@ -119,27 +115,6 @@ 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"},
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def test_start_recording_success(mock_update_metadata, mediator, mock_worker_ser
|
|||||||
# Verify worker service call
|
# Verify worker service call
|
||||||
expected_room_name = str(mock_recording.room.id)
|
expected_room_name = str(mock_recording.room.id)
|
||||||
mock_worker_service.start.assert_called_once_with(
|
mock_worker_service.start.assert_called_once_with(
|
||||||
expected_room_name, mock_recording.id, encoding_options=None
|
expected_room_name, mock_recording.id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify recording updates
|
# Verify recording updates
|
||||||
@@ -64,38 +64,6 @@ def test_start_recording_success(mock_update_metadata, mediator, mock_worker_ser
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@mock.patch("core.utils.update_room_metadata")
|
|
||||||
def test_start_recording_passes_resolved_encoding(
|
|
||||||
mock_update_room_metadata, mediator, mock_worker_service
|
|
||||||
):
|
|
||||||
"""The resolved encoding persisted in recording.options reaches the worker."""
|
|
||||||
mock_worker_service.start.return_value = "test-worker-123"
|
|
||||||
|
|
||||||
resolved = {
|
|
||||||
"key_frame_interval": 4.0,
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"framerate": 15,
|
|
||||||
"video_bitrate": 700,
|
|
||||||
}
|
|
||||||
mock_recording = RecordingFactory(
|
|
||||||
status=RecordingStatusChoices.INITIATED,
|
|
||||||
worker_id=None,
|
|
||||||
options={
|
|
||||||
"encoding": {
|
|
||||||
"resolution": "720p",
|
|
||||||
"profile": "talking_heads",
|
|
||||||
"resolved": resolved,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
mediator.start(mock_recording)
|
|
||||||
|
|
||||||
mock_worker_service.start.assert_called_once_with(
|
|
||||||
str(mock_recording.room.id), mock_recording.id, encoding_options=resolved
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"error_class", [WorkerRequestError, WorkerConnectionError, WorkerResponseError]
|
"error_class", [WorkerRequestError, WorkerConnectionError, WorkerResponseError]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -470,160 +470,6 @@ def test_start_recording_options_unknown_field_rejected(settings):
|
|||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
def test_start_recording_options_encoding_valid(
|
|
||||||
settings, mock_worker_service_factory, mock_worker_manager
|
|
||||||
):
|
|
||||||
"""Should accept a valid encoding configuration."""
|
|
||||||
settings.RECORDING_ENABLE = True
|
|
||||||
room = RoomFactory()
|
|
||||||
user = UserFactory()
|
|
||||||
room.accesses.create(user=user, role="owner")
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
|
||||||
{
|
|
||||||
"mode": "screen_recording",
|
|
||||||
"options": {"encoding": {"resolution": "720p", "profile": "talking_heads"}},
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_recording_persists_resolved_encoding(
|
|
||||||
settings, mock_worker_service_factory, mock_worker_manager
|
|
||||||
):
|
|
||||||
"""The resolved encoding should be persisted in recording.options alongside
|
|
||||||
the requested resolution/profile for traceability."""
|
|
||||||
settings.RECORDING_ENABLE = True
|
|
||||||
room = RoomFactory()
|
|
||||||
user = UserFactory()
|
|
||||||
room.accesses.create(user=user, role="owner")
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
|
||||||
{
|
|
||||||
"mode": "screen_recording",
|
|
||||||
"options": {"encoding": {"resolution": "720p", "profile": "talking_heads"}},
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
recording = Recording.objects.get(room=room)
|
|
||||||
assert recording.options["encoding"] == {
|
|
||||||
"resolution": "720p",
|
|
||||||
"profile": "talking_heads",
|
|
||||||
"resolved": {
|
|
||||||
"key_frame_interval": settings.RECORDING_ENCODING_KEY_FRAME_INTERVAL_S,
|
|
||||||
"width": 1280,
|
|
||||||
"height": 720,
|
|
||||||
"framerate": 15,
|
|
||||||
"video_bitrate": 700,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_recording_forwards_resolved_encoding_to_worker(
|
|
||||||
settings, mock_worker_service, mock_worker_service_factory
|
|
||||||
):
|
|
||||||
"""The resolved encoding should passed on to the worker."""
|
|
||||||
settings.RECORDING_ENABLE = True
|
|
||||||
room = RoomFactory()
|
|
||||||
user = UserFactory()
|
|
||||||
room.accesses.create(user=user, role="owner")
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
mock_worker_service.start.return_value = "egress-123"
|
|
||||||
|
|
||||||
with mock.patch("core.utils.update_room_metadata"):
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
|
||||||
{
|
|
||||||
"mode": "screen_recording",
|
|
||||||
"options": {
|
|
||||||
"encoding": {"resolution": "720p", "profile": "talking_heads"}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
recording = Recording.objects.get(room=room)
|
|
||||||
mock_worker_service.start.assert_called_once_with(
|
|
||||||
str(room.id),
|
|
||||||
recording.id,
|
|
||||||
encoding_options=recording.options["encoding"]["resolved"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_recording_options_encoding_invalid_resolution(settings):
|
|
||||||
"""Should reject invalid encoding resolution values."""
|
|
||||||
settings.RECORDING_ENABLE = True
|
|
||||||
room = RoomFactory()
|
|
||||||
user = UserFactory()
|
|
||||||
room.accesses.create(user=user, role="owner")
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
|
||||||
{"mode": "screen_recording", "options": {"encoding": {"resolution": "4K"}}},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_recording_options_encoding_unknown_key_rejected(settings):
|
|
||||||
"""Should reject unknown keys in encoding configuration."""
|
|
||||||
settings.RECORDING_ENABLE = True
|
|
||||||
room = RoomFactory()
|
|
||||||
user = UserFactory()
|
|
||||||
room.accesses.create(user=user, role="owner")
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
|
||||||
{
|
|
||||||
"mode": "screen_recording",
|
|
||||||
"options": {"encoding": {"bitrate": 9000}},
|
|
||||||
},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 400
|
|
||||||
|
|
||||||
|
|
||||||
def test_start_recording_options_without_encoding_unchanged(
|
|
||||||
settings, mock_worker_service_factory, mock_worker_manager
|
|
||||||
):
|
|
||||||
"""Requests without encoding should keep existing options behavior."""
|
|
||||||
settings.RECORDING_ENABLE = True
|
|
||||||
room = RoomFactory()
|
|
||||||
user = UserFactory()
|
|
||||||
room.accesses.create(user=user, role="owner")
|
|
||||||
client = APIClient()
|
|
||||||
client.force_login(user)
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
f"/api/v1.0/rooms/{room.id}/start-recording/",
|
|
||||||
{"mode": "screen_recording", "options": {"language": "fr"}},
|
|
||||||
format="json",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 201
|
|
||||||
recording = Recording.objects.get(room=room)
|
|
||||||
assert recording.options == {"language": "fr"}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value", ["foo", 12])
|
@pytest.mark.parametrize("value", ["foo", 12])
|
||||||
def test_start_recording_options_invalid_transcribe_type(settings, value):
|
def test_start_recording_options_invalid_transcribe_type(settings, value):
|
||||||
"""Should reject non-boolean transcribe values."""
|
"""Should reject non-boolean transcribe values."""
|
||||||
|
|||||||
@@ -26,6 +26,64 @@ def test_models_users_id_unique():
|
|||||||
factories.UserFactory(id=user.id)
|
factories.UserFactory(id=user.id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"sub,is_valid",
|
||||||
|
[
|
||||||
|
# cases from suitenumerique/docs PR #1295 (same validator)
|
||||||
|
("valid_sub.@+-:=/", True),
|
||||||
|
("invalid süb", False),
|
||||||
|
(12345, True),
|
||||||
|
# Auth0 emits "provider|user-id" subject identifiers
|
||||||
|
("auth0|644c0bc8f1874ef6d339fb34", True),
|
||||||
|
("google-oauth2|103547991597142817347", True),
|
||||||
|
# Keycloak-style UUID
|
||||||
|
("f:550e8400-e29b-41d4-a716-446655440000:jdoe", True),
|
||||||
|
# base64/URN-style identifiers
|
||||||
|
("dGVzdC1zdWItdmFsdWU=", True),
|
||||||
|
("urn:example:user/42", True),
|
||||||
|
# legacy format still accepted
|
||||||
|
("user@example.com", True),
|
||||||
|
# space (U+0020) is printable ASCII and remains allowed
|
||||||
|
("sub with space", True),
|
||||||
|
# non-ASCII values are rejected
|
||||||
|
("émilie", False),
|
||||||
|
# ASCII control characters (U+0000-U+001F, U+007F) are rejected:
|
||||||
|
# NUL passes isascii() but cannot be stored in PostgreSQL text
|
||||||
|
# fields, and the others invite log injection and interop issues
|
||||||
|
("nul\x00sub", False),
|
||||||
|
("\x00", False),
|
||||||
|
("tab\tsub", False),
|
||||||
|
("newline\nsub", False),
|
||||||
|
("del\x7fsub", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_models_users_sub_validator(sub, is_valid):
|
||||||
|
"""
|
||||||
|
The "sub" field should accept any ASCII string as required by
|
||||||
|
OpenID Connect Core 1.0 §2 and RFC 7519 §4.1.2, and reject non-ASCII values.
|
||||||
|
"""
|
||||||
|
user = factories.UserFactory()
|
||||||
|
user.sub = sub
|
||||||
|
if is_valid:
|
||||||
|
user.full_clean()
|
||||||
|
else:
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationError,
|
||||||
|
match="Enter a valid sub. This value should be printable ASCII only.",
|
||||||
|
):
|
||||||
|
user.full_clean()
|
||||||
|
|
||||||
|
|
||||||
|
def test_models_users_sub_max_length():
|
||||||
|
"""The "sub" field should enforce the 255 ASCII characters limit of OIDC Core 1.0 §2."""
|
||||||
|
user = factories.UserFactory(sub="a" * 255)
|
||||||
|
assert user.sub == "a" * 255
|
||||||
|
|
||||||
|
user.sub = "a" * 256
|
||||||
|
with pytest.raises(ValidationError, match="at most 255 characters"):
|
||||||
|
user.full_clean()
|
||||||
|
|
||||||
|
|
||||||
def test_models_users_send_mail_main_existing():
|
def test_models_users_send_mail_main_existing():
|
||||||
"""The "email_user' method should send mail to the user's email address."""
|
"""The "email_user' method should send mail to the user's email address."""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""Custom validators for the core app."""
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
def sub_validator(value):
|
||||||
|
"""Validate that the sub is printable ASCII only.
|
||||||
|
|
||||||
|
OpenID Connect Core 1.0 (section 2) allows any ASCII (RFC 20) string of
|
||||||
|
at most 255 characters, so no character whitelist is applied: providers
|
||||||
|
legitimately emit "|" (Auth0), ":" (Keycloak), "=", "/", etc. As a
|
||||||
|
deliberate hardening beyond the spec, ASCII control characters
|
||||||
|
(U+0000-U+001F and U+007F) are rejected: no known provider emits them,
|
||||||
|
NUL cannot be stored in PostgreSQL text fields, and the others invite
|
||||||
|
log-injection and interoperability issues. For str values,
|
||||||
|
``isprintable()`` is false exactly for those control characters, while
|
||||||
|
space (U+0020) remains allowed.
|
||||||
|
"""
|
||||||
|
if not value.isascii() or not value.isprintable():
|
||||||
|
raise ValidationError(
|
||||||
|
_("Enter a valid sub. This value should be printable ASCII only.")
|
||||||
|
)
|
||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-02 10:47+0000\n"
|
"POT-Creation-Date: 2026-09-02 22:52+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -51,11 +51,11 @@ msgstr ""
|
|||||||
msgid "File preview"
|
msgid "File preview"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/admin.py:300 core/admin.py:443
|
#: core/admin.py:300 core/admin.py:450
|
||||||
msgid "No owner"
|
msgid "No owner"
|
||||||
msgstr "Kein Eigentümer"
|
msgstr "Kein Eigentümer"
|
||||||
|
|
||||||
#: core/admin.py:303 core/admin.py:446
|
#: core/admin.py:303 core/admin.py:453
|
||||||
msgid "Multiple owners"
|
msgid "Multiple owners"
|
||||||
msgstr "Mehrere Eigentümer"
|
msgstr "Mehrere Eigentümer"
|
||||||
|
|
||||||
@@ -98,11 +98,11 @@ msgstr "%(count)s Aufnahme(n) erfolgreich als ‚Fehler beim Stoppen‘ markiert
|
|||||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||||
msgstr "%(count)s abgelaufene Aufnahme(n) übersprungen."
|
msgstr "%(count)s abgelaufene Aufnahme(n) übersprungen."
|
||||||
|
|
||||||
#: core/admin.py:510
|
#: core/admin.py:517
|
||||||
msgid "No scopes"
|
msgid "No scopes"
|
||||||
msgstr "Keine Scopes"
|
msgstr "Keine Scopes"
|
||||||
|
|
||||||
#: core/admin.py:512
|
#: core/admin.py:519
|
||||||
msgid "Scopes"
|
msgid "Scopes"
|
||||||
msgstr "Scopes"
|
msgstr "Scopes"
|
||||||
|
|
||||||
@@ -110,185 +110,201 @@ msgstr "Scopes"
|
|||||||
msgid "Creator is me"
|
msgid "Creator is me"
|
||||||
msgstr "Ersteller bin ich"
|
msgstr "Ersteller bin ich"
|
||||||
|
|
||||||
#: core/api/serializers.py:89
|
#: core/api/serializers.py:108
|
||||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
|
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
|
||||||
"hinzuzufügen."
|
"hinzuzufügen."
|
||||||
|
|
||||||
#: core/api/serializers.py:534
|
#: core/api/serializers.py:560
|
||||||
msgid "This file extension is not allowed."
|
msgid "This file extension is not allowed."
|
||||||
msgstr "Diese Dateiendung ist nicht erlaubt."
|
msgstr "Diese Dateiendung ist nicht erlaubt."
|
||||||
|
|
||||||
#: core/api/viewsets.py:1222
|
#: core/api/viewsets.py:1268
|
||||||
msgid "You have reached the maximum number of files for this type."
|
msgid "You have reached the maximum number of files for this type."
|
||||||
msgstr "Sie haben die maximale Anzahl an Dateien dieses Typs erreicht."
|
msgstr "Sie haben die maximale Anzahl an Dateien dieses Typs erreicht."
|
||||||
|
|
||||||
#: core/models.py:37
|
#: core/models.py:38
|
||||||
msgid "Member"
|
msgid "Member"
|
||||||
msgstr "Mitglied"
|
msgstr "Mitglied"
|
||||||
|
|
||||||
#: core/models.py:38
|
#: core/models.py:39
|
||||||
msgid "Administrator"
|
msgid "Administrator"
|
||||||
msgstr "Administrator"
|
msgstr "Administrator"
|
||||||
|
|
||||||
#: core/models.py:39
|
#: core/models.py:40
|
||||||
msgid "Owner"
|
msgid "Owner"
|
||||||
msgstr "Eigentümer"
|
msgstr "Eigentümer"
|
||||||
|
|
||||||
#: core/models.py:55
|
#: core/models.py:56
|
||||||
msgid "Initiated"
|
msgid "Initiated"
|
||||||
msgstr "Gestartet"
|
msgstr "Gestartet"
|
||||||
|
|
||||||
#: core/models.py:56
|
#: core/models.py:57
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr "Aktiv"
|
msgstr "Aktiv"
|
||||||
|
|
||||||
#: core/models.py:57
|
#: core/models.py:58
|
||||||
msgid "Stopped"
|
msgid "Stopped"
|
||||||
msgstr "Beendet"
|
msgstr "Beendet"
|
||||||
|
|
||||||
#: core/models.py:58
|
#: core/models.py:59
|
||||||
msgid "Saved"
|
msgid "Saved"
|
||||||
msgstr "Gespeichert"
|
msgstr "Gespeichert"
|
||||||
|
|
||||||
#: core/models.py:59
|
#: core/models.py:60
|
||||||
msgid "Aborted"
|
msgid "Aborted"
|
||||||
msgstr "Abgebrochen"
|
msgstr "Abgebrochen"
|
||||||
|
|
||||||
#: core/models.py:60
|
#: core/models.py:61
|
||||||
msgid "Failed to Start"
|
msgid "Failed to Start"
|
||||||
msgstr "Start fehlgeschlagen"
|
msgstr "Start fehlgeschlagen"
|
||||||
|
|
||||||
#: core/models.py:61
|
#: core/models.py:62
|
||||||
msgid "Failed to Stop"
|
msgid "Failed to Stop"
|
||||||
msgstr "Stopp fehlgeschlagen"
|
msgstr "Stopp fehlgeschlagen"
|
||||||
|
|
||||||
#: core/models.py:62
|
#: core/models.py:63
|
||||||
msgid "Notification succeeded"
|
msgid "Notification succeeded"
|
||||||
msgstr "Benachrichtigung erfolgreich"
|
msgstr "Benachrichtigung erfolgreich"
|
||||||
|
|
||||||
#: core/models.py:65
|
#: core/models.py:66
|
||||||
msgid "External process successful"
|
msgid "External process successful"
|
||||||
msgstr "Externer Prozess erfolgreich"
|
msgstr "Externer Prozess erfolgreich"
|
||||||
|
|
||||||
#: core/models.py:67
|
#: core/models.py:68
|
||||||
msgid "External process failed"
|
msgid "External process failed"
|
||||||
msgstr "Externer Prozess fehlgeschlagen"
|
msgstr "Externer Prozess fehlgeschlagen"
|
||||||
|
|
||||||
#: core/models.py:96
|
#: core/models.py:97
|
||||||
msgid "SCREEN_RECORDING"
|
msgid "SCREEN_RECORDING"
|
||||||
msgstr "BILDSCHIRMAUFZEICHNUNG"
|
msgstr "BILDSCHIRMAUFZEICHNUNG"
|
||||||
|
|
||||||
#: core/models.py:97
|
#: core/models.py:98
|
||||||
msgid "TRANSCRIPT"
|
msgid "TRANSCRIPT"
|
||||||
msgstr "TRANSKRIPT"
|
msgstr "TRANSKRIPT"
|
||||||
|
|
||||||
#: core/models.py:103
|
#: core/models.py:104
|
||||||
msgid "Public Access"
|
msgid "Public Access"
|
||||||
msgstr "Öffentlicher Zugriff"
|
msgstr "Öffentlicher Zugriff"
|
||||||
|
|
||||||
#: core/models.py:104
|
#: core/models.py:105
|
||||||
msgid "Trusted Access"
|
msgid "Trusted Access"
|
||||||
msgstr "Vertrauenswürdiger Zugriff"
|
msgstr "Vertrauenswürdiger Zugriff"
|
||||||
|
|
||||||
#: core/models.py:105
|
#: core/models.py:106
|
||||||
msgid "Restricted Access"
|
msgid "Restricted Access"
|
||||||
msgstr "Eingeschränkter Zugriff"
|
msgstr "Eingeschränkter Zugriff"
|
||||||
|
|
||||||
#: core/models.py:117
|
#: core/models.py:118
|
||||||
msgid "id"
|
msgid "id"
|
||||||
msgstr "ID"
|
msgstr "ID"
|
||||||
|
|
||||||
#: core/models.py:118
|
#: core/models.py:119
|
||||||
msgid "primary key for the record as UUID"
|
msgid "primary key for the record as UUID"
|
||||||
msgstr "Primärschlüssel des Eintrags als UUID"
|
msgstr "Primärschlüssel des Eintrags als UUID"
|
||||||
|
|
||||||
#: core/models.py:124
|
#: core/models.py:125
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr "erstellt am"
|
msgstr "erstellt am"
|
||||||
|
|
||||||
#: core/models.py:125
|
#: core/models.py:126
|
||||||
msgid "date and time at which a record was created"
|
msgid "date and time at which a record was created"
|
||||||
msgstr "Datum und Uhrzeit der Erstellung eines Eintrags"
|
msgstr "Datum und Uhrzeit der Erstellung eines Eintrags"
|
||||||
|
|
||||||
#: core/models.py:130
|
#: core/models.py:131
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
msgstr "aktualisiert am"
|
msgstr "aktualisiert am"
|
||||||
|
|
||||||
#: core/models.py:131
|
#: core/models.py:132
|
||||||
msgid "date and time at which a record was last updated"
|
msgid "date and time at which a record was last updated"
|
||||||
msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags"
|
msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags"
|
||||||
|
|
||||||
#: core/models.py:151
|
#: core/models.py:150
|
||||||
msgid ""
|
|
||||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
|
||||||
"_ characters."
|
|
||||||
msgstr ""
|
|
||||||
"Geben Sie einen gültigen Sub ein. Dieser Wert darf nur Buchstaben, Zahlen "
|
|
||||||
"und die Zeichen @/./+/-/_ enthalten."
|
|
||||||
|
|
||||||
#: core/models.py:157
|
|
||||||
msgid "sub"
|
msgid "sub"
|
||||||
msgstr "Sub"
|
msgstr "Sub"
|
||||||
|
|
||||||
#: core/models.py:159
|
#: core/models.py:152
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optional for pending users; required upon account activation. 255 characters "
|
"Optional for pending users; required upon account activation. 255 characters "
|
||||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
"or fewer. Printable ASCII characters only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Optional für ausstehende Benutzer; erforderlich nach Kontoaktivierung. "
|
"Optional für ausstehende Benutzer; erforderlich nach Kontoaktivierung. "
|
||||||
"Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ Zeichen erlaubt."
|
"Maximal 255 Zeichen. Nur druckbare ASCII-Zeichen erlaubt."
|
||||||
|
|
||||||
#: core/models.py:168
|
#: core/validators.py:22
|
||||||
|
msgid "Enter a valid sub. This value should be printable ASCII only."
|
||||||
|
msgstr "Geben Sie einen gültigen sub ein. Dieser Wert darf nur druckbare ASCII-Zeichen enthalten."
|
||||||
|
|
||||||
|
#: core/models.py:161
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "Identitäts-E-Mail-Adresse"
|
msgstr "Identitäts-E-Mail-Adresse"
|
||||||
|
|
||||||
#: core/models.py:173
|
#: core/models.py:166
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "Administrator-E-Mail-Adresse"
|
msgstr "Administrator-E-Mail-Adresse"
|
||||||
|
|
||||||
#: core/models.py:175
|
#: core/models.py:168
|
||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "Vollständiger Name"
|
msgstr "Vollständiger Name"
|
||||||
|
|
||||||
#: core/models.py:177
|
#: core/models.py:170
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "Kurzname"
|
msgstr "Kurzname"
|
||||||
|
|
||||||
#: core/models.py:183
|
#: core/models.py:176
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "Sprache"
|
msgstr "Sprache"
|
||||||
|
|
||||||
#: core/models.py:184
|
#: core/models.py:177
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte."
|
msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte."
|
||||||
|
|
||||||
#: core/models.py:190
|
#: core/models.py:183
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte."
|
msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte."
|
||||||
|
|
||||||
#: core/models.py:193
|
#: core/models.py:190
|
||||||
|
msgid "default room access level"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:192
|
||||||
|
msgid ""
|
||||||
|
"Access level applied by default to new rooms created by this user. When "
|
||||||
|
"empty, the instance default is used."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:199
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Visio room configuration"
|
||||||
|
msgid "default room configuration"
|
||||||
|
msgstr "Visio-Raumkonfiguration"
|
||||||
|
|
||||||
|
#: core/models.py:201
|
||||||
|
msgid "Configurations applied by default to new rooms created by this user."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:205
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "Gerät"
|
msgstr "Gerät"
|
||||||
|
|
||||||
#: core/models.py:195
|
#: core/models.py:207
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt."
|
msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt."
|
||||||
|
|
||||||
#: core/models.py:198
|
#: core/models.py:210
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "Mitarbeiterstatus"
|
msgstr "Mitarbeiterstatus"
|
||||||
|
|
||||||
#: core/models.py:200
|
#: core/models.py:212
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann."
|
msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann."
|
||||||
|
|
||||||
#: core/models.py:203
|
#: core/models.py:215
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "aktiv"
|
msgstr "aktiv"
|
||||||
|
|
||||||
#: core/models.py:206
|
#: core/models.py:218
|
||||||
msgid ""
|
msgid ""
|
||||||
"Whether this user should be treated as active. Unselect this instead of "
|
"Whether this user should be treated as active. Unselect this instead of "
|
||||||
"deleting accounts."
|
"deleting accounts."
|
||||||
@@ -296,66 +312,66 @@ msgstr ""
|
|||||||
"Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies "
|
"Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies "
|
||||||
"anstelle des Löschens des Kontos."
|
"anstelle des Löschens des Kontos."
|
||||||
|
|
||||||
#: core/models.py:219
|
#: core/models.py:231
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "Benutzer"
|
msgstr "Benutzer"
|
||||||
|
|
||||||
#: core/models.py:220
|
#: core/models.py:232
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "Benutzer"
|
msgstr "Benutzer"
|
||||||
|
|
||||||
#: core/models.py:286
|
#: core/models.py:298
|
||||||
msgid "Resource"
|
msgid "Resource"
|
||||||
msgstr "Ressource"
|
msgstr "Ressource"
|
||||||
|
|
||||||
#: core/models.py:287
|
#: core/models.py:299
|
||||||
msgid "Resources"
|
msgid "Resources"
|
||||||
msgstr "Ressourcen"
|
msgstr "Ressourcen"
|
||||||
|
|
||||||
#: core/models.py:345
|
#: core/models.py:357
|
||||||
msgid "Resource access"
|
msgid "Resource access"
|
||||||
msgstr "Ressourcenzugriff"
|
msgstr "Ressourcenzugriff"
|
||||||
|
|
||||||
#: core/models.py:346
|
#: core/models.py:358
|
||||||
msgid "Resource accesses"
|
msgid "Resource accesses"
|
||||||
msgstr "Ressourcenzugriffe"
|
msgstr "Ressourcenzugriffe"
|
||||||
|
|
||||||
#: core/models.py:352
|
#: core/models.py:364
|
||||||
msgid "Resource access with this User and Resource already exists."
|
msgid "Resource access with this User and Resource already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert "
|
"Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert "
|
||||||
"bereits."
|
"bereits."
|
||||||
|
|
||||||
#: core/models.py:409
|
#: core/models.py:421
|
||||||
msgid "Visio room configuration"
|
msgid "Visio room configuration"
|
||||||
msgstr "Visio-Raumkonfiguration"
|
msgstr "Visio-Raumkonfiguration"
|
||||||
|
|
||||||
#: core/models.py:410
|
#: core/models.py:422
|
||||||
msgid "Values for Visio parameters to configure the room."
|
msgid "Values for Visio parameters to configure the room."
|
||||||
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
|
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
|
||||||
|
|
||||||
#: core/models.py:417
|
#: core/models.py:429
|
||||||
msgid "Room PIN code"
|
msgid "Room PIN code"
|
||||||
msgstr "PIN-Code für den Raum"
|
msgstr "PIN-Code für den Raum"
|
||||||
|
|
||||||
#: core/models.py:418
|
#: core/models.py:430
|
||||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
|
"Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
|
||||||
|
|
||||||
#: core/models.py:424 core/models.py:578
|
#: core/models.py:436 core/models.py:597
|
||||||
msgid "Room"
|
msgid "Room"
|
||||||
msgstr "Raum"
|
msgstr "Raum"
|
||||||
|
|
||||||
#: core/models.py:425
|
#: core/models.py:437
|
||||||
msgid "Rooms"
|
msgid "Rooms"
|
||||||
msgstr "Räume"
|
msgstr "Räume"
|
||||||
|
|
||||||
#: core/models.py:589
|
#: core/models.py:608
|
||||||
msgid "Worker ID"
|
msgid "Worker ID"
|
||||||
msgstr "Worker-ID"
|
msgstr "Worker-ID"
|
||||||
|
|
||||||
#: core/models.py:591
|
#: core/models.py:610
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||||
"the worker stops, allowing for easy tracking."
|
"the worker stops, allowing for easy tracking."
|
||||||
@@ -364,153 +380,153 @@ msgstr ""
|
|||||||
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
|
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
|
||||||
"ermöglicht."
|
"ermöglicht."
|
||||||
|
|
||||||
#: core/models.py:599
|
#: core/models.py:618
|
||||||
msgid "Recording mode"
|
msgid "Recording mode"
|
||||||
msgstr "Aufzeichnungsmodus"
|
msgstr "Aufzeichnungsmodus"
|
||||||
|
|
||||||
#: core/models.py:600
|
#: core/models.py:619
|
||||||
msgid "Defines the mode of recording being called."
|
msgid "Defines the mode of recording being called."
|
||||||
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
|
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
|
||||||
|
|
||||||
#: core/models.py:605 core/models.py:606
|
#: core/models.py:624 core/models.py:625
|
||||||
msgid "Recording options"
|
msgid "Recording options"
|
||||||
msgstr "Aufnahmeoptionen"
|
msgstr "Aufnahmeoptionen"
|
||||||
|
|
||||||
#: core/models.py:613
|
#: core/models.py:632
|
||||||
msgid "External Process ID"
|
msgid "External Process ID"
|
||||||
msgstr "External Process ID"
|
msgstr "External Process ID"
|
||||||
|
|
||||||
#: core/models.py:614
|
#: core/models.py:633
|
||||||
msgid "ID of the external process associated with the recording."
|
msgid "ID of the external process associated with the recording."
|
||||||
msgstr "ID des externen Prozesses, der mit der Aufzeichnung verknüpft ist"
|
msgstr "ID des externen Prozesses, der mit der Aufzeichnung verknüpft ist"
|
||||||
|
|
||||||
#: core/models.py:620
|
#: core/models.py:639
|
||||||
msgid "Recording"
|
msgid "Recording"
|
||||||
msgstr "Aufzeichnung"
|
msgstr "Aufzeichnung"
|
||||||
|
|
||||||
#: core/models.py:621
|
#: core/models.py:640
|
||||||
msgid "Recordings"
|
msgid "Recordings"
|
||||||
msgstr "Aufzeichnungen"
|
msgstr "Aufzeichnungen"
|
||||||
|
|
||||||
#: core/models.py:731
|
#: core/models.py:750
|
||||||
msgid "Recording/user relation"
|
msgid "Recording/user relation"
|
||||||
msgstr "Beziehung Aufzeichnung/Benutzer"
|
msgstr "Beziehung Aufzeichnung/Benutzer"
|
||||||
|
|
||||||
#: core/models.py:732
|
#: core/models.py:751
|
||||||
msgid "Recording/user relations"
|
msgid "Recording/user relations"
|
||||||
msgstr "Beziehungen Aufzeichnung/Benutzer"
|
msgstr "Beziehungen Aufzeichnung/Benutzer"
|
||||||
|
|
||||||
#: core/models.py:738
|
#: core/models.py:757
|
||||||
msgid "This user is already in this recording."
|
msgid "This user is already in this recording."
|
||||||
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
|
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
|
||||||
|
|
||||||
#: core/models.py:744
|
#: core/models.py:763
|
||||||
msgid "This team is already in this recording."
|
msgid "This team is already in this recording."
|
||||||
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
|
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
|
||||||
|
|
||||||
#: core/models.py:750
|
#: core/models.py:769
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
|
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
|
||||||
|
|
||||||
#: core/models.py:767
|
#: core/models.py:786
|
||||||
msgid "Create rooms"
|
msgid "Create rooms"
|
||||||
msgstr "Räume erstellen"
|
msgstr "Räume erstellen"
|
||||||
|
|
||||||
#: core/models.py:768
|
#: core/models.py:787
|
||||||
msgid "List rooms"
|
msgid "List rooms"
|
||||||
msgstr "Räume auflisten"
|
msgstr "Räume auflisten"
|
||||||
|
|
||||||
#: core/models.py:769
|
#: core/models.py:788
|
||||||
msgid "Retrieve room details"
|
msgid "Retrieve room details"
|
||||||
msgstr "Raumdetails abrufen"
|
msgstr "Raumdetails abrufen"
|
||||||
|
|
||||||
#: core/models.py:770
|
#: core/models.py:789
|
||||||
msgid "Update rooms"
|
msgid "Update rooms"
|
||||||
msgstr "Räume aktualisieren"
|
msgstr "Räume aktualisieren"
|
||||||
|
|
||||||
#: core/models.py:771
|
#: core/models.py:790
|
||||||
msgid "Delete rooms"
|
msgid "Delete rooms"
|
||||||
msgstr "Räume löschen"
|
msgstr "Räume löschen"
|
||||||
|
|
||||||
#: core/models.py:784
|
#: core/models.py:803
|
||||||
msgid "Application name"
|
msgid "Application name"
|
||||||
msgstr "Anwendungsname"
|
msgstr "Anwendungsname"
|
||||||
|
|
||||||
#: core/models.py:785
|
#: core/models.py:804
|
||||||
msgid "Descriptive name for this application."
|
msgid "Descriptive name for this application."
|
||||||
msgstr "Beschreibender Name für diese Anwendung."
|
msgstr "Beschreibender Name für diese Anwendung."
|
||||||
|
|
||||||
#: core/models.py:795
|
#: core/models.py:814
|
||||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Beim Speichern gehasht. Jetzt kopieren, wenn dies ein neues Geheimnis ist."
|
"Beim Speichern gehasht. Jetzt kopieren, wenn dies ein neues Geheimnis ist."
|
||||||
|
|
||||||
#: core/models.py:806
|
#: core/models.py:825
|
||||||
msgid "Application"
|
msgid "Application"
|
||||||
msgstr "Anwendung"
|
msgstr "Anwendung"
|
||||||
|
|
||||||
#: core/models.py:807
|
#: core/models.py:826
|
||||||
msgid "Applications"
|
msgid "Applications"
|
||||||
msgstr "Anwendungen"
|
msgstr "Anwendungen"
|
||||||
|
|
||||||
#: core/models.py:830
|
#: core/models.py:849
|
||||||
msgid "Enter a valid domain"
|
msgid "Enter a valid domain"
|
||||||
msgstr "Geben Sie eine gültige Domain ein"
|
msgstr "Geben Sie eine gültige Domain ein"
|
||||||
|
|
||||||
#: core/models.py:833
|
#: core/models.py:852
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr "Domain"
|
msgstr "Domain"
|
||||||
|
|
||||||
#: core/models.py:834
|
#: core/models.py:853
|
||||||
msgid "Email domain this application can act on behalf of."
|
msgid "Email domain this application can act on behalf of."
|
||||||
msgstr "E-Mail-Domain, im Namen der diese Anwendung handeln kann."
|
msgstr "E-Mail-Domain, im Namen der diese Anwendung handeln kann."
|
||||||
|
|
||||||
#: core/models.py:846
|
#: core/models.py:865
|
||||||
msgid "Application domain"
|
msgid "Application domain"
|
||||||
msgstr "Anwendungsdomain"
|
msgstr "Anwendungsdomain"
|
||||||
|
|
||||||
#: core/models.py:847
|
#: core/models.py:866
|
||||||
msgid "Application domains"
|
msgid "Application domains"
|
||||||
msgstr "Anwendungsdomains"
|
msgstr "Anwendungsdomains"
|
||||||
|
|
||||||
#: core/models.py:865
|
#: core/models.py:884
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr "Ausstehend"
|
msgstr "Ausstehend"
|
||||||
|
|
||||||
#: core/models.py:866
|
#: core/models.py:885
|
||||||
msgid "Analyzing"
|
msgid "Analyzing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/models.py:873
|
#: core/models.py:892
|
||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr "Bereit"
|
msgstr "Bereit"
|
||||||
|
|
||||||
#: core/models.py:879
|
#: core/models.py:898
|
||||||
msgid "Background image"
|
msgid "Background image"
|
||||||
msgstr "Hintergrundbild"
|
msgstr "Hintergrundbild"
|
||||||
|
|
||||||
#: core/models.py:891
|
#: core/models.py:910
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "Titel"
|
msgstr "Titel"
|
||||||
|
|
||||||
#: core/models.py:915
|
#: core/models.py:934
|
||||||
msgid "Malware detection info when the analysis status is unsafe."
|
msgid "Malware detection info when the analysis status is unsafe."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Informationen zur Malware-Erkennung, wenn der Analyse-Status unsicher ist."
|
"Informationen zur Malware-Erkennung, wenn der Analyse-Status unsicher ist."
|
||||||
|
|
||||||
#: core/models.py:920
|
#: core/models.py:939
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "Datei"
|
msgstr "Datei"
|
||||||
|
|
||||||
#: core/models.py:921
|
#: core/models.py:940
|
||||||
msgid "Files"
|
msgid "Files"
|
||||||
msgstr "Dateien"
|
msgstr "Dateien"
|
||||||
|
|
||||||
#: core/models.py:1041
|
#: core/models.py:1060
|
||||||
msgid "This file is already hard deleted."
|
msgid "This file is already hard deleted."
|
||||||
msgstr "Diese Datei wurde bereits endgültig gelöscht."
|
msgstr "Diese Datei wurde bereits endgültig gelöscht."
|
||||||
|
|
||||||
#: core/models.py:1051
|
#: core/models.py:1070
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "To hard delete a file, it must first be soft deleted."
|
#| msgid "To hard delete a file, it must first be soft deleted."
|
||||||
msgid "To hard delete a file, it must first be soft deleted."
|
msgid "To hard delete a file, it must first be soft deleted."
|
||||||
@@ -518,15 +534,15 @@ msgstr ""
|
|||||||
"Um eine Datei endgültig zu löschen, muss sie zuvor weich gelöscht worden "
|
"Um eine Datei endgültig zu löschen, muss sie zuvor weich gelöscht worden "
|
||||||
"sein."
|
"sein."
|
||||||
|
|
||||||
#: core/recording/event/notification.py:123
|
#: core/recording/event/notification.py:124
|
||||||
msgid "Your recording is ready"
|
msgid "Your recording is ready"
|
||||||
msgstr "Ihre Aufzeichnung ist bereit"
|
msgstr "Ihre Aufzeichnung ist bereit"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:194
|
#: core/recording/event/notification.py:195
|
||||||
msgid "Transcription"
|
msgid "Transcription"
|
||||||
msgstr "Transkription"
|
msgstr "Transkription"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:204
|
#: core/recording/event/notification.py:206
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -537,25 +553,25 @@ msgstr ""
|
|||||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||||
msgstr "Videoanruf läuft: {sender.email} wartet auf Ihre Teilnahme"
|
msgstr "Videoanruf läuft: {sender.email} wartet auf Ihre Teilnahme"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:159
|
#: core/templates/mail/html/invitation.html:151
|
||||||
#: core/templates/mail/html/screen_recording.html:159
|
#: core/templates/mail/html/screen_recording.html:151
|
||||||
#: core/templates/mail/text/invitation.txt:3
|
#: core/templates/mail/text/invitation.txt:4
|
||||||
#: core/templates/mail/text/screen_recording.txt:3
|
#: core/templates/mail/text/screen_recording.txt:4
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo-E-Mail"
|
msgstr "Logo-E-Mail"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:189
|
#: core/templates/mail/html/invitation.html:181
|
||||||
#: core/templates/mail/text/invitation.txt:5
|
#: core/templates/mail/text/invitation.txt:6
|
||||||
msgid "invites you to join an ongoing video call"
|
msgid "invites you to join an ongoing video call"
|
||||||
msgstr "lädt Sie zu einem laufenden Videoanruf ein"
|
msgstr "lädt Sie zu einem laufenden Videoanruf ein"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:200
|
#: core/templates/mail/html/invitation.html:192
|
||||||
#: core/templates/mail/text/invitation.txt:7
|
#: core/templates/mail/text/invitation.txt:8
|
||||||
msgid "JOIN THE CALL"
|
msgid "JOIN THE CALL"
|
||||||
msgstr "AM ANRUF TEILNEHMEN"
|
msgstr "AM ANRUF TEILNEHMEN"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:227
|
#: core/templates/mail/html/invitation.html:219
|
||||||
#: core/templates/mail/text/invitation.txt:13
|
#: core/templates/mail/text/invitation.txt:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you can't click the button, copy and paste the URL into your browser to "
|
"If you can't click the button, copy and paste the URL into your browser to "
|
||||||
"join the call."
|
"join the call."
|
||||||
@@ -563,41 +579,47 @@ msgstr ""
|
|||||||
"Wenn Sie den Button nicht anklicken können, kopieren Sie die URL und fügen "
|
"Wenn Sie den Button nicht anklicken können, kopieren Sie die URL und fügen "
|
||||||
"Sie sie in Ihren Browser ein, um am Anruf teilzunehmen."
|
"Sie sie in Ihren Browser ein, um am Anruf teilzunehmen."
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:235
|
#: core/templates/mail/html/invitation.html:227
|
||||||
#: core/templates/mail/text/invitation.txt:15
|
#: core/templates/mail/text/invitation.txt:16
|
||||||
msgid "Tips for a better experience:"
|
msgid "Tips for a better experience:"
|
||||||
msgstr "Tipps für ein besseres Erlebnis:"
|
msgstr "Tipps für ein besseres Erlebnis:"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:237
|
#: core/templates/mail/html/invitation.html:229
|
||||||
#: core/templates/mail/text/invitation.txt:17
|
#: core/templates/mail/text/invitation.txt:18
|
||||||
msgid "Use Chrome or Firefox for better call quality"
|
msgid "Use Chrome or Firefox for better call quality"
|
||||||
msgstr "Verwenden Sie Chrome oder Firefox für eine bessere Anrufqualität"
|
msgstr "Verwenden Sie Chrome oder Firefox für eine bessere Anrufqualität"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:238
|
#: core/templates/mail/html/invitation.html:230
|
||||||
#: core/templates/mail/text/invitation.txt:18
|
#: core/templates/mail/text/invitation.txt:19
|
||||||
msgid "Test your microphone and camera before joining"
|
msgid "Test your microphone and camera before joining"
|
||||||
msgstr "Testen Sie Ihr Mikrofon und Ihre Kamera vor dem Beitritt"
|
msgstr "Testen Sie Ihr Mikrofon und Ihre Kamera vor dem Beitritt"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:239
|
#: core/templates/mail/html/invitation.html:231
|
||||||
#: core/templates/mail/text/invitation.txt:19
|
#: core/templates/mail/text/invitation.txt:20
|
||||||
msgid "Make sure you have a stable internet connection"
|
msgid "Make sure you have a stable internet connection"
|
||||||
msgstr "Stellen Sie sicher, dass Sie eine stabile Internetverbindung haben"
|
msgstr "Stellen Sie sicher, dass Sie eine stabile Internetverbindung haben"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:248
|
#: core/templates/mail/html/invitation.html:240
|
||||||
#: core/templates/mail/html/screen_recording.html:245
|
#: core/templates/mail/html/screen_recording.html:237
|
||||||
#: core/templates/mail/text/invitation.txt:21
|
#: core/templates/mail/text/invitation.txt:22
|
||||||
#: core/templates/mail/text/screen_recording.txt:23
|
#: core/templates/mail/text/screen_recording.txt:24
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Thank you for using %(brandname)s. "
|
msgid " Thank you for using %(brandname)s. "
|
||||||
msgstr " Vielen Dank für die Nutzung von %(brandname)s. "
|
msgstr " Vielen Dank für die Nutzung von %(brandname)s. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:188
|
#: core/templates/mail/html/invitation.html:271
|
||||||
#: core/templates/mail/text/screen_recording.txt:6
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/html/screen_recording.html:180
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:7
|
||||||
msgid "Your recording is ready!"
|
msgid "Your recording is ready!"
|
||||||
msgstr "Ihre Aufzeichnung ist fertig!"
|
msgstr "Ihre Aufzeichnung ist fertig!"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
||||||
@@ -606,14 +628,14 @@ msgstr ""
|
|||||||
" Ihre Aufzeichnung von \"%(room_name)s\" am %(recording_date)s um "
|
" Ihre Aufzeichnung von \"%(room_name)s\" am %(recording_date)s um "
|
||||||
"%(recording_time)s steht nun zum Herunterladen bereit. "
|
"%(recording_time)s steht nun zum Herunterladen bereit. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " The recording will expire in %(days)s days. "
|
msgid " The recording will expire in %(days)s days. "
|
||||||
msgstr " Die Aufzeichnung wird in %(days)s Tagen ablaufen. "
|
msgstr " Die Aufzeichnung wird in %(days)s Tagen ablaufen. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:200
|
#: core/templates/mail/html/screen_recording.html:192
|
||||||
#: core/templates/mail/text/screen_recording.txt:9
|
#: core/templates/mail/text/screen_recording.txt:10
|
||||||
msgid ""
|
msgid ""
|
||||||
" Sharing the recording via link is not yet available. Only organizers can "
|
" Sharing the recording via link is not yet available. Only organizers can "
|
||||||
"download it. "
|
"download it. "
|
||||||
@@ -621,33 +643,33 @@ msgstr ""
|
|||||||
" Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur "
|
" Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur "
|
||||||
"Organisatoren können sie herunterladen. "
|
"Organisatoren können sie herunterladen. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:206
|
#: core/templates/mail/html/screen_recording.html:198
|
||||||
#: core/templates/mail/text/screen_recording.txt:11
|
#: core/templates/mail/text/screen_recording.txt:12
|
||||||
msgid "To keep this recording permanently:"
|
msgid "To keep this recording permanently:"
|
||||||
msgstr "So speichern Sie diese Aufzeichnung dauerhaft:"
|
msgstr "So speichern Sie diese Aufzeichnung dauerhaft:"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:208
|
#: core/templates/mail/html/screen_recording.html:200
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgstr "Klicken Sie auf den Link „<a href=\"%(link)s\">Öffnen</a>\" unten "
|
msgstr "Klicken Sie auf den Link „<a href=\"%(link)s\">Öffnen</a>\" unten "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:209
|
#: core/templates/mail/html/screen_recording.html:201
|
||||||
#: core/templates/mail/text/screen_recording.txt:14
|
#: core/templates/mail/text/screen_recording.txt:15
|
||||||
msgid "Use the \"Download\" button in the interface "
|
msgid "Use the \"Download\" button in the interface "
|
||||||
msgstr "Verwenden Sie den Button „Herunterladen“ in der Oberfläche "
|
msgstr "Verwenden Sie den Button „Herunterladen“ in der Oberfläche "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:210
|
#: core/templates/mail/html/screen_recording.html:202
|
||||||
#: core/templates/mail/text/screen_recording.txt:15
|
#: core/templates/mail/text/screen_recording.txt:16
|
||||||
msgid "Save the file to your preferred location"
|
msgid "Save the file to your preferred location"
|
||||||
msgstr "Speichern Sie die Datei an einem gewünschten Ort"
|
msgstr "Speichern Sie die Datei an einem gewünschten Ort"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:221
|
#: core/templates/mail/html/screen_recording.html:213
|
||||||
#: core/templates/mail/text/screen_recording.txt:17
|
#: core/templates/mail/text/screen_recording.txt:18
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Öffnen"
|
msgstr "Öffnen"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:230
|
#: core/templates/mail/html/screen_recording.html:222
|
||||||
#: core/templates/mail/text/screen_recording.txt:19
|
#: core/templates/mail/text/screen_recording.txt:20
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" If you have any questions or need assistance, please contact our support "
|
" If you have any questions or need assistance, please contact our support "
|
||||||
@@ -656,28 +678,33 @@ msgstr ""
|
|||||||
" Wenn Sie Fragen haben oder Unterstützung benötigen, wenden Sie sich bitte "
|
" Wenn Sie Fragen haben oder Unterstützung benötigen, wenden Sie sich bitte "
|
||||||
"an unser Support-Team unter %(support_email)s. "
|
"an unser Support-Team unter %(support_email)s. "
|
||||||
|
|
||||||
#: core/templates/mail/text/screen_recording.txt:13
|
#: core/templates/mail/text/invitation.txt:24
|
||||||
|
#, python-format
|
||||||
|
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:14
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgid "Click the \"Open [%(link)s]\" link below "
|
msgid "Click the \"Open [%(link)s]\" link below "
|
||||||
msgstr "Klicken Sie auf den Link „<a href=\"%(link)s\">Öffnen</a>\" unten "
|
msgstr "Klicken Sie auf den Link „<a href=\"%(link)s\">Öffnen</a>\" unten "
|
||||||
|
|
||||||
#: meet/settings.py:228
|
#: meet/settings.py:238
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "Englisch"
|
msgstr "Englisch"
|
||||||
|
|
||||||
#: meet/settings.py:229
|
#: meet/settings.py:239
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr "Französisch"
|
msgstr "Französisch"
|
||||||
|
|
||||||
#: meet/settings.py:230
|
#: meet/settings.py:240
|
||||||
msgid "Dutch"
|
msgid "Dutch"
|
||||||
msgstr "Niederländisch"
|
msgstr "Niederländisch"
|
||||||
|
|
||||||
#: meet/settings.py:231
|
#: meet/settings.py:241
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr "Deutsch"
|
msgstr "Deutsch"
|
||||||
|
|
||||||
#: meet/settings.py:233
|
#: meet/settings.py:242
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
msgstr "Spanisch"
|
msgstr "Spanisch"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-02 10:47+0000\n"
|
"POT-Creation-Date: 2026-09-02 22:52+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -51,11 +51,11 @@ msgstr ""
|
|||||||
msgid "File preview"
|
msgid "File preview"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/admin.py:300 core/admin.py:443
|
#: core/admin.py:300 core/admin.py:450
|
||||||
msgid "No owner"
|
msgid "No owner"
|
||||||
msgstr "No owner"
|
msgstr "No owner"
|
||||||
|
|
||||||
#: core/admin.py:303 core/admin.py:446
|
#: core/admin.py:303 core/admin.py:453
|
||||||
msgid "Multiple owners"
|
msgid "Multiple owners"
|
||||||
msgstr "Multiple owners"
|
msgstr "Multiple owners"
|
||||||
|
|
||||||
@@ -98,11 +98,11 @@ msgstr "%(count)s recording(s) successfully marked as 'Failed to Stop'."
|
|||||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||||
msgstr "Skipped %(count)s expired recording(s)."
|
msgstr "Skipped %(count)s expired recording(s)."
|
||||||
|
|
||||||
#: core/admin.py:510
|
#: core/admin.py:517
|
||||||
msgid "No scopes"
|
msgid "No scopes"
|
||||||
msgstr "No scopes"
|
msgstr "No scopes"
|
||||||
|
|
||||||
#: core/admin.py:512
|
#: core/admin.py:519
|
||||||
msgid "Scopes"
|
msgid "Scopes"
|
||||||
msgstr "Scopes"
|
msgstr "Scopes"
|
||||||
|
|
||||||
@@ -110,183 +110,199 @@ msgstr "Scopes"
|
|||||||
msgid "Creator is me"
|
msgid "Creator is me"
|
||||||
msgstr "Creator is me"
|
msgstr "Creator is me"
|
||||||
|
|
||||||
#: core/api/serializers.py:89
|
#: core/api/serializers.py:108
|
||||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||||
msgstr "You must be administrator or owner of a room to add accesses to it."
|
msgstr "You must be administrator or owner of a room to add accesses to it."
|
||||||
|
|
||||||
#: core/api/serializers.py:534
|
#: core/api/serializers.py:560
|
||||||
msgid "This file extension is not allowed."
|
msgid "This file extension is not allowed."
|
||||||
msgstr "This file extension is not allowed."
|
msgstr "This file extension is not allowed."
|
||||||
|
|
||||||
#: core/api/viewsets.py:1222
|
#: core/api/viewsets.py:1268
|
||||||
msgid "You have reached the maximum number of files for this type."
|
msgid "You have reached the maximum number of files for this type."
|
||||||
msgstr "You have reached the maximum number of files for this type."
|
msgstr "You have reached the maximum number of files for this type."
|
||||||
|
|
||||||
#: core/models.py:37
|
#: core/models.py:38
|
||||||
msgid "Member"
|
msgid "Member"
|
||||||
msgstr "Member"
|
msgstr "Member"
|
||||||
|
|
||||||
#: core/models.py:38
|
#: core/models.py:39
|
||||||
msgid "Administrator"
|
msgid "Administrator"
|
||||||
msgstr "Administrator"
|
msgstr "Administrator"
|
||||||
|
|
||||||
#: core/models.py:39
|
#: core/models.py:40
|
||||||
msgid "Owner"
|
msgid "Owner"
|
||||||
msgstr "Owner"
|
msgstr "Owner"
|
||||||
|
|
||||||
#: core/models.py:55
|
#: core/models.py:56
|
||||||
msgid "Initiated"
|
msgid "Initiated"
|
||||||
msgstr "Initiated"
|
msgstr "Initiated"
|
||||||
|
|
||||||
#: core/models.py:56
|
#: core/models.py:57
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr "Active"
|
msgstr "Active"
|
||||||
|
|
||||||
#: core/models.py:57
|
#: core/models.py:58
|
||||||
msgid "Stopped"
|
msgid "Stopped"
|
||||||
msgstr "Stopped"
|
msgstr "Stopped"
|
||||||
|
|
||||||
#: core/models.py:58
|
#: core/models.py:59
|
||||||
msgid "Saved"
|
msgid "Saved"
|
||||||
msgstr "Saved"
|
msgstr "Saved"
|
||||||
|
|
||||||
#: core/models.py:59
|
#: core/models.py:60
|
||||||
msgid "Aborted"
|
msgid "Aborted"
|
||||||
msgstr "Aborted"
|
msgstr "Aborted"
|
||||||
|
|
||||||
#: core/models.py:60
|
#: core/models.py:61
|
||||||
msgid "Failed to Start"
|
msgid "Failed to Start"
|
||||||
msgstr "Failed to Start"
|
msgstr "Failed to Start"
|
||||||
|
|
||||||
#: core/models.py:61
|
#: core/models.py:62
|
||||||
msgid "Failed to Stop"
|
msgid "Failed to Stop"
|
||||||
msgstr "Failed to Stop"
|
msgstr "Failed to Stop"
|
||||||
|
|
||||||
#: core/models.py:62
|
#: core/models.py:63
|
||||||
msgid "Notification succeeded"
|
msgid "Notification succeeded"
|
||||||
msgstr "Notification succeeded"
|
msgstr "Notification succeeded"
|
||||||
|
|
||||||
#: core/models.py:65
|
#: core/models.py:66
|
||||||
msgid "External process successful"
|
msgid "External process successful"
|
||||||
msgstr "External process successful"
|
msgstr "External process successful"
|
||||||
|
|
||||||
#: core/models.py:67
|
#: core/models.py:68
|
||||||
msgid "External process failed"
|
msgid "External process failed"
|
||||||
msgstr "External process failed"
|
msgstr "External process failed"
|
||||||
|
|
||||||
#: core/models.py:96
|
#: core/models.py:97
|
||||||
msgid "SCREEN_RECORDING"
|
msgid "SCREEN_RECORDING"
|
||||||
msgstr "SCREEN_RECORDING"
|
msgstr "SCREEN_RECORDING"
|
||||||
|
|
||||||
#: core/models.py:97
|
#: core/models.py:98
|
||||||
msgid "TRANSCRIPT"
|
msgid "TRANSCRIPT"
|
||||||
msgstr "TRANSCRIPT"
|
msgstr "TRANSCRIPT"
|
||||||
|
|
||||||
#: core/models.py:103
|
#: core/models.py:104
|
||||||
msgid "Public Access"
|
msgid "Public Access"
|
||||||
msgstr "Public Access"
|
msgstr "Public Access"
|
||||||
|
|
||||||
#: core/models.py:104
|
#: core/models.py:105
|
||||||
msgid "Trusted Access"
|
msgid "Trusted Access"
|
||||||
msgstr "Trusted Access"
|
msgstr "Trusted Access"
|
||||||
|
|
||||||
#: core/models.py:105
|
#: core/models.py:106
|
||||||
msgid "Restricted Access"
|
msgid "Restricted Access"
|
||||||
msgstr "Restricted Access"
|
msgstr "Restricted Access"
|
||||||
|
|
||||||
#: core/models.py:117
|
#: core/models.py:118
|
||||||
msgid "id"
|
msgid "id"
|
||||||
msgstr "id"
|
msgstr "id"
|
||||||
|
|
||||||
#: core/models.py:118
|
#: core/models.py:119
|
||||||
msgid "primary key for the record as UUID"
|
msgid "primary key for the record as UUID"
|
||||||
msgstr "primary key for the record as UUID"
|
msgstr "primary key for the record as UUID"
|
||||||
|
|
||||||
#: core/models.py:124
|
#: core/models.py:125
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr "created on"
|
msgstr "created on"
|
||||||
|
|
||||||
#: core/models.py:125
|
#: core/models.py:126
|
||||||
msgid "date and time at which a record was created"
|
msgid "date and time at which a record was created"
|
||||||
msgstr "date and time at which a record was created"
|
msgstr "date and time at which a record was created"
|
||||||
|
|
||||||
#: core/models.py:130
|
#: core/models.py:131
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
msgstr "updated on"
|
msgstr "updated on"
|
||||||
|
|
||||||
#: core/models.py:131
|
#: core/models.py:132
|
||||||
msgid "date and time at which a record was last updated"
|
msgid "date and time at which a record was last updated"
|
||||||
msgstr "date and time at which a record was last updated"
|
msgstr "date and time at which a record was last updated"
|
||||||
|
|
||||||
#: core/models.py:151
|
#: core/models.py:150
|
||||||
msgid ""
|
|
||||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
|
||||||
"_ characters."
|
|
||||||
msgstr ""
|
|
||||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
|
||||||
"_ characters."
|
|
||||||
|
|
||||||
#: core/models.py:157
|
|
||||||
msgid "sub"
|
msgid "sub"
|
||||||
msgstr "sub"
|
msgstr "sub"
|
||||||
|
|
||||||
#: core/models.py:159
|
#: core/models.py:152
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optional for pending users; required upon account activation. 255 characters "
|
"Optional for pending users; required upon account activation. 255 characters "
|
||||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
"or fewer. Printable ASCII characters only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
"Optional for pending users; required upon account activation. 255 characters "
|
||||||
"characters only."
|
"or fewer. Printable ASCII characters only."
|
||||||
|
|
||||||
#: core/models.py:168
|
#: core/validators.py:22
|
||||||
|
msgid "Enter a valid sub. This value should be printable ASCII only."
|
||||||
|
msgstr "Enter a valid sub. This value should be printable ASCII only."
|
||||||
|
|
||||||
|
#: core/models.py:161
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "identity email address"
|
msgstr "identity email address"
|
||||||
|
|
||||||
#: core/models.py:173
|
#: core/models.py:166
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "admin email address"
|
msgstr "admin email address"
|
||||||
|
|
||||||
#: core/models.py:175
|
#: core/models.py:168
|
||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "full name"
|
msgstr "full name"
|
||||||
|
|
||||||
#: core/models.py:177
|
#: core/models.py:170
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "short name"
|
msgstr "short name"
|
||||||
|
|
||||||
#: core/models.py:183
|
#: core/models.py:176
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "language"
|
msgstr "language"
|
||||||
|
|
||||||
#: core/models.py:184
|
#: core/models.py:177
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "The language in which the user wants to see the interface."
|
msgstr "The language in which the user wants to see the interface."
|
||||||
|
|
||||||
#: core/models.py:190
|
#: core/models.py:183
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "The timezone in which the user wants to see times."
|
msgstr "The timezone in which the user wants to see times."
|
||||||
|
|
||||||
#: core/models.py:193
|
#: core/models.py:190
|
||||||
|
msgid "default room access level"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:192
|
||||||
|
msgid ""
|
||||||
|
"Access level applied by default to new rooms created by this user. When "
|
||||||
|
"empty, the instance default is used."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:199
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Visio room configuration"
|
||||||
|
msgid "default room configuration"
|
||||||
|
msgstr "Visio room configuration"
|
||||||
|
|
||||||
|
#: core/models.py:201
|
||||||
|
msgid "Configurations applied by default to new rooms created by this user."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:205
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "device"
|
msgstr "device"
|
||||||
|
|
||||||
#: core/models.py:195
|
#: core/models.py:207
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Whether the user is a device or a real user."
|
msgstr "Whether the user is a device or a real user."
|
||||||
|
|
||||||
#: core/models.py:198
|
#: core/models.py:210
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "staff status"
|
msgstr "staff status"
|
||||||
|
|
||||||
#: core/models.py:200
|
#: core/models.py:212
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Whether the user can log into this admin site."
|
msgstr "Whether the user can log into this admin site."
|
||||||
|
|
||||||
#: core/models.py:203
|
#: core/models.py:215
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "active"
|
msgstr "active"
|
||||||
|
|
||||||
#: core/models.py:206
|
#: core/models.py:218
|
||||||
msgid ""
|
msgid ""
|
||||||
"Whether this user should be treated as active. Unselect this instead of "
|
"Whether this user should be treated as active. Unselect this instead of "
|
||||||
"deleting accounts."
|
"deleting accounts."
|
||||||
@@ -294,63 +310,63 @@ msgstr ""
|
|||||||
"Whether this user should be treated as active. Unselect this instead of "
|
"Whether this user should be treated as active. Unselect this instead of "
|
||||||
"deleting accounts."
|
"deleting accounts."
|
||||||
|
|
||||||
#: core/models.py:219
|
#: core/models.py:231
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "user"
|
msgstr "user"
|
||||||
|
|
||||||
#: core/models.py:220
|
#: core/models.py:232
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "users"
|
msgstr "users"
|
||||||
|
|
||||||
#: core/models.py:286
|
#: core/models.py:298
|
||||||
msgid "Resource"
|
msgid "Resource"
|
||||||
msgstr "Resource"
|
msgstr "Resource"
|
||||||
|
|
||||||
#: core/models.py:287
|
#: core/models.py:299
|
||||||
msgid "Resources"
|
msgid "Resources"
|
||||||
msgstr "Resources"
|
msgstr "Resources"
|
||||||
|
|
||||||
#: core/models.py:345
|
#: core/models.py:357
|
||||||
msgid "Resource access"
|
msgid "Resource access"
|
||||||
msgstr "Resource access"
|
msgstr "Resource access"
|
||||||
|
|
||||||
#: core/models.py:346
|
#: core/models.py:358
|
||||||
msgid "Resource accesses"
|
msgid "Resource accesses"
|
||||||
msgstr "Resource accesses"
|
msgstr "Resource accesses"
|
||||||
|
|
||||||
#: core/models.py:352
|
#: core/models.py:364
|
||||||
msgid "Resource access with this User and Resource already exists."
|
msgid "Resource access with this User and Resource already exists."
|
||||||
msgstr "Resource access with this User and Resource already exists."
|
msgstr "Resource access with this User and Resource already exists."
|
||||||
|
|
||||||
#: core/models.py:409
|
#: core/models.py:421
|
||||||
msgid "Visio room configuration"
|
msgid "Visio room configuration"
|
||||||
msgstr "Visio room configuration"
|
msgstr "Visio room configuration"
|
||||||
|
|
||||||
#: core/models.py:410
|
#: core/models.py:422
|
||||||
msgid "Values for Visio parameters to configure the room."
|
msgid "Values for Visio parameters to configure the room."
|
||||||
msgstr "Values for Visio parameters to configure the room."
|
msgstr "Values for Visio parameters to configure the room."
|
||||||
|
|
||||||
#: core/models.py:417
|
#: core/models.py:429
|
||||||
msgid "Room PIN code"
|
msgid "Room PIN code"
|
||||||
msgstr "Room PIN code"
|
msgstr "Room PIN code"
|
||||||
|
|
||||||
#: core/models.py:418
|
#: core/models.py:430
|
||||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||||
msgstr "Unique n-digit code that identifies this room in telephony mode."
|
msgstr "Unique n-digit code that identifies this room in telephony mode."
|
||||||
|
|
||||||
#: core/models.py:424 core/models.py:578
|
#: core/models.py:436 core/models.py:597
|
||||||
msgid "Room"
|
msgid "Room"
|
||||||
msgstr "Room"
|
msgstr "Room"
|
||||||
|
|
||||||
#: core/models.py:425
|
#: core/models.py:437
|
||||||
msgid "Rooms"
|
msgid "Rooms"
|
||||||
msgstr "Rooms"
|
msgstr "Rooms"
|
||||||
|
|
||||||
#: core/models.py:589
|
#: core/models.py:608
|
||||||
msgid "Worker ID"
|
msgid "Worker ID"
|
||||||
msgstr "Worker ID"
|
msgstr "Worker ID"
|
||||||
|
|
||||||
#: core/models.py:591
|
#: core/models.py:610
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||||
"the worker stops, allowing for easy tracking."
|
"the worker stops, allowing for easy tracking."
|
||||||
@@ -358,171 +374,171 @@ msgstr ""
|
|||||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||||
"the worker stops, allowing for easy tracking."
|
"the worker stops, allowing for easy tracking."
|
||||||
|
|
||||||
#: core/models.py:599
|
#: core/models.py:618
|
||||||
msgid "Recording mode"
|
msgid "Recording mode"
|
||||||
msgstr "Recording mode"
|
msgstr "Recording mode"
|
||||||
|
|
||||||
#: core/models.py:600
|
#: core/models.py:619
|
||||||
msgid "Defines the mode of recording being called."
|
msgid "Defines the mode of recording being called."
|
||||||
msgstr "Defines the mode of recording being called."
|
msgstr "Defines the mode of recording being called."
|
||||||
|
|
||||||
#: core/models.py:605 core/models.py:606
|
#: core/models.py:624 core/models.py:625
|
||||||
msgid "Recording options"
|
msgid "Recording options"
|
||||||
msgstr "Recording options"
|
msgstr "Recording options"
|
||||||
|
|
||||||
#: core/models.py:613
|
#: core/models.py:632
|
||||||
msgid "External Process ID"
|
msgid "External Process ID"
|
||||||
msgstr "External Process ID"
|
msgstr "External Process ID"
|
||||||
|
|
||||||
#: core/models.py:614
|
#: core/models.py:633
|
||||||
msgid "ID of the external process associated with the recording."
|
msgid "ID of the external process associated with the recording."
|
||||||
msgstr "ID of the external process associated with the recording."
|
msgstr "ID of the external process associated with the recording."
|
||||||
|
|
||||||
#: core/models.py:620
|
#: core/models.py:639
|
||||||
msgid "Recording"
|
msgid "Recording"
|
||||||
msgstr "Recording"
|
msgstr "Recording"
|
||||||
|
|
||||||
#: core/models.py:621
|
#: core/models.py:640
|
||||||
msgid "Recordings"
|
msgid "Recordings"
|
||||||
msgstr "Recordings"
|
msgstr "Recordings"
|
||||||
|
|
||||||
#: core/models.py:731
|
#: core/models.py:750
|
||||||
msgid "Recording/user relation"
|
msgid "Recording/user relation"
|
||||||
msgstr "Recording/user relation"
|
msgstr "Recording/user relation"
|
||||||
|
|
||||||
#: core/models.py:732
|
#: core/models.py:751
|
||||||
msgid "Recording/user relations"
|
msgid "Recording/user relations"
|
||||||
msgstr "Recording/user relations"
|
msgstr "Recording/user relations"
|
||||||
|
|
||||||
#: core/models.py:738
|
#: core/models.py:757
|
||||||
msgid "This user is already in this recording."
|
msgid "This user is already in this recording."
|
||||||
msgstr "This user is already in this recording."
|
msgstr "This user is already in this recording."
|
||||||
|
|
||||||
#: core/models.py:744
|
#: core/models.py:763
|
||||||
msgid "This team is already in this recording."
|
msgid "This team is already in this recording."
|
||||||
msgstr "This team is already in this recording."
|
msgstr "This team is already in this recording."
|
||||||
|
|
||||||
#: core/models.py:750
|
#: core/models.py:769
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Either user or team must be set, not both."
|
msgstr "Either user or team must be set, not both."
|
||||||
|
|
||||||
#: core/models.py:767
|
#: core/models.py:786
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "created on"
|
#| msgid "created on"
|
||||||
msgid "Create rooms"
|
msgid "Create rooms"
|
||||||
msgstr "Create rooms"
|
msgstr "Create rooms"
|
||||||
|
|
||||||
#: core/models.py:768
|
#: core/models.py:787
|
||||||
msgid "List rooms"
|
msgid "List rooms"
|
||||||
msgstr "List rooms"
|
msgstr "List rooms"
|
||||||
|
|
||||||
#: core/models.py:769
|
#: core/models.py:788
|
||||||
msgid "Retrieve room details"
|
msgid "Retrieve room details"
|
||||||
msgstr "Retrieve room details"
|
msgstr "Retrieve room details"
|
||||||
|
|
||||||
#: core/models.py:770
|
#: core/models.py:789
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "updated on"
|
#| msgid "updated on"
|
||||||
msgid "Update rooms"
|
msgid "Update rooms"
|
||||||
msgstr "Update rooms"
|
msgstr "Update rooms"
|
||||||
|
|
||||||
#: core/models.py:771
|
#: core/models.py:790
|
||||||
msgid "Delete rooms"
|
msgid "Delete rooms"
|
||||||
msgstr "Delete rooms"
|
msgstr "Delete rooms"
|
||||||
|
|
||||||
#: core/models.py:784
|
#: core/models.py:803
|
||||||
msgid "Application name"
|
msgid "Application name"
|
||||||
msgstr "Application name"
|
msgstr "Application name"
|
||||||
|
|
||||||
#: core/models.py:785
|
#: core/models.py:804
|
||||||
msgid "Descriptive name for this application."
|
msgid "Descriptive name for this application."
|
||||||
msgstr "Descriptive name for this application."
|
msgstr "Descriptive name for this application."
|
||||||
|
|
||||||
#: core/models.py:795
|
#: core/models.py:814
|
||||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||||
msgstr "Hashed on Save. Copy it now if this is a new secret."
|
msgstr "Hashed on Save. Copy it now if this is a new secret."
|
||||||
|
|
||||||
#: core/models.py:806
|
#: core/models.py:825
|
||||||
msgid "Application"
|
msgid "Application"
|
||||||
msgstr "Application"
|
msgstr "Application"
|
||||||
|
|
||||||
#: core/models.py:807
|
#: core/models.py:826
|
||||||
msgid "Applications"
|
msgid "Applications"
|
||||||
msgstr "Applications"
|
msgstr "Applications"
|
||||||
|
|
||||||
#: core/models.py:830
|
#: core/models.py:849
|
||||||
msgid "Enter a valid domain"
|
msgid "Enter a valid domain"
|
||||||
msgstr "Enter a valid domain"
|
msgstr "Enter a valid domain"
|
||||||
|
|
||||||
#: core/models.py:833
|
#: core/models.py:852
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr "Domain"
|
msgstr "Domain"
|
||||||
|
|
||||||
#: core/models.py:834
|
#: core/models.py:853
|
||||||
msgid "Email domain this application can act on behalf of."
|
msgid "Email domain this application can act on behalf of."
|
||||||
msgstr "Email domain this application can act on behalf of."
|
msgstr "Email domain this application can act on behalf of."
|
||||||
|
|
||||||
#: core/models.py:846
|
#: core/models.py:865
|
||||||
msgid "Application domain"
|
msgid "Application domain"
|
||||||
msgstr "Application domain"
|
msgstr "Application domain"
|
||||||
|
|
||||||
#: core/models.py:847
|
#: core/models.py:866
|
||||||
msgid "Application domains"
|
msgid "Application domains"
|
||||||
msgstr "Application domains"
|
msgstr "Application domains"
|
||||||
|
|
||||||
#: core/models.py:865
|
#: core/models.py:884
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Recording"
|
#| msgid "Recording"
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr "Pending"
|
msgstr "Pending"
|
||||||
|
|
||||||
#: core/models.py:866
|
#: core/models.py:885
|
||||||
msgid "Analyzing"
|
msgid "Analyzing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/models.py:873
|
#: core/models.py:892
|
||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr "Ready"
|
msgstr "Ready"
|
||||||
|
|
||||||
#: core/models.py:879
|
#: core/models.py:898
|
||||||
msgid "Background image"
|
msgid "Background image"
|
||||||
msgstr "Background image"
|
msgstr "Background image"
|
||||||
|
|
||||||
#: core/models.py:891
|
#: core/models.py:910
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "title"
|
msgstr "title"
|
||||||
|
|
||||||
#: core/models.py:915
|
#: core/models.py:934
|
||||||
msgid "Malware detection info when the analysis status is unsafe."
|
msgid "Malware detection info when the analysis status is unsafe."
|
||||||
msgstr "Malware detection info when the analysis status is unsafe."
|
msgstr "Malware detection info when the analysis status is unsafe."
|
||||||
|
|
||||||
#: core/models.py:920
|
#: core/models.py:939
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "File"
|
msgstr "File"
|
||||||
|
|
||||||
#: core/models.py:921
|
#: core/models.py:940
|
||||||
msgid "Files"
|
msgid "Files"
|
||||||
msgstr "Files"
|
msgstr "Files"
|
||||||
|
|
||||||
#: core/models.py:1041
|
#: core/models.py:1060
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "This user is already in this recording."
|
#| msgid "This user is already in this recording."
|
||||||
msgid "This file is already hard deleted."
|
msgid "This file is already hard deleted."
|
||||||
msgstr "This file is already hard deleted."
|
msgstr "This file is already hard deleted."
|
||||||
|
|
||||||
#: core/models.py:1051
|
#: core/models.py:1070
|
||||||
msgid "To hard delete a file, it must first be soft deleted."
|
msgid "To hard delete a file, it must first be soft deleted."
|
||||||
msgstr "To hard delete a file, it must first be soft deleted."
|
msgstr "To hard delete a file, it must first be soft deleted."
|
||||||
|
|
||||||
#: core/recording/event/notification.py:123
|
#: core/recording/event/notification.py:124
|
||||||
msgid "Your recording is ready"
|
msgid "Your recording is ready"
|
||||||
msgstr "Your recording is ready"
|
msgstr "Your recording is ready"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:194
|
#: core/recording/event/notification.py:195
|
||||||
msgid "Transcription"
|
msgid "Transcription"
|
||||||
msgstr "Transcription"
|
msgstr "Transcription"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:204
|
#: core/recording/event/notification.py:206
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
||||||
msgstr "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
msgstr "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
||||||
@@ -532,25 +548,25 @@ msgstr "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
|||||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||||
msgstr "Video call in progress: {sender.email} is waiting for you to connect"
|
msgstr "Video call in progress: {sender.email} is waiting for you to connect"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:159
|
#: core/templates/mail/html/invitation.html:151
|
||||||
#: core/templates/mail/html/screen_recording.html:159
|
#: core/templates/mail/html/screen_recording.html:151
|
||||||
#: core/templates/mail/text/invitation.txt:3
|
#: core/templates/mail/text/invitation.txt:4
|
||||||
#: core/templates/mail/text/screen_recording.txt:3
|
#: core/templates/mail/text/screen_recording.txt:4
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo email"
|
msgstr "Logo email"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:189
|
#: core/templates/mail/html/invitation.html:181
|
||||||
#: core/templates/mail/text/invitation.txt:5
|
#: core/templates/mail/text/invitation.txt:6
|
||||||
msgid "invites you to join an ongoing video call"
|
msgid "invites you to join an ongoing video call"
|
||||||
msgstr "invites you to join an ongoing video call"
|
msgstr "invites you to join an ongoing video call"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:200
|
#: core/templates/mail/html/invitation.html:192
|
||||||
#: core/templates/mail/text/invitation.txt:7
|
#: core/templates/mail/text/invitation.txt:8
|
||||||
msgid "JOIN THE CALL"
|
msgid "JOIN THE CALL"
|
||||||
msgstr "JOIN THE CALL"
|
msgstr "JOIN THE CALL"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:227
|
#: core/templates/mail/html/invitation.html:219
|
||||||
#: core/templates/mail/text/invitation.txt:13
|
#: core/templates/mail/text/invitation.txt:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you can't click the button, copy and paste the URL into your browser to "
|
"If you can't click the button, copy and paste the URL into your browser to "
|
||||||
"join the call."
|
"join the call."
|
||||||
@@ -558,41 +574,47 @@ msgstr ""
|
|||||||
"If you can't click the button, copy and paste the URL into your browser to "
|
"If you can't click the button, copy and paste the URL into your browser to "
|
||||||
"join the call."
|
"join the call."
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:235
|
#: core/templates/mail/html/invitation.html:227
|
||||||
#: core/templates/mail/text/invitation.txt:15
|
#: core/templates/mail/text/invitation.txt:16
|
||||||
msgid "Tips for a better experience:"
|
msgid "Tips for a better experience:"
|
||||||
msgstr "Tips for a better experience:"
|
msgstr "Tips for a better experience:"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:237
|
#: core/templates/mail/html/invitation.html:229
|
||||||
#: core/templates/mail/text/invitation.txt:17
|
#: core/templates/mail/text/invitation.txt:18
|
||||||
msgid "Use Chrome or Firefox for better call quality"
|
msgid "Use Chrome or Firefox for better call quality"
|
||||||
msgstr "Use Chrome or Firefox for better call quality"
|
msgstr "Use Chrome or Firefox for better call quality"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:238
|
#: core/templates/mail/html/invitation.html:230
|
||||||
#: core/templates/mail/text/invitation.txt:18
|
#: core/templates/mail/text/invitation.txt:19
|
||||||
msgid "Test your microphone and camera before joining"
|
msgid "Test your microphone and camera before joining"
|
||||||
msgstr "Test your microphone and camera before joining"
|
msgstr "Test your microphone and camera before joining"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:239
|
#: core/templates/mail/html/invitation.html:231
|
||||||
#: core/templates/mail/text/invitation.txt:19
|
#: core/templates/mail/text/invitation.txt:20
|
||||||
msgid "Make sure you have a stable internet connection"
|
msgid "Make sure you have a stable internet connection"
|
||||||
msgstr "Make sure you have a stable internet connection"
|
msgstr "Make sure you have a stable internet connection"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:248
|
#: core/templates/mail/html/invitation.html:240
|
||||||
#: core/templates/mail/html/screen_recording.html:245
|
#: core/templates/mail/html/screen_recording.html:237
|
||||||
#: core/templates/mail/text/invitation.txt:21
|
#: core/templates/mail/text/invitation.txt:22
|
||||||
#: core/templates/mail/text/screen_recording.txt:23
|
#: core/templates/mail/text/screen_recording.txt:24
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Thank you for using %(brandname)s. "
|
msgid " Thank you for using %(brandname)s. "
|
||||||
msgstr " Thank you for using %(brandname)s. "
|
msgstr " Thank you for using %(brandname)s. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:188
|
#: core/templates/mail/html/invitation.html:271
|
||||||
#: core/templates/mail/text/screen_recording.txt:6
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/html/screen_recording.html:180
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:7
|
||||||
msgid "Your recording is ready!"
|
msgid "Your recording is ready!"
|
||||||
msgstr "Your recording is ready!"
|
msgstr "Your recording is ready!"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
||||||
@@ -601,14 +623,14 @@ msgstr ""
|
|||||||
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
||||||
"%(recording_time)s is now ready to download. "
|
"%(recording_time)s is now ready to download. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " The recording will expire in %(days)s days. "
|
msgid " The recording will expire in %(days)s days. "
|
||||||
msgstr " The recording will expire in %(days)s days. "
|
msgstr " The recording will expire in %(days)s days. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:200
|
#: core/templates/mail/html/screen_recording.html:192
|
||||||
#: core/templates/mail/text/screen_recording.txt:9
|
#: core/templates/mail/text/screen_recording.txt:10
|
||||||
msgid ""
|
msgid ""
|
||||||
" Sharing the recording via link is not yet available. Only organizers can "
|
" Sharing the recording via link is not yet available. Only organizers can "
|
||||||
"download it. "
|
"download it. "
|
||||||
@@ -616,33 +638,33 @@ msgstr ""
|
|||||||
" Sharing the recording via link is not yet available. Only organizers can "
|
" Sharing the recording via link is not yet available. Only organizers can "
|
||||||
"download it. "
|
"download it. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:206
|
#: core/templates/mail/html/screen_recording.html:198
|
||||||
#: core/templates/mail/text/screen_recording.txt:11
|
#: core/templates/mail/text/screen_recording.txt:12
|
||||||
msgid "To keep this recording permanently:"
|
msgid "To keep this recording permanently:"
|
||||||
msgstr "To keep this recording permanently:"
|
msgstr "To keep this recording permanently:"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:208
|
#: core/templates/mail/html/screen_recording.html:200
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgstr "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
msgstr "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:209
|
#: core/templates/mail/html/screen_recording.html:201
|
||||||
#: core/templates/mail/text/screen_recording.txt:14
|
#: core/templates/mail/text/screen_recording.txt:15
|
||||||
msgid "Use the \"Download\" button in the interface "
|
msgid "Use the \"Download\" button in the interface "
|
||||||
msgstr "Use the \"Download\" button in the interface "
|
msgstr "Use the \"Download\" button in the interface "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:210
|
#: core/templates/mail/html/screen_recording.html:202
|
||||||
#: core/templates/mail/text/screen_recording.txt:15
|
#: core/templates/mail/text/screen_recording.txt:16
|
||||||
msgid "Save the file to your preferred location"
|
msgid "Save the file to your preferred location"
|
||||||
msgstr "Save the file to your preferred location"
|
msgstr "Save the file to your preferred location"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:221
|
#: core/templates/mail/html/screen_recording.html:213
|
||||||
#: core/templates/mail/text/screen_recording.txt:17
|
#: core/templates/mail/text/screen_recording.txt:18
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Open"
|
msgstr "Open"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:230
|
#: core/templates/mail/html/screen_recording.html:222
|
||||||
#: core/templates/mail/text/screen_recording.txt:19
|
#: core/templates/mail/text/screen_recording.txt:20
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" If you have any questions or need assistance, please contact our support "
|
" If you have any questions or need assistance, please contact our support "
|
||||||
@@ -651,28 +673,33 @@ msgstr ""
|
|||||||
" If you have any questions or need assistance, please contact our support "
|
" If you have any questions or need assistance, please contact our support "
|
||||||
"team at %(support_email)s. "
|
"team at %(support_email)s. "
|
||||||
|
|
||||||
#: core/templates/mail/text/screen_recording.txt:13
|
#: core/templates/mail/text/invitation.txt:24
|
||||||
|
#, python-format
|
||||||
|
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:14
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgid "Click the \"Open [%(link)s]\" link below "
|
msgid "Click the \"Open [%(link)s]\" link below "
|
||||||
msgstr "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
msgstr "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
|
|
||||||
#: meet/settings.py:228
|
#: meet/settings.py:238
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "English"
|
msgstr "English"
|
||||||
|
|
||||||
#: meet/settings.py:229
|
#: meet/settings.py:239
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr "French"
|
msgstr "French"
|
||||||
|
|
||||||
#: meet/settings.py:230
|
#: meet/settings.py:240
|
||||||
msgid "Dutch"
|
msgid "Dutch"
|
||||||
msgstr "Dutch"
|
msgstr "Dutch"
|
||||||
|
|
||||||
#: meet/settings.py:231
|
#: meet/settings.py:241
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr "German"
|
msgstr "German"
|
||||||
|
|
||||||
#: meet/settings.py:233
|
#: meet/settings.py:242
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
msgstr "Spanish"
|
msgstr "Spanish"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-02 10:47+0000\n"
|
"POT-Creation-Date: 2026-09-02 22:52+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -41,20 +41,20 @@ msgstr "Eliminar Salas"
|
|||||||
msgid "Derived info"
|
msgid "Derived info"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/admin.py:237
|
|
||||||
# 'Marcas de tiempo' is a bad translation for spanish
|
# 'Marcas de tiempo' is a bad translation for spanish
|
||||||
|
#: core/admin.py:237
|
||||||
msgid "Timestamps"
|
msgid "Timestamps"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/admin.py:240
|
#: core/admin.py:240
|
||||||
msgid "File preview"
|
msgid "File preview"
|
||||||
msgstr "Vista previa"
|
msgstr "Vista previa"
|
||||||
|
|
||||||
#: core/admin.py:300 core/admin.py:443
|
#: core/admin.py:300 core/admin.py:450
|
||||||
msgid "No owner"
|
msgid "No owner"
|
||||||
msgstr "Sin propietario"
|
msgstr "Sin propietario"
|
||||||
|
|
||||||
#: core/admin.py:303 core/admin.py:446
|
#: core/admin.py:303 core/admin.py:453
|
||||||
msgid "Multiple owners"
|
msgid "Multiple owners"
|
||||||
msgstr "Varios propietarios"
|
msgstr "Varios propietarios"
|
||||||
|
|
||||||
@@ -89,18 +89,19 @@ msgstr "Marcar las grabaciones seleccionadas como «Error al detener»"
|
|||||||
#: core/admin.py:386
|
#: core/admin.py:386
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(count)s recording(s) successfully marked as 'Failed to Stop'."
|
msgid "%(count)s recording(s) successfully marked as 'Failed to Stop'."
|
||||||
msgstr "%(count)s grabación(es) marcada(s) correctamente como «Error al detener»."
|
msgstr ""
|
||||||
|
"%(count)s grabación(es) marcada(s) correctamente como «Error al detener»."
|
||||||
|
|
||||||
#: core/admin.py:394
|
#: core/admin.py:394
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||||
msgstr "Se han omitido %(count)s grabación(es) con un estado no elegible."
|
msgstr "Se han omitido %(count)s grabación(es) con un estado no elegible."
|
||||||
|
|
||||||
#: core/admin.py:510
|
#: core/admin.py:517
|
||||||
msgid "No scopes"
|
msgid "No scopes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/admin.py:512
|
#: core/admin.py:519
|
||||||
msgid "Scopes"
|
msgid "Scopes"
|
||||||
msgstr "Ámbitos"
|
msgstr "Ámbitos"
|
||||||
|
|
||||||
@@ -108,545 +109,599 @@ msgstr "Ámbitos"
|
|||||||
msgid "Creator is me"
|
msgid "Creator is me"
|
||||||
msgstr "Yo soy el creador"
|
msgstr "Yo soy el creador"
|
||||||
|
|
||||||
#: core/api/serializers.py:89
|
#: core/api/serializers.py:108
|
||||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||||
msgstr "Debes ser administrador o propietario de una reunión para añadirle accesos."
|
msgstr ""
|
||||||
|
"Debes ser administrador o propietario de una reunión para añadirle accesos."
|
||||||
|
|
||||||
#: core/api/serializers.py:534
|
#: core/api/serializers.py:560
|
||||||
msgid "This file extension is not allowed."
|
msgid "This file extension is not allowed."
|
||||||
msgstr "Esta extensión de archivo no está permitida."
|
msgstr "Esta extensión de archivo no está permitida."
|
||||||
|
|
||||||
#: core/api/viewsets.py:1222
|
#: core/api/viewsets.py:1268
|
||||||
msgid "You have reached the maximum number of files for this type."
|
msgid "You have reached the maximum number of files for this type."
|
||||||
msgstr "Has alcanzado el número máximo de archivos de este tipo."
|
msgstr "Has alcanzado el número máximo de archivos de este tipo."
|
||||||
|
|
||||||
#: core/models.py:37
|
#: core/models.py:38
|
||||||
msgid "Member"
|
msgid "Member"
|
||||||
msgstr "Miembro"
|
msgstr "Miembro"
|
||||||
|
|
||||||
#: core/models.py:38
|
#: core/models.py:39
|
||||||
msgid "Administrator"
|
msgid "Administrator"
|
||||||
msgstr "Administrador"
|
msgstr "Administrador"
|
||||||
|
|
||||||
#: core/models.py:39
|
#: core/models.py:40
|
||||||
msgid "Owner"
|
msgid "Owner"
|
||||||
msgstr "Propietario"
|
msgstr "Propietario"
|
||||||
|
|
||||||
#: core/models.py:55
|
|
||||||
# To check here and following lines for gender agreement (Masculine/Feminine)
|
# To check here and following lines for gender agreement (Masculine/Feminine)
|
||||||
|
#: core/models.py:56
|
||||||
msgid "Initiated"
|
msgid "Initiated"
|
||||||
msgstr "Iniciada"
|
msgstr "Iniciada"
|
||||||
|
|
||||||
#: core/models.py:56
|
#: core/models.py:57
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr "Activa"
|
msgstr "Activa"
|
||||||
|
|
||||||
#: core/models.py:57
|
#: core/models.py:58
|
||||||
msgid "Stopped"
|
msgid "Stopped"
|
||||||
msgstr "Detenida"
|
msgstr "Detenida"
|
||||||
|
|
||||||
#: core/models.py:58
|
#: core/models.py:59
|
||||||
msgid "Saved"
|
msgid "Saved"
|
||||||
msgstr "Guardada"
|
msgstr "Guardada"
|
||||||
|
|
||||||
#: core/models.py:59
|
#: core/models.py:60
|
||||||
msgid "Aborted"
|
msgid "Aborted"
|
||||||
msgstr "Cancelada"
|
msgstr "Cancelada"
|
||||||
|
|
||||||
#: core/models.py:60
|
#: core/models.py:61
|
||||||
msgid "Failed to Start"
|
msgid "Failed to Start"
|
||||||
msgstr "Error al iniciar"
|
msgstr "Error al iniciar"
|
||||||
|
|
||||||
#: core/models.py:61
|
#: core/models.py:62
|
||||||
msgid "Failed to Stop"
|
msgid "Failed to Stop"
|
||||||
msgstr "Error al detener"
|
msgstr "Error al detener"
|
||||||
|
|
||||||
#: core/models.py:62
|
#: core/models.py:63
|
||||||
msgid "Notification succeeded"
|
msgid "Notification succeeded"
|
||||||
msgstr "Notificado correctamente"
|
msgstr "Notificado correctamente"
|
||||||
|
|
||||||
#: core/models.py:65
|
#: core/models.py:66
|
||||||
msgid "External process successful"
|
msgid "External process successful"
|
||||||
msgstr "Proceso externo finalizado correctamente"
|
msgstr "Proceso externo finalizado correctamente"
|
||||||
|
|
||||||
#: core/models.py:67
|
#: core/models.py:68
|
||||||
msgid "External process failed"
|
msgid "External process failed"
|
||||||
msgstr "Error en el Proceso externo"
|
msgstr "Error en el Proceso externo"
|
||||||
|
|
||||||
#: core/models.py:96
|
#: core/models.py:97
|
||||||
msgid "SCREEN_RECORDING"
|
msgid "SCREEN_RECORDING"
|
||||||
msgstr "GRABACIÓN_DE_PANTALLA"
|
msgstr "GRABACIÓN_DE_PANTALLA"
|
||||||
|
|
||||||
#: core/models.py:97
|
#: core/models.py:98
|
||||||
msgid "TRANSCRIPT"
|
msgid "TRANSCRIPT"
|
||||||
msgstr "TRANSCRIPCIÓN"
|
msgstr "TRANSCRIPCIÓN"
|
||||||
|
|
||||||
#: core/models.py:103
|
#: core/models.py:104
|
||||||
msgid "Public Access"
|
msgid "Public Access"
|
||||||
msgstr "Acceso público"
|
msgstr "Acceso público"
|
||||||
|
|
||||||
#: core/models.py:104
|
#: core/models.py:105
|
||||||
msgid "Trusted Access"
|
msgid "Trusted Access"
|
||||||
msgstr "Acceso usuarios autorizados"
|
msgstr "Acceso usuarios autorizados"
|
||||||
|
|
||||||
#: core/models.py:105
|
#: core/models.py:106
|
||||||
msgid "Restricted Access"
|
msgid "Restricted Access"
|
||||||
msgstr "Acceso restringido"
|
msgstr "Acceso restringido"
|
||||||
|
|
||||||
#: core/models.py:117
|
#: core/models.py:118
|
||||||
msgid "id"
|
msgid "id"
|
||||||
msgstr "id"
|
msgstr "id"
|
||||||
|
|
||||||
#: core/models.py:118
|
#: core/models.py:119
|
||||||
msgid "primary key for the record as UUID"
|
msgid "primary key for the record as UUID"
|
||||||
msgstr "clave primaria del registro en forma de UUID"
|
msgstr "clave primaria del registro en forma de UUID"
|
||||||
|
|
||||||
#: core/models.py:124
|
#: core/models.py:125
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr "creado el"
|
msgstr "creado el"
|
||||||
|
|
||||||
#: core/models.py:125
|
#: core/models.py:126
|
||||||
msgid "date and time at which a record was created"
|
msgid "date and time at which a record was created"
|
||||||
msgstr "fecha y hora en que se creó un registro"
|
msgstr "fecha y hora en que se creó un registro"
|
||||||
|
|
||||||
#: core/models.py:130
|
#: core/models.py:131
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
msgstr "actualizado el"
|
msgstr "actualizado el"
|
||||||
|
|
||||||
#: core/models.py:131
|
#: core/models.py:132
|
||||||
msgid "date and time at which a record was last updated"
|
msgid "date and time at which a record was last updated"
|
||||||
msgstr "fecha y hora de la última actualización de un registro"
|
msgstr "fecha y hora de la última actualización de un registro"
|
||||||
|
|
||||||
#: core/models.py:151
|
#: core/models.py:150
|
||||||
msgid ""
|
|
||||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
|
||||||
"_ characters."
|
|
||||||
msgstr "Introduce un sub válido. Este valor solo puede contener letras, números y los caracteres @/./+/-/_."
|
|
||||||
|
|
||||||
#: core/models.py:157
|
|
||||||
msgid "sub"
|
msgid "sub"
|
||||||
msgstr "sub"
|
msgstr "sub"
|
||||||
|
|
||||||
#: core/models.py:159
|
#: core/models.py:152
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optional for pending users; required upon account activation. 255 characters "
|
"Optional for pending users; required upon account activation. 255 characters "
|
||||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
"or fewer. Printable ASCII characters only."
|
||||||
msgstr "Opcional para los usuarios pendientes; obligatorio al activar la cuenta. 255 caracteres como máximo. Solo letras, números y los caracteres @/./+/-/_."
|
msgstr ""
|
||||||
|
"Opcional para los usuarios pendientes; obligatorio al activar la cuenta. "
|
||||||
|
"255 caracteres como máximo. Solo caracteres ASCII imprimibles."
|
||||||
|
|
||||||
#: core/models.py:168
|
#: core/validators.py:22
|
||||||
|
msgid "Enter a valid sub. This value should be printable ASCII only."
|
||||||
|
msgstr "Introduzca un sub válido. Este valor solo debe contener caracteres ASCII imprimibles."
|
||||||
|
|
||||||
|
#: core/models.py:161
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "dirección de correo electrónico"
|
msgstr "dirección de correo electrónico"
|
||||||
|
|
||||||
#: core/models.py:173
|
#: core/models.py:166
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "dirección de correo electrónico de administrador"
|
msgstr "dirección de correo electrónico de administrador"
|
||||||
|
|
||||||
#: core/models.py:175
|
#: core/models.py:168
|
||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "nombre completo"
|
msgstr "nombre completo"
|
||||||
|
|
||||||
#: core/models.py:177
|
#: core/models.py:170
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "nombre corto"
|
msgstr "nombre corto"
|
||||||
|
|
||||||
#: core/models.py:183
|
#: core/models.py:176
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "idioma"
|
msgstr "idioma"
|
||||||
|
|
||||||
#: core/models.py:184
|
#: core/models.py:177
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "El idioma preferido de interfaz."
|
msgstr "El idioma preferido de interfaz."
|
||||||
|
|
||||||
#: core/models.py:190
|
#: core/models.py:183
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "La zona horaria en la que el usuario quiere ver las horas."
|
msgstr "La zona horaria en la que el usuario quiere ver las horas."
|
||||||
|
|
||||||
#: core/models.py:193
|
#: core/models.py:190
|
||||||
|
msgid "default room access level"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:192
|
||||||
|
msgid ""
|
||||||
|
"Access level applied by default to new rooms created by this user. When "
|
||||||
|
"empty, the instance default is used."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:199
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Visio room configuration"
|
||||||
|
msgid "default room configuration"
|
||||||
|
msgstr "Configuración de la videoconferencia"
|
||||||
|
|
||||||
|
#: core/models.py:201
|
||||||
|
msgid "Configurations applied by default to new rooms created by this user."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:205
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "dispositivo"
|
msgstr "dispositivo"
|
||||||
|
|
||||||
#: core/models.py:195
|
#: core/models.py:207
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Si el usuario es un dispositivo o un usuario real."
|
msgstr "Si el usuario es un dispositivo o un usuario real."
|
||||||
|
|
||||||
#: core/models.py:198
|
#: core/models.py:210
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "estado del personal"
|
msgstr "estado del personal"
|
||||||
|
|
||||||
#: core/models.py:200
|
#: core/models.py:212
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Si el usuario puede acceder a este sitio de administración."
|
msgstr "Si el usuario puede acceder a este sitio de administración."
|
||||||
|
|
||||||
#: core/models.py:203
|
#: core/models.py:215
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "activo"
|
msgstr "activo"
|
||||||
|
|
||||||
#: core/models.py:206
|
#: core/models.py:218
|
||||||
msgid ""
|
msgid ""
|
||||||
"Whether this user should be treated as active. Unselect this instead of "
|
"Whether this user should be treated as active. Unselect this instead of "
|
||||||
"deleting accounts."
|
"deleting accounts."
|
||||||
msgstr "Si este usuario debe considerarse activo. Desmarca esta opción en lugar de eliminar cuentas."
|
msgstr ""
|
||||||
|
"Si este usuario debe considerarse activo. Desmarca esta opción en lugar de "
|
||||||
|
"eliminar cuentas."
|
||||||
|
|
||||||
#: core/models.py:219
|
#: core/models.py:231
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "usuario"
|
msgstr "usuario"
|
||||||
|
|
||||||
#: core/models.py:220
|
#: core/models.py:232
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "usuarios"
|
msgstr "usuarios"
|
||||||
|
|
||||||
#: core/models.py:286
|
#: core/models.py:298
|
||||||
msgid "Resource"
|
msgid "Resource"
|
||||||
msgstr "Recurso"
|
msgstr "Recurso"
|
||||||
|
|
||||||
#: core/models.py:287
|
#: core/models.py:299
|
||||||
msgid "Resources"
|
msgid "Resources"
|
||||||
msgstr "Recursos"
|
msgstr "Recursos"
|
||||||
|
|
||||||
#: core/models.py:345
|
#: core/models.py:357
|
||||||
msgid "Resource access"
|
msgid "Resource access"
|
||||||
msgstr "Acceso a recursos"
|
msgstr "Acceso a recursos"
|
||||||
|
|
||||||
#: core/models.py:346
|
#: core/models.py:358
|
||||||
msgid "Resource accesses"
|
msgid "Resource accesses"
|
||||||
msgstr "Accesos a recursos"
|
msgstr "Accesos a recursos"
|
||||||
|
|
||||||
#: core/models.py:352
|
#: core/models.py:364
|
||||||
msgid "Resource access with this User and Resource already exists."
|
msgid "Resource access with this User and Resource already exists."
|
||||||
msgstr "Ya existe un acceso al recurso con este usuario y este recurso."
|
msgstr "Ya existe un acceso al recurso con este usuario y este recurso."
|
||||||
|
|
||||||
#: core/models.py:409
|
#: core/models.py:421
|
||||||
msgid "Visio room configuration"
|
msgid "Visio room configuration"
|
||||||
msgstr "Configuración de la videoconferencia"
|
msgstr "Configuración de la videoconferencia"
|
||||||
|
|
||||||
#: core/models.py:410
|
#: core/models.py:422
|
||||||
msgid "Values for Visio parameters to configure the room."
|
msgid "Values for Visio parameters to configure the room."
|
||||||
msgstr "Valores de los parámetros de videoconferencia para configurar la reunión."
|
msgstr ""
|
||||||
|
"Valores de los parámetros de videoconferencia para configurar la reunión."
|
||||||
|
|
||||||
#: core/models.py:417
|
#: core/models.py:429
|
||||||
msgid "Room PIN code"
|
msgid "Room PIN code"
|
||||||
msgstr "Código PIN de la reunión"
|
msgstr "Código PIN de la reunión"
|
||||||
|
|
||||||
#: core/models.py:418
|
#: core/models.py:430
|
||||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||||
msgstr "Código único de n dígitos que identifica esta reunión en modo telefónico."
|
msgstr ""
|
||||||
|
"Código único de n dígitos que identifica esta reunión en modo telefónico."
|
||||||
|
|
||||||
#: core/models.py:424 core/models.py:578
|
#: core/models.py:436 core/models.py:597
|
||||||
msgid "Room"
|
msgid "Room"
|
||||||
msgstr "Reunión"
|
msgstr "Reunión"
|
||||||
|
|
||||||
#: core/models.py:425
|
#: core/models.py:437
|
||||||
msgid "Rooms"
|
msgid "Rooms"
|
||||||
msgstr "Reuniones"
|
msgstr "Reuniones"
|
||||||
|
|
||||||
#: core/models.py:589
|
#: core/models.py:608
|
||||||
msgid "Worker ID"
|
msgid "Worker ID"
|
||||||
msgstr "ID del worker"
|
msgstr "ID del worker"
|
||||||
|
|
||||||
#: core/models.py:591
|
#: core/models.py:610
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||||
"the worker stops, allowing for easy tracking."
|
"the worker stops, allowing for easy tracking."
|
||||||
msgstr "Introduce un identificador para la grabación del worker. Este identificador se conserva incluso cuando el worker se detiene, lo que permite un seguimiento sencillo."
|
msgstr ""
|
||||||
|
"Introduce un identificador para la grabación del worker. Este identificador "
|
||||||
|
"se conserva incluso cuando el worker se detiene, lo que permite un "
|
||||||
|
"seguimiento sencillo."
|
||||||
|
|
||||||
#: core/models.py:599
|
#: core/models.py:618
|
||||||
msgid "Recording mode"
|
msgid "Recording mode"
|
||||||
msgstr "Modo de grabación"
|
msgstr "Modo de grabación"
|
||||||
|
|
||||||
#: core/models.py:600
|
#: core/models.py:619
|
||||||
msgid "Defines the mode of recording being called."
|
msgid "Defines the mode of recording being called."
|
||||||
msgstr "Define el modo de grabación a utilizar."
|
msgstr "Define el modo de grabación a utilizar."
|
||||||
|
|
||||||
#: core/models.py:605 core/models.py:606
|
#: core/models.py:624 core/models.py:625
|
||||||
msgid "Recording options"
|
msgid "Recording options"
|
||||||
msgstr "Opciones de grabación"
|
msgstr "Opciones de grabación"
|
||||||
|
|
||||||
#: core/models.py:613
|
#: core/models.py:632
|
||||||
msgid "External Process ID"
|
msgid "External Process ID"
|
||||||
msgstr "ID del proceso externo"
|
msgstr "ID del proceso externo"
|
||||||
|
|
||||||
#: core/models.py:614
|
#: core/models.py:633
|
||||||
msgid "ID of the external process associated with the recording."
|
msgid "ID of the external process associated with the recording."
|
||||||
msgstr "ID del proceso externo asociado a la grabación."
|
msgstr "ID del proceso externo asociado a la grabación."
|
||||||
|
|
||||||
#: core/models.py:620
|
#: core/models.py:639
|
||||||
msgid "Recording"
|
msgid "Recording"
|
||||||
msgstr "Grabación"
|
msgstr "Grabación"
|
||||||
|
|
||||||
#: core/models.py:621
|
#: core/models.py:640
|
||||||
msgid "Recordings"
|
msgid "Recordings"
|
||||||
msgstr "Grabaciones"
|
msgstr "Grabaciones"
|
||||||
|
|
||||||
#: core/models.py:731
|
#: core/models.py:750
|
||||||
msgid "Recording/user relation"
|
msgid "Recording/user relation"
|
||||||
msgstr "Relación grabación/usuario"
|
msgstr "Relación grabación/usuario"
|
||||||
|
|
||||||
#: core/models.py:732
|
#: core/models.py:751
|
||||||
msgid "Recording/user relations"
|
msgid "Recording/user relations"
|
||||||
msgstr "Relaciones grabación/usuario"
|
msgstr "Relaciones grabación/usuario"
|
||||||
|
|
||||||
#: core/models.py:738
|
#: core/models.py:757
|
||||||
msgid "This user is already in this recording."
|
msgid "This user is already in this recording."
|
||||||
msgstr "Este usuario ya está en esta grabación."
|
msgstr "Este usuario ya está en esta grabación."
|
||||||
|
|
||||||
#: core/models.py:744
|
#: core/models.py:763
|
||||||
msgid "This team is already in this recording."
|
msgid "This team is already in this recording."
|
||||||
msgstr "Este equipo ya está en esta grabación."
|
msgstr "Este equipo ya está en esta grabación."
|
||||||
|
|
||||||
#: core/models.py:750
|
#: core/models.py:769
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Debe definirse el usuario o el equipo, pero no ambos."
|
msgstr "Debe definirse el usuario o el equipo, pero no ambos."
|
||||||
|
|
||||||
#: core/models.py:767
|
#: core/models.py:786
|
||||||
msgid "Create rooms"
|
msgid "Create rooms"
|
||||||
msgstr "Crear reunión"
|
msgstr "Crear reunión"
|
||||||
|
|
||||||
#: core/models.py:768
|
#: core/models.py:787
|
||||||
msgid "List rooms"
|
msgid "List rooms"
|
||||||
msgstr "Listar reuniones"
|
msgstr "Listar reuniones"
|
||||||
|
|
||||||
#: core/models.py:769
|
#: core/models.py:788
|
||||||
msgid "Retrieve room details"
|
msgid "Retrieve room details"
|
||||||
msgstr "Ver los detalles de una reunión"
|
msgstr "Ver los detalles de una reunión"
|
||||||
|
|
||||||
#: core/models.py:770
|
#: core/models.py:789
|
||||||
msgid "Update rooms"
|
msgid "Update rooms"
|
||||||
msgstr "Actualizar las reuniones"
|
msgstr "Actualizar las reuniones"
|
||||||
|
|
||||||
#: core/models.py:771
|
#: core/models.py:790
|
||||||
msgid "Delete rooms"
|
msgid "Delete rooms"
|
||||||
msgstr "Eliminar las reuniones"
|
msgstr "Eliminar las reuniones"
|
||||||
|
|
||||||
#: core/models.py:784
|
#: core/models.py:803
|
||||||
msgid "Application name"
|
msgid "Application name"
|
||||||
msgstr "Nombre de la aplicación"
|
msgstr "Nombre de la aplicación"
|
||||||
|
|
||||||
#: core/models.py:785
|
#: core/models.py:804
|
||||||
msgid "Descriptive name for this application."
|
msgid "Descriptive name for this application."
|
||||||
msgstr "Nombre descriptivo de esta aplicación."
|
msgstr "Nombre descriptivo de esta aplicación."
|
||||||
|
|
||||||
#: core/models.py:795
|
#: core/models.py:814
|
||||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||||
msgstr "Se cifra al guardar. Cópialo ahora si se trata de un secreto nuevo."
|
msgstr "Se cifra al guardar. Cópialo ahora si se trata de un secreto nuevo."
|
||||||
|
|
||||||
#: core/models.py:806
|
#: core/models.py:825
|
||||||
msgid "Application"
|
msgid "Application"
|
||||||
msgstr "Aplicación"
|
msgstr "Aplicación"
|
||||||
|
|
||||||
#: core/models.py:807
|
#: core/models.py:826
|
||||||
msgid "Applications"
|
msgid "Applications"
|
||||||
msgstr "Aplicaciones"
|
msgstr "Aplicaciones"
|
||||||
|
|
||||||
#: core/models.py:830
|
#: core/models.py:849
|
||||||
msgid "Enter a valid domain"
|
msgid "Enter a valid domain"
|
||||||
msgstr "Introduce un dominio válido"
|
msgstr "Introduce un dominio válido"
|
||||||
|
|
||||||
#: core/models.py:833
|
#: core/models.py:852
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr "Dominio"
|
msgstr "Dominio"
|
||||||
|
|
||||||
#: core/models.py:834
|
#: core/models.py:853
|
||||||
msgid "Email domain this application can act on behalf of."
|
msgid "Email domain this application can act on behalf of."
|
||||||
msgstr "Dominio de correo electrónico en cuyo nombre puede actuar esta aplicación."
|
msgstr ""
|
||||||
|
"Dominio de correo electrónico en cuyo nombre puede actuar esta aplicación."
|
||||||
|
|
||||||
#: core/models.py:846
|
#: core/models.py:865
|
||||||
msgid "Application domain"
|
msgid "Application domain"
|
||||||
msgstr "Dominio de aplicación"
|
msgstr "Dominio de aplicación"
|
||||||
|
|
||||||
#: core/models.py:847
|
#: core/models.py:866
|
||||||
msgid "Application domains"
|
msgid "Application domains"
|
||||||
msgstr "Dominios de aplicación"
|
msgstr "Dominios de aplicación"
|
||||||
|
|
||||||
#: core/models.py:865
|
#: core/models.py:884
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr "Pendiente"
|
msgstr "Pendiente"
|
||||||
|
|
||||||
#: core/models.py:866
|
#: core/models.py:885
|
||||||
msgid "Analyzing"
|
msgid "Analyzing"
|
||||||
msgstr "Analizando"
|
msgstr "Analizando"
|
||||||
|
|
||||||
#: core/models.py:873
|
#: core/models.py:892
|
||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr "Listo"
|
msgstr "Listo"
|
||||||
|
|
||||||
#: core/models.py:879
|
#: core/models.py:898
|
||||||
msgid "Background image"
|
msgid "Background image"
|
||||||
msgstr "Imagen de fondo"
|
msgstr "Imagen de fondo"
|
||||||
|
|
||||||
#: core/models.py:891
|
#: core/models.py:910
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "título"
|
msgstr "título"
|
||||||
|
|
||||||
#: core/models.py:915
|
#: core/models.py:934
|
||||||
msgid "Malware detection info when the analysis status is unsafe."
|
msgid "Malware detection info when the analysis status is unsafe."
|
||||||
msgstr "Información sobre la detección de malware cuando el estado del análisis no es seguro."
|
msgstr ""
|
||||||
|
"Información sobre la detección de malware cuando el estado del análisis no "
|
||||||
|
"es seguro."
|
||||||
|
|
||||||
#: core/models.py:920
|
#: core/models.py:939
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "Archivo"
|
msgstr "Archivo"
|
||||||
|
|
||||||
#: core/models.py:921
|
#: core/models.py:940
|
||||||
msgid "Files"
|
msgid "Files"
|
||||||
msgstr "Archivos"
|
msgstr "Archivos"
|
||||||
|
|
||||||
#: core/models.py:1041
|
#: core/models.py:1060
|
||||||
msgid "This file is already hard deleted."
|
msgid "This file is already hard deleted."
|
||||||
msgstr "Este archivo ya se ha eliminado definitivamente."
|
msgstr "Este archivo ya se ha eliminado definitivamente."
|
||||||
|
|
||||||
#: core/models.py:1051
|
#: core/models.py:1070
|
||||||
msgid "To hard delete a file, it must first be soft deleted."
|
msgid "To hard delete a file, it must first be soft deleted."
|
||||||
msgstr "Para eliminar definitivamente un archivo, primero debe haberse marcado como eliminado."
|
msgstr ""
|
||||||
|
"Para eliminar definitivamente un archivo, primero debe haberse marcado como "
|
||||||
|
"eliminado."
|
||||||
|
|
||||||
#: core/recording/event/notification.py:123
|
#: core/recording/event/notification.py:124
|
||||||
msgid "Your recording is ready"
|
msgid "Your recording is ready"
|
||||||
msgstr "Tu grabación está lista"
|
msgstr "Tu grabación está lista"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:194
|
#: core/recording/event/notification.py:195
|
||||||
msgid "Transcription"
|
msgid "Transcription"
|
||||||
msgstr "Transcripción"
|
msgstr "Transcripción"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:204
|
#: core/recording/event/notification.py:206
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
||||||
msgstr "Reunión \"{room}\" del {room_recording_date} a las {room_recording_time}"
|
msgstr ""
|
||||||
|
"Reunión \"{room}\" del {room_recording_date} a las {room_recording_time}"
|
||||||
|
|
||||||
#: core/services/invitation.py:44
|
#: core/services/invitation.py:44
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||||
msgstr "Videollamada en curso: {sender.email} está esperando a que te conectes"
|
msgstr "Videollamada en curso: {sender.email} está esperando a que te conectes"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:159
|
#: core/templates/mail/html/invitation.html:151
|
||||||
#: core/templates/mail/html/screen_recording.html:159
|
#: core/templates/mail/html/screen_recording.html:151
|
||||||
#: core/templates/mail/text/invitation.txt:3
|
#: core/templates/mail/text/invitation.txt:4
|
||||||
#: core/templates/mail/text/screen_recording.txt:3
|
#: core/templates/mail/text/screen_recording.txt:4
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logotipo del correo"
|
msgstr "Logotipo del correo"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:189
|
#: core/templates/mail/html/invitation.html:181
|
||||||
#: core/templates/mail/text/invitation.txt:5
|
#: core/templates/mail/text/invitation.txt:6
|
||||||
msgid "invites you to join an ongoing video call"
|
msgid "invites you to join an ongoing video call"
|
||||||
msgstr "te invita a unirte a una videollamada en curso"
|
msgstr "te invita a unirte a una videollamada en curso"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:200
|
#: core/templates/mail/html/invitation.html:192
|
||||||
#: core/templates/mail/text/invitation.txt:7
|
#: core/templates/mail/text/invitation.txt:8
|
||||||
msgid "JOIN THE CALL"
|
msgid "JOIN THE CALL"
|
||||||
msgstr "UNIRSE A LA LLAMADA"
|
msgstr "UNIRSE A LA LLAMADA"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:227
|
#: core/templates/mail/html/invitation.html:219
|
||||||
#: core/templates/mail/text/invitation.txt:13
|
#: core/templates/mail/text/invitation.txt:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you can't click the button, copy and paste the URL into your browser to "
|
"If you can't click the button, copy and paste the URL into your browser to "
|
||||||
"join the call."
|
"join the call."
|
||||||
msgstr "Si no puedes hacer clic en el botón, copia y pega la URL en tu navegador para unirte a la llamada."
|
msgstr ""
|
||||||
|
"Si no puedes hacer clic en el botón, copia y pega la URL en tu navegador "
|
||||||
|
"para unirte a la llamada."
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:235
|
#: core/templates/mail/html/invitation.html:227
|
||||||
#: core/templates/mail/text/invitation.txt:15
|
#: core/templates/mail/text/invitation.txt:16
|
||||||
msgid "Tips for a better experience:"
|
msgid "Tips for a better experience:"
|
||||||
msgstr "Consejos para una mejor experiencia:"
|
msgstr "Consejos para una mejor experiencia:"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:237
|
#: core/templates/mail/html/invitation.html:229
|
||||||
#: core/templates/mail/text/invitation.txt:17
|
#: core/templates/mail/text/invitation.txt:18
|
||||||
msgid "Use Chrome or Firefox for better call quality"
|
msgid "Use Chrome or Firefox for better call quality"
|
||||||
msgstr "Usa Chrome o Firefox para una mejor calidad de llamada"
|
msgstr "Usa Chrome o Firefox para una mejor calidad de llamada"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:238
|
#: core/templates/mail/html/invitation.html:230
|
||||||
#: core/templates/mail/text/invitation.txt:18
|
#: core/templates/mail/text/invitation.txt:19
|
||||||
msgid "Test your microphone and camera before joining"
|
msgid "Test your microphone and camera before joining"
|
||||||
msgstr "Prueba tu micrófono y tu cámara antes de unirte"
|
msgstr "Prueba tu micrófono y tu cámara antes de unirte"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:239
|
#: core/templates/mail/html/invitation.html:231
|
||||||
#: core/templates/mail/text/invitation.txt:19
|
#: core/templates/mail/text/invitation.txt:20
|
||||||
msgid "Make sure you have a stable internet connection"
|
msgid "Make sure you have a stable internet connection"
|
||||||
msgstr "Asegúrate de tener una conexión a internet estable"
|
msgstr "Asegúrate de tener una conexión a internet estable"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:248
|
#: core/templates/mail/html/invitation.html:240
|
||||||
#: core/templates/mail/html/screen_recording.html:245
|
#: core/templates/mail/html/screen_recording.html:237
|
||||||
#: core/templates/mail/text/invitation.txt:21
|
#: core/templates/mail/text/invitation.txt:22
|
||||||
#: core/templates/mail/text/screen_recording.txt:23
|
#: core/templates/mail/text/screen_recording.txt:24
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Thank you for using %(brandname)s. "
|
msgid " Thank you for using %(brandname)s. "
|
||||||
msgstr " Gracias por usar %(brandname)s. "
|
msgstr " Gracias por usar %(brandname)s. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:188
|
#: core/templates/mail/html/invitation.html:271
|
||||||
#: core/templates/mail/text/screen_recording.txt:6
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/html/screen_recording.html:180
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:7
|
||||||
msgid "Your recording is ready!"
|
msgid "Your recording is ready!"
|
||||||
msgstr "¡Tu grabación está lista!"
|
msgstr "¡Tu grabación está lista!"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
||||||
"%(recording_time)s is now ready to download. "
|
"%(recording_time)s is now ready to download. "
|
||||||
msgstr " Tu grabación de \"%(room_name)s\" del %(recording_date)s a las %(recording_time)s ya está lista para descargar. "
|
msgstr ""
|
||||||
|
" Tu grabación de \"%(room_name)s\" del %(recording_date)s a las "
|
||||||
|
"%(recording_time)s ya está lista para descargar. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " The recording will expire in %(days)s days. "
|
msgid " The recording will expire in %(days)s days. "
|
||||||
msgstr " La grabación caducará dentro de %(days)s días. "
|
msgstr " La grabación caducará dentro de %(days)s días. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:200
|
#: core/templates/mail/html/screen_recording.html:192
|
||||||
#: core/templates/mail/text/screen_recording.txt:9
|
#: core/templates/mail/text/screen_recording.txt:10
|
||||||
msgid ""
|
msgid ""
|
||||||
" Sharing the recording via link is not yet available. Only organizers can "
|
" Sharing the recording via link is not yet available. Only organizers can "
|
||||||
"download it. "
|
"download it. "
|
||||||
msgstr " Compartir la grabación mediante un enlace todavía no está disponible. Solo los organizadores pueden descargarla. "
|
msgstr ""
|
||||||
|
" Compartir la grabación mediante un enlace todavía no está disponible. Solo "
|
||||||
|
"los organizadores pueden descargarla. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:206
|
#: core/templates/mail/html/screen_recording.html:198
|
||||||
#: core/templates/mail/text/screen_recording.txt:11
|
#: core/templates/mail/text/screen_recording.txt:12
|
||||||
msgid "To keep this recording permanently:"
|
msgid "To keep this recording permanently:"
|
||||||
msgstr "Para conservar esta grabación de forma permanente:"
|
msgstr "Para conservar esta grabación de forma permanente:"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:208
|
#: core/templates/mail/html/screen_recording.html:200
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgstr "Haz clic en el enlace \"<a href=\"%(link)s\">Abrir</a>\" que aparece abajo "
|
msgstr ""
|
||||||
|
"Haz clic en el enlace \"<a href=\"%(link)s\">Abrir</a>\" que aparece abajo "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:209
|
#: core/templates/mail/html/screen_recording.html:201
|
||||||
#: core/templates/mail/text/screen_recording.txt:14
|
#: core/templates/mail/text/screen_recording.txt:15
|
||||||
msgid "Use the \"Download\" button in the interface "
|
msgid "Use the \"Download\" button in the interface "
|
||||||
msgstr "Usa el botón \"Descargar\" de la interfaz "
|
msgstr "Usa el botón \"Descargar\" de la interfaz "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:210
|
#: core/templates/mail/html/screen_recording.html:202
|
||||||
#: core/templates/mail/text/screen_recording.txt:15
|
#: core/templates/mail/text/screen_recording.txt:16
|
||||||
msgid "Save the file to your preferred location"
|
msgid "Save the file to your preferred location"
|
||||||
msgstr "Guarda el archivo en la ubicación que prefieras"
|
msgstr "Guarda el archivo en la ubicación que prefieras"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:221
|
#: core/templates/mail/html/screen_recording.html:213
|
||||||
#: core/templates/mail/text/screen_recording.txt:17
|
#: core/templates/mail/text/screen_recording.txt:18
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Abrir"
|
msgstr "Abrir"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:230
|
#: core/templates/mail/html/screen_recording.html:222
|
||||||
#: core/templates/mail/text/screen_recording.txt:19
|
#: core/templates/mail/text/screen_recording.txt:20
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" If you have any questions or need assistance, please contact our support "
|
" If you have any questions or need assistance, please contact our support "
|
||||||
"team at %(support_email)s. "
|
"team at %(support_email)s. "
|
||||||
msgstr " Si tienes alguna pregunta o necesitas ayuda, ponte en contacto con nuestro equipo de soporte en %(support_email)s. "
|
msgstr ""
|
||||||
|
" Si tienes alguna pregunta o necesitas ayuda, ponte en contacto con nuestro "
|
||||||
|
"equipo de soporte en %(support_email)s. "
|
||||||
|
|
||||||
#: core/templates/mail/text/screen_recording.txt:13
|
#: core/templates/mail/text/invitation.txt:24
|
||||||
|
#, python-format
|
||||||
|
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:14
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Click the \"Open [%(link)s]\" link below "
|
msgid "Click the \"Open [%(link)s]\" link below "
|
||||||
msgstr "Haz clic en el enlace \"Abrir [%(link)s]\" que aparece abajo "
|
msgstr "Haz clic en el enlace \"Abrir [%(link)s]\" que aparece abajo "
|
||||||
|
|
||||||
#: meet/settings.py:228
|
#: meet/settings.py:238
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "Inglés"
|
msgstr "Inglés"
|
||||||
|
|
||||||
#: meet/settings.py:229
|
#: meet/settings.py:239
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr "Francés"
|
msgstr "Francés"
|
||||||
|
|
||||||
#: meet/settings.py:230
|
#: meet/settings.py:240
|
||||||
msgid "Dutch"
|
msgid "Dutch"
|
||||||
msgstr "Neerlandés"
|
msgstr "Neerlandés"
|
||||||
|
|
||||||
#: meet/settings.py:231
|
#: meet/settings.py:241
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr "Alemán"
|
msgstr "Alemán"
|
||||||
|
|
||||||
#: meet/settings.py:233
|
#: meet/settings.py:242
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
msgstr "Español"
|
msgstr "Español"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-02 10:47+0000\n"
|
"POT-Creation-Date: 2026-09-02 22:52+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
|
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -51,11 +51,11 @@ msgstr ""
|
|||||||
msgid "File preview"
|
msgid "File preview"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/admin.py:300 core/admin.py:443
|
#: core/admin.py:300 core/admin.py:450
|
||||||
msgid "No owner"
|
msgid "No owner"
|
||||||
msgstr "Pas de propriétaire"
|
msgstr "Pas de propriétaire"
|
||||||
|
|
||||||
#: core/admin.py:303 core/admin.py:446
|
#: core/admin.py:303 core/admin.py:453
|
||||||
msgid "Multiple owners"
|
msgid "Multiple owners"
|
||||||
msgstr "Plusieurs propriétaires"
|
msgstr "Plusieurs propriétaires"
|
||||||
|
|
||||||
@@ -99,11 +99,11 @@ msgstr ""
|
|||||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||||
msgstr "%(count)s enregistrement(s) avec un statut inéligible ignoré(s)."
|
msgstr "%(count)s enregistrement(s) avec un statut inéligible ignoré(s)."
|
||||||
|
|
||||||
#: core/admin.py:510
|
#: core/admin.py:517
|
||||||
msgid "No scopes"
|
msgid "No scopes"
|
||||||
msgstr "Aucun scopes"
|
msgstr "Aucun scopes"
|
||||||
|
|
||||||
#: core/admin.py:512
|
#: core/admin.py:519
|
||||||
msgid "Scopes"
|
msgid "Scopes"
|
||||||
msgstr "Scopes"
|
msgstr "Scopes"
|
||||||
|
|
||||||
@@ -111,187 +111,203 @@ msgstr "Scopes"
|
|||||||
msgid "Creator is me"
|
msgid "Creator is me"
|
||||||
msgstr "Je suis le créateur"
|
msgstr "Je suis le créateur"
|
||||||
|
|
||||||
#: core/api/serializers.py:89
|
#: core/api/serializers.py:108
|
||||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
|
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
|
||||||
"des accès."
|
"des accès."
|
||||||
|
|
||||||
#: core/api/serializers.py:534
|
#: core/api/serializers.py:560
|
||||||
msgid "This file extension is not allowed."
|
msgid "This file extension is not allowed."
|
||||||
msgstr "Cette extension n'est pas autorisée"
|
msgstr "Cette extension n'est pas autorisée"
|
||||||
|
|
||||||
#: core/api/viewsets.py:1222
|
#: core/api/viewsets.py:1268
|
||||||
msgid "You have reached the maximum number of files for this type."
|
msgid "You have reached the maximum number of files for this type."
|
||||||
msgstr "Vous avez atteint le nombre maximum de fichiers de ce type"
|
msgstr "Vous avez atteint le nombre maximum de fichiers de ce type"
|
||||||
|
|
||||||
#: core/models.py:37
|
#: core/models.py:38
|
||||||
msgid "Member"
|
msgid "Member"
|
||||||
msgstr "Membre"
|
msgstr "Membre"
|
||||||
|
|
||||||
#: core/models.py:38
|
#: core/models.py:39
|
||||||
msgid "Administrator"
|
msgid "Administrator"
|
||||||
msgstr "Administrateur"
|
msgstr "Administrateur"
|
||||||
|
|
||||||
#: core/models.py:39
|
#: core/models.py:40
|
||||||
msgid "Owner"
|
msgid "Owner"
|
||||||
msgstr "Propriétaire"
|
msgstr "Propriétaire"
|
||||||
|
|
||||||
#: core/models.py:55
|
#: core/models.py:56
|
||||||
msgid "Initiated"
|
msgid "Initiated"
|
||||||
msgstr "Initié"
|
msgstr "Initié"
|
||||||
|
|
||||||
#: core/models.py:56
|
#: core/models.py:57
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr "Actif"
|
msgstr "Actif"
|
||||||
|
|
||||||
#: core/models.py:57
|
#: core/models.py:58
|
||||||
msgid "Stopped"
|
msgid "Stopped"
|
||||||
msgstr "Arrêté"
|
msgstr "Arrêté"
|
||||||
|
|
||||||
#: core/models.py:58
|
#: core/models.py:59
|
||||||
msgid "Saved"
|
msgid "Saved"
|
||||||
msgstr "Enregistré"
|
msgstr "Enregistré"
|
||||||
|
|
||||||
#: core/models.py:59
|
#: core/models.py:60
|
||||||
msgid "Aborted"
|
msgid "Aborted"
|
||||||
msgstr "Abandonné"
|
msgstr "Abandonné"
|
||||||
|
|
||||||
#: core/models.py:60
|
#: core/models.py:61
|
||||||
msgid "Failed to Start"
|
msgid "Failed to Start"
|
||||||
msgstr "Échec au démarrage"
|
msgstr "Échec au démarrage"
|
||||||
|
|
||||||
#: core/models.py:61
|
#: core/models.py:62
|
||||||
msgid "Failed to Stop"
|
msgid "Failed to Stop"
|
||||||
msgstr "Échec à l'arrêt"
|
msgstr "Échec à l'arrêt"
|
||||||
|
|
||||||
#: core/models.py:62
|
#: core/models.py:63
|
||||||
msgid "Notification succeeded"
|
msgid "Notification succeeded"
|
||||||
msgstr "Notification réussie"
|
msgstr "Notification réussie"
|
||||||
|
|
||||||
#: core/models.py:65
|
#: core/models.py:66
|
||||||
msgid "External process successful"
|
msgid "External process successful"
|
||||||
msgstr "Traitement externe : succès"
|
msgstr "Traitement externe : succès"
|
||||||
|
|
||||||
#: core/models.py:67
|
#: core/models.py:68
|
||||||
msgid "External process failed"
|
msgid "External process failed"
|
||||||
msgstr "Traitement externe : erreur"
|
msgstr "Traitement externe : erreur"
|
||||||
|
|
||||||
#: core/models.py:96
|
#: core/models.py:97
|
||||||
msgid "SCREEN_RECORDING"
|
msgid "SCREEN_RECORDING"
|
||||||
msgstr "ENREGISTREMENT_ÉCRAN"
|
msgstr "ENREGISTREMENT_ÉCRAN"
|
||||||
|
|
||||||
#: core/models.py:97
|
#: core/models.py:98
|
||||||
msgid "TRANSCRIPT"
|
msgid "TRANSCRIPT"
|
||||||
msgstr "TRANSCRIPTION"
|
msgstr "TRANSCRIPTION"
|
||||||
|
|
||||||
#: core/models.py:103
|
#: core/models.py:104
|
||||||
msgid "Public Access"
|
msgid "Public Access"
|
||||||
msgstr "Accès public"
|
msgstr "Accès public"
|
||||||
|
|
||||||
#: core/models.py:104
|
#: core/models.py:105
|
||||||
msgid "Trusted Access"
|
msgid "Trusted Access"
|
||||||
msgstr "Accès de confiance"
|
msgstr "Accès de confiance"
|
||||||
|
|
||||||
#: core/models.py:105
|
#: core/models.py:106
|
||||||
msgid "Restricted Access"
|
msgid "Restricted Access"
|
||||||
msgstr "Accès restreint"
|
msgstr "Accès restreint"
|
||||||
|
|
||||||
#: core/models.py:117
|
#: core/models.py:118
|
||||||
msgid "id"
|
msgid "id"
|
||||||
msgstr "id"
|
msgstr "id"
|
||||||
|
|
||||||
#: core/models.py:118
|
#: core/models.py:119
|
||||||
msgid "primary key for the record as UUID"
|
msgid "primary key for the record as UUID"
|
||||||
msgstr "clé primaire pour l'enregistrement sous forme d'UUID"
|
msgstr "clé primaire pour l'enregistrement sous forme d'UUID"
|
||||||
|
|
||||||
#: core/models.py:124
|
#: core/models.py:125
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr "créé le"
|
msgstr "créé le"
|
||||||
|
|
||||||
#: core/models.py:125
|
#: core/models.py:126
|
||||||
msgid "date and time at which a record was created"
|
msgid "date and time at which a record was created"
|
||||||
msgstr "date et heure auxquelles un enregistrement a été créé"
|
msgstr "date et heure auxquelles un enregistrement a été créé"
|
||||||
|
|
||||||
#: core/models.py:130
|
#: core/models.py:131
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
msgstr "mis à jour le"
|
msgstr "mis à jour le"
|
||||||
|
|
||||||
#: core/models.py:131
|
#: core/models.py:132
|
||||||
msgid "date and time at which a record was last updated"
|
msgid "date and time at which a record was last updated"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"date et heure auxquelles un enregistrement a été mis à jour pour la dernière "
|
"date et heure auxquelles un enregistrement a été mis à jour pour la dernière "
|
||||||
"fois"
|
"fois"
|
||||||
|
|
||||||
#: core/models.py:151
|
#: core/models.py:150
|
||||||
msgid ""
|
|
||||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
|
||||||
"_ characters."
|
|
||||||
msgstr ""
|
|
||||||
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des "
|
|
||||||
"chiffres et les caractères @/./+/-/_."
|
|
||||||
|
|
||||||
#: core/models.py:157
|
|
||||||
msgid "sub"
|
msgid "sub"
|
||||||
msgstr "sub"
|
msgstr "sub"
|
||||||
|
|
||||||
#: core/models.py:159
|
#: core/models.py:152
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optional for pending users; required upon account activation. 255 characters "
|
"Optional for pending users; required upon account activation. 255 characters "
|
||||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
"or fewer. Printable ASCII characters only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Optionnel pour les utilisateurs en attente ; requis lors de l'activation du "
|
"Facultatif pour les utilisateurs en attente ; obligatoire lors de l’activation "
|
||||||
"compte. 255 caractères maximum. Lettres, chiffres et @/./+/-/_ uniquement."
|
"du compte. 255 caractères maximum. Caractères ASCII imprimables uniquement."
|
||||||
|
|
||||||
#: core/models.py:168
|
#: core/validators.py:22
|
||||||
|
msgid "Enter a valid sub. This value should be printable ASCII only."
|
||||||
|
msgstr "Saisissez un sub valide. Cette valeur ne doit contenir que des caractères ASCII imprimables."
|
||||||
|
|
||||||
|
#: core/models.py:161
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "adresse e-mail d'identité"
|
msgstr "adresse e-mail d'identité"
|
||||||
|
|
||||||
#: core/models.py:173
|
#: core/models.py:166
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "adresse e-mail d'administrateur"
|
msgstr "adresse e-mail d'administrateur"
|
||||||
|
|
||||||
#: core/models.py:175
|
#: core/models.py:168
|
||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "nom complet"
|
msgstr "nom complet"
|
||||||
|
|
||||||
#: core/models.py:177
|
#: core/models.py:170
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "nom court"
|
msgstr "nom court"
|
||||||
|
|
||||||
#: core/models.py:183
|
#: core/models.py:176
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "langue"
|
msgstr "langue"
|
||||||
|
|
||||||
#: core/models.py:184
|
#: core/models.py:177
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
||||||
|
|
||||||
#: core/models.py:190
|
#: core/models.py:183
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||||
|
|
||||||
#: core/models.py:193
|
#: core/models.py:190
|
||||||
|
msgid "default room access level"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:192
|
||||||
|
msgid ""
|
||||||
|
"Access level applied by default to new rooms created by this user. When "
|
||||||
|
"empty, the instance default is used."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:199
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Visio room configuration"
|
||||||
|
msgid "default room configuration"
|
||||||
|
msgstr "Configuration de la salle de visioconférence"
|
||||||
|
|
||||||
|
#: core/models.py:201
|
||||||
|
msgid "Configurations applied by default to new rooms created by this user."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:205
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "appareil"
|
msgstr "appareil"
|
||||||
|
|
||||||
#: core/models.py:195
|
#: core/models.py:207
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||||
|
|
||||||
#: core/models.py:198
|
#: core/models.py:210
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "statut du personnel"
|
msgstr "statut du personnel"
|
||||||
|
|
||||||
#: core/models.py:200
|
#: core/models.py:212
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||||
|
|
||||||
#: core/models.py:203
|
#: core/models.py:215
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "actif"
|
msgstr "actif"
|
||||||
|
|
||||||
#: core/models.py:206
|
#: core/models.py:218
|
||||||
msgid ""
|
msgid ""
|
||||||
"Whether this user should be treated as active. Unselect this instead of "
|
"Whether this user should be treated as active. Unselect this instead of "
|
||||||
"deleting accounts."
|
"deleting accounts."
|
||||||
@@ -299,65 +315,65 @@ msgstr ""
|
|||||||
"Si cet utilisateur doit être traité comme actif. Désélectionnez cette option "
|
"Si cet utilisateur doit être traité comme actif. Désélectionnez cette option "
|
||||||
"au lieu de supprimer des comptes."
|
"au lieu de supprimer des comptes."
|
||||||
|
|
||||||
#: core/models.py:219
|
#: core/models.py:231
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "utilisateur"
|
msgstr "utilisateur"
|
||||||
|
|
||||||
#: core/models.py:220
|
#: core/models.py:232
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "utilisateurs"
|
msgstr "utilisateurs"
|
||||||
|
|
||||||
#: core/models.py:286
|
#: core/models.py:298
|
||||||
msgid "Resource"
|
msgid "Resource"
|
||||||
msgstr "Ressource"
|
msgstr "Ressource"
|
||||||
|
|
||||||
#: core/models.py:287
|
#: core/models.py:299
|
||||||
msgid "Resources"
|
msgid "Resources"
|
||||||
msgstr "Ressources"
|
msgstr "Ressources"
|
||||||
|
|
||||||
#: core/models.py:345
|
#: core/models.py:357
|
||||||
msgid "Resource access"
|
msgid "Resource access"
|
||||||
msgstr "Accès aux ressources"
|
msgstr "Accès aux ressources"
|
||||||
|
|
||||||
#: core/models.py:346
|
#: core/models.py:358
|
||||||
msgid "Resource accesses"
|
msgid "Resource accesses"
|
||||||
msgstr "Accès aux ressources"
|
msgstr "Accès aux ressources"
|
||||||
|
|
||||||
#: core/models.py:352
|
#: core/models.py:364
|
||||||
msgid "Resource access with this User and Resource already exists."
|
msgid "Resource access with this User and Resource already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"L'accès à la ressource avec cet utilisateur et cette ressource existe déjà."
|
"L'accès à la ressource avec cet utilisateur et cette ressource existe déjà."
|
||||||
|
|
||||||
#: core/models.py:409
|
#: core/models.py:421
|
||||||
msgid "Visio room configuration"
|
msgid "Visio room configuration"
|
||||||
msgstr "Configuration de la salle de visioconférence"
|
msgstr "Configuration de la salle de visioconférence"
|
||||||
|
|
||||||
#: core/models.py:410
|
#: core/models.py:422
|
||||||
msgid "Values for Visio parameters to configure the room."
|
msgid "Values for Visio parameters to configure the room."
|
||||||
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
|
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
|
||||||
|
|
||||||
#: core/models.py:417
|
#: core/models.py:429
|
||||||
msgid "Room PIN code"
|
msgid "Room PIN code"
|
||||||
msgstr "Code PIN de la salle"
|
msgstr "Code PIN de la salle"
|
||||||
|
|
||||||
#: core/models.py:418
|
#: core/models.py:430
|
||||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Code unique à n chiffres qui identifie cette salle en mode téléphonique."
|
"Code unique à n chiffres qui identifie cette salle en mode téléphonique."
|
||||||
|
|
||||||
#: core/models.py:424 core/models.py:578
|
#: core/models.py:436 core/models.py:597
|
||||||
msgid "Room"
|
msgid "Room"
|
||||||
msgstr "Salle"
|
msgstr "Salle"
|
||||||
|
|
||||||
#: core/models.py:425
|
#: core/models.py:437
|
||||||
msgid "Rooms"
|
msgid "Rooms"
|
||||||
msgstr "Salles"
|
msgstr "Salles"
|
||||||
|
|
||||||
#: core/models.py:589
|
#: core/models.py:608
|
||||||
msgid "Worker ID"
|
msgid "Worker ID"
|
||||||
msgstr "ID du Worker"
|
msgstr "ID du Worker"
|
||||||
|
|
||||||
#: core/models.py:591
|
#: core/models.py:610
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||||
"the worker stops, allowing for easy tracking."
|
"the worker stops, allowing for easy tracking."
|
||||||
@@ -365,170 +381,170 @@ msgstr ""
|
|||||||
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est "
|
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est "
|
||||||
"conservé même lorsque le Worker s'arrête, permettant un suivi facile."
|
"conservé même lorsque le Worker s'arrête, permettant un suivi facile."
|
||||||
|
|
||||||
#: core/models.py:599
|
#: core/models.py:618
|
||||||
msgid "Recording mode"
|
msgid "Recording mode"
|
||||||
msgstr "Mode d'enregistrement"
|
msgstr "Mode d'enregistrement"
|
||||||
|
|
||||||
#: core/models.py:600
|
#: core/models.py:619
|
||||||
msgid "Defines the mode of recording being called."
|
msgid "Defines the mode of recording being called."
|
||||||
msgstr "Définit le mode d'enregistrement appelé."
|
msgstr "Définit le mode d'enregistrement appelé."
|
||||||
|
|
||||||
#: core/models.py:605 core/models.py:606
|
#: core/models.py:624 core/models.py:625
|
||||||
msgid "Recording options"
|
msgid "Recording options"
|
||||||
msgstr "Options d'enregistrement"
|
msgstr "Options d'enregistrement"
|
||||||
|
|
||||||
#: core/models.py:613
|
#: core/models.py:632
|
||||||
msgid "External Process ID"
|
msgid "External Process ID"
|
||||||
msgstr "ID Traitement externe"
|
msgstr "ID Traitement externe"
|
||||||
|
|
||||||
#: core/models.py:614
|
#: core/models.py:633
|
||||||
msgid "ID of the external process associated with the recording."
|
msgid "ID of the external process associated with the recording."
|
||||||
msgstr "ID du traitement externe associé avec l'enregistrement."
|
msgstr "ID du traitement externe associé avec l'enregistrement."
|
||||||
|
|
||||||
#: core/models.py:620
|
#: core/models.py:639
|
||||||
msgid "Recording"
|
msgid "Recording"
|
||||||
msgstr "Enregistrement"
|
msgstr "Enregistrement"
|
||||||
|
|
||||||
#: core/models.py:621
|
#: core/models.py:640
|
||||||
msgid "Recordings"
|
msgid "Recordings"
|
||||||
msgstr "Enregistrements"
|
msgstr "Enregistrements"
|
||||||
|
|
||||||
#: core/models.py:731
|
#: core/models.py:750
|
||||||
msgid "Recording/user relation"
|
msgid "Recording/user relation"
|
||||||
msgstr "Relation enregistrement/utilisateur"
|
msgstr "Relation enregistrement/utilisateur"
|
||||||
|
|
||||||
#: core/models.py:732
|
#: core/models.py:751
|
||||||
msgid "Recording/user relations"
|
msgid "Recording/user relations"
|
||||||
msgstr "Relations enregistrement/utilisateur"
|
msgstr "Relations enregistrement/utilisateur"
|
||||||
|
|
||||||
#: core/models.py:738
|
#: core/models.py:757
|
||||||
msgid "This user is already in this recording."
|
msgid "This user is already in this recording."
|
||||||
msgstr "Cet utilisateur est déjà dans cet enregistrement."
|
msgstr "Cet utilisateur est déjà dans cet enregistrement."
|
||||||
|
|
||||||
#: core/models.py:744
|
#: core/models.py:763
|
||||||
msgid "This team is already in this recording."
|
msgid "This team is already in this recording."
|
||||||
msgstr "Cette équipe est déjà dans cet enregistrement."
|
msgstr "Cette équipe est déjà dans cet enregistrement."
|
||||||
|
|
||||||
#: core/models.py:750
|
#: core/models.py:769
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
|
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
|
||||||
|
|
||||||
#: core/models.py:767
|
#: core/models.py:786
|
||||||
msgid "Create rooms"
|
msgid "Create rooms"
|
||||||
msgstr "Créer des salles"
|
msgstr "Créer des salles"
|
||||||
|
|
||||||
#: core/models.py:768
|
#: core/models.py:787
|
||||||
msgid "List rooms"
|
msgid "List rooms"
|
||||||
msgstr "Lister les salles"
|
msgstr "Lister les salles"
|
||||||
|
|
||||||
#: core/models.py:769
|
#: core/models.py:788
|
||||||
msgid "Retrieve room details"
|
msgid "Retrieve room details"
|
||||||
msgstr "Afficher les détails d’une salle"
|
msgstr "Afficher les détails d’une salle"
|
||||||
|
|
||||||
#: core/models.py:770
|
#: core/models.py:789
|
||||||
msgid "Update rooms"
|
msgid "Update rooms"
|
||||||
msgstr "Mettre à jour les salles"
|
msgstr "Mettre à jour les salles"
|
||||||
|
|
||||||
#: core/models.py:771
|
#: core/models.py:790
|
||||||
msgid "Delete rooms"
|
msgid "Delete rooms"
|
||||||
msgstr "Supprimer les salles"
|
msgstr "Supprimer les salles"
|
||||||
|
|
||||||
#: core/models.py:784
|
#: core/models.py:803
|
||||||
msgid "Application name"
|
msgid "Application name"
|
||||||
msgstr "Nom de l’application"
|
msgstr "Nom de l’application"
|
||||||
|
|
||||||
#: core/models.py:785
|
#: core/models.py:804
|
||||||
msgid "Descriptive name for this application."
|
msgid "Descriptive name for this application."
|
||||||
msgstr "Nom descriptif de cette application."
|
msgstr "Nom descriptif de cette application."
|
||||||
|
|
||||||
#: core/models.py:795
|
#: core/models.py:814
|
||||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Haché lors de l’enregistrement. Copiez-le maintenant s’il s’agit d’un "
|
"Haché lors de l’enregistrement. Copiez-le maintenant s’il s’agit d’un "
|
||||||
"nouveau secret."
|
"nouveau secret."
|
||||||
|
|
||||||
#: core/models.py:806
|
#: core/models.py:825
|
||||||
msgid "Application"
|
msgid "Application"
|
||||||
msgstr "Application"
|
msgstr "Application"
|
||||||
|
|
||||||
#: core/models.py:807
|
#: core/models.py:826
|
||||||
msgid "Applications"
|
msgid "Applications"
|
||||||
msgstr "Applications"
|
msgstr "Applications"
|
||||||
|
|
||||||
#: core/models.py:830
|
#: core/models.py:849
|
||||||
msgid "Enter a valid domain"
|
msgid "Enter a valid domain"
|
||||||
msgstr "Saisissez un domaine valide"
|
msgstr "Saisissez un domaine valide"
|
||||||
|
|
||||||
#: core/models.py:833
|
#: core/models.py:852
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr "Domaine"
|
msgstr "Domaine"
|
||||||
|
|
||||||
#: core/models.py:834
|
#: core/models.py:853
|
||||||
msgid "Email domain this application can act on behalf of."
|
msgid "Email domain this application can act on behalf of."
|
||||||
msgstr "Domaine de messagerie au nom duquel cette application peut agir."
|
msgstr "Domaine de messagerie au nom duquel cette application peut agir."
|
||||||
|
|
||||||
#: core/models.py:846
|
#: core/models.py:865
|
||||||
msgid "Application domain"
|
msgid "Application domain"
|
||||||
msgstr "Domaine d’application"
|
msgstr "Domaine d’application"
|
||||||
|
|
||||||
#: core/models.py:847
|
#: core/models.py:866
|
||||||
msgid "Application domains"
|
msgid "Application domains"
|
||||||
msgstr "Domaines d’application"
|
msgstr "Domaines d’application"
|
||||||
|
|
||||||
#: core/models.py:865
|
#: core/models.py:884
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr "En attente"
|
msgstr "En attente"
|
||||||
|
|
||||||
#: core/models.py:866
|
#: core/models.py:885
|
||||||
msgid "Analyzing"
|
msgid "Analyzing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/models.py:873
|
#: core/models.py:892
|
||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr "Prêt"
|
msgstr "Prêt"
|
||||||
|
|
||||||
#: core/models.py:879
|
#: core/models.py:898
|
||||||
msgid "Background image"
|
msgid "Background image"
|
||||||
msgstr "Image de fond"
|
msgstr "Image de fond"
|
||||||
|
|
||||||
#: core/models.py:891
|
#: core/models.py:910
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "Titre"
|
msgstr "Titre"
|
||||||
|
|
||||||
#: core/models.py:915
|
#: core/models.py:934
|
||||||
msgid "Malware detection info when the analysis status is unsafe."
|
msgid "Malware detection info when the analysis status is unsafe."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Information concernant la détection de Malware cand le statut n'est pas sain"
|
"Information concernant la détection de Malware cand le statut n'est pas sain"
|
||||||
|
|
||||||
#: core/models.py:920
|
#: core/models.py:939
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "Fichier"
|
msgstr "Fichier"
|
||||||
|
|
||||||
#: core/models.py:921
|
#: core/models.py:940
|
||||||
msgid "Files"
|
msgid "Files"
|
||||||
msgstr "Fichiers"
|
msgstr "Fichiers"
|
||||||
|
|
||||||
#: core/models.py:1041
|
#: core/models.py:1060
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "This user is already in this recording."
|
#| msgid "This user is already in this recording."
|
||||||
msgid "This file is already hard deleted."
|
msgid "This file is already hard deleted."
|
||||||
msgstr "Ce fichier a été supprimé."
|
msgstr "Ce fichier a été supprimé."
|
||||||
|
|
||||||
#: core/models.py:1051
|
#: core/models.py:1070
|
||||||
msgid "To hard delete a file, it must first be soft deleted."
|
msgid "To hard delete a file, it must first be soft deleted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Pour supprimer définitivement un fichier il doit d'abord avoir été marqué "
|
"Pour supprimer définitivement un fichier il doit d'abord avoir été marqué "
|
||||||
"comme supprimé (soft delete)"
|
"comme supprimé (soft delete)"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:123
|
#: core/recording/event/notification.py:124
|
||||||
msgid "Your recording is ready"
|
msgid "Your recording is ready"
|
||||||
msgstr "Votre enregistrement est prêt"
|
msgstr "Votre enregistrement est prêt"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:194
|
#: core/recording/event/notification.py:195
|
||||||
msgid "Transcription"
|
msgid "Transcription"
|
||||||
msgstr "Transcription"
|
msgstr "Transcription"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:204
|
#: core/recording/event/notification.py:206
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
||||||
msgstr "Réunion \"{room}\" du {room_recording_date} à {room_recording_time}"
|
msgstr "Réunion \"{room}\" du {room_recording_date} à {room_recording_time}"
|
||||||
@@ -538,25 +554,25 @@ msgstr "Réunion \"{room}\" du {room_recording_date} à {room_recording_time}"
|
|||||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||||
msgstr "Appel vidéo en cours : {sender.email} attend que vous vous connectiez"
|
msgstr "Appel vidéo en cours : {sender.email} attend que vous vous connectiez"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:159
|
#: core/templates/mail/html/invitation.html:151
|
||||||
#: core/templates/mail/html/screen_recording.html:159
|
#: core/templates/mail/html/screen_recording.html:151
|
||||||
#: core/templates/mail/text/invitation.txt:3
|
#: core/templates/mail/text/invitation.txt:4
|
||||||
#: core/templates/mail/text/screen_recording.txt:3
|
#: core/templates/mail/text/screen_recording.txt:4
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo email"
|
msgstr "Logo email"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:189
|
#: core/templates/mail/html/invitation.html:181
|
||||||
#: core/templates/mail/text/invitation.txt:5
|
#: core/templates/mail/text/invitation.txt:6
|
||||||
msgid "invites you to join an ongoing video call"
|
msgid "invites you to join an ongoing video call"
|
||||||
msgstr "vous invite à rejoindre un appel vidéo en cours"
|
msgstr "vous invite à rejoindre un appel vidéo en cours"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:200
|
#: core/templates/mail/html/invitation.html:192
|
||||||
#: core/templates/mail/text/invitation.txt:7
|
#: core/templates/mail/text/invitation.txt:8
|
||||||
msgid "JOIN THE CALL"
|
msgid "JOIN THE CALL"
|
||||||
msgstr "REJOINDRE L'APPEL"
|
msgstr "REJOINDRE L'APPEL"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:227
|
#: core/templates/mail/html/invitation.html:219
|
||||||
#: core/templates/mail/text/invitation.txt:13
|
#: core/templates/mail/text/invitation.txt:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you can't click the button, copy and paste the URL into your browser to "
|
"If you can't click the button, copy and paste the URL into your browser to "
|
||||||
"join the call."
|
"join the call."
|
||||||
@@ -564,41 +580,47 @@ msgstr ""
|
|||||||
"Si vous ne pouvez pas cliquer sur le bouton, copiez et collez l'URL dans "
|
"Si vous ne pouvez pas cliquer sur le bouton, copiez et collez l'URL dans "
|
||||||
"votre navigateur pour rejoindre l'appel."
|
"votre navigateur pour rejoindre l'appel."
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:235
|
#: core/templates/mail/html/invitation.html:227
|
||||||
#: core/templates/mail/text/invitation.txt:15
|
#: core/templates/mail/text/invitation.txt:16
|
||||||
msgid "Tips for a better experience:"
|
msgid "Tips for a better experience:"
|
||||||
msgstr "Conseils pour une meilleure expérience :"
|
msgstr "Conseils pour une meilleure expérience :"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:237
|
#: core/templates/mail/html/invitation.html:229
|
||||||
#: core/templates/mail/text/invitation.txt:17
|
#: core/templates/mail/text/invitation.txt:18
|
||||||
msgid "Use Chrome or Firefox for better call quality"
|
msgid "Use Chrome or Firefox for better call quality"
|
||||||
msgstr "Utilisez Chrome ou Firefox pour une meilleure qualité d'appel"
|
msgstr "Utilisez Chrome ou Firefox pour une meilleure qualité d'appel"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:238
|
#: core/templates/mail/html/invitation.html:230
|
||||||
#: core/templates/mail/text/invitation.txt:18
|
#: core/templates/mail/text/invitation.txt:19
|
||||||
msgid "Test your microphone and camera before joining"
|
msgid "Test your microphone and camera before joining"
|
||||||
msgstr "Testez votre microphone et votre caméra avant de rejoindre"
|
msgstr "Testez votre microphone et votre caméra avant de rejoindre"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:239
|
#: core/templates/mail/html/invitation.html:231
|
||||||
#: core/templates/mail/text/invitation.txt:19
|
#: core/templates/mail/text/invitation.txt:20
|
||||||
msgid "Make sure you have a stable internet connection"
|
msgid "Make sure you have a stable internet connection"
|
||||||
msgstr "Assurez-vous d'avoir une connexion Internet stable"
|
msgstr "Assurez-vous d'avoir une connexion Internet stable"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:248
|
#: core/templates/mail/html/invitation.html:240
|
||||||
#: core/templates/mail/html/screen_recording.html:245
|
#: core/templates/mail/html/screen_recording.html:237
|
||||||
#: core/templates/mail/text/invitation.txt:21
|
#: core/templates/mail/text/invitation.txt:22
|
||||||
#: core/templates/mail/text/screen_recording.txt:23
|
#: core/templates/mail/text/screen_recording.txt:24
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Thank you for using %(brandname)s. "
|
msgid " Thank you for using %(brandname)s. "
|
||||||
msgstr " Merci d'utiliser %(brandname)s. "
|
msgstr " Merci d'utiliser %(brandname)s. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:188
|
#: core/templates/mail/html/invitation.html:271
|
||||||
#: core/templates/mail/text/screen_recording.txt:6
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/html/screen_recording.html:180
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:7
|
||||||
msgid "Your recording is ready!"
|
msgid "Your recording is ready!"
|
||||||
msgstr "Votre enregistrement est prêt !"
|
msgstr "Votre enregistrement est prêt !"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
||||||
@@ -607,14 +629,14 @@ msgstr ""
|
|||||||
" Votre enregistrement de \"%(room_name)s\" du %(recording_date)s à "
|
" Votre enregistrement de \"%(room_name)s\" du %(recording_date)s à "
|
||||||
"%(recording_time)s est maintenant prêt à être téléchargé. "
|
"%(recording_time)s est maintenant prêt à être téléchargé. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " The recording will expire in %(days)s days. "
|
msgid " The recording will expire in %(days)s days. "
|
||||||
msgstr " L'enregistrement expirera dans %(days)s jours. "
|
msgstr " L'enregistrement expirera dans %(days)s jours. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:200
|
#: core/templates/mail/html/screen_recording.html:192
|
||||||
#: core/templates/mail/text/screen_recording.txt:9
|
#: core/templates/mail/text/screen_recording.txt:10
|
||||||
msgid ""
|
msgid ""
|
||||||
" Sharing the recording via link is not yet available. Only organizers can "
|
" Sharing the recording via link is not yet available. Only organizers can "
|
||||||
"download it. "
|
"download it. "
|
||||||
@@ -622,33 +644,33 @@ msgstr ""
|
|||||||
"Le partage de l'enregistrement via lien n'est pas encore disponible. Seuls "
|
"Le partage de l'enregistrement via lien n'est pas encore disponible. Seuls "
|
||||||
"les organisateurs peuvent le télécharger."
|
"les organisateurs peuvent le télécharger."
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:206
|
#: core/templates/mail/html/screen_recording.html:198
|
||||||
#: core/templates/mail/text/screen_recording.txt:11
|
#: core/templates/mail/text/screen_recording.txt:12
|
||||||
msgid "To keep this recording permanently:"
|
msgid "To keep this recording permanently:"
|
||||||
msgstr "Pour conserver cet enregistrement de façon permanente :"
|
msgstr "Pour conserver cet enregistrement de façon permanente :"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:208
|
#: core/templates/mail/html/screen_recording.html:200
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgstr "Cliquez sur le lien \"<a href=\"%(link)s\">Ouvrir</a>\" ci-dessous "
|
msgstr "Cliquez sur le lien \"<a href=\"%(link)s\">Ouvrir</a>\" ci-dessous "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:209
|
#: core/templates/mail/html/screen_recording.html:201
|
||||||
#: core/templates/mail/text/screen_recording.txt:14
|
#: core/templates/mail/text/screen_recording.txt:15
|
||||||
msgid "Use the \"Download\" button in the interface "
|
msgid "Use the \"Download\" button in the interface "
|
||||||
msgstr "Utilisez le bouton \"Télécharger\" dans l'interface "
|
msgstr "Utilisez le bouton \"Télécharger\" dans l'interface "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:210
|
#: core/templates/mail/html/screen_recording.html:202
|
||||||
#: core/templates/mail/text/screen_recording.txt:15
|
#: core/templates/mail/text/screen_recording.txt:16
|
||||||
msgid "Save the file to your preferred location"
|
msgid "Save the file to your preferred location"
|
||||||
msgstr "Enregistrez le fichier à l'emplacement de votre choix"
|
msgstr "Enregistrez le fichier à l'emplacement de votre choix"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:221
|
#: core/templates/mail/html/screen_recording.html:213
|
||||||
#: core/templates/mail/text/screen_recording.txt:17
|
#: core/templates/mail/text/screen_recording.txt:18
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Ouvrir"
|
msgstr "Ouvrir"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:230
|
#: core/templates/mail/html/screen_recording.html:222
|
||||||
#: core/templates/mail/text/screen_recording.txt:19
|
#: core/templates/mail/text/screen_recording.txt:20
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" If you have any questions or need assistance, please contact our support "
|
" If you have any questions or need assistance, please contact our support "
|
||||||
@@ -657,28 +679,33 @@ msgstr ""
|
|||||||
" Si vous avez des questions ou besoin d'assistance, veuillez contacter notre "
|
" Si vous avez des questions ou besoin d'assistance, veuillez contacter notre "
|
||||||
"équipe d'assistance à %(support_email)s. "
|
"équipe d'assistance à %(support_email)s. "
|
||||||
|
|
||||||
#: core/templates/mail/text/screen_recording.txt:13
|
#: core/templates/mail/text/invitation.txt:24
|
||||||
|
#, python-format
|
||||||
|
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:14
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgid "Click the \"Open [%(link)s]\" link below "
|
msgid "Click the \"Open [%(link)s]\" link below "
|
||||||
msgstr "Cliquez sur le lien \"<a href=\"%(link)s\">Ouvrir</a>\" ci-dessous "
|
msgstr "Cliquez sur le lien \"<a href=\"%(link)s\">Ouvrir</a>\" ci-dessous "
|
||||||
|
|
||||||
#: meet/settings.py:228
|
#: meet/settings.py:238
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "Anglais"
|
msgstr "Anglais"
|
||||||
|
|
||||||
#: meet/settings.py:229
|
#: meet/settings.py:239
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr "Français"
|
msgstr "Français"
|
||||||
|
|
||||||
#: meet/settings.py:230
|
#: meet/settings.py:240
|
||||||
msgid "Dutch"
|
msgid "Dutch"
|
||||||
msgstr "Néerlandais"
|
msgstr "Néerlandais"
|
||||||
|
|
||||||
#: meet/settings.py:231
|
#: meet/settings.py:241
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr "Allemand"
|
msgstr "Allemand"
|
||||||
|
|
||||||
#: meet/settings.py:233
|
#: meet/settings.py:242
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
msgstr "Espagnol"
|
msgstr "Espagnol"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-02 10:47+0000\n"
|
"POT-Creation-Date: 2026-09-02 22:52+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -51,11 +51,11 @@ msgstr ""
|
|||||||
msgid "File preview"
|
msgid "File preview"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/admin.py:300 core/admin.py:443
|
#: core/admin.py:300 core/admin.py:450
|
||||||
msgid "No owner"
|
msgid "No owner"
|
||||||
msgstr "Geen eigenaar"
|
msgstr "Geen eigenaar"
|
||||||
|
|
||||||
#: core/admin.py:303 core/admin.py:446
|
#: core/admin.py:303 core/admin.py:453
|
||||||
msgid "Multiple owners"
|
msgid "Multiple owners"
|
||||||
msgstr "Meerdere eigenaren"
|
msgstr "Meerdere eigenaren"
|
||||||
|
|
||||||
@@ -98,11 +98,11 @@ msgstr "%(count)s opname(s) succesvol gemarkeerd als 'Mislukt bij stoppen'."
|
|||||||
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
msgid "Skipped %(count)s recording(s) with an ineligible status."
|
||||||
msgstr "%(count)s opname(s) met een niet-toegestane status overgeslagen."
|
msgstr "%(count)s opname(s) met een niet-toegestane status overgeslagen."
|
||||||
|
|
||||||
#: core/admin.py:510
|
#: core/admin.py:517
|
||||||
msgid "No scopes"
|
msgid "No scopes"
|
||||||
msgstr "Geen scopes"
|
msgstr "Geen scopes"
|
||||||
|
|
||||||
#: core/admin.py:512
|
#: core/admin.py:519
|
||||||
msgid "Scopes"
|
msgid "Scopes"
|
||||||
msgstr "Scopes"
|
msgstr "Scopes"
|
||||||
|
|
||||||
@@ -110,184 +110,200 @@ msgstr "Scopes"
|
|||||||
msgid "Creator is me"
|
msgid "Creator is me"
|
||||||
msgstr "Maker ben ik"
|
msgstr "Maker ben ik"
|
||||||
|
|
||||||
#: core/api/serializers.py:89
|
#: core/api/serializers.py:108
|
||||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
|
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
|
||||||
|
|
||||||
#: core/api/serializers.py:534
|
#: core/api/serializers.py:560
|
||||||
msgid "This file extension is not allowed."
|
msgid "This file extension is not allowed."
|
||||||
msgstr "Deze bestandsextensie is niet toegestaan."
|
msgstr "Deze bestandsextensie is niet toegestaan."
|
||||||
|
|
||||||
#: core/api/viewsets.py:1222
|
#: core/api/viewsets.py:1268
|
||||||
msgid "You have reached the maximum number of files for this type."
|
msgid "You have reached the maximum number of files for this type."
|
||||||
msgstr "Het maximale aantal bestanden voor dit type is bereikt."
|
msgstr "Het maximale aantal bestanden voor dit type is bereikt."
|
||||||
|
|
||||||
#: core/models.py:37
|
#: core/models.py:38
|
||||||
msgid "Member"
|
msgid "Member"
|
||||||
msgstr "Lid"
|
msgstr "Lid"
|
||||||
|
|
||||||
#: core/models.py:38
|
#: core/models.py:39
|
||||||
msgid "Administrator"
|
msgid "Administrator"
|
||||||
msgstr "Beheerder"
|
msgstr "Beheerder"
|
||||||
|
|
||||||
#: core/models.py:39
|
#: core/models.py:40
|
||||||
msgid "Owner"
|
msgid "Owner"
|
||||||
msgstr "Eigenaar"
|
msgstr "Eigenaar"
|
||||||
|
|
||||||
#: core/models.py:55
|
#: core/models.py:56
|
||||||
msgid "Initiated"
|
msgid "Initiated"
|
||||||
msgstr "Gestart"
|
msgstr "Gestart"
|
||||||
|
|
||||||
#: core/models.py:56
|
#: core/models.py:57
|
||||||
msgid "Active"
|
msgid "Active"
|
||||||
msgstr "Actief"
|
msgstr "Actief"
|
||||||
|
|
||||||
#: core/models.py:57
|
#: core/models.py:58
|
||||||
msgid "Stopped"
|
msgid "Stopped"
|
||||||
msgstr "Gestopt"
|
msgstr "Gestopt"
|
||||||
|
|
||||||
#: core/models.py:58
|
#: core/models.py:59
|
||||||
msgid "Saved"
|
msgid "Saved"
|
||||||
msgstr "Opgeslagen"
|
msgstr "Opgeslagen"
|
||||||
|
|
||||||
#: core/models.py:59
|
#: core/models.py:60
|
||||||
msgid "Aborted"
|
msgid "Aborted"
|
||||||
msgstr "Afgebroken"
|
msgstr "Afgebroken"
|
||||||
|
|
||||||
#: core/models.py:60
|
#: core/models.py:61
|
||||||
msgid "Failed to Start"
|
msgid "Failed to Start"
|
||||||
msgstr "Starten mislukt"
|
msgstr "Starten mislukt"
|
||||||
|
|
||||||
#: core/models.py:61
|
#: core/models.py:62
|
||||||
msgid "Failed to Stop"
|
msgid "Failed to Stop"
|
||||||
msgstr "Stoppen mislukt"
|
msgstr "Stoppen mislukt"
|
||||||
|
|
||||||
#: core/models.py:62
|
#: core/models.py:63
|
||||||
msgid "Notification succeeded"
|
msgid "Notification succeeded"
|
||||||
msgstr "Notificatie geslaagd"
|
msgstr "Notificatie geslaagd"
|
||||||
|
|
||||||
#: core/models.py:65
|
#: core/models.py:66
|
||||||
msgid "External process successful"
|
msgid "External process successful"
|
||||||
msgstr "Externe procedure succesvol"
|
msgstr "Externe procedure succesvol"
|
||||||
|
|
||||||
#: core/models.py:67
|
#: core/models.py:68
|
||||||
msgid "External process failed"
|
msgid "External process failed"
|
||||||
msgstr "Het externe proces is mislukt."
|
msgstr "Het externe proces is mislukt."
|
||||||
|
|
||||||
#: core/models.py:96
|
#: core/models.py:97
|
||||||
msgid "SCREEN_RECORDING"
|
msgid "SCREEN_RECORDING"
|
||||||
msgstr "SCHERM_OPNAME"
|
msgstr "SCHERM_OPNAME"
|
||||||
|
|
||||||
#: core/models.py:97
|
#: core/models.py:98
|
||||||
msgid "TRANSCRIPT"
|
msgid "TRANSCRIPT"
|
||||||
msgstr "TRANSCRIPT"
|
msgstr "TRANSCRIPT"
|
||||||
|
|
||||||
#: core/models.py:103
|
#: core/models.py:104
|
||||||
msgid "Public Access"
|
msgid "Public Access"
|
||||||
msgstr "Openbare toegang"
|
msgstr "Openbare toegang"
|
||||||
|
|
||||||
#: core/models.py:104
|
#: core/models.py:105
|
||||||
msgid "Trusted Access"
|
msgid "Trusted Access"
|
||||||
msgstr "Vertrouwde toegang"
|
msgstr "Vertrouwde toegang"
|
||||||
|
|
||||||
#: core/models.py:105
|
#: core/models.py:106
|
||||||
msgid "Restricted Access"
|
msgid "Restricted Access"
|
||||||
msgstr "Beperkte toegang"
|
msgstr "Beperkte toegang"
|
||||||
|
|
||||||
#: core/models.py:117
|
#: core/models.py:118
|
||||||
msgid "id"
|
msgid "id"
|
||||||
msgstr "id"
|
msgstr "id"
|
||||||
|
|
||||||
#: core/models.py:118
|
#: core/models.py:119
|
||||||
msgid "primary key for the record as UUID"
|
msgid "primary key for the record as UUID"
|
||||||
msgstr "primaire sleutel voor het record als UUID"
|
msgstr "primaire sleutel voor het record als UUID"
|
||||||
|
|
||||||
#: core/models.py:124
|
#: core/models.py:125
|
||||||
msgid "created on"
|
msgid "created on"
|
||||||
msgstr "aangemaakt op"
|
msgstr "aangemaakt op"
|
||||||
|
|
||||||
#: core/models.py:125
|
#: core/models.py:126
|
||||||
msgid "date and time at which a record was created"
|
msgid "date and time at which a record was created"
|
||||||
msgstr "datum en tijd waarop een record werd aangemaakt"
|
msgstr "datum en tijd waarop een record werd aangemaakt"
|
||||||
|
|
||||||
#: core/models.py:130
|
#: core/models.py:131
|
||||||
msgid "updated on"
|
msgid "updated on"
|
||||||
msgstr "bijgewerkt op"
|
msgstr "bijgewerkt op"
|
||||||
|
|
||||||
#: core/models.py:131
|
#: core/models.py:132
|
||||||
msgid "date and time at which a record was last updated"
|
msgid "date and time at which a record was last updated"
|
||||||
msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt"
|
msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt"
|
||||||
|
|
||||||
#: core/models.py:151
|
#: core/models.py:150
|
||||||
msgid ""
|
|
||||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
|
||||||
"_ characters."
|
|
||||||
msgstr ""
|
|
||||||
"Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/"
|
|
||||||
"_ tekens bevatten."
|
|
||||||
|
|
||||||
#: core/models.py:157
|
|
||||||
msgid "sub"
|
msgid "sub"
|
||||||
msgstr "sub"
|
msgstr "sub"
|
||||||
|
|
||||||
#: core/models.py:159
|
#: core/models.py:152
|
||||||
msgid ""
|
msgid ""
|
||||||
"Optional for pending users; required upon account activation. 255 characters "
|
"Optional for pending users; required upon account activation. 255 characters "
|
||||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
"or fewer. Printable ASCII characters only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Optioneel voor gebruikers in afwachting; vereist bij accountactivering. "
|
"Optioneel voor gebruikers in afwachting; verplicht bij het activeren van het "
|
||||||
"Maximum 255 tekens. Alleen letters, cijfers en @/./+/-/_ toegestaan."
|
"account. Maximaal 255 tekens. Alleen afdrukbare ASCII-tekens."
|
||||||
|
|
||||||
#: core/models.py:168
|
#: core/validators.py:22
|
||||||
|
msgid "Enter a valid sub. This value should be printable ASCII only."
|
||||||
|
msgstr "Voer een geldige sub in. Deze waarde mag alleen afdrukbare ASCII-tekens bevatten."
|
||||||
|
|
||||||
|
#: core/models.py:161
|
||||||
msgid "identity email address"
|
msgid "identity email address"
|
||||||
msgstr "identiteit e-mailadres"
|
msgstr "identiteit e-mailadres"
|
||||||
|
|
||||||
#: core/models.py:173
|
#: core/models.py:166
|
||||||
msgid "admin email address"
|
msgid "admin email address"
|
||||||
msgstr "beheerder e-mailadres"
|
msgstr "beheerder e-mailadres"
|
||||||
|
|
||||||
#: core/models.py:175
|
#: core/models.py:168
|
||||||
msgid "full name"
|
msgid "full name"
|
||||||
msgstr "volledige naam"
|
msgstr "volledige naam"
|
||||||
|
|
||||||
#: core/models.py:177
|
#: core/models.py:170
|
||||||
msgid "short name"
|
msgid "short name"
|
||||||
msgstr "korte naam"
|
msgstr "korte naam"
|
||||||
|
|
||||||
#: core/models.py:183
|
#: core/models.py:176
|
||||||
msgid "language"
|
msgid "language"
|
||||||
msgstr "taal"
|
msgstr "taal"
|
||||||
|
|
||||||
#: core/models.py:184
|
#: core/models.py:177
|
||||||
msgid "The language in which the user wants to see the interface."
|
msgid "The language in which the user wants to see the interface."
|
||||||
msgstr "De taal waarin de gebruiker de interface wil zien."
|
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||||
|
|
||||||
#: core/models.py:190
|
#: core/models.py:183
|
||||||
msgid "The timezone in which the user wants to see times."
|
msgid "The timezone in which the user wants to see times."
|
||||||
msgstr "De tijdzone waarin de gebruiker tijden wil zien."
|
msgstr "De tijdzone waarin de gebruiker tijden wil zien."
|
||||||
|
|
||||||
#: core/models.py:193
|
#: core/models.py:190
|
||||||
|
msgid "default room access level"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:192
|
||||||
|
msgid ""
|
||||||
|
"Access level applied by default to new rooms created by this user. When "
|
||||||
|
"empty, the instance default is used."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:199
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Visio room configuration"
|
||||||
|
msgid "default room configuration"
|
||||||
|
msgstr "Visio-ruimteconfiguratie"
|
||||||
|
|
||||||
|
#: core/models.py:201
|
||||||
|
msgid "Configurations applied by default to new rooms created by this user."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/models.py:205
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr "apparaat"
|
msgstr "apparaat"
|
||||||
|
|
||||||
#: core/models.py:195
|
#: core/models.py:207
|
||||||
msgid "Whether the user is a device or a real user."
|
msgid "Whether the user is a device or a real user."
|
||||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||||
|
|
||||||
#: core/models.py:198
|
#: core/models.py:210
|
||||||
msgid "staff status"
|
msgid "staff status"
|
||||||
msgstr "personeelsstatus"
|
msgstr "personeelsstatus"
|
||||||
|
|
||||||
#: core/models.py:200
|
#: core/models.py:212
|
||||||
msgid "Whether the user can log into this admin site."
|
msgid "Whether the user can log into this admin site."
|
||||||
msgstr "Of de gebruiker kan inloggen op deze beheersite."
|
msgstr "Of de gebruiker kan inloggen op deze beheersite."
|
||||||
|
|
||||||
#: core/models.py:203
|
#: core/models.py:215
|
||||||
msgid "active"
|
msgid "active"
|
||||||
msgstr "actief"
|
msgstr "actief"
|
||||||
|
|
||||||
#: core/models.py:206
|
#: core/models.py:218
|
||||||
msgid ""
|
msgid ""
|
||||||
"Whether this user should be treated as active. Unselect this instead of "
|
"Whether this user should be treated as active. Unselect this instead of "
|
||||||
"deleting accounts."
|
"deleting accounts."
|
||||||
@@ -295,64 +311,64 @@ msgstr ""
|
|||||||
"Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in "
|
"Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in "
|
||||||
"plaats van accounts te verwijderen."
|
"plaats van accounts te verwijderen."
|
||||||
|
|
||||||
#: core/models.py:219
|
#: core/models.py:231
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "gebruiker"
|
msgstr "gebruiker"
|
||||||
|
|
||||||
#: core/models.py:220
|
#: core/models.py:232
|
||||||
msgid "users"
|
msgid "users"
|
||||||
msgstr "gebruikers"
|
msgstr "gebruikers"
|
||||||
|
|
||||||
#: core/models.py:286
|
#: core/models.py:298
|
||||||
msgid "Resource"
|
msgid "Resource"
|
||||||
msgstr "Bron"
|
msgstr "Bron"
|
||||||
|
|
||||||
#: core/models.py:287
|
#: core/models.py:299
|
||||||
msgid "Resources"
|
msgid "Resources"
|
||||||
msgstr "Bronnen"
|
msgstr "Bronnen"
|
||||||
|
|
||||||
#: core/models.py:345
|
#: core/models.py:357
|
||||||
msgid "Resource access"
|
msgid "Resource access"
|
||||||
msgstr "Brontoegang"
|
msgstr "Brontoegang"
|
||||||
|
|
||||||
#: core/models.py:346
|
#: core/models.py:358
|
||||||
msgid "Resource accesses"
|
msgid "Resource accesses"
|
||||||
msgstr "Brontoegangsrechten"
|
msgstr "Brontoegangsrechten"
|
||||||
|
|
||||||
#: core/models.py:352
|
#: core/models.py:364
|
||||||
msgid "Resource access with this User and Resource already exists."
|
msgid "Resource access with this User and Resource already exists."
|
||||||
msgstr "Brontoegang met deze gebruiker en bron bestaat al."
|
msgstr "Brontoegang met deze gebruiker en bron bestaat al."
|
||||||
|
|
||||||
#: core/models.py:409
|
#: core/models.py:421
|
||||||
msgid "Visio room configuration"
|
msgid "Visio room configuration"
|
||||||
msgstr "Visio-ruimteconfiguratie"
|
msgstr "Visio-ruimteconfiguratie"
|
||||||
|
|
||||||
#: core/models.py:410
|
#: core/models.py:422
|
||||||
msgid "Values for Visio parameters to configure the room."
|
msgid "Values for Visio parameters to configure the room."
|
||||||
msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
|
msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
|
||||||
|
|
||||||
#: core/models.py:417
|
#: core/models.py:429
|
||||||
msgid "Room PIN code"
|
msgid "Room PIN code"
|
||||||
msgstr "Pincode van de kamer"
|
msgstr "Pincode van de kamer"
|
||||||
|
|
||||||
#: core/models.py:418
|
#: core/models.py:430
|
||||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
|
"Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
|
||||||
|
|
||||||
#: core/models.py:424 core/models.py:578
|
#: core/models.py:436 core/models.py:597
|
||||||
msgid "Room"
|
msgid "Room"
|
||||||
msgstr "Ruimte"
|
msgstr "Ruimte"
|
||||||
|
|
||||||
#: core/models.py:425
|
#: core/models.py:437
|
||||||
msgid "Rooms"
|
msgid "Rooms"
|
||||||
msgstr "Ruimtes"
|
msgstr "Ruimtes"
|
||||||
|
|
||||||
#: core/models.py:589
|
#: core/models.py:608
|
||||||
msgid "Worker ID"
|
msgid "Worker ID"
|
||||||
msgstr "Worker ID"
|
msgstr "Worker ID"
|
||||||
|
|
||||||
#: core/models.py:591
|
#: core/models.py:610
|
||||||
msgid ""
|
msgid ""
|
||||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||||
"the worker stops, allowing for easy tracking."
|
"the worker stops, allowing for easy tracking."
|
||||||
@@ -360,152 +376,152 @@ msgstr ""
|
|||||||
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
|
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
|
||||||
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
|
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
|
||||||
|
|
||||||
#: core/models.py:599
|
#: core/models.py:618
|
||||||
msgid "Recording mode"
|
msgid "Recording mode"
|
||||||
msgstr "Opnamemodus"
|
msgstr "Opnamemodus"
|
||||||
|
|
||||||
#: core/models.py:600
|
#: core/models.py:619
|
||||||
msgid "Defines the mode of recording being called."
|
msgid "Defines the mode of recording being called."
|
||||||
msgstr "Definieert de modus van opname die wordt aangeroepen."
|
msgstr "Definieert de modus van opname die wordt aangeroepen."
|
||||||
|
|
||||||
#: core/models.py:605 core/models.py:606
|
#: core/models.py:624 core/models.py:625
|
||||||
msgid "Recording options"
|
msgid "Recording options"
|
||||||
msgstr "Opnameopties"
|
msgstr "Opnameopties"
|
||||||
|
|
||||||
#: core/models.py:613
|
#: core/models.py:632
|
||||||
msgid "External Process ID"
|
msgid "External Process ID"
|
||||||
msgstr "Externe proces-ID"
|
msgstr "Externe proces-ID"
|
||||||
|
|
||||||
#: core/models.py:614
|
#: core/models.py:633
|
||||||
msgid "ID of the external process associated with the recording."
|
msgid "ID of the external process associated with the recording."
|
||||||
msgstr "ID van het externe proces dat aan de opname is gekoppeld."
|
msgstr "ID van het externe proces dat aan de opname is gekoppeld."
|
||||||
|
|
||||||
#: core/models.py:620
|
#: core/models.py:639
|
||||||
msgid "Recording"
|
msgid "Recording"
|
||||||
msgstr "Opname"
|
msgstr "Opname"
|
||||||
|
|
||||||
#: core/models.py:621
|
#: core/models.py:640
|
||||||
msgid "Recordings"
|
msgid "Recordings"
|
||||||
msgstr "Opnames"
|
msgstr "Opnames"
|
||||||
|
|
||||||
#: core/models.py:731
|
#: core/models.py:750
|
||||||
msgid "Recording/user relation"
|
msgid "Recording/user relation"
|
||||||
msgstr "Opname/gebruiker-relatie"
|
msgstr "Opname/gebruiker-relatie"
|
||||||
|
|
||||||
#: core/models.py:732
|
#: core/models.py:751
|
||||||
msgid "Recording/user relations"
|
msgid "Recording/user relations"
|
||||||
msgstr "Opname/gebruiker-relaties"
|
msgstr "Opname/gebruiker-relaties"
|
||||||
|
|
||||||
#: core/models.py:738
|
#: core/models.py:757
|
||||||
msgid "This user is already in this recording."
|
msgid "This user is already in this recording."
|
||||||
msgstr "Deze gebruiker is al in deze opname."
|
msgstr "Deze gebruiker is al in deze opname."
|
||||||
|
|
||||||
#: core/models.py:744
|
#: core/models.py:763
|
||||||
msgid "This team is already in this recording."
|
msgid "This team is already in this recording."
|
||||||
msgstr "Dit team is al in deze opname."
|
msgstr "Dit team is al in deze opname."
|
||||||
|
|
||||||
#: core/models.py:750
|
#: core/models.py:769
|
||||||
msgid "Either user or team must be set, not both."
|
msgid "Either user or team must be set, not both."
|
||||||
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
|
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
|
||||||
|
|
||||||
#: core/models.py:767
|
#: core/models.py:786
|
||||||
msgid "Create rooms"
|
msgid "Create rooms"
|
||||||
msgstr "Ruimtes aanmaken"
|
msgstr "Ruimtes aanmaken"
|
||||||
|
|
||||||
#: core/models.py:768
|
#: core/models.py:787
|
||||||
msgid "List rooms"
|
msgid "List rooms"
|
||||||
msgstr "Ruimtes weergeven"
|
msgstr "Ruimtes weergeven"
|
||||||
|
|
||||||
#: core/models.py:769
|
#: core/models.py:788
|
||||||
msgid "Retrieve room details"
|
msgid "Retrieve room details"
|
||||||
msgstr "Details van een ruimte ophalen"
|
msgstr "Details van een ruimte ophalen"
|
||||||
|
|
||||||
#: core/models.py:770
|
#: core/models.py:789
|
||||||
msgid "Update rooms"
|
msgid "Update rooms"
|
||||||
msgstr "Ruimtes bijwerken"
|
msgstr "Ruimtes bijwerken"
|
||||||
|
|
||||||
#: core/models.py:771
|
#: core/models.py:790
|
||||||
msgid "Delete rooms"
|
msgid "Delete rooms"
|
||||||
msgstr "Ruimtes verwijderen"
|
msgstr "Ruimtes verwijderen"
|
||||||
|
|
||||||
#: core/models.py:784
|
#: core/models.py:803
|
||||||
msgid "Application name"
|
msgid "Application name"
|
||||||
msgstr "Naam van de applicatie"
|
msgstr "Naam van de applicatie"
|
||||||
|
|
||||||
#: core/models.py:785
|
#: core/models.py:804
|
||||||
msgid "Descriptive name for this application."
|
msgid "Descriptive name for this application."
|
||||||
msgstr "Beschrijvende naam voor deze applicatie."
|
msgstr "Beschrijvende naam voor deze applicatie."
|
||||||
|
|
||||||
#: core/models.py:795
|
#: core/models.py:814
|
||||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wordt gehasht bij het opslaan. Kopieer het nu als dit een nieuw geheim is."
|
"Wordt gehasht bij het opslaan. Kopieer het nu als dit een nieuw geheim is."
|
||||||
|
|
||||||
#: core/models.py:806
|
#: core/models.py:825
|
||||||
msgid "Application"
|
msgid "Application"
|
||||||
msgstr "Applicatie"
|
msgstr "Applicatie"
|
||||||
|
|
||||||
#: core/models.py:807
|
#: core/models.py:826
|
||||||
msgid "Applications"
|
msgid "Applications"
|
||||||
msgstr "Applicaties"
|
msgstr "Applicaties"
|
||||||
|
|
||||||
#: core/models.py:830
|
#: core/models.py:849
|
||||||
msgid "Enter a valid domain"
|
msgid "Enter a valid domain"
|
||||||
msgstr "Voer een geldig domein in"
|
msgstr "Voer een geldig domein in"
|
||||||
|
|
||||||
#: core/models.py:833
|
#: core/models.py:852
|
||||||
msgid "Domain"
|
msgid "Domain"
|
||||||
msgstr "Domein"
|
msgstr "Domein"
|
||||||
|
|
||||||
#: core/models.py:834
|
#: core/models.py:853
|
||||||
msgid "Email domain this application can act on behalf of."
|
msgid "Email domain this application can act on behalf of."
|
||||||
msgstr "E-maildomein namens welke deze applicatie kan handelen."
|
msgstr "E-maildomein namens welke deze applicatie kan handelen."
|
||||||
|
|
||||||
#: core/models.py:846
|
#: core/models.py:865
|
||||||
msgid "Application domain"
|
msgid "Application domain"
|
||||||
msgstr "Applicatiedomein"
|
msgstr "Applicatiedomein"
|
||||||
|
|
||||||
#: core/models.py:847
|
#: core/models.py:866
|
||||||
msgid "Application domains"
|
msgid "Application domains"
|
||||||
msgstr "Applicatiedomeinen"
|
msgstr "Applicatiedomeinen"
|
||||||
|
|
||||||
#: core/models.py:865
|
#: core/models.py:884
|
||||||
msgid "Pending"
|
msgid "Pending"
|
||||||
msgstr "In afwachting"
|
msgstr "In afwachting"
|
||||||
|
|
||||||
#: core/models.py:866
|
#: core/models.py:885
|
||||||
msgid "Analyzing"
|
msgid "Analyzing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/models.py:873
|
#: core/models.py:892
|
||||||
msgid "Ready"
|
msgid "Ready"
|
||||||
msgstr "Klaar"
|
msgstr "Klaar"
|
||||||
|
|
||||||
#: core/models.py:879
|
#: core/models.py:898
|
||||||
msgid "Background image"
|
msgid "Background image"
|
||||||
msgstr "Achtergrondafbeelding"
|
msgstr "Achtergrondafbeelding"
|
||||||
|
|
||||||
#: core/models.py:891
|
#: core/models.py:910
|
||||||
msgid "title"
|
msgid "title"
|
||||||
msgstr "Titel"
|
msgstr "Titel"
|
||||||
|
|
||||||
#: core/models.py:915
|
#: core/models.py:934
|
||||||
msgid "Malware detection info when the analysis status is unsafe."
|
msgid "Malware detection info when the analysis status is unsafe."
|
||||||
msgstr "Informatie over malwaredetectie wanneer de analysestatus onveilig is."
|
msgstr "Informatie over malwaredetectie wanneer de analysestatus onveilig is."
|
||||||
|
|
||||||
#: core/models.py:920
|
#: core/models.py:939
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "Bestand"
|
msgstr "Bestand"
|
||||||
|
|
||||||
#: core/models.py:921
|
#: core/models.py:940
|
||||||
msgid "Files"
|
msgid "Files"
|
||||||
msgstr "Bestanden"
|
msgstr "Bestanden"
|
||||||
|
|
||||||
#: core/models.py:1041
|
#: core/models.py:1060
|
||||||
msgid "This file is already hard deleted."
|
msgid "This file is already hard deleted."
|
||||||
msgstr "Dit bestand is al definitief verwijderd."
|
msgstr "Dit bestand is al definitief verwijderd."
|
||||||
|
|
||||||
#: core/models.py:1051
|
#: core/models.py:1070
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "To hard delete a file, it must first be soft deleted."
|
#| msgid "To hard delete a file, it must first be soft deleted."
|
||||||
msgid "To hard delete a file, it must first be soft deleted."
|
msgid "To hard delete a file, it must first be soft deleted."
|
||||||
@@ -513,15 +529,15 @@ msgstr ""
|
|||||||
"Om een bestand definitief te verwijderen, moet het eerst zacht verwijderd "
|
"Om een bestand definitief te verwijderen, moet het eerst zacht verwijderd "
|
||||||
"zijn."
|
"zijn."
|
||||||
|
|
||||||
#: core/recording/event/notification.py:123
|
#: core/recording/event/notification.py:124
|
||||||
msgid "Your recording is ready"
|
msgid "Your recording is ready"
|
||||||
msgstr "Je opname is klaar"
|
msgstr "Je opname is klaar"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:194
|
#: core/recording/event/notification.py:195
|
||||||
msgid "Transcription"
|
msgid "Transcription"
|
||||||
msgstr "Transcriptie"
|
msgstr "Transcriptie"
|
||||||
|
|
||||||
#: core/recording/event/notification.py:204
|
#: core/recording/event/notification.py:206
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
msgid "Meeting \"{room}\" on {room_recording_date} at {room_recording_time}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -532,25 +548,25 @@ msgstr ""
|
|||||||
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
msgid "Video call in progress: {sender.email} is waiting for you to connect"
|
||||||
msgstr "Video-oproep bezig: {sender.email} wacht op je verbinding"
|
msgstr "Video-oproep bezig: {sender.email} wacht op je verbinding"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:159
|
#: core/templates/mail/html/invitation.html:151
|
||||||
#: core/templates/mail/html/screen_recording.html:159
|
#: core/templates/mail/html/screen_recording.html:151
|
||||||
#: core/templates/mail/text/invitation.txt:3
|
#: core/templates/mail/text/invitation.txt:4
|
||||||
#: core/templates/mail/text/screen_recording.txt:3
|
#: core/templates/mail/text/screen_recording.txt:4
|
||||||
msgid "Logo email"
|
msgid "Logo email"
|
||||||
msgstr "Logo e-mail"
|
msgstr "Logo e-mail"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:189
|
#: core/templates/mail/html/invitation.html:181
|
||||||
#: core/templates/mail/text/invitation.txt:5
|
#: core/templates/mail/text/invitation.txt:6
|
||||||
msgid "invites you to join an ongoing video call"
|
msgid "invites you to join an ongoing video call"
|
||||||
msgstr "nodigt je uit om deel te nemen aan een lopende video-oproep"
|
msgstr "nodigt je uit om deel te nemen aan een lopende video-oproep"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:200
|
#: core/templates/mail/html/invitation.html:192
|
||||||
#: core/templates/mail/text/invitation.txt:7
|
#: core/templates/mail/text/invitation.txt:8
|
||||||
msgid "JOIN THE CALL"
|
msgid "JOIN THE CALL"
|
||||||
msgstr "NEEM DEEL AAN DE OPROEP"
|
msgstr "NEEM DEEL AAN DE OPROEP"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:227
|
#: core/templates/mail/html/invitation.html:219
|
||||||
#: core/templates/mail/text/invitation.txt:13
|
#: core/templates/mail/text/invitation.txt:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"If you can't click the button, copy and paste the URL into your browser to "
|
"If you can't click the button, copy and paste the URL into your browser to "
|
||||||
"join the call."
|
"join the call."
|
||||||
@@ -558,41 +574,47 @@ msgstr ""
|
|||||||
"Als je niet op de knop kunt klikken, kopieer en plak dan de URL in je "
|
"Als je niet op de knop kunt klikken, kopieer en plak dan de URL in je "
|
||||||
"browser om deel te nemen aan de oproep."
|
"browser om deel te nemen aan de oproep."
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:235
|
#: core/templates/mail/html/invitation.html:227
|
||||||
#: core/templates/mail/text/invitation.txt:15
|
#: core/templates/mail/text/invitation.txt:16
|
||||||
msgid "Tips for a better experience:"
|
msgid "Tips for a better experience:"
|
||||||
msgstr "Tips voor een betere ervaring:"
|
msgstr "Tips voor een betere ervaring:"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:237
|
#: core/templates/mail/html/invitation.html:229
|
||||||
#: core/templates/mail/text/invitation.txt:17
|
#: core/templates/mail/text/invitation.txt:18
|
||||||
msgid "Use Chrome or Firefox for better call quality"
|
msgid "Use Chrome or Firefox for better call quality"
|
||||||
msgstr "Gebruik Chrome of Firefox voor betere gesprekskwaliteit"
|
msgstr "Gebruik Chrome of Firefox voor betere gesprekskwaliteit"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:238
|
#: core/templates/mail/html/invitation.html:230
|
||||||
#: core/templates/mail/text/invitation.txt:18
|
#: core/templates/mail/text/invitation.txt:19
|
||||||
msgid "Test your microphone and camera before joining"
|
msgid "Test your microphone and camera before joining"
|
||||||
msgstr "Test je microfoon en camera voordat je deelneemt"
|
msgstr "Test je microfoon en camera voordat je deelneemt"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:239
|
#: core/templates/mail/html/invitation.html:231
|
||||||
#: core/templates/mail/text/invitation.txt:19
|
#: core/templates/mail/text/invitation.txt:20
|
||||||
msgid "Make sure you have a stable internet connection"
|
msgid "Make sure you have a stable internet connection"
|
||||||
msgstr "Zorg ervoor dat je een stabiele internetverbinding hebt"
|
msgstr "Zorg ervoor dat je een stabiele internetverbinding hebt"
|
||||||
|
|
||||||
#: core/templates/mail/html/invitation.html:248
|
#: core/templates/mail/html/invitation.html:240
|
||||||
#: core/templates/mail/html/screen_recording.html:245
|
#: core/templates/mail/html/screen_recording.html:237
|
||||||
#: core/templates/mail/text/invitation.txt:21
|
#: core/templates/mail/text/invitation.txt:22
|
||||||
#: core/templates/mail/text/screen_recording.txt:23
|
#: core/templates/mail/text/screen_recording.txt:24
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " Thank you for using %(brandname)s. "
|
msgid " Thank you for using %(brandname)s. "
|
||||||
msgstr " Bedankt voor het gebruik van %(brandname)s. "
|
msgstr " Bedankt voor het gebruik van %(brandname)s. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:188
|
#: core/templates/mail/html/invitation.html:271
|
||||||
#: core/templates/mail/text/screen_recording.txt:6
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"This mail has been sent to %(email)s by <a href=\"%(href)s\">%(name)s</a>"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/html/screen_recording.html:180
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:7
|
||||||
msgid "Your recording is ready!"
|
msgid "Your recording is ready!"
|
||||||
msgstr "Je opname is klaar!"
|
msgstr "Je opname is klaar!"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
" Your recording of \"%(room_name)s\" on %(recording_date)s at "
|
||||||
@@ -601,14 +623,14 @@ msgstr ""
|
|||||||
" Je opname van \"%(room_name)s\" op %(recording_date)s om %(recording_time)s "
|
" Je opname van \"%(room_name)s\" op %(recording_date)s om %(recording_time)s "
|
||||||
"is nu klaar om te downloaden. "
|
"is nu klaar om te downloaden. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:195
|
#: core/templates/mail/html/screen_recording.html:187
|
||||||
#: core/templates/mail/text/screen_recording.txt:8
|
#: core/templates/mail/text/screen_recording.txt:9
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid " The recording will expire in %(days)s days. "
|
msgid " The recording will expire in %(days)s days. "
|
||||||
msgstr " De opname verloopt over %(days)s dagen. "
|
msgstr " De opname verloopt over %(days)s dagen. "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:200
|
#: core/templates/mail/html/screen_recording.html:192
|
||||||
#: core/templates/mail/text/screen_recording.txt:9
|
#: core/templates/mail/text/screen_recording.txt:10
|
||||||
msgid ""
|
msgid ""
|
||||||
" Sharing the recording via link is not yet available. Only organizers can "
|
" Sharing the recording via link is not yet available. Only organizers can "
|
||||||
"download it. "
|
"download it. "
|
||||||
@@ -616,33 +638,33 @@ msgstr ""
|
|||||||
"Het delen van de opname via een link is nog niet beschikbaar. Alleen "
|
"Het delen van de opname via een link is nog niet beschikbaar. Alleen "
|
||||||
"organisatoren kunnen deze downloaden."
|
"organisatoren kunnen deze downloaden."
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:206
|
#: core/templates/mail/html/screen_recording.html:198
|
||||||
#: core/templates/mail/text/screen_recording.txt:11
|
#: core/templates/mail/text/screen_recording.txt:12
|
||||||
msgid "To keep this recording permanently:"
|
msgid "To keep this recording permanently:"
|
||||||
msgstr "Om deze opname permanent te bewaren:"
|
msgstr "Om deze opname permanent te bewaren:"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:208
|
#: core/templates/mail/html/screen_recording.html:200
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgstr "Klik op de \"<a href=\"%(link)s\">Openen</a>\"-link hieronder "
|
msgstr "Klik op de \"<a href=\"%(link)s\">Openen</a>\"-link hieronder "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:209
|
#: core/templates/mail/html/screen_recording.html:201
|
||||||
#: core/templates/mail/text/screen_recording.txt:14
|
#: core/templates/mail/text/screen_recording.txt:15
|
||||||
msgid "Use the \"Download\" button in the interface "
|
msgid "Use the \"Download\" button in the interface "
|
||||||
msgstr "Gebruik de \"Download\"-knop in de interface "
|
msgstr "Gebruik de \"Download\"-knop in de interface "
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:210
|
#: core/templates/mail/html/screen_recording.html:202
|
||||||
#: core/templates/mail/text/screen_recording.txt:15
|
#: core/templates/mail/text/screen_recording.txt:16
|
||||||
msgid "Save the file to your preferred location"
|
msgid "Save the file to your preferred location"
|
||||||
msgstr "Sla het bestand op naar je gewenste locatie"
|
msgstr "Sla het bestand op naar je gewenste locatie"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:221
|
#: core/templates/mail/html/screen_recording.html:213
|
||||||
#: core/templates/mail/text/screen_recording.txt:17
|
#: core/templates/mail/text/screen_recording.txt:18
|
||||||
msgid "Open"
|
msgid "Open"
|
||||||
msgstr "Openen"
|
msgstr "Openen"
|
||||||
|
|
||||||
#: core/templates/mail/html/screen_recording.html:230
|
#: core/templates/mail/html/screen_recording.html:222
|
||||||
#: core/templates/mail/text/screen_recording.txt:19
|
#: core/templates/mail/text/screen_recording.txt:20
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
" If you have any questions or need assistance, please contact our support "
|
" If you have any questions or need assistance, please contact our support "
|
||||||
@@ -651,28 +673,33 @@ msgstr ""
|
|||||||
" Als je vragen hebt of hulp nodig hebt, neem dan contact op met ons support "
|
" Als je vragen hebt of hulp nodig hebt, neem dan contact op met ons support "
|
||||||
"team via %(support_email)s. "
|
"team via %(support_email)s. "
|
||||||
|
|
||||||
#: core/templates/mail/text/screen_recording.txt:13
|
#: core/templates/mail/text/invitation.txt:24
|
||||||
|
#, python-format
|
||||||
|
msgid "This mail has been sent to %(email)s by %(name)s [%(href)s]"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: core/templates/mail/text/screen_recording.txt:14
|
||||||
#, fuzzy, python-format
|
#, fuzzy, python-format
|
||||||
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
#| msgid "Click the \"<a href=\"%(link)s\">Open</a>\" link below "
|
||||||
msgid "Click the \"Open [%(link)s]\" link below "
|
msgid "Click the \"Open [%(link)s]\" link below "
|
||||||
msgstr "Klik op de \"<a href=\"%(link)s\">Openen</a>\"-link hieronder "
|
msgstr "Klik op de \"<a href=\"%(link)s\">Openen</a>\"-link hieronder "
|
||||||
|
|
||||||
#: meet/settings.py:228
|
#: meet/settings.py:238
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr "Engels"
|
msgstr "Engels"
|
||||||
|
|
||||||
#: meet/settings.py:229
|
#: meet/settings.py:239
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr "Frans"
|
msgstr "Frans"
|
||||||
|
|
||||||
#: meet/settings.py:230
|
#: meet/settings.py:240
|
||||||
msgid "Dutch"
|
msgid "Dutch"
|
||||||
msgstr "Nederlands"
|
msgstr "Nederlands"
|
||||||
|
|
||||||
#: meet/settings.py:231
|
#: meet/settings.py:241
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr "Duits"
|
msgstr "Duits"
|
||||||
|
|
||||||
#: meet/settings.py:233
|
#: meet/settings.py:242
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
msgstr "Spaans"
|
msgstr "Spaans"
|
||||||
|
|||||||
@@ -129,6 +129,15 @@ class Base(Configuration):
|
|||||||
MEDIA_BASE_URL = values.Value(
|
MEDIA_BASE_URL = values.Value(
|
||||||
"", environ_name="MEDIA_BASE_URL", environ_prefix=None
|
"", environ_name="MEDIA_BASE_URL", environ_prefix=None
|
||||||
)
|
)
|
||||||
|
# Header the reverse proxy uses to pass the original request URL to the
|
||||||
|
# media-auth subrequest views. nginx-ingress sends X-Original-URL, which is
|
||||||
|
# the default. Other proxies use different headers -- Traefik's ForwardAuth,
|
||||||
|
# for instance, sends X-Forwarded-Uri and cannot emit X-Original-URL at all.
|
||||||
|
MEDIA_AUTH_ORIGINAL_URL_HEADER = values.Value(
|
||||||
|
default="HTTP_X_ORIGINAL_URL",
|
||||||
|
environ_name="MEDIA_AUTH_ORIGINAL_URL_HEADER",
|
||||||
|
environ_prefix=None,
|
||||||
|
)
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
@@ -747,63 +756,26 @@ class Base(Configuration):
|
|||||||
# recordings), whose request never carries advanced EncodingOptions.
|
# recordings), whose request never carries advanced EncodingOptions.
|
||||||
# When disabled, LiveKit falls back to its built-in H264_720P_30 preset
|
# When disabled, LiveKit falls back to its built-in H264_720P_30 preset
|
||||||
# (1280x720, 30 fps, 3000 kbps H.264 MAIN video, 128 kbps AAC audio).
|
# (1280x720, 30 fps, 3000 kbps H.264 MAIN video, 128 kbps AAC audio).
|
||||||
# When enabled, the encoding parameters are resolved from the default profile
|
# When enabled, the values below are passed to LiveKit as EncodingOptions
|
||||||
# and resolution below and passed to LiveKit as EncodingOptions (advanced),
|
# (advanced) and replace the preset. Lowering framerate and bitrate reduces
|
||||||
# replacing the preset. Lowering framerate and bitrate reduces output file
|
# output file size and CPU load on the egress worker.
|
||||||
# size and CPU load on the egress worker.
|
|
||||||
RECORDING_ENCODING_ENABLED = values.BooleanValue(
|
RECORDING_ENCODING_ENABLED = values.BooleanValue(
|
||||||
False, environ_name="RECORDING_ENCODING_ENABLED", environ_prefix=None
|
False, environ_name="RECORDING_ENCODING_ENABLED", environ_prefix=None
|
||||||
)
|
)
|
||||||
|
RECORDING_ENCODING_WIDTH = values.PositiveIntegerValue(
|
||||||
# Map resolution name -> {"width": ..., "height": ...} in pixels.
|
1280, environ_name="RECORDING_ENCODING_WIDTH", environ_prefix=None
|
||||||
RECORDING_ENCODING_AVAILABLE_RESOLUTIONS = values.DictValue(
|
)
|
||||||
{
|
RECORDING_ENCODING_HEIGHT = values.PositiveIntegerValue(
|
||||||
"540p": {"width": 960, "height": 540},
|
720, environ_name="RECORDING_ENCODING_HEIGHT", environ_prefix=None
|
||||||
"720p": {"width": 1280, "height": 720},
|
)
|
||||||
"1080p": {"width": 1920, "height": 1080},
|
RECORDING_ENCODING_FRAMERATE = values.PositiveIntegerValue(
|
||||||
},
|
30, environ_name="RECORDING_ENCODING_FRAMERATE", environ_prefix=None
|
||||||
environ_name="RECORDING_ENCODING_AVAILABLE_RESOLUTIONS",
|
)
|
||||||
|
RECORDING_ENCODING_VIDEO_BITRATE_KBPS = values.PositiveIntegerValue(
|
||||||
|
3000,
|
||||||
|
environ_name="RECORDING_ENCODING_VIDEO_BITRATE_KBPS",
|
||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bitrate scales with resolution so quality stays consistent across sizes.
|
|
||||||
RECORDING_ENCODING_AVAILABLE_PROFILES = values.DictValue(
|
|
||||||
{
|
|
||||||
"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},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
environ_name="RECORDING_ENCODING_AVAILABLE_PROFILES",
|
|
||||||
environ_prefix=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Defaults used when no profile/resolution is specified per recording.
|
|
||||||
# Must be keys of the two dicts above (validated at startup).
|
|
||||||
RECORDING_ENCODING_DEFAULT_PROFILE = values.Value(
|
|
||||||
"full",
|
|
||||||
environ_name="RECORDING_ENCODING_DEFAULT_PROFILE",
|
|
||||||
environ_prefix=None,
|
|
||||||
)
|
|
||||||
RECORDING_ENCODING_DEFAULT_RESOLUTION = values.Value(
|
|
||||||
"720p",
|
|
||||||
environ_name="RECORDING_ENCODING_DEFAULT_RESOLUTION",
|
|
||||||
environ_prefix=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Settings independent of profile/resolution.
|
|
||||||
RECORDING_ENCODING_AUDIO_BITRATE_KBPS = values.PositiveIntegerValue(
|
RECORDING_ENCODING_AUDIO_BITRATE_KBPS = values.PositiveIntegerValue(
|
||||||
128,
|
128,
|
||||||
environ_name="RECORDING_ENCODING_AUDIO_BITRATE_KBPS",
|
environ_name="RECORDING_ENCODING_AUDIO_BITRATE_KBPS",
|
||||||
@@ -818,7 +790,6 @@ class Base(Configuration):
|
|||||||
SUMMARY_SERVICE_VERSION = values.PositiveIntegerValue(
|
SUMMARY_SERVICE_VERSION = values.PositiveIntegerValue(
|
||||||
1, environ_name="SUMMARY_SERVICE_VERSION", environ_prefix=None
|
1, environ_name="SUMMARY_SERVICE_VERSION", environ_prefix=None
|
||||||
)
|
)
|
||||||
|
|
||||||
SUMMARY_SERVICE_ENDPOINT = values.Value(
|
SUMMARY_SERVICE_ENDPOINT = values.Value(
|
||||||
None, environ_name="SUMMARY_SERVICE_ENDPOINT", environ_prefix=None
|
None, environ_name="SUMMARY_SERVICE_ENDPOINT", environ_prefix=None
|
||||||
)
|
)
|
||||||
@@ -1207,49 +1178,6 @@ class Base(Configuration):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _check_recording_encoding_maps(cls):
|
|
||||||
"""Ensure the per-recording encoding maps are mutually consistent.
|
|
||||||
|
|
||||||
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, 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 "
|
|
||||||
"define a bitrate for exactly the resolutions in "
|
|
||||||
"RECORDING_ENCODING_AVAILABLE_RESOLUTIONS, mismatch on: "
|
|
||||||
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.
|
||||||
@@ -1263,8 +1191,6 @@ class Base(Configuration):
|
|||||||
"FILE_UPLOAD_TMP_PATH cannot be the same as FILE_UPLOAD_PATH"
|
"FILE_UPLOAD_TMP_PATH cannot be the same as FILE_UPLOAD_PATH"
|
||||||
)
|
)
|
||||||
|
|
||||||
cls._check_recording_encoding_maps()
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cls.SUMMARY_SERVICE_VERSION == 1
|
cls.SUMMARY_SERVICE_VERSION == 1
|
||||||
and cls.SUMMARY_SERVICE_ENDPOINT is not None
|
and cls.SUMMARY_SERVICE_ENDPOINT is not None
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ RUN apk update && apk upgrade \
|
|||||||
musl \
|
musl \
|
||||||
musl-utils \
|
musl-utils \
|
||||||
zlib>=1.3.2-r0 \
|
zlib>=1.3.2-r0 \
|
||||||
|
libexpat>=2.8.4-r0 \
|
||||||
&& apk del curl
|
&& apk del curl
|
||||||
|
|
||||||
USER nginx
|
USER nginx
|
||||||
|
|||||||
@@ -0,0 +1,595 @@
|
|||||||
|
import { CSSProperties, useState } from 'react'
|
||||||
|
import { useConnectionState, useRoomContext } from '@livekit/components-react'
|
||||||
|
import { ConnectionState } from 'livekit-client'
|
||||||
|
import { StatsSnapshot, TrackRow, useWebRTCStats } from './useWebRTCStats'
|
||||||
|
import { readRoomConfig } from './roomConfig'
|
||||||
|
import {
|
||||||
|
forceTransport,
|
||||||
|
releaseForcedTransport,
|
||||||
|
Scenario,
|
||||||
|
SCENARIOS,
|
||||||
|
setDownlinkCap,
|
||||||
|
setUplinkCap,
|
||||||
|
stepBitrate,
|
||||||
|
TransportMode,
|
||||||
|
transportModeFromRoute,
|
||||||
|
} from './simulation'
|
||||||
|
|
||||||
|
const SANS =
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
||||||
|
const MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'
|
||||||
|
|
||||||
|
const COLOR = {
|
||||||
|
text: '#e8e8e8',
|
||||||
|
muted: '#9a9a9a',
|
||||||
|
faint: '#6f6f6f',
|
||||||
|
hairline: '#2c2d31',
|
||||||
|
border: '#3d3e44',
|
||||||
|
surface: '#1b1c1e',
|
||||||
|
chartBg: '#141517',
|
||||||
|
down: '#f6821f',
|
||||||
|
up: '#5a9cf8',
|
||||||
|
ok: '#10b981',
|
||||||
|
bad: '#f05a4a',
|
||||||
|
busy: '#e5a13c',
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateColor = (state: ConnectionState): string => {
|
||||||
|
switch (state) {
|
||||||
|
case ConnectionState.Connected:
|
||||||
|
return COLOR.ok
|
||||||
|
case ConnectionState.Reconnecting:
|
||||||
|
case ConnectionState.SignalReconnecting:
|
||||||
|
return COLOR.busy
|
||||||
|
case ConnectionState.Connecting:
|
||||||
|
return COLOR.up
|
||||||
|
case ConnectionState.Disconnected:
|
||||||
|
return COLOR.bad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles: Record<string, CSSProperties> = {
|
||||||
|
toggle: {
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 12,
|
||||||
|
right: 12,
|
||||||
|
zIndex: 9999,
|
||||||
|
fontFamily: SANS,
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1,
|
||||||
|
padding: '8px 12px',
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: COLOR.border,
|
||||||
|
background: COLOR.surface,
|
||||||
|
color: COLOR.text,
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow: '0 2px 10px #0006',
|
||||||
|
},
|
||||||
|
panel: {
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 12,
|
||||||
|
right: 12,
|
||||||
|
zIndex: 9999,
|
||||||
|
width: 460,
|
||||||
|
maxWidth: 'calc(100vw - 24px)',
|
||||||
|
maxHeight: 'calc(100vh - 24px)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
fontFamily: SANS,
|
||||||
|
fontSize: 11,
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: COLOR.text,
|
||||||
|
background: COLOR.surface,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: COLOR.border,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 14,
|
||||||
|
boxShadow: '0 10px 34px #00000080',
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
margin: '14px 0 5px',
|
||||||
|
color: COLOR.faint,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: 600,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
metricsLine: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 16,
|
||||||
|
margin: '0 0 10px',
|
||||||
|
paddingBottom: 10,
|
||||||
|
borderBottom: `1px solid ${COLOR.hairline}`,
|
||||||
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: 'flex',
|
||||||
|
gap: 12,
|
||||||
|
marginTop: 3,
|
||||||
|
color: COLOR.faint,
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxHeight: '400px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
},
|
||||||
|
configList: { display: 'flex', flexWrap: 'wrap' },
|
||||||
|
configRow: {
|
||||||
|
display: 'flex',
|
||||||
|
gap: 6,
|
||||||
|
flex: '0 0 50%',
|
||||||
|
minWidth: 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
padding: '1px 8px 1px 0',
|
||||||
|
},
|
||||||
|
configLabel: {
|
||||||
|
color: COLOR.muted,
|
||||||
|
flex: '0 0 45%',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
configValue: {
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
trackRow: {
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
padding: '3px 0',
|
||||||
|
borderBottom: `1px solid ${COLOR.hairline}`,
|
||||||
|
alignItems: 'baseline',
|
||||||
|
},
|
||||||
|
trackLabel: {
|
||||||
|
flex: '0 0 34%',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
trackCodec: {
|
||||||
|
flex: '0 0 11%',
|
||||||
|
fontFamily: MONO,
|
||||||
|
fontSize: 10,
|
||||||
|
color: COLOR.muted,
|
||||||
|
},
|
||||||
|
trackKbps: {
|
||||||
|
flex: '0 0 9%',
|
||||||
|
textAlign: 'right',
|
||||||
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
},
|
||||||
|
trackDetail: {
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
mono: { fontFamily: MONO, fontSize: 10, color: COLOR.muted },
|
||||||
|
button: {
|
||||||
|
fontFamily: SANS,
|
||||||
|
fontSize: 11,
|
||||||
|
padding: '3px 9px',
|
||||||
|
borderRadius: 5,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: COLOR.border,
|
||||||
|
background: '#232428',
|
||||||
|
color: COLOR.text,
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
buttonActive: { borderColor: COLOR.down, color: COLOR.down },
|
||||||
|
buttonDisabled: { color: COLOR.faint, cursor: 'not-allowed' },
|
||||||
|
row: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
|
||||||
|
stepperValue: {
|
||||||
|
minWidth: 92,
|
||||||
|
textAlign: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: COLOR.hairline,
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: '2px 6px',
|
||||||
|
background: COLOR.chartBg,
|
||||||
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
color: COLOR.faint,
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Sparkline = ({ history }: { history: StatsSnapshot[] }) => {
|
||||||
|
const width = 430
|
||||||
|
const height = 54
|
||||||
|
const max = Math.max(...history.map((s) => Math.max(s.upKbps, s.downKbps)), 1)
|
||||||
|
const points = (pick: (s: StatsSnapshot) => number) =>
|
||||||
|
history
|
||||||
|
.map(
|
||||||
|
(s, i) =>
|
||||||
|
`${((i / Math.max(history.length - 1, 1)) * width).toFixed(1)},${(
|
||||||
|
height -
|
||||||
|
(pick(s) / max) * (height - 6) -
|
||||||
|
3
|
||||||
|
).toFixed(1)}`
|
||||||
|
)
|
||||||
|
.join(' ')
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
role="img"
|
||||||
|
aria-label="Bandwidth over the last minute"
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
background: COLOR.chartBg,
|
||||||
|
borderRadius: 6,
|
||||||
|
border: `1px solid ${COLOR.hairline}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{history.length >= 2 && (
|
||||||
|
<>
|
||||||
|
<polyline
|
||||||
|
points={points((s) => s.downKbps)}
|
||||||
|
fill="none"
|
||||||
|
stroke={COLOR.down}
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<polyline
|
||||||
|
points={points((s) => s.upKbps)}
|
||||||
|
fill="none"
|
||||||
|
stroke={COLOR.up}
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
{/* Legend outside the plot: text over the lines was unreadable. */}
|
||||||
|
<div style={styles.legend}>
|
||||||
|
<span>
|
||||||
|
<span style={{ color: COLOR.down }}>●</span> down
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span style={{ color: COLOR.up }}>●</span> up
|
||||||
|
</span>
|
||||||
|
<span>last 60s · max {Math.round(max)} kbps</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CapStepper = ({
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
valueKbps,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
hint: string
|
||||||
|
valueKbps: number | null
|
||||||
|
onChange: (kbps: number | null) => void
|
||||||
|
}) => (
|
||||||
|
<div style={styles.row}>
|
||||||
|
<span style={{ flex: '0 0 70px' }}>{label}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={styles.button}
|
||||||
|
aria-label={`Decrease ${label} bandwidth`}
|
||||||
|
onClick={() => onChange(stepBitrate(valueKbps, 'decrease'))}
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...styles.stepperValue,
|
||||||
|
...(valueKbps !== null ? { color: COLOR.down } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{valueKbps === null ? 'unlimited' : `≤ ${valueKbps} kbps`}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={styles.button}
|
||||||
|
aria-label={`Increase ${label} bandwidth`}
|
||||||
|
onClick={() => onChange(stepBitrate(valueKbps, 'increase'))}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
<span style={styles.mono}>{hint}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const TrackList = ({ title, rows }: { title: string; rows: TrackRow[] }) => (
|
||||||
|
<>
|
||||||
|
<div style={styles.sectionTitle}>
|
||||||
|
<span>
|
||||||
|
{title} ({rows.length})
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{Math.round(rows.reduce((sum, t) => sum + t.kbps, 0))} kbps media
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={styles.list}>
|
||||||
|
{rows.map((track) => (
|
||||||
|
<div key={track.key} style={styles.trackRow}>
|
||||||
|
<span style={styles.trackLabel}>{track.label}</span>
|
||||||
|
<span style={styles.trackCodec}>{track.codec ?? '–'}</span>
|
||||||
|
<span style={styles.trackKbps}>{Math.round(track.kbps)}</span>
|
||||||
|
<span style={styles.trackDetail}>
|
||||||
|
{[
|
||||||
|
track.res,
|
||||||
|
track.fps !== undefined && `${Math.round(track.fps)}fps`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' · ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{rows.length === 0 && <span style={{ color: COLOR.faint }}>none</span>}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
const MeetDevtools = () => {
|
||||||
|
const room = useRoomContext()
|
||||||
|
const connState = useConnectionState(room)
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [firedScenario, setFiredScenario] = useState<string>()
|
||||||
|
const [uplinkCap, setUplinkCapState] = useState<number | null>(null)
|
||||||
|
const [downlinkCap, setDownlinkCapState] = useState<number | null>(null)
|
||||||
|
const { snapshot, history } = useWebRTCStats(room, open)
|
||||||
|
|
||||||
|
const transportMode: TransportMode = transportModeFromRoute(snapshot?.route)
|
||||||
|
|
||||||
|
const fireScenario = (scenario: Scenario) => {
|
||||||
|
setFiredScenario(scenario.id)
|
||||||
|
scenario.run(room).catch((e) => {
|
||||||
|
console.warn('[MeetDevtools] simulateScenario failed', e)
|
||||||
|
})
|
||||||
|
window.setTimeout(
|
||||||
|
() =>
|
||||||
|
setFiredScenario((current) =>
|
||||||
|
current === scenario.id ? undefined : current
|
||||||
|
),
|
||||||
|
1200
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={styles.toggle}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
aria-label="Open WebRTC devtools"
|
||||||
|
>
|
||||||
|
<span style={{ color: stateColor(connState) }}>●</span> rtc
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = readRoomConfig(room)
|
||||||
|
const published = snapshot?.tracks.filter((t) => t.dir === 'up') ?? []
|
||||||
|
const subscribed = snapshot?.tracks.filter((t) => t.dir === 'down') ?? []
|
||||||
|
const turnProtocols = snapshot?.turnProtocols ?? []
|
||||||
|
const transports: Array<{
|
||||||
|
mode: TransportMode
|
||||||
|
label: string
|
||||||
|
requires?: string
|
||||||
|
title: string
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
mode: 'auto',
|
||||||
|
label: 'auto (udp)',
|
||||||
|
title:
|
||||||
|
'clears the server-cached transport preference and relay-only policy, then full reconnect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'tcp',
|
||||||
|
label: 'tcp',
|
||||||
|
title: 'force-tcp — server prefers TCP candidates (ICE/TCP)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'turn-udp',
|
||||||
|
label: 'turn:udp',
|
||||||
|
requires: 'udp',
|
||||||
|
title:
|
||||||
|
'client-side: iceTransportPolicy relay + full reconnect (no server hook exists)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'turn-tcp',
|
||||||
|
label: 'turn:tcp',
|
||||||
|
requires: 'tcp',
|
||||||
|
title: 'force-tcp — lands on TURN/TCP when it is the TCP path',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
mode: 'turn-tls',
|
||||||
|
label: 'turn:tls',
|
||||||
|
requires: 'tls',
|
||||||
|
title: 'force-tls — server switches you to TURN over TLS',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section style={styles.panel} aria-label="WebRTC devtools">
|
||||||
|
<div style={styles.metricsLine}>
|
||||||
|
<span>
|
||||||
|
<span style={{ color: COLOR.down }}>↓</span> {snapshot?.downKbps ?? 0}{' '}
|
||||||
|
kbps
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span style={{ color: COLOR.up }}>↑</span> {snapshot?.upKbps ?? 0}{' '}
|
||||||
|
kbps
|
||||||
|
</span>
|
||||||
|
<span style={{ color: COLOR.muted }}>
|
||||||
|
rtt{' '}
|
||||||
|
<span style={{ color: COLOR.text }}>{snapshot?.rttMs ?? '–'}</span> ms
|
||||||
|
</span>
|
||||||
|
<span style={{ color: COLOR.muted }}>
|
||||||
|
jitter{' '}
|
||||||
|
<span style={{ color: COLOR.text }}>{snapshot?.jitterMs ?? '–'}</span>{' '}
|
||||||
|
ms
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: stateColor(connState),
|
||||||
|
fontSize: 10,
|
||||||
|
marginLeft: 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
● {connState}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
style={styles.close}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
aria-label="Close WebRTC devtools"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Sparkline history={history} />
|
||||||
|
|
||||||
|
<div style={styles.sectionTitle}>room configuration (live)</div>
|
||||||
|
<div style={styles.configList}>
|
||||||
|
{config.map((entry) => (
|
||||||
|
<div key={entry.label} style={styles.configRow}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
entry.on === undefined
|
||||||
|
? COLOR.faint
|
||||||
|
: entry.on
|
||||||
|
? COLOR.ok
|
||||||
|
: COLOR.bad,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
●
|
||||||
|
</span>
|
||||||
|
<span style={styles.configLabel}>{entry.label}</span>
|
||||||
|
<span style={styles.configValue} title={entry.value}>
|
||||||
|
{entry.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TrackList title="published ↑" rows={published} />
|
||||||
|
<TrackList title="subscribed ↓" rows={subscribed} />
|
||||||
|
|
||||||
|
<div style={styles.sectionTitle}>bandwidth</div>
|
||||||
|
<div style={{ ...styles.list, gap: 4 }}>
|
||||||
|
<CapStepper
|
||||||
|
label="uplink"
|
||||||
|
hint="encoder cap (setParameters)"
|
||||||
|
valueKbps={uplinkCap}
|
||||||
|
onChange={(kbps) => {
|
||||||
|
setUplinkCapState(kbps)
|
||||||
|
void setUplinkCap(room, kbps)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CapStepper
|
||||||
|
label="downlink"
|
||||||
|
hint="SFU limit (subscriber-bandwidth)"
|
||||||
|
valueKbps={downlinkCap}
|
||||||
|
onChange={(kbps) => {
|
||||||
|
setDownlinkCapState(kbps)
|
||||||
|
void setDownlinkCap(room, kbps).catch((e) =>
|
||||||
|
console.warn('[MeetDevtools] downlink cap failed', e)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.sectionTitle}>transport</div>
|
||||||
|
<div style={styles.row}>
|
||||||
|
{transports.map(({ mode, label, requires, title }) => {
|
||||||
|
const available = !requires || turnProtocols.includes(requires)
|
||||||
|
const active = transportMode === mode
|
||||||
|
const clickable = available && !active
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={mode}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={active}
|
||||||
|
disabled={!clickable}
|
||||||
|
title={available ? title : `${title} — not configured`}
|
||||||
|
style={{
|
||||||
|
...styles.button,
|
||||||
|
...(clickable || active ? {} : styles.buttonDisabled),
|
||||||
|
...(active ? styles.buttonActive : {}),
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!clickable) return
|
||||||
|
void (
|
||||||
|
mode === 'auto'
|
||||||
|
? releaseForcedTransport(room)
|
||||||
|
: forceTransport(
|
||||||
|
room,
|
||||||
|
mode as 'tcp' | 'turn-udp' | 'turn-tcp' | 'turn-tls'
|
||||||
|
)
|
||||||
|
).catch((e) =>
|
||||||
|
console.warn('[MeetDevtools] transport change failed', e)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{requires && (
|
||||||
|
<span style={{ color: available ? COLOR.ok : COLOR.faint }}>
|
||||||
|
●{' '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<span style={styles.mono}>
|
||||||
|
route:{' '}
|
||||||
|
{snapshot?.route
|
||||||
|
? [
|
||||||
|
snapshot.route.protocol,
|
||||||
|
snapshot.route.type,
|
||||||
|
snapshot.route.relayProtocol &&
|
||||||
|
`relay:${snapshot.route.relayProtocol}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('·')
|
||||||
|
: '–'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={styles.sectionTitle}>connection scenarios</div>
|
||||||
|
<div style={styles.row}>
|
||||||
|
{SCENARIOS.map((scenario) => (
|
||||||
|
<button
|
||||||
|
key={scenario.id}
|
||||||
|
type="button"
|
||||||
|
title={`${scenario.side}-side simulation — momentary, watch the state dot`}
|
||||||
|
style={{
|
||||||
|
...styles.button,
|
||||||
|
...(firedScenario === scenario.id ? styles.buttonActive : {}),
|
||||||
|
}}
|
||||||
|
onClick={() => fireScenario(scenario)}
|
||||||
|
>
|
||||||
|
{scenario.side === 'server' ? '☁ ' : ''}
|
||||||
|
{scenario.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MeetDevtools
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { lazy, Suspense } from 'react'
|
||||||
|
|
||||||
|
const LazyPanel = import.meta.env.DEV
|
||||||
|
? lazy(() => import('./MeetDevtools'))
|
||||||
|
: null
|
||||||
|
|
||||||
|
export const MeetDevtools = () => {
|
||||||
|
if (!LazyPanel) return null
|
||||||
|
return (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<LazyPanel />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Room } from 'livekit-client'
|
||||||
|
|
||||||
|
export type ConfigEntry = {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
/** true = feature actively on, false = off, undefined = informational */
|
||||||
|
on?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatBackupCodec = (backup: unknown): string => {
|
||||||
|
if (backup === undefined || backup === true) return 'auto'
|
||||||
|
if (backup === false) return 'off'
|
||||||
|
if (typeof backup === 'object' && backup !== null && 'codec' in backup) {
|
||||||
|
return String((backup as { codec: unknown }).codec)
|
||||||
|
}
|
||||||
|
return String(backup)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readRoomConfig = (room: Room): ConfigEntry[] => {
|
||||||
|
const options = room.options
|
||||||
|
const publish = options.publishDefaults
|
||||||
|
const adaptive = options.adaptiveStream
|
||||||
|
|
||||||
|
const entries: ConfigEntry[] = [
|
||||||
|
{
|
||||||
|
label: 'adaptiveStream',
|
||||||
|
value:
|
||||||
|
typeof adaptive === 'object'
|
||||||
|
? JSON.stringify(adaptive)
|
||||||
|
: String(!!adaptive),
|
||||||
|
on: !!adaptive,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'dynacast',
|
||||||
|
value: String(!!options.dynacast),
|
||||||
|
on: !!options.dynacast,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'e2ee',
|
||||||
|
value: String(room.isE2EEEnabled),
|
||||||
|
on: room.isE2EEEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'videoCodec',
|
||||||
|
value: publish?.videoCodec ?? 'default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'backupCodec',
|
||||||
|
value: formatBackupCodec(publish?.backupCodec),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'simulcast',
|
||||||
|
value: String(publish?.simulcast ?? true),
|
||||||
|
on: publish?.simulcast ?? true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'audio dtx',
|
||||||
|
value: String(publish?.dtx ?? true),
|
||||||
|
on: publish?.dtx ?? true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'audio red',
|
||||||
|
value: String(publish?.red ?? true),
|
||||||
|
on: publish?.red ?? true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'quality (local)',
|
||||||
|
value: room.localParticipant.connectionQuality,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const server = room.serverInfo
|
||||||
|
if (server) {
|
||||||
|
entries.push({
|
||||||
|
label: 'server',
|
||||||
|
value: [
|
||||||
|
server.version && `v${server.version}`,
|
||||||
|
server.region,
|
||||||
|
server.protocol !== undefined && `proto ${server.protocol}`,
|
||||||
|
server.edition !== undefined && `edition ${server.edition}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' · '),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import { Room, Track } from 'livekit-client'
|
||||||
|
// Transitive dependency of livekit-client (pinned by it); used only to
|
||||||
|
// build the one signal request room.simulateScenario cannot express.
|
||||||
|
import { SimulateScenario } from '@livekit/protocol'
|
||||||
|
|
||||||
|
/** Shared bandwidth ladder for the −/+ steppers; null = unlimited. */
|
||||||
|
export const BITRATE_LADDER_KBPS: Array<number | null> = [
|
||||||
|
null,
|
||||||
|
2000,
|
||||||
|
1000,
|
||||||
|
600,
|
||||||
|
300,
|
||||||
|
150,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const stepBitrate = (
|
||||||
|
current: number | null,
|
||||||
|
direction: 'decrease' | 'increase'
|
||||||
|
): number | null => {
|
||||||
|
const index = BITRATE_LADDER_KBPS.indexOf(current)
|
||||||
|
const safeIndex = index === -1 ? 0 : index
|
||||||
|
const next =
|
||||||
|
direction === 'decrease'
|
||||||
|
? Math.min(safeIndex + 1, BITRATE_LADDER_KBPS.length - 1)
|
||||||
|
: Math.max(safeIndex - 1, 0)
|
||||||
|
return BITRATE_LADDER_KBPS[next]
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedEncodings = new WeakMap<RTCRtpSender, Array<number | undefined>>()
|
||||||
|
|
||||||
|
export const setUplinkCap = async (
|
||||||
|
room: Room,
|
||||||
|
kbps: number | null
|
||||||
|
): Promise<void> => {
|
||||||
|
const senders: RTCRtpSender[] = []
|
||||||
|
room.localParticipant.trackPublications.forEach((pub) => {
|
||||||
|
const track = pub.track
|
||||||
|
if (track?.kind === Track.Kind.Video && track.sender) {
|
||||||
|
senders.push(track.sender)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const sender of senders) {
|
||||||
|
const params = sender.getParameters()
|
||||||
|
if (!params.encodings || params.encodings.length === 0) continue
|
||||||
|
|
||||||
|
if (kbps === null) {
|
||||||
|
const original = savedEncodings.get(sender)
|
||||||
|
params.encodings.forEach((encoding, i) => {
|
||||||
|
encoding.maxBitrate = original?.[i]
|
||||||
|
})
|
||||||
|
savedEncodings.delete(sender)
|
||||||
|
} else {
|
||||||
|
if (!savedEncodings.has(sender)) {
|
||||||
|
savedEncodings.set(
|
||||||
|
sender,
|
||||||
|
params.encodings.map((encoding) => encoding.maxBitrate)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const activeCount =
|
||||||
|
params.encodings.filter((encoding) => encoding.active !== false)
|
||||||
|
.length || 1
|
||||||
|
// Split the budget across active simulcast layers / SVC encoding.
|
||||||
|
const perEncoding = Math.max(
|
||||||
|
30_000,
|
||||||
|
Math.floor((kbps * 1000) / activeCount)
|
||||||
|
)
|
||||||
|
params.encodings.forEach((encoding) => {
|
||||||
|
encoding.maxBitrate = perEncoding
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sender.setParameters(params)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[MeetDevtools] setParameters failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setDownlinkCap = (
|
||||||
|
room: Room,
|
||||||
|
kbps: number | null
|
||||||
|
): Promise<void> =>
|
||||||
|
room.simulateScenario('subscriber-bandwidth', kbps === null ? 0 : kbps * 1000)
|
||||||
|
|
||||||
|
export type Scenario = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
/** where the simulation happens */
|
||||||
|
side: 'client' | 'server'
|
||||||
|
run: (room: Room) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SCENARIOS: Scenario[] = [
|
||||||
|
{
|
||||||
|
id: 'resume',
|
||||||
|
label: 'reconnect (resume)',
|
||||||
|
side: 'client',
|
||||||
|
// Replays a signaling WebSocket loss; media keeps flowing, client resumes.
|
||||||
|
run: (room) => room.simulateScenario('signal-reconnect'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'resume-fail',
|
||||||
|
label: 'reconnect (resume fails)',
|
||||||
|
side: 'client',
|
||||||
|
// Same, but the next resume attempt fails → exercises the retry ladder.
|
||||||
|
run: (room) => room.simulateScenario('resume-reconnect'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'full-reconnect',
|
||||||
|
label: 'full reconnect',
|
||||||
|
side: 'client',
|
||||||
|
// Complete rejoin with brand-new peer connections.
|
||||||
|
run: (room) => room.simulateScenario('full-reconnect'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'migration',
|
||||||
|
label: 'server migration',
|
||||||
|
side: 'server',
|
||||||
|
run: (room) => room.simulateScenario('migration'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node-failure',
|
||||||
|
label: 'SFU node failure',
|
||||||
|
side: 'server',
|
||||||
|
run: (room) => room.simulateScenario('node-failure'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'server-leave',
|
||||||
|
label: 'server disconnect',
|
||||||
|
side: 'server',
|
||||||
|
// Server-initiated leave: the closest thing to "you got kicked".
|
||||||
|
run: (room) => room.simulateScenario('server-leave'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export type TransportMode =
|
||||||
|
| 'auto'
|
||||||
|
| 'tcp'
|
||||||
|
| 'turn-udp'
|
||||||
|
| 'turn-tcp'
|
||||||
|
| 'turn-tls'
|
||||||
|
|
||||||
|
const clearServerTransportPreference = (room: Room): Promise<void> =>
|
||||||
|
room.engine.client.sendSimulateScenario(
|
||||||
|
new SimulateScenario({
|
||||||
|
scenario: { case: 'switchCandidateProtocol', value: 0 },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const setRelayOnly = (room: Room, relay: boolean) => {
|
||||||
|
room.engine.rtcConfig = {
|
||||||
|
...room.engine.rtcConfig,
|
||||||
|
iceTransportPolicy: relay ? 'relay' : 'all',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const settle = () => new Promise((resolve) => setTimeout(resolve, 300))
|
||||||
|
|
||||||
|
export const forceTransport = async (
|
||||||
|
room: Room,
|
||||||
|
mode: 'tcp' | 'turn-udp' | 'turn-tcp' | 'turn-tls'
|
||||||
|
): Promise<void> => {
|
||||||
|
if (mode === 'turn-udp') {
|
||||||
|
await clearServerTransportPreference(room)
|
||||||
|
setRelayOnly(room, true)
|
||||||
|
await settle()
|
||||||
|
return room.simulateScenario('full-reconnect')
|
||||||
|
}
|
||||||
|
setRelayOnly(room, false)
|
||||||
|
return room.simulateScenario(mode === 'turn-tls' ? 'force-tls' : 'force-tcp')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const releaseForcedTransport = async (room: Room): Promise<void> => {
|
||||||
|
setRelayOnly(room, false)
|
||||||
|
await clearServerTransportPreference(room)
|
||||||
|
await settle()
|
||||||
|
await room.simulateScenario('full-reconnect')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transportModeFromRoute = (route?: {
|
||||||
|
protocol?: string
|
||||||
|
relayProtocol?: string
|
||||||
|
}): TransportMode => {
|
||||||
|
// relayProtocol is the client→TURN leg; protocol alone means no relay.
|
||||||
|
if (route?.relayProtocol === 'tls') return 'turn-tls'
|
||||||
|
if (route?.relayProtocol === 'tcp') return 'turn-tcp'
|
||||||
|
if (route?.relayProtocol === 'udp') return 'turn-udp'
|
||||||
|
if (route?.protocol === 'tcp') return 'tcp'
|
||||||
|
return 'auto'
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { Room } from 'livekit-client'
|
||||||
|
|
||||||
|
export type TrackRow = {
|
||||||
|
key: string
|
||||||
|
dir: 'up' | 'down'
|
||||||
|
label: string
|
||||||
|
codec?: string
|
||||||
|
kbps: number
|
||||||
|
fps?: number
|
||||||
|
res?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StatsSnapshot = {
|
||||||
|
ts: number
|
||||||
|
/** wire totals from transport stats (includes headers, RTCP, FEC) */
|
||||||
|
upKbps: number
|
||||||
|
downKbps: number
|
||||||
|
rttMs?: number
|
||||||
|
/** worst inbound RTP jitter across subscribed tracks */
|
||||||
|
jitterMs?: number
|
||||||
|
availableOutKbps?: number
|
||||||
|
/** selected ICE route of the publisher transport (measured, not assumed) */
|
||||||
|
route?: { protocol?: string; type?: string; relayProtocol?: string }
|
||||||
|
/**
|
||||||
|
* relayProtocol values of gathered relay local candidates — i.e. which
|
||||||
|
* client→TURN transports are actually configured (udp/tcp/tls). Empty
|
||||||
|
* when no TURN server is configured.
|
||||||
|
*/
|
||||||
|
turnProtocols: string[]
|
||||||
|
tracks: TrackRow[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatDict = Record<string, unknown>
|
||||||
|
type Counters = Record<string, number>
|
||||||
|
|
||||||
|
const asNumber = (v: unknown): number | undefined =>
|
||||||
|
typeof v === 'number' && Number.isFinite(v) ? v : undefined
|
||||||
|
|
||||||
|
const asString = (v: unknown): string | undefined =>
|
||||||
|
typeof v === 'string' ? v : undefined
|
||||||
|
|
||||||
|
const shortCodec = (mimeType?: string) =>
|
||||||
|
mimeType ? mimeType.replace(/^(audio|video)\//, '') : undefined
|
||||||
|
|
||||||
|
const buildTrackLabels = (room: Room) => {
|
||||||
|
const labels = new Map<string, string>()
|
||||||
|
room.localParticipant.trackPublications.forEach((pub) => {
|
||||||
|
const id = pub.track?.mediaStreamTrack?.id
|
||||||
|
if (id) labels.set(id, `local ${pub.source}`)
|
||||||
|
})
|
||||||
|
room.remoteParticipants.forEach((participant) => {
|
||||||
|
// Keep rows scannable: participant names capped at 10 chars.
|
||||||
|
const rawName = participant.name || participant.identity
|
||||||
|
const name = rawName.length > 10 ? `${rawName.slice(0, 10)}.` : rawName
|
||||||
|
participant.trackPublications.forEach((pub) => {
|
||||||
|
const id = pub.track?.mediaStreamTrack?.id
|
||||||
|
if (id) labels.set(id, `${name} ${pub.source}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWebRTCStats = (
|
||||||
|
room: Room,
|
||||||
|
enabled: boolean,
|
||||||
|
intervalMs = 1000
|
||||||
|
) => {
|
||||||
|
// Single source of truth: the snapshot is just the last history entry.
|
||||||
|
const [history, setHistory] = useState<StatsSnapshot[]>([])
|
||||||
|
const prevRef = useRef(new Map<string, Counters>())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
const prev = prevRef.current
|
||||||
|
|
||||||
|
/** Per-stat counter deltas; returns 0 on the first sighting. */
|
||||||
|
const deltas = (key: string, now: Counters): Counters => {
|
||||||
|
const before = prev.get(key)
|
||||||
|
prev.set(key, now)
|
||||||
|
const out: Counters = {}
|
||||||
|
for (const [name, value] of Object.entries(now)) {
|
||||||
|
out[name] = before?.[name] !== undefined ? value - before[name] : 0
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const collect = async () => {
|
||||||
|
const reports: Array<{ pc: 'pub' | 'sub'; report: RTCStatsReport }> = []
|
||||||
|
try {
|
||||||
|
// Not public API — see file header.
|
||||||
|
const manager = room.engine?.pcManager
|
||||||
|
const pub = await manager?.publisher?.getStats()
|
||||||
|
if (pub) reports.push({ pc: 'pub', report: pub })
|
||||||
|
const sub = await manager?.subscriber?.getStats()
|
||||||
|
if (sub) reports.push({ pc: 'sub', report: sub })
|
||||||
|
} catch {
|
||||||
|
// Engine not ready or SDK internals changed; panel shows nothing.
|
||||||
|
}
|
||||||
|
if (cancelled || reports.length === 0) return
|
||||||
|
|
||||||
|
const labels = buildTrackLabels(room)
|
||||||
|
const tracks: TrackRow[] = []
|
||||||
|
let upKbps = 0
|
||||||
|
let downKbps = 0
|
||||||
|
let rttMs: number | undefined
|
||||||
|
let jitterMs: number | undefined
|
||||||
|
let availableOutKbps: number | undefined
|
||||||
|
let route: StatsSnapshot['route']
|
||||||
|
const turnProtocols = new Set<string>()
|
||||||
|
|
||||||
|
for (const { pc, report } of reports) {
|
||||||
|
const byId = new Map<string, StatDict>()
|
||||||
|
report.forEach((stat) => byId.set(stat.id as string, stat as StatDict))
|
||||||
|
|
||||||
|
report.forEach((raw) => {
|
||||||
|
const stat = raw as StatDict
|
||||||
|
const type = asString(stat.type)
|
||||||
|
const ts = asNumber(stat.timestamp) ?? Date.now()
|
||||||
|
const key = `${pc}:${asString(stat.id) ?? ''}`
|
||||||
|
|
||||||
|
if (
|
||||||
|
type === 'local-candidate' &&
|
||||||
|
asString(stat.candidateType) === 'relay'
|
||||||
|
) {
|
||||||
|
// Relay candidates are only gathered when a TURN server is
|
||||||
|
// configured and reachable; relayProtocol says how the client
|
||||||
|
// reaches it (udp/tcp/tls).
|
||||||
|
turnProtocols.add(
|
||||||
|
asString(stat.relayProtocol) ?? asString(stat.protocol) ?? 'udp'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'transport') {
|
||||||
|
const d = deltas(key, {
|
||||||
|
sent: asNumber(stat.bytesSent) ?? 0,
|
||||||
|
received: asNumber(stat.bytesReceived) ?? 0,
|
||||||
|
ts,
|
||||||
|
})
|
||||||
|
if (d.ts > 0) {
|
||||||
|
upKbps += Math.max(0, (d.sent * 8) / d.ts)
|
||||||
|
downKbps += Math.max(0, (d.received * 8) / d.ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'candidate-pair' && stat.nominated === true) {
|
||||||
|
const rtt = asNumber(stat.currentRoundTripTime)
|
||||||
|
if (rtt !== undefined) rttMs = Math.round(rtt * 1000)
|
||||||
|
const available = asNumber(stat.availableOutgoingBitrate)
|
||||||
|
if (available !== undefined && pc === 'pub') {
|
||||||
|
availableOutKbps = Math.round(available / 1000)
|
||||||
|
}
|
||||||
|
if (pc === 'pub') {
|
||||||
|
const local = byId.get(asString(stat.localCandidateId) ?? '')
|
||||||
|
route = {
|
||||||
|
protocol: asString(local?.protocol),
|
||||||
|
type: asString(local?.candidateType),
|
||||||
|
relayProtocol: asString(local?.relayProtocol),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'outbound-rtp' || type === 'inbound-rtp') {
|
||||||
|
const isUp = type === 'outbound-rtp'
|
||||||
|
|
||||||
|
if (!isUp) {
|
||||||
|
const jitter = asNumber(stat.jitter)
|
||||||
|
if (jitter !== undefined) {
|
||||||
|
const ms = Math.round(jitter * 1000)
|
||||||
|
if (jitterMs === undefined || ms > jitterMs) jitterMs = ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = deltas(key, {
|
||||||
|
bytes: asNumber(isUp ? stat.bytesSent : stat.bytesReceived) ?? 0,
|
||||||
|
ts,
|
||||||
|
})
|
||||||
|
const kbps = d.ts > 0 ? Math.max(0, (d.bytes * 8) / d.ts) : 0
|
||||||
|
|
||||||
|
// Resolve codec + source track.
|
||||||
|
const codecStat = byId.get(asString(stat.codecId) ?? '')
|
||||||
|
let msTrackId = asString(stat.trackIdentifier)
|
||||||
|
if (!msTrackId && isUp) {
|
||||||
|
const mediaSource = byId.get(asString(stat.mediaSourceId) ?? '')
|
||||||
|
msTrackId = asString(mediaSource?.trackIdentifier)
|
||||||
|
}
|
||||||
|
const rid = asString(stat.rid)
|
||||||
|
const baseLabel =
|
||||||
|
(msTrackId && labels.get(msTrackId)) ??
|
||||||
|
`${asString(stat.kind) ?? 'media'} ssrc ${asNumber(stat.ssrc) ?? '?'}`
|
||||||
|
|
||||||
|
const width = asNumber(stat.frameWidth)
|
||||||
|
const height = asNumber(stat.frameHeight)
|
||||||
|
|
||||||
|
tracks.push({
|
||||||
|
key,
|
||||||
|
dir: isUp ? 'up' : 'down',
|
||||||
|
label: rid ? `${baseLabel} [${rid}]` : baseLabel,
|
||||||
|
codec: shortCodec(asString(codecStat?.mimeType)),
|
||||||
|
kbps,
|
||||||
|
fps: asNumber(stat.framesPerSecond),
|
||||||
|
res: width && height ? `${width}x${height}` : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stable order (direction, then label): sorting by bitrate would
|
||||||
|
// reshuffle rows on every tick as kbps fluctuates.
|
||||||
|
tracks.sort((a, b) =>
|
||||||
|
a.dir === b.dir
|
||||||
|
? a.label.localeCompare(b.label)
|
||||||
|
: a.dir === 'up'
|
||||||
|
? -1
|
||||||
|
: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const next: StatsSnapshot = {
|
||||||
|
ts: Date.now(),
|
||||||
|
upKbps: Math.round(upKbps),
|
||||||
|
downKbps: Math.round(downKbps),
|
||||||
|
rttMs,
|
||||||
|
jitterMs,
|
||||||
|
availableOutKbps,
|
||||||
|
route,
|
||||||
|
turnProtocols: Array.from(turnProtocols).sort(),
|
||||||
|
tracks,
|
||||||
|
}
|
||||||
|
setHistory((h) => [...h.slice(-59), next])
|
||||||
|
}
|
||||||
|
|
||||||
|
void collect()
|
||||||
|
const id = window.setInterval(() => void collect(), intervalMs)
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
window.clearInterval(id)
|
||||||
|
}
|
||||||
|
}, [room, enabled, intervalMs])
|
||||||
|
|
||||||
|
return { snapshot: history[history.length - 1], history }
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ import { useSnapshot } from 'valtio'
|
|||||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||||
import { userStore } from '@/stores/user'
|
import { userStore } from '@/stores/user'
|
||||||
import { WatchMediaDeviceErrors } from './WatchMediaDeviceErrors'
|
import { WatchMediaDeviceErrors } from './WatchMediaDeviceErrors'
|
||||||
|
import { MeetDevtools } from '@/features/devtools'
|
||||||
import { VOICE_AUDIO_CONSTRAINTS } from '@/features/rooms/livekit/utils/constants'
|
import { VOICE_AUDIO_CONSTRAINTS } from '@/features/rooms/livekit/utils/constants'
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
@@ -298,6 +299,7 @@ export const Conference = ({
|
|||||||
<VideoConference />
|
<VideoConference />
|
||||||
{!isMobile && <InviteDialog mode={mode} />}
|
{!isMobile && <InviteDialog mode={mode} />}
|
||||||
<PictureInPictureConference />
|
<PictureInPictureConference />
|
||||||
|
<MeetDevtools />
|
||||||
</LiveKitRoom>
|
</LiveKitRoom>
|
||||||
</Screen>
|
</Screen>
|
||||||
</QueryAware>
|
</QueryAware>
|
||||||
|
|||||||
+6
-2
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react'
|
||||||
import { Button, Popover } from '@/primitives'
|
import { Button, Popover } from '@/primitives'
|
||||||
import { RiArrowUpSLine, RiImageCircleAiFill } from '@remixicon/react'
|
import { RiArrowUpSLine, RiImageCircleAiFill } from '@remixicon/react'
|
||||||
import { Track, type VideoCaptureOptions } from 'livekit-client'
|
import { Track, type VideoCaptureOptions, VideoPresets } from 'livekit-client'
|
||||||
|
|
||||||
import { ToggleDevice } from './ToggleDevice'
|
import { ToggleDevice } from './ToggleDevice'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
@@ -57,7 +57,8 @@ export const VideoDeviceControl = ({
|
|||||||
}: VideoDeviceControlProps) => {
|
}: VideoDeviceControlProps) => {
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' })
|
||||||
|
|
||||||
const { videoDeviceId, processorConfig } = useSnapshot(userChoicesStore)
|
const { videoDeviceId, processorConfig, videoPublishResolution } =
|
||||||
|
useSnapshot(userChoicesStore)
|
||||||
|
|
||||||
const onChange = React.useCallback(
|
const onChange = React.useCallback(
|
||||||
(enabled: boolean, isUserInitiated: boolean) =>
|
(enabled: boolean, isUserInitiated: boolean) =>
|
||||||
@@ -97,6 +98,9 @@ export const VideoDeviceControl = ({
|
|||||||
|
|
||||||
await toggle(!trackProps.enabled, {
|
await toggle(!trackProps.enabled, {
|
||||||
processor: processor,
|
processor: processor,
|
||||||
|
...(videoPublishResolution && {
|
||||||
|
resolution: VideoPresets[videoPublishResolution].resolution,
|
||||||
|
}),
|
||||||
} as VideoCaptureOptions)
|
} as VideoCaptureOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
saveVideoPublishResolution,
|
saveVideoPublishResolution,
|
||||||
saveVideoSubscribeQuality,
|
saveVideoSubscribeQuality,
|
||||||
userChoicesStore,
|
userChoicesStore,
|
||||||
|
VIDEO_RESOLUTIONS,
|
||||||
VideoResolution,
|
VideoResolution,
|
||||||
} from '@/stores/userChoices'
|
} from '@/stores/userChoices'
|
||||||
import { RowWrapper } from './layout/RowWrapper'
|
import { RowWrapper } from './layout/RowWrapper'
|
||||||
@@ -32,7 +33,8 @@ const EMPTY_PROPS = {}
|
|||||||
|
|
||||||
export const VideoTab = ({ id }: VideoTabProps) => {
|
export const VideoTab = ({ id }: VideoTabProps) => {
|
||||||
const { t } = useTranslation('settings', { keyPrefix: 'video' })
|
const { t } = useTranslation('settings', { keyPrefix: 'video' })
|
||||||
const { localParticipant, remoteParticipants } = useRoomContext()
|
const room = useRoomContext()
|
||||||
|
const { localParticipant, remoteParticipants } = room
|
||||||
|
|
||||||
const {
|
const {
|
||||||
videoDeviceId,
|
videoDeviceId,
|
||||||
@@ -69,20 +71,21 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
|||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleVideoResolutionChange = async (key: 'h720' | 'h360' | 'h180') => {
|
const handleVideoResolutionChange = async (key: VideoResolution) => {
|
||||||
const videoPublication = localParticipant.getTrackPublication(
|
saveVideoPublishResolution(key)
|
||||||
|
const videoTrack = localParticipant.getTrackPublication(
|
||||||
Track.Source.Camera
|
Track.Source.Camera
|
||||||
)
|
)?.track
|
||||||
const videoTrack = videoPublication?.track
|
if (!videoTrack) {
|
||||||
if (videoTrack) {
|
return
|
||||||
saveVideoPublishResolution(key)
|
|
||||||
await videoTrack.restartTrack({
|
|
||||||
resolution: VideoPresets[key].resolution,
|
|
||||||
deviceId: { exact: videoDeviceId },
|
|
||||||
processor:
|
|
||||||
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await videoTrack.restartTrack({
|
||||||
|
resolution: VideoPresets[key].resolution,
|
||||||
|
deviceId: { exact: videoDeviceId },
|
||||||
|
processor:
|
||||||
|
BackgroundProcessorFactory.fromProcessorConfig(processorConfig),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,20 +125,13 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
|||||||
}, [videoDeviceId, videoElement])
|
}, [videoDeviceId, videoElement])
|
||||||
|
|
||||||
const resolutionItems = useMemo(() => {
|
const resolutionItems = useMemo(() => {
|
||||||
return [
|
const labels: Record<VideoResolution, string> = {
|
||||||
{
|
h1080: `${t('resolution.publish.items.veryHigh')} (1080p)`,
|
||||||
value: 'h720',
|
h720: `${t('resolution.publish.items.high')} (720p)`,
|
||||||
label: `${t('resolution.publish.items.high')} (720p)`,
|
h360: `${t('resolution.publish.items.medium')} (360p)`,
|
||||||
},
|
h180: `${t('resolution.publish.items.low')} (180p)`,
|
||||||
{
|
}
|
||||||
value: 'h360',
|
return VIDEO_RESOLUTIONS.map((value) => ({ value, label: labels[value] }))
|
||||||
label: `${t('resolution.publish.items.medium')} (360p)`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'h180',
|
|
||||||
label: `${t('resolution.publish.items.low')} (180p)`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, [t])
|
}, [t])
|
||||||
|
|
||||||
const videoQualityItems = useMemo(() => {
|
const videoQualityItems = useMemo(() => {
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"publish": {
|
"publish": {
|
||||||
"label": "Wähle die maximale Auflösung beim Senden",
|
"label": "Wähle die maximale Auflösung beim Senden",
|
||||||
"items": {
|
"items": {
|
||||||
|
"veryHigh": "Sehr hohe Auflösung",
|
||||||
"high": "Hohe Auflösung",
|
"high": "Hohe Auflösung",
|
||||||
"medium": "Mittlere Auflösung",
|
"medium": "Mittlere Auflösung",
|
||||||
"low": "Niedrige Auflösung"
|
"low": "Niedrige Auflösung"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"publish": {
|
"publish": {
|
||||||
"label": "Select your sending resolution (max.)",
|
"label": "Select your sending resolution (max.)",
|
||||||
"items": {
|
"items": {
|
||||||
|
"veryHigh": "Very high definition",
|
||||||
"high": "High definition",
|
"high": "High definition",
|
||||||
"medium": "Standard definition",
|
"medium": "Standard definition",
|
||||||
"low": "Low definition"
|
"low": "Low definition"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"publish": {
|
"publish": {
|
||||||
"label": "Selecciona tu resolución de envío (máx.)",
|
"label": "Selecciona tu resolución de envío (máx.)",
|
||||||
"items": {
|
"items": {
|
||||||
|
"veryHigh": "Muy alta definición",
|
||||||
"high": "Alta definición",
|
"high": "Alta definición",
|
||||||
"medium": "Definición estándar",
|
"medium": "Definición estándar",
|
||||||
"low": "Baja definición"
|
"low": "Baja definición"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"publish": {
|
"publish": {
|
||||||
"label": "Sélectionner votre résolution d'envoi (max.)",
|
"label": "Sélectionner votre résolution d'envoi (max.)",
|
||||||
"items": {
|
"items": {
|
||||||
|
"veryHigh": "Très haute définition",
|
||||||
"high": "Haute définition",
|
"high": "Haute définition",
|
||||||
"medium": "Définition standard",
|
"medium": "Définition standard",
|
||||||
"low": "Basse définition"
|
"low": "Basse définition"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"publish": {
|
"publish": {
|
||||||
"label": "Selecteer uw verzendresolutie (max.)",
|
"label": "Selecteer uw verzendresolutie (max.)",
|
||||||
"items": {
|
"items": {
|
||||||
|
"veryHigh": "Zeer hoge definitie",
|
||||||
"high": "Hoge definitie",
|
"high": "Hoge definitie",
|
||||||
"medium": "Standaarddefinitie",
|
"medium": "Standaarddefinitie",
|
||||||
"low": "Lage definitie"
|
"low": "Lage definitie"
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ import {
|
|||||||
} from '@livekit/components-core'
|
} from '@livekit/components-core'
|
||||||
import { VideoQuality } from 'livekit-client'
|
import { VideoQuality } from 'livekit-client'
|
||||||
|
|
||||||
export type VideoResolution = 'h720' | 'h360' | 'h180'
|
export const VIDEO_RESOLUTIONS = ['h1080', 'h720', 'h360', 'h180'] as const
|
||||||
|
|
||||||
|
export type VideoResolution = (typeof VIDEO_RESOLUTIONS)[number]
|
||||||
|
|
||||||
|
const isVideoResolution = (value: unknown): value is VideoResolution =>
|
||||||
|
VIDEO_RESOLUTIONS.includes(value as VideoResolution)
|
||||||
|
|
||||||
export type LocalUserChoices = Omit<LocalUserChoicesLK, 'username'> & {
|
export type LocalUserChoices = Omit<LocalUserChoicesLK, 'username'> & {
|
||||||
processorConfig?: ProcessorConfig
|
processorConfig?: ProcessorConfig
|
||||||
@@ -21,13 +26,17 @@ export type LocalUserChoices = Omit<LocalUserChoicesLK, 'username'> & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getUserChoicesState(): LocalUserChoices {
|
function getUserChoicesState(): LocalUserChoices {
|
||||||
return {
|
const stored: LocalUserChoices = {
|
||||||
noiseReductionEnabled: false,
|
noiseReductionEnabled: false,
|
||||||
audioOutputDeviceId: 'default', // Use 'default' to match LiveKit's standard device selection behavior
|
audioOutputDeviceId: 'default', // Use 'default' to match LiveKit's standard device selection behavior
|
||||||
videoPublishResolution: 'h720',
|
videoPublishResolution: 'h720',
|
||||||
videoSubscribeQuality: VideoQuality.HIGH,
|
videoSubscribeQuality: VideoQuality.HIGH,
|
||||||
...loadUserChoices(),
|
...loadUserChoices(),
|
||||||
}
|
}
|
||||||
|
if (!isVideoResolution(stored.videoPublishResolution)) {
|
||||||
|
stored.videoPublishResolution = 'h720'
|
||||||
|
}
|
||||||
|
return stored
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
|
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
|
||||||
|
|||||||
Reference in New Issue
Block a user