Opt-in lossy DCT compression profile (tag 4)#39
Conversation
e336ec2 to
b262b73
Compare
|
Hey Marius, Tested on a 2m47s clip at --cols 250 and --cols 450: 250 cols lossless: 293 MB → 250 cols profile: 32 MB (~9x smaller) |
Follow-up on the review notes. Decoder: it allocated typed arrays per 8x8 block (RLE scratch, dequantised coefficients, IDCT scratch) plus a fresh set of plane buffers every frame, so playback churned the GC. Per-block scratch is now hoisted and reused, plane buffers ping-pong, and DC-only blocks skip the IDCT entirely: the first row of the integer basis is constant, so the transform collapses to a flat value (checked exhaustively against the full IDCT, not just on the vectors). Allocations per decoded frame at 448x256 (2688 blocks): keyframe 10764 -> 12 inter frame 1624 -> 4 Decode time roughly halves (~5 -> ~3 ms/frame here, but JIT-noisy run to run), and GC events over the same workload drop ~4x. Decoded frames are byte-identical to the previous implementation, so this is a pure performance change. The hoisted scratch is module-level, which is only sound because the block loop never awaits and two decoders therefore cannot interleave inside it. experiments/check_profile.js now drives two decoders concurrently to guard that invariant instead of trusting it. Encoder: the transform, quantisation, reconstruction and run-length coding ran one 8x8 block at a time in Python. They are now batched over the whole plane and the integer SAD reduction uses einsum. Roughly 2x faster (84 -> 41 ms/frame at 448x256), with a byte-identical bitstream.
de65ee2 to
479d9aa
Compare
|
Thanks for testing it that thoroughly. You were right down to the The decoder allocated per 8x8 block (RLE scratch, dequantised coefficients and two more inside the IDCT) plus a fresh set of plane buffers every frame. Fixed in Allocations per decoded frame at 448x256, roughly your 450-col case: keyframe 10764 → 12, inter frame 1624 → 4. My clip is mostly static so many blocks skip; on real footage the inter number sits closer to the keyframe one. Decode time roughly halves here (~5 → ~3 ms/frame, JIT-noisy), GC events drop about 4x, and decoded frames are byte-identical to the previous version. One caveat on my own fix: the hoisted scratch is module-level, sound only because the block loop never awaits, so two decoders cannot interleave inside it. If "recovery" meant playback never catches back up after a hitch rather than the hitch itself, that lives in the Studio's playback loop rather than the codec. Tell me what you see. Compiler time: the encoder ran transform, quantisation, reconstruction and RLE one block at a time in Python. Batched over the whole plane now, about 2x faster (84 → 41 ms/frame), bitstream byte-identical. That halves your 15 minutes rather than fixing them. Beyond that the answer is parallel GOPs (keyframes every 48 frames, so chunks are independent), but that reshapes your compiler's main loop, so your call. On the blur: I nearly told you to raise Suite green, concurrency check included. |
Hey Yusuf, thanks a lot for the warm welcome, this was a fun one to build. As you suggested in #38, this is a separate opt-in lossy profile rather than a 5th competitor in the tag race, on tag 4 (I left 5+ free for later).
What it is. A userland lossy profile for pixel mode: YUV 4:2:0, an 8x8 integer DCT with a perceptual quantization matrix, per-block skip, luma block motion compensation, zigzag plus run-length entropy, DC prediction, deadzone quantization and a rate-distortion skip. Roughly 4 to 5x smaller than the current lossy path at matched quality (for example a 480x272 hero clip around 1.8 MB where the current codec is about 8 MB for the same look), and several times smaller than lossless. All in userland: no
<video>, no native decoder, it decodes on a canvas.See it in action.
This produces a normal
.ascfyou can play in the Studio or the static player, since the decoder outputs a standard BGR pixel framebuffer. Higher--qfmeans better quality and larger files.Additive, nothing on the current path changes.
.ascffiles and the Studio play byte-for-byte identically. I verified this: the updatedcodec.jsgives output identical to the old one on legacy streams..ascfheader change. Keyframes self-describe (QF, cols, rows), so the 14-byte header andreader.js/app.jsare untouched.codec.pyandcodec.jsstay bit-exact. Theexperiments/test mirrorsgen_vectors.pytocheck_vectors.js: encode in Python, decode through the shippedcodec.js, compare every frame.codec.jsnow degrades gracefully on an unknown tag (repeats the last frame instead of throwing), so future tags do not hard-crash old players.On the live streaming side you mentioned. The encoder lives in
codec.py, the same placestream_server.pyand the compiler both call, so wiring the profile into the live WebSocket path is a small, natural follow-up. Happy to send that next once you are happy with the static side, and to align the profile signaling and any details with you, as you offered.Files.
codec.py(addsProfileEncoder,encode_frameunchanged),codec.js(makeDecoderroutes tag 4),static_player/compiler.py(--profile --qf),experiments/profile_vectors.py+check_profile.js,PROFILE.md.