commit 6fae6a3a430410145501abbe57b2cc027fd2eaa0 Author: root Date: Fri Sep 4 20:23:10 2026 +0200 feat: add secure on-demand HLS playback plugin diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3606a2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/vendor/ +/.phpunit.cache/ +/.phpunit.result.cache +/composer.lock +/.idea/ +/.vscode/ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dcb72fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Lexian-droid contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..79b67b0 --- /dev/null +++ b/README.md @@ -0,0 +1,351 @@ +# 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 `