miniav_recorder 0.1.2 copy "miniav_recorder: ^0.1.2" to clipboard
miniav_recorder: ^0.1.2 copied to clipboard

High-level multi-source A/V recorder built on miniav + miniav_tools. Sync screen/camera/mic/loopback into MP4/MKV files or chunked streams.

miniav_recorder #

High-level multi-source A/V recorder for Dart. Combines screen, camera, microphone and loopback sources into one or more MP4/MKV/M4A/MP3 outputs (or live chunked streams) with a shared master clock.

Built on miniav + miniav_tools + miniav_tools_ffmpeg.


Quick start #

import 'package:miniav_recorder/miniav_recorder.dart';

final rec = (RecorderBuilder()
      ..addScreen(codec: VideoCodec.h264, scale: ScreenScalePolicy.h264Friendly)
      ..addMic(deviceId: mic.deviceId)
      ..addFileOutput('output.mkv'))
    .build();

await rec.start();
await Future.delayed(const Duration(seconds: 10));
await rec.stop();

Enumerate devices #

Use RecorderDevices to list available capture devices before building a recorder. Every deviceId returned can be passed directly to the matching builder method.

final displays = await RecorderDevices.displays();     // for addScreen(displayId:)
final windows  = await RecorderDevices.windows();      // for addScreen(windowId:)
final cameras  = await RecorderDevices.cameras();      // for addCamera(deviceId:)
final mics     = await RecorderDevices.microphones();  // for addMic(deviceId:)
final loops    = await RecorderDevices.loopbacks();    // for addLoopback(deviceId:)

print(displays.map((d) => '${d.name}: ${d.deviceId}').join('\n'));

All lists carry a MiniAVDeviceInfo with deviceId, name, and isDefault.


Sources #

Screen / display #

addScreen accepts the display ID string returned by RecorderDevices.displays() (or MiniScreen.enumerateDisplays()) directly, or null to capture the platform's default display.

// Use a specific display by ID
builder.addScreen(
  displayId: display.deviceId,   // from RecorderDevices.displays()
  codec:     VideoCodec.h264,
  hwAccel:   HwAccelPreference.preferred,   // default
  scale:     ScreenScalePolicy.h264Friendly, // optional downscale
);

// Platform default display
builder.addScreen();

// Specific application window
builder.addScreen(windowId: win.deviceId);

Camera #

builder.addCamera(
  deviceId: camera.deviceId,    // from RecorderDevices.cameras()
  codec:    VideoCodec.h264,
);

Microphone / Loopback #

builder.addMic(deviceId: mic.deviceId, codec: AudioCodec.aac);
builder.addLoopback(deviceId: loopback.deviceId, codec: AudioCodec.aac);

Sinks #

// Write to a container file — container is auto-selected (see below).
builder.addFileOutput('rec.mkv');

// Override the container explicitly.
builder.addFileOutput('audio.m4a', container: Container.m4a);

// Receive raw encoded packets for live streaming / network forwarding.
builder.addStreamOutput((Object chunk) {
  if (chunk is TrackChunk) {
    // chunk.bytes, chunk.kind, chunk.ptsUs, chunk.isKeyframe, …
  }
});

Auto-container selection #

When no container: override is supplied, the recorder infers the most natural container:

Track mix Auto-selected container
Video + audio .mkv
Video only .mp4
Audio-only (AAC) .m4a
Audio-only (MP3) .mp3
Audio-only (Opus) .ogg
Audio-only (mixed/other) .mkv

Audio-only recorders #

A recorder with no video sources is fully supported. Useful for recording mic or loopback audio to a standalone audio file:

final micRec = (RecorderBuilder()
      ..addMic(deviceId: mic.deviceId, codec: AudioCodec.aac)
      ..addFileOutput('mic.m4a'))  // auto-picks Container.m4a
    .build();

await micRec.start();
await Future.delayed(const Duration(seconds: 30));
await micRec.stop();

Synchronised multi-recorder (RecorderGroup) #

Record to multiple outputs simultaneously with clock-synchronised start. The prepare phase (encoder/muxer init, GPU bringup) runs concurrently on all recorders, then all master clocks are started in a tight sequential loop to minimise clock skew.

final avRec = (RecorderBuilder()
      ..addScreen()                   // null = platform default display
      ..addLoopback(deviceId: loop.deviceId)
      ..addFileOutput('av.mp4'))
    .build();

final micRec = (RecorderBuilder()
      ..addMic(deviceId: mic.deviceId, codec: AudioCodec.aac)
      ..addFileOutput('mic.m4a'))     // auto-picks Container.m4a
    .build();

final group = RecorderGroup([avRec, micRec]);
await group.start();
await Future.delayed(const Duration(seconds: 10));
await group.stop();

RecorderGroup.recorders exposes each individual Recorder so you can query state or add per-recorder error handling.


Zero-copy GPU path (Windows) #

RecorderBuilder.preferZeroCopy defaults to true.

When recording a screen source on Windows with a compatible hardware encoder (NVENC, AMD VCE, Intel QSV, Media Foundation), the recorder:

  1. Initialises a shared Dawn D3D12 context via minigpu.
  2. Creates a matching ID3D11Device on the same GPU adapter.
  3. Requests GPU output (MiniAVOutputPreference.gpu) from the DXGI capture so each frame arrives as a D3D11 NT shared handle — no pixel data crosses PCIe.
  4. Passes the shared device handle to the FFmpeg backend, which opens FfmpegD3d11HwEncoder with existingD3d11Device. The encoder reads the texture directly without any CPU copy.

Falls back silently to the CPU-upload path on any failure (unsupported GPU, non-Windows, no HW encoder).


GPU downscaling #

Screen sources can be downscaled on the GPU before encoding using ScreenScalePolicy.

Policy Behaviour
ScreenScalePolicy.none No scaling (default).
ScreenScalePolicy.h264Friendly Auto-downscale so max(width, height) ≤ 4096, preserving aspect ratio. Avoids the automatic H.264 → HEVC codec promotion on ultrawide / 4K+ displays.
ScreenScalePolicy.fixedSize(w, h) Encode at an explicit pixel size (rounded to even).
ScreenScalePolicy.scaleFactor(f) Uniform fractional scale, e.g. 0.5 = half each axis (rounded to even).

How it works #

When zero-copy is active and a non-none policy is set, a GpuDownscaler is created for the track. Per frame:

MiniAVBuffer (D3D11 NT handle)
  → gpu.importVideoFrame()          VideoTexture  (BGRA, srcW×srcH)
  → VideoTexture.toRGBA()           Buffer        (RGBA u32[], srcW×srcH)
  → WGSL bilinear dispatch          Buffer        (RGBA u32[], dstW×dstH)
  → SharedOutputTexture.copyFromBuffer
                                    SharedOutputTexture (RGBA, dstW×dstH)
  → D3D11TextureFrameSource → FfmpegD3d11HwEncoder

All GPU memory — no pixel data reaches the CPU.

Example: ultrawide display → H.264 #

builder.addScreen(
  displayId: display.deviceId,
  codec: VideoCodec.h264,                    // stays h264 after downscale
  scale: ScreenScalePolicy.h264Friendly,     // 5120×1440 → 4096×1152
);

Example: custom scale #

// Half resolution
builder.addScreen(scale: ScreenScalePolicy.scaleFactor(0.5));

// Fixed 1920×1080
builder.addScreen(scale: ScreenScalePolicy.fixedSize(1920, 1080));

GPU effects pipeline #

Screen sources support an ordered chain of GPU post-processing effects applied after downscaling. All effects run as WGSL compute shaders on the GPU — no CPU copies, no PCIe round-trips.

Effects are passed to addScreen() as a List<ScreenEffect>:

builder.addScreen(
  displayId: display.deviceId,
  scale: ScreenScalePolicy.h264Friendly,
  effects: [
    ScreenEffect.vignette(strength: 0.5),
  ],
);

Built-in effects #

Factory Description
ScreenEffect.vignette({strength}) Radial vignette + warm colour grade. strength 0–1 (default 0.4).

Custom WGSL effects #

Provide your own compute shader source via ScreenEffect.wgsl():

ScreenEffect.wgsl(
  '''
  struct Params { width: u32, height: u32, gamma: f32, };
  @group(0) @binding(0) var<storage, read_write> pixels : array<u32>;
  @group(0) @binding(1) var<storage, read_write> params : Params;

  @compute @workgroup_size(8, 8, 1)
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
    if (gid.x >= params.width || gid.y >= params.height) { return; }
    let idx = gid.y * params.width + gid.x;
    // ... transform pixels[idx] in place ...
  }
  ''',
  extraParams: [2.2], // passed as f32 fields from byte 8 in the Params struct
)

WGSL shader contract

Binding Type Meaning
@binding(0) array<u32> (read_write) Packed RGBA8 pixels, row-major. Modify in-place.
@binding(1) user-defined struct (read_write) width: u32 at offset 0, height: u32 at offset 4, then the extraParams values as consecutive f32 fields from byte 8.

Workgroups are dispatched at 8×8 threads. Always guard against out-of-bounds: if (gid.x >= params.width || gid.y >= params.height) { return; }.

Effects + scale together #

builder.addScreen(
  displayId: display.deviceId,
  scale: ScreenScalePolicy.h264Friendly,   // 1. downscale on GPU
  effects: [
    ScreenEffect.vignette(strength: 0.4),  // 2. vignette on smaller buffer
  ],
);

Downscaling runs first (on the full-resolution buffer), then effects run on the already-smaller destination buffer — maximising efficiency.

Requirements #

  • Zero-copy GPU path must be active (preferZeroCopy = true, Windows D3D12).
  • If the GPU context is unavailable at runtime, a warning is printed and effects are skipped (encoding continues normally without them).

Builder options #

final builder = RecorderBuilder()
  ..defaultVideoBitrate = 6_000_000   // bits/s
  ..defaultAudioBitrate = 128_000     // bits/s
  ..defaultFrameRate    = 30
  ..backendPreference   = BackendPreference.auto
  ..preferZeroCopy      = true;       // default — safe to leave on

Codec selection #

The recorder calls FfmpegBackend.bestCodecForResolution automatically. When hardware encoding is preferred and the source (or downscaled) dimensions exceed 4096 px in any dimension, it promotes h264 → hevc to stay within hardware encoder limits. Using ScreenScalePolicy.h264Friendly avoids this promotion.


Examples #

See examples/recorder/:

Script Description
screen_mic.dart Screen + microphone → MKV (zero-copy + h264Friendly by default).
camera_mic.dart Webcam + microphone → MP4.
screen_mic_loopback_chunked.dart Screen + loopback → chunked stream packets.

RecorderBuilder.preferZeroCopy defaults to true.

When recording a screen source on Windows with a compatible hardware encoder (NVENC, AMD VCE, Intel QSV, Media Foundation), the recorder:

  1. Initialises a shared Dawn D3D12 context via minigpu.
  2. Creates a matching ID3D11Device on the same GPU adapter.
  3. Requests GPU output (MiniAVOutputPreference.gpu) from the DXGI capture so each frame arrives as a D3D11 NT shared handle — no pixel data crosses PCIe.
  4. Passes the shared device handle to the FFmpeg backend, which opens FfmpegD3d11HwEncoder with existingD3d11Device. The encoder reads the texture directly without any CPU copy.

Falls back silently to the CPU-upload path on any failure (unsupported GPU, non-Windows, no HW encoder).


GPU downscaling #

Screen sources can be downscaled on the GPU before encoding using ScreenScalePolicy.

Policy Behaviour
ScreenScalePolicy.none No scaling (default).
ScreenScalePolicy.h264Friendly Auto-downscale so max(width, height) ≤ 4096, preserving aspect ratio. Avoids the automatic H.264 → HEVC codec promotion on ultrawide / 4K+ displays.
ScreenScalePolicy.fixedSize(w, h) Encode at an explicit pixel size (rounded to even).
ScreenScalePolicy.scaleFactor(f) Uniform fractional scale, e.g. 0.5 = half each axis (rounded to even).

How it works #

When zero-copy is active and a non-none policy is set, a GpuDownscaler is created for the track. Per frame:

MiniAVBuffer (D3D11 NT handle)
  → gpu.importVideoFrame()          VideoTexture  (BGRA, srcW×srcH)
  → VideoTexture.toRGBA()           Buffer        (RGBA u32[], srcW×srcH)
  → WGSL bilinear dispatch          Buffer        (RGBA u32[], dstW×dstH)
  → SharedOutputTexture.copyFromBuffer
                                    SharedOutputTexture (RGBA, dstW×dstH)
  → D3D11TextureFrameSource → FfmpegD3d11HwEncoder

All GPU memory — no pixel data reaches the CPU.

Example: ultrawide display → H.264 #

builder.addScreen(
  displayId: display.deviceId,
  codec: VideoCodec.h264,                    // stays h264 after downscale
  scale: ScreenScalePolicy.h264Friendly,     // 5120×1440 → 4096×1152
);

Example: custom scale #

// Half resolution
builder.addScreen(
  displayId: display.deviceId,
  scale: ScreenScalePolicy.scaleFactor(0.5),
);

// Fixed 1920×1080
builder.addScreen(
  displayId: display.deviceId,
  scale: ScreenScalePolicy.fixedSize(1920, 1080),
);

GPU effects pipeline #

Screen sources support an ordered chain of GPU post-processing effects applied after downscaling. All effects run as WGSL compute shaders on the GPU — no CPU copies, no PCIe round-trips.

Effects are passed to addScreen() as a List<ScreenEffect>:

builder.addScreen(
  displayId: display.deviceId,
  scale: ScreenScalePolicy.h264Friendly,
  effects: [
    ScreenEffect.vignette(strength: 0.5),
  ],
);

Built-in effects #

Factory Description
ScreenEffect.vignette({strength}) Radial vignette + warm colour grade. strength 0–1 (default 0.4).

Custom WGSL effects #

Provide your own compute shader source via ScreenEffect.wgsl():

ScreenEffect.wgsl(
  '''
  struct Params { width: u32, height: u32, gamma: f32, };
  @group(0) @binding(0) var<storage, read_write> pixels : array<u32>;
  @group(0) @binding(1) var<storage, read_write> params : Params;

  @compute @workgroup_size(8, 8, 1)
  fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
    if (gid.x >= params.width || gid.y >= params.height) { return; }
    let idx = gid.y * params.width + gid.x;
    // ... transform pixels[idx] in place ...
  }
  ''',
  extraParams: [2.2], // passed as f32 fields from byte 8 in the Params struct
)

WGSL shader contract

Binding Type Meaning
@binding(0) array<u32> (read_write) Packed RGBA8 pixels, row-major. Modify in-place.
@binding(1) user-defined struct (read_write) width: u32 at offset 0, height: u32 at offset 4, then the extraParams values as consecutive f32 fields from byte 8.

Workgroups are dispatched at 8×8 threads. Always guard against out-of-bounds: if (gid.x >= params.width || gid.y >= params.height) { return; }.

Effects + scale together #

builder.addScreen(
  displayId: display.deviceId,
  scale: ScreenScalePolicy.h264Friendly,   // 1. downscale on GPU
  effects: [
    ScreenEffect.vignette(strength: 0.4),  // 2. vignette on smaller buffer
  ],
);

Downscaling runs first (on the full-resolution buffer), then effects run on the already-smaller destination buffer — maximising efficiency.

Requirements #

  • Zero-copy GPU path must be active (preferZeroCopy = true, Windows D3D12).
  • If the GPU context is unavailable at runtime, a warning is printed and effects are skipped (encoding continues normally without them).

Builder options #

final builder = RecorderBuilder()
  ..defaultVideoBitrate = 6_000_000   // bits/s
  ..defaultAudioBitrate = 128_000     // bits/s
  ..defaultFrameRate    = 30
  ..backendPreference   = BackendPreference.auto
  ..preferZeroCopy      = true;       // default — safe to leave on

Codec selection #

The recorder calls FfmpegBackend.bestCodecForResolution automatically. When hardware encoding is preferred and the source (or downscaled) dimensions exceed 4096 px in any dimension, it promotes h264 → hevc to stay within hardware encoder limits. Using ScreenScalePolicy.h264Friendly avoids this promotion.


Examples #

See examples/recorder/:

Script Description
screen_mic.dart Screen + microphone → MKV (zero-copy + h264Friendly by default).
camera_mic.dart Webcam + microphone → MP4.
screen_mic_loopback_chunked.dart Screen + loopback → chunked stream packets.
3
likes
0
points
1.94k
downloads

Publisher

verified publisherpracticalxr.com

Weekly Downloads

High-level multi-source A/V recorder built on miniav + miniav_tools. Sync screen/camera/mic/loopback into MP4/MKV files or chunked streams.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

miniav, miniav_platform_interface, miniav_tools, miniav_tools_ffmpeg, miniav_tools_platform_interface, minigpu

More

Packages that depend on miniav_recorder