Media Optimization on Google Cloud
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. See the bibliography at the end of this page for the reference material consulted while preparing this page. |
Baseline date: 2026-10-07. This page follows the Bookshelf names of the Google Cloud section: project my-project-id
(placeholder), region us-central1, the uploads bucket bookshelf-uploads-my-project-id, and a new bucket
bookshelf-media-my-project-id for results. Check each linked Google Cloud page for the current launch stage, limits and
pricing before you rely on a feature (prices are not quoted here). The concepts are in
upload pipelines,
video encoding fundamentals and
video streaming.
Transcoder API
The Transcoder API is the managed, file-based video transcoding service. You create a job that reads a file from Cloud Storage, and it writes the outputs back to Cloud Storage. There is nothing to provision.
- Job
-
An input URI, an output URI, and either a template or an inline config. Jobs run asynchronously and report state (
PENDING,RUNNING,SUCCEEDED,FAILED). - Template and presets
-
A template is a saved job config you reference by
templateId. Google provides presets, with ids such aspreset/web-hd, which produce a ready-made output without a config, and you can create your own templates. See the concepts page for the preset list and what each produces. - Elementary streams
-
One encoded video, audio or text stream: codec (H.264, H.265 or VP9 for video, AAC for audio), resolution, bit rate, frame rate, GOP duration.
- Mux streams
-
A container that packages elementary streams:
mp4,ts(MPEG-TS),fmp4(also used for VP9) orvtt(subtitles), with a key that manifests reference. Check the MuxStream reference for the current list. - Manifests
-
HLS (
.m3u8) and DASH (.mpd) manifests that list mux streams. One job can write both, so one set of renditions serves every player, see video streaming. - Sprite sheets
-
A sprite sheet configuration produces tiled thumbnail images at an interval, for scrubbing previews, see video covers and thumbnails. Reference: generate a sprite sheet.
A job with HLS, DASH and a sprite sheet
The config below defines a 720p and a 360p H.264 rendition plus AAC audio, packages them as HLS (ts) and DASH (fmp4),
writes a sprite sheet and publishes job events to Pub/Sub. It was parsed against the Transcoder API’s protobuf definition
(client library google-cloud-video-transcoder 1.22) for this page.
{
"config": {
"inputs": [
{ "key": "input0", "uri": "gs://bookshelf-uploads-my-project-id/uploads/42/original.mov" }
],
"editList": [
{ "key": "atom0", "inputs": ["input0"] }
],
"elementaryStreams": [
{ "key": "video-720", "videoStream": { "h264": {
"widthPixels": 1280, "heightPixels": 720, "frameRate": 30,
"bitrateBps": 3000000, "gopDuration": "2s", "profile": "high" } } },
{ "key": "video-360", "videoStream": { "h264": {
"widthPixels": 640, "heightPixels": 360, "frameRate": 30,
"bitrateBps": 800000, "gopDuration": "2s", "profile": "main" } } },
{ "key": "audio-aac", "audioStream": { "codec": "aac", "bitrateBps": 128000, "channelCount": 2, "sampleRateHertz": 48000 } }
],
"muxStreams": [
{ "key": "hls-720", "container": "ts", "elementaryStreams": ["video-720", "audio-aac"] },
{ "key": "hls-360", "container": "ts", "elementaryStreams": ["video-360", "audio-aac"] },
{ "key": "dash-video-720", "container": "fmp4", "elementaryStreams": ["video-720"] },
{ "key": "dash-video-360", "container": "fmp4", "elementaryStreams": ["video-360"] },
{ "key": "dash-audio", "container": "fmp4", "elementaryStreams": ["audio-aac"] }
],
"manifests": [
{ "fileName": "manifest.m3u8", "type": "HLS", "muxStreams": ["hls-720", "hls-360"] },
{ "fileName": "manifest.mpd", "type": "DASH", "muxStreams": ["dash-video-720", "dash-video-360", "dash-audio"] }
],
"spriteSheets": [
{ "filePrefix": "scrub", "spriteWidthPixels": 160, "spriteHeightPixels": 90, "interval": "10s" }
],
"output": { "uri": "gs://bookshelf-media-my-project-id/videos/42/" },
"pubsubDestination": { "topic": "projects/my-project-id/topics/transcoder-events" }
},
"labels": { "media-id": "42" }
}
curl -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-d @job.json \
"https://transcoder.googleapis.com/v1/projects/my-project-id/locations/us-central1/jobs"
Reference: jobs REST resource and
creating jobs. The HLS and DASH outputs are written under
gs://bookshelf-media-my-project-id/videos/42/, which a CDN serves, see the delivery section below.
Client libraries
from google.cloud.video import transcoder_v1
from google.cloud.video.transcoder_v1.services.transcoder_service import TranscoderServiceClient
client = TranscoderServiceClient()
parent = "projects/my-project-id/locations/us-central1"
def start_transcode(media_id: str, source: str) -> str:
job = transcoder_v1.types.Job(
input_uri=source,
output_uri=f"gs://bookshelf-media-my-project-id/videos/{media_id}/",
template_id="preset/web-hd",
labels={"media-id": media_id},
)
return client.create_job(parent=parent, job=job).name
To use an inline config, parse the JSON above: transcoder_v1.types.Job.from_json(text). Reference:
Python client for the Transcoder API.
import { TranscoderServiceClient } from '@google-cloud/video-transcoder';
const client = new TranscoderServiceClient();
export async function startTranscode(mediaId, source) {
const [job] = await client.createJob({
parent: client.locationPath('my-project-id', 'us-central1'),
job: {
inputUri: source,
outputUri: `gs://bookshelf-media-my-project-id/videos/${mediaId}/`,
templateId: 'preset/web-hd',
labels: { 'media-id': mediaId },
},
});
return job.name;
}
Reference: Node.js client for the Transcoder API.
import com.google.cloud.video.transcoder.v1.CreateJobRequest;
import com.google.cloud.video.transcoder.v1.Job;
import com.google.cloud.video.transcoder.v1.LocationName;
import com.google.cloud.video.transcoder.v1.TranscoderServiceClient;
try (TranscoderServiceClient client = TranscoderServiceClient.create()) {
CreateJobRequest request = CreateJobRequest.newBuilder()
.setParent(LocationName.of("my-project-id", "us-central1").toString())
.setJob(Job.newBuilder()
.setInputUri(source)
.setOutputUri("gs://bookshelf-media-my-project-id/videos/" + mediaId + "/")
.setTemplateId("preset/web-hd")
.putLabels("media-id", mediaId)
.build())
.build();
String name = client.createJob(request).getName();
}
Reference: Java client for the Transcoder API
(add google-cloud-video-transcoder through the libraries-bom, see
the Google Cloud section).
using Google.Cloud.Video.Transcoder.V1;
var client = await TranscoderServiceClient.CreateAsync();
var job = await client.CreateJobAsync(new CreateJobRequest
{
Parent = LocationName.FormatProjectLocation("my-project-id", "us-central1"),
Job = new Job
{
InputUri = source,
OutputUri = $"gs://bookshelf-media-my-project-id/videos/{mediaId}/",
TemplateId = "preset/web-hd",
Labels = { ["media-id"] = mediaId },
},
});
Console.WriteLine(job.Name);
Reference: .NET client for the Transcoder API.
Knowing when a job is done
Set pubsubDestination in the config, as above, and the service publishes a message for each job state change to the topic.
A Cloud Run service or function subscribed to the topic reads the media-id label and updates the media row, as in
upload pipelines. Pub/Sub delivers at least once, so
the handler must be idempotent. Alternatively, poll GET on the job name, which is simpler and fine at low volume.
Live and ad insertion
The Live Stream API ingests live contribution streams (RTMP, SRT) and produces HLS and DASH output for live delivery. The Video Stitcher API inserts ads into video on demand and live streams on the server side. Check whether the Video Stitcher API still needs to be enabled through your Google Cloud account team, as the product documentation said at the time of writing. Both are outside the stored-video path of this page.
Delivery: Media CDN and Cloud CDN
Serve bookshelf-media-my-project-id through a CDN, see
Cloud CDN and Media CDN.
-
Media CDN is built for streaming and large downloads. Its signed requests authorise a whole path prefix with a token (signed with an Ed25519 key from an edge cache keyset), which is the right fit for HLS and DASH, where the player requests many segment URLs.
-
Cloud CDN signed URLs and signed cookies work with a backend bucket or service, with an HMAC key. Signed cookies cover a prefix. See signed URLs and the signed cookies page of Cloud CDN.
-
Set CORS on the bucket for the player’s origin, and long
Cache-Controlon segments, see video streaming.
Images: Cloud Storage triggers Cloud Run
A resize function
A Cloud Run function (the name of Cloud Functions since the Cloud Run merge) receives a CloudEvent when an object is finalized in Cloud Storage:
import io
import functions_framework
from google.cloud import storage
from PIL import Image, ImageOps
client = storage.Client()
DEST = "bookshelf-media-my-project-id"
WIDTHS = (320, 640, 1280)
@functions_framework.cloud_event
def thumbnailer(cloud_event):
data = cloud_event.data
bucket, name = data["bucket"], data["name"]
if not name.startswith("covers/"):
return
prefix = name.rsplit("/", 1)[0]
blob = client.bucket(bucket).blob(name)
with Image.open(io.BytesIO(blob.download_as_bytes())) as image:
image = ImageOps.exif_transpose(image).convert("RGB")
for width in WIDTHS:
target = min(width, image.width)
resized = image.resize((target, round(image.height * target / image.width)), Image.Resampling.LANCZOS)
out = io.BytesIO()
resized.save(out, "WEBP", quality=78)
target_blob = client.bucket(DEST).blob(f"{prefix}/w{width}.webp")
target_blob.cache_control = "public, max-age=31536000, immutable"
target_blob.upload_from_string(out.getvalue(), content_type="image/webp")
gcloud functions deploy thumbnailer --gen2 --region=us-central1 --runtime=python313 \
--source=. --entry-point=thumbnailer \
--trigger-event-filters="type=google.cloud.storage.object.v1.finalized" \
--trigger-event-filters="bucket=bookshelf-uploads-my-project-id" \
--memory=1Gi --timeout=120s
Use a separate destination bucket, so the function’s output does not trigger it again. Eventarc delivers events at least
once, so write outputs to deterministic names, overwritten on retry. The trigger’s service account needs
eventarc.eventReceiver and read access to the source bucket. Reference:
Cloud Storage triggers for Cloud Run and
Cloud Run functions and events. The same logic runs in Node.js
(sharp), Java and .NET using the libraries on the backend pages, see
Node.js and Rust and
Java.
A signed upload URL
import datetime
from google.cloud import storage
def upload_url(media_id: str, content_type: str) -> str:
blob = storage.Client().bucket("bookshelf-uploads-my-project-id").blob(f"uploads/{media_id}/original")
return blob.generate_signed_url(
version="v4", expiration=datetime.timedelta(minutes=10), method="PUT", content_type=content_type,
)
The client must send the same Content-Type header. For large files, use resumable uploads. Reference:
signed URLs and
Cloud Storage security.
FFmpeg in Cloud Run jobs
When the Transcoder API is not enough (a codec or filter it lacks, a custom pipeline, a cover chosen by your own logic), run FFmpeg in a Cloud Run job: a container that runs to completion, with retries and a task timeout, and no server to keep running. See Cloud Run: scaling, traffic and jobs and Kubernetes Jobs, the equivalent on Kubernetes.
gcloud run jobs create transcode --region=us-central1 \
--image=us-central1-docker.pkg.dev/my-project-id/bookshelf/transcoder-worker:1 \
--cpu=4 --memory=8Gi --task-timeout=3600s --max-retries=2 --tasks=1
gcloud run jobs execute transcode --region=us-central1 \
--args="gs://bookshelf-uploads-my-project-id/uploads/42/original.mov,gs://bookshelf-media-my-project-id/videos/42/"
The worker image holds FFmpeg and a small program that downloads the input (or reads it from a mounted Cloud Storage bucket), runs the commands from video encoding fundamentals, and uploads the output. Start the execution from the function that handles the upload event, or from a Pub/Sub subscriber. Make the job idempotent and write the status at the end.
Pub/Sub notifications
Instead of Eventarc, a bucket can publish to a Pub/Sub topic:
gcloud storage buckets notifications create gs://bookshelf-uploads-my-project-id \
--topic=media-uploads --event-types=OBJECT_FINALIZE --object-prefix=uploads/
Pub/Sub notifications are delivered at least once, so a message can arrive twice and workers must be idempotent. Use a dead-letter topic on the subscription for messages that keep failing. Reference: Pub/Sub notifications for Cloud Storage.
Bibliography
Every source below is the official site, repository or documentation page of the library, service or standard it supports. Links were checked from an authoring environment with restricted outbound access. Entries marked (unverified) could not be opened from there (the site refused or blocked the request), so open them and confirm the content, version and status before you rely on them. Entries without the mark were opened successfully, or, for package pages, confirmed through the package registry.
-
Google Cloud, Transcoder API overview (unverified)
-
Google Cloud, Create jobs with the Transcoder API (unverified)
-
Google Cloud, Generate sprite sheets (unverified)
-
Google Cloud, Transcoder API jobs REST resource (unverified)
-
Google Cloud, Python client for the Transcoder API (unverified)
-
Google Cloud, Node.js client for the Transcoder API (unverified)
-
Google Cloud, Java client for the Transcoder API (unverified)
-
Google Cloud, .NET client for the Transcoder API (unverified)
-
Google Cloud, Live Stream API (unverified)
-
Google Cloud, Video Stitcher API (unverified)
-
Google Cloud, Media CDN signed requests (unverified)
-
Google Cloud, Cloud CDN signed URLs (unverified)
-
Google Cloud, Cloud Storage triggers for Cloud Run (unverified)
-
Google Cloud, Create Cloud Run jobs (unverified)
-
Google Cloud, Pub/Sub notifications for Cloud Storage (unverified)
-
Google Cloud, Cloud Storage signed URLs (unverified)
-
FFmpeg documentation (unverified)
-
woltapp/blurhash (unverified)