Changelog
Changelog
All notable changes to Miraiclip. Format: Keep a Changelog; versions follow SemVer. The canonical file lives at CHANGELOG.md in the repo.
renderer-0.4.0 · server-export-0.2.0 — 2026-09-13
Added
@miraiclip/rendererstreaming export output —exportProject({ target })writes encoded chunks to aWritableStream(ashowSaveFilePicker()writable works directly) as the file is produced; nothing accumulates in memory. See Export · Streaming to disk.@miraiclip/rendererchunked offline audio mixing — export audio mixes in bounded sequential chunks (audioChunkSeconds, default 60) interleaved with the frame walk instead of one whole-timeline buffer (~1.4GB/hour before). Together with streaming, export memory no longer scales with timeline length (stress run: ~2MB settled heap spread; a demonstrated one-hour export — 86,400 frames, an hour of audio, 984MB streamed — peaked at 165MB heap at 1.6× realtime on a dev laptop). Not breaking:exportComposition’s whole-rangemixAudiois unchanged; the chunked contract is the newmixAudioChunk.@miraiclip/server-exportstreams exports to disk — without, chunks stream from the browser into the file (flat memory; result is{ filePath, bytesWritten }, nobytes— breaking for callers that used both); withoutout,{ bytes }is unchanged.- Export validation corpus (
pnpm corpus, nightly) — real exports verified by ffmpeg/ffprobe (an independent decoder): whole-file PTS + per-frame pixel checks through cuts/dissolves/resampling, audio cases, A/V sync (measured 0ms offset), and streamed-vs-buffered byte-identity.
Fixed
@miraiclip/renderersame-asset concurrency wedged playback (caught by the device benchmark tier): two clips of the SAME asset visible at once — picture-in-picture of one source, echo overlays — shared one decode pipeline and fought over its seek target every frame, wedging playback. Concurrent clips of one asset now each get a dedicated pipeline (as transition overlaps already did); sequential clips (splits, cuts) keep sharing. The pipeline LRU also now tracks per-frame use and never evicts an actively used pipeline — the cap is temporarily exceeded instead (newevictionIdleMsoption, default 500ms).@miraiclip/rendererpipeline-eviction blackout (caught by the new stress suite): the pipeline cap (engine default 4, configurable) evicts least-recently-used pipelines, but clips cached theirs forever and every clip acquired one at mount — long multi-asset timelines, or timelines whose transition participants outnumbered the cap, went permanently black. Acquisition is now lazy and self-healing (VideoPipeline.isDisposed+ re-acquire on tick/prepare); eviction costs one frame of catch-up.@miraiclip/rendererunbounded GPU memory in software-GL exports: under SwiftShader/llvmpipe (headless CI, some VMs — real GPUs unaffected), Chromium retains one GPU shared-image per captured frame while decode and encode run together, growing a 5-minute export to ~4GB.exportProjectnow detects software WebGL and routes capture through a CPU mirror automatically (flat ~300MB); real GPUs keep the zero-copy path.
Added
- Production-readiness stress tier (
pnpm stress): many-clip playback tracking, bounded-heap long exports, decoder churn with a scrub storm, export throughput benchmarks (incl. optional 4K→1080p), and parallel server exports — nightly in CI with a metrics artifact. See Production Readiness. @miraiclip/renderercreateMediabunnySink({ cpuCapture })andisSoftwareWebGL(canvas)exported for custom export sinks in headless environments.
core-0.2.0 · renderer-0.3.0 · server-export-0.1.1 — 2026-09-12
The coordinated v4 creative-features release (server-export 0.1.1 = rebuilt harness bundling renderer 0.3.0).
Fixed
@miraiclip/rendererreplay flicker under animated opacity: play and seek-while-playing now hold the transport (≤400ms) until the target frame has decoded and arrived, so the clock never runs over an empty or stale frame cache — the black/stale pops an opacity ramp made visible at playback starts and replays are gone, and audio restarts in sync with the ready frame.
Added
@miraiclip/renderercustom clip-kind factories:createPlayerandexportProjectacceptfactories(clip kind → scene-node factory) — register a kind with core’sregisterClipKind, pass the same factories to both, and it renders identically in preview and export. See Rendering.- Docs: a live Examples showcase — full-width sections with variant carousels (every transition kind, effect preset, and caption style) running in your browser against the real engine; the code shown is the code executed.
@miraiclip/rendererkaraoke captions rendered (v4 step 5): caption clips draw with word wrap, per-line centering, and all four presets (plain, highlight, karaoke, pop); font assets load as real FontFaces in both the live player and exports — a server export never falls back to default glyphs; SRT/VTT and ASR imports render end to end; playground Captions tab added. Color-census e2e: passed words stay lit across a word boundary, the highlight flips sides exactly at it. See Rendering.@miraiclip/renderertransitions rendered (v4 step 4): crossDissolve, dipToBlack, dipToWhite, wipe, and slide now draw — blend kinds render both clips through the window from source headroom (each participating clip gets a dedicated decode pipeline), dips cover the cut with an overlay that is fully opaque exactly at the cut, and every kind applies an equal-power audio crossfade through the same automation math as keyframed volume (identical in preview and export). Pixel-asserted e2e incl. re-decoding a dissolve out of an exported file. See Rendering.- Playground quick-test side panel: Effects / Text / Animate / Transitions tabs of one-click preset cards + undo/redo, all driving the ordinary command surface. Transition cards split the video at the playhead, jump the incoming side 1s ahead (so the cut is visible), and bridge it.
@miraiclip/renderereffects rendered (v4 step 3): colorAdjust, blur (composition-relative strength — identical look in preview and export), and a GLSL chroma key (chroma-distance keying, soft edges, spill suppression) applied per clip through the compositor; filters update in place on param changes. Pixel-asserted e2e incl. re-decoding a keyed export. See Rendering.@miraiclip/rendereranimations applied (v4 step 2): the compositor evaluates keyframes every render (animated transform/opacity live on canvas; exports inherit it — same compositor), and animated volume rides linear gain ramps through shared automation math (volumeAutomation) used identically by live playback and the offline export mixer. Closed-loop e2e: an opacity ramp lands on exact pixel values; an exported volume fade shows the right RMS decay.@miraiclip/corev4 creative-features model (step 1): per-property keyframes with bézier-backed easings + a pure alloc-free evaluator (evaluateClipAt); per-clip effect stacks with built-in schemas (colorAdjust, blur, chromaKey — params in composition units); transitions on the adjacent-clips + trim-handles model with adjacency/headroom validation; karaokecaptionclip kind with font assets, SRT/VTT parsing, and ASR word-timestamp import;registerClipKind/registerEffectKind/registerTransitionKindextension seams. All new commands are in the command catalog. Renderer application lands in step 2.
server-export-0.1.0 — 2026-09-06
Added
@miraiclip/server-export(v3.x): server-side export from Node —exportProjectFile(doc, options)+ amiraiclip-exportCLI run the browser’s ownexportProjectin headless Chrome (pixel-identical output by construction). Self-contained harness bundle, loopback media server with Range support, asset path mapping, progress + abort across the process boundary, system-Chrome resolution via playwright-core (real Chrome recommended — free Chromium is WebM-only). Integration-tested against real headless Chromium in CI; runnable example inexamples/server-export. See the Server export guide. Docs: the Export guide is now a section with Client side and Server side pages.
renderer-0.2.0 — 2026-09-06
Added
@miraiclip/rendereroffline export (v3):exportProject→ MP4 (H.264+AAC) or WebM (VP9+Opus), full-resolution, faster than realtime (backpressure-paced, zero re-seeks, midpoint frame sampling, per-frame wait-for-arrival (fixes duplicated-frame judder), decode capped at 2× output size (fixes 4K-source export speed)), offline audio mix sharing live playback’s clip math, quality presets, up-front codec probing, AbortSignal cancellation (including mid-audio-mix), progress events for both phases (audio reports seconds mixed — long timelines decode their full audio). Playground Export button with In/Out range marks; closed-loop e2e verifies the file with an independent decoder (frame colors, duration, audio RMS). See the Export guide.@miraiclip/rendererpipelined export encoding: a bounded in-flight window of encoder submissions (encodeAheadFrames, default 4) plus an unclamped macrotask yield in the frame-arrival poll (nestedsetTimeoutis clamped to ~4ms) — decode, compositing, and encoding now overlap instead of running in lockstep, which had measured ~realtime with an idle CPU. Sinks capture the canvas synchronously insideaddVideoFrame(explicit contract); memory stays bounded and error/abort paths still cancel the sink exactly once. Playground export gains an output-fps select (Source/30/24) — a lower rate cuts export time proportionally.
renderer-0.1.0 — 2026-09-06
First release of @miraiclip/renderer — WebCodecs media pipeline, PixiJS compositor, audio-master playback (createPlayer), frame-accurate seeking, proxy preview decode. Everything below shipped in it.
Added
- Golden-frame e2e suite (Playwright): a deterministic fixture whose every frame encodes its own index as a color — frame-exact mid-GOP seek tests and a displayed-frames-track-the-clock playback invariant, GPU-independent (
pnpm --filter miraiclip-playground e2e; CI job included).createPixiBackendgainspreserveDrawingBuffer. @miraiclip/rendererproxy preview decode:createWebCodecsDecoderFactory({ maxOutputDimensionPx: 1920 })decodes large sources down to preview resolution on the GPU — fixes dropped frames on 4K60 playback (full-res was ~4 GB/s of frame copies + uploads). Scene nodes normalize scale against the source’s native size (setSourceSize), so clips render at the same size at any decode resolution;createWebCodecsDecoderstays full-res for export.@miraiclip/rendereraudio playback +createPlayerfacade: streaming-windowed audio decode scheduled on WebAudio (clip volume × track mute/solo, trim-aware, video clips’ embedded tracks included), audio-master clock, full transport, playhead pushed to the core as ephemeral state. Playground plays sound.- Rendering guide documenting the in-development
@miraiclip/renderer: the three layers, quick start, frame-accurate seek behavior, exact-frame API, and browser support. @miraiclip/renderervideo playback:createVideoSupportwires cached WebCodecs frames into compositor nodes (timeline→media time mapping incl. trim, throttled decode-ahead),renderFrameAtfor exact single frames, Pixi video textures, and a Vite playground app playing real MP4s end to end.@miraiclip/renderercompositor: patch-driven scene graph as a pure function of time behind aSceneBackendabstraction, clip-kind node factory registry, and the PixiJS backend for image/text clips.@miraiclip/rendererpackage started with the v2 media layer: frame cache with eviction budgets and strict frame ownership, keyframe-aware abortable video pipeline, a MediaManager capping decoder use with LRU release, Step/Realtime clocks behind theClockinterface, and mediabunny + WebCodecs browser adapters with per-asset capability errors.
Changed
@miraiclip/rendererframe-accurate seeking via the WebCodecs settle pattern: post-seek lead-in frames are decoded but never presented, so a seek holds the last frame and snaps straight to the target — verified in-browser against a worst-case 5s-GOP file, paused and mid-playback. Playground DOM writes throttled (were ~120 layouts/sec).@miraiclip/rendererseek performance audit: removedverifyKeyPackets(a hidden per-seek decode pass), cached the decoder config/capability check per asset, and merged the keyframe lookup into onechunksFrom(target)seek. Simplified the display back to “nearest decoded frame” and dropped the extra hold/tolerance/buffering machinery.@miraiclip/renderersmoother seeks: decoder reuse viareset()(no per-seek hardware re-init), WebCodecsoptimizeForLatency, cache-clear on hard seek, and deduped GPU uploads. A seek now holds the last frame through the decode gap and cuts cleanly to the target — no backward-jump/fast-forward shake — and the playground pauses the clock while seeking, resuming from the exact point.@miraiclip/renderervideo pipeline reworked to continuous streaming decode — one long-lived decoder fed forward with a backpressure window, re-seeking only on real jumps. Removes the periodic playback stutter from per-second decoder teardown. Playground duration cap removed; preview is video-only (audio is step 4).- Package homepage now points at this documentation site; docs linked from the package and repo READMEs; “not yet published” notes removed after the 0.1.0 npm release.
Fixed
@miraiclip/rendererhidden-tab playback: a timer now keeps decode/audio windows rolling while rAF is frozen, so audio no longer stalls ~3s after the tab is hidden (createPlayeraccepts injectableschedule/cancelSchedule). Playground: paused-seek UI staleness fixed (trailing-edge throttle).@miraiclip/rendererre-seek decisions now use a contiguity watermark (highest timestamp below which every frame has arrived): an out-of-order/straggling frame conversion can no longer be mistaken for an eviction and trigger a stream re-seek — profiling on 4K60 showed those spurious re-seeks (~1/sec, each redecoding 100–300 lead-in frames) were the remaining playback stutter. The watermark anchors at the seek target (not the first arrival), so a slow first conversion after a seek cannot re-trigger the seek. Frame-cache eviction is past-before-future (drop behind frames first): symmetric distance eviction was discarding fresh decode-ahead output whenever the behind-tail was short, causing a re-seek every cache-capacity of playback (~1.07s at 4K60).@miraiclip/renderer4K playback decode storm fixed: the decode-ahead window is sized by measured frame spacing (not reported durations, which some streams omit), and the frame under the playhead can never be evicted — dropped-frame playback measured at 3–6× decode overwork is gone. Clips also render fit-to-composition (scale 1 = contain) instead of native-pixel cropping.@miraiclip/rendererre-seeks are decided from stream state, not decode timing: async frame arrival (long at 4K) no longer reads as a cache miss, ending the re-seek loop behind the black-video + pegged-CPU reports. Decode-ahead adapts to the frame-cache byte budget (4K decodes less ahead instead of decoding into eviction), and the Pixi backend skips rendering when nothing changed (a paused frame no longer burns GPU at 60fps). Regression tests cover the async-arrival storm and the budget churn.@miraiclip/rendererblack video + pegged CPU on streams reporting zero frame durations (e.g. 4K60 H.264): duration fallback in the WebCodecs adapter, a strictly-before settle check (the frame at the seek target is always presentable), and a duration-defensive covering check in the pipeline (no more infinite reseek loop). Regression-tested.@miraiclip/renderermedia errors now surface:createPlayer/createVideoSupporttakeonError(error, clipId)(default: loudconsole.error) instead of silently swallowing decode and pipeline failures.- CI on fresh checkouts: packages now expose source
exportsin development withpublishConfigrestoring dist exports at publish — no pre-build needed for typecheck/tests/playground; published tarballs unchanged. - Periodic playback stalls: decoded frames starved the hardware decoder’s small output pool. Frames are now copied to ImageBitmaps and released immediately, and upload straight to the GPU (no 2D-canvas hop).
0.1.0 — 2026-09-05
First release of @miraiclip/core.
Added
- Project state — Zustand (vanilla) store: composition document under
state.doc, ephemeralplayheadUs/selectionbeside it, selector subscriptions. - Command engine —
dispatchwith Zod v4 validation and Immer application; typed errors leave state untouched on failure. - 15 built-in commands across
project/,asset/,track/, andclip/namespaces, with semantic validation (entity existence, track-kind constraints, asset-in-use protection, split-range checks). - History — undo/redo from inverse patches; atomic
transaction(fn, label?)with rollback on failure; configurable history limit. - Patches & events — every document change emitted as RFC-6902 JSON Patch ops with inverses and a
sourcetag;patches,history,playhead,selectionevents. - AI catalog —
commandCatalog()exports one JSON Schema per command type, suitable as LLM tool definitions. - Custom commands —
registerCommand({ type, schema, handler })with full validation/history/patch semantics. - Serialization —
toJSON()round-trips throughcreateProject, withschemaVersionchecking. - Timeline utilities — µs ↔ seconds/frames/timecode, frame snapping, range overlap.
- Sync utilities —
applyJsonPatchesandfromJsonPointer: apply emitted RFC-6902 patches to a plain document copy, the follower side of collaboration. - Determinism & collaboration tests — replayed command scripts converge, leader→follower patch sync, inverse-patch rollback, undo/redo round-trips, history-limit eviction, frame-boundary splits, JSON Pointer escaping (26 tests).
- Tooling — pnpm monorepo, tsup build (ESM + CJS + d.ts, verified exports), Vitest, strict TypeScript, CI, Changesets.
- This docs site — Hugo + Hextra, including the Command Catalog reference generated from the actual schemas.