feat(linux): restore the Flutter-texture path as the SDR fallback
2.13.0 deleted the display-agnostic EGL/Flutter-texture renderer and made the native Wayland plane the only path, hard-rejecting every session that cannot host one - X11, XWayland (SteamOS Gaming Mode runs native apps through Gamescope's XWayland), or a plane whose EGL bootstrap failed. This restores the 2.11.0 texture path from git history as the fallback: - Re-add mpv_texture.cc/.h and mpv_gpu_bootstrap.cc/.h (2.11.0 verbatim): an FlTextureGL whose populate renders mpv into an offscreen FBO sampled by Flutter via an EGL image. - MpvPlayer gains the texture-mode render-context API (InitRenderContext, HasRenderContext, GetEglDisplay/Context, Render(w,h,fbo)) beside the plane's InitRenderContextForSurface/RenderToSurface. The isolated ES 2.0 context on Flutter's display and the X11 display param are exactly the 2.11.0 configuration hardware decode demonstrably worked in. - initialize now tries the plane first and falls back to the texture path when it cannot be brought up, returning the texture id (Dart's 2.11.0 'result is int' contract) with waitForVideoReady gating playback until the GPU bootstrap settles. Both paths share one mpv core, so the plane/texture decision precedes render-context creation. - hdr-enabled/hdr-tone-mapping are intercepted in texture mode (no plane, no HDR); the HDR toggle hides itself via isHDRSupported. - New Linux setting 'Video rendering mode' (Automatic / Texture) forces the fallback - the user-visible workaround for plane-only trouble and for the hwdec interop regression, plus translations in all locales. SDR only on the fallback, matching 2.11.0; the plane path is unchanged.
This commit is contained in:
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Altyazılar və konfiqurasiya",
|
||||
"seekAndTiming": "Sarğı və vaxt tənzimləməsi",
|
||||
"behavior": "Davranış",
|
||||
"exportDialogTitle": "Plezy tənzimləmələrini ixrac et"
|
||||
"exportDialogTitle": "Plezy tənzimləmələrini ixrac et",
|
||||
"linuxVideoRenderMode": "Video göstərmə rejimi",
|
||||
"linuxVideoRenderModeDescription": "Avtomatik yerli Wayland müstəvisinə üstünlük verir (HDR dəstəkləyir); Texture SDR ehtiyat yolunu məcbur edir.",
|
||||
"linuxVideoRenderModeAuto": "Avtomatik",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Kino, serial, musiqi axtar...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Субтитри и конфигурация",
|
||||
"seekAndTiming": "Търсене и време",
|
||||
"behavior": "Поведение",
|
||||
"exportDialogTitle": "Експортиране на настройките на Plezy"
|
||||
"exportDialogTitle": "Експортиране на настройките на Plezy",
|
||||
"linuxVideoRenderMode": "Режим на видеото",
|
||||
"linuxVideoRenderModeDescription": "Автоматично предпочита родната Wayland равнина (HDR-съвместима); Texture принуждава SDR резервния път.",
|
||||
"linuxVideoRenderModeAuto": "Автоматично",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Търсене на филми, сериали, музика...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Undertekster og konfiguration",
|
||||
"seekAndTiming": "Søgning og timing",
|
||||
"behavior": "Adfærd",
|
||||
"exportDialogTitle": "Eksportér Plezy-indstillinger"
|
||||
"exportDialogTitle": "Eksportér Plezy-indstillinger",
|
||||
"linuxVideoRenderMode": "Video-gengivelsestilstand",
|
||||
"linuxVideoRenderModeDescription": "Automatisk foretrækker det native Wayland-plan (HDR-kompatibelt); Texture tvinger SDR-fallback-stien.",
|
||||
"linuxVideoRenderModeAuto": "Automatisk",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Søg film, serier, musik...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Untertitel und Konfiguration",
|
||||
"seekAndTiming": "Spulen & Timing",
|
||||
"behavior": "Verhalten",
|
||||
"exportDialogTitle": "Plezy-Einstellungen exportieren"
|
||||
"exportDialogTitle": "Plezy-Einstellungen exportieren",
|
||||
"linuxVideoRenderMode": "Video-Rendermodus",
|
||||
"linuxVideoRenderModeDescription": "Automatisch bevorzugt die native Wayland-Ebene (HDR-fähig); Texture erzwingt den SDR-Fallback-Pfad.",
|
||||
"linuxVideoRenderModeAuto": "Automatisch",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Filme, Serien und Musik suchen …",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Subtitles & Configuration",
|
||||
"seekAndTiming": "Seek & Timing",
|
||||
"behavior": "Behavior",
|
||||
"exportDialogTitle": "Export Plezy settings"
|
||||
"exportDialogTitle": "Export Plezy settings",
|
||||
"linuxVideoRenderMode": "Video rendering mode",
|
||||
"linuxVideoRenderModeDescription": "Automatic prefers the native Wayland plane (HDR-capable); Texture forces the SDR fallback path.",
|
||||
"linuxVideoRenderModeAuto": "Automatic",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Search movies, shows, music...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Subtítulos y configuración",
|
||||
"seekAndTiming": "Desplazamiento y tiempos",
|
||||
"behavior": "Comportamiento",
|
||||
"exportDialogTitle": "Exportar ajustes de Plezy"
|
||||
"exportDialogTitle": "Exportar ajustes de Plezy",
|
||||
"linuxVideoRenderMode": "Modo de renderizado de vídeo",
|
||||
"linuxVideoRenderModeDescription": "Automático prefiere el plano Wayland nativo (compatible con HDR); Texture fuerza la ruta de reserva SDR.",
|
||||
"linuxVideoRenderModeAuto": "Automático",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Buscar películas, series, música...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Sous-titres et configuration",
|
||||
"seekAndTiming": "Déplacement et minutage",
|
||||
"behavior": "Comportement",
|
||||
"exportDialogTitle": "Exporter les paramètres de Plezy"
|
||||
"exportDialogTitle": "Exporter les paramètres de Plezy",
|
||||
"linuxVideoRenderMode": "Mode de rendu vidéo",
|
||||
"linuxVideoRenderModeDescription": "Automatique préfère le plan Wayland natif (compatible HDR) ; Texture force le chemin de repli SDR.",
|
||||
"linuxVideoRenderModeAuto": "Automatique",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Rechercher des films, des séries, de la musique...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Feliratok és konfiguráció",
|
||||
"seekAndTiming": "Tekerés és időzítés",
|
||||
"behavior": "Viselkedés",
|
||||
"exportDialogTitle": "Plezy-beállítások exportálása"
|
||||
"exportDialogTitle": "Plezy-beállítások exportálása",
|
||||
"linuxVideoRenderMode": "Videó megjelenítési mód",
|
||||
"linuxVideoRenderModeDescription": "Az Automatikus a natív Wayland síkot részesíti előnyben (HDR-képes); a Texture az SDR tartalék utat kényszeríti ki.",
|
||||
"linuxVideoRenderModeAuto": "Automatikus",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Keresés filmek, sorozatok és zenék között...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Sottotitoli e impostazioni",
|
||||
"seekAndTiming": "Avanzamento e tempi",
|
||||
"behavior": "Comportamento",
|
||||
"exportDialogTitle": "Esporta le impostazioni di Plezy"
|
||||
"exportDialogTitle": "Esporta le impostazioni di Plezy",
|
||||
"linuxVideoRenderMode": "Modalità di rendering video",
|
||||
"linuxVideoRenderModeDescription": "Automatico preferisce il piano Wayland nativo (compatibile HDR); Texture forza il percorso di riserva SDR.",
|
||||
"linuxVideoRenderModeAuto": "Automatico",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Cerca film, serie TV e musica...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "字幕と設定",
|
||||
"seekAndTiming": "シークとタイミング",
|
||||
"behavior": "動作",
|
||||
"exportDialogTitle": "Plezyの設定をエクスポート"
|
||||
"exportDialogTitle": "Plezyの設定をエクスポート",
|
||||
"linuxVideoRenderMode": "動画レンダリングモード",
|
||||
"linuxVideoRenderModeDescription": "自動はネイティブの Wayland プレーン(HDR 対応)を優先し、Texture は SDR フォールバック経路を強制します。",
|
||||
"linuxVideoRenderModeAuto": "自動",
|
||||
"linuxVideoRenderModeTexture": "Texture(SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "映画、番組、音楽を検索…",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Субтитрлер мен конфигурация",
|
||||
"seekAndTiming": "Айналдыру және уақытты реттеу",
|
||||
"behavior": "Мінез-құлық",
|
||||
"exportDialogTitle": "Plezy параметрлерін экспорттау"
|
||||
"exportDialogTitle": "Plezy параметрлерін экспорттау",
|
||||
"linuxVideoRenderMode": "Бейнені көрсету режимі",
|
||||
"linuxVideoRenderModeDescription": "Автоматты режим жергілікті Wayland жазықтығын қалайды (HDR қолдайды); Texture SDR резервтік жолды мәжбүрлейді.",
|
||||
"linuxVideoRenderModeAuto": "Автоматты",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Фильмдер, сериалдар, музыка іздеу...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "자막 및 구성",
|
||||
"seekAndTiming": "탐색 및 타이밍",
|
||||
"behavior": "동작",
|
||||
"exportDialogTitle": "Plezy 설정 내보내기"
|
||||
"exportDialogTitle": "Plezy 설정 내보내기",
|
||||
"linuxVideoRenderMode": "동영상 렌더링 모드",
|
||||
"linuxVideoRenderModeDescription": "자동은 네이티브 Wayland 평면(HDR 지원)을 선호하고, Texture는 SDR 대체 경로를 강제합니다.",
|
||||
"linuxVideoRenderModeAuto": "자동",
|
||||
"linuxVideoRenderModeTexture": "Texture(SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "영화, 시리즈, 음악 등을 검색하세요...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Undertekster og konfigurasjon",
|
||||
"seekAndTiming": "Spoling og tidsinnstillinger",
|
||||
"behavior": "Oppførsel",
|
||||
"exportDialogTitle": "Eksporter Plezy-innstillinger"
|
||||
"exportDialogTitle": "Eksporter Plezy-innstillinger",
|
||||
"linuxVideoRenderMode": "Video-gjengivelsesmodus",
|
||||
"linuxVideoRenderModeDescription": "Automatisk foretrekker det native Wayland-planet (HDR-kompatibelt); Texture tvinger SDR-fallback-stien.",
|
||||
"linuxVideoRenderModeAuto": "Automatisk",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Søk i filmer, serier, musikk...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Ondertitels en instellingen",
|
||||
"seekAndTiming": "Spoelen en timing",
|
||||
"behavior": "Gedrag",
|
||||
"exportDialogTitle": "Plezy-instellingen exporteren"
|
||||
"exportDialogTitle": "Plezy-instellingen exporteren",
|
||||
"linuxVideoRenderMode": "Videoweergavemodus",
|
||||
"linuxVideoRenderModeDescription": "Automatisch geeft de voorkeur aan het native Wayland-vlak (HDR-compatibel); Texture dwingt het SDR-reservepad af.",
|
||||
"linuxVideoRenderModeAuto": "Automatisch",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Zoek films, series, muziek...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Napisy i konfiguracja",
|
||||
"seekAndTiming": "Przewijanie i czas",
|
||||
"behavior": "Zachowanie",
|
||||
"exportDialogTitle": "Eksport ustawień Plezy"
|
||||
"exportDialogTitle": "Eksport ustawień Plezy",
|
||||
"linuxVideoRenderMode": "Tryb renderowania wideo",
|
||||
"linuxVideoRenderModeDescription": "Automatyczny preferuje natywną płaszczyznę Wayland (zgodną z HDR); Texture wymusza ścieżkę zapasową SDR.",
|
||||
"linuxVideoRenderModeAuto": "Automatyczny",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Szukaj filmów, seriali, muzyki...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Legendas e configuração",
|
||||
"seekAndTiming": "Busca e tempo",
|
||||
"behavior": "Comportamento",
|
||||
"exportDialogTitle": "Exportar configurações do Plezy"
|
||||
"exportDialogTitle": "Exportar configurações do Plezy",
|
||||
"linuxVideoRenderMode": "Modo de renderização de vídeo",
|
||||
"linuxVideoRenderModeDescription": "Automático prefere o plano Wayland nativo (compatível com HDR); Texture força o caminho de reserva SDR.",
|
||||
"linuxVideoRenderModeAuto": "Automático",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Buscar filmes, séries, músicas...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Субтитры и конфигурация",
|
||||
"seekAndTiming": "Перемотка и время",
|
||||
"behavior": "Поведение",
|
||||
"exportDialogTitle": "Экспорт настроек Plezy"
|
||||
"exportDialogTitle": "Экспорт настроек Plezy",
|
||||
"linuxVideoRenderMode": "Режим рендеринга видео",
|
||||
"linuxVideoRenderModeDescription": "Автоматический режим предпочитает нативную плоскость Wayland (поддержка HDR); Texture принудительно использует запасной путь SDR.",
|
||||
"linuxVideoRenderModeAuto": "Автоматически",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Поиск фильмов, сериалов, музыки...",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 22
|
||||
/// Strings: 40722 (1851 per locale)
|
||||
/// Strings: 40810 (1855 per locale)
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$az extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Sarğı və vaxt tənzimləməsi';
|
||||
@override String get behavior => 'Davranış';
|
||||
@override String get exportDialogTitle => 'Plezy tənzimləmələrini ixrac et';
|
||||
@override String get linuxVideoRenderMode => 'Video göstərmə rejimi';
|
||||
@override String get linuxVideoRenderModeDescription => 'Avtomatik yerli Wayland müstəvisinə üstünlük verir (HDR dəstəkləyir); Texture SDR ehtiyat yolunu məcbur edir.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Avtomatik';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsAz {
|
||||
'settings.seekAndTiming' => 'Sarğı və vaxt tənzimləməsi',
|
||||
'settings.behavior' => 'Davranış',
|
||||
'settings.exportDialogTitle' => 'Plezy tənzimləmələrini ixrac et',
|
||||
'settings.linuxVideoRenderMode' => 'Video göstərmə rejimi',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Avtomatik yerli Wayland müstəvisinə üstünlük verir (HDR dəstəkləyir); Texture SDR ehtiyat yolunu məcbur edir.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Avtomatik',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Kino, serial, musiqi axtar...',
|
||||
'search.tryDifferentTerm' => 'Fərqli axtarış sözü cəhd edin',
|
||||
'search.searchYourMedia' => 'Mediyanızda axtarın',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsAz {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy bunun hansı faylları siləcəyini yoxlaya bilmədi, ona görə də yuxarıda adı çəkilən elementdən daha çoxunu silə bilər. Ləğv edib təzədən cəhd edin və ya yenə də silin.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Serveriniz bu element üçün fayl təfərrüatlarını təqdim etmədi, ona görə Plezy bunun hansı faylları siləcəyini yoxlaya bilmir. Yuxarıda adı çəkilən elementdən daha çoxunu silə bilər.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Media elementi uğurla silindi',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Media elementi silinə bilmədi',
|
||||
'mediaMenu.rate' => 'Qiymətləndir',
|
||||
'mediaMenu.playFromBeginning' => 'Əvvəldən oynat',
|
||||
'mediaMenu.playVersion' => 'Versiyanı oynat...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Qiymətləndir',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Sevimli',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsAz {
|
||||
'explore.watchlistNoMatch' => 'Bu elementi heç bir baxış siyahısı ilə uyğunlaşdırmaq olmadı',
|
||||
'explore.notInLibrary' => 'Kitabxananızda yoxdur',
|
||||
'explore.inTheseLibraries' => 'Bu kitabxanalarda var',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Kitabxananız yoxlanılır...',
|
||||
'explore.emptyTitle' => 'Hələlik burada heç nə yoxdur',
|
||||
'explore.emptyMessage' => ({required Object source}) => '${source} mənbəsindən olan sətirlər burada görünəcək.',
|
||||
'explore.searchHint' => ({required Object source}) => '${source} daxilində axtar',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => '"${query}" üçün nəticə tapılmadı',
|
||||
'explore.searchPrompt' => ({required Object source}) => '${source} vasitəsilə kino və seriallar axtarın.',
|
||||
'explore.searchFailed' => 'Axtarış uğursuz oldu. Bağlantınızı yoxlayın.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsAz {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Heç bir ünvana qoşuluna bilmədi',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => '${attempts} cəhddən sonra əlaqə kəsildi',
|
||||
'companionRemote.errors.connectionLost' => 'Əlaqə kəsildi',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Bağlantı autentifikasiyadan əvvəl bağlandı',
|
||||
'videoSettings.playbackSpeed' => 'Oynatma sürəti',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Aktivdir (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Miqyas',
|
||||
'videoSettings.sleepTimer' => 'Yuxu taymeri',
|
||||
'videoSettings.audioSync' => 'Səs sinxronizasiyası',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$bg extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Търсене и време';
|
||||
@override String get behavior => 'Поведение';
|
||||
@override String get exportDialogTitle => 'Експортиране на настройките на Plezy';
|
||||
@override String get linuxVideoRenderMode => 'Режим на видеото';
|
||||
@override String get linuxVideoRenderModeDescription => 'Автоматично предпочита родната Wayland равнина (HDR-съвместима); Texture принуждава SDR резервния път.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Автоматично';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsBg {
|
||||
'settings.seekAndTiming' => 'Търсене и време',
|
||||
'settings.behavior' => 'Поведение',
|
||||
'settings.exportDialogTitle' => 'Експортиране на настройките на Plezy',
|
||||
'settings.linuxVideoRenderMode' => 'Режим на видеото',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Автоматично предпочита родната Wayland равнина (HDR-съвместима); Texture принуждава SDR резервния път.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Автоматично',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Търсене на филми, сериали, музика...',
|
||||
'search.tryDifferentTerm' => 'Опитайте с различна дума за търсене',
|
||||
'search.searchYourMedia' => 'Търсете в медийното си съдържание',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsBg {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy не успя да провери кои файлове ще бъдат премахнати, така че може да изтрие повече от посочения по-горе елемент. Откажете и опитайте отново, или изтрийте въпреки това.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Сървърът ви не предостави данни за файловете на този елемент, така че Plezy не може да провери кои файлове ще бъдат премахнати. Може да изтрие повече от посочения по-горе елемент.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Елементът е изтрит успешно',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Неуспешно изтриване на елемента',
|
||||
'mediaMenu.rate' => 'Оцени',
|
||||
'mediaMenu.playFromBeginning' => 'Пусни от началото',
|
||||
'mediaMenu.playVersion' => 'Пусни версия...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Оцени',
|
||||
'rateSheet.server' => 'Сървър',
|
||||
'rateSheet.favorite' => 'Добави в любими',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsBg {
|
||||
'explore.watchlistNoMatch' => 'Този елемент не можа да бъде съпоставен със списък за гледане',
|
||||
'explore.notInLibrary' => 'Не е в твоята библиотека',
|
||||
'explore.inTheseLibraries' => 'В тези библиотеки',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Проверка на твоята библиотека...',
|
||||
'explore.emptyTitle' => 'Тук все още няма нищо',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Редовете от ${source} ще се появят тук, когато има съдържание.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Търсене в ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Няма резултати за "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Търси филми и сериали в ${source}.',
|
||||
'explore.searchFailed' => 'Търсенето се провали. Провери връзката си и опитай отново.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsBg {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Неуспешно свързване към който и да е адрес',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Връзката е загубена след ${attempts} опита',
|
||||
'companionRemote.errors.connectionLost' => 'Връзката е загубена',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Връзката беше затворена преди удостоверяването',
|
||||
'videoSettings.playbackSpeed' => 'Скорост на възпроизвеждане',
|
||||
'videoSettings.normalSpeed' => 'Нормална',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Активен (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Мащаб',
|
||||
'videoSettings.sleepTimer' => 'Таймер за заспиване',
|
||||
'videoSettings.audioSync' => 'Синхронизация на аудио',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$da extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Søgning og timing';
|
||||
@override String get behavior => 'Adfærd';
|
||||
@override String get exportDialogTitle => 'Eksportér Plezy-indstillinger';
|
||||
@override String get linuxVideoRenderMode => 'Video-gengivelsestilstand';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automatisk foretrækker det native Wayland-plan (HDR-kompatibelt); Texture tvinger SDR-fallback-stien.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatisk';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsDa {
|
||||
'settings.seekAndTiming' => 'Søgning og timing',
|
||||
'settings.behavior' => 'Adfærd',
|
||||
'settings.exportDialogTitle' => 'Eksportér Plezy-indstillinger',
|
||||
'settings.linuxVideoRenderMode' => 'Video-gengivelsestilstand',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatisk foretrækker det native Wayland-plan (HDR-kompatibelt); Texture tvinger SDR-fallback-stien.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatisk',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Søg film, serier, musik...',
|
||||
'search.tryDifferentTerm' => 'Prøv en anden søgning',
|
||||
'search.searchYourMedia' => 'Søg i dine medier',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsDa {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy kunne ikke kontrollere, hvilke filer dette vil fjerne, så det kan slette mere end det ovennævnte emne. Annuller og prøv igen, eller slet alligevel.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Din server leverede ikke filoplysninger for dette emne, så Plezy kan ikke kontrollere, hvilke filer dette vil fjerne. Det kan slette mere end det ovennævnte emne.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Mediet blev slettet',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Mediet kunne ikke slettes',
|
||||
'mediaMenu.rate' => 'Bedøm',
|
||||
'mediaMenu.playFromBeginning' => 'Afspil fra begyndelsen',
|
||||
'mediaMenu.playVersion' => 'Afspil version...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Bedøm',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Favorit',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsDa {
|
||||
'explore.watchlistNoMatch' => 'Kunne ikke knytte dette element til en overvågningsliste',
|
||||
'explore.notInLibrary' => 'Ikke i dit bibliotek',
|
||||
'explore.inTheseLibraries' => 'I disse biblioteker',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Tjekker dit bibliotek...',
|
||||
'explore.emptyTitle' => 'Der er ikke noget her endnu',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Indholdsrækker fra ${source} vises her, når de har indhold.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Søg i ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Ingen resultater for "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Søg efter film og serier på ${source}.',
|
||||
'explore.searchFailed' => 'Søgningen mislykkedes. Tjek din forbindelse, og prøv igen.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsDa {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Kunne ikke oprette forbindelse til nogen adresse',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Forbindelse mistet efter ${attempts} forsøg',
|
||||
'companionRemote.errors.connectionLost' => 'Forbindelse mistet',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Forbindelsen blev lukket før godkendelsen',
|
||||
'videoSettings.playbackSpeed' => 'Afspilningshastighed',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Aktiv (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Sove-timer',
|
||||
'videoSettings.audioSync' => 'Lydsynkronisering',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$de extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Spulen & Timing';
|
||||
@override String get behavior => 'Verhalten';
|
||||
@override String get exportDialogTitle => 'Plezy-Einstellungen exportieren';
|
||||
@override String get linuxVideoRenderMode => 'Video-Rendermodus';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automatisch bevorzugt die native Wayland-Ebene (HDR-fähig); Texture erzwingt den SDR-Fallback-Pfad.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatisch';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsDe {
|
||||
'settings.seekAndTiming' => 'Spulen & Timing',
|
||||
'settings.behavior' => 'Verhalten',
|
||||
'settings.exportDialogTitle' => 'Plezy-Einstellungen exportieren',
|
||||
'settings.linuxVideoRenderMode' => 'Video-Rendermodus',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatisch bevorzugt die native Wayland-Ebene (HDR-fähig); Texture erzwingt den SDR-Fallback-Pfad.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatisch',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Filme, Serien und Musik suchen …',
|
||||
'search.tryDifferentTerm' => 'Anderen Suchbegriff versuchen',
|
||||
'search.searchYourMedia' => 'In den eigenen Medien suchen',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsDe {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy konnte nicht prüfen, welche Dateien dadurch entfernt werden. Es könnte also mehr gelöscht werden als das oben genannte Element. Brich ab und versuche es erneut, oder lösche trotzdem.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Dein Server hat für dieses Element keine Dateidetails bereitgestellt, daher kann Plezy nicht prüfen, welche Dateien entfernt werden. Es könnte mehr gelöscht werden als das oben genannte Element.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Medienelement gelöscht',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Medienelement konnte nicht gelöscht werden',
|
||||
'mediaMenu.rate' => 'Bewerten',
|
||||
'mediaMenu.playFromBeginning' => 'Von Anfang an abspielen',
|
||||
'mediaMenu.playVersion' => 'Version abspielen …',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Bewerten',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Favorit',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsDe {
|
||||
'explore.watchlistNoMatch' => 'Dieser Eintrag konnte keiner Watchlist zugeordnet werden',
|
||||
'explore.notInLibrary' => 'Nicht in deiner Mediathek',
|
||||
'explore.inTheseLibraries' => 'In diesen Mediatheken',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Deine Mediathek wird überprüft …',
|
||||
'explore.emptyTitle' => 'Hier ist noch nichts',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Zeilen aus ${source} erscheinen hier, sobald sie Inhalte enthalten.',
|
||||
'explore.searchHint' => ({required Object source}) => '${source} durchsuchen',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Keine Ergebnisse für „${query}“',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Suche nach Filmen und Serien auf ${source}.',
|
||||
'explore.searchFailed' => 'Suche fehlgeschlagen. Prüfe deine Verbindung und versuche es erneut.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsDe {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Keine Verbindung zu einer Adresse möglich',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Verbindung nach ${attempts} Versuchen verloren',
|
||||
'companionRemote.errors.connectionLost' => 'Verbindung verloren',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Die Verbindung wurde vor der Authentifizierung geschlossen',
|
||||
'videoSettings.playbackSpeed' => 'Wiedergabegeschwindigkeit',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Aktiv (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Schlaftimer',
|
||||
'videoSettings.audioSync' => 'Audio-Synchronisation',
|
||||
|
||||
@@ -1100,6 +1100,18 @@ class Translations$settings$en {
|
||||
|
||||
/// en: 'Export Plezy settings'
|
||||
String get exportDialogTitle => 'Export Plezy settings';
|
||||
|
||||
/// en: 'Video rendering mode'
|
||||
String get linuxVideoRenderMode => 'Video rendering mode';
|
||||
|
||||
/// en: 'Automatic prefers the native Wayland plane (HDR-capable); Texture forces the SDR fallback path.'
|
||||
String get linuxVideoRenderModeDescription => 'Automatic prefers the native Wayland plane (HDR-capable); Texture forces the SDR fallback path.';
|
||||
|
||||
/// en: 'Automatic'
|
||||
String get linuxVideoRenderModeAuto => 'Automatic';
|
||||
|
||||
/// en: 'Texture (SDR)'
|
||||
String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -6712,6 +6724,10 @@ extension on Translations {
|
||||
'settings.seekAndTiming' => 'Seek & Timing',
|
||||
'settings.behavior' => 'Behavior',
|
||||
'settings.exportDialogTitle' => 'Export Plezy settings',
|
||||
'settings.linuxVideoRenderMode' => 'Video rendering mode',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatic prefers the native Wayland plane (HDR-capable); Texture forces the SDR fallback path.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatic',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Search movies, shows, music...',
|
||||
'search.tryDifferentTerm' => 'Try a different search term',
|
||||
'search.searchYourMedia' => 'Search your media',
|
||||
@@ -6891,12 +6907,12 @@ extension on Translations {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy could not check which files this will remove, so it may delete more than the item named above. Cancel and try again, or delete anyway.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Your server did not provide file details for this item, so Plezy cannot check which files this will remove. It may delete more than the item named above.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Media item deleted successfully',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Failed to delete media item',
|
||||
'mediaMenu.rate' => 'Rate',
|
||||
'mediaMenu.playFromBeginning' => 'Play from Beginning',
|
||||
'mediaMenu.playVersion' => 'Play Version...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Rate',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Favorite',
|
||||
@@ -7405,12 +7421,12 @@ extension on Translations {
|
||||
'explore.watchlistNoMatch' => 'Couldn\'t match this item to a watchlist',
|
||||
'explore.notInLibrary' => 'Not in your library',
|
||||
'explore.inTheseLibraries' => 'In these libraries',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Checking your library...',
|
||||
'explore.emptyTitle' => 'Nothing here yet',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Rows from ${source} will appear here once they have content.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Search ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'No results for "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Search for movies and shows on ${source}.',
|
||||
'explore.searchFailed' => 'Search failed. Check your connection and try again.',
|
||||
@@ -7919,12 +7935,12 @@ extension on Translations {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Failed to connect to any address',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Connection lost after ${attempts} attempts',
|
||||
'companionRemote.errors.connectionLost' => 'Connection lost',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'The connection closed before authentication',
|
||||
'videoSettings.playbackSpeed' => 'Playback Speed',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Active (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Sleep Timer',
|
||||
'videoSettings.audioSync' => 'Audio Sync',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$es extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Desplazamiento y tiempos';
|
||||
@override String get behavior => 'Comportamiento';
|
||||
@override String get exportDialogTitle => 'Exportar ajustes de Plezy';
|
||||
@override String get linuxVideoRenderMode => 'Modo de renderizado de vídeo';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automático prefiere el plano Wayland nativo (compatible con HDR); Texture fuerza la ruta de reserva SDR.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automático';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsEs {
|
||||
'settings.seekAndTiming' => 'Desplazamiento y tiempos',
|
||||
'settings.behavior' => 'Comportamiento',
|
||||
'settings.exportDialogTitle' => 'Exportar ajustes de Plezy',
|
||||
'settings.linuxVideoRenderMode' => 'Modo de renderizado de vídeo',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automático prefiere el plano Wayland nativo (compatible con HDR); Texture fuerza la ruta de reserva SDR.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automático',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Buscar películas, series, música...',
|
||||
'search.tryDifferentTerm' => 'Prueba con un término de búsqueda diferente',
|
||||
'search.searchYourMedia' => 'Busca en tu contenido',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsEs {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy no pudo comprobar qué archivos se eliminarán, por lo que podría borrar más de lo indicado arriba. Cancela y vuelve a intentarlo, o elimina de todos modos.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Tu servidor no proporcionó los detalles del archivo de este elemento, por lo que Plezy no puede comprobar qué archivos se eliminarán. Podría borrar más de lo indicado arriba.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimedia eliminado con éxito',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Error al eliminar el elemento multimedia',
|
||||
'mediaMenu.rate' => 'Calificar',
|
||||
'mediaMenu.playFromBeginning' => 'Reproducir desde el inicio',
|
||||
'mediaMenu.playVersion' => 'Reproducir versión...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Calificar',
|
||||
'rateSheet.server' => 'Servidor',
|
||||
'rateSheet.favorite' => 'Favorito',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsEs {
|
||||
'explore.watchlistNoMatch' => 'No se pudo asociar este elemento con una lista de seguimiento',
|
||||
'explore.notInLibrary' => 'No está en tu biblioteca',
|
||||
'explore.inTheseLibraries' => 'En estas bibliotecas',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Comprobando tu biblioteca...',
|
||||
'explore.emptyTitle' => 'Aquí no hay nada todavía',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Las filas de ${source} aparecerán aquí cuando tengan contenido.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Buscar en ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Sin resultados para "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Busca películas y series en ${source}.',
|
||||
'explore.searchFailed' => 'La búsqueda falló. Comprueba tu conexión e inténtalo de nuevo.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsEs {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'No se pudo conectar a ninguna dirección',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Conexión perdida tras ${attempts} intentos',
|
||||
'companionRemote.errors.connectionLost' => 'Conexión perdida',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'La conexión se cerró antes de la autenticación',
|
||||
'videoSettings.playbackSpeed' => 'Velocidad de reproducción',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Activo (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Temporizador de apagado',
|
||||
'videoSettings.audioSync' => 'Sincronización de audio',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$fr extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Déplacement et minutage';
|
||||
@override String get behavior => 'Comportement';
|
||||
@override String get exportDialogTitle => 'Exporter les paramètres de Plezy';
|
||||
@override String get linuxVideoRenderMode => 'Mode de rendu vidéo';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automatique préfère le plan Wayland natif (compatible HDR) ; Texture force le chemin de repli SDR.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatique';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsFr {
|
||||
'settings.seekAndTiming' => 'Déplacement et minutage',
|
||||
'settings.behavior' => 'Comportement',
|
||||
'settings.exportDialogTitle' => 'Exporter les paramètres de Plezy',
|
||||
'settings.linuxVideoRenderMode' => 'Mode de rendu vidéo',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatique préfère le plan Wayland natif (compatible HDR) ; Texture force le chemin de repli SDR.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatique',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Rechercher des films, des séries, de la musique...',
|
||||
'search.tryDifferentTerm' => 'Essayez un autre terme de recherche',
|
||||
'search.searchYourMedia' => 'Rechercher dans vos médias',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsFr {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy n\'a pas pu vérifier quels fichiers seront supprimés ; il risque donc d\'en supprimer plus que l\'élément nommé ci-dessus. Annulez et réessayez, ou supprimez quand même.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Votre serveur n\'a pas fourni les détails du fichier pour cet élément, Plezy ne peut donc pas vérifier quels fichiers seront supprimés. Il risque d\'en supprimer plus que l\'élément nommé ci-dessus.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Élément média supprimé avec succès',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Échec de la suppression de l\'élément média',
|
||||
'mediaMenu.rate' => 'Noter',
|
||||
'mediaMenu.playFromBeginning' => 'Lire depuis le début',
|
||||
'mediaMenu.playVersion' => 'Lire la version...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Noter',
|
||||
'rateSheet.server' => 'Serveur',
|
||||
'rateSheet.favorite' => 'Favori',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsFr {
|
||||
'explore.watchlistNoMatch' => 'Impossible d’associer cet élément à une liste de suivi',
|
||||
'explore.notInLibrary' => 'Absent de votre bibliothèque',
|
||||
'explore.inTheseLibraries' => 'Dans ces bibliothèques',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Vérification de votre bibliothèque...',
|
||||
'explore.emptyTitle' => 'Rien ici pour l\'instant',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Les lignes de ${source} apparaîtront ici dès qu’elles contiendront des éléments.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Rechercher dans ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Aucun résultat pour "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Recherchez des films et des séries sur ${source}.',
|
||||
'explore.searchFailed' => 'Échec de la recherche. Vérifiez votre connexion et réessayez.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsFr {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Impossible de se connecter à une adresse',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Connexion perdue après ${attempts} tentatives',
|
||||
'companionRemote.errors.connectionLost' => 'Connexion perdue',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'La connexion a été fermée avant l’authentification',
|
||||
'videoSettings.playbackSpeed' => 'Vitesse de lecture',
|
||||
'videoSettings.normalSpeed' => 'Normale',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Actif (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Minuterie de mise en veille',
|
||||
'videoSettings.audioSync' => 'Synchronisation audio',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$hu extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Tekerés és időzítés';
|
||||
@override String get behavior => 'Viselkedés';
|
||||
@override String get exportDialogTitle => 'Plezy-beállítások exportálása';
|
||||
@override String get linuxVideoRenderMode => 'Videó megjelenítési mód';
|
||||
@override String get linuxVideoRenderModeDescription => 'Az Automatikus a natív Wayland síkot részesíti előnyben (HDR-képes); a Texture az SDR tartalék utat kényszeríti ki.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatikus';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsHu {
|
||||
'settings.seekAndTiming' => 'Tekerés és időzítés',
|
||||
'settings.behavior' => 'Viselkedés',
|
||||
'settings.exportDialogTitle' => 'Plezy-beállítások exportálása',
|
||||
'settings.linuxVideoRenderMode' => 'Videó megjelenítési mód',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Az Automatikus a natív Wayland síkot részesíti előnyben (HDR-képes); a Texture az SDR tartalék utat kényszeríti ki.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatikus',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Keresés filmek, sorozatok és zenék között...',
|
||||
'search.tryDifferentTerm' => 'Próbálj másik keresési kifejezést',
|
||||
'search.searchYourMedia' => 'Keresés a saját médiatartalmak között',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsHu {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'A Plezy nem tudta ellenőrizni, mely fájlokat távolítja el ez, ezért a fent nevezett elemnél többet is törölhet. Szakítsa meg és próbálja újra, vagy töröljön mindenképp.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'A szerver nem szolgáltatott fájladatokat ehhez az elemhez, ezért a Plezy nem tudja ellenőrizni, mely fájlokat távolít el. A fent nevezett elemnél többet is törölhet.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Médiaelem sikeresen törölve',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Nem sikerült a médiaelem törlése',
|
||||
'mediaMenu.rate' => 'Értékelés',
|
||||
'mediaMenu.playFromBeginning' => 'Lejátszás az elejétől',
|
||||
'mediaMenu.playVersion' => 'Verzió lejátszása...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Értékelés',
|
||||
'rateSheet.server' => 'Szerver',
|
||||
'rateSheet.favorite' => 'Kedvenc',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsHu {
|
||||
'explore.watchlistNoMatch' => 'Nem sikerült ezt az elemet figyelőlistához társítani',
|
||||
'explore.notInLibrary' => 'Nincs a könyvtáradban',
|
||||
'explore.inTheseLibraries' => 'Ezekben a könyvtárakban',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Könyvtár ellenőrzése...',
|
||||
'explore.emptyTitle' => 'Még nincs itt semmi',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'A(z) ${source} forrásból származó sorok itt fognak megjelenni, amint van tartalmuk.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Keresés itt: ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Nincs találat a következőre: "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Filmek és sorozatok keresése a következőn: ${source}.',
|
||||
'explore.searchFailed' => 'A keresés nem sikerült. Ellenőrizd a kapcsolatot és próbáld újra.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsHu {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Egyetlen címhez sem sikerült csatlakozni',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'A kapcsolat megszakadt ${attempts} próbálkozás után',
|
||||
'companionRemote.errors.connectionLost' => 'A kapcsolat megszakadt',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'A kapcsolat a hitelesítés előtt megszakadt',
|
||||
'videoSettings.playbackSpeed' => 'Lejátszási sebesség',
|
||||
'videoSettings.normalSpeed' => 'Normál',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Aktív (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Nagyítás',
|
||||
'videoSettings.sleepTimer' => 'Elalvási időzítő',
|
||||
'videoSettings.audioSync' => 'Hang szinkronizálása',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$it extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Avanzamento e tempi';
|
||||
@override String get behavior => 'Comportamento';
|
||||
@override String get exportDialogTitle => 'Esporta le impostazioni di Plezy';
|
||||
@override String get linuxVideoRenderMode => 'Modalità di rendering video';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automatico preferisce il piano Wayland nativo (compatibile HDR); Texture forza il percorso di riserva SDR.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatico';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsIt {
|
||||
'settings.seekAndTiming' => 'Avanzamento e tempi',
|
||||
'settings.behavior' => 'Comportamento',
|
||||
'settings.exportDialogTitle' => 'Esporta le impostazioni di Plezy',
|
||||
'settings.linuxVideoRenderMode' => 'Modalità di rendering video',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatico preferisce il piano Wayland nativo (compatibile HDR); Texture forza il percorso di riserva SDR.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatico',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Cerca film, serie TV e musica...',
|
||||
'search.tryDifferentTerm' => 'Prova altri termini di ricerca',
|
||||
'search.searchYourMedia' => 'Cerca nei tuoi media',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsIt {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy non ha potuto verificare quali file rimuoverà, quindi potrebbe eliminare più di quanto indicato sopra. Annulla e riprova, oppure elimina comunque.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Il tuo server non ha fornito i dettagli del file per questo elemento, quindi Plezy non può verificare quali file rimuoverà. Potrebbe eliminare più di quanto indicato sopra.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimediale eliminato correttamente',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Impossibile eliminare l\'elemento multimediale',
|
||||
'mediaMenu.rate' => 'Valuta',
|
||||
'mediaMenu.playFromBeginning' => 'Riproduci dall\'inizio',
|
||||
'mediaMenu.playVersion' => 'Riproduci versione...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Valuta',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Preferito',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsIt {
|
||||
'explore.watchlistNoMatch' => 'Impossibile associare questo elemento a una lista titoli',
|
||||
'explore.notInLibrary' => 'Non è nella tua libreria',
|
||||
'explore.inTheseLibraries' => 'In queste librerie',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Ricerca nella tua libreria...',
|
||||
'explore.emptyTitle' => 'Ancora niente qui',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Le sezioni di ${source} appariranno qui quando saranno disponibili dei contenuti.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Cerca su ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Nessun risultato per "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Cerca film e serie TV su ${source}.',
|
||||
'explore.searchFailed' => 'Ricerca fallita. Controlla la connessione e riprova.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsIt {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Impossibile connettersi a qualsiasi indirizzo',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Connessione persa dopo ${attempts} tentativi',
|
||||
'companionRemote.errors.connectionLost' => 'Connessione persa',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'La connessione si è chiusa prima dell\'autenticazione',
|
||||
'videoSettings.playbackSpeed' => 'Velocità di riproduzione',
|
||||
'videoSettings.normalSpeed' => 'Normale',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Attivo (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Timer di spegnimento',
|
||||
'videoSettings.audioSync' => 'Sincronizzazione audio',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$ja extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'シークとタイミング';
|
||||
@override String get behavior => '動作';
|
||||
@override String get exportDialogTitle => 'Plezyの設定をエクスポート';
|
||||
@override String get linuxVideoRenderMode => '動画レンダリングモード';
|
||||
@override String get linuxVideoRenderModeDescription => '自動はネイティブの Wayland プレーン(HDR 対応)を優先し、Texture は SDR フォールバック経路を強制します。';
|
||||
@override String get linuxVideoRenderModeAuto => '自動';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture(SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3009,6 +3013,10 @@ extension on TranslationsJa {
|
||||
'settings.seekAndTiming' => 'シークとタイミング',
|
||||
'settings.behavior' => '動作',
|
||||
'settings.exportDialogTitle' => 'Plezyの設定をエクスポート',
|
||||
'settings.linuxVideoRenderMode' => '動画レンダリングモード',
|
||||
'settings.linuxVideoRenderModeDescription' => '自動はネイティブの Wayland プレーン(HDR 対応)を優先し、Texture は SDR フォールバック経路を強制します。',
|
||||
'settings.linuxVideoRenderModeAuto' => '自動',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture(SDR)',
|
||||
'search.hint' => '映画、番組、音楽を検索…',
|
||||
'search.tryDifferentTerm' => '別の検索語をお試しください',
|
||||
'search.searchYourMedia' => 'メディアを検索',
|
||||
@@ -3188,12 +3196,12 @@ extension on TranslationsJa {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezyは削除対象のファイルを確認できなかったため、上記のアイテム以上に削除される可能性があります。キャンセルして再試行するか、それでも削除してください。',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'サーバーがこのアイテムのファイル情報を提供しなかったため、Plezyは削除対象のファイルを確認できません。上記のアイテム以上に削除される可能性があります。',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'メディアアイテムを正常に削除しました',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'メディアアイテムの削除に失敗しました',
|
||||
'mediaMenu.rate' => '評価',
|
||||
'mediaMenu.playFromBeginning' => '最初から再生',
|
||||
'mediaMenu.playVersion' => 'バージョンを選んで再生…',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => '評価',
|
||||
'rateSheet.server' => 'サーバー',
|
||||
'rateSheet.favorite' => 'お気に入り',
|
||||
@@ -3702,12 +3710,12 @@ extension on TranslationsJa {
|
||||
'explore.watchlistNoMatch' => 'このアイテムに一致するウォッチリスト項目が見つかりませんでした',
|
||||
'explore.notInLibrary' => 'ライブラリにありません',
|
||||
'explore.inTheseLibraries' => 'これらのライブラリにあります',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'ライブラリを確認中…',
|
||||
'explore.emptyTitle' => 'まだ何もありません',
|
||||
'explore.emptyMessage' => ({required Object source}) => '${source}にコンテンツが追加されると、ここに表示されます。',
|
||||
'explore.searchHint' => ({required Object source}) => '${source}を検索',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => '「${query}」の結果が見つかりません',
|
||||
'explore.searchPrompt' => ({required Object source}) => '${source}で映画やテレビ番組を検索します。',
|
||||
'explore.searchFailed' => '検索に失敗しました。接続を確認してもう一度お試しください。',
|
||||
@@ -4216,12 +4224,12 @@ extension on TranslationsJa {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'どのアドレスにも接続できませんでした',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => '${attempts}回試行後に接続が切断されました',
|
||||
'companionRemote.errors.connectionLost' => '接続が切断されました',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => '認証前に接続が切断されました',
|
||||
'videoSettings.playbackSpeed' => '再生速度',
|
||||
'videoSettings.normalSpeed' => '標準',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => '作動中(${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'ズーム',
|
||||
'videoSettings.sleepTimer' => 'スリープタイマー',
|
||||
'videoSettings.audioSync' => '音声同期',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$kk extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Айналдыру және уақытты реттеу';
|
||||
@override String get behavior => 'Мінез-құлық';
|
||||
@override String get exportDialogTitle => 'Plezy параметрлерін экспорттау';
|
||||
@override String get linuxVideoRenderMode => 'Бейнені көрсету режимі';
|
||||
@override String get linuxVideoRenderModeDescription => 'Автоматты режим жергілікті Wayland жазықтығын қалайды (HDR қолдайды); Texture SDR резервтік жолды мәжбүрлейді.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Автоматты';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsKk {
|
||||
'settings.seekAndTiming' => 'Айналдыру және уақытты реттеу',
|
||||
'settings.behavior' => 'Мінез-құлық',
|
||||
'settings.exportDialogTitle' => 'Plezy параметрлерін экспорттау',
|
||||
'settings.linuxVideoRenderMode' => 'Бейнені көрсету режимі',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Автоматты режим жергілікті Wayland жазықтығын қалайды (HDR қолдайды); Texture SDR резервтік жолды мәжбүрлейді.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Автоматты',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Фильмдер, сериалдар, музыка іздеу...',
|
||||
'search.tryDifferentTerm' => 'Басқа іздеу сөзін байқап көріңіз',
|
||||
'search.searchYourMedia' => 'Медиафайлдардан іздеу',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsKk {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy қандай файлдар өшірілетінін тексере алмады, сондықтан ол жоғарыда аталған элементтен артық өшіруі мүмкін. Бас тартып, қайталап көріңіз немесе бәрібір өшіріңіз.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Серверіңіз бұл элемент үшін файл мәліметтерін ұсынбады, сондықтан Plezy қандай файлдар өшірілетінін тексере алмайды. Ол жоғарыда аталған элементтен артық өшіруі мүмкін.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Медиа элементі сәтті өшірілді',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Медиа элементін өшіру мүмкін болмады',
|
||||
'mediaMenu.rate' => 'Бағалау',
|
||||
'mediaMenu.playFromBeginning' => 'Басынан бастап ойнату',
|
||||
'mediaMenu.playVersion' => 'Нұсқаны ойнату...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Бағалау',
|
||||
'rateSheet.server' => 'Сервер',
|
||||
'rateSheet.favorite' => 'Таңдаулы',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsKk {
|
||||
'explore.watchlistNoMatch' => 'Бұл элементті көру тізімімен сәйкестендіру мүмкін болмады',
|
||||
'explore.notInLibrary' => 'Кітапханаңызда жоқ',
|
||||
'explore.inTheseLibraries' => 'Осы кітапханаларда бар',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Кітапхана тексерілуде...',
|
||||
'explore.emptyTitle' => 'Әлі де мұнда ештеңе жоқ',
|
||||
'explore.emptyMessage' => ({required Object source}) => '${source} дереккөзінен алынған қатарлар мұнда көрінеді.',
|
||||
'explore.searchHint' => ({required Object source}) => '${source} ішінен іздеу',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => '"${query}" бойынша нәтиже табылмады',
|
||||
'explore.searchPrompt' => ({required Object source}) => '${source} арқылы фильмдер мен сериалдарды іздеңіз.',
|
||||
'explore.searchFailed' => 'Іздеу қатесі. Қосылымды тексеріңіз.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsKk {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Ешбір мекенжайға қосылу мүмкін болмады',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => '${attempts} әрекеттен кейін байланыс үзілді',
|
||||
'companionRemote.errors.connectionLost' => 'Байланыс үзілді',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Аутентификациядан бұрын байланыс жабылды',
|
||||
'videoSettings.playbackSpeed' => 'Ойнату жылдамдығы',
|
||||
'videoSettings.normalSpeed' => 'Қалыпты',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Белсенді (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Масштаб',
|
||||
'videoSettings.sleepTimer' => 'Ұйқы таймері',
|
||||
'videoSettings.audioSync' => 'Аудио синхрондау',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$ko extends Translations$settings$en {
|
||||
@override String get seekAndTiming => '탐색 및 타이밍';
|
||||
@override String get behavior => '동작';
|
||||
@override String get exportDialogTitle => 'Plezy 설정 내보내기';
|
||||
@override String get linuxVideoRenderMode => '동영상 렌더링 모드';
|
||||
@override String get linuxVideoRenderModeDescription => '자동은 네이티브 Wayland 평면(HDR 지원)을 선호하고, Texture는 SDR 대체 경로를 강제합니다.';
|
||||
@override String get linuxVideoRenderModeAuto => '자동';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture(SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3009,6 +3013,10 @@ extension on TranslationsKo {
|
||||
'settings.seekAndTiming' => '탐색 및 타이밍',
|
||||
'settings.behavior' => '동작',
|
||||
'settings.exportDialogTitle' => 'Plezy 설정 내보내기',
|
||||
'settings.linuxVideoRenderMode' => '동영상 렌더링 모드',
|
||||
'settings.linuxVideoRenderModeDescription' => '자동은 네이티브 Wayland 평면(HDR 지원)을 선호하고, Texture는 SDR 대체 경로를 강제합니다.',
|
||||
'settings.linuxVideoRenderModeAuto' => '자동',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture(SDR)',
|
||||
'search.hint' => '영화, 시리즈, 음악 등을 검색하세요...',
|
||||
'search.tryDifferentTerm' => '다른 검색어를 시도해 보세요',
|
||||
'search.searchYourMedia' => '미디어 검색',
|
||||
@@ -3188,12 +3196,12 @@ extension on TranslationsKo {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy가 어떤 파일이 삭제될지 확인하지 못해 위 항목보다 더 많은 파일이 삭제될 수 있습니다. 취소하고 다시 시도하거나 그래도 삭제하세요.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => '서버가 이 항목의 파일 정보를 제공하지 않아 Plezy가 어떤 파일이 삭제될지 확인할 수 없습니다. 위 항목보다 더 많은 파일이 삭제될 수 있습니다.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => '미디어 항목이 성공적으로 삭제되었습니다',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => '미디어 항목 삭제 실패',
|
||||
'mediaMenu.rate' => '평가',
|
||||
'mediaMenu.playFromBeginning' => '처음부터 재생',
|
||||
'mediaMenu.playVersion' => '버전 재생...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => '평가',
|
||||
'rateSheet.server' => '서버',
|
||||
'rateSheet.favorite' => '즐겨찾기',
|
||||
@@ -3702,12 +3710,12 @@ extension on TranslationsKo {
|
||||
'explore.watchlistNoMatch' => '이 항목을 관심 목록과 연결할 수 없습니다',
|
||||
'explore.notInLibrary' => '라이브러리에 없음',
|
||||
'explore.inTheseLibraries' => '이 라이브러리에 있음',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => '라이브러리 확인 중...',
|
||||
'explore.emptyTitle' => '아직 아무것도 없습니다',
|
||||
'explore.emptyMessage' => ({required Object source}) => '${source}에 콘텐츠가 추가되면 여기에 표시됩니다.',
|
||||
'explore.searchHint' => ({required Object source}) => '${source}에서 검색',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => '「${query}」에 대한 결과가 없습니다',
|
||||
'explore.searchPrompt' => ({required Object source}) => '${source}에서 영화와 TV 프로그램을 검색하세요.',
|
||||
'explore.searchFailed' => '검색에 실패했습니다. 연결을 확인하고 다시 시도하세요.',
|
||||
@@ -4216,12 +4224,12 @@ extension on TranslationsKo {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => '어떤 주소에도 연결하지 못했습니다',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => '${attempts}회 시도 후 연결이 끊어졌습니다',
|
||||
'companionRemote.errors.connectionLost' => '연결이 끊어졌습니다',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => '인증 전에 연결이 종료되었습니다',
|
||||
'videoSettings.playbackSpeed' => '재생 속도',
|
||||
'videoSettings.normalSpeed' => '보통',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => '활성 (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => '확대/축소',
|
||||
'videoSettings.sleepTimer' => '취침 타이머',
|
||||
'videoSettings.audioSync' => '오디오 동기화',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$nb extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Spoling og tidsinnstillinger';
|
||||
@override String get behavior => 'Oppførsel';
|
||||
@override String get exportDialogTitle => 'Eksporter Plezy-innstillinger';
|
||||
@override String get linuxVideoRenderMode => 'Video-gjengivelsesmodus';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automatisk foretrekker det native Wayland-planet (HDR-kompatibelt); Texture tvinger SDR-fallback-stien.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatisk';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsNb {
|
||||
'settings.seekAndTiming' => 'Spoling og tidsinnstillinger',
|
||||
'settings.behavior' => 'Oppførsel',
|
||||
'settings.exportDialogTitle' => 'Eksporter Plezy-innstillinger',
|
||||
'settings.linuxVideoRenderMode' => 'Video-gjengivelsesmodus',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatisk foretrekker det native Wayland-planet (HDR-kompatibelt); Texture tvinger SDR-fallback-stien.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatisk',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Søk i filmer, serier, musikk...',
|
||||
'search.tryDifferentTerm' => 'Prøv et annet søkeord',
|
||||
'search.searchYourMedia' => 'Søk i mediene dine',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsNb {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy kunne ikke sjekke hvilke filer dette vil fjerne, så det kan slette mer enn elementet som er nevnt ovenfor. Avbryt og prøv igjen, eller slett likevel.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Serveren din ga ikke fildetaljer for dette elementet, så Plezy kan ikke sjekke hvilke filer dette vil fjerne. Det kan slette mer enn elementet som er nevnt ovenfor.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Medieelement slettet',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Kunne ikke slette medieelement',
|
||||
'mediaMenu.rate' => 'Vurder',
|
||||
'mediaMenu.playFromBeginning' => 'Spill fra begynnelsen',
|
||||
'mediaMenu.playVersion' => 'Spill av versjon...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Vurder',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Favoritt',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsNb {
|
||||
'explore.watchlistNoMatch' => 'Kunne ikke koble dette elementet til en overvåkningsliste',
|
||||
'explore.notInLibrary' => 'Ikke i biblioteket ditt',
|
||||
'explore.inTheseLibraries' => 'I disse bibliotekene',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Sjekker biblioteket ditt...',
|
||||
'explore.emptyTitle' => 'Ingenting her ennå',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Rader fra ${source} vises her når de har innhold.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Søk i ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Ingen treff for "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Søk etter filmer og serier på ${source}.',
|
||||
'explore.searchFailed' => 'Søk mislyktes. Sjekk tilkoblingen og prøv igjen.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsNb {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Kunne ikke koble til noen adresse',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Tilkobling mistet etter ${attempts} forsøk',
|
||||
'companionRemote.errors.connectionLost' => 'Tilkobling mistet',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Tilkoblingen ble lukket før autentisering',
|
||||
'videoSettings.playbackSpeed' => 'Avspillingshastighet',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Aktiv (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Innsovningstimer',
|
||||
'videoSettings.audioSync' => 'Lydsynkronisering',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$nl extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Spoelen en timing';
|
||||
@override String get behavior => 'Gedrag';
|
||||
@override String get exportDialogTitle => 'Plezy-instellingen exporteren';
|
||||
@override String get linuxVideoRenderMode => 'Videoweergavemodus';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automatisch geeft de voorkeur aan het native Wayland-vlak (HDR-compatibel); Texture dwingt het SDR-reservepad af.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatisch';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsNl {
|
||||
'settings.seekAndTiming' => 'Spoelen en timing',
|
||||
'settings.behavior' => 'Gedrag',
|
||||
'settings.exportDialogTitle' => 'Plezy-instellingen exporteren',
|
||||
'settings.linuxVideoRenderMode' => 'Videoweergavemodus',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatisch geeft de voorkeur aan het native Wayland-vlak (HDR-compatibel); Texture dwingt het SDR-reservepad af.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatisch',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Zoek films, series, muziek...',
|
||||
'search.tryDifferentTerm' => 'Probeer een andere zoekterm',
|
||||
'search.searchYourMedia' => 'Zoek in je media',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsNl {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy kon niet controleren welke bestanden hiermee worden verwijderd, dus het kan meer verwijderen dan het hierboven genoemde item. Annuleer en probeer het opnieuw, of verwijder toch.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Je server heeft geen bestandsgegevens voor dit item verstrekt, dus Plezy kan niet controleren welke bestanden hiermee worden verwijderd. Het kan meer verwijderen dan het hierboven genoemde item.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Media-item succesvol verwijderd',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Verwijderen van media-item mislukt',
|
||||
'mediaMenu.rate' => 'Beoordelen',
|
||||
'mediaMenu.playFromBeginning' => 'Afspelen vanaf het begin',
|
||||
'mediaMenu.playVersion' => 'Versie afspelen...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Beoordelen',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Favoriet',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsNl {
|
||||
'explore.watchlistNoMatch' => 'Kon dit item niet aan een kijklijst koppelen',
|
||||
'explore.notInLibrary' => 'Niet in je bibliotheek',
|
||||
'explore.inTheseLibraries' => 'In deze bibliotheken',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Je bibliotheek controleren...',
|
||||
'explore.emptyTitle' => 'Hier is nog niets',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Rijen van ${source} verschijnen hier zodra ze inhoud hebben.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Zoeken in ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Geen resultaten voor "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Zoek naar films en series op ${source}.',
|
||||
'explore.searchFailed' => 'Zoeken mislukt. Controleer je verbinding en probeer opnieuw.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsNl {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Er kon met geen enkel adres verbinding worden gemaakt',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Verbinding verbroken na ${attempts} pogingen',
|
||||
'companionRemote.errors.connectionLost' => 'Verbinding verloren',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'De verbinding is vóór de authenticatie gesloten',
|
||||
'videoSettings.playbackSpeed' => 'Afspeelsnelheid',
|
||||
'videoSettings.normalSpeed' => 'Normaal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Actief (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Slaaptimer',
|
||||
'videoSettings.audioSync' => 'Audiosynchronisatie',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$pl extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Przewijanie i czas';
|
||||
@override String get behavior => 'Zachowanie';
|
||||
@override String get exportDialogTitle => 'Eksport ustawień Plezy';
|
||||
@override String get linuxVideoRenderMode => 'Tryb renderowania wideo';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automatyczny preferuje natywną płaszczyznę Wayland (zgodną z HDR); Texture wymusza ścieżkę zapasową SDR.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatyczny';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3039,6 +3043,10 @@ extension on TranslationsPl {
|
||||
'settings.seekAndTiming' => 'Przewijanie i czas',
|
||||
'settings.behavior' => 'Zachowanie',
|
||||
'settings.exportDialogTitle' => 'Eksport ustawień Plezy',
|
||||
'settings.linuxVideoRenderMode' => 'Tryb renderowania wideo',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatyczny preferuje natywną płaszczyznę Wayland (zgodną z HDR); Texture wymusza ścieżkę zapasową SDR.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatyczny',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Szukaj filmów, seriali, muzyki...',
|
||||
'search.tryDifferentTerm' => 'Spróbuj innego wyszukiwania',
|
||||
'search.searchYourMedia' => 'Przeszukaj swoje media',
|
||||
@@ -3218,12 +3226,12 @@ extension on TranslationsPl {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy nie mogło sprawdzić, które pliki zostaną usunięte, więc może usunąć więcej niż element wymieniony powyżej. Anuluj i spróbuj ponownie albo usuń mimo to.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Twój serwer nie podał informacji o plikach tego elementu, więc Plezy nie może sprawdzić, które pliki zostaną usunięte. Może usunąć więcej niż element wymieniony powyżej.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Usunięto element multimedialny',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Nie udało się usunąć elementu multimedialnego',
|
||||
'mediaMenu.rate' => 'Oceń',
|
||||
'mediaMenu.playFromBeginning' => 'Odtwórz od początku',
|
||||
'mediaMenu.playVersion' => 'Odtwórz wersję...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Oceń',
|
||||
'rateSheet.server' => 'Serwer',
|
||||
'rateSheet.favorite' => 'Dodaj do ulubionych',
|
||||
@@ -3732,12 +3740,12 @@ extension on TranslationsPl {
|
||||
'explore.watchlistNoMatch' => 'Nie udało się dopasować tej pozycji do listy do obejrzenia',
|
||||
'explore.notInLibrary' => 'Nie ma tego w Twojej bibliotece',
|
||||
'explore.inTheseLibraries' => 'W tych bibliotekach',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Sprawdzanie Twojej biblioteki...',
|
||||
'explore.emptyTitle' => 'Jeszcze nic tu nie ma',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Wiersze z ${source} pojawią się tutaj, gdy będą zawierać treści.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Szukaj w ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Brak wyników dla "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Szukaj filmów i seriali w ${source}.',
|
||||
'explore.searchFailed' => 'Wyszukiwanie nie powiodło się. Sprawdź połączenie i spróbuj ponownie.',
|
||||
@@ -4246,12 +4254,12 @@ extension on TranslationsPl {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Nie udało się połączyć z żadnym adresem',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Połączenie utracone po ${attempts} próbach',
|
||||
'companionRemote.errors.connectionLost' => 'Połączenie utracone',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Połączenie zostało zamknięte przed uwierzytelnieniem',
|
||||
'videoSettings.playbackSpeed' => 'Prędkość odtwarzania',
|
||||
'videoSettings.normalSpeed' => 'Normalna',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Aktywny (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Powiększenie',
|
||||
'videoSettings.sleepTimer' => 'Wyłącznik czasowy',
|
||||
'videoSettings.audioSync' => 'Synchronizacja audio',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$pt extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Busca e tempo';
|
||||
@override String get behavior => 'Comportamento';
|
||||
@override String get exportDialogTitle => 'Exportar configurações do Plezy';
|
||||
@override String get linuxVideoRenderMode => 'Modo de renderização de vídeo';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automático prefere o plano Wayland nativo (compatível com HDR); Texture força o caminho de reserva SDR.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automático';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsPt {
|
||||
'settings.seekAndTiming' => 'Busca e tempo',
|
||||
'settings.behavior' => 'Comportamento',
|
||||
'settings.exportDialogTitle' => 'Exportar configurações do Plezy',
|
||||
'settings.linuxVideoRenderMode' => 'Modo de renderização de vídeo',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automático prefere o plano Wayland nativo (compatível com HDR); Texture força o caminho de reserva SDR.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automático',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Buscar filmes, séries, músicas...',
|
||||
'search.tryDifferentTerm' => 'Tente um termo de busca diferente',
|
||||
'search.searchYourMedia' => 'Buscar suas mídias',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsPt {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'O Plezy não conseguiu verificar quais arquivos serão removidos, então pode excluir mais do que o item acima. Cancele e tente novamente, ou exclua mesmo assim.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Seu servidor não forneceu detalhes do arquivo para este item, então o Plezy não pode verificar quais arquivos serão removidos. Pode excluir mais do que o item acima.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Item de mídia excluído com sucesso',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Falha ao excluir item de mídia',
|
||||
'mediaMenu.rate' => 'Avaliar',
|
||||
'mediaMenu.playFromBeginning' => 'Reproduzir do início',
|
||||
'mediaMenu.playVersion' => 'Reproduzir versão...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Avaliar',
|
||||
'rateSheet.server' => 'Servidor',
|
||||
'rateSheet.favorite' => 'Favorito',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsPt {
|
||||
'explore.watchlistNoMatch' => 'Não foi possível associar este item a uma lista de interesses',
|
||||
'explore.notInLibrary' => 'Não está na sua biblioteca',
|
||||
'explore.inTheseLibraries' => 'Nestas bibliotecas',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Verificando sua biblioteca...',
|
||||
'explore.emptyTitle' => 'Ainda não há nada aqui',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'As linhas de ${source} aparecerão aqui quando tiverem conteúdo.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Buscar em ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Nenhum resultado para "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Busque filmes e séries em ${source}.',
|
||||
'explore.searchFailed' => 'Falha na busca. Verifique sua conexão e tente novamente.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsPt {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Falha ao conectar a qualquer endereço',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Conexão perdida após ${attempts} tentativas',
|
||||
'companionRemote.errors.connectionLost' => 'Conexão perdida',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'A conexão foi encerrada antes da autenticação',
|
||||
'videoSettings.playbackSpeed' => 'Velocidade de Reprodução',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Ativo (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Temporizador de suspensão',
|
||||
'videoSettings.audioSync' => 'Sincronia de áudio',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$ru extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Перемотка и время';
|
||||
@override String get behavior => 'Поведение';
|
||||
@override String get exportDialogTitle => 'Экспорт настроек Plezy';
|
||||
@override String get linuxVideoRenderMode => 'Режим рендеринга видео';
|
||||
@override String get linuxVideoRenderModeDescription => 'Автоматический режим предпочитает нативную плоскость Wayland (поддержка HDR); Texture принудительно использует запасной путь SDR.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Автоматически';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3039,6 +3043,10 @@ extension on TranslationsRu {
|
||||
'settings.seekAndTiming' => 'Перемотка и время',
|
||||
'settings.behavior' => 'Поведение',
|
||||
'settings.exportDialogTitle' => 'Экспорт настроек Plezy',
|
||||
'settings.linuxVideoRenderMode' => 'Режим рендеринга видео',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Автоматический режим предпочитает нативную плоскость Wayland (поддержка HDR); Texture принудительно использует запасной путь SDR.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Автоматически',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Поиск фильмов, сериалов, музыки...',
|
||||
'search.tryDifferentTerm' => 'Попробуйте другой запрос',
|
||||
'search.searchYourMedia' => 'Поиск в вашей медиатеке',
|
||||
@@ -3218,12 +3226,12 @@ extension on TranslationsRu {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy не смог проверить, какие файлы будут удалены, поэтому может быть удалено больше, чем указанный выше элемент. Отмените и попробуйте снова либо удалите в любом случае.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Ваш сервер не предоставил сведения о файлах для этого элемента, поэтому Plezy не может проверить, какие файлы будут удалены. Может быть удалено больше, чем указанный выше элемент.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Медиаэлемент успешно удалён',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Не удалось удалить медиаэлемент',
|
||||
'mediaMenu.rate' => 'Оценить',
|
||||
'mediaMenu.playFromBeginning' => 'Воспроизвести сначала',
|
||||
'mediaMenu.playVersion' => 'Воспроизвести версию...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Оценить',
|
||||
'rateSheet.server' => 'Сервер',
|
||||
'rateSheet.favorite' => 'Добавить в избранное',
|
||||
@@ -3732,12 +3740,12 @@ extension on TranslationsRu {
|
||||
'explore.watchlistNoMatch' => 'Не удалось сопоставить этот элемент со списком просмотра',
|
||||
'explore.notInLibrary' => 'Нет в вашей библиотеке',
|
||||
'explore.inTheseLibraries' => 'В этих библиотеках',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Проверка вашей библиотеки...',
|
||||
'explore.emptyTitle' => 'Здесь пока ничего нет',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Разделы из ${source} появятся здесь, когда в них появится контент.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Поиск в ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Нет результатов по запросу "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Ищите фильмы и сериалы в ${source}.',
|
||||
'explore.searchFailed' => 'Ошибка поиска. Проверьте подключение и повторите попытку.',
|
||||
@@ -4246,12 +4254,12 @@ extension on TranslationsRu {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Не удалось подключиться ни к одному адресу',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Соединение потеряно после ${attempts} попыток',
|
||||
'companionRemote.errors.connectionLost' => 'Соединение потеряно',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Соединение было закрыто до аутентификации',
|
||||
'videoSettings.playbackSpeed' => 'Скорость воспроизведения',
|
||||
'videoSettings.normalSpeed' => 'Обычная',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Активен (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Масштаб',
|
||||
'videoSettings.sleepTimer' => 'Таймер сна',
|
||||
'videoSettings.audioSync' => 'Синхронизация аудио',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$sv extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Spolning och tidsinställningar';
|
||||
@override String get behavior => 'Beteende';
|
||||
@override String get exportDialogTitle => 'Exportera Plezy-inställningar';
|
||||
@override String get linuxVideoRenderMode => 'Videorenderingsläge';
|
||||
@override String get linuxVideoRenderModeDescription => 'Automatiskt föredrar det inbyggda Wayland-planet (HDR-kompatibelt); Texture tvingar SDR-reservvägen.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Automatiskt';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsSv {
|
||||
'settings.seekAndTiming' => 'Spolning och tidsinställningar',
|
||||
'settings.behavior' => 'Beteende',
|
||||
'settings.exportDialogTitle' => 'Exportera Plezy-inställningar',
|
||||
'settings.linuxVideoRenderMode' => 'Videorenderingsläge',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Automatiskt föredrar det inbyggda Wayland-planet (HDR-kompatibelt); Texture tvingar SDR-reservvägen.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Automatiskt',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Sök filmer, serier, musik...',
|
||||
'search.tryDifferentTerm' => 'Prova en annan sökterm',
|
||||
'search.searchYourMedia' => 'Sök i dina media',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsSv {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy kunde inte kontrollera vilka filer detta tar bort, så det kan ta bort mer än objektet ovan. Avbryt och försök igen, eller ta bort ändå.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Din server tillhandahöll inte filinformation för detta objekt, så Plezy kan inte kontrollera vilka filer detta tar bort. Det kan ta bort mer än objektet ovan.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Medieobjektet har tagits bort',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Det gick inte att ta bort medieobjektet',
|
||||
'mediaMenu.rate' => 'Betygsätt',
|
||||
'mediaMenu.playFromBeginning' => 'Spela från början',
|
||||
'mediaMenu.playVersion' => 'Spela version...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Betygsätt',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Favorit',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsSv {
|
||||
'explore.watchlistNoMatch' => 'Det gick inte att matcha det här objektet mot en bevakningslista',
|
||||
'explore.notInLibrary' => 'Finns inte i ditt bibliotek',
|
||||
'explore.inTheseLibraries' => 'I dessa bibliotek',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Kontrollerar ditt bibliotek...',
|
||||
'explore.emptyTitle' => 'Inget här ännu',
|
||||
'explore.emptyMessage' => ({required Object source}) => 'Rader från ${source} visas här när de har innehåll.',
|
||||
'explore.searchHint' => ({required Object source}) => 'Sök i ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => 'Inga resultat för "${query}"',
|
||||
'explore.searchPrompt' => ({required Object source}) => 'Sök efter filmer och serier på ${source}.',
|
||||
'explore.searchFailed' => 'Sökningen misslyckades. Kontrollera din anslutning och försök igen.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsSv {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Det gick inte att ansluta till någon adress',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => 'Anslutningen bröts efter ${attempts} försök',
|
||||
'companionRemote.errors.connectionLost' => 'Anslutningen bröts',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Anslutningen stängdes före autentiseringen',
|
||||
'videoSettings.playbackSpeed' => 'Uppspelningshastighet',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Aktiv (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Zoom',
|
||||
'videoSettings.sleepTimer' => 'Sovtimer',
|
||||
'videoSettings.audioSync' => 'Ljudsynkronisering',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$tr extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Sarılma ve Zamanlama';
|
||||
@override String get behavior => 'Davranış';
|
||||
@override String get exportDialogTitle => 'Plezy ayarlarını dışa aktar';
|
||||
@override String get linuxVideoRenderMode => 'Video işleme modu';
|
||||
@override String get linuxVideoRenderModeDescription => 'Otomatik, yerel Wayland düzlemini tercih eder (HDR destekli); Texture, SDR yedek yolunu zorlar.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Otomatik';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsTr {
|
||||
'settings.seekAndTiming' => 'Sarılma ve Zamanlama',
|
||||
'settings.behavior' => 'Davranış',
|
||||
'settings.exportDialogTitle' => 'Plezy ayarlarını dışa aktar',
|
||||
'settings.linuxVideoRenderMode' => 'Video işleme modu',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Otomatik, yerel Wayland düzlemini tercih eder (HDR destekli); Texture, SDR yedek yolunu zorlar.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Otomatik',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Film, dizi, müzik ara...',
|
||||
'search.tryDifferentTerm' => 'Farklı bir arama terimi deneyin',
|
||||
'search.searchYourMedia' => 'Medyanızda arayın',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsTr {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy bunun hangi dosyaları sileceğini kontrol edemedi, bu yüzden yukarıda adı geçen ögeden daha fazlasını silebilir. İptal edip tekrar deneyin veya yine de silin.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Sunucunuz bu öge için dosya ayrıntıları sağlamadı, bu yüzden Plezy bunun hangi dosyaları sileceğini kontrol edemiyor. Yukarıda adı geçen ögeden daha fazlasını silebilir.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Medya ögesi başarıyla silindi',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Medya ögesi silinemedi',
|
||||
'mediaMenu.rate' => 'Oyla',
|
||||
'mediaMenu.playFromBeginning' => 'Baştan Oynat',
|
||||
'mediaMenu.playVersion' => 'Sürümü Oynat...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Oyla',
|
||||
'rateSheet.server' => 'Sunucu',
|
||||
'rateSheet.favorite' => 'Favori',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsTr {
|
||||
'explore.watchlistNoMatch' => 'Bu öğe bir izleme listesiyle eşleştirilemedi',
|
||||
'explore.notInLibrary' => 'Kitaplığınızda yok',
|
||||
'explore.inTheseLibraries' => 'Bu kitaplıklarda var',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Kitaplığınız kontrol ediliyor...',
|
||||
'explore.emptyTitle' => 'Henüz burada bir şey yok',
|
||||
'explore.emptyMessage' => ({required Object source}) => '${source} kaynağındaki satırlar içerik bulunduğunda burada görünecektir.',
|
||||
'explore.searchHint' => ({required Object source}) => '${source} üzerinde ara',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => '"${query}" için sonuç bulunamadı',
|
||||
'explore.searchPrompt' => ({required Object source}) => '${source} üzerinde film ve dizileri arayın.',
|
||||
'explore.searchFailed' => 'Arama başarısız. Bağlantınızı kontrol edip tekrar deneyin.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsTr {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Hiçbir adrese bağlanılamadı',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => '${attempts} denemeden sonra bağlantı kesildi',
|
||||
'companionRemote.errors.connectionLost' => 'Bağlantı kesildi',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Bağlantı, kimlik doğrulamadan önce kapandı',
|
||||
'videoSettings.playbackSpeed' => 'Oynatma Hızı',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Aktif (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Yakınlaştırma',
|
||||
'videoSettings.sleepTimer' => 'Uyku Zamanlayıcısı',
|
||||
'videoSettings.audioSync' => 'Ses Senkronizasyonu',
|
||||
|
||||
@@ -462,6 +462,10 @@ class _Translations$settings$uz extends Translations$settings$en {
|
||||
@override String get seekAndTiming => 'Oʻtkazish va vaqt sozlamalari';
|
||||
@override String get behavior => 'Xatti-harakat';
|
||||
@override String get exportDialogTitle => 'Plezy sozlamalarini eksport qilish';
|
||||
@override String get linuxVideoRenderMode => 'Video ko\'rsatish rejimi';
|
||||
@override String get linuxVideoRenderModeDescription => 'Avtomatik rejim mahalliy Wayland tekisligini afzal ko\'radi (HDR qo\'llab-quvvatlaydi); Texture SDR zaxira yo\'lini majbur qiladi.';
|
||||
@override String get linuxVideoRenderModeAuto => 'Avtomatik';
|
||||
@override String get linuxVideoRenderModeTexture => 'Texture (SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3019,6 +3023,10 @@ extension on TranslationsUz {
|
||||
'settings.seekAndTiming' => 'Oʻtkazish va vaqt sozlamalari',
|
||||
'settings.behavior' => 'Xatti-harakat',
|
||||
'settings.exportDialogTitle' => 'Plezy sozlamalarini eksport qilish',
|
||||
'settings.linuxVideoRenderMode' => 'Video ko\'rsatish rejimi',
|
||||
'settings.linuxVideoRenderModeDescription' => 'Avtomatik rejim mahalliy Wayland tekisligini afzal ko\'radi (HDR qo\'llab-quvvatlaydi); Texture SDR zaxira yo\'lini majbur qiladi.',
|
||||
'settings.linuxVideoRenderModeAuto' => 'Avtomatik',
|
||||
'settings.linuxVideoRenderModeTexture' => 'Texture (SDR)',
|
||||
'search.hint' => 'Filmlar, seriallar, musiqa qidirish...',
|
||||
'search.tryDifferentTerm' => 'Boshqa qidiruv soʻzini kiriting',
|
||||
'search.searchYourMedia' => 'Medialaringizdan qidiring',
|
||||
@@ -3198,12 +3206,12 @@ extension on TranslationsUz {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy qaysi fayllar olib tashlanishini tekshira olmadi, shuning uchun u yuqorida koʻrsatilgan elementdan koʻproq narsani oʻchirishi mumkin. Bekor qilib qaytadan urining yoki baribir oʻchiring.',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Serveringiz ushbu element uchun fayl tafsilotlarini taqdim etmadi, shuning uchun Plezy qaysi fayllar olib tashlanishini tekshira olmaydi. U yuqorida koʻrsatilgan elementdan koʻproq narsani oʻchirishi mumkin.',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => 'Media elementi muvaffaqiyatli oʻchirildi',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => 'Media elementini oʻchirib boʻlmadi',
|
||||
'mediaMenu.rate' => 'Baho berish',
|
||||
'mediaMenu.playFromBeginning' => 'Boshidan ijro etish',
|
||||
'mediaMenu.playVersion' => 'Versiyani ijro etish...',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => 'Baho berish',
|
||||
'rateSheet.server' => 'Server',
|
||||
'rateSheet.favorite' => 'Tanlangan',
|
||||
@@ -3712,12 +3720,12 @@ extension on TranslationsUz {
|
||||
'explore.watchlistNoMatch' => 'Bu elementni tomosha roʻyxatiga moslab boʻlmadi',
|
||||
'explore.notInLibrary' => 'Kutubxonangizda yoʻq',
|
||||
'explore.inTheseLibraries' => 'Ushbu kutubxonalarda bor',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => 'Kutubxona tekshirilmoqda...',
|
||||
'explore.emptyTitle' => 'Hali bu yerda hech narsa yoʻq',
|
||||
'explore.emptyMessage' => ({required Object source}) => '${source} manbasidan olingan qatorlar bu yerda koʻrinadi.',
|
||||
'explore.searchHint' => ({required Object source}) => '${source} ichidan qidirish',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => '"${query}" boʻyicha natija topilmadi',
|
||||
'explore.searchPrompt' => ({required Object source}) => '${source} orqali filmlar va seriallarni qidiring.',
|
||||
'explore.searchFailed' => 'Qidiruv xatoligi. Ulanishni tekshiring.',
|
||||
@@ -4226,12 +4234,12 @@ extension on TranslationsUz {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => 'Hech bir manzilga ulanib boʻlmadi',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => '${attempts} urinishdan keyin aloqa uzildi',
|
||||
'companionRemote.errors.connectionLost' => 'Aloqa uzildi',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => 'Ulanish autentifikatsiyadan oldin yopildi',
|
||||
'videoSettings.playbackSpeed' => 'Ijro tezligi',
|
||||
'videoSettings.normalSpeed' => 'Normal',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => 'Faol (${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => 'Masshtab',
|
||||
'videoSettings.sleepTimer' => 'Uyqu taymeri',
|
||||
'videoSettings.audioSync' => 'Audio sinxronlash',
|
||||
|
||||
@@ -462,6 +462,10 @@ class Translations$settings$zh extends Translations$settings$en {
|
||||
@override String get seekAndTiming => '跳转与计时';
|
||||
@override String get behavior => '行为';
|
||||
@override String get exportDialogTitle => '导出 Plezy 设置';
|
||||
@override String get linuxVideoRenderMode => '视频渲染模式';
|
||||
@override String get linuxVideoRenderModeDescription => '自动优先使用原生 Wayland 平面(支持 HDR);纹理强制使用 SDR 回退路径。';
|
||||
@override String get linuxVideoRenderModeAuto => '自动';
|
||||
@override String get linuxVideoRenderModeTexture => '纹理(SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3009,6 +3013,10 @@ extension on TranslationsZh {
|
||||
'settings.seekAndTiming' => '跳转与计时',
|
||||
'settings.behavior' => '行为',
|
||||
'settings.exportDialogTitle' => '导出 Plezy 设置',
|
||||
'settings.linuxVideoRenderMode' => '视频渲染模式',
|
||||
'settings.linuxVideoRenderModeDescription' => '自动优先使用原生 Wayland 平面(支持 HDR);纹理强制使用 SDR 回退路径。',
|
||||
'settings.linuxVideoRenderModeAuto' => '自动',
|
||||
'settings.linuxVideoRenderModeTexture' => '纹理(SDR)',
|
||||
'search.hint' => '搜索电影、剧集、音乐…',
|
||||
'search.tryDifferentTerm' => '尝试不同的搜索词',
|
||||
'search.searchYourMedia' => '搜索媒体',
|
||||
@@ -3188,12 +3196,12 @@ extension on TranslationsZh {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy 无法确认此操作会删除哪些文件,因此删除范围可能超出上面提到的项目。请取消后重试,或仍然删除。',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => '你的服务器没有提供此项目的文件详情,因此 Plezy 无法确认此操作会删除哪些文件。删除范围可能超出上面提到的项目。',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => '媒体项已成功删除',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => '删除媒体项失败',
|
||||
'mediaMenu.rate' => '评分',
|
||||
'mediaMenu.playFromBeginning' => '从头播放',
|
||||
'mediaMenu.playVersion' => '播放版本…',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => '评分',
|
||||
'rateSheet.server' => '服务器',
|
||||
'rateSheet.favorite' => '收藏',
|
||||
@@ -3702,12 +3710,12 @@ extension on TranslationsZh {
|
||||
'explore.watchlistNoMatch' => '无法将此项目与待看列表匹配',
|
||||
'explore.notInLibrary' => '不在你的媒体库中',
|
||||
'explore.inTheseLibraries' => '在这些媒体库中',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => '正在检查你的媒体库…',
|
||||
'explore.emptyTitle' => '这里还什么都没有',
|
||||
'explore.emptyMessage' => ({required Object source}) => '当 ${source} 有内容时,相关内容将显示在这里。',
|
||||
'explore.searchHint' => ({required Object source}) => '搜索 ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => '没有“${query}”的结果',
|
||||
'explore.searchPrompt' => ({required Object source}) => '在 ${source} 上搜索电影和剧集。',
|
||||
'explore.searchFailed' => '搜索失败。请检查网络连接后重试。',
|
||||
@@ -4216,12 +4224,12 @@ extension on TranslationsZh {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => '无法连接到任何地址',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => '${attempts} 次尝试后连接丢失',
|
||||
'companionRemote.errors.connectionLost' => '连接丢失',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => '连接在身份验证前已关闭',
|
||||
'videoSettings.playbackSpeed' => '播放速度',
|
||||
'videoSettings.normalSpeed' => '正常',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => '运行中(${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => '缩放',
|
||||
'videoSettings.sleepTimer' => '睡眠定时器',
|
||||
'videoSettings.audioSync' => '音频同步',
|
||||
|
||||
@@ -463,6 +463,10 @@ class _Translations$settings$zh_Hant extends Translations$settings$zh {
|
||||
@override String get seekAndTiming => '跳轉與計時';
|
||||
@override String get behavior => '行為';
|
||||
@override String get exportDialogTitle => '匯出 Plezy 設定';
|
||||
@override String get linuxVideoRenderMode => '影片轉譯模式';
|
||||
@override String get linuxVideoRenderModeDescription => '自動偏好原生 Wayland 平面(支援 HDR);紋理強制使用 SDR 備援路徑。';
|
||||
@override String get linuxVideoRenderModeAuto => '自動';
|
||||
@override String get linuxVideoRenderModeTexture => '紋理(SDR)';
|
||||
}
|
||||
|
||||
// Path: search
|
||||
@@ -3010,6 +3014,10 @@ extension on TranslationsZhHant {
|
||||
'settings.seekAndTiming' => '跳轉與計時',
|
||||
'settings.behavior' => '行為',
|
||||
'settings.exportDialogTitle' => '匯出 Plezy 設定',
|
||||
'settings.linuxVideoRenderMode' => '影片轉譯模式',
|
||||
'settings.linuxVideoRenderModeDescription' => '自動偏好原生 Wayland 平面(支援 HDR);紋理強制使用 SDR 備援路徑。',
|
||||
'settings.linuxVideoRenderModeAuto' => '自動',
|
||||
'settings.linuxVideoRenderModeTexture' => '紋理(SDR)',
|
||||
'search.hint' => '搜尋電影、影集、音樂…',
|
||||
'search.tryDifferentTerm' => '嘗試不同的關鍵字',
|
||||
'search.searchYourMedia' => '搜尋媒體庫',
|
||||
@@ -3189,12 +3197,12 @@ extension on TranslationsZhHant {
|
||||
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy 無法確認此操作會刪除哪些檔案,因此刪除範圍可能超出上方所列的項目。請取消後重試,或仍要刪除。',
|
||||
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => '您的伺服器未提供此項目的檔案資訊,因此 Plezy 無法確認此操作會刪除哪些檔案。刪除範圍可能超出上方所列的項目。',
|
||||
'mediaMenu.mediaDeletedSuccessfully' => '媒體已成功刪除',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'mediaMenu.mediaFailedToDelete' => '刪除媒體失敗',
|
||||
'mediaMenu.rate' => '評分',
|
||||
'mediaMenu.playFromBeginning' => '從頭播放',
|
||||
'mediaMenu.playVersion' => '播放版本…',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'rateSheet.title' => '評分',
|
||||
'rateSheet.server' => '伺服器',
|
||||
'rateSheet.favorite' => '最愛',
|
||||
@@ -3703,12 +3711,12 @@ extension on TranslationsZhHant {
|
||||
'explore.watchlistNoMatch' => '無法將此項目與待看清單配對',
|
||||
'explore.notInLibrary' => '不在您的媒體庫中',
|
||||
'explore.inTheseLibraries' => '在這些媒體庫中',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.checkingLibrary' => '正在檢查您的媒體庫…',
|
||||
'explore.emptyTitle' => '這裡還沒有任何內容',
|
||||
'explore.emptyMessage' => ({required Object source}) => '當 ${source} 有內容時,相關資訊將顯示在此處。',
|
||||
'explore.searchHint' => ({required Object source}) => '搜尋 ${source}',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'explore.searchEmpty' => ({required Object query}) => '沒有「${query}」的結果',
|
||||
'explore.searchPrompt' => ({required Object source}) => '在 ${source} 搜尋電影與影集。',
|
||||
'explore.searchFailed' => '搜尋失敗。請檢查網路連線後重試。',
|
||||
@@ -4217,12 +4225,12 @@ extension on TranslationsZhHant {
|
||||
'companionRemote.errors.failedToConnectAnyAddress' => '無法連線至任何位址',
|
||||
'companionRemote.errors.connectionLostAfterAttempts' => ({required Object attempts}) => '嘗試 ${attempts} 次後連線中斷',
|
||||
'companionRemote.errors.connectionLost' => '連線已中斷',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'companionRemote.closedBeforeAuth' => '連線在驗證前已關閉',
|
||||
'videoSettings.playbackSpeed' => '播放速度',
|
||||
'videoSettings.normalSpeed' => '正常速度',
|
||||
'videoSettings.sleepTimerActive' => ({required Object duration}) => '執行中(${duration})',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'videoSettings.zoom' => '縮放',
|
||||
'videoSettings.sleepTimer' => '睡眠計時器',
|
||||
'videoSettings.audioSync' => '音訊同步調整',
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Undertexter och konfiguration",
|
||||
"seekAndTiming": "Spolning och tidsinställningar",
|
||||
"behavior": "Beteende",
|
||||
"exportDialogTitle": "Exportera Plezy-inställningar"
|
||||
"exportDialogTitle": "Exportera Plezy-inställningar",
|
||||
"linuxVideoRenderMode": "Videorenderingsläge",
|
||||
"linuxVideoRenderModeDescription": "Automatiskt föredrar det inbyggda Wayland-planet (HDR-kompatibelt); Texture tvingar SDR-reservvägen.",
|
||||
"linuxVideoRenderModeAuto": "Automatiskt",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Sök filmer, serier, musik...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Altyazılar ve Yapılandırma",
|
||||
"seekAndTiming": "Sarılma ve Zamanlama",
|
||||
"behavior": "Davranış",
|
||||
"exportDialogTitle": "Plezy ayarlarını dışa aktar"
|
||||
"exportDialogTitle": "Plezy ayarlarını dışa aktar",
|
||||
"linuxVideoRenderMode": "Video işleme modu",
|
||||
"linuxVideoRenderModeDescription": "Otomatik, yerel Wayland düzlemini tercih eder (HDR destekli); Texture, SDR yedek yolunu zorlar.",
|
||||
"linuxVideoRenderModeAuto": "Otomatik",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Film, dizi, müzik ara...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "Subtitrlar va konfiguratsiya",
|
||||
"seekAndTiming": "Oʻtkazish va vaqt sozlamalari",
|
||||
"behavior": "Xatti-harakat",
|
||||
"exportDialogTitle": "Plezy sozlamalarini eksport qilish"
|
||||
"exportDialogTitle": "Plezy sozlamalarini eksport qilish",
|
||||
"linuxVideoRenderMode": "Video ko'rsatish rejimi",
|
||||
"linuxVideoRenderModeDescription": "Avtomatik rejim mahalliy Wayland tekisligini afzal ko'radi (HDR qo'llab-quvvatlaydi); Texture SDR zaxira yo'lini majbur qiladi.",
|
||||
"linuxVideoRenderModeAuto": "Avtomatik",
|
||||
"linuxVideoRenderModeTexture": "Texture (SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "Filmlar, seriallar, musiqa qidirish...",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "字幕與設定",
|
||||
"seekAndTiming": "跳轉與計時",
|
||||
"behavior": "行為",
|
||||
"exportDialogTitle": "匯出 Plezy 設定"
|
||||
"exportDialogTitle": "匯出 Plezy 設定",
|
||||
"linuxVideoRenderMode": "影片轉譯模式",
|
||||
"linuxVideoRenderModeDescription": "自動偏好原生 Wayland 平面(支援 HDR);紋理強制使用 SDR 備援路徑。",
|
||||
"linuxVideoRenderModeAuto": "自動",
|
||||
"linuxVideoRenderModeTexture": "紋理(SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "搜尋電影、影集、音樂…",
|
||||
|
||||
@@ -340,7 +340,11 @@
|
||||
"subtitlesAndConfig": "字幕与配置",
|
||||
"seekAndTiming": "跳转与计时",
|
||||
"behavior": "行为",
|
||||
"exportDialogTitle": "导出 Plezy 设置"
|
||||
"exportDialogTitle": "导出 Plezy 设置",
|
||||
"linuxVideoRenderMode": "视频渲染模式",
|
||||
"linuxVideoRenderModeDescription": "自动优先使用原生 Wayland 平面(支持 HDR);纹理强制使用 SDR 回退路径。",
|
||||
"linuxVideoRenderModeAuto": "自动",
|
||||
"linuxVideoRenderModeTexture": "纹理(SDR)"
|
||||
},
|
||||
"search": {
|
||||
"hint": "搜索电影、剧集、音乐…",
|
||||
|
||||
@@ -70,6 +70,12 @@ abstract class Player {
|
||||
/// The type of player backend being used (e.g., 'mpv', 'exoplayer').
|
||||
String get playerType;
|
||||
|
||||
/// The Flutter texture ID video renders through, when the platform
|
||||
/// composites video as a GL texture (the Linux SDR fallback path). Null
|
||||
/// when the platform presents through its own surface (the Wayland plane,
|
||||
/// a child HWND) or no texture has been published yet.
|
||||
int? get textureId;
|
||||
|
||||
/// Open a media source for playback.
|
||||
///
|
||||
/// [media] - The media source to open.
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart' show protected, visibleForTesting;
|
||||
import 'package:flutter/foundation.dart' show ValueListenable, ValueNotifier, protected, visibleForTesting;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../media/media_display_criteria.dart';
|
||||
@@ -30,6 +30,23 @@ import 'player_streams.dart';
|
||||
abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
PlayerState _state = const PlayerState();
|
||||
|
||||
/// The Flutter GL texture id published by the platform when video is
|
||||
/// composited as a texture (the Linux SDR fallback path). Null everywhere
|
||||
/// else and before publication; [video.dart] keys its render surface off it.
|
||||
final ValueNotifier<int?> _textureId = ValueNotifier<int?>(null);
|
||||
|
||||
@override
|
||||
int? get textureId => _textureId.value;
|
||||
|
||||
/// Listenable form of [textureId] for widgets that rebuild on publication.
|
||||
ValueListenable<int?> get textureIdListenable => _textureId;
|
||||
|
||||
/// Publishes (or clears) the Flutter texture id backing this player.
|
||||
@protected
|
||||
void setTextureId(int? value) {
|
||||
if (!_disposed) _textureId.value = value;
|
||||
}
|
||||
|
||||
@override
|
||||
PlayerState get state => _state;
|
||||
|
||||
@@ -1379,6 +1396,10 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
Future<void> dispose({bool preserveDisplayMode = false}) async {
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
// The texture is going away with the player; clear the id first so a
|
||||
// still-mounted Video widget stops keying off it before the notifier is
|
||||
// disposed below.
|
||||
_textureId.value = null;
|
||||
|
||||
final channelName = eventChannel.name;
|
||||
if (identical(_eventChannelOwners[channelName], this)) {
|
||||
@@ -1428,6 +1449,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
|
||||
);
|
||||
}
|
||||
await closeStreamControllers();
|
||||
_textureId.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../media/media_display_criteria.dart';
|
||||
import '../../services/settings_service.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../models.dart';
|
||||
import 'audio_rendering_mode.dart';
|
||||
@@ -215,8 +216,24 @@ class PlayerNative extends PlayerBase {
|
||||
|
||||
Future<void> _doInitialize() async {
|
||||
try {
|
||||
final result = await invoke<Object>('initialize');
|
||||
if (result != true) {
|
||||
// Linux render-mode preference: 'texture' forces the SDR Flutter-texture
|
||||
// fallback (the user-visible workaround for plane-only trouble);
|
||||
// anything else prefers the Wayland plane and falls back when the plane
|
||||
// cannot be brought up.
|
||||
final initArgs = usesLinuxVideoPlane
|
||||
? <String, Object?>{'renderMode': SettingsService.instance.read(SettingsService.linuxVideoRenderMode)}
|
||||
: null;
|
||||
final result = await invoke<Object>('initialize', initArgs);
|
||||
if (result is int) {
|
||||
// Linux texture path: the native side registers the texture and
|
||||
// returns its id immediately, but the GPU bootstrap (Flutter invoking
|
||||
// FlTextureGL::populate, which creates the render context and the
|
||||
// shared EGL image) completes asynchronously. Playback must not start
|
||||
// against an unusable texture, so initialization stays gated until
|
||||
// waitForVideoReady reports the texture usable.
|
||||
setTextureId(result);
|
||||
await invoke('waitForVideoReady');
|
||||
} else if (result != true) {
|
||||
throw Exception('Failed to initialize player');
|
||||
}
|
||||
if (_nativeCoreUnavailable) throw StateError('Player was disposed during initialization');
|
||||
@@ -245,6 +262,10 @@ class PlayerNative extends PlayerBase {
|
||||
if (_nativeCoreUnavailable) throw StateError('Player was disposed during initialization');
|
||||
initialized = true;
|
||||
} catch (e) {
|
||||
// A texture published provisionally (Linux fallback) is not usable if
|
||||
// initialization aborted; drop it so the Video widget stops keying off
|
||||
// it and falls back to its other surface path.
|
||||
setTextureId(null);
|
||||
_initFuture = null;
|
||||
if (!_nativeCoreUnavailable) {
|
||||
errorController.add(PlayerError('Initialization failed: $e'));
|
||||
|
||||
@@ -123,6 +123,24 @@ class _VideoState extends State<Video> {
|
||||
}
|
||||
|
||||
Widget _buildVideoSurface() {
|
||||
final player = widget.player;
|
||||
if (player is PlayerBase) {
|
||||
// The texture id can appear after the widget is built (the Linux SDR
|
||||
// fallback publishes it once the native side registers the texture), so
|
||||
// the surface must rebuild on publication.
|
||||
return ValueListenableBuilder<int?>(
|
||||
valueListenable: player.textureIdListenable,
|
||||
builder: (context, textureId, _) => _buildVideoSurfaceForId(textureId),
|
||||
);
|
||||
}
|
||||
return _buildVideoSurfaceForId(player.textureId);
|
||||
}
|
||||
|
||||
Widget _buildVideoSurfaceForId(int? textureId) {
|
||||
if (textureId != null) {
|
||||
// Linux SDR fallback: video arrives as a GL texture Flutter composites.
|
||||
return Texture(textureId: textureId);
|
||||
}
|
||||
if (widget.player is VideoRectSupport) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
|
||||
@@ -77,6 +77,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||
if (Platform.isAndroid) _playerBackendSelector(),
|
||||
if (PlatformDetector.supportsExternalPlayers()) _externalPlayerTile(),
|
||||
_hardwareDecodingTile(),
|
||||
if (Platform.isLinux) _linuxVideoRenderModeTile(),
|
||||
if (PlatformDetector.supportsPictureInPicture()) _autoPipTile(),
|
||||
if (Platform.isAndroid) _matchContentFrameRateTile(),
|
||||
if (Platform.isWindows) _matchRefreshRateTile(),
|
||||
@@ -404,6 +405,19 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||
DvConversionModePreference.hevcStrip => t.settings.dvConversionHevcStrip,
|
||||
};
|
||||
|
||||
Widget _linuxVideoRenderModeTile() => SettingSelectionTile<String>(
|
||||
pref: SettingsService.linuxVideoRenderMode,
|
||||
icon: Symbols.video_settings_rounded,
|
||||
title: t.settings.linuxVideoRenderMode,
|
||||
subtitleBuilder: (mode) =>
|
||||
'${mode == 'texture' ? t.settings.linuxVideoRenderModeTexture : t.settings.linuxVideoRenderModeAuto}'
|
||||
' · ${t.settings.linuxVideoRenderModeDescription}',
|
||||
options: [
|
||||
DialogOption(value: 'auto', title: t.settings.linuxVideoRenderModeAuto),
|
||||
DialogOption(value: 'texture', title: t.settings.linuxVideoRenderModeTexture),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _bufferSizeTile() {
|
||||
final bufferOptions = const [0, 64, 128, 256, 512, 1024];
|
||||
return SettingSelectionTile<int>(
|
||||
|
||||
@@ -377,6 +377,13 @@ class SettingsService extends BaseSharedPreferencesService {
|
||||
static const audioSyncOffset = IntPref('audio_sync_offset');
|
||||
static const subtitleSyncOffset = IntPref('subtitle_sync_offset');
|
||||
static const subtitleSearchLanguage = NullableStringPref('subtitle_search_language');
|
||||
/// Linux video rendering mode. 'auto' prefers the native Wayland plane and
|
||||
/// falls back to the Flutter-texture path when the plane cannot be brought
|
||||
/// up (X11/XWayland sessions, failed plane bootstrap); 'texture' forces the
|
||||
/// texture path (SDR only) — the user-visible workaround for plane-only
|
||||
/// trouble, and the exact environment hardware decode worked in before the
|
||||
/// plane existed.
|
||||
static const linuxVideoRenderMode = StringPref('linux_video_render_mode', defaultValue: 'auto');
|
||||
static const volume = DoublePref('volume', defaultValue: 100.0);
|
||||
static const rotationLocked = BoolPref('rotation_locked', defaultValue: true);
|
||||
static const subtitleFontSize = IntPref('subtitle_font_size', defaultValue: 38);
|
||||
@@ -968,6 +975,7 @@ class SettingsService extends BaseSharedPreferencesService {
|
||||
audioNormalization,
|
||||
audioDownmix,
|
||||
audioDownmixNormalize,
|
||||
linuxVideoRenderMode,
|
||||
appLocale,
|
||||
autoPip,
|
||||
maxVolume,
|
||||
|
||||
@@ -4,8 +4,10 @@ project(runner LANGUAGES CXX)
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"mpv/mpv_gpu_bootstrap.cc"
|
||||
"mpv/mpv_player.cc"
|
||||
"mpv/mpv_plugin.cc"
|
||||
"mpv/mpv_texture.cc"
|
||||
"mpv/wayland_video_surface.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#include "mpv_gpu_bootstrap.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace mpv {
|
||||
namespace {
|
||||
|
||||
bool HasExtension(const char* extensions, const char* requested) {
|
||||
if (!extensions || !requested || requested[0] == '\0' || std::strchr(requested, ' ')) return false;
|
||||
const size_t requested_length = std::strlen(requested);
|
||||
const char* current = extensions;
|
||||
while ((current = std::strstr(current, requested)) != nullptr) {
|
||||
const bool starts_token = current == extensions || current[-1] == ' ';
|
||||
const char following = current[requested_length];
|
||||
if (starts_token && (following == '\0' || following == ' ')) return true;
|
||||
current += requested_length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ParseEglVersion(const char* version, int* major, int* minor) {
|
||||
if (!version || !major || !minor) return false;
|
||||
char* end = nullptr;
|
||||
const long parsed_major = std::strtol(version, &end, 10);
|
||||
if (end == version || *end != '.') return false;
|
||||
const char* minor_start = end + 1;
|
||||
const long parsed_minor = std::strtol(minor_start, &end, 10);
|
||||
if (end == minor_start || parsed_major < 0 || parsed_minor < 0) return false;
|
||||
*major = static_cast<int>(parsed_major);
|
||||
*minor = static_cast<int>(parsed_minor);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AtLeastEgl15(const GpuBootstrapProbe& probe) {
|
||||
return probe.egl_major > 1 || (probe.egl_major == 1 && probe.egl_minor >= 5);
|
||||
}
|
||||
|
||||
bool Fail(std::string* error, const char* message) {
|
||||
if (error) *error = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EGLImageKHR GpuImageDispatch::Create(EGLDisplay display, EGLContext context, EGLClientBuffer buffer) const {
|
||||
if (uses_core) {
|
||||
if (!create_image_core) return EGL_NO_IMAGE_KHR;
|
||||
const EGLAttrib attributes[] = {EGL_NONE};
|
||||
return reinterpret_cast<EGLImageKHR>(
|
||||
create_image_core(display, context, EGL_GL_TEXTURE_2D_KHR, buffer, attributes));
|
||||
}
|
||||
if (!create_image_khr) return EGL_NO_IMAGE_KHR;
|
||||
const EGLint attributes[] = {EGL_NONE};
|
||||
return create_image_khr(display, context, EGL_GL_TEXTURE_2D_KHR, buffer, attributes);
|
||||
}
|
||||
|
||||
bool GpuImageDispatch::Destroy(EGLDisplay display, EGLImageKHR image) const {
|
||||
if (image == EGL_NO_IMAGE_KHR) return true;
|
||||
if (uses_core) {
|
||||
return destroy_image_core && destroy_image_core(display, reinterpret_cast<EGLImage>(image)) == EGL_TRUE;
|
||||
}
|
||||
return destroy_image_khr && destroy_image_khr(display, image) == EGL_TRUE;
|
||||
}
|
||||
|
||||
GpuImageDispatch::operator bool() const {
|
||||
const bool image_functions =
|
||||
uses_core ? create_image_core && destroy_image_core : create_image_khr && destroy_image_khr;
|
||||
return image_functions && image_target_texture;
|
||||
}
|
||||
|
||||
bool ValidateGpuBootstrapProbe(const GpuBootstrapProbe& probe, std::string* error) {
|
||||
const bool egl15 = AtLeastEgl15(probe);
|
||||
if (!egl15 && !HasExtension(probe.egl_extensions, "EGL_KHR_surfaceless_context")) {
|
||||
return Fail(error, "EGL surfaceless contexts are unavailable");
|
||||
}
|
||||
|
||||
const bool core_images = egl15 && probe.create_image_core && probe.destroy_image_core;
|
||||
const bool has_khr_image_extension =
|
||||
HasExtension(probe.egl_extensions, "EGL_KHR_image") || HasExtension(probe.egl_extensions, "EGL_KHR_image_base");
|
||||
const bool khr_images = has_khr_image_extension && probe.create_image_khr && probe.destroy_image_khr;
|
||||
if (!core_images && !khr_images) {
|
||||
return Fail(error, "EGL image creation is unavailable");
|
||||
}
|
||||
if (!HasExtension(probe.gl_extensions, "GL_OES_EGL_image")) {
|
||||
return Fail(error, "OpenGL EGL image binding is unavailable");
|
||||
}
|
||||
if (!probe.image_target_texture) {
|
||||
return Fail(error, "OpenGL EGL image entry point is unavailable");
|
||||
}
|
||||
if (error) error->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResolveGpuImageDispatch(EGLDisplay display, GpuImageDispatch* dispatch, std::string* error) {
|
||||
if (!dispatch || display == EGL_NO_DISPLAY || eglGetCurrentContext() == EGL_NO_CONTEXT) {
|
||||
return Fail(error, "No current EGL context is available");
|
||||
}
|
||||
|
||||
GpuBootstrapProbe probe;
|
||||
if (!ParseEglVersion(eglQueryString(display, EGL_VERSION), &probe.egl_major, &probe.egl_minor)) {
|
||||
return Fail(error, "EGL version is unavailable");
|
||||
}
|
||||
probe.egl_extensions = eglQueryString(display, EGL_EXTENSIONS);
|
||||
probe.gl_extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||
probe.create_image_core = reinterpret_cast<void*>(eglGetProcAddress("eglCreateImage"));
|
||||
probe.destroy_image_core = reinterpret_cast<void*>(eglGetProcAddress("eglDestroyImage"));
|
||||
probe.create_image_khr = reinterpret_cast<void*>(eglGetProcAddress("eglCreateImageKHR"));
|
||||
probe.destroy_image_khr = reinterpret_cast<void*>(eglGetProcAddress("eglDestroyImageKHR"));
|
||||
probe.image_target_texture = reinterpret_cast<void*>(eglGetProcAddress("glEGLImageTargetTexture2DOES"));
|
||||
if (!ValidateGpuBootstrapProbe(probe, error)) return false;
|
||||
|
||||
GpuImageDispatch resolved;
|
||||
const bool egl15 = AtLeastEgl15(probe);
|
||||
if (egl15 && probe.create_image_core && probe.destroy_image_core) {
|
||||
resolved.uses_core = true;
|
||||
resolved.create_image_core = reinterpret_cast<EglCreateImageCoreProc>(probe.create_image_core);
|
||||
resolved.destroy_image_core = reinterpret_cast<EglDestroyImageCoreProc>(probe.destroy_image_core);
|
||||
} else {
|
||||
resolved.create_image_khr = reinterpret_cast<EglCreateImageKhrProc>(probe.create_image_khr);
|
||||
resolved.destroy_image_khr = reinterpret_cast<EglDestroyImageKhrProc>(probe.destroy_image_khr);
|
||||
}
|
||||
resolved.image_target_texture = reinterpret_cast<GlImageTargetTextureProc>(probe.image_target_texture);
|
||||
if (!resolved) return Fail(error, "GPU image dispatch is incomplete");
|
||||
*dispatch = resolved;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mpv
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef MPV_GPU_BOOTSTRAP_H_
|
||||
#define MPV_GPU_BOOTSTRAP_H_
|
||||
|
||||
#include <epoxy/egl.h>
|
||||
#include <epoxy/gl.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mpv {
|
||||
|
||||
using EglCreateImageCoreProc = EGLImage (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLAttrib*);
|
||||
using EglDestroyImageCoreProc = EGLBoolean (*)(EGLDisplay, EGLImage);
|
||||
using EglCreateImageKhrProc = EGLImageKHR (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
|
||||
using EglDestroyImageKhrProc = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
|
||||
using GlImageTargetTextureProc = void (*)(GLenum, GLeglImageOES);
|
||||
|
||||
struct GpuBootstrapProbe {
|
||||
int egl_major = 0;
|
||||
int egl_minor = 0;
|
||||
const char* egl_extensions = nullptr;
|
||||
const char* gl_extensions = nullptr;
|
||||
void* create_image_core = nullptr;
|
||||
void* destroy_image_core = nullptr;
|
||||
void* create_image_khr = nullptr;
|
||||
void* destroy_image_khr = nullptr;
|
||||
void* image_target_texture = nullptr;
|
||||
};
|
||||
|
||||
struct GpuImageDispatch {
|
||||
bool uses_core = false;
|
||||
EglCreateImageCoreProc create_image_core = nullptr;
|
||||
EglDestroyImageCoreProc destroy_image_core = nullptr;
|
||||
EglCreateImageKhrProc create_image_khr = nullptr;
|
||||
EglDestroyImageKhrProc destroy_image_khr = nullptr;
|
||||
GlImageTargetTextureProc image_target_texture = nullptr;
|
||||
|
||||
EGLImageKHR Create(EGLDisplay display, EGLContext context, EGLClientBuffer buffer) const;
|
||||
bool Destroy(EGLDisplay display, EGLImageKHR image) const;
|
||||
explicit operator bool() const;
|
||||
};
|
||||
|
||||
bool ValidateGpuBootstrapProbe(const GpuBootstrapProbe& probe, std::string* error);
|
||||
bool ResolveGpuImageDispatch(EGLDisplay display, GpuImageDispatch* dispatch, std::string* error);
|
||||
|
||||
} // namespace mpv
|
||||
|
||||
#endif // MPV_GPU_BOOTSTRAP_H_
|
||||
@@ -8,6 +8,9 @@
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
#include <gdk/gdkwayland.h>
|
||||
#endif
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
#include <locale.h>
|
||||
|
||||
// EGL 1.5 names; EGL_KHR_create_context introduced the same values earlier.
|
||||
@@ -589,6 +592,190 @@ bool MpvPlayer::InitRenderContextForSurface(EGLDisplay display, EGLConfig config
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MpvPlayer::InitRenderContext() {
|
||||
RetryPendingNativeTeardown();
|
||||
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
if (audio_only_ || disposed_) {
|
||||
g_warning("MPV: Render context requested for an unavailable player");
|
||||
return false;
|
||||
}
|
||||
if (mpv_gl_) return true;
|
||||
if (!mpv_) {
|
||||
g_warning("MPV: Cannot create render context - mpv not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The texture path rides Flutter's EGL display and derives an isolated
|
||||
// context from Flutter's config, exactly as 2.11.0 did: the mpv FBO is
|
||||
// sampled by Flutter via an EGL image, so both sides must speak the same
|
||||
// display's formats. The caller (FlTextureGL::populate) has Flutter's
|
||||
// context current; capture it and restore it around every GL/EGL step
|
||||
// below, since mpv's render context creation may bind its own.
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum previous_api = eglQueryAPI();
|
||||
if (flutter_display == EGL_NO_DISPLAY || flutter_context == EGL_NO_CONTEXT || previous_api == EGL_NONE) {
|
||||
g_warning("MPV: No Flutter EGL context available for the texture render path");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto restore_flutter = [&]() {
|
||||
const EGLBoolean api_restored = previous_api == EGL_NONE ? EGL_TRUE : eglBindAPI(previous_api);
|
||||
const EGLBoolean restored =
|
||||
api_restored == EGL_TRUE ? eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context)
|
||||
: EGL_FALSE;
|
||||
return restored == EGL_TRUE && api_restored == EGL_TRUE;
|
||||
};
|
||||
|
||||
EGLint config_id = 0;
|
||||
if (!eglQueryContext(flutter_display, flutter_context, EGL_CONFIG_ID, &config_id)) {
|
||||
g_warning("MPV: Failed to query Flutter EGL config: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
EGLConfig config = nullptr;
|
||||
EGLint num_configs = 0;
|
||||
const EGLint config_attribs[] = {EGL_CONFIG_ID, config_id, EGL_NONE};
|
||||
if (!eglChooseConfig(flutter_display, config_attribs, &config, 1, &num_configs) || num_configs != 1) {
|
||||
g_warning("MPV: Failed to select Flutter EGL config: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API)) {
|
||||
g_warning("MPV: Failed to bind OpenGL ES API: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// ES 2.0, as 2.11.0 shipped: this path exists to reproduce a configuration
|
||||
// hardware decode demonstrably worked against, not to push the driver.
|
||||
const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
|
||||
EGLContext candidate_context = eglCreateContext(flutter_display, config, EGL_NO_CONTEXT, context_attribs);
|
||||
if (candidate_context == EGL_NO_CONTEXT) {
|
||||
g_warning("MPV: Failed to create isolated EGL context: 0x%x", eglGetError());
|
||||
if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) {
|
||||
g_warning("MPV: Failed to restore EGL client API: 0x%x", eglGetError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto destroy_candidate_context = [&]() {
|
||||
const EGLenum api_before_cleanup = eglQueryAPI();
|
||||
if (eglGetCurrentContext() == candidate_context) {
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) ||
|
||||
!eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
g_warning("MPV: Failed to release rejected EGL context: 0x%x", eglGetError());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!eglDestroyContext(flutter_display, candidate_context)) {
|
||||
g_warning("MPV: Failed to destroy rejected EGL context: 0x%x", eglGetError());
|
||||
}
|
||||
if (api_before_cleanup != EGL_NONE && !eglBindAPI(api_before_cleanup)) {
|
||||
g_warning("MPV: Failed to restore EGL API after context cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
};
|
||||
|
||||
if (!eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, candidate_context)) {
|
||||
g_warning("MPV: Failed to activate isolated EGL context: 0x%x", eglGetError());
|
||||
destroy_candidate_context();
|
||||
if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) {
|
||||
g_warning("MPV: Failed to restore EGL client API: 0x%x", eglGetError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
mpv_opengl_init_params gl_init_params{};
|
||||
gl_init_params.get_proc_address = get_opengl_proc_address;
|
||||
gl_init_params.get_proc_address_ctx = nullptr;
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_API_TYPE, const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
|
||||
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
|
||||
// The display handle for hwdec interop, whichever session this is: the
|
||||
// texture path is display-agnostic and must work under both X11 and
|
||||
// Wayland, so both slots are offered exactly as 2.11.0 did.
|
||||
GdkDisplay* gdk_display = gdk_display_get_default();
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
if (gdk_display != nullptr && GDK_IS_WAYLAND_DISPLAY(gdk_display)) {
|
||||
params[2].type = MPV_RENDER_PARAM_WL_DISPLAY;
|
||||
params[2].data = gdk_wayland_display_get_wl_display(gdk_display);
|
||||
}
|
||||
#endif
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
if (gdk_display != nullptr && GDK_IS_X11_DISPLAY(gdk_display)) {
|
||||
params[2].type = MPV_RENDER_PARAM_X11_DISPLAY;
|
||||
params[2].data = gdk_x11_display_get_xdisplay(gdk_display);
|
||||
}
|
||||
#endif
|
||||
|
||||
mpv_render_context* candidate_gl = nullptr;
|
||||
const int error = mpv_render_context_create(&candidate_gl, mpv_, params);
|
||||
const bool restored = restore_flutter();
|
||||
if (error < 0 || candidate_gl == nullptr || !restored) {
|
||||
if (error < 0) {
|
||||
g_warning("MPV: mpv_render_context_create() failed for the texture path: %s", mpv_error_string(error));
|
||||
} else if (!restored) {
|
||||
g_warning("MPV: Failed to restore Flutter EGL state: 0x%x", eglGetError());
|
||||
} else {
|
||||
g_warning("MPV: mpv returned a null render context for the texture path");
|
||||
}
|
||||
if (candidate_gl) mpv_render_context_free(candidate_gl);
|
||||
destroy_candidate_context();
|
||||
return false;
|
||||
}
|
||||
|
||||
egl_display_ = flutter_display;
|
||||
egl_context_ = candidate_context;
|
||||
// The texture path renders into an 8-bit RGBA FBO; tell mpv the depth is 8
|
||||
// so it dithers for the format it actually draws into.
|
||||
surface_depth_bits_ = 8;
|
||||
mpv_gl_ = candidate_gl;
|
||||
mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, callback_context_.get());
|
||||
g_message("MPV: Render context created on the Flutter-texture path");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MpvPlayer::HasRenderContext() const {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
return mpv_gl_ != nullptr;
|
||||
}
|
||||
|
||||
EGLDisplay MpvPlayer::GetEglDisplay() const {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
return egl_display_;
|
||||
}
|
||||
|
||||
EGLContext MpvPlayer::GetEglContext() const {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
return egl_context_;
|
||||
}
|
||||
|
||||
void MpvPlayer::Render(int width, int height, int fbo) {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
if (disposed_ || !mpv_gl_) return;
|
||||
|
||||
mpv_opengl_fbo mpv_fbo{};
|
||||
mpv_fbo.fbo = fbo;
|
||||
mpv_fbo.w = width;
|
||||
mpv_fbo.h = height;
|
||||
// 2.11.0 shipped this path with internal_format 0 and no flip: the FBO is
|
||||
// sampled by Flutter as a GL texture, and the orientation was correct with
|
||||
// these exact values. Keep them.
|
||||
mpv_fbo.internal_format = 0;
|
||||
|
||||
int flip_y = 0;
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_OPENGL_FBO, &mpv_fbo},
|
||||
{MPV_RENDER_PARAM_FLIP_Y, &flip_y},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
mpv_render_context_render(mpv_gl_, params);
|
||||
}
|
||||
|
||||
bool MpvPlayer::RenderToSurface(EGLSurface surface, int width, int height) {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
if (disposed_ || !mpv_gl_ || egl_context_ == EGL_NO_CONTEXT || surface == EGL_NO_SURFACE) return false;
|
||||
|
||||
@@ -98,6 +98,36 @@ class MpvPlayer {
|
||||
/// @return true if render context creation succeeded.
|
||||
bool InitRenderContextForSurface(EGLDisplay display, EGLConfig config, EGLSurface surface, int depth_bits);
|
||||
|
||||
/// Creates the mpv render context on an isolated ES 2.0 context derived
|
||||
/// from Flutter's current EGL display and config — the 2.11.0 texture path,
|
||||
/// restored as the SDR fallback for sessions that cannot host the Wayland
|
||||
/// plane (X11, XWayland, or a plane that failed to initialize). Shares
|
||||
/// nothing with Flutter's own context beyond the display (the isolated-
|
||||
/// context design from 3d9c6b20), and must be called while Flutter's EGL
|
||||
/// context is current — the texture's populate path is what guarantees
|
||||
/// that. The texture mode also passes the X11 display handle where present,
|
||||
/// which is what makes hwdec interop work under XWayland exactly as it did
|
||||
/// in 2.11.0. No-op once a render context exists in either mode.
|
||||
/// @return true if render context creation succeeded.
|
||||
bool InitRenderContext();
|
||||
|
||||
/// Whether a render context exists (either mode).
|
||||
bool HasRenderContext() const;
|
||||
|
||||
/// The EGL display/context backing the render context, for callers that
|
||||
/// need to activate it (the texture path renders into an offscreen FBO
|
||||
/// created in this context).
|
||||
EGLDisplay GetEglDisplay() const;
|
||||
EGLContext GetEglContext() const;
|
||||
|
||||
/// Renders one frame into the caller's FBO (texture mode). The caller owns
|
||||
/// the current EGL context and FBO binding; this mirrors the 2.11.0
|
||||
/// contract exactly — it does not make the context current and does not
|
||||
/// consume the redraw latch (MpvTexturePopulate clears the latch first, and
|
||||
/// the latch is what keeps Flutter's populate from being invoked
|
||||
/// needlessly).
|
||||
void Render(int width, int height, int fbo);
|
||||
|
||||
/// Renders one frame into |surface|'s default framebuffer. The caller
|
||||
/// presents it (eglSwapBuffers) once this returns.
|
||||
/// @return true if the frame was rendered.
|
||||
|
||||
+249
-17
@@ -7,11 +7,17 @@
|
||||
#include <new>
|
||||
#include <optional>
|
||||
|
||||
#include "mpv_texture.h"
|
||||
#include "wayland_video_surface.h"
|
||||
|
||||
using PlayerPtr = std::unique_ptr<mpv::MpvPlayer>;
|
||||
using VideoSurfacePtr = std::unique_ptr<mpv::WaylandVideoSurface>;
|
||||
|
||||
// Texture-bootstrap state machine (the SDR fallback path). kIdle means no
|
||||
// texture session exists; kPending means a texture is registered and Flutter
|
||||
// has been asked to populate it; kReady/kFailed are terminal for the session.
|
||||
enum class VideoBootstrapState { kIdle, kPending, kReady, kFailed };
|
||||
|
||||
// One queued HDR transaction: what to apply, and who to tell when it settles.
|
||||
//
|
||||
// `mode` is engaged only when the request *is* the mode change. Everything else
|
||||
@@ -36,9 +42,24 @@ struct _MpvPlugin {
|
||||
|
||||
PlayerPtr player;
|
||||
// The native Wayland plane every video instance renders into. Non-null once
|
||||
// initialize() has brought it up; there is no second render path, so a null
|
||||
// here on a video instance means initialization refused.
|
||||
// initialize() has brought it up. Where the plane cannot be brought up -
|
||||
// X11/XWayland sessions, a failed EGL bootstrap - the Flutter-texture path
|
||||
// (2.11.0's renderer) takes over as the SDR fallback, and a null plane with
|
||||
// a non-null texture means texture mode.
|
||||
VideoSurfacePtr video_surface;
|
||||
FlTextureRegistrar* texture_registrar;
|
||||
// The fallback render path: an FlTextureGL whose populate renders mpv into
|
||||
// an offscreen FBO sampled by Flutter. Owned via GObject ref; the plugin
|
||||
// keeps the player alive for it. Non-null only when a texture session has
|
||||
// been bootstrapped.
|
||||
MpvTexture* texture; // owned via GObject ref
|
||||
gboolean texture_registered;
|
||||
VideoBootstrapState bootstrap_state;
|
||||
gchar* bootstrap_error;
|
||||
// The one waitForVideoReady method call outstanding, answered when the
|
||||
// texture's GPU bootstrap settles or the 5 s deadline expires.
|
||||
FlMethodCall* ready_call;
|
||||
guint ready_timeout_source_id;
|
||||
// Set when the plane must be redrawn even though mpv has no new frame -
|
||||
// after a resize or after becoming visible, where the buffer on screen is
|
||||
// stale or absent. Sticky, because the render it asks for may first have to
|
||||
@@ -192,6 +213,97 @@ static void send_named_event(MpvPlugin* self, const char* name) {
|
||||
send_event(self, event);
|
||||
}
|
||||
|
||||
static gboolean handle_ready_timeout(gpointer user_data);
|
||||
static void complete_ready_call(MpvPlugin* self, gboolean success, const char* message) {
|
||||
if (self->ready_timeout_source_id != 0) {
|
||||
g_source_remove(self->ready_timeout_source_id);
|
||||
self->ready_timeout_source_id = 0;
|
||||
}
|
||||
if (!self->ready_call) return;
|
||||
g_autoptr(FlMethodResponse) response =
|
||||
success ? FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr))
|
||||
: FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INIT_FAILED", message ? message : "Video initialization failed", nullptr));
|
||||
fl_method_call_respond(self->ready_call, response, nullptr);
|
||||
g_object_unref(self->ready_call);
|
||||
self->ready_call = nullptr;
|
||||
}
|
||||
|
||||
// The texture's populate callback runs on Flutter's raster thread, so the
|
||||
// outcome is bounced to the GTK main context before touching the plugin's
|
||||
// state.
|
||||
struct TextureReadyContext {
|
||||
MpvPlugin* plugin; // g_object_ref'd
|
||||
guint64 generation;
|
||||
};
|
||||
|
||||
struct TextureReadyResult {
|
||||
MpvPlugin* plugin; // owns a ref
|
||||
guint64 generation;
|
||||
gboolean success;
|
||||
gchar* message;
|
||||
};
|
||||
|
||||
static gboolean handle_texture_ready_result(gpointer data) {
|
||||
auto* result = static_cast<TextureReadyResult*>(data);
|
||||
MpvPlugin* self = result->plugin;
|
||||
if (result->generation != self->generation || self->bootstrap_state != VideoBootstrapState::kPending) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
if (result->success) {
|
||||
self->bootstrap_state = VideoBootstrapState::kReady;
|
||||
self->initialized = TRUE;
|
||||
complete_ready_call(self, TRUE, nullptr);
|
||||
} else {
|
||||
self->bootstrap_state = VideoBootstrapState::kFailed;
|
||||
g_free(self->bootstrap_error);
|
||||
self->bootstrap_error = g_strdup(result->message ? result->message : "Video initialization failed");
|
||||
complete_ready_call(self, FALSE, self->bootstrap_error);
|
||||
release_video_resources(self);
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void destroy_texture_ready_result(gpointer data) {
|
||||
auto* result = static_cast<TextureReadyResult*>(data);
|
||||
g_object_unref(result->plugin);
|
||||
g_free(result->message);
|
||||
delete result;
|
||||
}
|
||||
|
||||
static void on_texture_ready(gboolean success, const gchar* message, gpointer user_data) {
|
||||
auto* context = static_cast<TextureReadyContext*>(user_data);
|
||||
auto* result = new TextureReadyResult{
|
||||
MPV_PLUGIN(g_object_ref(context->plugin)),
|
||||
context->generation,
|
||||
success,
|
||||
g_strdup(message),
|
||||
};
|
||||
g_main_context_invoke_full(
|
||||
nullptr, G_PRIORITY_DEFAULT, handle_texture_ready_result, result, destroy_texture_ready_result);
|
||||
}
|
||||
|
||||
static void destroy_texture_ready_context(gpointer data) {
|
||||
auto* context = static_cast<TextureReadyContext*>(data);
|
||||
g_object_unref(context->plugin);
|
||||
delete context;
|
||||
}
|
||||
|
||||
static gboolean handle_ready_timeout(gpointer user_data) {
|
||||
MpvPlugin* self = MPV_PLUGIN(user_data);
|
||||
self->ready_timeout_source_id = 0;
|
||||
if (self->bootstrap_state != VideoBootstrapState::kPending || !self->ready_call) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
self->bootstrap_state = VideoBootstrapState::kFailed;
|
||||
g_free(self->bootstrap_error);
|
||||
self->bootstrap_error = g_strdup("Video texture did not become ready before the initialization deadline");
|
||||
complete_ready_call(self, FALSE, self->bootstrap_error);
|
||||
release_video_resources(self);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void release_video_resources(MpvPlugin* self) {
|
||||
// Anything still in flight is now answering for a plane that is going away.
|
||||
++self->generation;
|
||||
@@ -201,6 +313,7 @@ static void release_video_resources(MpvPlugin* self) {
|
||||
g_source_remove(self->hdr_mpv_leg_timeout_source_);
|
||||
self->hdr_mpv_leg_timeout_source_ = 0;
|
||||
}
|
||||
complete_ready_call(self, FALSE, "Video initialization was cancelled");
|
||||
// Queued transactions will never run, and each may be holding a reference to a
|
||||
// Dart method call that has to be answered or it is leaked along with its
|
||||
// response.
|
||||
@@ -212,12 +325,25 @@ static void release_video_resources(MpvPlugin* self) {
|
||||
if (request.done) request.done(MPV_ERROR_UNINITIALIZED);
|
||||
}
|
||||
if (self->player) {
|
||||
// The plane is a raw callback target. Revoke every callback path before
|
||||
// tearing it down; Dispose then drains any callback already holding a
|
||||
// native lease.
|
||||
// The plane and the texture are raw callback targets. Revoke every
|
||||
// callback path before tearing either down; Dispose then drains any
|
||||
// callback already holding a native lease.
|
||||
self->player->SetRedrawCallback(nullptr);
|
||||
self->player->SetSourceMetadataCallback(nullptr);
|
||||
self->player->SetEventCallback(nullptr);
|
||||
}
|
||||
if (self->texture) {
|
||||
// Texture must be disposed BEFORE the player — mpv_texture_dispose needs
|
||||
// the player's EGL context to clean up GL resources.
|
||||
if (self->texture_registered && self->texture_registrar) {
|
||||
fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture));
|
||||
self->texture_registered = FALSE;
|
||||
}
|
||||
mpv_texture_dispose(self->texture);
|
||||
g_object_unref(self->texture);
|
||||
self->texture = nullptr;
|
||||
}
|
||||
if (self->player) {
|
||||
self->player->Dispose();
|
||||
self->player.reset();
|
||||
}
|
||||
@@ -231,6 +357,8 @@ static void release_video_resources(MpvPlugin* self) {
|
||||
self->initialized = FALSE;
|
||||
self->visible = FALSE;
|
||||
self->plane_needs_render = FALSE;
|
||||
self->bootstrap_state = VideoBootstrapState::kIdle;
|
||||
g_clear_pointer(&self->bootstrap_error, g_free);
|
||||
// All of these describe a plane and an mpv instance that no longer exist, and
|
||||
// initialize() builds both fresh: a new MpvPlayer whose applied target
|
||||
// properties are back to "auto", and a new surface with no description
|
||||
@@ -887,6 +1015,13 @@ static void mpv_plugin_init(MpvPlugin* self) {
|
||||
self->initialized = FALSE;
|
||||
self->audio_only = FALSE;
|
||||
self->generation = 0;
|
||||
self->texture = nullptr;
|
||||
self->texture_registered = FALSE;
|
||||
self->texture_registrar = nullptr;
|
||||
self->bootstrap_state = VideoBootstrapState::kIdle;
|
||||
self->bootstrap_error = nullptr;
|
||||
self->ready_call = nullptr;
|
||||
self->ready_timeout_source_id = 0;
|
||||
}
|
||||
|
||||
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar, const gchar* channel_name, gboolean audio_only) {
|
||||
@@ -894,6 +1029,9 @@ MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar, const gchar* channel_nam
|
||||
|
||||
self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
|
||||
self->audio_only = audio_only;
|
||||
// The audio-only core never renders; leaving the texture registrar unset
|
||||
// makes the GL/texture path structurally unreachable for it.
|
||||
self->texture_registrar = audio_only ? nullptr : fl_plugin_registrar_get_texture_registrar(registrar);
|
||||
|
||||
self->player = std::make_unique<mpv::MpvPlayer>(audio_only);
|
||||
|
||||
@@ -952,7 +1090,18 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
}
|
||||
} else if (self->video_surface && self->video_surface->valid()) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE)));
|
||||
} else if (self->texture && (self->bootstrap_state == VideoBootstrapState::kPending ||
|
||||
self->bootstrap_state == VideoBootstrapState::kReady)) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(mpv_texture_get_id(self->texture))));
|
||||
} else {
|
||||
// Optional 'renderMode' argument: "texture" forces the SDR
|
||||
// Flutter-texture fallback (the user-visible workaround for plane-only
|
||||
// trouble); anything else prefers the Wayland plane and falls back only
|
||||
// when it cannot be brought up.
|
||||
FlValue* render_mode_value = args != nullptr ? fl_value_lookup_string(args, "renderMode") : nullptr;
|
||||
const bool force_texture =
|
||||
render_mode_value != nullptr && fl_value_get_type(render_mode_value) == FL_VALUE_TYPE_STRING &&
|
||||
g_strcmp0(fl_value_get_string(render_mode_value), "texture") == 0;
|
||||
if (!self->player || self->player->IsDisposed()) {
|
||||
self->player = std::make_unique<mpv::MpvPlayer>();
|
||||
}
|
||||
@@ -962,22 +1111,86 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
release_video_resources(self);
|
||||
response =
|
||||
FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", "Failed to initialize MPV player", nullptr));
|
||||
} else if (start_video_plane(self, fl_plugin_registrar_get_view(self->registrar), &error)) {
|
||||
++self->generation;
|
||||
self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
|
||||
self->initialized = TRUE;
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE)));
|
||||
} else {
|
||||
// There is no second render path. Refuse with the reason rather than
|
||||
// presenting into something the user cannot see.
|
||||
g_warning("MPV: no video plane: %s", error.c_str());
|
||||
release_video_resources(self);
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("VIDEO_PLANE_UNSUPPORTED", error.c_str(), nullptr));
|
||||
bool plane_up = false;
|
||||
if (!force_texture) {
|
||||
plane_up = start_video_plane(self, fl_plugin_registrar_get_view(self->registrar), &error);
|
||||
}
|
||||
if (plane_up) {
|
||||
++self->generation;
|
||||
self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
|
||||
self->initialized = TRUE;
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE)));
|
||||
} else if (self->texture_registrar != nullptr) {
|
||||
// The plane cannot host this session (X11/XWayland, or the plane
|
||||
// bootstrap failed). Fall back to the 2.11.0 Flutter-texture path:
|
||||
// display-agnostic, SDR only, and the exact environment hardware
|
||||
// decode demonstrably worked in. The texture is registered
|
||||
// immediately so Flutter can invoke FlTextureGL::populate; playback
|
||||
// stays gated until the GPU bootstrap reports the texture usable
|
||||
// (the Dart side awaits waitForVideoReady).
|
||||
if (!force_texture) {
|
||||
g_warning("MPV: no video plane (%s); falling back to the Flutter-texture path", error.c_str());
|
||||
}
|
||||
FlView* view = fl_plugin_registrar_get_view(self->registrar);
|
||||
self->texture = mpv_texture_new(self->player.get(), self->texture_registrar, view);
|
||||
++self->generation;
|
||||
self->bootstrap_state = VideoBootstrapState::kPending;
|
||||
auto* ready_context = new TextureReadyContext{MPV_PLUGIN(g_object_ref(self)), self->generation};
|
||||
mpv_texture_set_ready_callback(self->texture, on_texture_ready, ready_context, destroy_texture_ready_context);
|
||||
|
||||
if (!fl_texture_registrar_register_texture(self->texture_registrar, FL_TEXTURE(self->texture))) {
|
||||
self->bootstrap_state = VideoBootstrapState::kFailed;
|
||||
self->bootstrap_error = g_strdup("Failed to register video texture");
|
||||
release_video_resources(self);
|
||||
response =
|
||||
FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", self->bootstrap_error, nullptr));
|
||||
} else {
|
||||
self->texture_registered = TRUE;
|
||||
MpvTexture* texture = self->texture;
|
||||
self->player->SetRedrawCallback([texture]() { mpv_texture_mark_frame_available(texture); });
|
||||
self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
|
||||
mpv_texture_mark_frame_available(self->texture);
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_success_response_new(fl_value_new_int(mpv_texture_get_id(self->texture))));
|
||||
}
|
||||
} else {
|
||||
// No texture registrar at all: refuse with the plane's reason
|
||||
// rather than presenting into something the user cannot see.
|
||||
g_warning("MPV: no video plane: %s", error.c_str());
|
||||
release_video_resources(self);
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("VIDEO_PLANE_UNSUPPORTED", error.c_str(), nullptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (strcmp(method, "dispose") == 0) {
|
||||
release_video_resources(self);
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (strcmp(method, "waitForVideoReady") == 0) {
|
||||
// Texture-path only: initialize returns the texture id as soon as the
|
||||
// texture is registered, but the GPU bootstrap (FlTextureGL::populate,
|
||||
// which creates the render context and the shared EGL image) completes
|
||||
// asynchronously. Playback must not start against an unusable texture.
|
||||
if (self->audio_only) {
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_error_response_new("INIT_FAILED", "Audio players have no video readiness state", nullptr));
|
||||
} else if (self->bootstrap_state == VideoBootstrapState::kReady && self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (self->bootstrap_state == VideoBootstrapState::kFailed) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INIT_FAILED", self->bootstrap_error ? self->bootstrap_error : "Video initialization failed", nullptr));
|
||||
} else if (self->bootstrap_state != VideoBootstrapState::kPending || !self->texture) {
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_error_response_new("INIT_FAILED", "Video initialization is not pending", nullptr));
|
||||
} else if (self->ready_call) {
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_error_response_new("INIT_IN_PROGRESS", "Video readiness is already being awaited", nullptr));
|
||||
} else {
|
||||
self->ready_call = FL_METHOD_CALL(g_object_ref(method_call));
|
||||
self->ready_timeout_source_id = g_timeout_add_seconds_full(
|
||||
G_PRIORITY_DEFAULT, 5, handle_ready_timeout, g_object_ref(self), g_object_unref);
|
||||
return;
|
||||
}
|
||||
} else if (strcmp(method, "command") == 0) {
|
||||
if (!self->player || !self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr));
|
||||
@@ -1133,6 +1346,17 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (!self->video_surface && !self->audio_only && g_strcmp0(fl_value_get_string(name_value), "hdr-tone-mapping") == 0) {
|
||||
// Texture-fallback mode has no plane and no description machinery, so
|
||||
// the tone-map owner is meaningless. Answered success so the startup
|
||||
// preference push does not read a refusal.
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (!self->video_surface && !self->audio_only && g_strcmp0(fl_value_get_string(name_value), "hdr-enabled") == 0) {
|
||||
// No plane, no HDR. Honest refusal; Dart swallows it on Linux and
|
||||
// reconciles the stored preference down, so the settings switch
|
||||
// agrees with the session.
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"HDR_UNSUPPORTED", "This session has no video plane and cannot carry HDR", nullptr));
|
||||
} else if (!self->audio_only && g_strcmp0(fl_value_get_string(name_value), "vo") == 0 &&
|
||||
g_strcmp0(fl_value_get_string(value_value), "libmpv") != 0) {
|
||||
// Embedded rendering is authoritative: the render context was created
|
||||
@@ -1267,6 +1491,10 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
// consumed (or suppressed) while hidden, so no callback is pending.
|
||||
if (self->visible) render_video_plane(self, TRUE);
|
||||
}
|
||||
} else if (self->visible && self->texture) {
|
||||
// Texture path: becoming visible has to kick Flutter's populate, for
|
||||
// the same reason the plane renders explicitly above.
|
||||
mpv_texture_mark_frame_available(self->texture);
|
||||
}
|
||||
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
@@ -1345,8 +1573,12 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
} else if (strcmp(method, "updateFrame") == 0) {
|
||||
// The cross-platform "kick the video output" call. On the plane that means
|
||||
// forcing a render: mpv may have no new frame, but the caller is asking
|
||||
// because what is on screen is stale.
|
||||
if (self->visible) render_video_plane(self, TRUE);
|
||||
// because what is on screen is stale. On the texture path the same kick
|
||||
// marks the texture available.
|
||||
if (self->visible) {
|
||||
if (self->video_surface) render_video_plane(self, TRUE);
|
||||
if (self->texture) mpv_texture_mark_frame_available(self->texture);
|
||||
}
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (strcmp(method, "isInitialized") == 0) {
|
||||
gboolean initialized = self->player && self->initialized;
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
#include "mpv_texture.h"
|
||||
|
||||
#include <epoxy/egl.h>
|
||||
#include <epoxy/gl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mpv_gpu_bootstrap.h"
|
||||
|
||||
namespace {
|
||||
|
||||
GQuark TextureErrorDomain() { return g_quark_from_static_string("plezy-mpv-texture"); }
|
||||
|
||||
struct TextureResources {
|
||||
GLuint mpv_fbo = 0;
|
||||
GLuint mpv_texture = 0;
|
||||
GLuint flutter_texture = 0;
|
||||
EGLImageKHR egl_image = EGL_NO_IMAGE_KHR;
|
||||
int32_t width = 0;
|
||||
int32_t height = 0;
|
||||
|
||||
bool complete() const {
|
||||
return mpv_fbo != 0 && mpv_texture != 0 && flutter_texture != 0 && egl_image != EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
};
|
||||
|
||||
bool SetError(GError** error, const char* message) {
|
||||
g_set_error_literal(error, TextureErrorDomain(), 1, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
void ClearGlErrors() {
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct _MpvTexture {
|
||||
FlTextureGL parent_instance;
|
||||
|
||||
mpv::MpvPlayer* player;
|
||||
FlTextureRegistrar* registrar;
|
||||
FlView* view;
|
||||
|
||||
GMutex mutex;
|
||||
bool disposed;
|
||||
TextureResources* active;
|
||||
std::vector<TextureResources>* retired;
|
||||
mpv::GpuImageDispatch* image_dispatch;
|
||||
EGLDisplay flutter_display;
|
||||
EGLContext flutter_share_context;
|
||||
EGLContext flutter_cleanup_context;
|
||||
|
||||
GMutex bootstrap_mutex;
|
||||
gint bootstrap_state;
|
||||
gchar* bootstrap_error;
|
||||
MpvTextureReadyCallback ready_callback;
|
||||
gpointer ready_user_data;
|
||||
GDestroyNotify ready_destroy_notify;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MpvTexture, mpv_texture, fl_texture_gl_get_type())
|
||||
|
||||
namespace {
|
||||
|
||||
void SignalBootstrap(MpvTexture* self, gboolean success, const char* message) {
|
||||
MpvTextureReadyCallback callback = nullptr;
|
||||
gpointer user_data = nullptr;
|
||||
g_mutex_lock(&self->bootstrap_mutex);
|
||||
if (self->bootstrap_state == 0) {
|
||||
self->bootstrap_state = success ? 1 : 2;
|
||||
if (!success) self->bootstrap_error = g_strdup(message ? message : "Video initialization failed");
|
||||
callback = self->ready_callback;
|
||||
user_data = self->ready_user_data;
|
||||
}
|
||||
g_mutex_unlock(&self->bootstrap_mutex);
|
||||
if (callback) callback(success, message, user_data);
|
||||
}
|
||||
|
||||
bool RestoreContext(
|
||||
EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context, EGLenum api, GError** error) {
|
||||
if (api != EGL_NONE && !eglBindAPI(api)) {
|
||||
g_warning("MPV texture: failed to restore Flutter EGL API: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to restore Flutter EGL API");
|
||||
}
|
||||
if (eglMakeCurrent(display, draw, read, context)) return true;
|
||||
g_warning("MPV texture: failed to restore EGL context: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to restore Flutter EGL context");
|
||||
}
|
||||
|
||||
void RestoreOrReleaseContext(
|
||||
EGLDisplay flutter_display, EGLSurface flutter_draw, EGLSurface flutter_read, EGLContext flutter_context,
|
||||
EGLenum flutter_api, EGLDisplay mpv_display) {
|
||||
if (flutter_display != EGL_NO_DISPLAY && flutter_context != EGL_NO_CONTEXT) {
|
||||
const bool api_restored = flutter_api == EGL_NONE || eglBindAPI(flutter_api);
|
||||
if (api_restored && eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context)) return;
|
||||
g_warning("MPV texture: failed to restore EGL state during cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
|
||||
if (mpv_display != EGL_NO_DISPLAY) {
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API)) {
|
||||
g_warning("MPV texture: failed to bind OpenGL while releasing cleanup context: 0x%x", eglGetError());
|
||||
} else if (!eglMakeCurrent(mpv_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
g_warning("MPV texture: failed to release EGL context during cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
if (flutter_api != EGL_NONE && !eglBindAPI(flutter_api)) {
|
||||
g_warning("MPV texture: failed to restore Flutter EGL API after cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceSetEmpty(const TextureResources& resources) {
|
||||
return resources.mpv_fbo == 0 && resources.mpv_texture == 0 && resources.flutter_texture == 0 &&
|
||||
resources.egl_image == EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
|
||||
bool EnsureFlutterCleanupContext(
|
||||
MpvTexture* self, EGLDisplay flutter_display, EGLContext flutter_context, EGLenum flutter_api, GError** error) {
|
||||
if (self->flutter_cleanup_context != EGL_NO_CONTEXT) {
|
||||
if (self->flutter_display == flutter_display && self->flutter_share_context == flutter_context) return true;
|
||||
return SetError(error, "Flutter EGL context changed while video textures were active");
|
||||
}
|
||||
if (flutter_api != EGL_OPENGL_ES_API) {
|
||||
return SetError(error, "Flutter is not using an OpenGL ES context");
|
||||
}
|
||||
|
||||
EGLint config_id = 0;
|
||||
EGLint client_version = 0;
|
||||
if (!eglQueryContext(flutter_display, flutter_context, EGL_CONFIG_ID, &config_id) ||
|
||||
!eglQueryContext(flutter_display, flutter_context, EGL_CONTEXT_CLIENT_VERSION, &client_version)) {
|
||||
g_warning("MPV texture: failed to query Flutter EGL context: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to query Flutter EGL context");
|
||||
}
|
||||
EGLConfig config = nullptr;
|
||||
EGLint num_configs = 0;
|
||||
const EGLint config_attribs[] = {EGL_CONFIG_ID, config_id, EGL_NONE};
|
||||
if (!eglChooseConfig(flutter_display, config_attribs, &config, 1, &num_configs) || num_configs != 1) {
|
||||
g_warning("MPV texture: failed to select Flutter EGL config: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to select Flutter EGL config");
|
||||
}
|
||||
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API)) {
|
||||
g_warning("MPV texture: failed to bind OpenGL ES for cleanup context creation: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to bind OpenGL ES for video cleanup");
|
||||
}
|
||||
const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, client_version, EGL_NONE};
|
||||
const EGLContext cleanup_context = eglCreateContext(flutter_display, config, flutter_context, context_attribs);
|
||||
const bool api_restored = eglBindAPI(flutter_api) == EGL_TRUE;
|
||||
if (cleanup_context == EGL_NO_CONTEXT || !api_restored) {
|
||||
if (cleanup_context != EGL_NO_CONTEXT && !eglDestroyContext(flutter_display, cleanup_context)) {
|
||||
g_warning("MPV texture: failed to destroy rejected cleanup context: 0x%x", eglGetError());
|
||||
}
|
||||
if (!api_restored) {
|
||||
g_warning("MPV texture: failed to restore Flutter EGL API after cleanup context creation: 0x%x", eglGetError());
|
||||
}
|
||||
return SetError(error, "Failed to create video cleanup context");
|
||||
}
|
||||
|
||||
self->flutter_display = flutter_display;
|
||||
self->flutter_share_context = flutter_context;
|
||||
self->flutter_cleanup_context = cleanup_context;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DestroyFlutterCleanupContext(MpvTexture* self) {
|
||||
if (self->flutter_cleanup_context == EGL_NO_CONTEXT || self->flutter_display == EGL_NO_DISPLAY) return;
|
||||
|
||||
const EGLenum previous_api = eglQueryAPI();
|
||||
if (eglGetCurrentContext() == self->flutter_cleanup_context) {
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) ||
|
||||
!eglMakeCurrent(self->flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
g_warning("MPV texture: failed to release Flutter cleanup context: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
if (!eglDestroyContext(self->flutter_display, self->flutter_cleanup_context)) {
|
||||
g_warning("MPV texture: failed to destroy Flutter cleanup context: 0x%x", eglGetError());
|
||||
}
|
||||
if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) {
|
||||
g_warning("MPV texture: failed to restore EGL API after cleanup context destruction: 0x%x", eglGetError());
|
||||
}
|
||||
self->flutter_cleanup_context = EGL_NO_CONTEXT;
|
||||
self->flutter_share_context = EGL_NO_CONTEXT;
|
||||
self->flutter_display = EGL_NO_DISPLAY;
|
||||
}
|
||||
|
||||
void RetireIncompleteCandidate(MpvTexture* self, const TextureResources& candidate) {
|
||||
if (!ResourceSetEmpty(candidate)) self->retired->push_back(candidate);
|
||||
}
|
||||
|
||||
void CleanupResourceSet(
|
||||
MpvTexture* self, TextureResources* resources, EGLDisplay flutter_display, EGLSurface flutter_draw,
|
||||
EGLSurface flutter_read, EGLContext flutter_context, EGLenum flutter_api) {
|
||||
const EGLDisplay mpv_display = self->player ? self->player->GetEglDisplay() : EGL_NO_DISPLAY;
|
||||
const EGLContext mpv_context = self->player ? self->player->GetEglContext() : EGL_NO_CONTEXT;
|
||||
|
||||
if (resources->egl_image != EGL_NO_IMAGE_KHR && mpv_display != EGL_NO_DISPLAY && self->image_dispatch &&
|
||||
*self->image_dispatch) {
|
||||
if (self->image_dispatch->Destroy(mpv_display, resources->egl_image)) {
|
||||
resources->egl_image = EGL_NO_IMAGE_KHR;
|
||||
} else {
|
||||
g_warning("MPV texture: failed to destroy EGL image: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
|
||||
if (resources->flutter_texture) {
|
||||
const bool flutter_current = flutter_display == self->flutter_display &&
|
||||
flutter_context == self->flutter_share_context && flutter_context != EGL_NO_CONTEXT &&
|
||||
eglGetCurrentContext() == self->flutter_share_context;
|
||||
const bool cleanup_current =
|
||||
!flutter_current && self->flutter_display != EGL_NO_DISPLAY &&
|
||||
self->flutter_cleanup_context != EGL_NO_CONTEXT && eglBindAPI(EGL_OPENGL_ES_API) &&
|
||||
eglMakeCurrent(self->flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->flutter_cleanup_context);
|
||||
if (flutter_current || cleanup_current) {
|
||||
glDeleteTextures(1, &resources->flutter_texture);
|
||||
resources->flutter_texture = 0;
|
||||
} else {
|
||||
g_warning("MPV texture: failed to activate Flutter cleanup context: 0x%x", eglGetError());
|
||||
}
|
||||
if (cleanup_current) {
|
||||
RestoreOrReleaseContext(
|
||||
flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, self->flutter_display);
|
||||
}
|
||||
}
|
||||
|
||||
if (resources->mpv_fbo || resources->mpv_texture) {
|
||||
const bool mpv_current = mpv_display != EGL_NO_DISPLAY && mpv_context != EGL_NO_CONTEXT &&
|
||||
eglBindAPI(EGL_OPENGL_ES_API) &&
|
||||
eglMakeCurrent(mpv_display, EGL_NO_SURFACE, EGL_NO_SURFACE, mpv_context);
|
||||
if (mpv_current) {
|
||||
if (resources->mpv_fbo) glDeleteFramebuffers(1, &resources->mpv_fbo);
|
||||
if (resources->mpv_texture) glDeleteTextures(1, &resources->mpv_texture);
|
||||
resources->mpv_fbo = 0;
|
||||
resources->mpv_texture = 0;
|
||||
} else {
|
||||
g_warning("MPV texture: failed to activate EGL context during cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
RestoreOrReleaseContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, mpv_display);
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupRetired(MpvTexture* self) {
|
||||
if (!self->retired || self->retired->empty() || !self->player) return;
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum flutter_api = eglQueryAPI();
|
||||
for (auto& resources : *self->retired) {
|
||||
CleanupResourceSet(self, &resources, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
}
|
||||
auto& retired = *self->retired;
|
||||
retired.erase(
|
||||
std::remove_if(
|
||||
retired.begin(), retired.end(),
|
||||
[](const TextureResources& resources) { return ResourceSetEmpty(resources); }),
|
||||
retired.end());
|
||||
}
|
||||
|
||||
bool EnsureTextures(MpvTexture* self, int32_t width, int32_t height, GError** error) {
|
||||
if (self->active->complete() && self->active->width == width && self->active->height == height) return true;
|
||||
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum flutter_api = eglQueryAPI();
|
||||
const EGLDisplay mpv_display = self->player->GetEglDisplay();
|
||||
const EGLContext mpv_context = self->player->GetEglContext();
|
||||
if (flutter_display == EGL_NO_DISPLAY || flutter_context == EGL_NO_CONTEXT || mpv_display == EGL_NO_DISPLAY ||
|
||||
mpv_context == EGL_NO_CONTEXT) {
|
||||
return SetError(error, "Video EGL contexts are unavailable");
|
||||
}
|
||||
|
||||
if (!EnsureFlutterCleanupContext(self, flutter_display, flutter_context, flutter_api, error)) return false;
|
||||
|
||||
if (!*self->image_dispatch) {
|
||||
std::string dispatch_error;
|
||||
if (!mpv::ResolveGpuImageDispatch(flutter_display, self->image_dispatch, &dispatch_error)) {
|
||||
g_warning("MPV texture: GPU bootstrap rejected: %s", dispatch_error.c_str());
|
||||
return SetError(error, dispatch_error.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
TextureResources candidate;
|
||||
candidate.width = width;
|
||||
candidate.height = height;
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) || !eglMakeCurrent(mpv_display, EGL_NO_SURFACE, EGL_NO_SURFACE, mpv_context)) {
|
||||
RestoreOrReleaseContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, mpv_display);
|
||||
return SetError(error, "Failed to activate video EGL context");
|
||||
}
|
||||
|
||||
ClearGlErrors();
|
||||
glGenTextures(1, &candidate.mpv_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, candidate.mpv_texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glGenFramebuffers(1, &candidate.mpv_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, candidate.mpv_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, candidate.mpv_texture, 0);
|
||||
bool framebuffer_complete = candidate.mpv_texture != 0 && candidate.mpv_fbo != 0 &&
|
||||
glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE &&
|
||||
glGetError() == GL_NO_ERROR;
|
||||
if (framebuffer_complete) {
|
||||
candidate.egl_image = self->image_dispatch->Create(
|
||||
mpv_display, mpv_context, reinterpret_cast<EGLClientBuffer>(static_cast<uintptr_t>(candidate.mpv_texture)));
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glFlush();
|
||||
framebuffer_complete = framebuffer_complete && glGetError() == GL_NO_ERROR;
|
||||
|
||||
if (!RestoreContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, error)) {
|
||||
CleanupResourceSet(self, &candidate, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
RetireIncompleteCandidate(self, candidate);
|
||||
return false;
|
||||
}
|
||||
if (!framebuffer_complete || candidate.egl_image == EGL_NO_IMAGE_KHR) {
|
||||
CleanupResourceSet(self, &candidate, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
RetireIncompleteCandidate(self, candidate);
|
||||
return SetError(error, "Failed to create a complete video framebuffer");
|
||||
}
|
||||
|
||||
ClearGlErrors();
|
||||
glGenTextures(1, &candidate.flutter_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, candidate.flutter_texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
self->image_dispatch->image_target_texture(GL_TEXTURE_2D, reinterpret_cast<GLeglImageOES>(candidate.egl_image));
|
||||
const bool flutter_texture_complete = candidate.flutter_texture != 0 && glGetError() == GL_NO_ERROR;
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (!flutter_texture_complete) {
|
||||
CleanupResourceSet(self, &candidate, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
RetireIncompleteCandidate(self, candidate);
|
||||
return SetError(error, "Failed to bind the shared video image");
|
||||
}
|
||||
|
||||
if (self->active->complete()) self->retired->push_back(*self->active);
|
||||
*self->active = candidate;
|
||||
CleanupRetired(self);
|
||||
return true;
|
||||
}
|
||||
|
||||
static gboolean MpvTexturePopulate(
|
||||
FlTextureGL* texture, uint32_t* target, uint32_t* name, uint32_t* width, uint32_t* height, GError** error) {
|
||||
MpvTexture* self = MPV_TEXTURE(texture);
|
||||
g_mutex_lock(&self->mutex);
|
||||
if (self->disposed || !self->player) {
|
||||
g_object_ref(self);
|
||||
g_mutex_unlock(&self->mutex);
|
||||
SignalBootstrap(self, FALSE, "Video texture was disposed");
|
||||
g_object_unref(self);
|
||||
return SetError(error, "Video texture was disposed");
|
||||
}
|
||||
if (!self->player->HasRenderContext() && !self->player->InitRenderContext()) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return SetError(error, "Failed to create video render context");
|
||||
}
|
||||
|
||||
GtkAllocation allocation;
|
||||
gtk_widget_get_allocation(GTK_WIDGET(self->view), &allocation);
|
||||
const int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self->view));
|
||||
const int32_t requested_width = allocation.width * scale;
|
||||
const int32_t requested_height = allocation.height * scale;
|
||||
if (requested_width <= 0 || requested_height <= 0) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return SetError(error, "Video surface has no drawable size");
|
||||
}
|
||||
// GL/EGL failures during the first populate are not terminal. Flutter may
|
||||
// call populate again while waitForVideoReady owns the bounded deadline.
|
||||
if (!EnsureTextures(self, requested_width, requested_height, error)) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum flutter_api = eglQueryAPI();
|
||||
const EGLDisplay mpv_display = self->player->GetEglDisplay();
|
||||
const EGLContext mpv_context = self->player->GetEglContext();
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) || !eglMakeCurrent(mpv_display, EGL_NO_SURFACE, EGL_NO_SURFACE, mpv_context)) {
|
||||
RestoreOrReleaseContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, mpv_display);
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return SetError(error, "Failed to activate video EGL context");
|
||||
}
|
||||
|
||||
ClearGlErrors();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, self->active->mpv_fbo);
|
||||
self->player->ClearRedrawFlag();
|
||||
self->player->Render(requested_width, requested_height, static_cast<int>(self->active->mpv_fbo));
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glFlush();
|
||||
const bool render_succeeded = glGetError() == GL_NO_ERROR;
|
||||
if (!RestoreContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, error)) {
|
||||
RestoreOrReleaseContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, mpv_display);
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return FALSE;
|
||||
}
|
||||
if (!render_succeeded) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return SetError(error, "Video render operation failed");
|
||||
}
|
||||
|
||||
*target = GL_TEXTURE_2D;
|
||||
*name = self->active->flutter_texture;
|
||||
*width = static_cast<uint32_t>(requested_width);
|
||||
*height = static_cast<uint32_t>(requested_height);
|
||||
g_object_ref(self);
|
||||
g_mutex_unlock(&self->mutex);
|
||||
SignalBootstrap(self, TRUE, nullptr);
|
||||
g_object_unref(self);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void MpvTextureFinalize(GObject* object) {
|
||||
MpvTexture* self = MPV_TEXTURE(object);
|
||||
if (self->ready_destroy_notify && self->ready_user_data) {
|
||||
self->ready_destroy_notify(self->ready_user_data);
|
||||
}
|
||||
g_free(self->bootstrap_error);
|
||||
delete self->active;
|
||||
delete self->retired;
|
||||
delete self->image_dispatch;
|
||||
g_mutex_clear(&self->bootstrap_mutex);
|
||||
g_mutex_clear(&self->mutex);
|
||||
G_OBJECT_CLASS(mpv_texture_parent_class)->finalize(object);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void mpv_texture_class_init(MpvTextureClass* klass) {
|
||||
FL_TEXTURE_GL_CLASS(klass)->populate = MpvTexturePopulate;
|
||||
G_OBJECT_CLASS(klass)->finalize = MpvTextureFinalize;
|
||||
}
|
||||
|
||||
static void mpv_texture_init(MpvTexture* self) {
|
||||
self->player = nullptr;
|
||||
self->registrar = nullptr;
|
||||
self->view = nullptr;
|
||||
g_mutex_init(&self->mutex);
|
||||
self->disposed = false;
|
||||
self->active = new TextureResources();
|
||||
self->retired = new std::vector<TextureResources>();
|
||||
self->image_dispatch = new mpv::GpuImageDispatch();
|
||||
self->flutter_display = EGL_NO_DISPLAY;
|
||||
self->flutter_share_context = EGL_NO_CONTEXT;
|
||||
self->flutter_cleanup_context = EGL_NO_CONTEXT;
|
||||
g_mutex_init(&self->bootstrap_mutex);
|
||||
self->bootstrap_state = 0;
|
||||
self->bootstrap_error = nullptr;
|
||||
self->ready_callback = nullptr;
|
||||
self->ready_user_data = nullptr;
|
||||
self->ready_destroy_notify = nullptr;
|
||||
}
|
||||
|
||||
MpvTexture* mpv_texture_new(mpv::MpvPlayer* player, FlTextureRegistrar* registrar, FlView* view) {
|
||||
MpvTexture* self = MPV_TEXTURE(g_object_new(MPV_TEXTURE_TYPE, nullptr));
|
||||
self->player = player;
|
||||
self->registrar = registrar;
|
||||
self->view = view;
|
||||
return self;
|
||||
}
|
||||
|
||||
void mpv_texture_set_ready_callback(
|
||||
MpvTexture* self, MpvTextureReadyCallback callback, gpointer user_data, GDestroyNotify destroy_notify) {
|
||||
gboolean success = FALSE;
|
||||
const gchar* message = nullptr;
|
||||
bool complete = false;
|
||||
g_mutex_lock(&self->bootstrap_mutex);
|
||||
self->ready_callback = callback;
|
||||
self->ready_user_data = user_data;
|
||||
self->ready_destroy_notify = destroy_notify;
|
||||
if (self->bootstrap_state != 0) {
|
||||
complete = true;
|
||||
success = self->bootstrap_state == 1;
|
||||
message = self->bootstrap_error;
|
||||
}
|
||||
g_mutex_unlock(&self->bootstrap_mutex);
|
||||
if (complete && callback) callback(success, message, user_data);
|
||||
}
|
||||
|
||||
void mpv_texture_mark_frame_available(MpvTexture* self) {
|
||||
if (!self) return;
|
||||
g_mutex_lock(&self->mutex);
|
||||
FlTextureRegistrar* registrar = self->disposed ? nullptr : self->registrar;
|
||||
g_mutex_unlock(&self->mutex);
|
||||
if (registrar) {
|
||||
fl_texture_registrar_mark_texture_frame_available(registrar, FL_TEXTURE(self));
|
||||
}
|
||||
}
|
||||
|
||||
void mpv_texture_dispose(MpvTexture* self) {
|
||||
if (!self) return;
|
||||
g_mutex_lock(&self->mutex);
|
||||
if (self->disposed) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return;
|
||||
}
|
||||
self->disposed = true;
|
||||
SignalBootstrap(self, FALSE, "Video initialization was cancelled");
|
||||
|
||||
if (self->player) {
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum flutter_api = eglQueryAPI();
|
||||
CleanupResourceSet(self, self->active, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
for (auto& resources : *self->retired) {
|
||||
CleanupResourceSet(self, &resources, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
}
|
||||
DestroyFlutterCleanupContext(self);
|
||||
const auto leaked_sets =
|
||||
static_cast<size_t>(!ResourceSetEmpty(*self->active)) +
|
||||
static_cast<size_t>(std::count_if(self->retired->begin(), self->retired->end(), [](const auto& resources) {
|
||||
return !ResourceSetEmpty(resources);
|
||||
}));
|
||||
if (leaked_sets != 0) {
|
||||
g_warning("MPV texture: %zu resource set(s) could not be released before disposal", leaked_sets);
|
||||
}
|
||||
}
|
||||
*self->active = TextureResources{};
|
||||
self->retired->clear();
|
||||
self->player = nullptr;
|
||||
self->registrar = nullptr;
|
||||
self->view = nullptr;
|
||||
g_mutex_unlock(&self->mutex);
|
||||
}
|
||||
|
||||
int64_t mpv_texture_get_id(MpvTexture* self) { return fl_texture_get_id(FL_TEXTURE(self)); }
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef MPV_TEXTURE_H_
|
||||
#define MPV_TEXTURE_H_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
#include "mpv_player.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define MPV_TEXTURE_TYPE (mpv_texture_get_type())
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MpvTexture, mpv_texture, MPV, TEXTURE, FlTextureGL)
|
||||
|
||||
/// Creates a new MpvTexture that renders mpv video to an offscreen FBO.
|
||||
MpvTexture* mpv_texture_new(mpv::MpvPlayer* player, FlTextureRegistrar* registrar, FlView* view);
|
||||
|
||||
typedef void (*MpvTextureReadyCallback)(gboolean success, const gchar* error_message, gpointer user_data);
|
||||
|
||||
/// Installs the one-shot video bootstrap result callback.
|
||||
void mpv_texture_set_ready_callback(
|
||||
MpvTexture* self, MpvTextureReadyCallback callback, gpointer user_data, GDestroyNotify destroy_notify);
|
||||
|
||||
/// Notifies Flutter that a new frame is available.
|
||||
void mpv_texture_mark_frame_available(MpvTexture* self);
|
||||
|
||||
/// Cleans up GL resources (FBO/texture).
|
||||
void mpv_texture_dispose(MpvTexture* self);
|
||||
|
||||
/// Returns the Flutter texture ID.
|
||||
int64_t mpv_texture_get_id(MpvTexture* self);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif // MPV_TEXTURE_H_
|
||||
@@ -12,7 +12,9 @@
|
||||
// On Wayland the mpv video plane is a wl_subsurface stacked *below* this
|
||||
// window's surface, so the window needs an alpha channel for it to show
|
||||
// through. Harmless when the plane is unavailable: the Flutter UI paints
|
||||
// opaque anyway, and X11 sessions refuse video at initialize by design.
|
||||
// opaque anyway, and sessions that cannot host the plane (X11/XWayland)
|
||||
// fall back to the Flutter-texture path, which composites through Flutter
|
||||
// and needs no alpha channel.
|
||||
static void enable_video_plane_transparency(GtkWindow* window, FlView* view) {
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(window));
|
||||
@@ -85,8 +87,9 @@ static void my_application_activate(GApplication* application) {
|
||||
// Register Flutter plugins.
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(self->flutter_view));
|
||||
|
||||
// Register the MPV plugin. Video goes to the native Wayland plane; there is no other path, so a session that
|
||||
// cannot host one is refused by name rather than played into nothing.
|
||||
// Register the MPV plugin. Video prefers the native Wayland plane; sessions
|
||||
// that cannot host one (X11/XWayland, or a failed plane bootstrap) fall back
|
||||
// to the Flutter-texture path in SDR.
|
||||
FlPluginRegistrar* registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(self->flutter_view), "MpvPlugin");
|
||||
mpv_plugin_register_with_registrar(registrar);
|
||||
|
||||
Reference in New Issue
Block a user