# Preview & Waveform Workflow | AudioDN Docs

> Generate a short public preview clip and a waveform visualization for an AudioDN track, then gate the full-quality stream behind entitlement.

Source: https://audiodeliverynetwork.com/docs/recipes/preview-and-waveform/

---

# Preview & Waveform Workflow

Show a waveform and a short, public preview of every track, while keeping the full-quality audio gated. AudioDN generates the waveform automatically during processing and can build a preview clip as a variant.

#### When AudioDN fits this job

Use AudioDN when you want preview clips and waveform data produced for you at upload time rather than running FFmpeg and waveform extraction yourself. The [waveform component](/docs/components/waveform) renders directly from the amplitude data the API returns.

## Architecture

-   **Upload (server):** ingest the track; a default 320-sample waveform is generated during processing.
-   **Preview (server):** add a `preview` variant so there is a short, shareable clip.
-   **Deliver (server):** mint a play session that returns waveform `levels` and signed variant URLs; request `preview` for everyone, `hq` only for entitled users.
-   **Render (client):** draw the waveform with `<audiodn-waveform>` and play the preview.

## Environment variables

```
# Server-only
ADN_API_KEY=adn_...        # API Access key (server-side)
ADN_COLLECTION_ID=...      # collection to upload into

# Safe to expose to the browser
PUBLIC_ADN_PLAYER_KEY=adn_player_...   # Client-Side Player key, scoped to the collection
```

## 1\. Upload the track

```
// upload.mjs — standard upload lifecycle (server-side).
const API = 'https://api.audiodelivery.net/v1';

export async function uploadTrack(bytes, fileName) {
  const session = await fetch(`${API}/upload_session`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ collection_id: process.env.ADN_COLLECTION_ID }),
  }).then((r) => r.json());

  // Per-track create — no Bearer, the session ID authorizes it.
  const { track_id, track_upload } = await fetch(
    `${API}/upload/${session.upload_session_id}/track`,
    { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file_name: fileName }) },
  ).then((r) => r.json());

  await fetch(track_upload.upload_url, { method: track_upload.method, body: bytes });

  // Wait until ready (a default waveform is generated during processing).
  let status;
  do {
    await new Promise((r) => setTimeout(r, 5000));
    const t = await fetch(`${API}/track/${track_id}`, {
      headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}` },
    }).then((r) => r.json());
    status = t.track?.track_status_id;
  } while (!['ready', 'incomplete', 'error'].includes(status));

  return { track_id, status };
}
```

## 2\. Add a preview variant

Configure a preview variant once in the dashboard so it applies to every upload, or add one per track with the [Variants API](/docs/api/variants):

```
// Add a short public preview clip to an already-processed track.
// Types: "transcode" (full-length) or "preview" (clip). See the Variants API.
await fetch(`https://api.audiodelivery.net/v1/track/${track_id}/variant`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    index: 'preview',
    variant_type_id: 'preview',
    // codec settings optional; defaults to aac / 128kbps
  }),
});
// The track re-enters "processing" and returns to "ready" when the clip is built.
```

## 3\. Mint a play session (levels + signed URLs)

```
// server.mjs — mint a play session for the waveform + preview.
// Request only the "preview" variant for the public teaser; add "hq" for entitled users.
app.get('/api/preview/:trackId', async (req, res) => {
  const variants = (await isEntitled(req)) ? ['hq', 'preview'] : ['preview'];
  const session = await fetch('https://api.audiodelivery.net/v1/play_session/track', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.ADN_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ track_id: req.params.trackId, variants, expires_in: 600 }),
  }).then((r) => r.json());
  // first_track.levels feeds the waveform; first_track.variants[].url are signed audio URLs.
  res.json({
    levels: session.first_track.levels.levels,
    variants: session.first_track.variants,
  });
});
```

## 4\. Render the waveform and preview

```
<!-- Render the waveform from the levels array and play the preview. -->
<audiodn-waveform id="wf" variant="vertical" height="100" line-width="2" gap="3"></audiodn-waveform>
<audio id="audio" controls></audio>

<script type="module">
  import '@audiodn/components/waveform';

  const data = await fetch('/api/preview/TRACK_ID').then((r) => r.json());

  // Feed amplitude data (0–1 values) to the waveform component.
  document.getElementById('wf').levels = data.levels;

  // Play the preview variant's signed URL.
  const preview = data.variants.find((v) => v.variant.index === 'preview');
  document.getElementById('audio').src = preview.url;
</script>
```

#### Preview vs. full

The public teaser only ever receives the `preview` variant URL. The full `hq` URL is added to the play session **only** after your server confirms entitlement, so the full track is never exposed to anonymous visitors.

## Expected errors

-   `401` — missing/invalid Bearer key on the server-side variant or play-session calls.
-   `400` — adding a variant with a duplicate `index`, or an unsupported codec.
-   `404` — the track is not ready yet or the ID is wrong.
-   Empty `variants` in the play session response — the requested variant name does not exist on the track.

## Testing

-   Upload a track and confirm `track_status_id` reaches `ready`, then that a `preview` variant appears.
-   Call `/api/preview/<trackId>` as anonymous and as entitled; assert the anonymous response omits `hq`.
-   Confirm the waveform renders (non-empty `levels`) and the preview audio plays.

## Deployment

The server route needs the API Access key; deploy it on any server or serverless function. The client only uses the signed URLs and the public player key, so it can be a fully static frontend.

### OpenAPI operations used

-   `createUploadSession` — `POST /v1/upload_session` ([reference](/docs/api/upload-sessions), [openapi.json](/openapi.json))
-   `createUploadSessionTrack` — `POST /v1/upload/:upload_session_id/track` ([reference](/docs/api/upload-sessions))
-   `getTrack` — `GET /v1/track/:track_id` ([reference](/docs/api/tracks))
-   `addTrackVariant` — `POST /v1/track/:track_id/variant` ([reference](/docs/api/variants))
-   `createPlaySession` — `POST /v1/play_session/:scope` ([reference](/docs/api/play-sessions))
