# AzuraCast On-Demand HLS Protected, expiring on-demand HLS playback for media in an AzuraCast station. The plugin adds an authenticated endpoint that creates a short-lived playback session for eligible station media. On the first request for a rendition, FFmpeg generates an audio-only AAC/HLS package. Later sessions reuse that package while every playlist and segment request is authorized by an opaque bearer token. PHP performs authorization; AzuraCast's protected Nginx `X-Accel-Redirect` mapping serves the bytes. ## Compatibility and requirements This implementation targets the current AzuraCast plugin APIs used by the Rolling Release codebase: - PHP **8.4** (`composer.json` requires `^8.4`) - Symfony Process **8.x** - AzuraCast classes and events used by `events.php`, including `BuildRoutes`, `GetSyncTasks`, `WriteNginxConfiguration`, station feature/permission middleware, and `App\Nginx\CustomUrls` - FFmpeg available to the AzuraCast web container/process, with AAC encoding and HLS muxing support - A local AzuraCast station temp directory underneath AzuraCast's protected station-files mapping (the normal Docker layout under `/var/azuracast/stations`) - The station's **On-Demand Streaming** feature enabled - Media assigned to at least one enabled playlist whose **Include in On-Demand** option is enabled There is no declared minimum AzuraCast release or compatibility shim for older plugin APIs. Pin and test a known AzuraCast release before production deployment; older Stable releases using PHP below 8.4 or different middleware/event interfaces are not compatible. The implementation produces a single audio rendition: the first audio stream is transcoded to AAC at the configured bitrate. It does not preserve video or create an adaptive bitrate ladder. ## Installation > The directory basename is significant: it must be `azuracast-on-demand-hls` so AzuraCast maps it to the `Plugin\AzuraCastOnDemandHls` namespace. AzuraCast must see the complete checkout at: ```text /var/azuracast/www/plugins/azuracast-on-demand-hls ``` ### Docker installation (recommended) Keep the checkout on the host, for example at `/var/azuracast/plugins/azuracast-on-demand-hls`, and mount it into the web container. Merge the following into `/var/azuracast/docker-compose.override.yml`; do not replace unrelated existing override settings. ```yaml services: web: environment: COMPOSER_PLUGIN_MODE: "true" volumes: - ./plugins/azuracast-on-demand-hls:/var/azuracast/www/plugins/azuracast-on-demand-hls:ro ``` Add configuration variables under the same `web.environment` mapping if desired. From `/var/azuracast`, recreate the containers so the mount and environment are applied: ```bash ./docker.sh restart ``` AzuraCast's root Composer configuration merges `plugins/*/composer.json`. After the plugin is visible inside the container, refresh the parent autoloader (do **not** create a separate production `vendor/` directory inside this plugin): ```bash docker compose exec --user azuracast web composer dump-autoload ./docker.sh restart ``` The second restart reloads the web and long-running worker processes so `services.php`, `events.php`, and the refreshed autoloader are active. ### Direct/non-Docker installation Place the checkout directly at `/var/azuracast/www/plugins/azuracast-on-demand-hls`, set the environment variables for the AzuraCast web and worker processes, run `composer dump-autoload` from the AzuraCast application root, and restart those processes. Direct installs must provide PHP 8.4, FFmpeg, writable station temp directories, and AzuraCast's internal station-files Nginx location. ### Regenerate station Nginx configuration The plugin contributes a per-station Nginx block that disables access logging for playback-token URLs. It is not present until station configuration is rewritten. Restart each affected station from the AzuraCast UI, or run the current AzuraCast CLI command using the station **short name**: ```bash cd /var/azuracast ./docker.sh cli azuracast:radio:restart STATION_SHORT_NAME ``` Omit the short name to rewrite/restart all stations: ```bash ./docker.sh cli azuracast:radio:restart ``` A radio restart is service-affecting; schedule it appropriately. Repeat this step after plugin upgrades that change `NginxConfiguration.php`, after enabling/disabling the plugin, and after changing station configuration. Confirm the generated station Nginx configuration contains the protected `ondemand-hls/playback` location with `access_log off`. ## Configuration Configuration is read from the process environment when AzuraCast builds its container. Blank values use the defaults. Restart/recreate AzuraCast processes after changes, and regenerate station Nginx configuration if `AZURACAST_ONDEMAND_HLS_ENABLED` changes. | Variable | Default | Accepted value / range | Purpose | |---|---:|---|---| | `AZURACAST_ONDEMAND_HLS_ENABLED` | `true` | PHP boolean strings such as `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off` | Enables route behavior, scheduled cleanup, and generation of the station Nginx block. | | `AZURACAST_ONDEMAND_HLS_SESSION_TTL` | `1800` | `60`–`86400` seconds | Lifetime of a playback bearer URL. | | `AZURACAST_ONDEMAND_HLS_SEGMENT_DURATION` | `6` | `2`–`20` seconds | FFmpeg HLS target segment duration. Actual segment duration can vary at codec boundaries. | | `AZURACAST_ONDEMAND_HLS_CACHE_TTL` | `86400` | At least `SESSION_TTL`, at most `2592000` seconds (30 days) | Minimum idle age before an unreferenced rendition can be removed. | | `AZURACAST_ONDEMAND_HLS_MAX_SESSIONS` | `10` | `1`–`100` | Maximum active playback renditions per principal hash within one station. | | `AZURACAST_ONDEMAND_HLS_CACHE_DIRECTORY` | `ondemand-hls` | 1–64 characters; starts with lowercase letter/digit, then lowercase letters, digits, `_`, `-` | Relative directory created beneath each station temp directory. | | `AZURACAST_ONDEMAND_HLS_FFMPEG_BINARY` | `ffmpeg` | 1–255 characters from letters, digits, `.`, `_`, `+`, `/`, `-`; must not contain `..` | FFmpeg executable name or absolute path. | | `AZURACAST_ONDEMAND_HLS_AUDIO_BITRATE` | `128k` | `10k`–`9999k` in the exact form `[1-9][0-9]{1,3}k` | AAC target bitrate and part of the rendition cache key. Use a sensible audio bitrate despite the broad validation range. | | `AZURACAST_ONDEMAND_HLS_TRANSCODE_TIMEOUT` | `600` | `30`–`3600` seconds | Maximum synchronous FFmpeg run time. | ### Cross-origin HLS playback (CORS) Cross-origin playback is **disabled by default**. Configure an explicit allowlist per station with the plugin commands; do not edit generated Nginx files. The setting is stored in the AzuraCast database by the plugin, and every modifying command rewrites that station's generated Nginx configuration and reloads Nginx only when the file changed. First, run the plugin migration once after installing/upgrading the plugin: ```bash cd /var/azuracast ./docker.sh cli migrations:migrate --no-interaction ``` Then configure the station (the station argument accepts its ID or short name): ```bash # Replace the entire allowlist with the two approved web origins. ./docker.sh cli azuracast:ondemand-hls:cors:set 11 \ https://aifrequency.org \ https://www.aifrequency.org # Inspect the current allowlist. ./docker.sh cli azuracast:ondemand-hls:cors:list 11 # Add or remove one origin without changing the others. ./docker.sh cli azuracast:ondemand-hls:cors:add 11 https://player.example.org ./docker.sh cli azuracast:ondemand-hls:cors:remove 11 https://player.example.org # Disable cross-origin playback for this station again. ./docker.sh cli azuracast:ondemand-hls:cors:clear 11 ``` Origins must be absolute `http://` or `https://` origins without paths, credentials, query strings, or fragments. They are normalized before being stored. The plugin emits an exact `Access-Control-Allow-Origin` value for the requesting allowed origin; it never joins multiple origins and never substitutes `*`. It also emits `Vary: Origin`, supports `GET`, `HEAD`, and token-authorized `OPTIONS` preflight requests (including the `Range` request header), and leaves requests with no `Origin` header unchanged. The allowlist is generated into the plugin's protected internal Nginx HLS location, after the authorized `X-Accel-Redirect`. It therefore covers `master.m3u8`, `media.m3u8`, and HLS `.ts` segments rather than only the initial PHP authorization response. The bearer URL remains required for every resource request; allowing an origin does not make a resource public or extend its expiry. Example Docker override fragment: ```yaml services: web: environment: COMPOSER_PLUGIN_MODE: "true" AZURACAST_ONDEMAND_HLS_SESSION_TTL: "900" AZURACAST_ONDEMAND_HLS_CACHE_TTL: "86400" AZURACAST_ONDEMAND_HLS_AUDIO_BITRATE: "128k" AZURACAST_ONDEMAND_HLS_TRANSCODE_TIMEOUT: "600" ``` Invalid values fail configuration construction and can prevent AzuraCast from booting. Integer variables are cast by PHP before range validation, so use plain base-10 integer strings. ## API ### Create a playback session ```http POST /api/station/{station_id}/ondemand-hls/{media_id} Authorization: Bearer ``` `station_id` is the AzuraCast station ID or identifier accepted by AzuraCast's station middleware. `media_id` is the media `unique_id` (letters, digits, and hyphens), not a filesystem path. The API key's user must have the station **Media** permission. AzuraCast also accepts `X-API-Key`, but `Authorization: Bearer` is preferred. Do not call this authenticated endpoint from untrusted browser code, because that would expose the long-lived AzuraCast API key; call it from your backend and return only the short-lived playback URL to the client. ```bash curl --fail-with-body \ -X POST \ -H "Authorization: Bearer ${AZURACAST_API_KEY}" \ "https://radio.example.com/api/station/1/ondemand-hls/abcdef123456abcdef123456" ``` A successful request returns HTTP `201 Created`, `Cache-Control: no-store`, and JSON: ```json { "url": "https://radio.example.com/api/station/1/ondemand-hls/playback/OPAQUE_TOKEN/master.m3u8", "expires_at": 1788490440, "media_id": "abcdef123456abcdef123456" } ``` `expires_at` is a Unix timestamp. The returned absolute URL is the HLS master playlist. Its relative `media.m3u8` and segment references retain the same token automatically. Creation is limited by AzuraCast middleware to 10 requests per 60-second interval. Asset delivery is limited to 150 requests per 5-second interval. Relevant application responses include: | Status | Meaning | |---:|---| | `201` | Session created. | | `404` | Plugin disabled, station lacks On-Demand support, media is missing, or media is not in an enabled on-demand playlist. | | `429` | AzuraCast request-rate limit or the configured active-session limit was reached. | | `503` | This rendition/another station rendition is being generated, or FFmpeg failed. A generation-lock conflict includes `Retry-After: 10`. | | `500` | Session persistence or another runtime operation failed. | The first request for a cache key performs FFmpeg transcoding synchronously and may take time. On `503` with `Retry-After`, retry with bounded exponential backoff rather than sending parallel requests. ### Play the returned URL Safari and other clients with native HLS support can assign the URL directly to an `