# 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 sessions per principal hash within one station's session store. | | `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. | 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 `