Links
Tags
Creators
Details
All versions
3.0.0.23
Beta
3.0.0.232 weeks ago 12.9K
Compatibility
Minecraft: Java Edition
26.2
26.1.2
1.21.11
1.21.1
1.20.1
1.19.2
1.18.2
Platforms
Fabric
Forge
NeoForge
Supported environments
Client-side
Required content
Changes
📦 UPDATE 3.0.0.23 (RC)
⚡ MediaAPI
- 💥 BREAKING CHANGE (requires re-compile):
start()andstartPaused()returnbooleaninstead ofvoid. Every other control already answered whether it took effect, and the sync layer needs that answer from these two as well: an implementation now writesif (!super.start()) return false;before its pipeline work, and a bridged player reportsfalsewhen the call went to the session instead of to the local pipeline. Calling code does not change —player.start();andplayer::startas aRunnableboth still compile — but this is a binary break: anything compiled against 3.0.0.22 must be recompiled, or it fails at runtime withNoSuchMethodError - ✨ Added: synchronized playback is now built into
MediaPlayer— pass aBridge(one method,send(ByteBuffer), backed by a game packet, a socket, any byte carrier) to the constructor orMediaAPI.createPlayer(...), feed what you receive intoplayer.sync(ByteBuffer), and that is the whole integration. Nothing about peer identity reaches WaterMedia: the bridge knows which session it serves and routes accordingly, so the natural shape is a small class holding that key: no polling loop, no state machine and no correction math on the developer's side. A bridgedServerMediaPlayerbecomes the authority (registers spectators, broadcasts on change plus a ~5s heartbeat, applies control requests, sweeps clients that vanish); any bridged client player becomes a follower that replicates it and keeps itself aligned - ✨ Added:
org.watermedia.api.media.players.sync— a sealedPacketfamily with fixed-size versioned big-endian codecs, validated at decode (the trust boundary):Sync(29 B authoritative snapshot),Config(11 B),Watch/Unwatch(10 B),Report(20 B),Control(19 B). Decoding consumes only the packet's own bytes and leaves the rest, so a payload can travel inside a larger frame carrying the developer's routing fields - ✨ Added:
Config.Capability— the authority declares what the session grants and every follower learns it in the handshake:LOCKSTEP(while any ready spectator loads or buffers, the audience holds on a frozen clock and resumes exactly where it froze — failed clients ignored, and a mid-playback joiner never interrupts the others until it reports ready),CONTROLS(follower control calls travel upstream as requests and come back as authoritative state, making the API symmetric on both sides),VOLUME(volume/mute follow the authority instead of staying client-local). Followers read what was granted withMediaPlayer.granted(capability) - ✨ Added:
MediaPlayer.role()(SOLO/AUTHORITY/FOLLOWER) — the axis is not client versus server but who owns the truth, so aServerMediaPlayercan be either side while players backed by real media can only follow - ✨ Added:
MediaPlayer.authority()/authorityTime()/drift()plus thetolerance(ms)tunable andServerMediaPlayer.watcherTimeout(ms)— a follower keeps the last snapshot it heard and ages it into a live position (snapshots arrive seconds apart; a stale target would drag every correction backwards), then jumps to it with aseekQuickonce drift passes the tolerance (1s default). Drift is circular on repeating media, corrections are rate-limited while a pipeline resettles, and none run while the local player loads or buffers - ✨ Added:
ServerMediaPlayer.revision()/snapshot()/syncLive(boolean)— every successful mutation (including the internal repeat-loop wrap and lockstep gate flips) bumps a monotonic counter that drives the automatic re-broadcast; on the receiving end an out-of-order snapshot is dropped while a heartbeat (same revision, fresher time) lands - ⚙️ Changed: A player built with a
Bridgeno longer acts on its own control calls.start/pause/stop/togglePlay/seek/seekQuick/speed/repeatbecome requests to the session and returnfalsewhen local state was left untouched;previousFrame/nextFrameare refused outright, since stepping frames against a synced timeline is meaningless. This only affects players you deliberately bridge — built without one, they behave exactly as before - ⚙️ Changed:
speed(float)now rejectsFloat.NaN. It used to pass validation (NaN <= 0andNaN > 4are both false) and poison the playback clock and the audio engine; onlyTxMediaPlayerguarded against it, so every FFmpeg player was exposed - ⚙️ Changed:
TxMediaPlayerno longer shadows the basespeedfield nor reimplements its validation — it callssuper.speed(...)and only rebases its own passive clock, closing the segment at the outgoing rate so a rate change never retroactively rescales time that already passed - ⚙️ Changed: the sync flow never touches the game thread — inbound payloads are decoded and validated on whatever thread hands them to
sync(), and everything they cause runs on the shared 50ms daemon ticker - ⚙️ Changed:
ServerMediaPlayer.syncDurationis now first-wins per session — mixed-quality clients report durations differing by a few ms, and last-wins moved the loop modulo on every report, desyncing everyone; divergent reports (>500ms) are logged and ignored - ⚙️ Changed:
ServerMediaPlayer.seek()on an ENDED/STOPPED clock lands PAUSED at the (duration-clamped) position instead of staying dead — scrubbing semantics, matching VLC/mpv - ⚙️ Changed:
ServerMediaPlayermutators aresynchronized(network/game threads vs the shared ticker);time()/status()reads stay volatile lock-free
⚡ CodecsAPI
- 🐛 Fixed: every decoder capped each axis but never the pixel product, so a header of a few dozen bytes forced gigabyte allocations before any pixel data was read (
OutOfMemoryError); PNG/JPEG/GIF/NETPBM/DDS now share WEBP's total-pixel cap, andImageReader.readAllbounds what one animation may retain - 🐛 Fixed: PNG compressed text (
zTXt/iTXt/iCCP) spun forever at 100% CPU on a zlib stream requesting a preset dictionary — the inflate loop never checkedneedsDictionary(), so it never made progress and never hit its own size cap - 🐛 Fixed: progressive JPEG accepted refinement scans with no preceding first-pass scan and an unlimited scan count, letting a small file burn minutes of CPU; scan ordering is now tracked per coefficient and the scan count is capped
- 🐛 Fixed: JPEG accepted duplicate frame headers and read table data past its own declared segment length
- 🐛 Fixed: VP8L declared pixel and huffman-group counts its bitstream cannot encode, and the VP8 bool decoder kept decoding from implicit zero bytes past the end of its partition
- 🐛 Fixed: WEBP XMP metadata was scanned quadratically, retaining one copy of the same element per iteration
- 🐛 Fixed: SVG rasterization was quadratic in edge count, and non-finite values (
1e999) drove curve flattening to full depth from a 200-byte file; geometry must now be finite and the fill sorts inO(n log n)under explicit budgets - 🐛 Fixed: SVG entity expansion inherited JDK limits that differ by orders of magnitude across Java 17/21/25 — they are now set explicitly and the decoder owns its own element, path and gradient budgets
- 🐛 Fixed: NETPBM merged two numbers into a single dimension across a comment, disagreeing with every reference decoder, and an unbounded
TUPLTYPEreached the log verbatim - 🐛 Fixed: DDS block-count arithmetic overflowed to negative or zero sizes; it is now
long-typed with dimension and array-size caps - 🐛 Fixed: GIF metadata records accumulated without bound across
reset(), and an LZW stream could reference a dictionary entry the current frame never defined - 🐛 Fixed: malformed input escaped as
IllegalArgumentException, rawEOFExceptionor bareIOExceptionacross the codec tree, making a corrupt image indistinguishable from a transport failure; the contract isXCodecExceptionthroughout
Supplementary resources
| File | Type | Size | |
|---|---|---|---|
| watermedia-3.0.0.23-sources.jar | Unknown | 2.01 MiB |
Projects on Modrinth are automatically available through a Maven repository for use with JVM build tools such as Gradle. To learn more about the Modrinth Maven API, click here.
Note: When available, you should use the creator's maven repo instead as it will have transitive dependency information that the Modrinth Maven API does not. You may also end up with duplicate dependencies if you use a mix of Modrinth and non-Modrinth Maven repositories for your dependencies, because the group identifier will be different when served through the Modrinth Maven API.
Maven coordinates:
Version ID:
build.gradle:
repositories {
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
// forRepositories(fg.repository) // Uncomment when using ForgeGradle
filter {
includeGroup "maven.modrinth"
}
}
}
// Standard Gradle dependency
dependencies {
implementation "maven.modrinth:G922NeHS:FdCZ5Rxq"
}
// Legacy Loom dependency
dependencies {
modImplementation "maven.modrinth:G922NeHS:FdCZ5Rxq"
}


