Images and video
mediaTurn uploads into image sizes, HLS video and audio in a background job, written back through files with location data stripped by default.
mediaFree in your app, Apache-2.0Status 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.
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 }import { media } from "mainsdk/media";
const job = await media.image(photo, { widths: [320, 1280] }); // returns at once; a workflow does the workCalls
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.
media:
driver: getqrChange this one line to swap providers; your calls stay the same.getqr.id integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-MEDIA-0007 | Checkmainsdk check, before deploy | media.keep lists gps, location or an unknown metadata name (start fails; check finding) |
MAIN-MEDIA-0010 | Checkmainsdk check, before deploy | The ffmpeg driver in production |
MAIN-MEDIA-0011 | Checkmainsdk check, before deploy | An image or video library (sharp, jimp, fluent-ffmpeg, @ffmpeg/ffmpeg, ffmpeg-static) or an ffmpeg program run outside a driver |
MAIN-MEDIA-0012 | Checkmainsdk check, before deploy | delivery: provider on mux or cloudinary: outputs served by the provider, outside files (named gap) |
MAIN-MEDIA-0013 | Checkmainsdk check, before deploy | A 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-0001 | RuntimeError responses and logs | The source file isn't ready, doesn't exist in the current tenant, or the caller may not read it (404) |
MAIN-MEDIA-0002 | RuntimeError responses and logs | The source is over a limit: duration, size or pixels; the job failed in the probe step, nothing was transformed (422) |
MAIN-MEDIA-0003 | RuntimeError responses and logs | The source's magic bytes don't match a supported type, or don't match its declared type (415) |
MAIN-MEDIA-0004 | RuntimeError responses and logs | The driver doesn't support this kind or format, such as an image on mux (501) |
MAIN-MEDIA-0005 | RuntimeError responses and logs | A transform step failed after its last retry: the provider failed, or ffmpeg exited non-zero; the job is failed |
MAIN-MEDIA-0006 | RuntimeError responses and logs | No 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.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| media@1 contract | Phase 1 | 0.4 | media#contract |
| Processing as a workflow | Phase 1 | 0.4 | media#workflow |
| Metadata stripped by default | Phase 1 | 0.4 | media#strip |
| Video and audio to HLS with thumbnails | Phase 1 | 0.4 | media#video-audio |
| Limits before work | Phase 1 | 0.4 | media#limits |
| ffmpeg development driver | Phase 1 | 0.4 | media#driver-ffmpeg |
| getqr.id driver | Phase 1 | 0.4 | media#driver-getqr |
| Mux and Cloudinary drivers | Phase 1 | 0.4 | media#drivers-mux-cloudinary |
| Moderation hold | Phase 1 | 0.4 | media#hold |
| Tenants | Phase 1 | 0.4 | media#tenancy-access |
| Media checks | Phase 1 | 0.4 | media#checks |
Quick start
media:
driver: getqrimport { 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
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| Key | What it does |
|---|---|
driver | getqr.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, audio | Output sizes and formats, and the limits checked before any work |
keep | icc 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:
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 URLA 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
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
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
| Code | When | Fix |
|---|---|---|
MAIN-MEDIA-0001 | The source is not ready, not in this tenant, or the caller may not read it | Wait for files.complete, or check the owner |
MAIN-MEDIA-0002 | The source is over a duration, size or pixel limit | Send a smaller file or raise the limit on purpose |
MAIN-MEDIA-0003 | The bytes are not a supported type, or not the declared type | Send a JPEG, PNG, WebP, AVIF, HEIC, GIF, MP4, MOV, WebM, MP3, AAC, M4A, WAV or Ogg file |
MAIN-MEDIA-0004 | The driver does not support this kind or format | Use a driver that does |
MAIN-MEDIA-0005 | A transform step failed after its last retry | Read job.error, then retry with the same idempotency key |
MAIN-MEDIA-0006 | No job with that id in this tenant | Use 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
media:
driver: mux # was getqrYour 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.medianever serves the original. - With
moderate.hold.media, outputs stayhelduntil 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
- File uploads: where media comes from.
- Share links and QR codes: share the result.
- Privacy and data rights: export or erase a person's media.
Was this page helpful?