---
title: Bind to Workers API
description: Bind the Media Transformations API to a Cloudflare Worker to transform videos programmatically.
image: https://developers.cloudflare.com/dev-products-preview.png
---

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/stream/llms.txt  
> Use this file to discover all available pages before exploring further.

[Skip to content](#%5Ftop) 

# Bind to Workers API

A [binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/) connects your [Worker](https://developers.cloudflare.com/workers/) to external resources on the Developer Platform, like [Media Transformations](https://developers.cloudflare.com/stream/transform-videos/), [R2 buckets](https://developers.cloudflare.com/r2/buckets/), or [KV namespaces](https://developers.cloudflare.com/kv/concepts/kv-namespaces/).

You can bind the Media Transformations API to your Worker to transform, resize, and extract content from videos without requiring them to be accessible through a URL.

For example, when you use Media Transformations within Workers, you can:

* Transform a video stored in a private R2 bucket or other protected source
* Optimize videos and store the output directly back in R2 without serving it to the browser
* Extract still frames and spritesheets for videos and use them for classification or description with [Workers AI](https://developers.cloudflare.com/workers-ai/)
* Extract audio tracks from video files for dynamic transcription using Workers AI

Beta restrictions

The binding for Media Transformations is currently in public open beta. During this period:

* Transformation operations via the binding will not be billed.
* Certain operations may incur higher latency.

## Setup

The Media binding is enabled on a per-Worker basis.

[Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/) can be configured in the Cloudflare dashboard for your Worker or in the Wrangler configuration file in your project directory.

To bind Media Transformations to your Worker, add the following to the end of your Wrangler configuration file:

* [  wrangler.jsonc ](#tab-panel-8445)
* [  wrangler.toml ](#tab-panel-8446)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "media": {

    "binding": "MEDIA"

  }

}


```

TOML

```

[media]

binding = "MEDIA" # available in your Worker on env.MEDIA


```

Within your Worker code, you can interact with this binding by using `env.MEDIA.input()` to build an object that can manipulate the video (passed as a `ReadableStream`).

## Methods

The Media Transformations binding is similar to the [Images binding](https://developers.cloudflare.com/images/optimization/transformations/bindings/), except the method chain order is fixed and the result of an `input()` cannot be reused across multiple transformations.

### `.input()`

The starting point for the Media binding, which accepts raw content.

* Accepts a `ReadableStream<Uint8Array>` containing the video bytes.

### `.transform()` (optional)

Defines how the video input should be transformed by resizing or cropping. This method is optional — if you do not need to resize or crop, you can call `.output()` directly on the result of `.input()`.

* Accepts the following parameters (all optional):  
   * `width`: Target width in pixels (10-2000).  
   * `height`: Target height in pixels (10-2000).  
   * `fit`: How to resize the video to fit the specified dimensions.  
         * `contain`: Scales the video to fit entirely within the output dimensions while respecting aspect ratio.  
         * `cover`: Scales the video to entirely cover the output dimensions with a center-weighted crop.  
         * `scale-down`: Same as `contain`, but only scales down. Does not upscale.
* Refer to [Transform videos options](https://developers.cloudflare.com/stream/transform-videos/#options) for more details.

### `.output()`

Defines what to extract from the video and how the output will be formatted. Refer to [source video requirements](https://developers.cloudflare.com/stream/transform-videos/#source-video-requirements) and [limitations](https://developers.cloudflare.com/stream/transform-videos/#limitations) for input and output constraints.

* Accepts the following parameters:  
   * `mode`: The type of output to generate.  
         * `video`: Outputs an H.264/AAC optimized MP4 file.  
         * `frame`: Outputs a still image (JPEG or PNG).  
         * `spritesheet`: Outputs a JPEG containing multiple frames.  
         * `audio`: Outputs an AAC encoded M4A file.  
   * `time`: Start timestamp for extraction (for example, `"2s"`, `"1m"`). Default: `"0s"`.  
   * `duration`: Duration of the output for `video`, `audio`, or `spritesheet` modes (for example, `"5s"`).  
   * `imageCount`: Number of frames to include in a spritesheet.  
   * `format`: Output format for `frame` mode (`jpg`, `png`) or `audio` mode (`m4a`).  
   * `audio`: Boolean to include or exclude audio in `video` mode. Default: `true`.

### Result methods

Finally, after configuring the output, three methods are available to receive results. These methods return Promises and must be awaited:

* `.response()`: Returns a `Promise<Response>` — the transformed media as an HTTP Response object, ready to return to the client or store in cache.
* `.media()`: Returns a `Promise<ReadableStream<Uint8Array>>` — the transformed media as a byte stream.
* `.contentType()`: Returns a `Promise<string>` — the MIME type of the output (for example, `video/mp4`, `image/jpeg`, `audio/mp4`).

## Examples

### Generate an optimized video clip

Resize a video and extract a five-second clip:

TypeScript

```

export default {

  async fetch(request, env) {

    const video = await env.R2_BUCKET.get("input.mp4");


    const result = env.MEDIA.input(video.body)

      .transform({ width: 480, height: 270 })

      .output({ mode: "video", time: "0s", duration: "5s" });


    return await result.response();

  },

};


```

### Extract a still frame

Extract a single frame as a JPEG thumbnail:

TypeScript

```

export default {

  async fetch(request, env) {

    const video = await env.R2_BUCKET.get("input.mp4");


    const result = env.MEDIA.input(video.body)

      .transform({ width: 640, height: 360 })

      .output({ mode: "frame", time: "2s", format: "jpg" });


    return await result.response();

  },

};


```

#### Identify content with Media Transformations and Workers AI

Extract a frame (still image) from a video, then use a model like [UForm-Gen on Workers AI](https://developers.cloudflare.com/workers-ai/models/uform-gen2-qwen-500m/) to generate a caption.

TypeScript

```

export default {

  async fetch(request, env) {

    // First, load the video file from a source like R2 (or a fetch)


    // Loading from R2

    const video = await env.R2_BUCKET.get("input.mp4");


    // Or using a fetch:

    // const video = await fetch('https://example.com/video.mp4');


    // Isolate a frame (still image)

    const frame = await env.MEDIA.input(video.body)

      .transform({ width: 720 })

      .output({

        mode: 'frame',

        time: '3s',

      })

      .response();


    // Set up the payload for Workers AI

    const payload = {

      image: [...new Uint8Array(await frame.arrayBuffer())],

      prompt: "Generate a caption for this image",

      max_tokens: 512,

    };

    const response = await env.AI.run(

      "@cf/unum/uform-gen2-qwen-500m",

      payload

    );

    return new Response(JSON.stringify(response));

  }

}


```

### Extract audio

Extract the audio track from a video as an M4A file. This example demonstrates skipping `.transform()` since no resizing is needed:

TypeScript

```

export default {

  async fetch(request, env) {

    const video = await env.R2_BUCKET.get("input.mp4");


    const result = env.MEDIA.input(video.body).output({

      mode: "audio",

      time: "0s",

      duration: "30s",

    });


    return await result.response();

  },

};


```

#### Transcribe audio with Media Transformations and Workers AI

Extract audio, then transcribe using [Whisper on Workers AI](https://developers.cloudflare.com/workers-ai/models/whisper/).

TypeScript

```

export default {

  async fetch(request, env) {

    // First, load the video file from a source like R2 (or a fetch)


    // Loading from R2

    const video = await env.R2_BUCKET.get("input.mp4");


    // Or using a fetch:

    // const video = await fetch('https://example.com/video.mp4');


    // Extract audio using the media transformations binding:

    const audio = await env.MEDIA.input(video.body)

      .transform()

      .output({

        mode: 'audio',

        })

      .response();


    // Prepare and run Workers AI inference

    const payload = {

      audio: [...new Uint8Array(await audio.arrayBuffer())],

    };

    const response = await env.AI.run(

      "@cf/openai/whisper",

      payload

    );


    // response will have props {text, word_count, vtt, words}

    return new Response(

      JSON.stringify(response, null, 2),

      {

        headers: {'Content-Type': 'application/json'}

      }

    );

  }

}


```

### Store transformed output in R2

Transform a video and store the result directly in R2:

TypeScript

```

export default {

  async fetch(request, env) {

    const video = await env.R2_BUCKET.get("input.mp4");


    const result = env.MEDIA.input(video.body)

      .transform({ width: 480, height: 270, fit: "contain" })

      .output({ mode: "video", time: "0s", duration: "10s", audio: false });


    // Store the transformed video directly in R2

    await env.R2_BUCKET.put("output-480p.mp4", await result.media(), {

      httpMetadata: { contentType: await result.contentType() },

    });


    return new Response("Video transformed and stored", { status: 200 });

  },

};


```

## Error handling

Errors can be thrown at different points in the method chain:

* `.input()` can throw errors related to account limits (free tier or subscription) or service disruptions.
* `.output()` can throw errors related to the transformation operation itself, such as invalid parameters or unsupported input formats.

Errors throw a `MediaError`, which extends the standard `Error` interface with additional information:

* `code`: A numeric error code.
* `message`: A description of the error.
* `stack`: Optional stack trace.

Use a `try...catch` block to handle errors:

TypeScript

```

export default {

  async fetch(request, env) {

    const video = await env.R2_BUCKET.get("input.mp4");


    try {

      const result = env.MEDIA.input(video.body)

        .transform({ width: 480, height: 270 })

        .output({ mode: "video", time: "0s", duration: "5s" });


      return await result.response();

    } catch (e) {

      if (e instanceof Error && "code" in e) {

        // Handle MediaError

        return new Response(`Transformation failed: ${e.message}`, {

          status: 500,

        });

      }

      throw e;

    }

  },

};


```

## Caching

Unlike transformations via URL, responses from the Media binding are _not_ automatically cached. Workers lets you interact directly with the [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) to customize cache behavior. You can implement logic in your script to store transformations in Cloudflare's cache or R2 storage.

## Billing

Refer to Stream's [Pricing](https://developers.cloudflare.com/stream/pricing/) information for costs. Transformations executed via the binding are billed on a per-operation basis, not based on request uniqueness. For best cost and performance optimization, cache or store outputs for reuse.

## Local development

The Media Transformations API is available _in remote mode_ for local development through [Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/), the command-line interface for Workers. Transformations operations will be performed using a remote resource and are subject to usage charges beyond the included free tier.

To enable usage in local development, add `remote` to the binding configuration:

* [  wrangler.jsonc ](#tab-panel-8447)
* [  wrangler.toml ](#tab-panel-8448)

JSONC

```

{

  "$schema": "./node_modules/wrangler/config-schema.json",

  "media": {

    "binding": "MEDIA",

    "remote": true

  }

}


```

TOML

```

[media]

binding = "MEDIA" # available in your Worker on env.MEDIA

remote = true


```

Then run:

Terminal window

```

npx wrangler dev


```

Note

The Media Transformation binding does not support local simulation. If `remote = true` is not specified, the binding will produce an error during local development.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"item":{"@id":"/directory/","name":"Directory"}},{"@type":"ListItem","position":2,"item":{"@id":"/stream/","name":"Stream"}},{"@type":"ListItem","position":3,"item":{"@id":"/stream/transform-videos/","name":"Transform videos"}},{"@type":"ListItem","position":4,"item":{"@id":"/stream/transform-videos/bindings/","name":"Bind to Workers API"}}]}
```
