[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