- New /videoCache clear subcommand: drops every named entry from server config
and broadcasts DeleteCachePayload per URL so each client purges its disk cache
in one shot.
- Default max_preload_mb lowered from 1024 → 750. Sized to fit ~50 short FHD
clips (FHD H.264 ~5 Mbps × 20 s ≈ 12.5 MB → 50 × 15 MB ≈ 750 MB with headroom).
Config file's max_preload_mb still wins when set; the new default only
affects fresh installs and the client-side bootstrap value before the JOIN
policy packet arrives.
Crash fix (4K delete EXCEPTION_ACCESS_VIOLATION):
- JavaCvBackend.stopWorker() no longer calls grabber.close() from caller thread.
Only flips running=false, stops/flushes audio line, then interrupt+join(2s). The
worker's own finally still closes grabber from the decoder thread, so the av_frame
native plane is never freed mid-memCopy.
- Validate memCopy length against ByteBuffer.capacity() AND width*height*4 before
copying, and re-check running/closed inside the frameLock.
Config:
- max_preload_mb (default 1024) — replaces the hard-coded 512 MB cap in VideoCache.
Pushed to clients at join via CachePolicyPayload.
- render_distance_blocks (default 128) — replaces the hard-coded 128 in
VideoAnchorRenderer.getViewDistance(). Mirrored client-side via ClientPolicy.
Command rename: /videopreload → /videoCache add|list|remove
- Persistent named index in cache_entries (server config).
- /videoCache list prints clickable URLs (ClickEvent.OpenUrl).
- /videoCache remove broadcasts DeleteCachePayload so each client purges its disk
cache file.
Name resolution:
- /videoPlace ... <urlOrName> and the GUI save path both accept a /videoCache name
in place of an http(s) URL; VideoPlayerConfig.resolveUrlOrName() does the lookup
server-side before persisting to the anchor BE.
Cleanup:
- Drop the lowercase Brigadier aliases (videoplace, videostick, videodelete,
videomute) — keep camelCase only.
The anchor sits flush against a wall by design, so the back face never
gets a viewer. Drawing it cost an extra 4 vertices and doubled the
fragment shader work for the video texture per anchor for no visible
benefit. Removing it also fixes the mirrored-texture artifact a player
would see if they clipped behind the wall (e.g. spectator mode).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0.4.10 still played at ~2-5 fps even though the decoder buffer was
preallocated. Root cause: the single-slot staging buffer was paced by
SourceDataLine backpressure at the audio buffer's granularity (~0.5 s),
so the decoder burst-produced ~12 video frames into the slot while audio
drained, the consumer saw only the last frame of each burst, then the
decoder stalled until audio drained again. Net visible rate ~ source_fps
/ frames_per_burst.
Fix:
- Replace single staging slot with a 4-slot ring (preallocated, FIFO).
Decoder writes to ringTail; if full, overwrites oldest and bumps
droppedFrames so we can see overflow in the log. Render thread drains
oldest under the same lock — no allocation, no race.
- Shrink audio driver buffer 0.5 s → 0.1 s so the decoder is paced more
tightly. Burst size collapses from ~12 frames to 2-3, which fits
inside the ring.
- Log decoder spec on start (WxH @ fps, audio Hz x ch, ring depth) and
produced/consumed/dropped counters every ~10 s. Lets the user log
confirm whether the decoder is keeping real-time pace and whether the
ring is overflowing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0.4.9 allocated a fresh w*h*4 direct ByteBuffer on every grab() — at
1080p × 24fps that's ~192 MB/s of direct memory churn (page zero-fill +
Cleaner enqueue). The decoder thread spent most of its frame budget on
memory bookkeeping instead of decoding, fell behind real time, and the
single-slot AtomicReference saw bursty refills that the render thread
could only sample at ~5fps. Game thread was fine, only the video looked
like 5fps.
Replace it with one preallocated direct buffer per backend instance,
filled under a short-held lock on the decoder side. Swap the pollFrame()
ByteBuffer-returning API for consumeFrame(dstAddr, maxBytes) so the
render thread memcpys straight from staging buffer → GPU texture
pointer under the same lock — no allocation, no race window between
"got buffer" and "decoder overwrote it".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Stutter fix (root cause):
- 0.4.7 made the GPU upload a memcpy, but toRgba() in JavaCvBackend was
still doing BufferedImage.getRGB() + a per-pixel ARGB->RGBA loop. That
loop ran 20-50ms per 1080p frame on the decode thread. When it slipped
behind real-time, the audio buffer drained, backpressure vanished,
the decoder burst-fired catch-up frames into the single-slot
AtomicReference (dropping 11 of 12 for ~0.5s of buffer), then blocked
again on the next audio refill -- exactly the periodic stutter the
user reported.
- Force the grabber to output AV_PIX_FMT_RGBA (=26) via setPixelFormat.
Now frame.image[0] is already a ByteBuffer of RGBA bytes; we just
copy it into a fresh direct buffer and hand it to the upload path.
The colorspace conversion happens inside swscale (native SIMD) at
<1ms per frame, so the decoder consistently keeps real-time pace and
the audio backpressure stays smooth.
- Removed Java2DFrameConverter / BufferedImage usage entirely.
Defensive delete fix (potential crash on anchor delete):
- Entry.close() now calls TextureManager.release(id) before closing the
texture itself. Without this, a RenderType cached by Identifier could
still try to bind the dead GL handle on the next frame and crash the
render thread. The crash report the user reported couldn't be located
(no crash-reports/ folder) so this is the most plausible suspect from
reading the code; full diagnosis still pending the tail of latest.log.
- build.gradle: optional -Pplatform=<id> property switches the build into
a fat-jar mode where javacv 1.5.13 + javacpp + ffmpeg 8.0.1 (java + the
picked platform's native jar) are all nested into the mod jar via
Fabric loom's `include` directive. Fabric loader unpacks them at
runtime, so users no longer need -Xbootclasspath/a:... or 5 separate
jars in .minecraft/libraries.
- Without -Pplatform, the build produces the same small ~85KB vanilla
jar as before, so devs/server-side and bring-your-own-JavaCV setups
still work.
- Per-platform artifacts: video_player-<platform>-0.4.8.jar where
<platform> ∈ windows-x86_64 / linux-x86_64 / macosx-x86_64 /
macosx-arm64. Sizes 21-32MB.
- README: STEP 5 (the long JavaCV manual-install + -Xbootclasspath
section) is gone. New STEP 4 just says 'pick the jar for your OS'.
Also added a warning about removing the old -Xbootclasspath JVM arg
when upgrading, since duplicate JavaCV on the boot classpath can
silently break decoding.
- Replace per-pixel RGBA->ABGR loop in Entry.upload() with a single
MemoryUtil.memCopy() into NativeImage's native buffer. The two layouts
are identical when viewed as little-endian bytes, so no swap is needed.
Cuts 1080p upload time from a ~2M-iter Java loop to one native memcpy.
- Move the frame-pump tick from 20Hz client tick (END_CLIENT_TICK) to
per-render-frame (LevelRenderEvents.START_MAIN). At 60+fps display vs
24fps source, this removes the worst stutter window where a decoded
frame waited up to 50ms for the next tick. Distance-gain math stays on
20Hz where it's plenty.
- Bump version 0.4.6 -> 0.4.7 in gradle.properties and README.
- new: config/video_player.json on first server start. Field preload_urls
is a list of HTTP(S) URLs (≤256 chars each) that the server broadcasts
via PreloadPayload to every player when they finish joining, so common
videos are warmed into each client's video_player_cache/ before they
ever play. Reuses the same PreloadPayload + VideoCache path as
/videopreload, so chat feedback ("[videopreload] 완료") still applies.
- config is loaded once at mod init; invalid entries are dropped with a
WARN line. Edit + restart server to apply changes.
- audio: distance attenuation now uses the panel center (width/2, height/2
offset from the anchor along the renderer's right/up axes) instead of the
anchor block corner, so a 4x4 panel sounds like it's coming from the
middle of the screen and not the bottom-left.
- preload: each client now posts a chat line on start / completion / failure
/ cache-hit, so a command-block sequence like /videopreload -> /videoplace
can be timed against the visible "[videopreload] 완료" message.
- safety: VideoPlayback.tick() verifies the anchor BE still exists at each
active position and forcibly stops playback if it doesn't — covers any
edge case where BLOCK_ENTITY_UNLOAD doesn't fire.
- /videopreload feedback now explicitly states "완료 알림 후 재생하세요".
- fix: stop playback when anchor block entity unloads (BLOCK_ENTITY_UNLOAD)
so deleting a video while audio is playing actually silences it.
- fix: force-stop SourceDataLine and grabber from outside the worker thread
so a blocked line.write() / grab() unblocks immediately on close.
- perf: tune FFmpeg streaming options (buffer_size, probesize, analyzeduration,
max_delay, fflags=+genpts, reconnect_delay_max) and pre-size audio line buffer
to ~0.5s to smooth out mid-stream stutter.
- feat: /videopreload <url> broadcasts a S2C PreloadPayload to all clients;
each client downloads the URL to <gameDir>/video_player_cache/<sha256> and
subsequent playback reads from the local file instead of streaming.
Gated by COMMANDS_GAMEMASTER (op level 2), so command blocks can invoke it.
JavaCPP Loader (javacpp.jar) ships pure-Java code that needs its own
JNI bridge DLL (jnijavacpp) to extract & link other native libraries.
That bridge lives in the platform-specific javacpp-<os>-<arch>.jar
which the old install guide silently omitted — users following it
ended up with a black panel and an UnsatisfiedLinkError for jnijavacpp
because FFmpeg natives could never be loaded.
This bumps the required JavaCV jar list from 4 to 5, updates the
-Xbootclasspath/a: examples on all three OSes, and adds a diagnostic
note for the matching log line.
Docs-only change; mod code is unchanged from v0.4.2.
Reviewer caught that the README still pointed users at video_player-0.4.1.jar
even though 0.4.2 fixes the stick missing-model issue. Updated:
- "current version" header → 0.4.2
- STEP 4 download filename → video_player-0.4.2.jar
- build output path → build/libs/video_player-0.4.2.jar
- STEP 6 verification: explicitly call out that 0.4.1 and below have a known
stick-icon defect (unprefixed item/generated parent rejected by the 26.1.2
model loader) so users on those versions need to upgrade, not just dedupe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The held video_stick item rendered as the default missing-model cube even
with v0.4.1 jar loaded (lang strings resolved, so the mod itself was active).
Root cause confirmed against Fabric 26.1.2 docs: the new model loader no
longer auto-resolves unprefixed parent paths. `item/generated` needs to be
written as `minecraft:item/generated`.
models/item/video_stick.json — parent → minecraft:item/generated.
gradle.properties — 0.4.1 → 0.4.2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User followed README using %APPDATA% in -Xbootclasspath/a: and the official
Mojang launcher passed the literal string through to Java without expanding
it, so boot classpath ended up empty and video stayed black despite all 4
JavaCV jars being present.
Replaced the %APPDATA% example with an absolute C:\Users\<name>\AppData\
Roaming\... template, added a callout warning that the launcher does not
expand env vars in JVM args, and pointed at `echo %APPDATA%` from cmd as the
way to discover the right path.
Also corrected the ffmpeg jar filenames: the bundle zip uses the short form
(e.g. ffmpeg-windows-x86_64.jar), not the Maven-style
ffmpeg-8.0.1-1.5.13-windows-x86_64.jar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User reported Incompatible-mods crash because they downloaded
fabric-api-0.140.2+1.21.11.jar from Modrinth (Modrinth's version-filter URL
param does not always restrict the listing to the requested game version).
Replaced the generic search-page link with the direct CDN URL of
fabric-api-0.149.0+26.1.2.jar and added a callout telling readers to verify
the filename suffix ends in +26.1.2.jar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previous README used "방법 A/B/C" terminology that confused readers and
recommended Prism over the official launcher. Per user feedback, restructured
into a step-by-step guide assuming the official Mojang launcher:
1. boot 26.1.2 vanilla once to create .minecraft
2. run fabric-installer-1.x for client / 26.1.2 / loader 0.19.2
3. open .minecraft/mods (per-OS instructions)
4. drop fabric-api + video_player-0.4.1.jar, remove old versions
5. install JavaCV — two routes:
5-A. Prism Launcher (easiest)
5-B. official launcher via -Xbootclasspath/a: with Windows/macOS/Linux examples
6. verify with /videostick
Moved Maven coords to a developer footnote. Added install verification step
to disambiguate "missing texture" symptom from leftover old-version jars.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three fixes for v0.4.1:
1. Video stick item rendered as missing-texture because 26.1.2 requires the
new client_item descriptor at assets/<mod>/items/<name>.json. Add it; the
existing models/item/video_stick.json is kept as the underlying model.
2. Quad placement now anchors the local (0,0) corner at the bottom-left of
the wall face the player clicked, so the clicked block is the BL and the
video grows up & right. Previously it was centered on the anchor.
3. EAST/WEST face rotations were swapped, which placed the quad on the far
side of the air block (~1 block away from the wall) instead of flush.
Derived the correct rotations from first principles:
EAST = Axis.YP +90° (local +Z → world +X, +X → -Z = north)
WEST = Axis.YP -90° (local +Z → world -X, +X → +Z = south)
NORTH/SOUTH/UP/DOWN math re-verified — those were already correct.
The anchor block becomes invisible and non-collidable; it exists only as a
BlockEntity host in the air block adjacent to the clicked wall. The renderer
now translates and rotates the textured quad so it sits flush against the
surface of the wall the user actually clicked, on any of the six faces.
Stick interaction:
right-click face → place anchor at hit.relative(face), facing=face, open GUI
right-click face with anchor already there → reopen the GUI
sneak + left-click face with stick → delete the anchor on that face
The anchor's selection outline / collision / occlusion are all empty, so the
player can target the wall block behind it without interference.
JavaCV / streaming polish:
- Bump missing-JavaCV log to WARN so users notice when the runtime jar is
not installed (previously buried at INFO).
- Add HTTP resilience options: `timeout`, `reconnect`, `reconnect_streamed`,
`reconnect_at_eof`, and a `user_agent` so picky servers don't 403 us.
setVolume/Mute previously stored gain without affecting audible output: the
backend only called grabImage() and never opened an audio sink. Switch to
grab() (interleaved video+audio frames), force AV_SAMPLE_FMT_S16 on the
grabber so samples are always interleaved signed 16-bit PCM, open a matching
JavaSound SourceDataLine and write scaled samples per-frame. gain is read
on every block so /videoMute, GUI Mute and the per-tick distance attenuation
now take effect immediately. SourceDataLine.write blocking provides natural
A/V pacing, so the legacy 15ms sleep is dropped when an audio line is open;
sleep is retained as a 60fps cap when there is no audio device.
bump version to 0.3.1.
VideoPlayback now allocates a DynamicTexture per active anchor under a unique
Identifier (registered on Minecraft.getTextureManager()) and pumps RGBA frames
into it via NativeImage.setPixelABGR + DynamicTexture.upload() during the
client tick. Until the backend (JavaCV) produces a first frame, the texture
shows a dark gray placeholder with a thin border so the anchor screen is
visibly present.
VideoAnchorRenderer.submit() now uses SubmitNodeCollector.submitCustomGeometry
with RenderTypes.entityCutout(textureId), drawing a two-sided width×height
quad oriented by Direction.toYRot() + Axis.YP.rotationDegrees. Vertex
attributes use the new VertexConsumer fluent API (addVertex(Matrix4f, ...)
.setColor.setUv.setOverlay(NO_OVERLAY).setLight.setNormal).
JavaCvBackend / WatermediaBackend / WatermediaProbe / VideoBackend are
unchanged — JavaCV is referenced entirely via reflection so the mod jar
remains loadable when the bytedeco classifier jars aren't on the runtime
classpath, in which case the anchor renders its placeholder surface.
- Block/BE/Item: BaseEntityBlock + useItemOn(InteractionResult), useOn(UseOnContext),
setChanged(), loadAdditional(ValueInput) / saveAdditional(ValueOutput) with
getStringOr/getIntOr/getBooleanOr/getFloatOr defaults
- Registries: BuiltInRegistries + ResourceKey + Properties.setId(ResourceKey)
- Networking: CustomPacketPayload.Type + StreamCodec.composite + RegistryFriendlyByteBuf
(note: clientboundPlay/serverboundPlay names in fabric-networking-api-v1 6.3.1)
- Commands: Commands.literal/argument, CommandSourceStack.sendSuccess/sendFailure,
PermissionSet.hasPermission(Permissions.COMMANDS_GAMEMASTER) (level-2 equivalent)
- Client GUI: EditBox / Button / Checkbox / AbstractSliderButton + addRenderableWidget
(no render override; widgets render themselves under the new pipeline)
- Renderer: rewritten as stub against new BlockEntityRenderer<T, S extends BlockEntityRenderState>
pattern (createRenderState / extractRenderState / submit). Stub does not draw a quad yet
— frame upload and dynamic texture surface deferred until Watermedia/JavaCV are
re-audited for Java 25
- Playback: stripped to bookkeeping-only stub (tracks active anchors, no frame pump)
- Client entrypoint: ClientTickEvents.END_LEVEL_TICK (was END_WORLD_TICK), Minecraft.level,
LocalPlayer, Vec3, InteractionResult
./gradlew build passes against MC 26.1.2 + Fabric Loader 0.19.2 + fabric-api 0.149.0+26.1.2.
Block placement, anchor BE, payloads, commands, and GUI are functional; the anchor renders
as the plain block until the new render-state pipeline is wired with a texture.
- net.fabricmc.fabric-loom 1.16-SNAPSHOT (no remap; MC 26.1+ ships unobfuscated)
- gradle.properties: minecraft_version=26.1.2, loader=0.19.2, fabric-api=0.149.0+26.1.2
- Java 25 toolchain
- fabric.mod.json: fabricloader>=0.19.0, java>=25
- Drop multi-version build script + matrix CI (single-target now)
- Backup of 1.21.6/7/8 working tree preserved on mc-1.21.x branch
Source migration to Mojmap names is in progress on follow-up commits;
this commit alone will not build until source files are ported.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>