Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dda09bdd3b | ||
|
|
73d12a02c3 |
@@ -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.39
|
||||||
maven_group=com.ejclaw.videoplayer
|
maven_group=com.ejclaw.videoplayer
|
||||||
archives_base_name=video_player
|
archives_base_name=video_player
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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}.
|
||||||
|
|||||||
@@ -60,9 +60,27 @@ 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());
|
||||||
|
// Diagnostic: record, per anchor, whether this play resolved to the local cache file or
|
||||||
|
// fell back to a live network stream. A LIVE STREAM line on a video that doesn't show
|
||||||
|
// means a cache miss (download hadn't finished / failed) and the failure is on the
|
||||||
|
// network/decoder-open path; a CACHE line that still doesn't show means the local file
|
||||||
|
// decoded badly. This is the single fact that splits "cache problem" from "decode
|
||||||
|
// problem" when comparing two clients' logs.
|
||||||
|
if (cached != null) {
|
||||||
|
VideoPlayerMod.LOG.info("[{}] play {} at {} -> CACHE {}",
|
||||||
|
VideoPlayerMod.MOD_ID, be.getUrl(), pos.toShortString(), cached.getFileName());
|
||||||
|
} else {
|
||||||
|
VideoPlayerMod.LOG.info("[{}] play {} at {} -> LIVE STREAM (not cached yet)",
|
||||||
|
VideoPlayerMod.MOD_ID, be.getUrl(), pos.toShortString());
|
||||||
|
}
|
||||||
backend.play(source, be.isLoop());
|
backend.play(source, be.isLoop());
|
||||||
backend.setVolume(be.isMuted() ? 0F : be.getVolume());
|
backend.setVolume(be.isMuted() ? 0F : be.getVolume());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user