Media Optimization on Azure
|
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 uses the Bookshelf names of the Azure section: resource group rg-bookshelf, region
eastus2 (a placeholder), the storage account stbookshelf with the containers uploads and media, and the Container
Apps environment cae-bookshelf. Verify every date, product status and limit on the linked Microsoft Learn page before you
rely on it. Prices are not quoted here. The concepts are in
upload pipelines,
video encoding fundamentals and
video streaming.
This is a single page because Azure no longer has a first-party managed transcoder, so most of the work is a self-hosted design built from general services.
Azure Media Services is retired
Azure Media Services was retired on 30 June 2024. Its encoding (transcoding), packaging, streaming endpoint and live event features no longer exist, so no new design should use it. Microsoft’s retirement guide describes the options:
-
Azure AI Video Indexer extracts insights from video and audio (transcripts, faces, labels, topics, scenes and thumbnails). It is an analysis service, not a transcoder: it does not produce a bitrate ladder for streaming. See the Video Indexer overview. Video Indexer and the other analysis services are placed among the Azure AI offerings in Immersive Reader, Video Indexer and retired services.
-
Partner solutions from the Azure Marketplace and from partner companies (the guide names providers such as Bitmovin and MediaKind) offer managed encoding, packaging, DRM and delivery. Evaluate them like any vendor: formats, price model, regions, and exit.
-
Self-hosted FFmpeg on Azure compute, described next.
Self-hosted FFmpeg on Azure
These are architecture choices, not an official Microsoft replacement for Media Services: you own the FFmpeg version, the ladder and the packaging, see video streaming.
Container Apps event-driven jobs with a KEDA queue scaler
An event-driven Container Apps job starts one execution when a scale rule sees work, and the execution runs to completion
and exits, so nothing runs (or is paid for) while the queue is empty. The queue scaler is
KEDA’s Azure Storage Queue scaler. This is the same job
model as the Bookshelf notifier, see
Container Apps: scaling, jobs and Dapr and
Container Apps.
az storage queue create --account-name stbookshelf --name media-jobs --auth-mode login
az containerapp job create \
--name ffmpeg-worker --resource-group rg-bookshelf --environment cae-bookshelf \
--trigger-type Event \
--replica-timeout 3600 --replica-retry-limit 2 --parallelism 1 --replica-completion-count 1 \
--image acrbookshelf.azurecr.io/ffmpeg-worker:1.0.0 \
--cpu 4 --memory 8Gi \
--user-assigned "$(az identity show --name id-ffmpeg --resource-group rg-bookshelf --query id -o tsv)" \
--min-executions 0 --max-executions 10 --polling-interval 30 \
--scale-rule-name media-queue \
--scale-rule-type azure-queue \
--scale-rule-metadata "accountName=stbookshelf" "queueName=media-jobs" "queueLength=1" \
--scale-rule-identity "$(az identity show --name id-ffmpeg --resource-group rg-bookshelf --query id -o tsv)"
Check the CPU and memory combinations that your environment’s workload profile allows: video encoding needs more than the
consumption profile’s smaller sizes, so use a dedicated workload profile for large jobs. The worker’s managed identity
needs Storage Queue Data Contributor on the queue and Storage Blob Data Contributor on stbookshelf, see
Blob Storage. The worker:
import subprocess, tempfile
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from azure.storage.queue import QueueClient
cred = DefaultAzureCredential()
queue = QueueClient("https://stbookshelf.queue.core.windows.net", "media-jobs", credential=cred)
blobs = BlobServiceClient("https://stbookshelf.blob.core.windows.net", credential=cred)
def run_once():
for message in queue.receive_messages(max_messages=1, visibility_timeout=3600):
media_id = message.content # the job carries ids, never the file
with tempfile.TemporaryDirectory() as tmp:
src = f"{tmp}/in"
with open(src, "wb") as f:
blobs.get_blob_client("uploads", f"{media_id}/original").download_blob().readinto(f)
subprocess.run(["ffmpeg", "-y", "-i", src, "-vf", "scale=-2:720", "-c:v", "libx264", "-crf", "22",
"-g", "48", "-c:a", "aac", "-movflags", "+faststart", f"{tmp}/720p.mp4"],
check=True, timeout=3300)
with open(f"{tmp}/720p.mp4", "rb") as f:
blobs.get_blob_client("media", f"{media_id}/720p.mp4").upload_blob(f, overwrite=True)
queue.delete_message(message) # only after the output is written
if __name__ == "__main__":
run_once()
If the process dies, the message becomes visible again after the visibility timeout, and another execution picks it up, so the work is repeated: keep outputs at fixed names (overwritten on retry), as described in upload pipelines. Use a poison-message policy (a dequeue count check that moves the message to a dead-letter queue). Service Bus gives dead-lettering built in, see Messaging and Integration. The ffmpeg commands for the full ladder and HLS packaging are in video streaming. References: event-driven jobs in Container Apps and Container Apps jobs.
Azure Batch
Azure Batch runs many compute tasks in parallel on a pool of VMs. Microsoft’s tutorial for the .NET API converts a set of MP4 files with FFmpeg, which is the same job as above at larger scale, on VMs you size and (optionally) on low-priority or Spot capacity. Batch suits a big backlog (a library migration) or GPU or very large encodes. For a steady stream of uploads, Container Apps jobs are simpler to operate. Treat the tutorial as a sample of the approach, since it targets an older .NET and is not a maintained product.
Images: Blob Storage, Event Grid and Azure Functions
Client -- PUT with SAS --> Blob uploads/ -- BlobCreated --> Event Grid --> Azure Function --> Blob media/
The official resize images on blob upload tutorial shows this pattern with a function that Event Grid calls when a blob is created. That sample is old: it targets an earlier Functions programming model. For a new function, use the isolated worker model (a separate .NET process) because Microsoft is retiring the in-process model (check the migration guide for the date), or Python or Node.js, see App Service and Functions.
.NET isolated worker function
using Azure.Messaging;
using Azure.Storage.Blobs;
using Microsoft.Azure.Functions.Worker;
using SkiaSharp;
public class Thumbnailer(BlobServiceClient blobs)
{
private static readonly int[] Widths = [320, 640, 1280];
[Function("Thumbnailer")]
public async Task Run([EventGridTrigger] CloudEvent cloudEvent)
{
var subject = cloudEvent.Subject ?? throw new InvalidOperationException("No subject");
// subject: /blobServices/default/containers/uploads/blobs/covers/b81f/original.jpg
var blobPath = subject[(subject.IndexOf("/blobs/", StringComparison.Ordinal) + 7)..];
var prefix = blobPath[..blobPath.LastIndexOf('/')];
var source = blobs.GetBlobContainerClient("uploads").GetBlobClient(blobPath);
using var original = SKBitmap.Decode((await source.DownloadContentAsync()).Value.Content.ToStream());
foreach (var width in Widths)
{
var target = Math.Min(width, original.Width);
var height = (int)Math.Round(original.Height * (double)target / original.Width);
using var resized = original.Resize(new SKImageInfo(target, height), new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear));
using var data = SKImage.FromBitmap(resized).Encode(SKEncodedImageFormat.Webp, 78);
var dest = blobs.GetBlobContainerClient("media").GetBlobClient($"{prefix}/w{width}.webp");
await dest.UploadAsync(data.AsStream(), overwrite: true);
}
}
}
Register the BlobServiceClient with a managed identity in Program.cs, and give the function app Storage Blob Data
Contributor. SkiaSharp needs its native assets for Linux (SkiaSharp.NativeAssets.Linux.NoDependencies on a Functions
host without system libraries). For libraries and licences see
.NET libraries. The function binds a CloudEvent,
so the Event Grid subscription must deliver the CloudEvents 1.0 schema (--event-delivery-schema cloudeventschemav1_0):
az eventgrid system-topic create --name evgt-uploads --resource-group rg-bookshelf --location eastus2 \
--topic-type microsoft.storage.storageaccounts \
--source "$(az storage account show --name stbookshelf --query id -o tsv)"
az eventgrid system-topic event-subscription create --name resize --resource-group rg-bookshelf \
--system-topic-name evgt-uploads --included-event-types Microsoft.Storage.BlobCreated \
--subject-begins-with /blobServices/default/containers/uploads/blobs/covers/ \
--event-delivery-schema cloudeventschemav1_0 \
--endpoint-type azurefunction \
--endpoint "$(az functionapp function show --name func-bookshelf --resource-group rg-bookshelf --function-name Thumbnailer --query id -o tsv)"
Event Grid retries delivery and supports dead-lettering to a storage account, which gives the retry and dead-letter behaviour of the pipeline pattern. Events are delivered at least once, so the function overwrites fixed names. Reference: Blob storage events and Event Grid trigger.
Python
import io
import azure.functions as func
from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from PIL import Image, ImageOps
app = func.FunctionApp()
blobs = BlobServiceClient("https://stbookshelf.blob.core.windows.net", credential=DefaultAzureCredential())
@app.event_grid_trigger(arg_name="event")
def thumbnailer(event: func.EventGridEvent):
path = event.subject.split("/blobs/", 1)[1] # covers/b81f/original.jpg
prefix = path.rsplit("/", 1)[0]
data = blobs.get_blob_client("uploads", path).download_blob().readall()
with Image.open(io.BytesIO(data)) as image:
image = ImageOps.exif_transpose(image).convert("RGB")
for width in (320, 640, 1280):
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)
blobs.get_blob_client("media", f"{prefix}/w{width}.webp").upload_blob(out.getvalue(), overwrite=True)
This function reads a func.EventGridEvent, so give it its own subscription created like the one above but without
--event-delivery-schema, which keeps the default Event Grid schema. Reference:
Azure Functions Python developer guide.
For Node.js and Java, use the app.eventGrid registration and the @EventGridTrigger annotation respectively, with the
libraries from Node.js and
Java.
Upload with a user delegation SAS
The client uploads straight to uploads/ with a short-lived SAS. A user delegation SAS is signed with Microsoft Entra
credentials instead of the account key, which is preferred. Python:
import datetime
from azure.storage.blob import BlobSasPermissions, BlobServiceClient, generate_blob_sas
def upload_url(media_id: str) -> str:
service = BlobServiceClient("https://stbookshelf.blob.core.windows.net", credential=cred) # cred as above
now = datetime.datetime.now(datetime.timezone.utc)
key = service.get_user_delegation_key(now, now + datetime.timedelta(minutes=15))
sas = generate_blob_sas(
account_name="stbookshelf", container_name="uploads", blob_name=f"{media_id}/original",
user_delegation_key=key, permission=BlobSasPermissions(create=True, write=True),
expiry=now + datetime.timedelta(minutes=10),
)
return f"https://stbookshelf.blob.core.windows.net/uploads/{media_id}/original?{sas}"
The .NET equivalent uses BlobSasBuilder with blobServiceClient.GetUserDelegationKeyAsync, the Java one
BlobServiceSasSignatureValues with getUserDelegationKey, and Node.js generateBlobSASQueryParameters, all described in
creating a user
delegation SAS and the other language pages next to it. The client sends x-ms-blob-type: BlockBlob on the PUT. For the
account layout see Blob Storage.
Smart-crop thumbnails with Azure AI Vision
Azure AI Vision can generate thumbnails with a smart crop around the area of interest, see generating thumbnails. This feature is deprecated and retires on 25 September 2028, so do not build new designs on it. Alternatives are a focal point chosen by the user, attention-based crops in libvips and sharp (see responsive images and thumbnails), or analysis with the services in Foundry Tools.
Delivery: Front Door and the CDN retirements
Serve media/ from stbookshelf through Azure Front Door Standard or Premium, which is Microsoft’s current CDN and global
load balancer, see Front Door and CDN and the
Front Door overview. Cache long on segments and
variants, set CORS for the player origin, and use the Premium tier’s Private Link to keep the storage account private. Private video
needs a prefix-wide authorisation, as discussed in video streaming. Check the
Front Door overview for its current options for token-based access control, and if none fits, put the access decision in your own
API and keep the storage account private behind Front Door.
Several other Azure CDN offerings were retired or have a retirement date. Per Microsoft’s pages:
-
Azure CDN from Edgio was retired on 15 January 2025.
-
Azure CDN Standard from Microsoft (classic) retires on 30 September 2027, with migration to Front Door Standard or Premium.
-
Azure CDN from Akamai was retired earlier. Verify the date on the Microsoft Learn CDN page.
Check the Azure CDN overview and the Front Door migration guidance before you pick a CDN, and plan to leave any classic CDN profile.
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.
-
Microsoft Learn, Azure Media Services retirement guide (unverified)
-
Microsoft Learn, Azure AI Video Indexer overview (unverified)
-
Microsoft Learn, Event-driven jobs in Azure Container Apps (unverified)
-
Microsoft Learn, Jobs in Azure Container Apps (unverified)
-
KEDA, Azure Storage Queue scaler (unverified)
-
Microsoft Learn, Run a parallel workload with Azure Batch (unverified)
-
Microsoft Learn, Resize images on blob upload with Event Grid (unverified)
-
Microsoft Learn, Blob storage events for Event Grid (unverified)
-
Microsoft Learn, Event Grid trigger for Azure Functions (unverified)
-
Microsoft Learn, Azure Functions Python developer guide (unverified)
-
Microsoft Learn, Migrate .NET apps to the isolated worker model (unverified)
-
Microsoft Learn, Create a user delegation SAS with Python (unverified)
-
Microsoft Learn, Generate smart-cropped thumbnails (unverified)
-
Microsoft Learn, Azure Front Door overview (unverified)
-
Microsoft Learn, Azure CDN overview (unverified)
-
FFmpeg documentation (unverified)
-
woltapp/blurhash (unverified)