[Android] PlaylistManager: wait for the font cache before playback
Duncan McNamara
git at videolan.org
Thu Oct 1 12:16:57 UTC 2026
vlc-android | branch: master | Duncan McNamara <dcn.mcnamara at gmail.com> | Fri Sep 18 13:27:51 2026 +0000| [ba316c9819ee24678ff56fd21600e0cf6259e828] | committer: Duncan McNamara
PlaylistManager: wait for the font cache before playback
The font cache build started at app startup can still be running when
the user plays a media, in which case libvlc would scan the fonts a
second time and delay the playback.
Wait for it in playIndex, which every playback goes through, and warn
the user with a toast when there is something to wait for.
> https://code.videolan.org/videolan/vlc-android/commit/ba316c9819ee24678ff56fd21600e0cf6259e828
---
.../resources/src/main/res/values/strings.xml | 1 +
.../src/org/videolan/vlc/PreviewVideoInputService.kt | 3 +++
.../src/org/videolan/vlc/media/PlaylistManager.kt | 8 ++++++++
.../src/org/videolan/vlc/util/FontCache.kt | 20 +++++++++++++++++---
4 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/application/resources/src/main/res/values/strings.xml b/application/resources/src/main/res/values/strings.xml
index 8307f92d65..58cbe1731a 100644
--- a/application/resources/src/main/res/values/strings.xml
+++ b/application/resources/src/main/res/values/strings.xml
@@ -264,6 +264,7 @@
<string name="load_3_period" translatable="false">...</string>
<string name="invalid_location">The media at %1$s cannot be played.</string>
+ <string name="font_cache_building">Preparing the fonts for the first playback, this can take a few seconds</string>
<string name="missing_location">This media is on an unavailable storage device and cannot be played.</string>
<string name="search">Search</string>
diff --git a/application/vlc-android/src/org/videolan/vlc/PreviewVideoInputService.kt b/application/vlc-android/src/org/videolan/vlc/PreviewVideoInputService.kt
index 112cf46434..70f87b61f7 100644
--- a/application/vlc-android/src/org/videolan/vlc/PreviewVideoInputService.kt
+++ b/application/vlc-android/src/org/videolan/vlc/PreviewVideoInputService.kt
@@ -24,6 +24,7 @@ import org.videolan.resources.util.getFromMl
import org.videolan.tools.getContextWithLocale
import org.videolan.vlc.media.MediaPlayerEventListener
import org.videolan.vlc.media.PlayerController
+import org.videolan.vlc.util.FontCache
import org.videolan.vlc.util.random
import java.io.IOException
@@ -66,6 +67,8 @@ class PreviewVideoInputService : TvInputService(), CoroutineScope by MainScope()
return at launch
}
try {
+ // No preview until the fonts have been scanned, see [FontCache]
+ FontCache.await(this at PreviewVideoInputService)
val media = factory.getFromUri(VLCInstance.getInstance(this at PreviewVideoInputService), mw.uri)
val start = if (mw.length <= 0L) 0L else mw.length.random()
media.addOption(":start-time=${start/1000L}")
diff --git a/application/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt b/application/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt
index 31f85062f4..b65ff7fa32 100644
--- a/application/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt
+++ b/application/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt
@@ -89,6 +89,7 @@ import org.videolan.vlc.R
import org.videolan.vlc.gui.browser.BaseBrowserFragment
import org.videolan.vlc.gui.video.VideoPlayerActivity
import org.videolan.vlc.util.FileUtils
+import org.videolan.vlc.util.FontCache
import org.videolan.vlc.util.awaitMedialibraryStarted
import org.videolan.vlc.util.isSchemeFD
import org.videolan.vlc.util.isSchemeHttpOrHttps
@@ -481,6 +482,13 @@ class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventList
}
suspend fun playIndex(index: Int, flags: Int = 0, forceResume:Boolean = false, forceRestart:Boolean = false) {
+ // The fonts are scanned when the first text renderer is created, which would otherwise
+ // delay the playback by several seconds on the first run. See [FontCache]
+ // Wait before reading any state, as the queue can change in the meantime
+ FontCache.await(service) {
+ service.showToast(service.getString(R.string.font_cache_building), Toast.LENGTH_LONG)
+ }
+
videoBackground = videoBackground || (!player.isVideoPlaying() && player.canSwitchToVideo())
if (mediaList.size() == 0) {
Log.w(TAG, "Warning: empty media list, nothing to play !")
diff --git a/application/vlc-android/src/org/videolan/vlc/util/FontCache.kt b/application/vlc-android/src/org/videolan/vlc/util/FontCache.kt
index e741fd56f4..2e093b6173 100644
--- a/application/vlc-android/src/org/videolan/vlc/util/FontCache.kt
+++ b/application/vlc-android/src/org/videolan/vlc/util/FontCache.kt
@@ -24,6 +24,7 @@ package org.videolan.vlc.util
import android.content.Context
import android.os.Build
+import android.os.SystemClock
import android.util.Log
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
@@ -38,12 +39,16 @@ import org.videolan.tools.Settings
import org.videolan.tools.putSingle
import org.videolan.vlc.buildFontCacheIfSupported
import java.io.File
+import java.util.concurrent.atomic.AtomicLong
private const val TAG = "VLC/FontCache"
private const val SYSTEM_FONTS_DIR = "/system/fonts"
/** Give up after this many failed builds, so that a device which can't store the cache
* doesn't delay every playback. */
private const val MAX_FAILURES = 3
+/** Don't warn about the running build more often than this, which is how long a
+ * [android.widget.Toast.LENGTH_LONG] toast stays on screen. */
+private const val WAIT_NOTIFICATION_INTERVAL = 3500L
/**
* Builds the fontconfig font cache ahead of playback.
@@ -58,6 +63,8 @@ private const val MAX_FAILURES = 3
object FontCache {
private var job: Deferred<Boolean>? = null
+ /** When a caller was last told about the running build, see [await] */
+ private val lastWaitNotification = AtomicLong(-WAIT_NOTIFICATION_INTERVAL)
/**
* Start the build if it's needed, and return the running job.
@@ -88,11 +95,12 @@ object FontCache {
/**
* Wait for the font cache to be built, if a build is needed and still running.
- * @param onWait called before waiting, and only when there is something to wait for
+ * @param onWait called before waiting, only when there is something to wait for, and at most
+ * once per [WAIT_NOTIFICATION_INTERVAL], so that playbacks started in a row warn only once
*/
- suspend fun await(context: Context, onWait: () -> Unit = {}) {
+ suspend fun await(context: Context, onWait: (() -> Unit)? = null) {
val pending = start(context) ?: return
- if (!pending.isCompleted) onWait()
+ if (onWait != null && !pending.isCompleted && shouldNotifyWait()) onWait()
try {
pending.await()
} catch (e: Exception) {
@@ -100,6 +108,12 @@ object FontCache {
}
}
+ private fun shouldNotifyWait(): Boolean {
+ val now = SystemClock.elapsedRealtime()
+ val last = lastWaitNotification.get()
+ return now - last >= WAIT_NOTIFICATION_INTERVAL && lastWaitNotification.compareAndSet(last, now)
+ }
+
private fun build(context: Context): Boolean {
val settings = Settings.getInstance(context)
// A new fingerprint means new fonts to scan, so the previous failures don't count
More information about the Android
mailing list