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: falsemeans no session anywhere. - Change-driven, not fixed-rate. Events arrive only when the reported
state changes. Seed with
status(), and whilestatus === "playing"interpolate the displayed position locally asseconds + (Date.now() - asOfEpochMs) / 1000(asOfEpochMsnull 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 sAll 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
audiogrant 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
});