Compare commits

...

1 Commits

Author SHA1 Message Date
tkrmagid
73d12a02c3 v0.4.38: auto-encode URLs for playback too + eager cache dir in active gameDir
All checks were successful
build / build (push) Successful in 1m47s
- Share the percent-encoder as VideoCache.encodeUrl() and apply it to the
  live URL handed to FFmpeg in VideoPlayback (cache-miss path), not just the
  cache download. The lookup key stays the anchor's raw URL so cache hits
  still match; only the wire/FFmpeg URL is encoded. Non-ASCII paths now both
  cache and stream correctly instead of relying on FFmpeg's lenient handling.
- Add VideoCache.ensureCacheDir(), called on every client JOIN, to create
  video_player_cache/ inside whatever game dir Minecraft actually runs from
  (vanilla .minecraft or a custom-launcher dir like .mc_custom) and log the
  resolved absolute path, so the folder exists up front and the active
  install location is visible in the log.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:21:27 +09:00
4 changed files with 50 additions and 15 deletions

View File

@@ -5,7 +5,7 @@ org.gradle.configuration-cache=false
# Mod # Mod
mod_id=video_player mod_id=video_player
mod_version=0.4.37 mod_version=0.4.38
maven_group=com.ejclaw.videoplayer maven_group=com.ejclaw.videoplayer
archives_base_name=video_player archives_base_name=video_player

View File

@@ -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.ClientBlockEntityEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; 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.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.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry; import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry;
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents; 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 // 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 // sessions. Cache entries are re-broadcast by the server on every JOIN, so a freshly
// started game will repopulate the cache automatically. // started game will repopulate the cache automatically.

View File

@@ -381,7 +381,7 @@ public final class VideoCache {
return; return;
} }
URLConnection raw = URI.create(encodeForRequest(url)).toURL().openConnection(); URLConnection raw = URI.create(encodeUrl(url)).toURL().openConnection();
raw.setConnectTimeout(10_000); raw.setConnectTimeout(10_000);
raw.setReadTimeout(30_000); raw.setReadTimeout(30_000);
raw.setRequestProperty("User-Agent", "video_player/" + VideoPlayerMod.MOD_ID); 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} * Percent-encode any non-ASCII characters in the URL so it is a valid wire URL for both the
* puts a valid request line on the wire. The stored cache key, the {@link #sha256(String)} * cache download ({@link HttpURLConnection}) and playback (FFmpeg). Callers keep the ORIGINAL
* filename, and the {@link #READY} map all keep the ORIGINAL {@code url} string — only the * {@code url} string as the cache key — the {@link #sha256(String)} filename and {@link #READY}
* bytes actually sent in the HTTP request are encoded — so {@link #lookup(String)} still * map are unchanged — and only encode at the point a request is actually made, so
* matches the anchor's raw URL. * {@link #lookup(String)} still matches the anchor's raw URL.
* *
* <p>Without this, a URL with a non-ASCII path segment (e.g. {@code .../음악퀴즈/...}) is sent * <p>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 * 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 * cache stays empty — clients then fall back to live streaming for every video. The multi-arg
* (playback) tolerates the raw URL, which is why playback "works" while the cache never * {@link URI} constructor encodes each component; an already-encoded URL round-trips unchanged
* fills. The multi-arg {@link URI} constructor encodes each component; an already-encoded * (decode-then-encode is idempotent for these paths). On any parse failure we fall back to the
* URL round-trips unchanged (decode-then-encode is idempotent for these paths). On any * raw string rather than dropping the request.
* parse failure we fall back to the raw string rather than dropping the download.
*/ */
private static String encodeForRequest(String url) { public static String encodeUrl(String url) {
if (url == null) return null;
try { try {
URI u = URI.create(url); URI u = URI.create(url);
URI enc = new URI(u.getScheme(), u.getAuthority(), u.getPath(), u.getQuery(), u.getFragment()); 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 <gameDir>/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 * 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}. * (e.g. {@code .webm} for a webm stream). Falls back to {@code .bin}.

View File

@@ -60,9 +60,14 @@ public final class VideoPlayback {
VideoBackend backend = WatermediaProbe.isAvailable() ? new WatermediaBackend() : new JavaCvBackend(); VideoBackend backend = WatermediaProbe.isAvailable() ? new WatermediaBackend() : new JavaCvBackend();
// If /videopreload already cached the URL to disk, hand the local file path to FFmpeg // 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 // 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()); 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.play(source, be.isLoop());
backend.setVolume(be.isMuted() ? 0F : be.getVolume()); backend.setVolume(be.isMuted() ? 0F : be.getVolume());