Alpha

useMedia

Media playback state and controls.

Subscribe to now-playing information and control media playback.

Usage

import { useMedia } from "@deskctl/sdk";

function MediaPlayer({ bridgeId }: { bridgeId: string }) {
  const { data, control } = useMedia(bridgeId);

  if (!data) return <div>No media playing</div>;

  return (
    <div>
      <h3>{data.title}</h3>
      <p>{data.artist}</p>

      {data.supports_ctrl && (
        <div className="controls">
          <button onClick={() => control.mutate({ action: "previous" })}>⏮</button>
          <button onClick={() => control.mutate({ action: "play_pause" })}>
            {data.playing ? "⏸" : "▶"}
          </button>
          <button onClick={() => control.mutate({ action: "next" })}>⏭</button>
        </div>
      )}

      {data.volume !== null && (
        <input
          type="range"
          min={0}
          max={100}
          value={data.volume}
          onChange={(e) =>
            control.mutate({ action: "set_volume", value: Number(e.target.value) })
          }
        />
      )}
    </div>
  );
}

Return Type

The hook returns MediaStatus from the Bridge API:

interface MediaStatus {
  status: string;              // Status description
  volume: number | null;       // 0-100 or null if unavailable
  muted: boolean | null;       // Mute state or null if unavailable
  playing: boolean | null;     // Playing state or null if unavailable
  title: string | null;        // Track title
  artist: string | null;       // Artist name
  supports_ctrl: boolean;      // Whether playback control is supported
}

// Control mutation
const { control } = useMedia(bridgeId);
control.mutate(action: MediaAction);

interface MediaAction {
  action: MediaActionType;
  value?: number;              // Required for set_volume
}

Options

Connection Mode

Control how the hook handles WebSocket connection:

// Auto (default) - connects if disconnected
const { data, control } = useMedia(bridgeId);

// Passive - don't trigger connection
const { data, control } = useMedia(bridgeId, { connectionMode: "passive" });

// Eager - connect once, respect manual disconnects
const { data, control } = useMedia(bridgeId, { connectionMode: "eager" });
ModeBehavior
"auto"Connects if disconnected. Reconnects on rerenders.
"passive"Never triggers connection. Only uses data if connected.
"eager"Connects once on mount. Respects manual disconnects.

Available Actions

ActionDescriptionValue
playStart playback-
pausePause playback-
play_pauseToggle play/pause-
nextSkip to next track-
previous / prevGo to previous track-
set_volumeSet volume level0-100
volume_upIncrease volume-
volume_downDecrease volume-
muteMute audio-
unmuteUnmute audio-
toggle_muteToggle mute state-

Examples

Volume Slider with Optimistic Updates

Volume changes are optimistic - the UI updates immediately:

function VolumeSlider({ bridgeId }: { bridgeId: string }) {
  const { data, control } = useMedia(bridgeId);

  if (data?.volume === null) return null;

  return (
    <div className="volume">
      <button onClick={() => control.mutate({ action: "toggle_mute" })}>
        {data?.muted ? "🔇" : "🔊"}
      </button>
      <input
        type="range"
        min={0}
        max={100}
        value={data?.volume ?? 50}
        onChange={(e) =>
          control.mutate({
            action: "set_volume",
            value: Number(e.target.value),
          })
        }
      />
      <span>{data?.volume}%</span>
    </div>
  );
}

Now Playing Card

function NowPlaying({ bridgeId }: { bridgeId: string }) {
  const { data } = useMedia(bridgeId);

  if (!data?.title) {
    return (
      <div className="now-playing empty">
        <p>Nothing playing</p>
      </div>
    );
  }

  return (
    <div className="now-playing">
      <div className="info">
        <h4>{data.title}</h4>
        {data.artist && <p>{data.artist}</p>}
        <p className="status">{data.status}</p>
      </div>
    </div>
  );
}

Full Media Controls

function MediaControls({ bridgeId }: { bridgeId: string }) {
  const { data, control } = useMedia(bridgeId);

  if (!data) return <div>Loading media...</div>;

  // Check if controls are supported
  if (!data.supports_ctrl) {
    return (
      <div className="media-info">
        <p>{data.title ?? "No media"}</p>
        <p className="note">Playback control not available</p>
      </div>
    );
  }

  return (
    <div className="media-controls">
      <div className="track-info">
        <h3>{data.title ?? "Unknown"}</h3>
        <p>{data.artist ?? "Unknown artist"}</p>
      </div>

      <div className="playback-controls">
        <button onClick={() => control.mutate({ action: "previous" })}>Previous</button>
        <button onClick={() => control.mutate({ action: "play_pause" })}>
          {data.playing ? "Pause" : "Play"}
        </button>
        <button onClick={() => control.mutate({ action: "next" })}>Next</button>
      </div>

      {data.volume !== null && (
        <div className="volume-controls">
          <button onClick={() => control.mutate({ action: "volume_down" })}>-</button>
          <span>{data.volume}%</span>
          <button onClick={() => control.mutate({ action: "volume_up" })}>+</button>
          <button onClick={() => control.mutate({ action: "toggle_mute" })}>
            {data.muted ? "Unmute" : "Mute"}
          </button>
        </div>
      )}
    </div>
  );
}

On this page