[Android] Add support for switching between server and client-side remote access playback

octoclonius git at videolan.org
Thu Mar 26 11:47:57 UTC 2026


vlc-android | branch: master | octoclonius <25781800+octoclonius at users.noreply.github.com> | Mon Mar 23 07:31:10 2026 -0500| [6f58e9bda385c1c9cad04afd71ce78b70d8a4c13] | committer: octoclonius

Add support for switching between server and client-side remote access playback

> https://code.videolan.org/videolan/vlc-android/commit/6f58e9bda385c1c9cad04afd71ce78b70d8a4c13
---

 .../vlc/remoteaccessserver/RemoteAccessRouting.kt  | 32 ++++++++++++++++++++++
 .../websockets/RemoteAccessWebSockets.kt           |  9 ++++++
 .../websockets/WSIncomingMessage.kt                |  3 +-
 .../src/org/videolan/vlc/media/PlaylistManager.kt  | 15 ++++++++++
 4 files changed, 58 insertions(+), 1 deletion(-)

diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt
index a31420007d..c2ea2fa3bc 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessRouting.kt
@@ -1255,6 +1255,38 @@ fun Route.setupRouting(appContext: Context, scope: CoroutineScope) {
             runCatching { if (dstFile.exists()) dstFile.delete() }
             return at get
         }
+        // Stream an audio file for in-browser playback
+        get("/stream") {
+            if (!settings.serveAudios(appContext)) {
+                call.respond(HttpStatusCode.Forbidden)
+                return at get
+            }
+            val id = call.request.queryParameters["id"] ?: run {
+                call.respond(HttpStatusCode.BadRequest, "Missing id parameter")
+                return at get
+            }
+            val media = appContext.getFromMl { getMedia(id.toLong()) }
+            if (media == null) {
+                call.respond(HttpStatusCode.NotFound)
+                return at get
+            }
+            val path = media.uri.path
+            if (path == null || !media.uri.scheme.isSchemeFile()) {
+                call.respond(HttpStatusCode.NotFound)
+                return at get
+            }
+            val file = File(path)
+            if (!file.exists() || !file.canRead()) {
+                call.respond(HttpStatusCode.NotFound)
+                return at get
+            }
+            call.response.header(
+                HttpHeaders.ContentDisposition,
+                ContentDisposition.Inline.withParameter(ContentDisposition.Parameters.FileName, file.name)
+                    .toString()
+            )
+            call.respondFile(file)
+        }
         //Change the favorite state of a media
         get("/favorite") {
             val type = call.request.queryParameters["type"] ?: "media"
diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/websockets/RemoteAccessWebSockets.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/websockets/RemoteAccessWebSockets.kt
index df3071f966..1dfdf90573 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/websockets/RemoteAccessWebSockets.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/websockets/RemoteAccessWebSockets.kt
@@ -96,6 +96,12 @@ object RemoteAccessWebSockets {
             } finally {
                 webSocketSessions.remove(sessionId)?.close()
                 if (BuildConfig.DEBUG) Log.d(TAG, "WebSockets: Removed and closed session: $sessionId")
+                if (webSocketSessions.isEmpty()) {
+                    val service = RemoteAccessServer.getInstance(context).service
+                    withContext(Dispatchers.Main) {
+                        service?.playlistManager?.setBrowserAudio(false)
+                    }
+                }
             }
         }
     }
@@ -240,6 +246,9 @@ object RemoteAccessWebSockets {
                 }
             }
             REMOTE -> incomingMessage.stringValue?.let { action -> VideoPlayerActivity.videoRemoteFlow.emit(action) }
+            SET_BROWSER_AUDIO -> {
+                service?.playlistManager?.setBrowserAudio(incomingMessage.id == 1)
+            }
         }
         return true
     }
diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/websockets/WSIncomingMessage.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/websockets/WSIncomingMessage.kt
index a5286b2e38..1222bab0bf 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/websockets/WSIncomingMessage.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/websockets/WSIncomingMessage.kt
@@ -58,7 +58,8 @@ enum class IncomingMessageType(private val type: String, val controlRequired: Bo
     DELETE_MEDIA("delete-media"),
     MOVE_MEDIA_BOTTOM("move-media-bottom"),
     MOVE_MEDIA_TOP("move-media-top"),
-    REMOTE("remote" );
+    REMOTE("remote" ),
+    SET_BROWSER_AUDIO("set-browser-audio");
 
     override fun toString(): String = type
 
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 640067e52c..eea72584bd 100644
--- a/application/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt
+++ b/application/vlc-android/src/org/videolan/vlc/media/PlaylistManager.kt
@@ -150,6 +150,8 @@ class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventList
         private set
     var isBenchmark = false
     var isHardware = false
+    var browserAudioActive = false
+    private var preBrowserVolume = -1
     private var parsed = false
     var savedTime = 0L
     private var random = SecureRandom()
@@ -568,6 +570,7 @@ class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventList
             media.setEventListener(this at PlaylistManager)
             player.startPlayback(media, mediaplayerEventListener, start)
             player.setSlaves(media, mw)
+            if (browserAudioActive) player.setVolume(0)
             newMedia = true
             determinePrevAndNextIndices()
             service.onNewPlayback()
@@ -586,6 +589,18 @@ class PlaylistManager(val service: PlaybackService) : MediaWrapperList.EventList
         player.release()
     }
 
+    fun setBrowserAudio(enable: Boolean) {
+        if (browserAudioActive == enable) return
+        browserAudioActive = enable
+        if (enable) {
+            preBrowserVolume = player.getVolume()
+            player.setVolume(0)
+        } else {
+            player.setVolume(if (preBrowserVolume >= 0) preBrowserVolume else 100)
+            preBrowserVolume = -1
+        }
+    }
+
     @MainThread
     fun switchToVideo(): Boolean {
         val media = getCurrentMedia()



More information about the Android mailing list