← Back to DevBytes

Designing a Video Streaming on GCP

Introduction to Video Streaming on Google Cloud Platform

Building a scalable, reliable video streaming platform requires careful orchestration of ingestion, processing, storage, and delivery components. Google Cloud Platform (GCP) offers a comprehensive suite of managed services that handle every stage of the video pipeline—from live ingest and transcoding to global edge caching and playback analytics. This tutorial walks you through designing a complete video streaming solution on GCP, covering both Video on Demand (VOD) and live streaming architectures with practical code examples you can deploy today.

What is Video Streaming on GCP?

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Video streaming on GCP refers to the use of Google Cloud's managed media services to ingest, process, store, and deliver video content at scale. Rather than building custom media processing infrastructure from scratch, developers leverage purpose-built APIs and services that handle the heavy lifting of video transcoding, packaging, encryption, and global distribution. The platform supports two primary streaming paradigms:

The core GCP services that form a video streaming architecture include Cloud Storage for durable media storage, the Transcoder API for format conversion and adaptive bitrate packaging, Cloud CDN or Media CDN for edge caching, and the Live Stream API for real-time ingest and transcoding of live feeds. Together, these services eliminate the need to manage fleets of encoding servers or complex content delivery networks.

Why GCP for Video Streaming Matters

Choosing GCP for video streaming infrastructure offers several compelling advantages that directly impact both developer productivity and end-user experience:

Core GCP Services for Video Streaming

Before diving into implementation, let's map out the key services and their roles in a streaming architecture:

Architecture Overview: Video on Demand (VOD)

The classic VOD workflow on GCP follows a linear ingestion-to-delivery pipeline. A source video file lands in an input Cloud Storage bucket, which triggers a transcoding job via the Transcoder API. The transcoded adaptive bitrate outputs (multiple renditions plus HLS/DASH manifests) are written to an output bucket. Cloud CDN or Media CDN is configured with the output bucket as its origin, caching segments at the edge for efficient delivery to end-user devices.

A typical VOD architecture diagram flows as follows:

Architecture Overview: Live Streaming

Live streaming introduces a real-time dimension. A broadcaster pushes a live RTMP or SRT feed to the Live Stream API input endpoint. The API transcodes the incoming stream into multiple adaptive bitrate renditions and continuously writes HLS segments and updating manifests to a Cloud Storage bucket. Viewers consume the live stream via Cloud CDN, which caches segments for the few seconds they remain relevant before the next segment arrives.

How to Build a VOD Streaming Pipeline

Let's walk through building a complete VOD pipeline step by step. We'll use gcloud commands and Python code examples that you can adapt directly.

Step 1: Create Cloud Storage Buckets

Create separate buckets for input (source files) and output (transcoded content). Using separate buckets simplifies permissions and lifecycle management.

# Create the input bucket (standard storage, regional for cost optimization)
gcloud storage buckets create gs://my-vod-input-bucket \
    --location=us-central1 \
    --storage-class=STANDARD

# Create the output bucket (standard storage, multi-region for global delivery)
gcloud storage buckets create gs://my-vod-output-bucket \
    --location=us \
    --storage-class=STANDARD

# Enable uniform bucket-level access for security
gcloud storage buckets update gs://my-vod-input-bucket --uniform-bucket-level-access
gcloud storage buckets update gs://my-vod-output-bucket --uniform-bucket-level-access

Step 2: Define a Transcoder Job Template

Job templates encapsulate your encoding settings so you can reuse them across jobs without specifying every detail each time. The following template creates an HLS adaptive bitrate ladder with four renditions.

# Create a job template for HLS adaptive bitrate output
gcloud transcoder templates create vod-hls-ladder \
    --json='{
  "templateId": "vod-hls-ladder",
  "jobConfig": {
    "inputList": [{"key": "input0"}],
    "outputList": [
      {
        "key": "output0",
        "uri": "gs://my-vod-output-bucket/${videoId}/master.m3u8",
        "editList": [
          {
            "key": "edit0",
            "inputs": ["input0"],
            "processing": {
              "videoStreams": [
                {
                  "key": "video-1080p",
                  "resolution": {"x": 1920, "y": 1080},
                  "bitrateBps": 5500000,
                  "frameRate": 30,
                  "codec": "h264",
                  "profile": "high",
                  "preset": "veryfast"
                },
                {
                  "key": "video-720p",
                  "resolution": {"x": 1280, "y": 720},
                  "bitrateBps": 3000000,
                  "frameRate": 30,
                  "codec": "h264",
                  "profile": "high",
                  "preset": "veryfast"
                },
                {
                  "key": "video-480p",
                  "resolution": {"x": 854, "y": 480},
                  "bitrateBps": 1200000,
                  "frameRate": 30,
                  "codec": "h264",
                  "profile": "main",
                  "preset": "veryfast"
                },
                {
                  "key": "video-360p",
                  "resolution": {"x": 640, "y": 360},
                  "bitrateBps": 600000,
                  "frameRate": 30,
                  "codec": "h264",
                  "profile": "main",
                  "preset": "veryfast"
                }
              ],
              "audioStreams": [
                {
                  "key": "audio-aac",
                  "codec": "aac",
                  "bitrateBps": 128000,
                  "channels": 2,
                  "sampleRateHertz": 48000
                }
              ],
              "manifests": [
                {
                  "fileName": "master.m3u8",
                  "type": "hls",
                  "muxStreams": [
                    {"key": "mux-1080p", "videoKeys": ["video-1080p"], "audioKeys": ["audio-aac"]},
                    {"key": "mux-720p", "videoKeys": ["video-720p"], "audioKeys": ["audio-aac"]},
                    {"key": "mux-480p", "videoKeys": ["video-480p"], "audioKeys": ["audio-aac"]},
                    {"key": "mux-360p", "videoKeys": ["video-360p"], "audioKeys": ["audio-aac"]}
                  ],
                  "segmentSettings": {
                    "segmentDuration": "4s",
                    "individualSegments": true
                  }
                }
              ]
            }
          }
        ]
      }
    ]
  }
}' --location=us-central1

Step 3: Create a Cloud Function Orchestrator

When a new video lands in the input bucket, we want to automatically trigger transcoding. This Python Cloud Function listens for Cloud Storage events and submits a job to the Transcoder API.

# requirements.txt for the Cloud Function
# google-cloud-transcoder==1.0.0
# google-cloud-storage==2.10.0

# main.py - Cloud Function triggered by Cloud Storage object finalize events
import os
import uuid
from google.cloud import transcoder_v1
from google.cloud import storage

def trigger_transcoding(event, context):
    """
    Triggered by a Cloud Storage object finalize event.
    event: Contains bucket name, file name, and other metadata.
    context: Contains event metadata like event ID and timestamp.
    """
    bucket_name = event['bucket']
    file_name = event['name']
    
    # Extract video ID from filename (remove extension)
    video_id = os.path.splitext(file_name)[0]
    
    # Initialize the Transcoder API client
    client = transcoder_v1.TranscoderServiceClient()
    
    project_id = os.environ.get('GCP_PROJECT', 'my-project-id')
    location = 'us-central1'
    parent = f'projects/{project_id}/locations/{location}'
    
    # Build the job configuration using our template
    job_config = {
        'template_id': 'vod-hls-ladder',
        'input_uri': f'gs://{bucket_name}/{file_name}',
        'output_uri': f'gs://my-vod-output-bucket/{video_id}/'
    }
    
    # Submit the transcoding job
    job = client.create_transcoder_job(
        parent=parent,
        transcoder_job={
            'input_uri': job_config['input_uri'],
            'output_uri': job_config['output_uri'],
            'template_id': job_config['template_id']
        }
    )
    
    print(f"Transcoding job created: {job.name}")
    print(f"Video ID: {video_id}")
    print(f"Job state: {job.state}")
    
    return f"Job {job.name} submitted successfully", 200

# Deploy the Cloud Function
# gcloud functions deploy trigger-transcoding \
#   --runtime=python310 \
#   --trigger-event=google.storage.object.finalize \
#   --trigger-resource=my-vod-input-bucket \
#   --entry-point=trigger_transcoding \
#   --region=us-central1 \
#   --memory=512MB \
#   --timeout=300s

Step 4: Configure Cloud CDN for VOD Delivery

With transcoded content in the output bucket, configure Cloud CDN to serve it globally. You'll need a load balancer and a backend bucket pointing to your output storage bucket.

# Step 1: Create a backend bucket pointing to the output storage bucket
gcloud compute backend-buckets create vod-backend-bucket \
    --gcs-bucket=my-vod-output-bucket \
    --enable-cdn \
    --cache-mode=CACHE_ALL_STATIC \
    --default-ttl=86400 \
    --max-ttl=2592000 \
    --description="Backend bucket for VOD HLS segments and manifests"

# Step 2: Create a URL map to route requests to the backend bucket
gcloud compute url-maps create vod-url-map \
    --default-backend-bucket=vod-backend-bucket \
    --description="URL map for VOD CDN delivery"

# Step 3: Create a global external HTTP(S) load balancer
gcloud compute target-http-proxies create vod-http-proxy \
    --url-map=vod-url-map

# Step 4: Reserve a global static IP and create a forwarding rule
gcloud compute addresses create vod-cdn-ip --global
gcloud compute forwarding-rules create vod-http-rule \
    --address=vod-cdn-ip \
    --target-http-proxy=vod-http-proxy \
    --ports=80 \
    --global

# After DNS propagation, viewers access content at:
# http://[CDN_IP]/[video_id]/master.m3u8

How to Build a Live Streaming Pipeline

Live streaming uses the Live Stream API, which handles ingest and real-time transcoding. Here's how to set up a live channel and start broadcasting.

Step 1: Create a Live Stream Input Endpoint

The input endpoint receives the broadcaster's RTMP or SRT stream. You create it once and it persists for the lifetime of your channel.

# Create an input endpoint for RTMP ingest
gcloud live-stream inputs create my-live-input \
    --location=us-central1 \
    --type=RTMP

# Retrieve the input endpoint URI (this is where you'll push your stream)
gcloud live-stream inputs describe my-live-input \
    --location=us-central1 \
    --format="get(uri)"

# Example output: 
# rtmp://1.2.3.4:1935/my-live-input/stream-key-abc123
# Broadcasters push to this URI using OBS, FFmpeg, or similar tools

Step 2: Create a Live Stream Channel

The channel defines the transcoding settings, output destination, and any recording configuration. It links an input endpoint to output renditions.

# Create a live channel configuration file: live_channel.json
cat > live_channel.json << 'EOF'
{
  "inputAttachments": [
    {
      "key": "main-input",
      "input": "projects/my-project-id/locations/us-central1/inputs/my-live-input"
    }
  ],
  "output": {
    "uri": "gs://my-live-output-bucket/live-stream-001/"
  },
  "elementaryStreams": [
    {
      "key": "video-1080p",
      "videoStream": {
        "h264": {
          "profile": "high",
          "bitrateBps": 5000000,
          "widthPixels": 1920,
          "heightPixels": 1080,
          "frameRate": 30
        }
      }
    },
    {
      "key": "video-720p",
      "videoStream": {
        "h264": {
          "profile": "high",
          "bitrateBps": 2800000,
          "widthPixels": 1280,
          "heightPixels": 720,
          "frameRate": 30
        }
      }
    },
    {
      "key": "video-480p",
      "videoStream": {
        "h264": {
          "profile": "main",
          "bitrateBps": 1100000,
          "widthPixels": 854,
          "heightPixels": 480,
          "frameRate": 30
        }
      }
    },
    {
      "key": "audio-aac",
      "audioStream": {
        "codec": "aac",
        "bitrateBps": 128000,
        "channelCount": 2,
        "sampleRateHertz": 48000
      }
    }
  ],
  "muxStreams": [
    {
      "key": "mux-1080p",
      "elementaryStreams": ["video-1080p", "audio-aac"],
      "segmentSettings": {
        "segmentDuration": "2s"
      }
    },
    {
      "key": "mux-720p",
      "elementaryStreams": ["video-720p", "audio-aac"],
      "segmentSettings": {
        "segmentDuration": "2s"
      }
    },
    {
      "key": "mux-480p",
      "elementaryStreams": ["video-480p", "audio-aac"],
      "segmentSettings": {
        "segmentDuration": "2s"
      }
    }
  ],
  "manifests": [
    {
      "fileName": "main.m3u8",
      "type": "HLS",
      "muxStreams": ["mux-1080p", "mux-720p", "mux-480p"],
      "maxSegmentCount": 5
    }
  ]
}
EOF

# Create the live channel
gcloud live-stream channels create my-live-channel \
    --location=us-central1 \
    --config-file=live_channel.json

# Note the channel ID returned for starting/stopping the channel

Step 3: Start the Live Channel

Once the channel is created, you start it to begin accepting the ingest stream and producing output. The channel remains running until explicitly stopped.

# Start the live channel
gcloud live-stream channels start my-live-channel \
    --location=us-central1

# The channel is now accepting input on the RTMP endpoint
# Output segments and manifests will appear in gs://my-live-output-bucket/live-stream-001/

# To stop the channel when the broadcast ends:
gcloud live-stream channels stop my-live-channel \
    --location=us-central1

Step 4: Serve the Live Stream via Cloud CDN

Live streams require different CDN caching behavior than VOD. Manifests should be cached for only a few seconds so viewers always get the latest segments, while segments can be cached longer since they are immutable once written.

# Create a backend bucket with live-appropriate cache settings
gcloud compute backend-buckets create live-backend-bucket \
    --gcs-bucket=my-live-output-bucket \
    --enable-cdn \
    --cache-mode=USE_ORIGIN_HEADERS \
    --default-ttl=2 \
    --max-ttl=5 \
    --description="Backend bucket for live HLS streaming with short cache TTLs"

# Create URL map and load balancer as shown in the VOD section
# The viewer plays the live stream at:
# http://[CDN_IP]/live-stream-001/main.m3u8

Advanced Transcoding with the Transcoder API

Beyond basic adaptive bitrate ladders, the Transcoder API supports sophisticated workflows including thumbnail generation, DRM encryption, and multi-input editing. Here are practical examples for each.

Generating Thumbnails (Sprite Sheets)

Thumbnail sprite sheets enable video players to show preview images when users hover over the timeline. The Transcoder API can generate these automatically as part of a transcoding job.

# Add thumbnail generation to a transcoding job configuration
# Include this in the editList processing section
"spriteSheets": [
  {
    "filePrefix": "gs://my-vod-output-bucket/${videoId}/thumbnails/sprite",
    "spriteWidthPixels": 160,
    "spriteHeightPixels": 90,
    "columnCount": 10,
    "rowCount": 10,
    "interval": "5s",
    "quality": 80
  }
]

# This generates sprite sheet images like:
# sprite-000000000.jpg, sprite-000000001.jpg, etc.
# Each containing 10x10=100 thumbnails at 5-second intervals

DRM Encryption with Widevine and FairPlay

For premium content, you can encrypt your HLS streams using the Transcoder API's built-in DRM support. This requires integration with a DRM license server.

# DRM encryption configuration for HLS with Widevine
# Add to the manifest settings in your transcoding job
"drm": {
  "widevine": {
    "contentId": "my-unique-content-id-001",
    "pssh": "AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ...",
    "keyProvider": {
      "type": "widevine",
      "widevine": {
        "keyId": "1234567890abcdef1234567890abcdef",
        "key": "abcdef1234567890abcdef1234567890"
      }
    }
  }
}

# For FairPlay (used by Apple devices), add:
"fairplay": {
  "keyProvider": {
    "type": "fairplay",
    "fairplay": {
      "keyId": "11223344556677889900aabbccddeeff",
      "iv": "00112233445566778899aabbccddeeff",
      "key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
    }
  }
}

Securing Your Video Streams

Protecting your content requires multiple layers of security, from access control at the CDN level to encryption at the segment level. GCP provides robust tools for both.

Signed URL Authentication with Cloud CDN

Signed URLs restrict access to your CDN content so that only authorized viewers with valid, time-limited tokens can retrieve manifests and segments.

# Python code to generate a signed URL for Cloud CDN
import hashlib
import hmac
import base64
import time
from urllib.parse import quote

def generate_signed_url(base_url, secret_key, expiration_seconds=3600):
    """
    Generate a signed URL for Cloud CDN content access.
    
    Args:
        base_url: The CDN URL path (e.g., '/my-video/master.m3u8')
        secret_key: Shared secret configured in the backend bucket
        expiration_seconds: How long the URL remains valid
    
    Returns:
        Full signed URL string
    """
    # Current timestamp in seconds since epoch
    current_time = int(time.time())
    expiration_time = current_time + expiration_seconds
    
    # Build the signature components
    # Format: URL|EXPIRATION|KEY_NAME
    signature_input = f"{base_url}|{expiration_time}|my-key-name"
    
    # Create HMAC-SHA1 signature using the secret key
    # Note: Cloud CDN expects the key decoded from base64url
    decoded_key = base64.urlsafe_b64decode(secret_key)
    signature = hmac.new(
        decoded_key,
        signature_input.encode('utf-8'),
        hashlib.sha1
    ).digest()
    
    # Base64url-encode the signature (without padding)
    encoded_signature = base64.urlsafe_b64encode(signature).rstrip(b'=').decode('utf-8')
    
    # Construct the full signed URL
    signed_url = (
        f"https://cdn.example.com{base_url}"
        f"?Expires={expiration_time}"
        f"&KeyName=my-key-name"
        f"&Signature={encoded_signature}"
    )
    
    return signed_url

# Example usage
secret = "your-base64url-encoded-secret-key-from-backend-bucket-config"
url = generate_signed_url("/videos/abc123/master.m3u8", secret, 3600)
print(url)
# Output: https://cdn.example.com/videos/abc123/master.m3u8?Expires=1712345678&KeyName=my-key-name&Signature=abc123def456...

Setting Up the Signed URL Secret in Cloud CDN

# Configure the signed URL secret on the backend bucket
# First, generate a strong secret and base64url-encode it
head -c 32 /dev/urandom | base64 -w 0 | tr '+/' '-_' | tr -d '='
# Example output: kQj9nX8vL2mP5rT6wYzA3bC4dE1fG7hI...

# Apply the secret to the backend bucket
gcloud compute backend-buckets update vod-backend-bucket \
    --add-signed-url-key \
    --key-name=my-key-name \
    --key-value=kQj9nX8vL2mP5rT6wYzA3bC4dE1fG7hI... \
    --caches-duration=3600

# Now only requests with valid signed URLs will be served
# Unauthorized requests receive HTTP 403 Forbidden

Monitoring and Analytics

A production video streaming platform requires observability into every stage of the pipeline. GCP integrates logging and metrics across services, enabling you to build dashboards and alerts for operational health.

Cloud Logging for Transcoder and Live Stream API

# View transcoding job logs
gcloud logging read 'resource.type="transcoder.googleapis.com/Job"' \
    --limit=20 \
    --format="json"

# View live stream channel events
gcloud logging read 'resource.type="livestream.googleapis.com/Channel"' \
    --limit=20 \
    --format="json"

# Common log query for failed transcoding jobs in the last 24 hours
gcloud logging read 'resource.type="transcoder.googleapis.com/Job" AND severity>=ERROR' \
    --freshness=1d \
    --limit=50

Playback Analytics with BigQuery

For deeper insights into viewer behavior, instrument your video player to emit playback events (play, pause, seek, buffering, quality switches) to a Cloud Run endpoint that writes to BigQuery. Here's a sample schema and ingestion function:

# BigQuery table schema for playback events
# Create the dataset and table
bq mk --dataset my-project:video_analytics

bq mk --table my-project:video_analytics.playback_events \
  event_id:STRING, \
  video_id:STRING, \
  session_id:STRING, \
  user_id:STRING, \
  event_type:STRING, \
  timestamp:TIMESTAMP, \
  playhead_position:FLOAT, \
  bitrate:FLOAT, \
  buffering_duration:FLOAT, \
  player_resolution:STRING, \
  cdn_node:STRING, \
  client_ip:STRING, \
  user_agent:STRING

# Example Cloud Run service (Python) to ingest playback events
# main.py
import os
import json
from flask import Flask, request
from google.cloud import bigquery
from datetime import datetime

app = Flask(__name__)
client = bigquery.Client()
table_id = os.environ['BQ_TABLE_ID']  # my-project:video_analytics.playback_events

@app.route('/ingest', methods=['POST'])
def ingest_event():
    """Receive playback analytics events from video players."""
    event_data = request.get_json()
    
    # Add server-side metadata
    event_data['timestamp'] = datetime.utcnow().isoformat()
    event_data['client_ip'] = request.remote_addr
    event_data['user_agent'] = request.headers.get('User-Agent', 'unknown')
    
    # Insert into BigQuery (streaming insert for near-real-time analytics)
    errors = client.insert_rows_json(table_id, [event_data])
    
    if errors:
        return json.dumps({'status': 'error', 'errors': errors}), 500
    
    return json.dumps({'status': 'ok'}), 200

# Deploy as Cloud Run service
# gcloud run deploy playback-analytics-ingest \
#   --source=. \
#   --region=us-central1 \
#   --allow-unauthenticated \
#   --set-env-vars=BQ_TABLE_ID=my-project:video_analytics.playback_events

Cost Optimization Strategies

Video streaming can generate significant cloud costs if not managed carefully. Here are concrete strategies to keep your infrastructure efficient:

Best Practices for Production Deployments

Drawing from real-world deployments, these best practices will help you build a robust, secure, and performant video streaming platform on GCP:

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles