---
title: "Process uploaded images and video"
description: "Turn an upload into image sizes, HLS video and audio in a background job, then serve a signed link to the result."
section: "guides"
status: "Building for 0.4"
spec: "media"
canonical: "https://mainsdk.dev/docs/guides/process-uploaded-media"
updated: "2026-10-28"
---

# Process uploaded images and video

> Turn an upload into image sizes, HLS video and audio in a background job, then serve a signed link to the result.

Status: Building for 0.4 · Canonical: https://mainsdk.dev/docs/guides/process-uploaded-media · Updated: 2026-10-28

You will resize an uploaded photo in a background job and serve a signed link to a thumbnail. It takes about 15 minutes.

## You need

- A MainSDK app with `files` set up (see [file uploads](https://mainsdk.dev/docs/guides/file-uploads)).
- A file that is `ready`.

## 1. Choose a driver

```yaml title="main.yaml"
media:
  driver: getqr
```

The `local` driver keeps the job and file flow working offline but does not transcode, so use it in tests.

## 2. Start a job

```ts title="app/photos/thumb.ts"
import { media } from "mainsdk/media";
import type { FileRef } from "mainsdk/files";

export async function thumbnails(photo: FileRef) {
  const job = await media.image(photo, { widths: [320, 640], formats: ["avif", "webp"] });
  return job.id;
}
```

The job is `queued` when the call returns, so the request never waits on the work. For video use `media.video(file, { renditions: ["360p", "720p"], thumbnails: true })`. Location data is stripped from every output.

## 3. Serve a result

```ts title="app/api/photos/[id]/thumb/route.ts"
import { auth } from "mainsdk";
import { media } from "mainsdk/media";

export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
  const session = await auth.require(req);
  const { id } = await params;
  const job = await media.get(id);
  const thumb = job.outputs.find((o) => o.kind === "image" && o.width === 320);
  if (!thumb) return new Response("not ready", { status: 404 });
  return Response.json({ url: await media.url(thumb, { for: { id: session.user } }) });
}
```

For an HLS video, `media.playlist(job, output, { for })` returns a playlist whose paths are already signed, so a player makes no second request.

## Check that it works

- `media.get(job.id)` moves from `queued` to `ready`.
- A user who may not read the source gets `MAIN-MEDIA-0001`.

## Next

- [Images and video reference](https://mainsdk.dev/docs/interfaces/media)
- [File uploads](https://mainsdk.dev/docs/guides/file-uploads)
