Skip to content
transcodelyproduct updateper-title encodingVMAFABR

Per-Title Encoding Is Live: One Field, and the Search Runs on Our Side

10 min read Dimitar Todorov

Set content_aware.mode to per_title on an output and Transcodely finds the CRF that source needs to reach your VMAF target, then encodes the ladder you declared at it. 1.5x on the output, charged only when a search actually ran. Here is how the search works, what it refuses, and the numbers from the run that qualified it.

Per-title encoding is available on the Transcodely API as of version 5.21.0. It is one field on an output: set content_aware.mode to per_title, keep the ladder you already declare, and the worker finds the CRF this particular source needs to reach a VMAF target before it encodes. The ladder itself does not change. The price quoted at create still holds. And if the search cannot run for any reason, you get the ordinary encode at the ordinary price.

If you want the background on why fixed ladders waste bits, the per-title explainer covers Netflix’s original work and the savings by content type. This post is about what we shipped, exactly how it behaves, and what it measured on the day it went live.

What you send

An HLS ladder with per-title enabled looks like this:

{
  "outputs": [
    {
      "type": "hls",
      "video": [
        {"codec": "h264", "resolution": "1080p", "quality": "standard"},
        {"codec": "h264", "resolution": "720p",  "quality": "standard"},
        {"codec": "h264", "resolution": "480p",  "quality": "standard"}
      ],
      "content_aware": {"mode": "per_title"}
    }
  ]
}

That is the whole change. vmaf_target is optional, accepts 80 to 97, and defaults to 93 on every quality tier. There is no tier restriction: economy, standard and premium all accept it, and the search seeds from whatever CRF the rung would have used anyway.

How the search works

One search per output, on its top rung. The worker takes the output’s highest-resolution quality-driven rendition, cuts a sample from the middle of the source (three short samples spread across it for long sources), and encodes that sample at a handful of CRFs. Each sample is scored with libvmaf against the untouched original.

The search is bounded. At most four sample encodes of at most fifteen seconds each. It interpolates between the last two measurements instead of bisecting blindly, and because it starts from the CRF the rung would have used, it is usually already close.

The answer is the highest CRF that still scores at or above the target, within a 0.5 tolerance. That is the cheapest setting that clears your quality bar, not the best quality the encoder could produce.

The lower rungs move with it, for free. They are not searched. Each takes the CRF offset your ladder already declares relative to the top rung, so your quality shape is preserved and only its absolute level shifts. The seeded ABR presets carry that shape explicitly: 2160p at CRF 22, 1080p at 23, 720p at 24, 480p at 26. Search the top rung to 27 and the rest follow at their offsets.

It is deterministic. Sample positions are a pure function of the source’s duration, the candidate arithmetic is integer, and the encoders run in their deterministic modes. The same file searched twice gives the same CRF.

What comes back

A per-title output reports what the search decided on report.content_aware:

{
  "content_aware": {
    "mode": "per_title",
    "vmaf_target": 93,
    "vmaf_achieved": 93.043,
    "crf_chosen": 27
  }
}

Two things to read carefully. vmaf_target is the target the search actually used, so if you asked for a value it clamped, you see the clamped one. vmaf_achieved is the score the chosen setting reached on the analysis samples; the delivered file is never itself scored. The block is absent on an output whose search never ran, which is the honest reading: that encode used the quality tier’s own CRF.

The report rides everywhere an output does, including the job.succeeded and output.ready webhooks and the output report panel in the dashboard.

API 5.23.0 added the evidence behind the conclusion to the same block: seed_crf, the CRF the rung would have used without the search; met_target, whether any probe actually reached the target; and probes[], every sample the search measured in the order it measured them, each with its crf, vmaf and bitrate_kbps. Those three are filled by workers from 1.29.0 onward. At the time of writing the fleet runs 1.28.2, so on today’s jobs they are absent and only the conclusion above is reported; the job page in the dashboard draws the curve as soon as a report carries one.

When the curve is there, it is the only place a saving can honestly be read from a single job: the ratio between the seed_crf probe’s bitrate and the crf_chosen probe’s. Both are the same cut of the same source at the same delivery settings, so they compare directly. A probe is encoded video-only, so its bitrate is never comparable to the delivered file’s muxed rate, and nothing on the report was measured on the delivered file itself.

What it costs, and when it does not

Per-title carries a 1.5x multiplier on the output, applied once rather than once per rung, on top of the ordinary per-minute rate. The lower rungs carry it too, since they inherit the searched CRF. The flat processing fee per job is unchanged.

A worked example at the published rates, for a 20-minute source and the three-rung H.264 ladder above at standard quality:

Per minute 20 minutes
Ladder without per-title (1080p €0.01 + 720p €0.0075 + 480p €0.005) €0.0225 €0.45
Same ladder with per-title (×1.5) €0.03375 €0.675
Uplift €0.01125 €0.225

The multiplier is only charged when a search actually chose the CRF. A search that cannot run, because of an unknown duration, an encode error, or a worker that could not perform it, falls back to the tier’s own CRF, the job proceeds, and that output is billed at 1.0x. Its own pricing snapshot states the corrected number. We would rather deliver a correct file at the ordinary price than fail a job, and we do not think you should pay an uplift for an analysis that did not shape anything.

What it refuses

Three requests are refused, all for the same reason: you would pay 1.5x for an analysis that could not do anything.

Refusal When Error
No video to tune The output encodes no video at all (captions-only, audio-only) parameter_incompatible at create
Nothing for a CRF to do Every video rendition is pinned to an explicit bitrate, so the search result would be discarded. A mixed ladder is fine; one quality-driven rendition is enough, and it is the one searched parameter_incompatible at create
Too short to pay for itself The effective encode window is under 120 seconds. The search costs the same whatever the source length, while the multiplier scales with it, so below two minutes the uplift does not cover the machine time per_title_source_too_short at probe

The third is decided at probe rather than at create because nothing has read your input at create. It measures what will be encoded: a twenty-second clip of a ten-minute film is refused.

Auto-ABR ladder generation is not available. content_aware.mode = auto_abr is still rejected at job creation with parameter_unsupported. The reason is the one thing per-title deliberately does not do: change the rendition count. A job is priced per declared rendition at create, and a mode that decides the count afterwards would invalidate the quote you already accepted. We will ship it when we have a pricing shape that does not do that.

What it measured on the day it shipped

Every capability we sell has a validation case that runs against real workers and real object storage, and the results are on the public conformance page. The validation post explains the machinery. The per-title cases ran against worker 1.28.1 on 2026-09-15, on a 150-second 1080p real-content source:

Case Target VMAF Achieved on samples CRF chosen Searched output vs identical untuned twin
Single 1080p H.264 output, default target 93 93.043 27 1,896 vs 2,988 kbps, 63.5%
Three-rung HLS ladder, non-default target 90 90.426 29 1.5x applied once, at the output level

The delivered 1080p file, scored independently by the harness, came in at VMAF 91.90 against an asserted floor of 88. The ladder case asks for a non-default target on purpose: every other case asks for 93, which is also the default, so only this one can tell an honoured target from an ignored one.

These are measurements of that clip. They are not a promised savings figure for your library, and I would be suspicious of any vendor who quotes one without showing you the source it was measured on. Talking-head content will compress further; sport and grain will compress less, and on some sources the search will land above the tier’s default bitrate because that is what the target needs.

When the uplift pays for itself

Per-title costs encoding money once and saves delivery money on every view. The break-even depends entirely on what your content does under the search and what you pay for egress, so here is the arithmetic with the one number we actually have.

Take the 20-minute ladder above, whose per-title uplift was €0.225. If the 1080p rung is what most viewers watch, at the platform’s 5 Mbps 1080p bitrate one full view moves 750 MB. If your title behaves like the validation clip and lands at 63.5% of that, a view moves 476 MB, a saving of 274 MB. On Transcodely hosting egress at €0.010 per GB, that is about €0.0027 saved per full view, so the uplift is repaid after roughly 82 views. On a CDN that charges several times that per gigabyte, the break-even comes several times sooner.

Below a hundred views per title, per-title encoding is probably not worth it. Above a few hundred, it is difficult to argue against.

What took two attempts

Per-title was accepted in 5.20.0 at 01:05 UTC on 15 September, withdrawn in 5.20.1 at 03:55 the same morning, and re-accepted in 5.21.0 at 09:25. The first real-worker run measured the search producing a larger file, and the uplift guard described above is what kept that from ever reaching an invoice. The worker fix, the root cause, and why our CI did not catch it deserve their own post, and they will get one. For now, the short version is that the feature you can use today is the one that passed on real workers, and the one that did not was on the API for less than three hours with no customer job on record.

Try it

Per-title needs a source of at least two minutes and an output with at least one quality-driven rendition. Add content_aware: {"mode": "per_title"} to an output on any job, or switch on “Per-title (content-aware)” on an output in the dashboard’s job wizard, and read report.content_aware when it completes. The API reference has every field and every refusal, and the pricing docs have the multiplier arithmetic.

Topics

transcodelyproduct updateper-title encodingVMAFABR

Share this article