MainSDK
Search⌘K
Interfaces

Files, sharing and media

Images and video

media

Turn uploads into image sizes, HLS video and audio in a background job, written back through files with location data stripped by default.

Building for 0.4ships as a beta APIPhase 1 · Core, public betaSpec mediaFree in your app, Apache-2.0

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

media turns uploads into AVIF and WebP images and HLS video and audio in a durable job, with location data stripped. Reach for it after a user uploads through files.

At a glance

media:
  driver: getqr
  image: { widths: [320, 640, 1280], formats: [avif, webp], max-pixels: 40MP }
  video: { renditions: [360p, 720p], max-duration: 10m, max-size: 2GB }

Calls

  • media.image(file, { widths?, formats? })
  • media.video(file, { renditions? })
  • media.audio(file, { bitrates? })
  • media.get(jobId)
  • media.cancel(jobId, { reason })
  • media.url(output, { for })
  • media.playlist(job, output, { for })

Guarantees

Whatever the driver.

  • Never processed in the request path; every call is a resumable workflow
  • Every output goes through files, owned by the source's owner, in the tenant's region
  • EXIF, XMP, GPS and container metadata stripped from every output; location can't be kept
  • Type, size, duration and pixel limits checked before any transform
  • ffmpeg runs only as a declared program through system.run

Drivers

5 drivers. Select one to see its line in main.yaml.

main.yamlgetqr.id (default)
media:
  driver: getqr

Change this one line to swap providers; your calls stay the same.getqr.id integration

Codes

CodeWhere it appearsMeaning
MAIN-MEDIA-0007Checkmainsdk check, before deploymedia.keep lists gps, location or an unknown metadata name (start fails; check finding)
MAIN-MEDIA-0010Checkmainsdk check, before deployThe ffmpeg driver in production
MAIN-MEDIA-0011Checkmainsdk check, before deployAn image or video library (sharp, jimp, fluent-ffmpeg, @ffmpeg/ffmpeg, ffmpeg-static) or an ffmpeg program run outside a driver
MAIN-MEDIA-0012Checkmainsdk check, before deploydelivery: provider on mux or cloudinary: outputs served by the provider, outside files (named gap)
MAIN-MEDIA-0013Checkmainsdk check, before deployA media kind used with no limits, or a limit above its ceiling: max-duration over 4h, max-size over 10GB, max-pixels over 100MP
MAIN-MEDIA-0001RuntimeError responses and logsThe source file isn't ready, doesn't exist in the current tenant, or the caller may not read it (404)
MAIN-MEDIA-0002RuntimeError responses and logsThe source is over a limit: duration, size or pixels; the job failed in the probe step, nothing was transformed (422)
MAIN-MEDIA-0003RuntimeError responses and logsThe source's magic bytes don't match a supported type, or don't match its declared type (415)
MAIN-MEDIA-0004RuntimeError responses and logsThe driver doesn't support this kind or format, such as an image on mux (501)
MAIN-MEDIA-0005RuntimeError responses and logsA transform step failed after its last retry: the provider failed, or ffmpeg exited non-zero; the job is failed
MAIN-MEDIA-0006RuntimeError responses and logsNo job with that id in the current tenant (404)

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
media@1 contractPhase 10.4media#contract
Processing as a workflowPhase 10.4media#workflow
Metadata stripped by defaultPhase 10.4media#strip
Video and audio to HLS with thumbnailsPhase 10.4media#video-audio
Limits before workPhase 10.4media#limits
ffmpeg development driverPhase 10.4media#driver-ffmpeg
getqr.id driverPhase 10.4media#driver-getqr
Mux and Cloudinary driversPhase 10.4media#drivers-mux-cloudinary
Moderation holdPhase 10.4media#hold
TenantsPhase 10.4media#tenancy-access
Media checksPhase 10.4media#checks

Quick start

main.yaml
media:
  driver: getqr
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;   // read it with media.get(job.id)
}

The job is queued when the call returns. Its outputs are files, owned by the source's owner, in the tenant's region.

Configuration

main.yaml
media:
  driver: getqr                    # getqr (default) | mux | cloudinary | ffmpeg (development)
  image: { widths: [320, 640, 1280, 2048], formats: [avif, webp], quality: 70, max-pixels: 40MP, max-size: 25MB }
  video: { renditions: [360p, 720p, 1080p], segment: 6s, max-duration: 10m, max-size: 2GB }
  audio: { bitrates: [64k, 128k], max-duration: 60m, max-size: 200MB }
  keep: []                         # metadata kept: icc, orientation-tag; never gps or location
KeyWhat it does
drivergetqr.id by default; mux serves video and audio, cloudinary images and video, ffmpeg is for development and runs only as a declared program
image, video, audioOutput sizes and formats, and the limits checked before any work
keepicc and orientation-tag only. gps or location fails with MAIN-MEDIA-0007

What you get

Every call starts a durable workflow and returns a job, so a request never waits on a transcode. The calls:

text
media.image(file, { widths?, formats?, quality?, fit?, idempotencyKey? })   // MediaJob
media.video(file, { renditions?, thumbnails?, idempotencyKey? })
media.audio(file, { bitrates?, idempotencyKey? })
media.get(jobId)
media.cancel(jobId, { reason })                                             // deletes partial outputs
media.url(output, { for: user, expiresIn? })                                // a signed URL, as files.url
media.playlist(job, output, { for, expiresIn? })                            // an HLS playlist with each relative path replaced by a signed URL

A job has state (queued, running, held, ready, failed, cancelled), a safe probe and outputs; each output is { kind, format, width?, height?, bytes, file }. The workflow runs probe, limit check, one transform per output, write, then ready, and resumes after a crash without redoing a finished output.

Examples

Transcode a proof video

lib/proof-video.ts
import { media } from "mainsdk/media";
import type { FileRef } from "mainsdk/files";

export async function prepareVideo(video: FileRef) {
  const job = await media.video(video, { renditions: ["360p", "720p"], thumbnails: true });
  return job.id;
}

Serve a thumbnail

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);   // here id is the job 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 } }) });
}

Errors and checks

CodeWhenFix
MAIN-MEDIA-0001The source is not ready, not in this tenant, or the caller may not read itWait for files.complete, or check the owner
MAIN-MEDIA-0002The source is over a duration, size or pixel limitSend a smaller file or raise the limit on purpose
MAIN-MEDIA-0003The bytes are not a supported type, or not the declared typeSend a JPEG, PNG, WebP, AVIF, HEIC, GIF, MP4, MOV, WebM, MP3, AAC, M4A, WAV or Ogg file
MAIN-MEDIA-0004The driver does not support this kind or formatUse a driver that does
MAIN-MEDIA-0005A transform step failed after its last retryRead job.error, then retry with the same idempotency key
MAIN-MEDIA-0006No job with that id in this tenantUse a job id from this tenant

The spec also defines check codes MAIN-MEDIA-0010 to 0013 (the ffmpeg driver in production, an image or video library used outside a driver, provider-served delivery, limits above their ceilings).

Swapping drivers

main.yaml
media:
  driver: mux   # was getqr

Your calls stay the same. A driver that cannot serve a kind fails with MAIN-MEDIA-0004, such as an image on mux. See Swap a driver.

Testing

fakeMediaDriver() and the fixtures makeJpeg, makePng, makeMp4 and the like come from mainsdk/testing, and runMediaConformance checks a driver against the spec. See Test your app.

Security and privacy

  • Every output carries no EXIF, XMP, GPS or container metadata; orientation is applied to the pixels first.
  • Outputs are written through files, so owner scope, signed URLs, scanning and regions apply. media never serves the original.
  • With moderate.hold.media, outputs stay held until screened.
  • Deleting the source, erasing its owner or erasing the tenant deletes every output made from it.

FAQ

Which formats are supported? Images: JPEG, PNG, WebP, AVIF, HEIC, GIF. Video: MP4, MOV, WebM. Audio: MP3, AAC, M4A, WAV, Ogg. Outputs are AVIF and WebP images, and HLS for video and audio. HEIC support is an open question in the spec.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet