OTT service

Outputs streams over HTTP-based protocols — HLS, MPEG-DASH (since version 1.12) and MPEG-TS over HTTP. HTTPS (SSL) is supported. The output is enabled on the OTT tab of the Stream settings.

URLs for connections have format:

host and port - set in http server settings.

stream - stream ID. Not to be confused with the sequence number in streams list. ID is shown in stream stats page header and in streams list ID column, ID is set at stream creation and never changes.

Likewise for HLS and DASH:

Output URL templates and working status are shown on stream stats page. Unauthorized access is denied, all clients should be registered in Peers.

Additional parameters are available in the URL for HLS and DASH (optional):

[URL]?a=1&s=40&m=40&v=5&h3=1

  • a: 1 — absolute path in the playlist, 0 — relative path (default).

  • s - dynamic play list duration (sec), default 40 sec.

  • m: minimum dynamic-playlist duration (sec), 40 sec by default. The maximum dynamic-playlist size is 60 sec. If the current chunk-buffer size is less than the minimum size requested, an HTTP 404 is returned. This is done so that HLS starts with a filled chunk buffer on the server.

  • v: version of the HLS protocol declared in the playlist. By default the value depends on the HLS mode (see below): OTT mode6, Peer mode3. An explicit value in the URL overrides the default. A version change may be required for compatibility with a specific HLS client.

  • h3: opt-in to HTTP/3 (QUIC) for this OTT session. Causes the server to emit an Alt-Svc header in the response, after which a compatible player / browser switches to QUIC. See section HTTP/3 (QUIC) below.

File name index.m3u8 could be added to URL for some players support, for example: http://host:port/hls/stream/login/password/index.m3u8.

There are two modes of operation of the HLS server — Peer mode and OTT mode.

Peer mode — a mode with simple segment (chunk) slicing. Recommended for peering (distribution) of streams. By default the playlist is delivered as EXT-X-VERSION:3 for compatibility with peer clients.

OTT mode — a mode with segment slicing optimised for fast player start-up under OTT broadcasting. CPU load is higher in this mode, recommended for broadcasting. By default the playlist is delivered as EXT-X-VERSION:6 with EXT-X-INDEPENDENT-SEGMENTS and a CHARACTERISTICS attribute on EXT-X-MEDIA TYPE=SUBTITLES (Apple HLS, hls.js, Safari, dash.js/Shaka). If a particular client needs an earlier value — set it via the ?v= query parameter (see the parameter list above).

SSL (HTTPS) can be enabled for HTTP server, this is done in the server settings.

Chunk Min Interval and Chunk Max Interval

In OTT mode the stream is analysed for PAT/PMT/SPS/PPS/IFrame and chunks are sliced according to the fast-start criterion for players. The analysis starts from min interval, and if for any reason the data is not found, the chunk is forcibly sliced at max interval.

GOP-aligned segments

Since version 1.13.1.438, in OTT mode every .ts TS segment is guaranteed to start with SPS / PPS / IDR (for HEVC — also VPS). The segmenter cuts out the “tail” of the previous GOP — P / B slices — from the window between the leading PAT and the chunk’s first SPS. This:

  • eliminates the initial black frame at VOD session start (?t= / ?epg=) in hls.js, Safari and VLC: the MSE decoder receives the correct header set of NAL units already in the first segment and starts immediately;

  • brings the output into compliance with HLS RFC 8216 §3 (“Each Media Segment MUST contain a SPS and a PPS that decode its first Access Unit”);

  • makes the EXT-X-INDEPENDENT-SEGMENTS declaration in an EXT-X-VERSION:6 playlist accurate.

Controlled by the per-stream gop-aligned-segment setting (default true). Applies only when HLS = OTT mode: in Peer mode and for MPTS streams the behaviour is unchanged. PSI (PAT / PMT) and audio are preserved in full; the only side effect is a one-frame continuity_counter break on the video PID at the boundary between two adjacent chunks, which is a legitimate decode boundary for HLS / DASH MSE.

HLS Adaptive Multistream

Since version 1.10 HLS Adaptive Multistream is supported, and since version 1.12 DASH Adaptive Multistream as well.

HLS playlist should be configured for each adaptive stream. To do it:

  • Enable HLS with OTT mode for each stream you going to use in adaptive streams.

  • Streams Adaptive menu item will available in main menu. There you should add adaptive stream and select all streams you need for current adaptive stream playlist.

  • Streams can have a bitrate parameter set. By default it is 0, which means the bitrate is taken from the measured value. Otherwise it can be set explicitly.

Adaptive stream URL is differ:

Peer (client) can have access list where adaptive streams are also available.Permission for an adaptive stream includes permissions for all streams that are included in it.

HTTP/3 (QUIC)

As of version 1.13.1.438, Perfect Streamer ships a built-in HTTP/3 server for OTT delivery of HLS and MPEG-DASH over QUIC (RFC 9000 + RFC 9114). The stack is ngtcp2 (QUIC v1) + nghttp3 (HTTP/3 frame layer); the TLS infrastructure reuses the same certificate as the HTTPS listener.

QUIC serves OTT routes only:

  • / — root redirect,

  • /hls/... and /dash/... — master playlists / MPD,

  • /h<sessID>/... — per-session URL (media playlists, segments, VTT),

  • /http/<stream>/... — raw MPEG-TS over HTTP.

Administrative paths (/data, /config, /xmltv, /db/, /login, /logout, /restart) return 404 over QUIC — the admin API stays on HTTPS / HTTP-TCP. This is a deliberate restriction that reduces the attack surface of the QUIC listener.

Enabling

The QUIC settings reside in the Configuration / HTTP server section (node /config/http-server):

  • HTTP/3 Enable (http3-enable) — global flag that enables the QUIC listener. Default is off. Enabling opens a UDP socket on http3-port; disabling closes it.

  • HTTP/3 Port (http3-port) — UDP port of the QUIC listener. Default is 43984. QUIC runs on UDP and HTTPS on TCP; the ports may coincide or differ — there is no conflict between them.

  • HTTP/3 0-RTT Enable (http3-zero-rtt-enable) — allows the 0-RTT handshake (RFC 9001 §4.6.1) on resumed connections from the same client. Reduces start-up latency; the replay risk must be considered for non-idempotent requests (it is safe for read-only OTT workloads). Default is on.

Configuration changes are applied hot, without restarting the service.

The certificate and key are taken from the HTTPS listener — there is no separate certificate setting for HTTP/3. If HTTPS is not configured (ssl-enable=false), enabling HTTP/3 yields nothing — the handshake will not complete.

The ?h3 parameter — per-session opt-in

A browser does not connect to an HTTP/3 endpoint directly — it first reaches HTTPS/TCP, receives the Alt-Svc: h3=":<port>"; ma=86400 header in the response (RFC 7838), and only subsequent requests to that origin are switched to QUIC.

In Perfect Streamer, Alt-Svc emission is opt-in per OTT session. The server does not advertise QUIC to every client indiscriminately: the client must explicitly request HTTP/3 via the ?h3 query parameter on the master HLS / DASH URL.

Value in URL

Opt-in value

Alt-Svc in response

parameter absent

off (default)

no

?h3 (no value)

on

yes

?h3=1, ?h3=on, ?h3=yes, ?h3=true

on

yes

?h3=0, ?h3=off, ?h3=no, ?h3=false

explicit off

no (as when absent)

Once the client has obtained the session URL /h<sess>/..., the wantH3 flag is stored in the session — all subsequent media-playlist refreshes, segment GETs and VTT-chunk GETs under that session URL also receive Alt-Svc, even if ?h3 is absent from the individual request. Without opt-in on the master, no sticky behaviour is established.

Alt-Svc gating:

  1. The QUIC listener is globally enabled (http3-enable=true); otherwise the advertisement is suppressed even when ?h3 is set.

  2. The request did not arrive over QUIC — on requests already served over H3, Alt-Svc is meaningless and is not emitted.

  3. Per-session or per-URL wantH3=true (see the table above).

  4. The admin and EPG servers never emit Alt-Svc — they have no QUIC listener.

This behaviour is consistent with RFC 7838 §3 — the server itself decides on which responses to emit Alt-Svc.

QUIC switch-over scenario in the browser

Typical flow (Chrome / Firefox / Safari):

  1. The player opens the master URL with explicit opt-in:

    https://stream.example.com:41982/hls/test1/login/password/index.m3u8?h3=1
    
  2. The first request goes over HTTPS/TCP. In the response the server emits Alt-Svc: h3=":43984"; ma=86400.

  3. The browser caches the mapping stream.example.com:41982 h3=":43984". In Chrome it can be inspected on the chrome://net-internals/#alt-svc page; in Firefox — about:networking#http3.

  4. On subsequent requests to the same origin (playlist refresh, segment GETs, VTT) the browser opens a QUIC connection on udp/43984 and continues over HTTP/3. In DevTools → Network the Protocol column for these requests becomes h3.

If the QUIC connection cannot be established (UDP port blocked by firewall, certificate not trusted by the system), the browser transparently remains on HTTPS/TCP — functionally the stream keeps playing without interruption.

Client accounting and monitoring

The real IP address of a QUIC client in the active-peer accounting (/data/http-clients, concurrent-connection limits, IP-based ACLs) is the actual peer address of the QUIC connection, not the loopback of the internal backend bridge.

The ott-type attribute in /data/http-clients is composite, with the format <PROTO>/<scheme>:

  • PROTO — OTT protocol: HLS, DASH or HTTP.

  • scheme — the actual network transport: http, https or quic.

Examples: HLS/quic, DASH/https, HTTP/http. The admin UI shows both the OTT protocol and the network transport of each client in a single column.

The OTT session timeout is 60 seconds, regardless of the transport. When a client switches between transports, the scheme is only upgraded along the priority chain httphttpsquic. A concurrent fallback to a less secure connection does not overwrite the current type — the most secure transport seen is retained in the accounting.

Player compatibility

HTTP/3 for OTT is supported by:

  • iOS AVPlayer / Safari — natively, via Alt-Svc; 0-RTT is supported.

  • Chrome, Firefox, Edge, Brave — via Alt-Svc; HLS is played through hls.js, DASH through dash.js / Shaka, and the player traffic inherits HTTP/3 from the browser fetch API.

  • Android ExoPlayer — via the Cronet QUIC engine (not the default transport; requires client-side configuration).

VLC (libVLC) does not support HTTP/3 — on these clients the stream continues to work over HTTPS/TCP, without the benefit of QUIC.

The real-world benefit of QUIC vs HTTPS/TCP is most noticeable on mobile networks / Wi-Fi / lossy networks:

  • no head-of-line blocking at the TCP layer — a loss in one HTTP stream does not delay the others;

  • 0-RTT handshake on resumed connections from the same client;

  • more stable ABR bitrate at packet losses of 1–5 %.

On managed networks / corporate Wi-Fi the benefit is usually modest — 5–10 % on startup latency and close to zero in steady state. CPU load on the server is higher for QUIC than for kernel TCP — this is the price of user-space TLS plus transport.

Caching model for OTT HLS and DASH

The server emits responses of three categories that differ in content lifetime and suitability for caching by intermediate nodes (reverse proxy, CDN, client cache).

1. Caching model

1.1. Resources and HTTP headers

Resource

URL

Content-Type

Cache-Control

TS segment

/h<sess>/<keyHex>.ts, /h<sess>/sub/<pid>/<keyHex>.vtt

video/mp2t

public, max-age=60, immutable

DASH MPD

/h<sess>/index.mpd

application/dash+xml; charset=utf-8

public, max-age=1

HLS master

/hls/<stream>/<login>/<pass>/index.m3u8

application/vnd.apple.mpegurl

public, max-age=1

HLS media

/h<sess>/index.m3u8, /h<sess>/sub/<pid>/index.m3u8

application/vnd.apple.mpegurl

public, max-age=1

302 Redirect

/dash/<stream>/<login>/<pass>/index.mpd

no-cache, no-store

Raw TS

/http/<stream>/<login>/<pass>

video/mp2t

not set; not cached

1.2. TS segment characteristics

The hexadecimal segment identifier in the URL (<keyHex> in paths /h<sess>/<keyHex>.ts) is computed as a CRC64 over the segment start time and the stream ID, and is globally unique. The segment URL addresses immutable content — repeated requests for the same URL return an identical byte stream (as long as the segment stays within the sliding window).

The immutable directive suppresses conditional revalidation by the client (If-None-Match, If-Modified-Since). The max-age=60 value is compatible with a typical timeShiftBufferDepth=40s.

1.3. Manifest characteristics

max-age=1 caps the upper bound of cached-content staleness at one second. Combined with proxy_cache_lock on (nginx) it collapses bursts of manifest requests into a single origin request per second.

1.4. Content variance

With absPath=0 (the default, no a URL parameter) the HLS media and DASH MPD manifests do not embed a session identifier in the body. The manifest content is identical across sessions belonging to the same (stream, param) combination. This lets a reverse-proxy cache reuse a single entry across sessions when the cache key is normalised.

With absPath=1 (URL parameter a=1) the manifest body contains absolute URLs that include the scheme, host, and session identifier. The content becomes session-specific; cross-session cache reuse is not available.

2. Client behaviour

Client

Manifest refresh URL

Effect on session count

VLC 3.x HLS

/h<sess>/index.m3u8

One session per playback

VLC 3.x DASH

/dash/<stream>/.../index.mpd

Handled by session reuse (see 3.3)

ffmpeg 5.x HLS

/h<sess>/index.m3u8

One session per playback

ffmpeg 5.x DASH

/dash/<stream>/.../index.mpd (re-request loop)

Handled by session reuse (see 3.3)

dash.js, hls.js

/h<sess>/... via <Location> / session URL

One session per playback

3. Specialised mechanisms

3.1. HTTP 302 redirect for DASH

A /dash/<stream>/<login>/<pass>/index.mpd request returns 302 Found with a Location: /h<sess>/index.mpd header. The body is empty. Authentication and session allocation happen during the redirect.

Clients that cache the redirect address the session URL directly in subsequent requests. Clients that do not, re-issue the redirect request. The cost of repeat redirect handling is limited to auth check and session-reuse operations.

3.2. Session reuse for DASH

When processing a /dash/.../index.mpd request from the same login to the same stream (with the same adaptive flag), the server finds an existing DASH session and returns its identifier again. No new session is created; a slot in the concurrent-connections limit is not consumed.

Applies to DASH only. HLS does not require a separate reuse mechanism: HLS clients refresh the media playlist via the session URL and do not create a new session on each refresh.

3.3. Cross-session segment reuse

The path /h<sess>/<keyHex>.ts does not depend on <sess> when resolving <keyHex> to content: <keyHex> uniquely identifies a TS segment within a stream globally. Nginx with a normalised cache key (stripping the /h<sess>/ prefix) serves every request for the same <keyHex> from a single cache entry, regardless of which clients issued them.

4. Request parameters

Parameter

Default value

Effect

a

0

1 — absolute URLs in manifests; 0 — relative

s

40

timeShiftBufferDepth in seconds

m

40

Minimum window length for manifest emission

v

6 for OTT mode, 3 for Peer mode

#EXT-X-VERSION in HLS (ignored by DASH); an explicit value in the URL overrides the default

h3

absent (off)

opt-in to HTTP/3 (QUIC) — causes the server to emit Alt-Svc in the response. Recognised values: presence ⇒ on, 1/on/yes/true ⇒ on, 0/off/no/false ⇒ explicit off. Sticky on the session URL /h<sess>/.... See section HTTP/3 (QUIC).

Changing a parameter via query string updates the values stored in the session on its next re-opening.

5. Load characteristics

Origin load scales with the number of distinct streams being watched concurrently. Increasing the number of clients watching the same stream does not increase origin requests when a reverse-proxy cache with a normalised cache key is in place.

Scenario

Origin request rate (ref.)

1 client per stream X

MPD: 0.4 req/s, segment: 0.2 req/s

N clients on one stream X (cache enabled)

MPD: 1 req/s, segment: 0.2 req/s

N ffmpeg clients in replay mode on one stream

MPD: 1 req/s (with proxy_cache_lock)

N clients on N distinct streams

MPD: 0.4·N req/s, segment: 0.2·N req/s

6. Nginx as a caching reverse proxy

6.1. Basic configuration

proxy_cache_path /var/cache/nginx/pss_segments
    levels=1:2 keys_zone=pss_segments:100m
    max_size=20g inactive=30m use_temp_path=off;

proxy_cache_path /var/cache/nginx/pss_manifests
    levels=1:2 keys_zone=pss_manifests:10m
    max_size=256m inactive=5m use_temp_path=off;

upstream pss_backend {
    server 127.0.0.1:41972;
    keepalive 64;
}

map $uri $pss_cache_key {
    ~^/h[0-9a-f]{16}(?<tail>/.+\.(ts|m3u8))$  "stream:$tail";
    default                                     $uri;
}

server {
    listen 80;
    server_name stream.example.com;

    location ~* "^/h[0-9a-f]{16}(/[0-9]+)?/[0-9a-f]+\.ts$" {
        proxy_cache               pss_segments;
        proxy_cache_key           $pss_cache_key;
        proxy_cache_valid         200 60s;
        proxy_cache_valid         404 403 0s;
        proxy_cache_lock          on;
        proxy_cache_use_stale     updating error timeout;
        proxy_cache_revalidate    on;
        add_header                X-Cache-Status $upstream_cache_status;

        proxy_pass                http://pss_backend;
        proxy_http_version        1.1;
        proxy_set_header          Connection "";
        proxy_buffering           on;
    }

    location ~* "(^/h[0-9a-f]{16}(/[0-9]+)?/index\.(m3u8|mpd)$|^/(hls|dash)/.*\.(m3u8|mpd)$)" {
        proxy_cache               pss_manifests;
        proxy_cache_key           $pss_cache_key;
        proxy_cache_valid         200 1s;
        proxy_cache_valid         404 403 0s;
        proxy_cache_lock          on;
        proxy_cache_lock_timeout  2s;
        proxy_cache_use_stale     updating;
        add_header                X-Cache-Status $upstream_cache_status;

        proxy_pass                http://pss_backend;
        proxy_http_version        1.1;
        proxy_set_header          Connection "";
    }

    location / {
        proxy_pass                http://pss_backend;
        proxy_http_version        1.1;
        proxy_set_header          Connection "";
        proxy_set_header          X-Forwarded-Proto $scheme;
        proxy_set_header          X-Forwarded-Host  $host;
        proxy_buffering           off;
        proxy_read_timeout        3600s;
    }
}

6.2. Directive purposes

Directive

Purpose

proxy_cache_lock on

Serialises upstream requests when concurrent cache misses target the same key

proxy_cache_use_stale updating

Returns the stale copy to concurrent requests while the cache is being refreshed

proxy_cache_revalidate on

Uses If-Modified-Since on cache miss when a saved copy exists

proxy_cache_valid 404 403 0s

Disables caching of authorisation errors and 404

keepalive 64 in upstream

Maintains a pool of persistent connections to origin

proxy_buffering on

For segments; enables response buffering in nginx

proxy_buffering off

For the / location; disables buffering (raw streaming)

6.3. Computing segment cache max_size

Rough value: bitrate × timeShiftBufferDepth × distinct_streams × 2

Example: 10 streams × 8 Mbps × 40s × 2 ≈ 800 MB. A 10x headroom is recommended to absorb bitrate variance.

6.4. TLS termination

The Perfect Streamer server accepts connections on HTTP and HTTPS ports. With TLS termination at nginx the upstream uses the HTTP port. Forwarding X-Forwarded-Proto and X-Forwarded-Host headers is required for correct absolute URL composition when absPath=1.

server {
    listen 443 ssl http2;
    server_name stream.example.com;

    ssl_certificate           /etc/letsencrypt/live/stream.example.com/fullchain.pem;
    ssl_certificate_key       /etc/letsencrypt/live/stream.example.com/privkey.pem;
    ssl_protocols             TLSv1.2 TLSv1.3;
    ssl_session_cache         shared:SSL:10m;
    ssl_session_timeout       1d;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location ... {
        proxy_pass                http://pss_backend;
        proxy_set_header          X-Forwarded-Proto https;
        proxy_set_header          X-Forwarded-Host  $host;
        proxy_set_header          Host              $host;
        # + caching directives from 6.1
    }
}

server {
    listen 80;
    server_name stream.example.com;
    return 301 https://$host$request_uri;
}

For HTTPS between nginx and origin, proxy_ssl_verify and proxy_ssl_trusted_certificate directives apply. Encryption is redundant for loopback connections.

6.5. Multi-host

When serving multiple server_name from a single nginx process, $host is added to the cache key to isolate content:

map $uri $pss_cache_key {
    ~^/h[0-9a-f]{16}(?<tail>/.+\.(ts|m3u8))$  "$host:stream:$tail";
    default                                     "$host:$uri";
}

keys_zone size is sized at 8000 keys per MB. For multi-host installations with thousands of streams, keys_zone=...:300m or higher is recommended.

7. Client-side caching

Cache-Control: immutable is honoured by Chrome/Firefox/Safari. The client cache returns the segment without a conditional request on re-access (including backward seek within the player buffer).

Service Workers can apply a cache-first strategy based on Cache-Control content. DASH players (dash.js, Shaka) use MSE through SourceBuffer; a segment placed in the buffer remains available without a repeat HTTP request until it slides out of the window.

For cross-domain requests the Access-Control-Allow-Origin: * header allows caching in shared caches without Vary: Origin. Switching ACAO to a specific Origin requires Vary: Origin, which reduces shared-cache efficiency.

8. Distribution via CDN

Perfect Streamer is compatible with pull-from-origin CDNs (Cloudflare, Akamai, Fastly, BunnyCDN, Amazon CloudFront).

Origin shield. Placing one or more shield nodes between CDN edge and origin is recommended to reduce origin request rate when clients are globally distributed.

Purge. Content-addressed segments require no purge. When stream metadata changes (codec, resolution), manifests refresh within max-age=1 without an explicit purge.

Cache warming. When a specific stream is expected to spike, the CDN may be warmed from several geographic points before broadcast start.

Geo-distribution. Segments (max-age=60) are well suited for geographically distributed caching. Manifests (max-age=1) tolerate up to one-second delivery delay — acceptable for non-low-latency live.

9. Monitoring

9.1. X-Cache-Status

Add add_header X-Cache-Status $upstream_cache_status; in every cached location. Values:

Value

Description

HIT

Response from cache

MISS

Not in cache; fetched from origin and stored

EXPIRED

Expired, refreshed

UPDATING

Stale copy returned to a concurrent request during refresh

STALE

use_stale returned the expired copy (origin unreachable)

REVALIDATED

Origin returned 304 Not Modified

BYPASS

proxy_cache_bypass triggered

9.2. Access-log format

log_format pss_cache '$remote_addr $status $request_method "$request" '
                     '$body_bytes_sent rt=$request_time ut=$upstream_response_time '
                     'cache=$upstream_cache_status key=$pss_cache_key';

server {
    access_log /var/log/nginx/pss.log pss_cache;
}

9.3. Metrics

The nginx-vts module exports per-zone metrics in Prometheus format:

GET /status/format/prometheus

Recommended alert thresholds:

Metric

Threshold

Possible cause

Segment HIT rate

< 90% over 5 minutes

Cache-key normalisation broken; max_size too small

Manifest MISS rate

> 50% over 1 minute

proxy_cache_lock is not serialising requests

Upstream response time p95

> 500 ms over 1 minute

Origin overload

Cache zone fill

> 90% over 10 minutes

Approaching max_size; LRU eviction expected

10. Diagnostics

Symptom

Likely cause

Resolution

Low segment HIT rate

Vary: Origin with high Origin variance; broken normalisation in map

Inspect headers and the regex in the map directive

404 on segments after they leave the sliding window

Cached 404 for a segment that fell out of the sliding window

Add proxy_cache_valid 404 0s in the segments location

Playback start delay of 2–5 s

proxy_cache_lock_timeout exceeds target latency

Lower to 1–2 s; enable proxy_cache_use_stale updating

Manifest does not refresh

proxy_cache_valid overrides max-age

Set proxy_cache_valid 200 1s explicitly

Growing TIME_WAIT on upstream

keepalive missing in upstream block

Add keepalive 64, proxy_http_version 1.1, proxy_set_header Connection ""

403 on /dash/.../<segment>.ts from ffmpeg

Client resolves relative URLs against the pre-redirect URL

Server emits <BaseURL>/h<sess>/</BaseURL> (absolute path); compatible in the current build

Lags, frequent rebuffering for remote clients

Low effective TCP throughput due to slow start and idle restart at large RTTs (300 ms and above)

Tuning of the Linux network stack on the origin: see 10.1

10.1. TCP tuning of the origin for high-RTT clients

The problem appears on clients with a large RTT to the origin (e.g. 300 ms and above) when the stream bitrate is close to the channel capacity. Player symptoms (VLC, ffmpeg, dash.js) — frequent rebuffering, warnings such as ES_OUT_SET_PCR called too late (with pts_delay increasing), buffer deadlock prevented, stream interruptions. On the server side the client looks normal, there are no errors, and the throughput on /data/stream/... matches the input stream.

Cause:

  • TCP slow start. Every new TCP connection starts with a congestion window of about 14 KB and grows it over several RTTs. At an RTT of 300 ms, reaching the full window takes 2-3 seconds. During that time, an HLS/DASH segment lasting 5 s (4-6 MB) downloads noticeably slower than real time.

  • TCP idle restart. Between segment requests, an HLS pull-model client pauses for 4-5 s. By default, after such a pause the Linux kernel resets the connection’s congestion window back to the initial cwnd (the net.ipv4.tcp_slow_start_after_idle=1 behaviour). As a result, on the next GET the keep-alive connection restarts transmission from slow start anew — even on an already warmed-up session.

An additional aggravation — the default CUBIC congestion control copes poorly with long RTTs and packet loss on intermediate network segments.

Solution — two sysctl parameters on the origin:

# Keep congestion window across idle pauses inside keep-alive sessions.
sysctl -w net.ipv4.tcp_slow_start_after_idle=0

# Use BBR instead of CUBIC: better behaviour on long-RTT paths
# with mild packet loss; paces sending instead of bursting.
modprobe tcp_bbr
sysctl -w net.ipv4.tcp_congestion_control=bbr

For persistent application:

cat > /etc/sysctl.d/99-pss-net.conf <<EOF
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_congestion_control    = bbr
EOF
sysctl --system

The main effect comes from the first parameter (tcp_slow_start_after_idle=0). It directly eliminates repeated slow start between segment requests within the same keep-alive connection. The second (BBR) provides additional robustness and applies to all new connections.

The tuning does not require a Perfect Streamer restart and applies to all new TCP connections immediately after sysctl -w. Existing connections retain the congestion control with which they were established.

11. Security

11.1. Session URL

A URL of the form /h<sess>/... acts as the session token — no repeat authentication is required. Lifetime is bounded by an idle timeout (30 s). On inactivity the session is removed by the cleaner task.

Requirements:

  • HTTPS on every OTT path (/hls/, /dash/, /h<sess>/) in production

  • Session ID in the Location header of 302 is not cached (no-cache, no-store)

11.2. Rate limiting

limit_req_zone $binary_remote_addr zone=dash_top:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=hls_top:10m  rate=5r/s;

server {
    location /dash/ {
        limit_req zone=dash_top burst=20 nodelay;
        proxy_pass http://pss_backend;
    }
    location /hls/ {
        limit_req zone=hls_top burst=20 nodelay;
        proxy_pass http://pss_backend;
    }
}

Session URLs (/h<sess>/) do not require rate limiting — handling is cheap and responses are cached.

11.3. Caching of error responses

proxy_cache_valid 200 60s;
proxy_cache_valid 301 302 0s;
proxy_cache_valid 404 403 0s;
proxy_cache_valid any 1s;

Disables caching of redirects (unique sess in Location) and of authorisation/missing-resource error responses.

11.4. Restricting network access to origin

Port 41972 (41982 for HTTPS) must be closed to external traffic. Acceptable configurations:

  1. Bind Perfect Streamer to 127.0.0.1 (when nginx is co-located)

  2. Firewall rule:

iptables -A INPUT -p tcp --dport 41972 ! -s 10.0.0.0/8 -j DROP

12. Middleware integration

12.1. Prefix-login model

Perfect Streamer supports delegating user identification to a middleware/billing system via the prefix-login mechanism. An external connector to the billing system is not included in the current release.

Embedded-user configuration:

{
  "id": 9,
  "login": "sub",
  "password": "xxx",
  "is-prefix": true,
  "max-conn-http-hls": 1,
  "accept-stream": [ ... ]
}

With is-prefix: true the server accepts URLs whose login follows <prefix><billing_user_id>:

/dash/test1/sub42/xxx/index.mpd
/hls/test1/sub43/xxx/index.m3u8

12.2. Statistics format

<clients>
  <client login-id="-1974387287" login="sub" match-login="sub42"
          sess-id="11331..." ott-type="dash" stream-id="10000" .../>
  <client login-id="-2147031294" login="sub" match-login="sub43"
          sess-id="11132..." ott-type="dash" stream-id="10000" .../>
</clients>

The login-id field holds the hash of the URL login. The login field is the configured value. The match-login field is the URL login used by the client.

12.3. Prefix-login limitations

  • Shared password. All subscribers of a prefix pool use a single password value. Compromising the password grants access to any <prefix><string>.

  • ACL granularity. accept-stream applies to the whole prefix pool. Per-subscriber ACL is not available without external billing.

  • Password rotation. Changing the password disconnects all active subscribers. A gradual rollover requires temporarily using two prefix logins.

13. WebVTT subtitles

The subtitle source is DVB Teletext / DVB Subtitling from the input MPEG-TS. Teletext subtitle tracks must be present in the Media Information or Original Media Information sections. The Analyzer section can also be used to verify that packets of the corresponding PIDs are active.

For OTT HLS/DASH the OTT mode must be enabled (in Peer mode WebVTT subtitles are not available). The chunk counter OTT WebVTT buffer chunk count in the Output # OTT section must become non-zero.

To diagnose subtitles, enable Analyze and Trace on the stream. At stream start the stream log should contain:

Start Teletext subtitle decoder
[ttxsubdec] ttx: pid=331 magazine=8 page=0x88 lang=***

Subsequent log entries contain the decoded subtitle text.

13.1. VTT segment URLs

Scheme

URL

Content

HLS master

/hls/.../index.m3u8

#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",...,URI="/h<sess>/sub/<pid>/index.m3u8"

HLS subtitle playlist

/h<sess>/sub/<pid>/index.m3u8

list of <keyHex>.vtt with #EXTINF

HLS VTT segment

/h<sess>/sub/<pid>/<keyHex>.vtt

VTT with HLS-flavoured X-TIMESTAMP-MAP

DASH MPD AdaptationSet

inside index.mpd

contentType="text" mimeType="text/vtt" + <SegmentTemplate media="$Number$.vtt">

DASH VTT segment

/h<sess>/sub/<pid>/<seq>.vtt

VTT with DASH-flavoured X-TIMESTAMP-MAP

<keyHex> is a 16-character hex CRC64 of the segment start time, stream ID, and subtitle-track PID. <seq> is the decimal sequence number of a subtitle-stream chunk (subtitle numbering is unrelated to TS-chunk numbering).