diff --git a/gradle.properties b/gradle.properties
index a4f01a9..8af22c6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,7 +5,7 @@ org.gradle.configuration-cache=false
# Mod
mod_id=video_player
-mod_version=0.4.31
+mod_version=0.4.32
maven_group=com.ejclaw.videoplayer
archives_base_name=video_player
diff --git a/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java b/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java
index 97f1e6d..e7d7c0b 100644
--- a/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java
+++ b/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java
@@ -100,17 +100,22 @@ public class VideoPlayerClient implements ClientModInitializer {
*
*
Gain is also gated by the Minecraft sound options so the in-game sliders work as
* expected: vanilla {@code SoundEngine.calculateVolume} multiplies by master × category, so
- * we do the same with {@link SoundSource#PLAYERS} as the category. Result: dragging the
- * "Players" slider in Options → Music & Sounds attenuates video audio just like other
- * player sounds, and "Master" still gates everything.
+ * we do the same. The category is configurable via {@code sound_category} in
+ * {@code config/video_player.json} (default {@link SoundSource#RECORDS} = the "Jukebox/Note
+ * Blocks" slider, since video playback is media). Master always gates on top; when the
+ * configured category is Master itself we don't square it. Result: dragging the configured
+ * category's slider in Options → Music & Sounds attenuates video audio, and "Master"
+ * still gates everything.
*/
private static void updateDistanceGains(Minecraft client) {
LocalPlayer p = client.player;
if (p == null || client.level == null) return;
Vec3 eye = p.getEyePosition();
float masterVol = client.options.getSoundSourceVolume(SoundSource.MASTER);
- float playersVol = client.options.getSoundSourceVolume(SoundSource.PLAYERS);
- float categoryScale = masterVol * playersVol;
+ SoundSource category = VideoPlayerConfig.soundCategory();
+ float categoryScale = (category == SoundSource.MASTER)
+ ? masterVol
+ : masterVol * client.options.getSoundSourceVolume(category);
for (BlockPos pos : VideoPlayback.activePositions()) {
if (!(client.level.getBlockEntity(pos) instanceof VideoAnchorBlockEntity be)) continue;
Vec3 center = be.panelCenter();
diff --git a/src/main/java/com/ejclaw/videoplayer/VideoPlayerConfig.java b/src/main/java/com/ejclaw/videoplayer/VideoPlayerConfig.java
index 09fbcc7..ff6a1bc 100644
--- a/src/main/java/com/ejclaw/videoplayer/VideoPlayerConfig.java
+++ b/src/main/java/com/ejclaw/videoplayer/VideoPlayerConfig.java
@@ -7,6 +7,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.sounds.SoundSource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -62,10 +63,17 @@ public final class VideoPlayerConfig {
private static final int DEFAULT_MAX_CACHE_MB = 750;
/** Default render-distance cap for video anchors, in blocks. 128 = the legacy hard-coded value. */
private static final int DEFAULT_RENDER_DISTANCE = 128;
+ /**
+ * Default Minecraft sound category the client uses to gate video audio. RECORDS = the
+ * "Jukebox/Note Blocks" (음반) slider — video playback is media, so it belongs there rather
+ * than on the player-sound slider. Master always gates on top of whatever category is set.
+ */
+ private static final SoundSource DEFAULT_SOUND_CATEGORY = SoundSource.RECORDS;
private static volatile int maxPreloadMb = DEFAULT_MAX_PRELOAD_MB;
private static volatile int maxCacheMb = DEFAULT_MAX_CACHE_MB;
private static volatile int renderDistanceBlocks = DEFAULT_RENDER_DISTANCE;
+ private static volatile SoundSource soundCategory = DEFAULT_SOUND_CATEGORY;
private static volatile List preloadUrls = Collections.emptyList();
/** Insertion-ordered name → url. Mutated only under the class monitor. */
private static final Map CACHE_ENTRIES = new LinkedHashMap<>();
@@ -80,6 +88,8 @@ public final class VideoPlayerConfig {
VideoPlayerMod.MOD_ID, path);
maxPreloadMb = DEFAULT_MAX_PRELOAD_MB;
maxCacheMb = DEFAULT_MAX_CACHE_MB;
+ renderDistanceBlocks = DEFAULT_RENDER_DISTANCE;
+ soundCategory = DEFAULT_SOUND_CATEGORY;
preloadUrls = Collections.emptyList();
CACHE_ENTRIES.clear();
return;
@@ -127,6 +137,23 @@ public final class VideoPlayerConfig {
if (rd > 2048) rd = 2048;
renderDistanceBlocks = rd;
+ // sound_category (client uses this to pick which volume slider gates video audio)
+ SoundSource cat = DEFAULT_SOUND_CATEGORY;
+ if (json.has("sound_category") && json.get("sound_category").isJsonPrimitive()
+ && json.get("sound_category").getAsJsonPrimitive().isString()) {
+ SoundSource parsed = parseSoundSource(json.get("sound_category").getAsString());
+ if (parsed != null) {
+ cat = parsed;
+ } else {
+ VideoPlayerMod.LOG.warn("[{}] config: unknown sound_category '{}' — using '{}'",
+ VideoPlayerMod.MOD_ID, json.get("sound_category").getAsString(),
+ DEFAULT_SOUND_CATEGORY.getName());
+ }
+ } else {
+ augmented = true;
+ }
+ soundCategory = cat;
+
// preload_urls (legacy)
List urls = new ArrayList<>();
if (json.has("preload_urls") && json.get("preload_urls").isJsonArray()) {
@@ -162,9 +189,9 @@ public final class VideoPlayerConfig {
VideoPlayerMod.LOG.info(
"[{}] config loaded: per-video={} MB, total-cache={} MB, render={} blocks, "
- + "preload_urls={}, cache_entries={}",
+ + "sound_category={}, preload_urls={}, cache_entries={}",
VideoPlayerMod.MOD_ID, maxPreloadMb, maxCacheMb, renderDistanceBlocks,
- urls.size(), CACHE_ENTRIES.size());
+ soundCategory.getName(), urls.size(), CACHE_ENTRIES.size());
// Auto-augment: rewrite the file once so missing keys appear after a mod update.
if (augmented) {
@@ -179,11 +206,23 @@ public final class VideoPlayerConfig {
maxPreloadMb = DEFAULT_MAX_PRELOAD_MB;
maxCacheMb = DEFAULT_MAX_CACHE_MB;
renderDistanceBlocks = DEFAULT_RENDER_DISTANCE;
+ soundCategory = DEFAULT_SOUND_CATEGORY;
preloadUrls = Collections.emptyList();
CACHE_ENTRIES.clear();
}
}
+ /** Match a config string against {@link SoundSource#getName()} (case-insensitive). Null = no match. */
+ private static SoundSource parseSoundSource(String s) {
+ if (s == null) return null;
+ String t = s.trim();
+ if (t.isEmpty()) return null;
+ for (SoundSource src : SoundSource.values()) {
+ if (src.getName().equalsIgnoreCase(t)) return src;
+ }
+ return null;
+ }
+
// -- accessors ---------------------------------------------------------------------------
/** Hard cap on a single client-side video download, in MB. */
@@ -201,6 +240,12 @@ public final class VideoPlayerConfig {
/** Anchor BE view-distance cap, in blocks. */
public static int renderDistanceBlocks() { return renderDistanceBlocks; }
+ /**
+ * Minecraft sound category the client gates video audio with (default {@link SoundSource#RECORDS}).
+ * Read client-side from the local config; not pushed from the server.
+ */
+ public static SoundSource soundCategory() { return soundCategory; }
+
/** Legacy un-named preload list (still pushed at join). Never null. */
public static List preloadUrls() { return preloadUrls; }
@@ -280,11 +325,15 @@ public final class VideoPlayerConfig {
"max_preload_mb: per-video download cap (each client). "
+ "max_cache_mb: total cache directory cap (each client). "
+ "render_distance_blocks: max distance at which a video anchor still renders. "
+ + "sound_category: which Minecraft volume slider gates video audio (client-side). "
+ + "One of: master, music, record, weather, block, hostile, neutral, player, "
+ + "ambient, voice, ui. Default 'record' = the Jukebox/Note Blocks slider. "
+ "preload_urls: HTTP(S) videos auto-pushed to every player on join (no name). "
+ "cache_entries: named entries managed by /videoCache add|list|remove.");
root.addProperty("max_preload_mb", DEFAULT_MAX_PRELOAD_MB);
root.addProperty("max_cache_mb", DEFAULT_MAX_CACHE_MB);
root.addProperty("render_distance_blocks", DEFAULT_RENDER_DISTANCE);
+ root.addProperty("sound_category", DEFAULT_SOUND_CATEGORY.getName());
root.add("preload_urls", new JsonArray());
root.add("cache_entries", new JsonArray());
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@@ -301,11 +350,15 @@ public final class VideoPlayerConfig {
"max_preload_mb: per-video download cap (each client). "
+ "max_cache_mb: total cache directory cap (each client). "
+ "render_distance_blocks: max distance at which a video anchor still renders. "
+ + "sound_category: which Minecraft volume slider gates video audio (client-side). "
+ + "One of: master, music, record, weather, block, hostile, neutral, player, "
+ + "ambient, voice, ui. Default 'record' = the Jukebox/Note Blocks slider. "
+ "preload_urls: legacy un-named auto-preload list. "
+ "cache_entries: managed by /videoCache add|list|remove.");
root.addProperty("max_preload_mb", maxPreloadMb);
root.addProperty("max_cache_mb", maxCacheMb);
root.addProperty("render_distance_blocks", renderDistanceBlocks);
+ root.addProperty("sound_category", soundCategory.getName());
JsonArray legacyArr = new JsonArray();
for (String u : preloadUrls) legacyArr.add(u);
root.add("preload_urls", legacyArr);