mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-21 18:48:19 +00:00
Compare commits
3 Commits
chromecast
...
I10n_crowd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ee43de305 | ||
|
|
c7077bbcfe | ||
|
|
c0f25a2b8b |
@@ -449,7 +449,7 @@ export default function page() {
|
||||
async (data: { nativeEvent: MpvOnProgressEventPayload }) => {
|
||||
if (isSeeking.get() || isPlaybackStopped) return;
|
||||
|
||||
const { position } = data.nativeEvent;
|
||||
const { position, cacheSeconds } = data.nativeEvent;
|
||||
// MPV reports position in seconds, convert to ms
|
||||
const currentTime = position * 1000;
|
||||
|
||||
@@ -459,6 +459,12 @@ export default function page() {
|
||||
|
||||
progress.set(currentTime);
|
||||
|
||||
// Update cache progress (current position + buffered seconds ahead)
|
||||
if (cacheSeconds !== undefined && cacheSeconds > 0) {
|
||||
const cacheEnd = currentTime + cacheSeconds * 1000;
|
||||
cacheProgress.set(cacheEnd);
|
||||
}
|
||||
|
||||
// Update URL immediately after seeking, or every 30 seconds during normal playback
|
||||
const now = Date.now();
|
||||
const shouldUpdateUrl = wasJustSeeking.get();
|
||||
|
||||
BIN
modules/mpv-player/android/src/main/assets/subfont.ttf
Normal file
BIN
modules/mpv-player/android/src/main/assets/subfont.ttf
Normal file
Binary file not shown.
@@ -1,10 +1,13 @@
|
||||
package expo.modules.mpvplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.AssetManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/**
|
||||
* MPV renderer that wraps libmpv for video playback.
|
||||
@@ -26,7 +29,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
}
|
||||
|
||||
interface Delegate {
|
||||
fun onPositionChanged(position: Double, duration: Double)
|
||||
fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double)
|
||||
fun onPauseChanged(isPaused: Boolean)
|
||||
fun onLoadingChanged(isLoading: Boolean)
|
||||
fun onReadyToSeek()
|
||||
@@ -46,6 +49,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
// Cached state
|
||||
private var cachedPosition: Double = 0.0
|
||||
private var cachedDuration: Double = 0.0
|
||||
private var cachedCacheSeconds: Double = 0.0
|
||||
private var _isPaused: Boolean = true
|
||||
private var _isLoading: Boolean = false
|
||||
private var _playbackSpeed: Double = 1.0
|
||||
@@ -101,6 +105,52 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
MPVLib.create(context)
|
||||
MPVLib.addObserver(this)
|
||||
|
||||
/**
|
||||
* Create mpv config directory and copy font files to ensure SubRip subtitles load properly on Android.
|
||||
*
|
||||
* Technical Background:
|
||||
* ====================
|
||||
* On Android, mpv requires access to a font file to render text-based subtitles, particularly SubRip (.srt)
|
||||
* format subtitles. Without an available font in the config directory, mpv will fail to display subtitles
|
||||
* even when subtitle tracks are properly detected and loaded.
|
||||
*
|
||||
* Why This Is Necessary:
|
||||
* =====================
|
||||
* 1. Android's font system is isolated from native libraries like mpv. While Android has system fonts,
|
||||
* mpv cannot access them directly due to sandboxing and library isolation.
|
||||
*
|
||||
* 2. SubRip subtitles require a font to render text overlay on video. When no font is available in the
|
||||
* configured directory, mpv either:
|
||||
* - Fails silently (subtitles don't appear)
|
||||
* - Falls back to a default font that may not support the required character set
|
||||
* - Crashes or produces rendering errors
|
||||
*
|
||||
* 3. By placing a font file (font.ttf) in mpv's config directory and setting that directory via
|
||||
* MPVLib.setOptionString("config-dir", ...), we ensure mpv has a known, accessible font source.
|
||||
*
|
||||
* Reference:
|
||||
* =========
|
||||
* This workaround is documented in the mpv-android project:
|
||||
* https://github.com/mpv-android/mpv-android/issues/96
|
||||
*
|
||||
* The issue discusses that without a font in the config directory, SubRip subtitles fail to load
|
||||
* properly on Android, and the solution is to copy a font file to a known location that mpv can access.
|
||||
*/
|
||||
// Create mpv config directory and copy font files
|
||||
val mpvDir = File(context.getExternalFilesDir(null) ?: context.filesDir, "mpv")
|
||||
//Log.i(TAG, "mpv config dir: $mpvDir")
|
||||
if (!mpvDir.exists()) mpvDir.mkdirs()
|
||||
// This needs to be named `subfont.ttf` else it won't work
|
||||
arrayOf("subfont.ttf").forEach { fileName ->
|
||||
val file = File(mpvDir, fileName)
|
||||
if (file.exists()) return@forEach
|
||||
context.assets
|
||||
.open(fileName, AssetManager.ACCESS_STREAMING)
|
||||
.copyTo(FileOutputStream(file))
|
||||
}
|
||||
MPVLib.setOptionString("config", "yes")
|
||||
MPVLib.setOptionString("config-dir", mpvDir.path)
|
||||
|
||||
// Configure mpv options before initialization (based on Findroid)
|
||||
MPVLib.setOptionString("vo", "gpu")
|
||||
MPVLib.setOptionString("gpu-context", "android")
|
||||
@@ -124,7 +174,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
MPVLib.setOptionString("hr-seek-framedrop", "yes")
|
||||
|
||||
// Subtitle settings
|
||||
MPVLib.setOptionString("sub-scale-with-window", "yes")
|
||||
MPVLib.setOptionString("sub-scale-with-window", "no")
|
||||
MPVLib.setOptionString("sub-use-margins", "no")
|
||||
MPVLib.setOptionString("subs-match-os-language", "yes")
|
||||
MPVLib.setOptionString("subs-fallback", "yes")
|
||||
@@ -283,6 +333,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
MPVLib.observeProperty("pause", MPV_FORMAT_FLAG)
|
||||
MPVLib.observeProperty("track-list/count", MPV_FORMAT_INT64)
|
||||
MPVLib.observeProperty("paused-for-cache", MPV_FORMAT_FLAG)
|
||||
MPVLib.observeProperty("demuxer-cache-duration", MPV_FORMAT_DOUBLE)
|
||||
// Video dimensions for PiP aspect ratio
|
||||
MPVLib.observeProperty("video-params/w", MPV_FORMAT_INT64)
|
||||
MPVLib.observeProperty("video-params/h", MPV_FORMAT_INT64)
|
||||
@@ -561,7 +612,7 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
when (property) {
|
||||
"duration" -> {
|
||||
cachedDuration = value
|
||||
mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) }
|
||||
mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration, cachedCacheSeconds) }
|
||||
}
|
||||
"time-pos" -> {
|
||||
cachedPosition = value
|
||||
@@ -570,9 +621,12 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
|
||||
val shouldUpdate = _isSeeking || (now - lastProgressUpdateTime >= 1000)
|
||||
if (shouldUpdate) {
|
||||
lastProgressUpdateTime = now
|
||||
mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration) }
|
||||
mainHandler.post { delegate?.onPositionChanged(cachedPosition, cachedDuration, cachedCacheSeconds) }
|
||||
}
|
||||
}
|
||||
"demuxer-cache-duration" -> {
|
||||
cachedCacheSeconds = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -307,7 +307,7 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
|
||||
// MARK: - MPVLayerRenderer.Delegate
|
||||
|
||||
override fun onPositionChanged(position: Double, duration: Double) {
|
||||
override fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) {
|
||||
cachedPosition = position
|
||||
cachedDuration = duration
|
||||
|
||||
@@ -319,7 +319,8 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
||||
onProgress(mapOf(
|
||||
"position" to position,
|
||||
"duration" to duration,
|
||||
"progress" to if (duration > 0) position / duration else 0.0
|
||||
"progress" to if (duration > 0) position / duration else 0.0,
|
||||
"cacheSeconds" to cacheSeconds
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import CoreVideo
|
||||
import AVFoundation
|
||||
|
||||
protocol MPVLayerRendererDelegate: AnyObject {
|
||||
func renderer(_ renderer: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double)
|
||||
func renderer(_ renderer: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double, cacheSeconds: Double)
|
||||
func renderer(_ renderer: MPVLayerRenderer, didChangePause isPaused: Bool)
|
||||
func renderer(_ renderer: MPVLayerRenderer, didChangeLoading isLoading: Bool)
|
||||
func renderer(_ renderer: MPVLayerRenderer, didBecomeReadyToSeek: Bool)
|
||||
@@ -44,6 +44,7 @@ final class MPVLayerRenderer {
|
||||
// Thread-safe state for playback
|
||||
private var _cachedDuration: Double = 0
|
||||
private var _cachedPosition: Double = 0
|
||||
private var _cachedCacheSeconds: Double = 0
|
||||
private var _isPaused: Bool = true
|
||||
private var _playbackSpeed: Double = 1.0
|
||||
private var _isLoading: Bool = false
|
||||
@@ -75,6 +76,10 @@ final class MPVLayerRenderer {
|
||||
get { stateQueue.sync { _cachedPosition } }
|
||||
set { stateQueue.async(flags: .barrier) { self._cachedPosition = newValue } }
|
||||
}
|
||||
private var cachedCacheSeconds: Double {
|
||||
get { stateQueue.sync { _cachedCacheSeconds } }
|
||||
set { stateQueue.async(flags: .barrier) { self._cachedCacheSeconds = newValue } }
|
||||
}
|
||||
private var isPaused: Bool {
|
||||
get { stateQueue.sync { _isPaused } }
|
||||
set { stateQueue.async(flags: .barrier) { self._isPaused = newValue } }
|
||||
@@ -162,16 +167,16 @@ final class MPVLayerRenderer {
|
||||
// Use AVFoundation video output - required for PiP support
|
||||
checkError(mpv_set_option_string(handle, "vo", "avfoundation"))
|
||||
|
||||
// Enable composite OSD mode - renders subtitles directly onto video frames using GPU
|
||||
// This is better for PiP as subtitles are baked into the video
|
||||
checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes"))
|
||||
|
||||
// Hardware decoding with VideoToolbox
|
||||
// On simulator, use software decoding since VideoToolbox is not available
|
||||
// On device, use VideoToolbox with software fallback enabled
|
||||
#if targetEnvironment(simulator)
|
||||
checkError(mpv_set_option_string(handle, "hwdec", "no"))
|
||||
#else
|
||||
// Only enable composite OSD mode on real device (OSD is not supported in simulator).
|
||||
// This renders subtitles directly onto video frames using the GPU, which is better for PiP since subtitles are baked into the video.
|
||||
checkError(mpv_set_option_string(handle, "avfoundation-composite-osd", "yes"))
|
||||
|
||||
checkError(mpv_set_option_string(handle, "hwdec", "videotoolbox"))
|
||||
#endif
|
||||
checkError(mpv_set_option_string(handle, "hwdec-codecs", "all"))
|
||||
@@ -340,7 +345,8 @@ final class MPVLayerRenderer {
|
||||
("time-pos", MPV_FORMAT_DOUBLE),
|
||||
("pause", MPV_FORMAT_FLAG),
|
||||
("track-list/count", MPV_FORMAT_INT64),
|
||||
("paused-for-cache", MPV_FORMAT_FLAG)
|
||||
("paused-for-cache", MPV_FORMAT_FLAG),
|
||||
("demuxer-cache-duration", MPV_FORMAT_DOUBLE)
|
||||
]
|
||||
for (name, format) in properties {
|
||||
mpv_observe_property(handle, 0, name, format)
|
||||
@@ -484,7 +490,7 @@ final class MPVLayerRenderer {
|
||||
cachedDuration = value
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration)
|
||||
self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration, cacheSeconds: self.cachedCacheSeconds)
|
||||
}
|
||||
}
|
||||
case "time-pos":
|
||||
@@ -499,10 +505,16 @@ final class MPVLayerRenderer {
|
||||
lastProgressUpdateTime = now
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration)
|
||||
self.delegate?.renderer(self, didUpdatePosition: self.cachedPosition, duration: self.cachedDuration, cacheSeconds: self.cachedCacheSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "demuxer-cache-duration":
|
||||
var value = Double(0)
|
||||
let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_DOUBLE, value: &value)
|
||||
if status >= 0 {
|
||||
cachedCacheSeconds = value
|
||||
}
|
||||
case "pause":
|
||||
var flag: Int32 = 0
|
||||
let status = getProperty(handle: handle, name: name, format: MPV_FORMAT_FLAG, value: &flag)
|
||||
|
||||
@@ -298,7 +298,7 @@ class MpvPlayerView: ExpoView {
|
||||
// MARK: - MPVLayerRendererDelegate
|
||||
|
||||
extension MpvPlayerView: MPVLayerRendererDelegate {
|
||||
func renderer(_: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double) {
|
||||
func renderer(_: MPVLayerRenderer, didUpdatePosition position: Double, duration: Double, cacheSeconds: Double) {
|
||||
cachedPosition = position
|
||||
cachedDuration = duration
|
||||
|
||||
@@ -313,6 +313,7 @@ extension MpvPlayerView: MPVLayerRendererDelegate {
|
||||
"position": position,
|
||||
"duration": duration,
|
||||
"progress": duration > 0 ? position / duration : 0,
|
||||
"cacheSeconds": cacheSeconds,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ export type OnProgressEventPayload = {
|
||||
position: number;
|
||||
duration: number;
|
||||
progress: number;
|
||||
/** Seconds of video buffered ahead of current position */
|
||||
cacheSeconds: number;
|
||||
};
|
||||
|
||||
export type OnErrorEventPayload = {
|
||||
|
||||
@@ -39,39 +39,39 @@
|
||||
"please_login_again": "Su sesión guardada ha caducado. Por favor, inicie sesión de nuevo.",
|
||||
"remove_saved_login": "Eliminar inicio de sesión guardado",
|
||||
"remove_saved_login_description": "Esto eliminará tus credenciales guardadas para este servidor. Tendrás que volver a introducir tu nombre de usuario y contraseña la próxima vez.",
|
||||
"accounts_count": "{{count}} accounts",
|
||||
"select_account": "Select Account",
|
||||
"add_account": "Add Account",
|
||||
"remove_account_description": "This will remove the saved credentials for {{username}}."
|
||||
"accounts_count": "{{count}} cuentas",
|
||||
"select_account": "Seleccione una cuenta",
|
||||
"add_account": "Añadir cuenta",
|
||||
"remove_account_description": "Esto eliminará las credenciales guardadas para {{username}}."
|
||||
},
|
||||
"save_account": {
|
||||
"title": "Save Account",
|
||||
"save_for_later": "Save this account",
|
||||
"security_option": "Security Option",
|
||||
"no_protection": "No protection",
|
||||
"no_protection_desc": "Quick login without authentication",
|
||||
"pin_code": "PIN code",
|
||||
"pin_code_desc": "4-digit PIN required when switching",
|
||||
"password": "Re-enter password",
|
||||
"password_desc": "Password required when switching",
|
||||
"save_button": "Save",
|
||||
"cancel_button": "Cancel"
|
||||
"title": "Guardar Cuenta",
|
||||
"save_for_later": "Guardar esta cuenta",
|
||||
"security_option": "Opciones de seguridad",
|
||||
"no_protection": "Sin Protección",
|
||||
"no_protection_desc": "Inicio de sesión rápido sin autenticación",
|
||||
"pin_code": "Código PIN",
|
||||
"pin_code_desc": "PIN de 4 dígitos requerido al cambiar",
|
||||
"password": "Vuelva a introducir la contraseña",
|
||||
"password_desc": "Contraseña requerida al cambiar",
|
||||
"save_button": "Guardar",
|
||||
"cancel_button": "Cancelar"
|
||||
},
|
||||
"pin": {
|
||||
"enter_pin": "Enter PIN",
|
||||
"enter_pin_for": "Enter PIN for {{username}}",
|
||||
"enter_4_digits": "Enter 4 digits",
|
||||
"invalid_pin": "Invalid PIN",
|
||||
"setup_pin": "Set Up PIN",
|
||||
"confirm_pin": "Confirm PIN",
|
||||
"pins_dont_match": "PINs don't match",
|
||||
"forgot_pin": "Forgot PIN?",
|
||||
"forgot_pin_desc": "Your saved credentials will be removed"
|
||||
"enter_pin": "Introduce el PIN",
|
||||
"enter_pin_for": "Introduzca el PIN para {{username}}",
|
||||
"enter_4_digits": "Introduce 4 dígitos",
|
||||
"invalid_pin": "PIN inválido",
|
||||
"setup_pin": "Configurar PIN",
|
||||
"confirm_pin": "Confirmar PIN",
|
||||
"pins_dont_match": "Los códigos PIN no coinciden",
|
||||
"forgot_pin": "¿Olvidó el PIN?",
|
||||
"forgot_pin_desc": "Sus credenciales guardadas serán eliminadas"
|
||||
},
|
||||
"password": {
|
||||
"enter_password": "Enter Password",
|
||||
"enter_password_for": "Enter password for {{username}}",
|
||||
"invalid_password": "Invalid password"
|
||||
"enter_password": "Introduzca la contraseña",
|
||||
"enter_password_for": "Introduzca la contraseña para {{username}}",
|
||||
"invalid_password": "Contraseña inválida"
|
||||
},
|
||||
"home": {
|
||||
"checking_server_connection": "Comprobando conexión con el servidor...",
|
||||
@@ -124,32 +124,32 @@
|
||||
"hide_remote_session_button": "Ocultar botón de sesión remota"
|
||||
},
|
||||
"network": {
|
||||
"title": "Network",
|
||||
"local_network": "Local Network",
|
||||
"auto_switch_enabled": "Auto-switch when at home",
|
||||
"auto_switch_description": "Automatically switch to local URL when connected to home WiFi",
|
||||
"local_url": "Local URL",
|
||||
"local_url_hint": "Enter your local server address (e.g., http://192.168.1.100:8096)",
|
||||
"title": "Cadena",
|
||||
"local_network": "Red local",
|
||||
"auto_switch_enabled": "Cambiar automáticamente en casa",
|
||||
"auto_switch_description": "Cambiar automáticamente a la URL local cuando se conecta a la WiFi de casa",
|
||||
"local_url": "URL local",
|
||||
"local_url_hint": "Introduzca la dirección de su servidor local (por ejemplo, http://192.168.1.100:8096)",
|
||||
"local_url_placeholder": "http://192.168.1.100:8096",
|
||||
"home_wifi_networks": "Home WiFi Networks",
|
||||
"add_current_network": "Add \"{{ssid}}\"",
|
||||
"not_connected_to_wifi": "Not connected to WiFi",
|
||||
"no_networks_configured": "No networks configured",
|
||||
"add_network_hint": "Add your home WiFi network to enable auto-switching",
|
||||
"current_wifi": "Current WiFi",
|
||||
"using_url": "Using",
|
||||
"local": "Local URL",
|
||||
"remote": "Remote URL",
|
||||
"not_connected": "Not connected",
|
||||
"current_server": "Current Server",
|
||||
"remote_url": "Remote URL",
|
||||
"active_url": "Active URL",
|
||||
"not_configured": "Not configured",
|
||||
"network_added": "Network added",
|
||||
"network_already_added": "Network already added",
|
||||
"no_wifi_connected": "Not connected to WiFi",
|
||||
"permission_denied": "Location permission denied",
|
||||
"permission_denied_explanation": "Location permission is required to detect WiFi network for auto-switching. Please enable it in Settings."
|
||||
"home_wifi_networks": "Redes WiFi domésticas",
|
||||
"add_current_network": "Añadir \"{{ssid}}\"",
|
||||
"not_connected_to_wifi": "No está conectado a WiFi",
|
||||
"no_networks_configured": "No hay redes configuradas",
|
||||
"add_network_hint": "Añade tu red WiFi doméstica para activar el cambio automático",
|
||||
"current_wifi": "WiFi actual",
|
||||
"using_url": "Utilizando",
|
||||
"local": "URL local",
|
||||
"remote": "URL Remota",
|
||||
"not_connected": "Sin conexión",
|
||||
"current_server": "Servidor actual",
|
||||
"remote_url": "URL Remota",
|
||||
"active_url": "URL Activa",
|
||||
"not_configured": "Sin configurar",
|
||||
"network_added": "Red añadida",
|
||||
"network_already_added": "Red ya añadida",
|
||||
"no_wifi_connected": "Sin conexión a WiFi",
|
||||
"permission_denied": "Permiso de ubicación denegado",
|
||||
"permission_denied_explanation": "Se necesita el permiso de ubicación para detectar la red WiFi para cambiar automáticamente. Por favor, actívala en Configuración."
|
||||
},
|
||||
"user_info": {
|
||||
"user_info_title": "Información de usuario",
|
||||
@@ -195,12 +195,12 @@
|
||||
"none": "Ninguno",
|
||||
"language": "Idioma",
|
||||
"transcode_mode": {
|
||||
"title": "Audio Transcoding",
|
||||
"description": "Controls how surround audio (7.1, TrueHD, DTS-HD) is handled",
|
||||
"title": "Transcodificación de audio",
|
||||
"description": "Controla cómo el audio envolvente (7.1, TrueHD, DTS-HD) es manejado",
|
||||
"auto": "Auto",
|
||||
"stereo": "Force Stereo",
|
||||
"5_1": "Allow 5.1",
|
||||
"passthrough": "Passthrough"
|
||||
"stereo": "Forzar salida estéreo",
|
||||
"5_1": "Permitir 5.1",
|
||||
"passthrough": "Directo"
|
||||
}
|
||||
},
|
||||
"subtitles": {
|
||||
@@ -259,16 +259,16 @@
|
||||
"hardware_decode_description": "Utilizar la aceleración de hardware para la decodificación de vídeo. Deshabilite si experimenta problemas de reproducción."
|
||||
},
|
||||
"vlc_subtitles": {
|
||||
"title": "VLC Subtitle Settings",
|
||||
"hint": "Customize subtitle appearance for VLC player. Changes take effect on next playback.",
|
||||
"text_color": "Text Color",
|
||||
"background_color": "Background Color",
|
||||
"background_opacity": "Background Opacity",
|
||||
"outline_color": "Outline Color",
|
||||
"outline_opacity": "Outline Opacity",
|
||||
"outline_thickness": "Outline Thickness",
|
||||
"bold": "Bold Text",
|
||||
"margin": "Bottom Margin"
|
||||
"title": "Configuración de subtítulos VLC",
|
||||
"hint": "Personalizar la apariencia de los subtítulos para el reproductor VLC. Los cambios tendrán efecto en la próxima reproducción.",
|
||||
"text_color": "Color del texto",
|
||||
"background_color": "Color del fondo",
|
||||
"background_opacity": "Opacidad del fondo",
|
||||
"outline_color": "Color del contorno",
|
||||
"outline_opacity": "Opacidad del contorno",
|
||||
"outline_thickness": "Grosor del contorno",
|
||||
"bold": "Texto en negrita",
|
||||
"margin": "Margen inferior"
|
||||
},
|
||||
"video_player": {
|
||||
"title": "Reproductor de vídeo",
|
||||
@@ -300,13 +300,13 @@
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"show_custom_menu_links": "Mostrar enlaces de menú personalizados",
|
||||
"show_large_home_carousel": "Show Large Home Carousel (beta)",
|
||||
"show_large_home_carousel": "Mostrar carrusel del menú principal grande (beta)",
|
||||
"hide_libraries": "Ocultar bibliotecas",
|
||||
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
|
||||
"disable_haptic_feedback": "Desactivar feedback háptico",
|
||||
"default_quality": "Calidad por defecto",
|
||||
"default_playback_speed": "Velocidad de reproducción predeterminada",
|
||||
"auto_play_next_episode": "Auto-play Next Episode",
|
||||
"auto_play_next_episode": "Reproducir automáticamente el siguiente episodio",
|
||||
"max_auto_play_episode_count": "Máximo número de episodios de Auto Play",
|
||||
"disabled": "Deshabilitado"
|
||||
},
|
||||
@@ -317,10 +317,10 @@
|
||||
"title": "Música",
|
||||
"playback_title": "Reproducir",
|
||||
"playback_description": "Configurar cómo se reproduce la música.",
|
||||
"prefer_downloaded": "Prefer Downloaded Songs",
|
||||
"prefer_downloaded": "Preferir las canciones descargadas",
|
||||
"caching_title": "Almacenando en caché",
|
||||
"caching_description": "Automatically cache upcoming tracks for smoother playback.",
|
||||
"lookahead_enabled": "Enable Look-Ahead Caching",
|
||||
"caching_description": "Cachear automáticamente las próximas canciones para una reproducción más suave.",
|
||||
"lookahead_enabled": "Activar el look-Ahead Cache",
|
||||
"lookahead_count": "",
|
||||
"max_cache_size": "Tamaño máximo del caché"
|
||||
},
|
||||
@@ -399,7 +399,7 @@
|
||||
"size_used": "{{used}} de {{total}} usado",
|
||||
"delete_all_downloaded_files": "Eliminar todos los archivos descargados",
|
||||
"music_cache_title": "Caché de música",
|
||||
"music_cache_description": "Automatically cache songs as you listen for smoother playback and offline support",
|
||||
"music_cache_description": "Cachear automáticamente las canciones mientras escuchas una reproducción más suave y soporte sin conexión",
|
||||
"enable_music_cache": "Activar Caché de Música",
|
||||
"clear_music_cache": "Borrar Caché de Música",
|
||||
"music_cache_size": "Caché {{Tamaño}}",
|
||||
@@ -504,10 +504,10 @@
|
||||
"delete": "Borrar",
|
||||
"ok": "Aceptar",
|
||||
"remove": "Eliminar",
|
||||
"next": "Next",
|
||||
"back": "Back",
|
||||
"continue": "Continue",
|
||||
"verifying": "Verifying..."
|
||||
"next": "Siguiente",
|
||||
"back": "Atrás",
|
||||
"continue": "Continuar",
|
||||
"verifying": "Verificando..."
|
||||
},
|
||||
"search": {
|
||||
"search": "Buscar...",
|
||||
@@ -753,8 +753,8 @@
|
||||
"downloaded": "Descargado",
|
||||
"downloading": "Descargando...",
|
||||
"cached": "En caché",
|
||||
"delete_download": "Delete Download",
|
||||
"delete_cache": "Remove from Cache",
|
||||
"delete_download": "Eliminar descarga",
|
||||
"delete_cache": "Borrar del caché",
|
||||
"go_to_artist": "Ir al artista",
|
||||
"go_to_album": "Ir al álbum",
|
||||
"add_to_favorites": "Añadir a Favoritos",
|
||||
|
||||
@@ -305,8 +305,8 @@
|
||||
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.",
|
||||
"disable_haptic_feedback": "Désactiver le retour haptique",
|
||||
"default_quality": "Qualité par défaut",
|
||||
"default_playback_speed": "Default Playback Speed",
|
||||
"auto_play_next_episode": "Auto-play Next Episode",
|
||||
"default_playback_speed": "Vitesse de lecture par défaut",
|
||||
"auto_play_next_episode": "Lecture automatique de l'épisode suivant",
|
||||
"max_auto_play_episode_count": "Nombre d'épisodes en lecture automatique max",
|
||||
"disabled": "Désactivé"
|
||||
},
|
||||
@@ -314,15 +314,15 @@
|
||||
"downloads_title": "Téléchargements"
|
||||
},
|
||||
"music": {
|
||||
"title": "Music",
|
||||
"playback_title": "Playback",
|
||||
"playback_description": "Configure how music is played.",
|
||||
"prefer_downloaded": "Prefer Downloaded Songs",
|
||||
"caching_title": "Caching",
|
||||
"caching_description": "Automatically cache upcoming tracks for smoother playback.",
|
||||
"lookahead_enabled": "Enable Look-Ahead Caching",
|
||||
"lookahead_count": "Tracks to Pre-cache",
|
||||
"max_cache_size": "Max Cache Size"
|
||||
"title": "Musique",
|
||||
"playback_title": "Lecture",
|
||||
"playback_description": "Configurer le mode de lecture de la musique.",
|
||||
"prefer_downloaded": "Supprimer toutes les musiques téléchargées",
|
||||
"caching_title": "Mise en cache",
|
||||
"caching_description": "Mettre automatiquement en cache les pistes à venir pour une lecture plus fluide.",
|
||||
"lookahead_enabled": "Activer la mise en cache guidée",
|
||||
"lookahead_count": "Pistes à pré-mettre en cache",
|
||||
"max_cache_size": "Taille max de cache"
|
||||
},
|
||||
"plugins": {
|
||||
"plugins_title": "Plugins",
|
||||
@@ -357,19 +357,19 @@
|
||||
"save_button": "Enregistrer",
|
||||
"toasts": {
|
||||
"saved": "Enregistré",
|
||||
"refreshed": "Settings refreshed from server"
|
||||
"refreshed": "Paramètres actualisés depuis le serveur"
|
||||
},
|
||||
"refresh_from_server": "Refresh Settings from Server"
|
||||
"refresh_from_server": "Rafraîchir les paramètres depuis le serveur"
|
||||
},
|
||||
"streamystats": {
|
||||
"enable_streamystats": "Enable Streamystats",
|
||||
"disable_streamystats": "Disable Streamystats",
|
||||
"enable_search": "Use for Search",
|
||||
"enable_streamystats": "Activer Streamystats",
|
||||
"disable_streamystats": "Désactiver Streamystats",
|
||||
"enable_search": "Utiliser pour la recherche",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://streamystats.example.com",
|
||||
"streamystats_search_hint": "Enter the URL for your Streamystats server. The URL should include http or https and optionally the port.",
|
||||
"read_more_about_streamystats": "Read More About Streamystats.",
|
||||
"save_button": "Save",
|
||||
"streamystats_search_hint": "Entrez l'URL de votre serveur Streamystats. L'URL doit inclure http ou https et éventuellement le port.",
|
||||
"read_more_about_streamystats": "En savoir plus sur Streamystats.",
|
||||
"save_button": "Enregistrer",
|
||||
"save": "Enregistrer",
|
||||
"features_title": "Fonctionnalités",
|
||||
"home_sections_title": "Sections de la page d´accueil",
|
||||
@@ -572,7 +572,7 @@
|
||||
"genres": "Genres",
|
||||
"years": "Années",
|
||||
"sort_by": "Trier par",
|
||||
"filter_by": "Filter By",
|
||||
"filter_by": "Filtrer par",
|
||||
"sort_order": "Ordre de tri",
|
||||
"tags": "Tags"
|
||||
}
|
||||
@@ -719,25 +719,25 @@
|
||||
"favorites": "Favoris"
|
||||
},
|
||||
"music": {
|
||||
"title": "Music",
|
||||
"title": "Musique",
|
||||
"tabs": {
|
||||
"suggestions": "Suggestions",
|
||||
"albums": "Albums",
|
||||
"artists": "Artists",
|
||||
"artists": "Artistes",
|
||||
"playlists": "Playlists",
|
||||
"tracks": "tracks"
|
||||
"tracks": "morceaux"
|
||||
},
|
||||
"filters": {
|
||||
"all": "All"
|
||||
"all": "Toutes"
|
||||
},
|
||||
"recently_added": "Recently Added",
|
||||
"recently_played": "Recently Played",
|
||||
"frequently_played": "Frequently Played",
|
||||
"explore": "Explore",
|
||||
"top_tracks": "Top Tracks",
|
||||
"play": "Play",
|
||||
"shuffle": "Shuffle",
|
||||
"play_top_tracks": "Play Top Tracks",
|
||||
"recently_added": "Ajoutés récemment",
|
||||
"recently_played": "Récemment joué",
|
||||
"frequently_played": "Fréquemment joué",
|
||||
"explore": "Explorez",
|
||||
"top_tracks": "Top chansons",
|
||||
"play": "Lecture",
|
||||
"shuffle": "Aléatoire",
|
||||
"play_top_tracks": "Jouer les pistes les plus populaires",
|
||||
"no_suggestions": "No suggestions available",
|
||||
"no_albums": "No albums found",
|
||||
"no_artists": "No artists found",
|
||||
|
||||
Reference in New Issue
Block a user