3.0.0.23
Curse Maven Snippet
What's new
📦 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

