mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
fix(player): apply per-item mute live on Android + web (#129)
Honor the new per-item mute from the server, both in real time and on reload. Android: - WebSocketService: onMuteChanged callback + main-thread device:mute-changed handler. - MediaPlayerManager.setVideoMuted(): flips the live ExoPlayer volume on the current video (YouTube autoplays muted; images/widgets are silent). - MainActivity: on device:mute-changed, apply immediately if the toggled item is the one playing now. - PlaylistController.sig(): include muted so a published mute change re-renders/persists instead of being de-duped. Web player (server/player/index.html): - device:mute-changed handler toggles the current <video>; the video mount now also honors item.muted so a published mute sticks across reloads. Tizen intentionally not included: its player mutes ALL video for autoplay, so per-item unmute isn't achievable there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
332f4c5b5c
commit
c55118d8d8
|
|
@ -562,6 +562,16 @@ class MainActivity : AppCompatActivity() {
|
|||
wsService?.onPipShow = { data -> if (::pipOverlay.isInitialized) pipOverlay.show(data) }
|
||||
wsService?.onPipClear = { data -> if (::pipOverlay.isInitialized) pipOverlay.clearFrom(data) }
|
||||
|
||||
// #129: real-time mute. Apply immediately if the toggled item is the one playing now;
|
||||
// otherwise it's already persisted server-side and lands via the next playlist update.
|
||||
wsService?.onMuteChanged = { data ->
|
||||
val contentId = if (data.isNull("content_id")) "" else data.optString("content_id", "")
|
||||
val current = playlistController.currentContentId ?: ""
|
||||
if (contentId.isNotEmpty() && contentId == current && ::mediaPlayer.isInitialized) {
|
||||
mediaPlayer.setVideoMuted(data.optBoolean("muted", false))
|
||||
}
|
||||
}
|
||||
|
||||
wsService?.onRegistered = { _ ->
|
||||
hideStatus()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,6 +184,15 @@ class MediaPlayerManager(
|
|||
|
||||
fun isPlayingVideo(): Boolean = currentType == MediaType.VIDEO && (exoPlayer?.isPlaying == true)
|
||||
|
||||
// #129: live per-item mute. Applies a dashboard mute toggle to the CURRENTLY playing
|
||||
// video in real time (decoupled from a playlist reload). Only video carries audio here
|
||||
// — YouTube embeds autoplay muted and images/widgets are silent — so this targets the
|
||||
// ExoPlayer volume. Persistence across the next play comes from the playlist payload's
|
||||
// per-item `muted` (honored in playVideo). Main thread only.
|
||||
fun setVideoMuted(muted: Boolean) {
|
||||
if (currentType == MediaType.VIDEO) exoPlayer?.volume = if (muted) 0f else 1f
|
||||
}
|
||||
|
||||
// ---- Video-wall (wall:sync) accessors. All must be called on the main thread. ----
|
||||
|
||||
/** Current video position in ms (0 when no video). */
|
||||
|
|
|
|||
|
|
@ -109,7 +109,10 @@ class PlaylistController(
|
|||
// widget items share an empty contentId).
|
||||
// #74/#75: a schedule edit changes playback even when content is identical, so
|
||||
// the change signature must include schedules (else updated blocks are dropped).
|
||||
fun sig(it: PlaylistItem) = it.contentId + "|" + (it.widgetId ?: "") + "|" +
|
||||
// #129: include muted too, so a mute-only change (same content) re-renders with the
|
||||
// new flag instead of being de-duped (the real-time event handles the live toggle;
|
||||
// this makes a published mute persist across reloads).
|
||||
fun sig(it: PlaylistItem) = it.contentId + "|" + (it.widgetId ?: "") + "|" + (if (it.muted) "m" else "") + "|" +
|
||||
it.schedules.joinToString(";") { b ->
|
||||
b.days.sorted().joinToString(",") + "@" + b.start + "-" + b.end + ":" + (b.startDate ?: "") + "~" + (b.endDate ?: "")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class WebSocketService : Service() {
|
|||
var onWallSyncRequest: ((JSONObject) -> Unit)? = null
|
||||
var onPipShow: ((JSONObject) -> Unit)? = null
|
||||
var onPipClear: ((JSONObject) -> Unit)? = null
|
||||
var onMuteChanged: ((JSONObject) -> Unit)? = null
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): WebSocketService = this@WebSocketService
|
||||
|
|
@ -248,6 +249,12 @@ class WebSocketService : Service() {
|
|||
handler.post { try { onPipClear?.invoke(data) } catch (e: Throwable) { Log.e("WebSocketService", "onPipClear cb: ${e.message}") } }
|
||||
}
|
||||
|
||||
// #129: real-time mute toggle. Post to the main thread — it touches the player.
|
||||
safeOn("device:mute-changed") { args ->
|
||||
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||
handler.post { try { onMuteChanged?.invoke(data) } catch (e: Throwable) { Log.e("WebSocketService", "onMuteChanged cb: ${e.message}") } }
|
||||
}
|
||||
|
||||
safeOn("device:command") { args ->
|
||||
val data = args.firstOrNull() as? JSONObject ?: return@safeOn
|
||||
val type = data.optString("type", "")
|
||||
|
|
|
|||
|
|
@ -932,6 +932,15 @@
|
|||
if (data.type === 'screen_on') { document.getElementById('screenOffOverlay')?.remove(); }
|
||||
});
|
||||
|
||||
// #129: real-time mute. Apply immediately if the toggled item is the one playing now;
|
||||
// the value is also persisted in the snapshot so it sticks on the next playlist load.
|
||||
socket.on('device:mute-changed', (data) => {
|
||||
const item = playlist[currentIndex];
|
||||
if (data && item && data.content_id && item.content_id === data.content_id && currentVideoEl) {
|
||||
try { currentVideoEl.muted = !!data.muted; } catch (_) {}
|
||||
}
|
||||
});
|
||||
|
||||
// #109: PiP overlay — a pushed floating layer above the playlist. The player
|
||||
// fetches uri itself (same trust model as remote_url content).
|
||||
socket.on('device:pip-show', (data) => pipShow(data));
|
||||
|
|
@ -1637,7 +1646,8 @@
|
|||
video.autoplay = true;
|
||||
// Followers stay muted unconditionally (leader-only audio); leaders
|
||||
// start muted only if the user hasn't gestured yet (autoplay policy).
|
||||
video.muted = isFollower ? true : !userHasInteracted;
|
||||
// #129: a per-item mute (set in the admin console) also forces muted.
|
||||
video.muted = isFollower ? true : (!userHasInteracted || !!item.muted);
|
||||
// Explicit max volume on the leader so audio is at full level when
|
||||
// unmute happens (default is 1.0 but make it visible in logs).
|
||||
if (!isFollower) video.volume = 1.0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue