ReferenceAPI

Media and audio

Reference for dd.media now-playing status and transport controls, and the dd.audio spectrum stream.

dd.media

Reads need the media permission; the transport verbs additionally need mediaControls.

dd.media.status()

dd.media.status(): Promise<NowPlaying | null>

The latest now-playing snapshot, a cheap cache read (null until the watcher's first pass). Seed your UI with this, then follow the stream.

dd.media.onNowPlaying()

dd.media.onNowPlaying(cb: (np: NowPlaying) => void): () => void

interface NowPlaying {
  /** False = no media session anywhere (fields null, can* false). */
  hasSession: boolean;
  status: "playing" | "paused" | "stopped";
  title: string | null;
  artist: string | null;
  album: string | null;
  /** Source app id (e.g. "Spotify.exe"): for feature detection, not UI. */
  sourceApp: string | null;
  /** Album art as an image data URI, or null. */
  art: string | null;
  /** Null when the player reports no usable duration (live streams). */
  position: {
    /** Seconds from track start as of asOfEpochMs. */
    seconds: number;
    durationSeconds: number;
    /** The player's own timeline stamp (epoch ms). Null = don't interpolate. */
    asOfEpochMs: number | null;
  } | null;
  canPlayPause: boolean;
  canSkipNext: boolean;
  canSkipPrevious: boolean;
  canSeek: boolean;
}

Two things to know:

  • Selection policy. The host picks the actually playing session across all Windows media sessions, not Windows' own "current session" (which favors whoever last touched the media keys, like a paused YouTube tab). It falls back to the most recent paused session so a play button works from the desktop. hasSession: false means no session anywhere.
  • Change-driven, not fixed-rate. Events arrive only when the reported state changes. Seed with status(), and while status === "playing" interpolate the displayed position locally as seconds + (Date.now() - asOfEpochMs) / 1000 (asOfEpochMs null means don't interpolate).

title, artist, album, art and position are independently nullable; render what you have.

Transport controls

dd.media.playPause(): Promise<void>   // 20 calls / 10 s
dd.media.next(): Promise<void>        // 30 calls / 10 s
dd.media.previous(): Promise<void>    // 30 calls / 10 s
dd.media.seek(seconds: number): Promise<void>  // 60 calls / 10 s

All act on the selected session. A player rejecting an action (or the session vanishing mid-call) is a silent no-op; the next snapshot corrects your UI. Nothing to control rejects NOT_FOUND. seek takes seconds from track start, clamped to the live track end host-side; check canSeek first, and send on slider release rather than every drag tick.

dd.audio

Needs the audio permission. Event-only: there are no audio methods.

dd.audio.onSpectrum()

dd.audio.onSpectrum(cb: (frame: SpectrumFrame) => void): () => void

interface SpectrumFrame {
  bins: number[];
}

500 log-spaced loudness bands (roughly 35 Hz to 16 kHz), each 0..1, derived host-side from system audio output. Never raw samples: loudness bands only, and speech is not reconstructible from them.

Delivery contract:

  • Frames arrive at roughly 30 Hz while media plays (20 Hz when the host runs in low performance mode).
  • A single all-zero frame marks each transition to silence (pause, track gap, capture stopping), then the stream goes quiet. Let your bars decay to zero on it and stop drawing.
  • Capture is demand-driven: it runs only while some enabled, visible widget with the audio grant exists (the active taskbar counts as one, since app 0.3.0) and the host's media watcher reports a playing session. Subscribing is what turns it on. Sources without a media session (audiobook players, most games) do not animate it.
  • Since app 0.2.4 a widget may opt out of the media gate: declare a boolean setting with the key anyAudio (default false). The host reads it by name; while any visible audio widget has it on, capture runs for all system sound instead of only media sessions. The checkbox applies on Save, and real silence still produces the zero frame.

Performance contract: do not draw in the callback. Copy bins into a reused buffer and render in your own requestAnimationFrame loop, interpolating between frames (fast attack, slow decay), and stop the loop once the bars settle at zero.

const levels = new Float32Array(500);
dd.audio.onSpectrum((frame) => {
  latest = frame.bins;      // stash
  ensureRafLoop();          // draw elsewhere, at your own pace
});