Feature/failover (#924)

* feature stream failover
This commit is contained in:
euzu
2026-10-16 21:10:07 +02:00
committed by GitHub
parent 86d19f955c
commit 0e4e01409c
203 changed files with 15664 additions and 820 deletions
+2
View File
@@ -23,6 +23,8 @@
- [Metadata Updater & FFprobe](./configuration/metadata-update.md)
- [Local Media Library](./configuration/local-library.md)
- [source.yml (Inputs, Panel API & Targets)](./configuration/source.md)
- [Channel Groups & Stream Failover](./configuration/channel-groups.md)
- [Channel Groups: Settings Reference](./configuration/channel-groups-settings.md)
- [api-proxy.yml (Server, Users & RBAC)](./configuration/api-proxy.md)
- [template.yml (Macros & Regex)](./configuration/template.md)
- [mapping.yml (Mapper DSL & Logic)](./configuration/mapping-dsl.md)
@@ -0,0 +1,486 @@
# Channel Groups: Settings Reference
This page explains every channel group setting: what it does, which values it takes, its default, when to change it,
and an example. For how channel groups work as a whole, see [Channel Groups & Stream Failover](./channel-groups.md).
## Where the settings live
| Where | What |
|:--------------------------------------------------------------|:--------------------------------------------------------------------|
| `config.yml` → `channel_groups` | All settings below. They apply to every target with channel groups. |
| Web UI: **Settings → Channel groups** | Edits the same block. |
| `source.yml` → `targets[].options.channel_groups` | `enabled` and a few [overrides](#target-overrides) for one target. |
| Web UI: **Source editor → Targets → target → Channel groups** | Edits the same target block. |
Nothing happens until a target sets `enabled: true`. The global block is optional; without it every setting has
its default.
A complete global block with all defaults:
```yaml
channel_groups:
match_by: caption
match_as_ascii: false
match_normalize:
strip_regex: null
ignore_tokens: []
aliases: {}
epg_conflict: split
name_strip_quality: false
policy: auto
allow_provider_grace: true
start:
timeout_secs: 15
max_attempts: 4
failover:
enabled: true
no_data_timeout_secs: 8
slow_source_pct: 90
slow_source_secs: 15
max_outage_secs: 20
same_source_retries: 1
stable_after_secs: 30
max_switches: 6
max_switches_window_secs: 600
audio_language: prefer
circuit:
source_failures: 2
cooldown_secs: 60
cooldown_max_secs: 900
input_failures: 3
input_window_secs: 120
```
---
## Grouping
Grouping decides which channels become one channel. It runs during the playlist update, after the target's filters,
renames and mappings, so it sees the channel names as the target outputs them. Changes take effect with the next
playlist update.
### How two channels are found equal
For every live channel Tuliprox builds a **match key**. Channels with the same key form a group (a key with a single
channel leaves that channel alone). The key is built in this order:
1. Take the field chosen by [`match_by`](#match_by).
2. Remove everything [`strip_regex`](#match_normalizestrip_regex) matches.
3. With [`match_as_ascii`](#match_as_ascii), turn accented and special letters into plain letters.
4. Split into words at every character that is not a letter or digit (spaces, `:`, `|`, `-`, brackets, ...).
5. Drop quality words (`SD`, `HD`, `FHD`, `UHD`, `QHD`, `4K`, `480p`, `576p`, `720p`, `1080p`, `1440p`, `2160p`) and
the words of [`ignore_tokens`](#match_normalizeignore_tokens).
6. Join the rest in lower case with single spaces.
7. Replace the key if [`aliases`](#match_normalizealiases) maps it to another key.
Example with `strip_regex: "^[A-Z]{2}:\\s*"`, `ignore_tokens: [backup]` and `aliases: {"das erste": "ard"}`:
| Channel name | After step 2 | Words kept (step 5) | Key after step 6 | Final key |
|:--------------------|:----------------|:--------------------|:-----------------|:----------|
| `DE: Das Erste HD` | `Das Erste HD` | `Das`, `Erste` | `das erste` | `ard` |
| `ARD [FHD]` | `ARD [FHD]` | `ARD` | `ard` | `ard` |
| `DE: ARD HD backup` | `ARD HD backup` | `ARD` | `ard` | `ard` |
| `ZDF-HD` | `ZDF-HD` | `ZDF` | `zdf` | `zdf` |
The first three channels become one group `ard`. `ZDF-HD` stays a normal channel unless another input has a ZDF
channel too.
### `match_by`
Which channel field the match key is built from.
| Value | Uses |
|:-----------------|:-----------------------------------------------------------------------------------------------------------------------|
| `caption` | The title, or the name when the title is empty. Right for most setups. |
| `name` | The `name` field only. |
| `title` | The `title` field only. |
| `epg_channel_id` | The EPG channel id, compared without case. Steps 2 to 7 above do not apply. Channels without EPG id are never grouped. |
**Default:** `caption`. **Can be overridden per target.**
Use `epg_channel_id` when your providers name channels differently but carry clean, identical EPG ids – for example
`ZDF HD` at one provider and `ZDF Deutschland` at the other, both with `ZDF.de`.
```yaml
match_by: epg_channel_id
```
### `match_as_ascii`
Compares names after turning accented and special letters into plain letters: `Télé 5` and `Tele 5` match, as do
`Ö3` and `O3`.
**Default:** `false`. **Can be overridden per target.**
Turn it on when one provider writes names with accents and another without.
### `match_normalize.strip_regex`
A [regular expression](https://docs.rs/regex/latest/regex/#syntax); every match is removed from the name before the
key is built. Use it for prefixes and suffixes that differ between providers.
**Default:** none.
| Goal | `strip_regex` | Name | Becomes |
|:------------------------------------|:--------------------------|:-------------------|:----------|
| Drop a country prefix like `DE:` | `"^[A-Z]{2}:\\s*"` | `DE: ZDF HD` | `ZDF HD` |
| Drop a prefix like `DE -` or `DE /` | `"^[A-Z]{2}\\s*[-/]\\s*"` | `DE - ZDF HD` | `ZDF HD` |
| Drop anything in round brackets | `"\\(.*?\\)"` | `ZDF (Germany) HD` | `ZDF HD` |
For a prefix with a pipe (`DE | ZDF`) use `"^[A-Z]{2}\\s*\\|\\s*"`. Without stripping, the country code stays a word
of the key: `DE: ZDF` becomes `de zdf` and does not match `ZDF` of a provider without prefix.
In YAML, write the expression in double quotes and double every backslash (`\\s`). In the Web UI, enter it once
(`^[A-Z]{2}:\s*`).
### `match_normalize.ignore_tokens`
Words that are dropped when names are compared, in addition to the quality words. Case does not matter. A token is a
whole word: `backup` drops `ZDF backup` → `zdf`, but not `ZDFbackup`.
**Default:** empty.
```yaml
match_normalize:
ignore_tokens: [backup, hevc, h265, raw, "+1"]
```
Careful with words that tell channels apart: ignoring `plus` would join `Sky Sport` and `Sky Sport Plus`.
### `match_normalize.aliases`
Maps one match key to another, for channels that have different names at different providers. Both sides are
normalized like a channel name first, so write them as you would read them: `"Das Erste"` and `"das erste"` are the
same.
**Default:** empty.
```yaml
match_normalize:
aliases:
"das erste": "ard"
"zdf deutschland": "zdf"
"rtl television": "rtl"
```
In the Web UI, enter the name on the left and the name it should be compared as on the right.
### `epg_conflict`
What happens when channels with the same key carry **different** EPG ids – often a sign that they are different
channels with the same name, for example two regional `WDR` channels.
| Value | Effect |
|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------|
| `split` | One group per EPG id. Channels without EPG id in such a key stay normal channels. A split part with a single channel stays a normal channel. |
| `ignore` | All channels with the key form one group, whatever their EPG ids. |
**Default:** `split`. **Can be overridden per target.**
Example: `WDR HD` (EPG `wdr.koeln`), `WDR FHD` (EPG `wdr.koeln`) and `WDR` (EPG `wdr.dortmund`). With `split`,
the two Köln channels form a group and the Dortmund channel stays alone. With `ignore`, all three are one group.
Use `ignore` only when your providers use different EPG ids for the same channel and you are sure the names are
unique.
### `name_strip_quality`
Removes quality words from the name of the joined channel, so the group is shown as `ZDF` instead of `ZDF HD`. Only
whole, space-separated words are removed: `ZDF [FHD]` becomes `ZDF`, `ZDF-HD` stays `ZDF-HD`.
**Default:** `false`. **Can be overridden per target.**
The group takes name, logo and EPG id of its first channel; this setting changes only the name.
### `policy`
The order in which the channels of a group ("members") are tried.
| Value | Order |
|:-----------|:--------------------------------------------------------------------------------------------------------------|
| `auto` | By score: 60 % reliability history, 30 % picture quality, 10 % position in the playlist. Paused members last. |
| `priority` | By position in the playlist (or the order in the overrides file). Paused members last. |
**Default:** `auto`. **Can be overridden per target.**
- **Reliability history** comes from the stream history: how often a member started and played without problems in
the last 24 hours and 7 days. It needs `reverse_proxy.stream_history` and `reverse_proxy.qos_aggregation` in
`config.yml`; without them every member counts as average.
- **Picture quality** comes from the metadata prober (resolution and bitrate); without a probe the quality word in
the name is used (`FHD` beats `HD`).
- **Position:** the channel that appears first in the target playlist ranks highest.
Use `priority` when you want a fixed order, for example "always the paid provider first, the second provider only
as backup". Sort your playlist or the overrides file accordingly.
---
## Start
These settings control what happens when a client starts a group. They apply to the next stream start.
### `allow_provider_grace`
Providers can have a grace connection: one connection above `max_connections` that is allowed for a short time while
an old stream is closing. With this setting a start may use it when no regular connection is free.
**Default:** `true`. **Can be overridden per target.**
Switches while watching never use the grace connection. Turn it off for targets with many users on few provider
connections, so a group start does not hold the slot other users need while they zap.
### `start.timeout_secs`
The longest time a start may try members – selecting, opening and checking that the provider sends a video stream.
When it runs out, the client gets the "channel unavailable" video.
**Default:** `15`. `0` means no limit.
Example: member 1 does not answer (given up after 4 seconds), member 2 answers with HTTP 403, member 3 plays after
2 seconds. The start takes about 6 seconds and stays well inside 15.
Lower it (for example to `8`) when your players give up early; raise it when your providers are slow to answer.
### `start.max_attempts`
The most members a start tries.
**Default:** `4`. Must be greater than 0.
With 6 members and `max_attempts: 4`, at most the 4 best-ranked members are tried before the start gives up.
---
## Failover while watching
When the playing member fails, Tuliprox opens the next member and continues the **same** response, so the player
does not notice more than a short freeze. This works for MPEG-TS streams (direct and shared); HLS streams change the
member only when a new stream starts. Changes apply to the next stream start.
### `failover.enabled`
Turns the switch while watching on or off. With `false` a group still starts on the best member, but the stream ends
when its member fails, as for a normal channel.
**Default:** `true`. **Can be overridden per target** (as `failover: false`).
Turn it off for a target whose players show artefacts or stop when the stream changes – some built-in TV players
do. VLC, Kodi, ffmpeg-based players and most IPTV apps handle the switch.
### `failover.no_data_timeout_secs`
A member that sends no picture and no sound for this long is replaced.
**Default:** `8`. `0` turns the check off.
Keep it above the keyframe interval of your channels: after a switch, Tuliprox waits up to 4 seconds for a keyframe
of the new member, and some sports channels send a keyframe only every 5 to 10 seconds. Too short a value replaces
members that are merely slow to start. Raise it to `12`–`15` for providers that pause for a few seconds now and then
without really failing.
### `failover.slow_source_pct` and `failover.slow_source_secs`
A member must deliver video at least as fast as it is played. A member that is slower than this percentage of
real-time speed for `slow_source_secs` seconds is replaced.
**Defaults:** `slow_source_pct: 90`, `slow_source_secs: 15`. `slow_source_pct: 0` turns the check off; at most `100`.
What the numbers mean: with `90`, the member must deliver at least 9 seconds of video every 10 seconds. A member
that delivers only 7 seconds every 10 seconds drains the player's buffer – after a while the picture stutters or
stops to buffer. If that lasts for 15 seconds, Tuliprox switches before the viewer sees it.
The speed is measured with the clock inside the stream (PCR), not by counting bytes, so channels with a low bitrate
are not mistaken for slow ones.
| Situation | Suggestion |
|:-----------------------------------------------------|:---------------------------------------------------------------------|
| Switches happen although the picture is fine | Lower `slow_source_pct` to `80` or raise `slow_source_secs` to `30`. |
| The picture stutters for a long time before a switch | Lower `slow_source_secs` to `8`. |
| You do not want this check | `slow_source_pct: 0`. |
### `failover.max_outage_secs`
The longest time without picture while Tuliprox looks for a replacement. When no member plays within this time, the
stream ends (with the "channel unavailable" video, if configured).
**Default:** `20`. Must be greater than 0.
During the search, the player receives empty packets every half second so it keeps the connection. A member that
played for 5 seconds (or half of `max_outage_secs`, if that is shorter) counts as working; a later failure starts a
new outage with the full time.
### `failover.same_source_retries`
A member that played well (for at least [`stable_after_secs`](#failoverstable_after_secs)) and then fails is first
tried again this many times before another member is used. Short hiccups of a good provider are common; reopening it
is often the best choice.
**Default:** `1`. `0` switches to another member at once.
### `failover.stable_after_secs`
After this much smooth playback a member counts as stable: the current incident is over, earlier failed members may
be tried again, and a new failure first retries the same member (see above).
**Default:** `30`.
### `failover.max_switches` and `failover.max_switches_window_secs`
The switch limit. When more than `max_switches` switches happen within `max_switches_window_secs`, the stream ends
instead of jumping between members that all fail.
**Defaults:** `max_switches: 6`, `max_switches_window_secs: 600` (10 minutes). `max_switches: 0` turns the limit off.
Keep the limit on: a provider that answers every request with a short error video would otherwise keep a stream
switching forever.
### `failover.audio_language`
Whether a switch must keep the audio language of the member that played (as found by the metadata prober).
| Value | Effect |
|:----------|:-------------------------------------------------------------------------------------|
| `prefer` | Members with the same language rank higher; others are used when nothing else works. |
| `require` | Only members with the same language (or without a known language) are used. |
**Default:** `prefer`.
Use `require` when a group mixes language versions, for example an English and a German feed of the same sports
channel.
---
## Pausing failing members (`circuit`)
A member that keeps failing is paused, so starts and switches try the others first. Pauses belong to the member
(input and stream), so they are shared by all targets. Answers like "too many connections" (HTTP 429/509) never
count as a failure – the member is fine, the account is just full.
### `circuit.source_failures`
How many failures in a row pause a member. A successful start resets the count.
**Default:** `2`. Must be greater than 0.
### `circuit.cooldown_secs` and `circuit.cooldown_max_secs`
How long a member stays paused. After the pause it is tried once; if that fails, the next pause is twice as long, up
to `cooldown_max_secs`. A success ends the pausing.
**Defaults:** `cooldown_secs: 60`, `cooldown_max_secs: 900`. A `cooldown_max_secs` below `cooldown_secs` is raised to
it.
Example with the defaults: pauses of 1, 2, 4, 8 minutes, then 15 minutes for every further failure.
A paused member is not removed: when every member of a group is paused, the best of them is still tried.
### `circuit.input_failures` and `circuit.input_window_secs`
When **several different** channels of one input fail within a short time, the provider itself is probably down. Then
the whole input is paused and its members are tried last.
**Defaults:** `input_failures: 3`, `input_window_secs: 120`. `input_failures` must be at least 2; the failures must
come from at least two different members.
Example: within 2 minutes, `ZDF` of provider A fails twice and `ARD` of provider A once – 3 failures from 2 members.
All channels of provider A now rank behind provider B until the pause ends.
---
## Target overrides
A target turns channel groups on with `enabled` and can override these settings. Every override is optional; a
missing one uses the global value. In the Web UI each override offers "Global (*value*)".
| Key | Overrides |
|:-----------------------|:------------------------------------------------|
| `enabled` | – (target only, default `false`) |
| `match_by` | [`match_by`](#match_by) |
| `match_as_ascii` | [`match_as_ascii`](#match_as_ascii) |
| `epg_conflict` | [`epg_conflict`](#epg_conflict) |
| `name_strip_quality` | [`name_strip_quality`](#name_strip_quality) |
| `policy` | [`policy`](#policy) |
| `allow_provider_grace` | [`allow_provider_grace`](#allow_provider_grace) |
| `failover` | [`failover.enabled`](#failoverenabled) |
`match_normalize`, `start`, the failover timing and `circuit` exist only globally: the pause state belongs to the
member and is shared by all targets, and the timing describes your providers, not a target.
---
## Recipes
### Two providers with the same channels
```yaml
# config.yml – nothing needed, the defaults fit
# source.yml
targets:
- name: tv
options:
channel_groups:
enabled: true
```
### Providers with country prefixes and different spellings
```yaml
# config.yml
channel_groups:
match_as_ascii: true
name_strip_quality: true
match_normalize:
strip_regex: "^[A-Z]{2}\\s*[:|-]\\s*"
ignore_tokens: [backup, hevc]
aliases:
"das erste": "ard"
```
### A paid provider first, a cheap one only as backup
```yaml
# config.yml
channel_groups:
policy: priority
```
Put the paid provider's input first in the source, so its channels come first in the playlist.
### One target for a TV box that cannot handle a switch
```yaml
# source.yml
targets:
- name: living-room
options:
channel_groups:
enabled: true
failover: false
```
### Many users on few provider connections
```yaml
# source.yml
targets:
- name: family
options:
channel_groups:
enabled: true
allow_provider_grace: false
share_live_streams:
mpeg_ts: true # viewers of the same group share one provider connection
```
---
## Troubleshooting
| Problem | Check |
|:---------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Channels that should be one are not grouped | Compare their names after the target's renames; add `strip_regex`, `ignore_tokens` or `aliases`; check that the EPG ids are equal or set `epg_conflict: ignore`; run a playlist update. |
| Different channels were joined | Keep `epg_conflict: split`; remove too broad `ignore_tokens`; use the mapping field `channel_group: ""` to keep a channel out. |
| A switch shows artefacts for a moment | Normal when the members differ in codec or resolution; members with the same codec and resolution are preferred. Turn `failover` off for players that cannot handle it. |
| Streams switch although the picture was fine | Raise `no_data_timeout_secs`; lower `slow_source_pct` or raise `slow_source_secs`. |
| Streams end after many switches | All members fail; check the providers. `max_switches` ended the stream as intended. |
| A good provider is always tried last | It is paused after failures; wait for the pause to end or check the events `channel_group.*`. |
+226
View File
@@ -0,0 +1,226 @@
# Channel Groups and Stream Failover
Many setups carry the same live channel from several inputs: two providers, two accounts of one provider, or an
HD and an FHD variant. **Channel groups** collapse these equal channels into **one channel** in the target. When a
client plays it, Tuliprox picks the best reachable source ("member") and, if that source fails while the client is
watching, switches to another member **underneath the open response** – the player keeps its connection and does
not have to reconnect.
Channel groups are **turned on per target** and are **off by default**. How channels are matched and how sources are
selected and switched is set **once in `config.yml`** under `channel_groups`; a target can override a few of these
settings where it needs to.
## Quick start
```yaml
# source.yml
sources:
- inputs: [provider_a, provider_b]
targets:
- name: tv
output:
- type: xtream
options:
channel_groups:
enabled: true
```
With this, live channels of `provider_a` and `provider_b` whose caption matches (quality words such as `HD`, `FHD`,
`4K` are ignored) appear once in the `tv` target. All other settings use the defaults of `config.yml`.
In the Web UI:
- **Settings → Channel groups** edits the global block of `config.yml`.
- **Source editor → Targets → *target* → Channel groups** turns the feature on for a target and holds its overrides.
Every override offers "Global (*value*)" to keep the global setting.
## How channels are grouped
Grouping runs at the end of the target processing, after filters, renames and mappings.
1. Only live channels take part (`XtreamCluster::Live`, no local library items).
2. Each channel gets a **match key** from the field chosen by `match_by` (`caption`, `name`, `title` or
`epg_channel_id`), normalized with `match_as_ascii` and `match_normalize`.
3. Channels with the same key form a group; a key with a single channel leaves that channel as it is.
4. With `epg_conflict: split`, a group whose members carry **different EPG ids** is split into one group per EPG id,
so channels with different programmes are never mixed. `ignore` keeps such groups together.
5. The group appears as one playlist entry. Its name, logo and EPG id come from its first member; with
`name_strip_quality: true` the quality words are removed from the name. The members disappear from the
playlist but keep their stable virtual ids, so old favourites and direct member requests still play (a request
for a member id plays the group).
The mapping DSL can steer grouping per channel with the header field `channel_group`: a value forces the channel
into the group with that key, an empty string (`""`) keeps the channel out of any group.
### Manual groups and corrections
Per target, an optional overrides file under `<config_path>/channel_groups/<target>--<hash>.yml` holds manual groups
(`groups`), corrections of automatic groups (`auto_overrides`: order, added/removed members, name, logo, EPG id,
policy) and excluded channels (`excluded`). Members are referenced by input name and provider stream id. A manual
group with fewer than two resolvable members is dropped; members that vanished from the provider are
skipped with a warning. There is no Web UI editor for this file yet.
## How a source is selected
When a client starts a group, Tuliprox ranks the members and opens the best one that has provider capacity.
- **Ranking (`policy: auto`).** Members are scored from their QoS history (24 h / 7 d success, needs
`stream_history` and `qos_aggregation` in `config.yml`), their probed quality (resolution, codec, bitrate from the
metadata prober) and their position in the group. Members with an open circuit or a long failure streak go last.
`priority` keeps the configured member order and only moves failing members back.
- **Capacity.** A member with a free provider connection wins over a better member that would need the grace slot
(`allow_provider_grace`) or the preemption of a lower-priority stream. Inputs in maintenance are skipped.
- **Start attempts.** If a member cannot be opened (HTTP error, provider refuses the account, no MPEG-TS/playlist in
the first bytes, no answer), the next member is tried, up to `start.max_attempts` and bounded by
`start.timeout_secs`. A provider that does not answer at all is given up after 4 seconds per member.
- **Pausing failing sources.** `circuit.source_failures` consecutive failures of one member pause it for
`circuit.cooldown_secs`, doubling up to `circuit.cooldown_max_secs` while it keeps failing. Failures of at least two
different members of one input within `circuit.input_window_secs` (`circuit.input_failures` in total) put the whole
input last. This state is shared by all targets, because it belongs to the source, not to the target.
"Too many connections" answers (`429`/`509`) never open a circuit.
HLS entries use the same start selection; with the shared HLS cache enabled, viewers of the same member share one
origin session.
## Failover while watching (MPEG-TS)
With `failover.enabled` (default on), MPEG-TS playback – direct or [shared](./source.md#326-options) – replaces a
running member when
- its provider body ends or fails (`upstream_eof`, `upstream_error`),
- it sends no media for `failover.no_data_timeout_secs` (`stall`),
- it is too slow (`underrun`): it delivers less than `failover.slow_source_pct` percent of real-time speed for
`failover.slow_source_secs`. With 90 % a source must deliver at least 9 seconds of video every 10 seconds;
a slower source drains the player's buffer and the picture stutters. The speed is measured from the stream's PCR.
With `failover.enabled: false` a group still starts on the best source, but the stream ends with its member.
The switch keeps the client response open. While the next member opens, null packets are sent every 500 ms so players
and proxies do not time out. The new member is spliced in at a clean point: the output continues with its PAT and
PMT (version numbers continue the previous member's, CRC recomputed) followed by a keyframe, and the first packet of
each stream carries the MPEG-TS `discontinuity_indicator`. Players such as VLC, ffmpeg-based players and Kodi handle
this well; some built-in TV players may stutter when codec or resolution change between members, so the ranking
prefers members with the same codec, resolution and audio as the one that played.
Rules of a switch:
- A member that played for at least `failover.stable_after_secs` is retried `failover.same_source_retries` times before
others.
- Runtime switches never use the provider grace slot and never preempt other streams.
- A member whose slot was preempted by a higher-priority stream is not failed over; the stream ends as before.
- Members that refuse the account or have no capacity, or that deliver no MPEG-TS, are not asked again within the
same switch. Others are retried in rounds with a growing pause (0.5 s up to 4 s).
- **Outage limit.** The time without playback while looking for a replacement is bounded by `failover.max_outage_secs`.
A member counts as playing once it has sent media for 5 seconds (or half of `max_outage_secs`, if that is shorter);
then a later failure starts a fresh outage limit. A provider that answers every request with an error clip longer
than that therefore keeps the stream alive between switches – `failover.max_switches` is the outer bound for that
case.
- **Switch limit.** More than `failover.max_switches` switches within `failover.max_switches_window_secs` end the stream.
- When the outage limit or the switch limit ends the stream, the `channel_unavailable` custom video is shown if
configured.
Each switch emits the event `channel_group.member_switched` (with the reason); start selections emit
`channel_group.member_selected`, exhausted groups `channel_group.exhausted`.
HLS playback switches members only when a new entry is started; HLS runtime failover is not implemented.
## Global settings (`config.yml` → `channel_groups`)
All keys are optional; the block itself can be left out. Every setting is explained with examples in the
[Settings Reference](./channel-groups-settings.md).
| Parameter | Type | Default | Description |
|:-----------------------|:-----|:----------|:-----------------------------------------------------------------------------------------------------------------|
| `match_by` | Enum | `caption` | Field that decides which channels are equal: `caption`, `name`, `title`, `epg_channel_id`. |
| `match_as_ascii` | Bool | `false` | Compares match keys after transliteration to ASCII ("Télé" matches "Tele"). |
| `match_normalize` | Map | empty | Extra normalization: `strip_regex` (removed from the key), `ignore_tokens` (words ignored), `aliases` (key map). |
| `epg_conflict` | Enum | `split` | `split` separates members with different EPG ids into own groups, `ignore` keeps them together. |
| `name_strip_quality` | Bool | `false` | Removes quality words (`HD`, `FHD`, `4K`, ...) from the group's name. |
| `policy` | Enum | `auto` | Member order: `auto` (ranked by QoS, quality, order) or `priority` (configured order, failing members last). |
| `allow_provider_grace` | Bool | `true` | A start may use the provider grace slot when no free connection is left. Switches while watching never do. |
| `start` | Map | see below | How a start picks a member. |
| `failover` | Map | see below | Switching members while watching. |
| `circuit` | Map | see below | Pausing failing members and inputs. |
### `start`
| Parameter | Type | Default | Description |
|:---------------|:-----|:--------|:-------------------------------------------------------------------------------------|
| `timeout_secs` | Int | `15` | Longest time a start may try members before the client gets an error. `0`: no limit. |
| `max_attempts` | Int | `4` | Most members tried at start. Must be greater than 0. |
### `failover`
| Parameter | Type | Default | Description |
|:---------------------------|:-----|:---------|:--------------------------------------------------------------------------------------------------|
| `enabled` | Bool | `true` | Switches to another member while a client watches an MPEG-TS stream. |
| `no_data_timeout_secs` | Int | `8` | Seconds without media before a member is replaced. Keep it above the keyframe interval. `0`: off. |
| `slow_source_pct` | Int | `90` | Minimum share of real-time speed a member must deliver. `0`: off; at most 100. |
| `slow_source_secs` | Int | `15` | How long a member may stay below `slow_source_pct` before it is replaced. |
| `max_outage_secs` | Int | `20` | Longest time without playback while looking for a replacement. Must be greater than 0. |
| `same_source_retries` | Int | `1` | Retries of a member that played stably before other members are tried. |
| `stable_after_secs` | Int | `30` | Playback after which a member counts as stable and an incident is over. |
| `max_switches` | Int | `6` | Most switches within `max_switches_window_secs`; then the stream ends. `0`: no limit. |
| `max_switches_window_secs` | Int | `600` | Window of `max_switches`. |
| `audio_language` | Enum | `prefer` | Switches `prefer` members with the same audio language, or `require` it. |
### `circuit`
| Parameter | Type | Default | Description |
|:--------------------|:-----|:--------|:--------------------------------------------------------------------------------------------------|
| `source_failures` | Int | `2` | Consecutive failures that pause a member. Must be greater than 0. |
| `cooldown_secs` | Int | `60` | First pause of a failing member; doubles while it keeps failing. |
| `cooldown_max_secs` | Int | `900` | Longest pause. Raised to `cooldown_secs` when lower. |
| `input_failures` | Int | `3` | Failures of one input (from at least two members) within the window that put the input last. ≥ 2. |
| `input_window_secs` | Int | `120` | Window of `input_failures`. |
Changes to grouping settings take effect with the next playlist update; selection and failover settings apply to the
next stream start.
## Target settings (`source.yml` → `targets[].options.channel_groups`)
| Parameter | Type | Default | Description |
|:-----------------------|:-----|:---------|:---------------------------------------------------------------------------------|
| `enabled` | Bool | `false` | Turns channel groups on for this target. |
| `match_by` | Enum | (global) | Overrides `match_by`, e.g. `epg_channel_id` for a target with clean EPG ids. |
| `match_as_ascii` | Bool | (global) | Overrides `match_as_ascii`. |
| `epg_conflict` | Enum | (global) | Overrides `epg_conflict`. |
| `name_strip_quality` | Bool | (global) | Overrides `name_strip_quality`. |
| `policy` | Enum | (global) | Overrides `policy`. |
| `allow_provider_grace` | Bool | (global) | Overrides `allow_provider_grace`, e.g. `false` for a target with many users. |
| `failover` | Bool | (global) | Overrides `failover.enabled`, e.g. `false` for players that stutter on a switch. |
Timing, `start`, `circuit` and `match_normalize` exist only globally: the pause state of a source is shared by all
targets, and the timing describes the providers, not the target.
## Example
```yaml
# config.yml
channel_groups:
name_strip_quality: true
match_normalize:
strip_regex: "^[A-Z]{2}:\\s*" # drop country prefixes like "DE: "
ignore_tokens: [hevc, backup]
aliases:
"das erste": "ard"
failover:
no_data_timeout_secs: 10
max_outage_secs: 30
```
```yaml
# source.yml
targets:
- name: family
options:
channel_groups:
enabled: true
share_live_streams:
mpeg_ts: true # optional: one provider connection per group for all viewers
- name: tv-box
options:
channel_groups:
enabled: true
failover: false # this player stutters on a switch; keep the start selection only
```
+16 -14
View File
@@ -40,6 +40,7 @@ hdhomerun:
library:
reverse_proxy:
metadata_update:
channel_groups:
```
### Global System & Storage Settings (Flat Keys)
@@ -87,20 +88,21 @@ instead of your mount point.
### Subsections (Object Keys)
| Block | Description | Link |
|:------------------|:----------------------------------------------------------------------------------|:------------------------------------------------|
| `api` | Internal web server binding settings. | [See section](#1-api-server-api) |
| `web_ui` | Web Dashboard, RBAC, and Authentication. | [See section](#2-web-ui--administration-web_ui) |
| `log` | Console output verbosity and sanitization. | [See section](#3-logging-log) |
| `schedules` | Automated background tasks (Cronjobs). | [See section](#4-schedules-schedules) |
| `messaging` | Webhooks & Push-Notifications (Telegram, Discord, etc.). | [See section](#5-messaging-messaging) |
| `video` | Extension mapping and Web UI download behavior. | [See section](#6-video--web-search-video) |
| `proxy` | SOCKS5/HTTP proxy settings for outgoing requests. | [See section](#7-outgoing-proxy-proxy) |
| `ipcheck` | IP detection to verify in the Web UI which public IP Tuliprox is currently using. | [See section](#8-ip-check-ipcheck) |
| `hdhomerun` | Virtual DVB-C/T network tuner emulation. | [See section](#9-hdhomerun-emulation-hdhomerun) |
| `library` | Local Media Library integration. | [See Local Library](./local-library.md) |
| `reverse_proxy` | Streaming buffers, rate limits, caching. | [See Reverse Proxy](./reverse-proxy.md) |
| `metadata_update` | TMDB matching, FFprobe processing, Job Queues. | [See Metadata Update](./metadata-update.md) |
| Block | Description | Link |
|:------------------|:----------------------------------------------------------------------------------|:---------------------------------------------------|
| `api` | Internal web server binding settings. | [See section](#1-api-server-api) |
| `web_ui` | Web Dashboard, RBAC, and Authentication. | [See section](#2-web-ui--administration-web_ui) |
| `log` | Console output verbosity and sanitization. | [See section](#3-logging-log) |
| `schedules` | Automated background tasks (Cronjobs). | [See section](#4-schedules-schedules) |
| `messaging` | Webhooks & Push-Notifications (Telegram, Discord, etc.). | [See section](#5-messaging-messaging) |
| `video` | Extension mapping and Web UI download behavior. | [See section](#6-video--web-search-video) |
| `proxy` | SOCKS5/HTTP proxy settings for outgoing requests. | [See section](#7-outgoing-proxy-proxy) |
| `ipcheck` | IP detection to verify in the Web UI which public IP Tuliprox is currently using. | [See section](#8-ip-check-ipcheck) |
| `hdhomerun` | Virtual DVB-C/T network tuner emulation. | [See section](#9-hdhomerun-emulation-hdhomerun) |
| `library` | Local Media Library integration. | [See Local Library](./local-library.md) |
| `reverse_proxy` | Streaming buffers, rate limits, caching. | [See Reverse Proxy](./reverse-proxy.md) |
| `metadata_update` | TMDB matching, FFprobe processing, Job Queues. | [See Metadata Update](./metadata-update.md) |
| `channel_groups` | Matching, source selection and failover of channel groups. | [See Channel Groups](./channel-groups-settings.md) |
*(Note: The advanced topics **Local Library**, **Reverse Proxy** and **Metadata Update** are extremely extensive and
have their own
+3
View File
@@ -1620,6 +1620,8 @@ targets:
match_by: caption
keep: best_quality
match_as_ascii: false
channel_groups:
enabled: true
```
#### Target Option Parameters
@@ -1635,6 +1637,7 @@ targets:
| `epg_output.adopt_guide_id_case` | Bool | No | `true` | Rewrites a live channel EPG id that matches a guide channel only case-insensitively (for example `usa4k.us` against `<channel id="USA4K.us">`) to the guide's exact spelling, so the M3U `tvg-id`, Xtream `epg_channel_id`, XMLTV `<channel id>` and EPG API lookups use the same id. Set `false` to keep the playlist's original spelling. Changing this option requires a full target refresh. |
| `epg_output.lowercase_ids` | Bool | No | `false` | Canonicalizes visible technical EPG IDs with ASCII lowercase across M3U `tvg-id`, Xtream `epg_channel_id`, XMLTV channel/programme references, and EPG API responses. Changing this option requires a full target refresh. |
| `epg_output.lowercase_xmltv_display_names` | Bool | No | `false` | Applies Unicode lowercase exclusively to XMLTV `<display-name>` values during serialization. Playlist names, Xtream names, programme titles, and programme descriptions remain unchanged; persisted target data does not require rebuilding. |
| `channel_groups` | Map | No | - | Turns channel groups on for this target (`enabled`) and overrides single global settings of `config.yml` `channel_groups`. See [Channel Groups & Stream Failover](./channel-groups.md). |
| `force_redirect` | Bool | No | `false` | Optional redirect-related behavior switch. This influences how Tuliprox serves final stream delivery where redirect-style output handling is required by the deployment model. |
> **Shared HLS:** `share_live_streams.hls` requires `reverse_proxy.hls_cache` in `config.yml`.
+2
View File
@@ -40,6 +40,8 @@ That makes it usable both for IPTV players and for media-server-oriented workflo
- put inputs into maintenance mode: new streams get a maintenance video, the provider is not contacted
- persist optional stream history for per-variant connect/disconnect, startup-failure, and reconnect telemetry
- aggregate optional QoS snapshots from stream history to rank equivalent channel variants by reliability
- group equal live channels from several inputs into one channel with source selection and runtime failover
([Channel Groups](./configuration/channel-groups.md))
## Metadata and library