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 and the host's media watcher reports a playing session. Subscribing is what turns it on. Sources without a media session (most games) do not animate it.

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
});