[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