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