INTEGRITY Cloudflare Docs

Bind to Workers API

A binding connects your Worker to external resources on the Developer Platform, like Media Transformations, R2 buckets, or 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:

Setup

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

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:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "media": {
    "binding": "MEDIA"
  }
}
[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, 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.

.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().

.output()

Defines what to extract from the video and how the output will be formatted. Refer to source video requirements and limitations for input and output constraints.

Result methods

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

Examples

Generate an optimized video clip

Resize a video and extract a five-second clip:

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:

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 to generate a caption.

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:

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.

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:

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:

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

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

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 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 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, 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:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "media": {
    "binding": "MEDIA",
    "remote": true
  }
}
[media]
binding = "MEDIA" # available in your Worker on env.MEDIA
remote = true

Then run:

npx wrangler dev