feat(hybrid): generate dolby vision from hdr10+ with --generate-dv

Builds per-scene L1 in unshackle (PQ mean, letterbox-aware) with a generate_dv_mode config (v29/v40/legacy); a failed hybrid no longer exits 0 with no video.
This commit is contained in:
imSp4rky
2026-10-17 16:40:14 -06:00
parent 14bd2f2c7e
commit 35871f3a52
22 changed files with 1370 additions and 286 deletions
+7
View File
@@ -156,8 +156,15 @@ class TrackRequest:
codecs: list[Video.Codec] = [] # empty == accept any codec
ranges: list[Video.Range] = [Video.Range.SDR]
best_available: bool = False
generate_dv: bool = False # --generate-dv: make Dolby Vision from HDR10+
```
!!! note "`generate_dv` and a hand-made HYBRID"
A service that fetches the HYBRID variants itself (HDR10 and DV) must also fetch
HDR10P when `track_request.generate_dv` is set and the title has no DV track.
The conversion needs an HEVC HDR10+ track as its base.
`get_tracks_for_variants` does this for you.
!!! note "Read the request, don't second-guess the user"
Services may narrow `track_request` for hard technical constraints (e.g.
*"this service delivers HDR only as HEVC"*), but must **not** filter
+1
View File
@@ -471,6 +471,7 @@ Make a download job. It requires `service` and `title_id`. Every other field is
| `downloads` | int | `1` | Concurrent tracks. |
| `worst` | boolean | `false` | Lowest bitrate within quality. |
| `best_available` | boolean | `false` | Fall back to best available. With `all_drm`, a DRM system that does not license is a warning. |
| `generate_dv` | boolean | `false` | With `HYBRID` in `range`, make Dolby Vision from the HDR10+ metadata when a title has no DV track. Needs `hdr10plus_tool` on the server. Without `HYBRID` in `range`, the request returns `400`. |
| `repack` | boolean | `false` | Add REPACK tag. |
| `tag` | string | `null` | Release group tag. |
| `tmdb_id` / `imdb_id` / `tvdb_id` / `anilist_id` | - | `null` | External ID overrides. Each resolves its metadata directly instead of by a title search, and is used for tagging. Set `enrich` to also take the title, year and original language. Give at most one of `tmdb_id`, `imdb_id` and `tvdb_id`, since unshackle resolves the others from it. Sending two returns `400`. `anilist_id` still combines with one of them. `tmdb_id` and `tvdb_id` must be positive integers, `imdb_id` must look like `tt1375666`, and `anilist_id` must be a positive integer or a string like `mal:12345`, or the request returns `400`. An ID whose provider is unconfigured, such as `tmdb_id` with no `tmdb_api_key`, fails the job rather than returning `400`. `anilist_id` needs no API key. |
+3 -3
View File
@@ -130,8 +130,8 @@ hood the client walks a remote session through its lifecycle.
The client also downloads playlists and segments through the same proxy,
as a local run does. `--no-proxy-download` and `--proxy-download` change
that for downloads only.
- Track-selection hints (`range_`, `vcodec`, `quality`, `best_available`) so
the server fetches the right manifests
- Track-selection hints (`range_`, `vcodec`, `quality`, `best_available`,
`generate_dv`) so the server fetches the right manifests
- Your language and audio codec selection (`-l`, `-vl`, `-al`, `-a`, `-fs`)
in a `dl_params` object. Some services fetch one manifest
for each audio language, and pick the languages from these values
@@ -428,7 +428,7 @@ The server mounts all these routes, even in `--remote-only` mode. Paths use the
answers CDM calls), `cdm_system_id` and `cdm_device_type` (Widevine only), `service_config`
(the identity config keys the service lists in `client_config`), `client` (a freeform identity
object the dashboard shows as sent), and the track-selection hints `range_`, `vcodec`,
`quality`, `best_available`, plus arbitrary service CLI options
`quality`, `best_available`, `generate_dv`, plus arbitrary service CLI options
(`additionalProperties: true`).
`dl_params` holds the `dl` track selection: `lang`, `v_lang` and `a_lang`
+4 -2
View File
@@ -133,8 +133,10 @@ See [Proxies and VPN](../guide/proxies-and-vpn.md).
!!! note "`TrackRequest`"
`self.track_request` is a small dataclass built from the CLI: `codecs`
(from `--vcodec`, empty means "accept any"), `ranges` (from `--range`, default
`[Video.Range.SDR]`), and `best_available`. Use it in `get_tracks` to decide which
variants to fetch.
`[Video.Range.SDR]`), `best_available`, and `generate_dv` (from `--generate-dv`).
Use it in `get_tracks` to decide which variants to fetch. A service that fetches
the HYBRID variants itself must also fetch HDR10P when `generate_dv` is set and
the title has no DV track, because the conversion needs an HEVC HDR10+ base.
## Titles and the `Title` hierarchy
+1
View File
@@ -80,6 +80,7 @@ unshackle dl [OPTIONS] SERVICE [SERVICE ARGS...]
| `-vb-range`, `--vbitrate-range` | - | Video bitrate range in kbps, e.g. `6000-7000`; picks highest within. Mutually exclusive with `--vbitrate`. |
| `-ab-range`, `--abitrate-range` | - | Audio bitrate range in kbps, e.g. `128-256`. Mutually exclusive with `--abitrate`. |
| `-r`, `--range` | `SDR` | Colour range(s): `SDR`, `HLG`, `HDR10`, `HDR10P` (HDR10+), `DV`, `HYBRID`. |
| `--generate-dv` | off | With `-r HYBRID`, make Dolby Vision from the HDR10+ metadata when a title has no Dolby Vision track. Needs `hdr10plus_tool`. See [Dolby Vision from HDR10+](downloading.md#dolby-vision-from-hdr10). |
| `-c`, `--channels` | - | Audio channels; matches sub-layouts (5.1 ≈ 6.0). |
| `-naa`, `--noatmos` | off | Exclude Dolby Atmos audio tracks. |
| `--worst` | off | Pick the lowest bitrate within the requested quality. **Requires `-q`.** |
+44 -4
View File
@@ -149,14 +149,54 @@ unshackle dl -q 2160 -r HDR10,DV EXAMPLE '...'
`-r HYBRID` makes a single hybrid track by injecting the Dolby Vision RPU
metadata onto an HDR10/HDR10+ base layer with **dovi_tool**.
It requires the `dovi_tool` binary, resolved from unshackle's `binaries/` folder,
the `binaries` folder of your user data directory, or your `PATH`. The normal case is a DV track plus an HDR10 or HDR10+ base. With HDR10+
and no DV, unshackle converts the HDR10+ metadata to DV instead, which also needs
`hdr10plus_tool`. With no DV and no HDR10+, the title fails.
When HDR10+ is present, unshackle prefers it over HDR10 as the base layer.
the `binaries` folder of your user data directory, or your `PATH`. A hybrid needs a DV track plus an HDR10 or
HDR10+ base. With no DV track, the title fails, unless you add `--generate-dv` (see below).
When HDR10+ is present, unshackle prefers it over HDR10 as the base layer. The base must be
HEVC; a title whose HDR10/HDR10+ tracks are all in another codec fails before it downloads.
If the hybrid step fails for one resolution, unshackle logs an error, skips that resolution
and goes on with the other resolutions of the title. When no hybrid file at all was made,
the title fails.
To keep an HDR10+ deliverable *alongside* the hybrid, request both ranges:
`-r HYBRID,HDR10P`. `-r HYBRID` on its own muxes only the merged hybrid.
#### Dolby Vision from HDR10+
When a title has HDR10+ but no DV track, `-r HYBRID --generate-dv` makes a Dolby Vision
Profile 8.1 track from the HDR10+ metadata. It needs `hdr10plus_tool` as well as `dovi_tool`.
When a title has a DV track, unshackle uses that track and ignores the flag. When it has none,
the flag also makes unshackle ask the service for its HDR10+ tracks.
```shell title="Generate Dolby Vision from HDR10+, and keep the HDR10+ file too"
unshackle dl -q 2160 -r HYBRID,HDR10P --generate-dv EXAMPLE '...'
```
Each HDR10+ scene becomes one Dolby Vision shot, with its own scene cut and its own Level 1
brightness values (peak and average). unshackle calculates the average itself, as a PQ mean of
the HDR10+ brightness distribution without the letterbox bars. A scene with no black pixels keeps
its whole frame, so a title that changes aspect ratio is also correct. The `dovi_tool generate
--hdr10plus-json` average makes most scenes too bright.
The [`generate_dv_mode`](../reference/configuration/download.md#generate_dv_mode) setting in
`unshackle.yaml` changes how the average is set: `v29` (default), `v40` for the 10-nit floor of studio
Dolby Vision, or `legacy` for the `dovi_tool` average.
!!! warning "Generated Dolby Vision is not studio Dolby Vision"
- There are no colourist trims (Level 2 and Level 8). The TV does all the tone mapping
from the Level 1 values.
- Dolby Vision does not let the average go below about 2.4 nits (10 nits with
`generate_dv_mode: v40`), so a very dark scene reads a little brighter than its pixels.
- The HDR10+ tone-mapping curve (Profile B) has no Dolby Vision equivalent. The HDR10+
metadata stays in the file, so an HDR10+ TV still reads it.
- The result can be no better than the HDR10+ metadata. Profile A metadata is often less
reliable than Profile B, and unshackle shows a warning for Profile A.
- A TV or player that supports both formats almost always picks Dolby Vision. On such a
TV, the generated Dolby Vision replaces the original HDR10+. To keep the original, also
request `HDR10P`, as in the example above.
An HDR10 track that a service labels HDR10 but that carries HDR10+ metadata does not
convert. Only a track with the `HDR10P` range does.
## Bitrate selection
By default unshackle keeps the highest-bitrate rendition for each selected
+22
View File
@@ -52,6 +52,7 @@ Common config keys. This is a useful subset, and every `dl` flag works:
| `speed_limit` | str | *(unlimited)* | Total download speed cap across all downloads combined, e.g. `500k`, `5M`, `1.5G` or plain bytes/sec. Values are bytes, not bits (`5M` = 5.0 MB/s). |
| `slow` | str | *(unset)* | Inter-title delay, e.g. `"20-40"`. |
| `best_available` | bool | `false` | Warn instead of failing when a requested resolution, range, or language is missing, or when one DRM system does not license under `--all-drm`. The CLI flag is `--best-available`, also spelled `--warn-only`. |
| `generate_dv` | bool | `false` | With `-r HYBRID`, make Dolby Vision from the HDR10+ metadata when a title has no DV track. Needs `hdr10plus_tool`. See [Dolby Vision from HDR10+](../../guide/downloading.md#dolby-vision-from-hdr10). |
| `proxy` | str | *(unset)* | Default proxy URI or 2-letter country. |
| `no_folder` | bool | `false` | Do not create a per-title folder. |
@@ -368,3 +369,24 @@ dual_multi_mode: strict
!!! note "Dialects count as one language"
Regional variants of the same base language (e.g. `en-US` and `en-GB`) collapse to a single
language in both modes, so they never trigger `{dual}` on their own.
## `generate_dv_mode`
- **Type:** `str`  ·  **Default:** `v29`
How [`--generate-dv`](../../guide/downloading.md#dolby-vision-from-hdr10) sets the Level 1 brightness
values of the Dolby Vision it makes from HDR10+. All modes make one shot per HDR10+ scene and keep the
same peak and letterbox area. They differ in the scene average, which the TV uses to tone-map.
| Mode | Scene average |
|------|---------------|
| `v29` (default) | unshackle's average from the HDR10+ brightness distribution, at least about 2.4 nits. Closest to the pixels. |
| `v40` | The same average, at least about 10 nits, as in studio Dolby Vision (CM v4.0). Dark scenes read brighter than `v29`. |
| `legacy` | The `dovi_tool generate --hdr10plus-json` average. Most scenes read brighter than the pixels. |
```yaml
generate_dv_mode: v29
```
The setting applies on the machine that runs the hybrid step: the client for a remote download, the
server for a REST API job.
+1 -1
View File
@@ -31,7 +31,7 @@ Where the file lives, how unshackle discovers it, and how to edit it:
|------|------|
| [Directories](directories.md) | `directories`, `filenames`, `services_repo_force` |
| [Services & authentication](services.md) | `services`, `credentials`, `firefox_cookies`, `remote_services`, `serve` |
| [Download & processing](download.md) | `dl`, `continue_downloads`, `merge_segments`, `subtitle`, `audio`, `muxing`, `language_tags`, `dual_multi_mode` |
| [Download & processing](download.md) | `dl`, `continue_downloads`, `merge_segments`, `subtitle`, `audio`, `muxing`, `language_tags`, `dual_multi_mode`, `generate_dv_mode` |
| [Output & naming](output.md) | `output_template`, `tag`, `tag_rules`, `tag_group_name`, `tag_imdb_tmdb`, `chapter_fallback_name`, `unicode_filenames` |
| [DRM & CDM](drm.md) | `cdm`, `remote_cdm`, `decryption` |
| [Network & proxy](network.md) | `network`, `headers`, `proxy_providers` |