miniav_ffi 0.7.4
miniav_ffi: ^0.7.4 copied to clipboard
A Dart FFI library for the miniav project, providing bindings to the native miniav C library.
miniav_ffi CHANGELOG #
0.7.4 #
- Increment downstream deps
0.7.3 #
-
New
MiniAV_Audio_SetCaptureMirror: the capture callback also writes each block into a caller-owned single-producer/single-consumer ring, so a consumer on another thread - a web worker sharing the wasm linear memory - can drain captured audio without calling into wasm at all. The ring header is eight u32 slots (monotonic write/read cursors, capacity, channels, dropped-frame count, sample rate), so occupancy is unambiguous across a 2^32 wrap with no full/empty flag;base = NULLdetaches. The cursor accesses map onto Interlocked intrinsics under MSVC, which has no__atomic_*builtins. -
MiniAV_AudioOutput_Configureno longer opens the wrong device. Adevice_idthat matched no enumerated playback device was replaced by the SYSTEM DEFAULT and reported as success, so a caller asking for one endpoint silently got another — indistinguishable, from the outside, from the request having worked. It now returnsMINIAV_ERROR_DEVICE_NOT_FOUND.- An EMPTY
device_idstill means "system default"; that is the supported way to ask for it, and callers who want a fallback can retry with it. - The refusal applies only when device enumeration itself SUCCEEDED. If the device list could not be read at all (a transient COM failure on Windows), the previous default-fallback behaviour is kept — absence cannot be proven without a list.
- Behaviour change in an error path: a caller relying on the silent
substitution will now see a failure instead of unexpected audio. Callers
that need a guaranteed-audible output should retry with
"".
- An EMPTY
-
MiniAV_AudioOutput_GetDefaultFormatnow honours itsdevice_idargument (it wasMINIAV_UNUSEDand always answered for the system default device, while the interface promised "for a device"). The id is the device name, matching the portable-id conventionConfigureresolves; an unmatched name falls back to the default with a warning, same policy asConfigure.
0.7.2 #
- released 08/13/26 - MR
0.7.1 #
-
<miniav.h>now includesminiav_playback.h. The umbrella header pulled in types/buffer/capture but not playback, stranding all 21 publicMiniAV_AudioOutput_*declarations. A C consumer that included only<miniav.h>, as the docs say to, got an implicit declaration forMiniAV_AudioOutput_CreateContext— which C assumes returnsint, silently truncating the returned 64-bit handle and segfaulting inside miniaudio on first use. Dart/FFI callers were never affected (they bind symbols directly). Trap: the symbols were exported all along, so this never failed to LINK — it failed at the language level, and the crash surfaces far from the include. Any new public header must be added tominiav.h. -
Windows per-process loopback had never worked: every
pid:target received the WHOLE SYSTEM's audio. The backend calledIAudioClient3::InitializeSharedAudioStream(flags, <PID>, fmt, NULL), putting the target PID in thePeriodInFramesargument. A PID is never a legal period, so the call always failed withAUDCLNT_E_INVALID_DEVICE_PERIOD(0x88890021) and the backend fell through to whole-system loopback — right format, wrong content, no error above DEBUG, and indistinguishable from correct output by inspecting the PCM. Real per-process capture is now implemented withActivateAudioInterfaceAsyncagainstVIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACKandAUDIOCLIENT_ACTIVATION_PARAMS, modePROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE(apps routinely render audio from child processes; excluding the tree would make browsers, Electron shells and launchers silent). Requires Windows 10 build 20348 / Windows 11. Verified with a two-tone oracle: two child processes render 997 Hz and 3001 Hz, and apid:capture of either contains only its own tone at ~40000x separation, while a whole-system capture contains both (src/loopback/test/test_loopback_process_isolation.c; positive controlMINIAV_LOOPBACK_STRESS_NO_PROCESS_LOOPBACK=1+MINIAV_LOOPBACK_ALLOW_SYSTEM_FALLBACK=1reproduces the old substitution). -
Behaviour change: per-process loopback that cannot be delivered now FAILS
Loopback.configureinstead of silently substituting whole-system audio. Applies to an unsupported Windows build, an activation failure, or a PID that is not running.MINIAV_LOOPBACK_ALLOW_SYSTEM_FALLBACK=1opts back into the substitution, which is then logged at ERROR. Whole-system loopback (aNULLor MMDevice-ID target) is untouched. -
New:
MiniAV_Loopback_GetActiveTargetInfo(C API; not in the Dart bindings) reports the scope the backend ACTUALLY achieved —MINIAV_LOOPBACK_TARGET_PROCESSwith the PID, orMINIAV_LOOPBACK_TARGET_SYSTEM_AUDIO— as opposed to the one requested.MINIAV_LOOPBACK_TARGET_NONEmeans the backend does not report scope (macOS/Linux). Windows always reports one of the first two. -
Per-process loopback genuinely negotiates, unlike endpoint loopback: the requested rate/channels/format is honoured and converted into (48 k, 44.1 k and 16 kHz verified), and it taps the target BEFORE the endpoint master volume, so it is typically louder than the same audio via system loopback. The format read-back contract from earlier in 0.7.1 is unchanged — whatever the stream is actually initialized with is what
getConfiguredFormat()and every buffer'sinforeport. -
Traps, all three of which fail silently or misleadingly: (1)
ActivateAudioInterfaceAsyncreturnsE_ILLEGAL_METHOD_CALL(0x8000000E) — before looking at the activation params at all — unless the completion handler is agile: it must answerQueryInterfaceforIAgileObjectand delegateIMarshalto a free-threaded marshaler. C++ samples inherit this from WRL'sFtmBase, so it is invisible in the docs. (2)IAudioClient::GetMixFormatreturnsE_NOTIMPLon a process-loopback client — there is no endpoint format to inherit, the caller must supply one. (3) Activating for a nonexistent PID succeeds and then delivers zeroed frames forever; the PID is therefore validated withOpenProcessfirst (ERROR_ACCESS_DENIEDcounts as alive — elevated targets do work). -
MiniAVInputConfigbindings were 16 bytes short of the C struct, so everyInput_Configureread past the end of its own allocation. The C struct grew three appended motion fields (motion_rate_hz,motion_mode,motion_callback) — 48 → 64 bytes — whileminiav_ffi_bindings.dartstill described the 7-field, 48-byte shape.calloc<MiniAVInputConfig>()sized the allocation from the short definition andinput_api.c'sctx->config = *config;copied 64 bytes out of it, so 16 bytes of unrelated heap landed inctx->config.motion_*;miniav_input_deliver_motioncallsmotion_callbackas a function pointer (iOS/Android backends read the same two fields straight from the caller's buffer duringconfigure). The bindings now mirror the C struct (MiniAVVec3,MiniAVQuat,MiniAVMotionEvent,MiniAVMotionMode,MiniAVAttitudeRef,MiniAVDisplayRotation,MiniAVMotionCallbackadded), andcopyInputConfigToNativewrites the motion trio explicitly zero/null. Motion is still not delivered over FFI — the fields are present and inert, andMINIAV_INPUT_TYPE_MOTIONis never requested, so no backend starts a sensor. Trap: appending a field keeps OFFSETS stable but changessizeof(), andsizeof()is what callers allocate with and what struct assignment copies — "ABI-additive" never meant a stale binding was safe. -
New:
MiniAV_ABI_StructSize/MiniAV_ABI_StructCount/MiniAV_ABI_StructNameAt(src/common/miniav_abi.c) exportsizeof()for every struct that crosses the FFI boundary.test/abi_struct_size_test.dartassertssizeOf<T>()against C for all of them and fails if C adds a struct Dart does not check. Audited with it and clean:MiniAVDeviceInfo513,MiniAVVideoInfo24,MiniAVAudioInfo16,MiniAVLoopbackTargetInfo16,MiniAVKeyboardEvent24,MiniAVMouseEvent48,MiniAVGamepadEvent32,MiniAVVec324,MiniAVQuat32,MiniAVMotionEvent232,MiniAVVideoPlane40,MiniAVBuffer264,MiniAVNativeBufferInternalPayload72, plus the anonymousMiniAVLoopbackTargetInfo.TARGETHANDLE/MiniAVBuffer.data{,.video,.audio}members.MiniAVInputConfigwas the only mismatch. -
Windows loopback reported the REQUESTED audio format, not the endpoint's, and stamped every delivered PCM buffer with it.
MiniAV_Loopback_Configurecached the caller's request intoconfigured_video_formatright after the backend had stored the real WASAPI mix format there. WASAPI shared-mode loopback has no format negotiation — it always runs at the endpoint mix format — so on a 44.1 kHz or 5.1 endpoint the bytes were right and the label was wrong, with no error and no log; muxing that audio wrote the wrong sample rate into the file header. Both Windows screen backends hardcode a 48 kHz / 2 ch request, so this hit every screen recording with audio. Configure now reads the format back from the backend (get_configured_video_format) and caches THAT. Contract, matching the PipeWire and CoreAudio backends: the negotiated format wins and is reported back —MiniAV_Loopback_GetConfiguredFormatandMiniAVAudioBuffer.infodescribe what is actually delivered; a request the endpoint cannot provide is logged at WARN, not faked and not failed.num_framesis still carried through from the request (a caller-side chunk hint WASAPI does not negotiate). Trap: a backend that writes its result into the shared context field can have it clobbered by the generic layer immediately afterwards — PipeWire/CoreAudio were unaffected only because they keep theirs in the platform context. -
Windows window capture with
captureAudio: truedelivered video only, and returned success. The WGC backend formatted its per-process audio target as"PID:%lu"whileMiniAV_Loopback_Configurematched a case-sensitive"pid:", so the ID fell through to the "assume an MMDevice ID" branch,IMMDeviceEnumerator::GetDevice(L"PID:1234")failed, and audio was disabled with a WARN. Producers now emit lowercasepid:(canonical, same shape ashwnd:) and the consumer matches case-insensitively so IDs from older builds still resolve. Behaviour change:MiniAV_Screen_ConfigureWindow/ConfigureDisplay/ConfigureRegionandMiniAV_Screen_StartCapturenow return an error whencapture_audiowas requested and no audio path could be established, instead of returningMINIAV_SUCCESSwith a video-only capture. Both Windows screen backends (WGC and DXGI) answer the same way. Callers that want video regardless should configure withcaptureAudio: false. Note: per-process loopback still falls back to whole-system audio (seedocs/PLATFORM_SUPPORT.md). -
Windows WGC: a window resized mid-capture could be handed a
dataSizeBytespast the end of the mapped buffer. TheDirect3D11CaptureFramePoolwas sized once fromcapture_item.Size()at StartCapture and never revisited, while per-frame dimensions came from the liveframe.ContentSize(); the CPU path then computedRowPitch(pool-sized) * height(new, larger)(measured: 1,180,160 bytes advertised over a 903,680-byte mapping), and the GPU path had the same shape. The pool is now recreated (Direct3D11CaptureFramePool::Recreate) when the content size changes, and the reported extent is clamped to what was actually mapped/allocated either way. Recreation runs under the context's critical section — the same lock teardown holds while closing the pool — after the frame is closed and outside the callback drain, so it cannot deadlock against the existingWGCCallbackRef/wgc_drain_callbacksprotocol. New exportsminiav_wgc_debug_oversize_reports()andminiav_wgc_debug_pool_recreates()back the regression harness. Trap: a frame pool's surfaces are always pool-sized;ContentSize()is the target's live size and the two are only equal until something resizes. -
New Windows-only C regression harnesses, both with positive controls that restore the pre-fix behaviour and fail if the bug does not reproduce:
src/loopback/test/test_loopback_format_honesty.c(MINIAV_LOOPBACK_STRESS_REQUESTED_FORMAT=1,MINIAV_LOOPBACK_STRESS_CASE_SENSITIVE_ID=1) andsrc/screen/test/test_wgc_resize_stress.c(MINIAV_WGC_STRESS_NO_POOL_RECREATE=1,MINIAV_WGC_STRESS_UPPERCASE_PID=1,MINIAV_SCREEN_STRESS_AUDIO_OPTIONAL=1). -
Windows camera: destroying a context no longer frees the Media Foundation source-reader callback while MF still holds it.
MFPlatformContextis theIMFSourceReaderCallbackgiven to the source reader, so MF holds a reference and drops it asynchronously on an RTWorkQ thread — afterIMFSourceReader_Releasehas returned.mf_destroy_platformfreed the object outright andMFPlatform_Releasedeliberately did nothing at refcount 0, so MFReadWrite then called through a freed vtable and killed the process with0xC0000005at a heap address. Release now frees on the last reference (whichever thread holds it) and destroy drops only its own. No API change. Traps:parent_ctxmust be cleared under the callback's critical section and re-read inside it (a callback that latched the pointer before blocking on the lock would use the parent after the caller freed it); and the critical section must be deleted in the final release, not in destroy. -
Windows COM/Media Foundation initialisation is now process-lifetime and library-owned (
src/common/miniav_com_win.{h,c}; oneCoIncrementMTAUsageplus oneMFStartup, never released). It used to be driven per FFI entry point in the camera (MF), loopback (WASAPI) and screen (WGC) backends.CoInitializeEx/CoUninitializeare PER-THREAD and a Dart isolate does not own a fixed OS thread: measured on this suite, one isolate's consecutive FFI calls ran on three different pool threads, and single pool threads served two different isolates. ACoUninitializetherefore evicted a thread another isolate believed it owned, and the last thread leaving the MTA destroyed every COM object in it. No API change; single-consumer behaviour is unchanged and an STA host keeps its STA. Traps: releasing on a refcount edge would reintroduce the race (the count can reach zero while another isolate's objects are live), so init is deliberately monotonic; the held MTA reference is also what makes miniaudio's own unpairedCoUninitializesurvivable, soMINIAV_COM_ENSURE_MTA()must stay ahead of everyma_context_init.MINIAV_COM_TRACE=1logs each call site with its OS thread id. -
The WASAPI loopback capture thread now joins the MTA explicitly. It calls
IAudioCaptureClient/IAudioClientmethods but had never entered an apartment at all; it worked only because the process-lifetime MTA reference above makes an uninitialised thread an implicit MTA member and because these are direct in-process vtable calls with no marshalling — an accident of the current design rather than a contract. It now pairsCoInitializeEx/CoUninitializearound the thread body. Trap, and the distinction that makes this correct where per-call COM was not: a DEDICATED thread we create and join is the one place the pair is sound, because both calls run on the SAME thread with balanced lifetime — unlike FFI entry points, where the Dart VM hands an isolate's consecutive calls to different pool threads. The process MTA reference additionally guarantees thisCoUninitializecan never be the last one out.RPC_E_CHANGED_MODEis handled by NOT uninitialising (it would unbalance whoever put the thread in an STA). -
Windows screen capture (WGC): stopping or destroying a context now waits for in-flight capture callbacks. WinRT dispatches
FrameArrived/GraphicsCaptureItem.Closedon threadpool threads, and revoking an event token does not wait for a handler that is already running — while the FPS pacing loop deliberately blocks for up to a frame interval after releasing the context lock. Teardown therefore closed the pacing timer and stop event the callback was waiting on, deleted the critical section and freed the context under it. Stop/destroy now clearis_streaming, signal the stop event, revoke the tokens, release the lock, then drain an in-flight callback count (2 s bound, logs on expiry) before freeing. Capture-lost notification also takes the lock to readlost_cb, and invokes it unlocked so an app may stop or destroy from inside it. No API change; teardown may now block for the few ms a callback needs to unwind. Traps: the drain MUST run with the critical section released (an in-flight handler acquires it — draining under it deadlocks); and the pacing loop's no-waitable-timer fallback used a bareSleep()that ignored the stop event, parking teardown for a whole frame interval — it now waits on the event, keeping the same absolute deadline. -
setLogCallbacknow delivers through a Dart native port instead of aNativeCallable(new C exportsMiniAV_InitDartApi/MiniAV_SetLogPort;miniav_c/third_party/dart_dl/vendors the BSD-licensed Dart SDK dynamic-linking API).MiniAV_SetLogCallbackis a PROCESS-GLOBAL registry: the installed pointer is owned by ONE isolate, and when that isolate exits the VM deletes the trampoline while the C library keeps the pointer — the nextminiav_log()from a capture thread, or from inside a leaf FFI call such asMiniAV_ReleaseBuffer, aborted the whole process (runtime_entry.cc: Callback invoked after it has been deleted). A whole-suitedart testhit it every run, since every test FILE is a separate isolate in ONE VM process. Semantics are unchanged and now documented: PROCESS-GLOBAL, LAST WRITER WINS — with several isolates registered only the most recent receives lines. Trap: the message crosses as raw bytes, not aDart_CObjectstring, because device names can carry Latin-1 andDart_CObject_kStringrequires valid UTF-8.MiniAVLogCallbackstays in the C API for non-Dart embedders, which own their function's lifetime; on the web/wasm buildMiniAV_InitDartApireturnsMINIAV_ERROR_NOT_SUPPORTEDand the port path is inert. -
stopCapture()no longer closes aNativeCallablethe native side is still holding. Loopback, input and the device-change registry closed theirs unconditionally and onlyprinted the native result — butMINIAV_ERROR_TIMEOUTis precisely the code the C layer returns to say "the thread did not join and still has your pointer" (which is why it deliberately leaks its own context there). Those paths now drop the callable without closing it, matching the native leak; closing it was a use-after-free that aborted the VM. Camera, screen and audio input were already correct (their backends fence under a critical section or a device join). -
hook/build.dartregistersminiav_c/**sources as build dependencies. Without them an edited.c/.hkept serving the previously built DLL, so a newly added export resolved to nothing and the failure looked like a Dart bug. -
code_assets,hooksandloggingare now real dependencies.hook/build.dartimports them and a consumer never receivesdev_dependencies, so the native asset could not build downstream.dart pub publishreports this as an error; 0.7.0 shipped past it because publishing runs with validation skipped. -
miniav_platform_interfaceis now a caret range (^0.7.0) rather than an exact pin, which had made every patch release of it unsatisfiable alongside this package.
0.7.0 #
GPU buffer handoff contract (Windows) — leak fixes #
- The shared NT handle handed out on the GPU path is now closed by miniav in the release path of all three Windows backends (WGC screen, DXGI screen, MF camera). It was previously only logged as "app is responsible", and no caller closed it: one leaked kernel handle per captured GPU frame. The DXGI backend did not even store the handle, so it was unclosable. Callers must import the handle before releasing the buffer and must not close it themselves. WGC additionally keeps an outstanding-handle counter and logs an ERROR at context destroy if any handle was never released.
- Buffers queued to a
NativeCallable.listenerare no longer dropped on close.stopCapture()/destroy()closed the callable immediately, andclose()discards undelivered messages — a dropped buffer never reachesMiniAV_ReleaseBuffer, leaking its D3D11 texture reference and shared handle. Screen and camera contexts now stop the native capture first, drain pending callbacks into a release-only branch, and close afterwards.destroy()also no longer skips the native StopCapture. MiniAVBuffer.native_fenceis documented inminiav_buffer.has never-populated, together with the 16 ms busy-poll and proceed-on-timeout behaviour that stands in for it.
Mobile platform catch-up: first-class Android and iOS backends for
camera and screen capture, per miniav_c/MOBILE_PLATFORM_SPEC.md (six Opus
implementation agents + an 8-dimension adversarial review — 17 raw findings,
11 confirmed, all fixed). Mic capture on both platforms rides the existing
portable miniaudio module. No mobile input tier in v1; Android loopback
(AudioPlaybackCapture) deferred.
New backends #
- Android camera (Camera2 NDK, API 24+). ACameraManager enumeration
(facing in the device name), AImageReader YUV_420_888 CPU path with
truthful per-frame plane labeling (NV12/NV21 by chroma pixel-stride;
planar always delivered as I420 with explicit per-plane pointers), and a
runtime-gated (26+)
AHARDWAREBUFFERGPU path. Camera clock nanoseconds →miniav_rebase_time_us. No owned threads: the NDK looper model is documented in the backend header. - Android screen (MediaProjection, effective floor 26+). The app supplies
a consented
MediaProjectionvia the newMiniAV_Screen_SetAndroidMediaProjection(jvm, projection)seam (global-ref ownership transfers to native; clearing with a NULL projection is the authoritative stop signal and fireslost_cb). Native builds the AImageReader→Surface→VirtualDisplay pipeline; RGBA CPU path + AHardwareBuffer GPU path; drop-oldest under backpressure. - iOS camera (AVFoundation port of the macOS backend). Discovery-session
enumeration with front/back naming, NV12-preferred formats, the same planar
Metal zero-copy texture path (UMA), permission gate returns
PERMISSION_DENIEDwithout prompting, interruption-aware one-shotlost_cb. Sensor-native orientation in v1. - iOS screen (ReplayKit, two tiers).
app_screen= in-app RPScreenRecorder (video CPU+Metal GPU paths, app-audio + optional mic).system_screen_broadcast= system-wide capture via a Broadcast Upload Extension: the new producer kit (miniav_broadcast_sender+ reference SwiftSampleHandler+SETUP.md) writes NV12 into a page-aligned App-Group shared-memory ring (the pipeline's only pixel copy); the host wraps ring slots zero-copy withnewBufferWithBytesNoCopy+ Metal texture views and can deliverGPU_METAL_TEXTURE. Drop-oldest slot leases tied toMiniAV_ReleaseBuffer; host app group set viaMiniAV_Screen_SetIOSAppGroup. Protocol pinned inminiav_broadcast_protocol.h. - iOS mic session shim.
AVAudioSessionPlayAndRecord (+MixWithOthers, +DefaultToSpeaker) is activated around miniaudio start/stop — balanced on every failure path — so shared mic capture works on iOS unchanged.
API / infrastructure #
- New error code
MINIAV_ERROR_PERMISSION_DENIED (-23); miniAV never prompts — apps request OS permissions first. Error-string table completed for all codes;MiniAV_GetVersionString()now reports the real version. - Dart bindings:
MINIAV_ERROR_PERMISSION_DENIEDadded toMiniAVResultCode(previously an unknown code madefromValuethrowArgumentError), andMiniAV_Screen_SetIOSAppGroupis bound with aMiniFFIScreenPlatform.setIOSAppGroup(String)implementation. - Android JNI plumbing (
common/miniav_jni_android): explicitJavaVM*publication (dlopen does NOT runJNI_OnLoad; the C-API seam is authoritative), per-thread attach/detach helpers. - Platform-gate hardening: Android no longer falls into bare
__linux__arms, iOS no longer falls into bare__APPLE__arms (loopback/input are cleanlyNOT_SUPPORTEDon mobile);miniav_timed_joincorrectly gated to glibc. CMake: iOS deployment target 13.0, Metal linked on iOS, Android link floor API 24 with__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__for runtime-gated 26+ APIs. New GitHub Actions matrix (.github/workflows/native-build.yml): windows / linux / macos / android (arm64-v8a, API 24) / ios configure+build legs. - Flutter consent piece (in
miniav_flutter): Kotlin MethodChannel plugin (requestScreenCapture()), mediaProjection-typed foreground service started beforegetMediaProjection(Android 14 ordering), Java-sideMediaProjection.Callback.onStoprelay to both Dart and native, JNI shim handoff to the C seam.
Remote-desktop primitives (desktop output/control direction) #
Adds the sink/control half of the A/V/Input layer so a cross-platform
remote-desktop client/server can be built on miniAV. Per
miniav_c/REMOTE_DESKTOP_AV_PLAN.md (three Opus per-platform agents + a
three-dimension adversarial review — 1 major + 3 minor findings, all fixed).
Audio/video playback is intentionally NOT here — it becomes a future
_tools media player (bring-your-own / bundled codec, GPU hotpath preserved).
- Input injection — new
MiniAV_Inject_*module (the sink twin of input capture): replays synthetic keyboard/mouse events onto the local machine. WindowsSendInput(compiled + linked + smoke-tested), macOSCGEventPost(Accessibility-gated, MRC), Linux/dev/uinput(works under X11 and Wayland). Handles keyboard down/up, mouse absolute/relative move, all buttons (incl. X1/X2), and vertical + horizontal wheel. The sameMiniAVKeyboardEvent/MiniAVMouseEventstructs used by capture are replayed, so a captured event injects verbatim on the same platform. Codes are platform-native; cross-platform translation is the caller's job. Permissions surface asMINIAV_ERROR_PERMISSION_DENIED(macOS Accessibility; Linux/dev/uinputaccess) — miniAV never prompts. Gamepad injection is out of scope in v1 (needs a virtual-gamepad driver). - Cursor in captured frames —
MiniAV_Screen_SetCaptureCursor(ctx, bool)(call before configure; off by default). Honored on Windows WGC (IsCursorCaptureEnabled), macOS ScreenCaptureKit (showsCursor), and Linux PipeWire (portal embedded cursor mode). Windows DXGI Desktop Duplication cannot draw the cursor — it logs a warning and captures cursor-less; use the WGC backend when you need the cursor. - Horizontal scroll —
MiniAVMouseEventgainswheel_delta_x(horizontal wheel, populated by all three capture backends) alongside the existing verticalwheel_delta, plusis_absolute(capture sets it true; injection uses it to pick absolute vs relative move). Dart FFI struct updated to match (byte-exact, review-verified). - Dart:
MiniInject/MiniInjectContext,MiniScreen.setCaptureCursor, and the new mouse fields wired through all five packages; web reports injection as unsupported. - Review fixes: Linux abs-positioning no longer no-ops (a single uinput device advertising both relative and absolute axes was classified as a mouse and ignored its ABS axes → split into a relative-mouse + absolute-pointer device); Linux partial-event-on-EAGAIN and cross-platform wheel over-scroll hardened; macOS relative-move bursts now accumulate against a shadow cursor instead of a stale async pointer read. Linux ABS device classification is the one item still pending real-compositor verification (see the plan doc).
0.6.0 #
- Camera timestamps are now real microseconds on the shared monotonic
epoch on all three platforms. Windows/Media Foundation stored the 100 ns
REFERENCE_TIME into
timestamp_usunconverted (10× too large, wrong epoch); Linux/PipeWire stored the nanosecond graph-clock value (1000× too large); macOS/AVFoundation used the raw CMSampleBuffer PTS (session epoch, float math). All three now convert with integer math and rebase through the new sharedminiav_rebase_time_us()(common/miniav_time.h) — first-sample anchored againstminiav_get_time_us(), automatic re-anchor on device-clock discontinuities. The macOS ScreenCaptureKit screen path rebases its sample PTS the same way. Verified on hardware: c922 webcam median inter-frame delta 32 ms at 30 fps. - Linux loopback audio no longer leaks (and no longer aliases freed
memory). Every delivered buffer now carries the standard audio release
payload (
internal_handle), and the PCM is copied out of PipeWire's ring buffer before thepw_bufferis requeued — previously the delivered pointer aliased memory PipeWire immediately reused (use-after-free for any async consumer) and no buffer was ever freeable. MiniAV_SetLogCallbackactually works now. The registered callback was stored and never invoked — all native logs went to stderr only, i.e. nowhere in GUI apps.miniav_lognow delivers to the callback when one is set (stderr as fallback). Contract note: the message is heap-allocated and owned by the receiver (release withMiniAV_Free) because receivers may dispatch asynchronously — the Dart FFI shim (NativeCallable.listener) does exactly that, and now decodes + frees accordingly.- Device-lost notifications (
lost_cb) wired across the board. Previously only DXGI screen, WASAPI loopback, and mic input fired it — everywhere else a hot-unplug/permission revoke was a silent permanent stall:- WGC screen:
GraphicsCaptureItem.Closedhandler + device-removed detection in the frame path (one-shot, unblocks the pacing wait). - macOS ScreenCaptureKit:
didStopWithError:now flipsis_streamingand fireslost_cb(it previously only logged). - Linux camera + Linux loopback: PipeWire stream error states fire
lost_cbbefore teardown. - macOS camera: AVCaptureSession runtime-error + device-disconnected notification observers.
- macOS loopback:
kAudioDevicePropertyDeviceIsAlivelistener on the tap/aggregate or virtual device.
- WGC screen:
- macOS teardown races fixed. Camera and screen stop/destroy paths now
drain their delegate/sample dispatch queues (
dispatch_syncbarriers) before callbacks are cleared or the context is freed, and ScreenCaptureKit stop waits (bounded) for the stream's stop completion — previously in-flight frame callbacks could race context teardown (use-after-free class). - macOS ScreenCaptureKit
StartCapturereports real failures. The async setup chain is now awaited (bounded, 10 s →MINIAV_ERROR_TIMEOUT); permission/setup failures return an error instead of "success + zero frames forever". - Windows camera GPU path: the shareable-copy texture is now owned by the
frame payload and released in
release_buffer(it leaked one texture per GPU frame — both on success and on share-failure paths); the device context is flushed beforeCreateSharedHandle(same producer-side race the screen path guards against); frame-payload cleanup now interprets the cpu/gpu union by the path actually taken (a GPU-preference frame that fell back to CPU was cleaned up as GPU — misreading CPU pointers as COM objects). MiniAV_ReleaseBufferno longer leaks the payload wrapper on invalid-context/unknown-handle-type branches, and its logs no longer claim to free things they don't.- DXGI screen:
GetConfiguredFormatsworks again (is_configuredwas never set on this backend); removed the vestigial 4th parameter fromdxgi_configure_displaythat mismatched the ops-table function-pointer type (formally undefined behavior). lost_cbcontract formalized (MiniAVContextLostCallbackdocs): the callback runs on internal capture/notification threads — do not synchronously call StopCapture/DestroyContext from inside it (several backends join/drain the delivering thread; the Linux stop paths now also detect and refuse a self-join defensively). The Dart FFI shim satisfies the contract automatically via its asynchronous listener delivery.- The whole pass was itself adversarially reviewed (25-agent diff review, 11
confirmed findings, all fixed): the SCK async start chain now uses a
generation/abandonment protocol so a timed-out start can never
use-after-free a destroyed context (destroy waits for — or deliberately
leaks rather than frees under — a still-pending chain); SCK destroy uses
the same bounded stop as stop_capture; macOS dispatch_sync drains carry
same-queue reentrancy guards; one-shot lost_cb guards are atomic on all
platforms; the WGC
Closedregistration is revoked on stop (no stacking across stop/start cycles); the Linux loopback path acquires the dispatch guard before allocating (no per-quantum leak afterMiniAV_Dispose); PipeWire's signed buffer time is validated before rebasing; MF's timebase also recalibrates on reconfigure. - Added
miniav_c/NATIVE_AUDIT.md— the full cross-platform audit (parity matrix, remaining P1/P2 findings, improvement roadmap). - Shutdown is now bounded on every platform. The Linux PipeWire
screen/camera/loopback stop paths use a new
miniav_timed_join()(5 s) and returnMINIAV_ERROR_TIMEOUTinstead of hanging forever on a wedged compositor/PipeWire call; WASAPI's stop no longer waitsINFINITEon the capture thread; the device watcher bounds its poll-thread join (a wedged platformenumerate()can no longer hangMiniAV_Dispose). Destroy paths retry the join and, if a thread genuinely will not exit, deliberately LEAK the platform context (loudly logged) rather than free memory a live thread still dereferences. - The callback-dispatch quiesce guard is real on Linux/macOS — a
pthread_rwlockmirror of the Windows SRWLOCK implementation. Previously the non-Windows stubs madeMiniAV_Dispose's "block until in-flight callbacks drain" guarantee (Flutter hot restart) a no-op. - Mic-input lifecycle hardening (shared miniaudio module): a new
device_initedflag decouples teardown fromis_running, so Stop/Destroy after a device-lost notification actually uninitializes the device (previously silently leaked, device + worker thread); device-lost fires exactly once per run; DestroyContext force-uninits if Stop fails. - Format truth-telling:
- Windows camera: after committing a media type the reader's ACTUAL committed type is read back into the configured format (drivers may adjust), and frame-rate matching is rational (30000/1001 now matches an equivalent expression) instead of exact numerator+denominator equality.
- Linux camera: the negotiated stream format is written back to the
configured format (frames were stamped with the original request), and
GetSupportedFormatsno longer truncates the device's mode list to one entry — enumeration completes on the core sync-done event after ALL EnumFormat params have arrived. - macOS loopback: the tap/aggregate (and virtual-device AudioUnit) ACTUAL
negotiated stream format is read back after start (frame counts were
computed from the requested format, with an unguarded division);
GetDefaultFormatqueries the real target/default-output device instead of returning a hardcoded 44.1 kHz constant;GetSupportedFormatsis implemented (was a NULL op that failed unconditionally). - Linux loopback: the negotiated audio format is persisted to the configured format; the format-query stubs now truthfully describe PipeWire's format-adaptive semantics instead of warning about a hardcoded device constraint.
- Mic input:
GetDefaultFormat/GetSupportedFormatsquery the actual device's native formats via miniaudio instead of returning hardcoded tables (the old code ran a dead enumeration loop purely to decorate a log line).
- Input capture now exists on Linux and macOS (was Windows-only). New raw
evdev backend (
/dev/input/event*, no libudev) and new CGEventTap + GameController backend deliver keyboard/mouse/gamepad events. The Windows input backend was hardened: shared monotonic clock, single-active-context guard (a second concurrent capture is rejected, not silently hijacked), absolute-QPC gamepad pacing (was a driftingSleep(1000/hz)), and it no longerTerminateThreads the hook thread (which would have leaked a systemwide hook). - macOS camera GPU path honors planar formats — NV12/I420 are now delivered
as per-plane Metal textures instead of silently downgrading to CPU, and the
buffer carries a signaled
MTLSharedEventinnative_fence. - macOS system-audio loopback works on stock macOS 13+ — a ScreenCaptureKit audio tier is tried ahead of the third-party virtual-device requirement (no BlackHole needed).
- macOS screen region capture implemented via
SCStreamConfiguration.sourceRect(was unsupported). - Clock conversions are overflow-safe — QPC→µs and mach-time→µs use whole/remainder split arithmetic so they don't wrap on weeks-scale uptime.
- Media Foundation camera now escalates a persistently-failing read to device-lost (30 consecutive failures) instead of spinning the re-arm loop forever on a device-lost HRESULT outside the fixed terminal set.
- GPU-sync poll no longer silently proceeds — the pre-share
D3D11_QUERY_EVENTwait on the DXGI/WGC zero-copy paths is bounded at 16 ms and logs (rate-limited) on timeout, surfacing a black-frame risk under GPU contention instead of hiding it. - Deferred (documented in
NATIVE_AUDIT.md, both peak improvements not defects): fullnative_fencehandoff to the encoder consumer, transparent WASAPI default-device reroute, and Windows screen region-crop. - Waves 5+6 were adversarially reviewed (17-agent diff review, 6 confirmed
findings + several polish items, all fixed): the new Linux evdev backend
coalesces per-axis mouse deltas into one throttled MOVE per report (a
diagonal move was dropping its Y axis) and widens stick normalization to
64-bit (32-bit overflow on LP32 arches); the macOS input backend no longer
destructively clears keyboard/mouse from the configured set on a transient
permission failure (a later restart re-attempts the tap), guards its worker
run loop against a busy-spin, and clamps the gamepad poll rate; macOS
SCK-audio stop now tears down an abandoned (timed-out) SCK chain and falls
through to stop the virtual-device fallback instead of returning early;
macOS region capture adopts the caller's frame rate and parses the
display_%uid (was 0/0 fps and always the main display); the Windows input gamepad-creation-failure rollback routes through the never-TerminateThreadstop path; and the misleading empty-command-buffer macOS camera fence was removed (the IOSurface path already serializes). - Waves 3+4 were themselves adversarially reviewed (18-agent diff review, 7
confirmed findings, all fixed): the leak-instead-of-free destroy protocol
now returns
MINIAV_ERROR_TIMEOUTand theMiniAV_*_DestroyContextAPI layers leak the PARENT context too (the wedged thread dereferences it — leaking only the platform half was still a use-after-free); normalMiniAV_Audio_StopCaptureno longer fires a spuriousMINIAV_ERROR_DEVICE_LOST(miniaudio posts its "stopped" notification during uninit —is_runningnow clears first); device formats with no MiniAV equivalent (e.g. s24) fall back to F32 instead of reporting format 0; the Linux camera negotiated-format write-back is mutex-guarded against torn reads fromGetConfiguredFormat; the format-enumeration loop cannot hang when a node never reports info (initial core sync); the macOS loopback format read-backs happen BEFORE IO starts (realtime-thread publication order + render-buffer sizing) with the deprecated CoreAudio selector locally silenced; the GLib loop timeout path detaches coherently.
0.5.11 #
- Windows screen capture (WGC + DXGI): frame pacing rewritten against an
absolute QPC schedule slept on a high-resolution waitable timer. The old
relative sleeps (WGC's
Sleep(interval - 2), DXGI's GetTickCount64 + integer-msSleep) systematically over-delivered ~5% in timer-resolution-raised processes (any Flutter app): a 30 fps target delivered ~31.4 fps, and the recorder's fps throttle then deleted the surplus frame every ~20 frames — one double-length presentation hole every ~0.7 s, a metronomic, clearly visible stutter in recordings. In default-resolution processes the same sleeps tick-rounded the other way (~46.9 ms spacing ≈ 21 fps). Deliveries now land on the exact requested rational interval (measured mean 33.33 ms for a 30 fps target); after a stall or idle stretch the schedule resyncs instead of bursting stale catch-up frames, and the pacing wait watches the stop event so shutdown stays responsive mid-interval.
0.5.10 #
- DXGI screen capture: best-effort GPU scheduling boost at capture start so
capture keeps its cadence when another process (e.g. a game) saturates the
GPU — raises the process GPU scheduling priority to HIGH via
D3DKMTSetProcessSchedulingPriorityClass(resolved dynamically from gdi32; covers every D3D device in the process, including minigpu's) and setsIDXGIDevice::SetGPUThreadPriority(+7)on the capture device. Failures are logged and capture proceeds at normal priority.
0.5.9 #
- override
releaseBufferSync()to release synchronously (the underlyingMiniAV_ReleaseBufferC call is synchronous, so noFuture/microtask is allocated);releaseBuffer()now delegates to it. - DXGI screen capture (
screen_context_win_dxgi.c): release the duplication frame immediately after the per-frame copy instead of holding it across the pacingSleepuntil the next loop iteration. Desktop Duplication will not compose the next frame untilReleaseFrame, so the old ordering capped producer FPS and added a full frame of latency. Pacing is now driven by wall-clock elapsed since the last delivered frame (max(0, interval - elapsed)).
0.5.8 #
- fix audio buffer allocations and leak issue
0.5.7 #
- Fix logger noisiness
0.5.6 #
- fix FormatException on non-UTF-8 bytes in MiniAV log callback: use Utf8Decoder(allowMalformed: true) instead of toDartString()
- implement setLogCallback with NativeCallable.listener in miniav_ffi
- add setLogCallback and installStderrLogger to route native MiniAV C library logs to a Dart callback
0.5.5 #
- fix wasapi loopback issue
0.5.4 #
- adds bindings observer lib to fix crash on hot restart
0.5.3 #
- Fix crash bug on hot refresh, fix crash on second use of recorder
0.5.2 #
- adds shared textures
0.5.1 #
- adds subscriptions and fixes lost device crashes
0.5.0 #
- adding input support
0.4.7 #
- fix loopback crackles
0.4.6 #
- fix build hook null
- update cmake toolchain
0.4.5 #
- fixed windows screen cpu path
0.4.4 #
0.4.3 #
0.4.1 #
- fix issue with num frames not being reported for audio_inputs
1.0.0 #
- Initial version.