diff --git a/gradle.properties b/gradle.properties index 0950c49..cef02f2 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.37 +mod_version=0.4.38 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 e7d7c0b..5b43345 100644 --- a/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java +++ b/src/main/java/com/ejclaw/videoplayer/VideoPlayerClient.java @@ -15,6 +15,7 @@ import net.fabricmc.api.Environment; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientBlockEntityEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry; import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents; @@ -84,6 +85,13 @@ public class VideoPlayerClient implements ClientModInitializer { } }); + // On every server join, create video_player_cache/ inside whatever game directory this + // client is actually running from (vanilla .minecraft or a custom-launcher dir such as + // .mc_custom) and log the resolved path. The directory then exists up front rather than + // only appearing once the first download starts, and the log line shows exactly where it + // lives so the active install can be confirmed. + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> VideoCache.ensureCacheDir()); + // Wipe video_player_cache/ on game exit so preloaded clips don't pile up across // sessions. Cache entries are re-broadcast by the server on every JOIN, so a freshly // started game will repopulate the cache automatically. diff --git a/src/main/java/com/ejclaw/videoplayer/client/playback/VideoCache.java b/src/main/java/com/ejclaw/videoplayer/client/playback/VideoCache.java index a4049ae..4bf82b4 100644 --- a/src/main/java/com/ejclaw/videoplayer/client/playback/VideoCache.java +++ b/src/main/java/com/ejclaw/videoplayer/client/playback/VideoCache.java @@ -381,7 +381,7 @@ public final class VideoCache { return; } - URLConnection raw = URI.create(encodeForRequest(url)).toURL().openConnection(); + URLConnection raw = URI.create(encodeUrl(url)).toURL().openConnection(); raw.setConnectTimeout(10_000); raw.setReadTimeout(30_000); raw.setRequestProperty("User-Agent", "video_player/" + VideoPlayerMod.MOD_ID); @@ -523,21 +523,21 @@ public final class VideoCache { } /** - * Percent-encode any non-ASCII characters in the URL so Java's {@link HttpURLConnection} - * puts a valid request line on the wire. The stored cache key, the {@link #sha256(String)} - * filename, and the {@link #READY} map all keep the ORIGINAL {@code url} string — only the - * bytes actually sent in the HTTP request are encoded — so {@link #lookup(String)} still - * matches the anchor's raw URL. + * Percent-encode any non-ASCII characters in the URL so it is a valid wire URL for both the + * cache download ({@link HttpURLConnection}) and playback (FFmpeg). Callers keep the ORIGINAL + * {@code url} string as the cache key — the {@link #sha256(String)} filename and {@link #READY} + * map are unchanged — and only encode at the point a request is actually made, so + * {@link #lookup(String)} still matches the anchor's raw URL. * *

Without this, a URL with a non-ASCII path segment (e.g. {@code .../음악퀴즈/...}) is sent * verbatim and the server answers HTTP 400, so every preload fails silently and the disk - * cache stays empty — clients then fall back to live streaming for every video. FFmpeg - * (playback) tolerates the raw URL, which is why playback "works" while the cache never - * fills. The multi-arg {@link URI} constructor encodes each component; an already-encoded - * URL round-trips unchanged (decode-then-encode is idempotent for these paths). On any - * parse failure we fall back to the raw string rather than dropping the download. + * cache stays empty — clients then fall back to live streaming for every video. The multi-arg + * {@link URI} constructor encodes each component; an already-encoded URL round-trips unchanged + * (decode-then-encode is idempotent for these paths). On any parse failure we fall back to the + * raw string rather than dropping the request. */ - private static String encodeForRequest(String url) { + public static String encodeUrl(String url) { + if (url == null) return null; try { URI u = URI.create(url); URI enc = new URI(u.getScheme(), u.getAuthority(), u.getPath(), u.getQuery(), u.getFragment()); @@ -547,6 +547,28 @@ public final class VideoCache { } } + /** + * Create the {@code /video_player_cache/} directory eagerly and log the resolved + * absolute path, so it exists (and is visible to the user) regardless of which game directory + * Minecraft is actually running from — vanilla {@code .minecraft} or a custom launcher dir + * (e.g. {@code .mc_custom}). Downloads also create it on demand; this just makes it appear up + * front and surfaces the exact location in the log for diagnosis. + */ + public static void ensureCacheDir() { + Path dir = cacheDir(); + if (dir == null) { + VideoPlayerMod.LOG.warn("[{}] cache dir: no game dir available yet", VideoPlayerMod.MOD_ID); + return; + } + try { + Files.createDirectories(dir); + VideoPlayerMod.LOG.info("[{}] cache dir ready: {}", VideoPlayerMod.MOD_ID, dir.toAbsolutePath()); + } catch (Throwable t) { + VideoPlayerMod.LOG.warn("[{}] could not create cache dir {}: {}", + VideoPlayerMod.MOD_ID, dir, t.toString()); + } + } + /** * Best-effort filename extension from the URL path so FFmpeg's container probe gets a hint * (e.g. {@code .webm} for a webm stream). Falls back to {@code .bin}. diff --git a/src/main/java/com/ejclaw/videoplayer/client/playback/VideoPlayback.java b/src/main/java/com/ejclaw/videoplayer/client/playback/VideoPlayback.java index db2dfb9..5a6a72f 100644 --- a/src/main/java/com/ejclaw/videoplayer/client/playback/VideoPlayback.java +++ b/src/main/java/com/ejclaw/videoplayer/client/playback/VideoPlayback.java @@ -60,9 +60,14 @@ public final class VideoPlayback { VideoBackend backend = WatermediaProbe.isAvailable() ? new WatermediaBackend() : new JavaCvBackend(); // If /videopreload already cached the URL to disk, hand the local file path to FFmpeg // instead of the HTTP URL — eliminates the network read entirely. Falls back to the - // live URL when the cache miss or the download hasn't finished yet. + // live URL when the cache miss or the download hasn't finished yet. The lookup key stays + // the anchor's raw URL (matching how the download published it); only the live URL handed + // to FFmpeg on a miss is percent-encoded, so a non-ASCII path (e.g. ".../음악퀴즈/...") + // streams correctly instead of relying on FFmpeg's lenient raw handling. Path cached = VideoCache.lookup(be.getUrl()); - String source = cached != null ? cached.toAbsolutePath().toString() : be.getUrl(); + String source = cached != null + ? cached.toAbsolutePath().toString() + : VideoCache.encodeUrl(be.getUrl()); backend.play(source, be.isLoop()); backend.setVolume(be.isMuted() ? 0F : be.getVolume());