mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-02 21:58:29 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 089d016d12 | |||
| cf3960db95 | |||
| d80d31897c | |||
| 63a7751072 |
@@ -8,6 +8,11 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) add 1080p sending resolution option #1660
|
||||
- ✨(backend) add Traefik support via configurable media-auth url header #1649
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) keep the sending resolution picked while the camera is off #1667
|
||||
|
||||
@@ -65,6 +65,7 @@ RUN apk update && apk upgrade \
|
||||
musl \
|
||||
musl-utils \
|
||||
zlib>=1.3.2-r0 \
|
||||
libexpat>=2.8.4-r0 \
|
||||
&& apk del curl
|
||||
|
||||
USER nginx
|
||||
|
||||
@@ -1076,9 +1076,10 @@ class RecordingViewSet(
|
||||
|
||||
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.
|
||||
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
|
||||
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
||||
is configured to do this.
|
||||
@@ -1088,9 +1089,13 @@ class RecordingViewSet(
|
||||
reasons.
|
||||
"""
|
||||
# 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:
|
||||
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()
|
||||
|
||||
logger.debug("Original url: '%s'", original_url)
|
||||
@@ -1415,7 +1420,8 @@ class FileViewSet(
|
||||
Authorize access based on the original URL of an Nginx subrequest
|
||||
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
|
||||
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
||||
is configured to do this.
|
||||
@@ -1434,9 +1440,13 @@ class FileViewSet(
|
||||
- PermissionDenied if authorization fails.
|
||||
"""
|
||||
# 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:
|
||||
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()
|
||||
|
||||
parsed_url = urlparse(original_url)
|
||||
|
||||
@@ -7,6 +7,7 @@ from urllib.parse import quote, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.test import override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
@@ -143,3 +144,59 @@ def test_api_files_media_auth_own_file_deleted():
|
||||
)
|
||||
|
||||
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.core.files.storage import default_storage
|
||||
from django.test import override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
@@ -282,3 +283,63 @@ def test_api_recordings_media_auth_success_administrator(mode):
|
||||
timeout=1,
|
||||
)
|
||||
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
|
||||
|
||||
@@ -129,6 +129,15 @@ class Base(Configuration):
|
||||
MEDIA_BASE_URL = values.Value(
|
||||
"", 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
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ RUN apk update && apk upgrade \
|
||||
musl \
|
||||
musl-utils \
|
||||
zlib>=1.3.2-r0 \
|
||||
libexpat>=2.8.4-r0 \
|
||||
&& apk del curl
|
||||
|
||||
USER nginx
|
||||
|
||||
@@ -32,14 +32,18 @@ export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind>
|
||||
}
|
||||
|
||||
export class BackgroundProcessorFactory {
|
||||
private static _isSupported?: boolean
|
||||
|
||||
static hasModernApiSupport() {
|
||||
return ProcessorWrapper.hasModernApiSupport
|
||||
}
|
||||
|
||||
static isSupported() {
|
||||
return (
|
||||
supportsBackgroundProcessors() || BackgroundCustomProcessor.isSupported
|
||||
)
|
||||
if (this._isSupported === undefined) {
|
||||
this._isSupported =
|
||||
supportsBackgroundProcessors() || BackgroundCustomProcessor.isSupported
|
||||
}
|
||||
return this._isSupported
|
||||
}
|
||||
|
||||
static getProcessor(
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
saveVideoPublishResolution,
|
||||
saveVideoSubscribeQuality,
|
||||
userChoicesStore,
|
||||
VIDEO_RESOLUTIONS,
|
||||
VideoResolution,
|
||||
} from '@/stores/userChoices'
|
||||
import { RowWrapper } from './layout/RowWrapper'
|
||||
@@ -70,7 +71,7 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
isDisabled: true,
|
||||
}
|
||||
|
||||
const handleVideoResolutionChange = async (key: 'h720' | 'h360' | 'h180') => {
|
||||
const handleVideoResolutionChange = async (key: VideoResolution) => {
|
||||
saveVideoPublishResolution(key)
|
||||
const videoTrack = localParticipant.getTrackPublication(
|
||||
Track.Source.Camera
|
||||
@@ -124,20 +125,13 @@ export const VideoTab = ({ id }: VideoTabProps) => {
|
||||
}, [videoDeviceId, videoElement])
|
||||
|
||||
const resolutionItems = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
value: 'h720',
|
||||
label: `${t('resolution.publish.items.high')} (720p)`,
|
||||
},
|
||||
{
|
||||
value: 'h360',
|
||||
label: `${t('resolution.publish.items.medium')} (360p)`,
|
||||
},
|
||||
{
|
||||
value: 'h180',
|
||||
label: `${t('resolution.publish.items.low')} (180p)`,
|
||||
},
|
||||
]
|
||||
const labels: Record<VideoResolution, string> = {
|
||||
h1080: `${t('resolution.publish.items.veryHigh')} (1080p)`,
|
||||
h720: `${t('resolution.publish.items.high')} (720p)`,
|
||||
h360: `${t('resolution.publish.items.medium')} (360p)`,
|
||||
h180: `${t('resolution.publish.items.low')} (180p)`,
|
||||
}
|
||||
return VIDEO_RESOLUTIONS.map((value) => ({ value, label: labels[value] }))
|
||||
}, [t])
|
||||
|
||||
const videoQualityItems = useMemo(() => {
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"publish": {
|
||||
"label": "Wähle die maximale Auflösung beim Senden",
|
||||
"items": {
|
||||
"veryHigh": "Sehr hohe Auflösung",
|
||||
"high": "Hohe Auflösung",
|
||||
"medium": "Mittlere Auflösung",
|
||||
"low": "Niedrige Auflösung"
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"publish": {
|
||||
"label": "Select your sending resolution (max.)",
|
||||
"items": {
|
||||
"veryHigh": "Very high definition",
|
||||
"high": "High definition",
|
||||
"medium": "Standard definition",
|
||||
"low": "Low definition"
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"publish": {
|
||||
"label": "Selecciona tu resolución de envío (máx.)",
|
||||
"items": {
|
||||
"veryHigh": "Muy alta definición",
|
||||
"high": "Alta definición",
|
||||
"medium": "Definición estándar",
|
||||
"low": "Baja definición"
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"publish": {
|
||||
"label": "Sélectionner votre résolution d'envoi (max.)",
|
||||
"items": {
|
||||
"veryHigh": "Très haute définition",
|
||||
"high": "Haute définition",
|
||||
"medium": "Définition standard",
|
||||
"low": "Basse définition"
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"publish": {
|
||||
"label": "Selecteer uw verzendresolutie (max.)",
|
||||
"items": {
|
||||
"veryHigh": "Zeer hoge definitie",
|
||||
"high": "Hoge definitie",
|
||||
"medium": "Standaarddefinitie",
|
||||
"low": "Lage definitie"
|
||||
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
} from '@livekit/components-core'
|
||||
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'> & {
|
||||
processorConfig?: ProcessorConfig
|
||||
@@ -21,13 +26,17 @@ export type LocalUserChoices = Omit<LocalUserChoicesLK, 'username'> & {
|
||||
}
|
||||
|
||||
function getUserChoicesState(): LocalUserChoices {
|
||||
return {
|
||||
const stored: LocalUserChoices = {
|
||||
noiseReductionEnabled: false,
|
||||
audioOutputDeviceId: 'default', // Use 'default' to match LiveKit's standard device selection behavior
|
||||
videoPublishResolution: 'h720',
|
||||
videoSubscribeQuality: VideoQuality.HIGH,
|
||||
...loadUserChoices(),
|
||||
}
|
||||
if (!isVideoResolution(stored.videoPublishResolution)) {
|
||||
stored.videoPublishResolution = 'h720'
|
||||
}
|
||||
return stored
|
||||
}
|
||||
|
||||
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
|
||||
|
||||
Reference in New Issue
Block a user