mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-31 11:03:22 +01:00
The single 469-operation document becomes eleven: the player, playlist, admin and system specs are rewritten from it, and streaming, MAG portal, Enigma2, activation, reseller, cluster and monitoring specs are new. Every operation is copied verbatim; the admin and reseller api_key parameter becomes a security scheme so Authorize works again. The old specs listed 20 actions the code no longer has (backup_database, restart_server, getFile, view_log, ...), and described /api.php as an HTTP API although it now answers 404 and runs over node.rpc. The Swagger UI gets one tab per spec with documentation condensed from the tag descriptions, wraps the tab row and keeps line breaks in code blocks.
2280 lines
80 KiB
YAML
2280 lines
80 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: XC_VM System API
|
|
version: 2.7.2
|
|
summary: Internal Server API, Legacy Admin Stream Endpoints of the Xtream Codes compatible HTTP surface, as implemented by XC_VM.
|
|
description: |
|
|
A source-level reference for the **Xtream Codes (XC) API**: the de facto standard HTTP interface
|
|
between IPTV panels and player apps. The reference implementation is
|
|
[XC_VM](https://github.com/Vateron-Media/XC_VM) 2.7.2, an open-source continuation of Xtream UI / XC.
|
|
Each operation cites the XC_VM source file and line it was traced from (`src/...:line`).
|
|
|
|
## Authentication models
|
|
|
|
The panel has several independent auth schemes. Each endpoint states which one it uses.
|
|
|
|
| Who | How |
|
|
|---|---|
|
|
| Line (subscriber) | `username` + `password` query parameters, or `token` (32-char line access token). Also in path form: `/player_api/{u}/{p}`, `/live/{u}/{p}/...` |
|
|
| Activation code | The code typed as `username` (or `token`) is activated on first use and becomes a line |
|
|
| MAG / Stalker box | MAC address cookie + bearer token from `handshake` (`/c/portal.php`, `/stalker_portal/...`) |
|
|
| Enigma2 box | MAC/device registration (`enigma2.php`) |
|
|
| Admin / reseller REST | `api_key` query parameter (named `xct_` token or legacy per-user key) on `/{access_code}/?action=...`; the access code's IP whitelist is enforced by nginx |
|
|
| Load-balancer nodes | Shared cluster secret (`/cluster/v1`); signed `node.rpc` (legacy `api.php` on nodes older than 2.7.0) |
|
|
|
|
## Notation
|
|
|
|
OpenAPI cannot express query-string multiplexing, so action-based endpoints are written as separate
|
|
literal paths such as `/player_api.php?action=get_live_streams`. The request is still a single
|
|
`GET /player_api.php` with `action` as an ordinary query parameter.
|
|
|
|
The admin and reseller REST APIs share one URL shape, `/{access_code}/?action=...`. Each access code has a type
|
|
(3 = Admin API, 4 = Reseller API) and nginx routes it to that API, so `/{access_code}/...` and `/{reseller_access_code}/...` are the
|
|
same URL and differ only in which kind of code is used.
|
|
contact:
|
|
name: XC_VM project
|
|
url: https://github.com/Vateron-Media/XC_VM
|
|
license:
|
|
name: AGPL-3.0 (XC_VM source); this document describes its public interface
|
|
identifier: AGPL-3.0-only
|
|
servers:
|
|
- url: '{protocol}://{host}:{port}'
|
|
description: Any XC compatible panel or load-balancer node
|
|
variables:
|
|
protocol:
|
|
default: http
|
|
enum:
|
|
- http
|
|
- https
|
|
host:
|
|
default: tv.example.com
|
|
port:
|
|
default: '80'
|
|
description: HTTP broadcast port (often 80, 8080, 8000, 25461); HTTPS often 443 or 25463.
|
|
externalDocs:
|
|
description: XC_VM source
|
|
url: https://github.com/Vateron-Media/XC_VM
|
|
components:
|
|
parameters:
|
|
Username:
|
|
name: username
|
|
in: query
|
|
required: true
|
|
description: Line username. Optional when `token` is sent instead.
|
|
schema:
|
|
type: string
|
|
example: demo
|
|
Password:
|
|
name: password
|
|
in: query
|
|
required: true
|
|
description: Line password. Optional when `token` is sent instead.
|
|
schema:
|
|
type: string
|
|
format: password
|
|
example: s3cret
|
|
Internal_Password:
|
|
name: password
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
description: |
|
|
Legacy (pre-2.7.0 nodes) only: the panel's `live_streaming_pass`. It is also sent as a form field by ApiClient::systemRequest.
|
|
Not used on the `node.rpc` path.
|
|
LegacyAdmin_ApiPass:
|
|
name: api_pass
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Must equal the `api_pass` setting when that setting is non-empty (src/Public/admin/api.php:32-34).
|
|
LegacyAdmin_LivePassword:
|
|
name: password
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: '`live_streaming_pass` (from an allowed server IP with DATAPLANE flow off) or a loopback `lb1-...` token (127.0.0.1/::1 only).'
|
|
LegacyAdmin_UIToken:
|
|
name: uitoken
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Panel-issued AdminStreamToken (encrypted with live_streaming_pass; bound to IP/subnet; carries stream id and expiry).
|
|
LegacyAdmin_RelayKey:
|
|
name: rk
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: RelaySeal sealed session key (relay callers). Output is framed and encrypted under it. An invalid key gives 404.
|
|
LegacyAdmin_RelayTicket:
|
|
name: X-XCVM-Relay
|
|
in: header
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Panel-signed `rly` ticket (parent_sid, stream_id, child_sid, child_gen).
|
|
LegacyAdmin_RelayAuth:
|
|
name: X-XCVM-Relay-Auth
|
|
in: header
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Child's Ed25519 proof `<ts_ms>.<32-hex nonce>.<b64url sig>` over ticket, method and target (±90 s).
|
|
responses:
|
|
Internal_RouteRemoved:
|
|
description: |
|
|
This build: `/api` and `/api.php` have no controller, so the reply is 404 with an empty body (src/Public/index.php:185-187).
|
|
On pre-2.7.0 nodes, a wrong password or IP gave a 404 nginx-style page (generateError, production mode).
|
|
LegacyAdmin_NotFound:
|
|
description: '`generate404()`: 404 with an nginx-mimicking HTML page (src/Core/Error/ErrorResponder.php:119-121,235).'
|
|
content:
|
|
text/html:
|
|
schema:
|
|
type: string
|
|
example: "<html>\r\n<head><title>404 Not Found</title></head>\r\n<body>\r\n<center><h1>404 Not Found</h1></center>\r\n<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n"
|
|
schemas:
|
|
Internal_Result:
|
|
type: object
|
|
properties:
|
|
result:
|
|
type: boolean
|
|
required:
|
|
- result
|
|
Internal_Stats:
|
|
type: object
|
|
description: SystemInfo::getStats (src/Core/Util/SystemInfo.php:30-90).
|
|
properties:
|
|
cpu:
|
|
type: number
|
|
cpu_cores:
|
|
type: integer
|
|
cpu_avg:
|
|
type: number
|
|
cpu_name:
|
|
type: string
|
|
total_mem:
|
|
type: integer
|
|
total_mem_free:
|
|
type: integer
|
|
total_mem_used:
|
|
type: integer
|
|
total_mem_used_percent:
|
|
type: number
|
|
total_disk_space:
|
|
type: integer
|
|
free_disk_space:
|
|
type: integer
|
|
kernel:
|
|
type: string
|
|
uptime:
|
|
type: string
|
|
total_running_streams:
|
|
type: integer
|
|
bytes_sent:
|
|
type: number
|
|
bytes_sent_total:
|
|
type: number
|
|
bytes_received:
|
|
type: number
|
|
bytes_received_total:
|
|
type: number
|
|
network_speed:
|
|
type:
|
|
- integer
|
|
- string
|
|
interfaces:
|
|
type: array
|
|
items:
|
|
type: string
|
|
network_info:
|
|
type: object
|
|
audio_devices:
|
|
type: array
|
|
video_devices:
|
|
type: array
|
|
gpu_info:
|
|
type:
|
|
- object
|
|
- array
|
|
iostat_info:
|
|
type:
|
|
- object
|
|
- array
|
|
cpu_load_average:
|
|
type: number
|
|
example:
|
|
cpu: 12.5
|
|
cpu_cores: 8
|
|
cpu_avg: 10.2
|
|
cpu_name: Intel(R) Xeon(R) E-2288G
|
|
total_mem: 32768000
|
|
total_mem_free: 20480000
|
|
total_mem_used: 12288000
|
|
total_mem_used_percent: 37.5
|
|
total_disk_space: 500107862016
|
|
free_disk_space: 380107862016
|
|
kernel: 6.8.0-45-generic
|
|
uptime: 12d 4h 3m
|
|
total_running_streams: 42
|
|
bytes_sent: 1250000
|
|
bytes_sent_total: 98765432100
|
|
bytes_received: 900000
|
|
bytes_received_total: 87654321000
|
|
network_speed: 1000
|
|
interfaces:
|
|
- eth0
|
|
network_info: {}
|
|
audio_devices: []
|
|
video_devices: []
|
|
gpu_info: []
|
|
iostat_info: []
|
|
cpu_load_average: 0.85
|
|
tags:
|
|
- name: Internal Server API
|
|
description: |
|
|
Server-to-server "system API" of a load balancer / node: `/api.php?action=<name>` (alias `/api`).
|
|
Not part of the Xtream Codes client API.
|
|
|
|
**Status in this tree (2.8.0):** Phase 10 removed `InternalApiController::index` (docs/adr/0004-cluster-api.md).
|
|
nginx still routes `location = /api` and `location = /api.php` to `Public/index.php` with
|
|
`XC_SCOPE=api`, `XC_API=internal` (src/bin/nginx/conf/nginx.conf:451-479), but `internal` is not in
|
|
`$rApiEndpoints`, so every HTTP request to `/api` or `/api.php` gets **404 with an empty body**
|
|
(src/Public/index.php:170-187).
|
|
|
|
The actions survive as the payload of the signed `node.rpc` cluster command:
|
|
`cluster:exec` (src/Cli/Commands/ClusterExecCommand.php:280-289) calls
|
|
`InternalApiController::runCommand` (src/Public/Controllers/Api/InternalApiController.php:39-41).
|
|
That path does no password or IP check: the panel's Ed25519 command signature replaces them.
|
|
The node returns the action's raw output in `ack.result`, truncated at 4194304 bytes
|
|
(CommandBus::MAX_RESULT, src/Domain/Cluster/CommandBus.php:59,355-359). `scandir_recursive` still pages `stat`
|
|
listings at 60000 bytes, per a stale comment claiming a 65536 cap (InternalApiController.php:394-399).
|
|
|
|
MAIN still sends the legacy HTTP form to nodes that are not on the cluster bus (pre-2.7.0 nodes):
|
|
`ApiClient::systemRequest` POSTs `http://<server_ip>:<http_broadcast_port>/api` with the fields form-encoded
|
|
and `password=<live_streaming_pass>` added (src/Core/Http/ApiClient.php:69-90).
|
|
`ApiClient::asyncRequest` POSTs to `api_url` = `http://<host>:<port>/api?password=<live_streaming_pass>`
|
|
(src/Core/Http/ApiClient.php:99-112, src/Domain/Server/ServerRepository.php:153-154).
|
|
On such nodes access also required the source IP in `ServerRepository::getAllowedIPs()`
|
|
(src/Domain/Server/ServerRepository.php:524). That list is 127.0.0.1, SERVER_ADDR, and every server's
|
|
`server_ip`, `private_ip`, `whitelist_ips` and IP-valued `domain_name`, cached 60 s.
|
|
Failures used the error codes `INVALID_API_PASSWORD` / `API_IP_NOT_ALLOWED` (src/Core/Error/ErrorResponder.php:35,57).
|
|
In production those render as a 404 nginx-style page.
|
|
|
|
Corrections to docs/_media/system-api.openapi.yaml:
|
|
- These actions are gone from the dispatcher and answer `{"result":false}` via `default`:
|
|
`view_log`, `pidsAreRunning`, `kill_watch`, `reload_epg`, `watch_force`, `plex_force`, `getFile`, `redirect_connection`.
|
|
`getFile` is replaced by `/xfile`; MAIN still builds `action=getFile` URLs only for pre-2.7.0 nodes
|
|
(src/Domain/Server/ServerRepository.php:357).
|
|
- `activity_id` is a string connection reference, not an integer.
|
|
- Callers send POST, not GET.
|
|
- name: Legacy Admin Stream Endpoints
|
|
description: |
|
|
`/admin/(api|live|proxy_api|thumb|timeshift|vod)` on every server. nginx
|
|
`location ~ ^/admin/(api|index|live|proxy_api|thumb|timeshift|vod)$` uses `limit_req zone=one burst=8`
|
|
and sets `XC_ADMIN=$1` for src/Public/admin/index.php (src/bin/nginx/conf/nginx.conf:306-321).
|
|
- The `.php` forms get a 302 to `/admin/$1?$args` (nginx.conf:323-325).
|
|
- `/admin/index`, or an unknown or missing handler, gets 404 with an nginx-style HTML body
|
|
(src/Public/admin/index.php:23-26,39-43).
|
|
|
|
Stream endpoints are admitted by `RelayGuard::admit` (src/Core/Cluster/RelayGuard.php:59-70), in one of three ways:
|
|
1. A relay presents `X-XCVM-Relay` (panel-signed `rly` ticket naming parent_sid, stream_id, child_sid, child_gen)
|
|
plus `X-XCVM-Relay-Auth`, the child's Ed25519 proof. Once these headers are present they alone decide.
|
|
2. A loopback `password=lb1-...` token, accepted only from 127.0.0.1 or ::1.
|
|
3. The legacy `password=<live_streaming_pass>` from an IP in `getAllowedIPs()`, only when that server's DATAPLANE flow
|
|
is off (RelayGuard.php:118-133).
|
|
|
|
Alternatively a panel `uitoken` (AdminStreamToken, decoded with live_streaming_pass, IP- or subnet-bound)
|
|
is accepted by live, vod, timeshift and thumb.
|
|
Auth failures call `generate404()`: HTTP 404 with the nginx-style HTML page (src/Core/Error/ErrorResponder.php:119,235).
|
|
paths:
|
|
/api.php?action=fpm_status:
|
|
post:
|
|
operationId: Internal_fpmStatus
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Raw PHP-FPM / local status page
|
|
description: |
|
|
Echoes `file_get_contents('http://127.0.0.1:<http_broadcast_port>/status')`
|
|
(src/Public/Controllers/Api/InternalApiController.php:45-48). See `/status`.
|
|
HTTP route removed in this build: 404 (src/Public/index.php:185-187). It runs as a `node.rpc` command.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- fpm_status
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Body of the local `/status` request, passed through unchanged.
|
|
content:
|
|
text/plain:
|
|
schema:
|
|
type: string
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=restore_images:
|
|
post:
|
|
operationId: Internal_restoreImages
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Re-download missing images in the background
|
|
description: |
|
|
Runs `console.php tools images` detached (src/Public/Controllers/Api/InternalApiController.php:50-53).
|
|
It returns no body. HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- restore_images
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Empty body.
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=reload_nginx:
|
|
post:
|
|
operationId: Internal_reloadNginx
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Reload nginx and nginx_rtmp
|
|
description: |
|
|
Runs `nginx_rtmp -s reload` then `nginx -s reload` (src/Public/Controllers/Api/InternalApiController.php:55-59).
|
|
It returns no body. HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- reload_nginx
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Empty body.
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=streams_ramdisk:
|
|
post:
|
|
operationId: Internal_streamsRamdisk
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Bytes on the streams ramdisk per stream id
|
|
description: |
|
|
Parses `ls -l STREAMS_PATH`, summing the size of files named `<id>_<x>` per stream id
|
|
(src/Public/Controllers/Api/InternalApiController.php:61-92). `set_time_limit(30)`.
|
|
Quirk: `streams` is an object keyed by stream id, but `[]` (an empty array) when nothing matches.
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- streams_ramdisk
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Totals.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
result:
|
|
type: boolean
|
|
const: true
|
|
streams:
|
|
description: Map of stream id to total bytes; `[]` when empty.
|
|
oneOf:
|
|
- type: object
|
|
additionalProperties:
|
|
type: integer
|
|
- type: array
|
|
maxItems: 0
|
|
examples:
|
|
some:
|
|
value:
|
|
result: true
|
|
streams:
|
|
'12': 4823104
|
|
'57': 1310720
|
|
none:
|
|
value:
|
|
result: true
|
|
streams: []
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=vod:
|
|
post:
|
|
operationId: Internal_vod
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Start or stop movie (VOD) encodes
|
|
description: |
|
|
`function=start`: for each id, `StreamProcess::stopMovie(id, true)`. Then `startMovie` if `force` is truthy,
|
|
else `queueMovie`. `function=stop`: `stopMovie(id)`.
|
|
Source: src/Public/Controllers/Api/InternalApiController.php:94-124.
|
|
Quirk: when `stream_ids` or `function` is empty, or `function` is neither start nor stop, it falls through to
|
|
`rtmp_stats` and returns the RTMP stats JSON instead. HTTP route removed in this build: 404.
|
|
MAIN sends it via `NodeRpc::broadcast` from `/admin/api?action=vod`.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- vod
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- stream_ids
|
|
- function
|
|
properties:
|
|
stream_ids:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: Movie stream ids (`stream_ids[]=`), intval'd.
|
|
function:
|
|
type: string
|
|
enum:
|
|
- start
|
|
- stop
|
|
force:
|
|
type: string
|
|
description: 'start only: truthy runs startMovie now instead of queueing.'
|
|
responses:
|
|
'200':
|
|
description: '`{"result":true}`, or the rtmp_stats body on fall-through.'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=rtmp_stats:
|
|
post:
|
|
operationId: Internal_rtmpStats
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: nginx-rtmp /stat as JSON
|
|
description: |
|
|
`json_encode(ServerRepository::getLocalRTMPStats())`: the RTMP module's `/stat` XML converted to an array
|
|
(src/Public/Controllers/Api/InternalApiController.php:127-130; src/Domain/Server/ServerRepository.php:565-571).
|
|
The value is `null` when the stat page cannot be read. HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- rtmp_stats
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: The XML-to-JSON tree of nginx-rtmp `/stat` (element names as keys, text values as strings).
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type:
|
|
- object
|
|
- 'null'
|
|
example:
|
|
nginx_version: 1.24.0
|
|
uptime: '86400'
|
|
server:
|
|
application:
|
|
name: live
|
|
live:
|
|
nclients: '1'
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=kill_pid:
|
|
post:
|
|
operationId: Internal_killPid
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: SIGKILL a process
|
|
description: |
|
|
`posix_kill(intval(pid), 9)` (src/Public/Controllers/Api/InternalApiController.php:132-142).
|
|
It returns `{"result":false}` when pid ≤ 0. The return value of `posix_kill` is ignored.
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- kill_pid
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- pid
|
|
properties:
|
|
pid:
|
|
type: integer
|
|
responses:
|
|
'200':
|
|
description: Result.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
examples:
|
|
ok:
|
|
value:
|
|
result: true
|
|
badPid:
|
|
value:
|
|
result: false
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=rtmp_kill:
|
|
post:
|
|
operationId: Internal_rtmpKill
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Drop an RTMP publisher
|
|
description: |
|
|
Fires `wget <rtmp_mport_url>control/drop/publisher?app=live&name=<name>` in the background
|
|
(src/Public/Controllers/Api/InternalApiController.php:144-149). It always returns `{"result":true}`.
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- rtmp_kill
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- name
|
|
properties:
|
|
name:
|
|
type: string
|
|
description: RTMP stream name (escapeshellcmd'd).
|
|
responses:
|
|
'200':
|
|
description: Always true.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=stream:
|
|
post:
|
|
operationId: Internal_stream
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Start or stop live streams
|
|
description: |
|
|
`function=start`: `StreamProcess::startMonitor(id, 1)`, sleeping 50 ms after each PHP-monitor spawn.
|
|
`function=stop`: `StreamProcess::stopStream(id, true)`.
|
|
Source: src/Public/Controllers/Api/InternalApiController.php:151-183.
|
|
Quirk: when `stream_ids` or `function` is empty, or `function` is unknown, it falls through to `stats` and
|
|
returns the system stats object. HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- stream
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- stream_ids
|
|
- function
|
|
properties:
|
|
stream_ids:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
function:
|
|
type: string
|
|
enum:
|
|
- start
|
|
- stop
|
|
responses:
|
|
'200':
|
|
description: '`{"result":true}`, or the `stats` object on fall-through.'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=stats:
|
|
post:
|
|
operationId: Internal_stats
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Server hardware and traffic statistics
|
|
description: |
|
|
`json_encode(SystemInfo::getStats())` (src/Public/Controllers/Api/InternalApiController.php:186-189;
|
|
src/Core/Util/SystemInfo.php:30-90). HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- stats
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Stats.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Stats'
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=force_stream:
|
|
post:
|
|
operationId: Internal_forceStream
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Force a stream onto a given source index
|
|
description: |
|
|
For `stream_id > 0`: `FanoutClient::forceSource(stream_id, force_id)`. When that fails, it writes
|
|
`SIGNALS_TMP_PATH/<stream_id>.force` for the PHP monitor
|
|
(src/Public/Controllers/Api/InternalApiController.php:191-204). It always returns `{"result":true}`.
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- force_stream
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- stream_id
|
|
- force_id
|
|
properties:
|
|
stream_id:
|
|
type: integer
|
|
force_id:
|
|
type: integer
|
|
description: Source index to switch to.
|
|
responses:
|
|
'200':
|
|
description: Always true.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=closeConnection:
|
|
post:
|
|
operationId: Internal_closeConnection
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Close a viewer connection
|
|
description: |
|
|
`ConnectionTracker::closeConnection(ConnectionTracker::reference(activity_id))`
|
|
(src/Public/Controllers/Api/InternalApiController.php:206-209). It always returns `{"result":true}`.
|
|
`activity_id` is a string reference (a connection uuid or a numeric id), not necessarily an integer.
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- closeConnection
|
|
description: Action name (camelCase).
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
activity_id:
|
|
type: string
|
|
description: Connection reference; defaults to empty string.
|
|
responses:
|
|
'200':
|
|
description: Always true.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=scandir_recursive:
|
|
post:
|
|
operationId: Internal_scandirRecursive
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Recursive file listing under a scan root
|
|
description: |
|
|
Source: src/Public/Controllers/Api/InternalApiController.php:211-246. `set_time_limit(30)`.
|
|
`dir` must exist and lie under `lb_scan_roots` (ClusterSettings::pathAllowed), else `{"result":false}`.
|
|
`allowed` must match `^[A-Za-z0-9]{1,16}(\|[A-Za-z0-9]{1,16}){0,63}$`, else `{"result":false}` (findCommand :383).
|
|
|
|
Response forms:
|
|
- Without `stat`: a JSON array of paths from `find`.
|
|
- With `stat`: a page `{files:{path:mtime}, next}`, paths sorted, under 60000 bytes (LISTING_PAGE_BYTES :399, findPage :411-429; sized for a 64 KiB cap that is now 4 MiB).
|
|
`files` is always an object (`{}` when empty). `next` is the last path of the page, or `null` on the last page.
|
|
- With `stat`, `size` and `dir` naming a file (or a missing file whose folder is allowed): `{files, sizes, next:null}`
|
|
(fileStat :437-443). Both maps are `{}` when the file is gone.
|
|
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- scandir_recursive
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- dir
|
|
properties:
|
|
dir:
|
|
type: string
|
|
description: Directory (urldecoded).
|
|
allowed:
|
|
type: string
|
|
description: Pipe-separated extensions, e.g. `mp4|mkv` (urldecoded).
|
|
stat:
|
|
type: string
|
|
description: Non-empty to get mtimes and paging.
|
|
after:
|
|
type: string
|
|
description: 'With stat: path cursor (previous `next`).'
|
|
size:
|
|
type: string
|
|
description: 'With stat: `dir` is one file; also return its size.'
|
|
responses:
|
|
'200':
|
|
description: Listing (one of three shapes) or failure.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
oneOf:
|
|
- type: array
|
|
items:
|
|
type: string
|
|
- type: object
|
|
required:
|
|
- files
|
|
- next
|
|
properties:
|
|
files:
|
|
type: object
|
|
additionalProperties:
|
|
type: integer
|
|
sizes:
|
|
type: object
|
|
additionalProperties:
|
|
type: integer
|
|
next:
|
|
type:
|
|
- string
|
|
- 'null'
|
|
- $ref: '#/components/schemas/Internal_Result'
|
|
examples:
|
|
plain:
|
|
value:
|
|
- /media/movies/a.mkv
|
|
- /media/movies/b/c.mp4
|
|
statPage:
|
|
value:
|
|
files:
|
|
/media/movies/a.mkv: 1760600000
|
|
next: null
|
|
fileSize:
|
|
value:
|
|
files:
|
|
/media/movies/a.mkv: 1760600000
|
|
sizes:
|
|
/media/movies/a.mkv: 1048576000
|
|
next: null
|
|
refused:
|
|
value:
|
|
result: false
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=scandir:
|
|
post:
|
|
operationId: Internal_scandir
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: One-level directory listing under a scan root
|
|
description: |
|
|
Source: src/Public/Controllers/Api/InternalApiController.php:248-280.
|
|
`dir` (urldecoded) must exist and be under `lb_scan_roots`, else `{"result":false}`.
|
|
Files are filtered by `allowed`, a pipe list compared case-sensitively against the *lowercased* extension.
|
|
Directories are never filtered. HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- scandir
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- dir
|
|
properties:
|
|
dir:
|
|
type: string
|
|
allowed:
|
|
type: string
|
|
description: e.g. `mp4|mkv|avi`
|
|
responses:
|
|
'200':
|
|
description: Listing.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
oneOf:
|
|
- type: object
|
|
properties:
|
|
result:
|
|
type: boolean
|
|
const: true
|
|
dirs:
|
|
type: array
|
|
items:
|
|
type: string
|
|
files:
|
|
type: array
|
|
items:
|
|
type: string
|
|
- $ref: '#/components/schemas/Internal_Result'
|
|
examples:
|
|
ok:
|
|
value:
|
|
result: true
|
|
dirs:
|
|
- Season 1
|
|
files:
|
|
- movie.mkv
|
|
refused:
|
|
value:
|
|
result: false
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=get_free_space:
|
|
post:
|
|
operationId: Internal_getFreeSpace
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: '`df -h` output lines'
|
|
description: |
|
|
`exec('df -h')`, returned as an array of raw lines including the header (src/Public/Controllers/Api/InternalApiController.php:282-286).
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- get_free_space
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Lines.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: string
|
|
example:
|
|
- Filesystem Size Used Avail Use% Mounted on
|
|
- /dev/sda1 98G 21G 73G 23% /
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=get_pids:
|
|
post:
|
|
operationId: Internal_getPids
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Process list (redacted)
|
|
description: |
|
|
`ps -e -o user,pid,%cpu,%mem,vsz,rss,tty,stat,time,etime,command`. Each line goes through `Redactor::redact`,
|
|
which masks credentials in argv (src/Public/Controllers/Api/InternalApiController.php:288-294).
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- get_pids
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Lines, header first.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: string
|
|
example:
|
|
- USER PID %CPU %MEM VSZ RSS TT STAT TIME ELAPSED COMMAND
|
|
- xc_vm 1234 2.0 0.5 123456 45678 ? Ssl 00:01:02 01:00:00 /home/xc_vm/bin/ffmpeg ...
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=free_temp:
|
|
post:
|
|
operationId: Internal_freeTemp
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Clear tmp/ and rebuild caches
|
|
description: |
|
|
`rm -rf MAIN_HOME/tmp/*` then `console.php cron:cache` (synchronous)
|
|
(src/Public/Controllers/Api/InternalApiController.php:296-301). HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- free_temp
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Always true.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=free_streams:
|
|
post:
|
|
operationId: Internal_freeStreams
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Delete everything in content/streams
|
|
description: |
|
|
`rm MAIN_HOME/content/streams/*` (src/Public/Controllers/Api/InternalApiController.php:303-307). Running streams lose their segments.
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- free_streams
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Always true.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=signal_send:
|
|
post:
|
|
operationId: Internal_signalSend
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Push an on-screen message to a viewer connection
|
|
description: |
|
|
When `message` and `uuid` are both non-empty, the request gets `type=signal`. It is then passed to
|
|
`FanoutClient::sendSignal(uuid, request)` (burned into the viewer's next segment when fanout is on), and the whole
|
|
request is written as JSON to `SIGNALS_PATH/<uuid>` (src/Public/Controllers/Api/InternalApiController.php:309-321).
|
|
It returns no body in either case. HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- signal_send
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- message
|
|
- uuid
|
|
properties:
|
|
message:
|
|
type: string
|
|
uuid:
|
|
type: string
|
|
description: Connection uuid.
|
|
responses:
|
|
'200':
|
|
description: Empty body.
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=get_certificate_info:
|
|
post:
|
|
operationId: Internal_getCertificateInfo
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: TLS certificate details
|
|
description: |
|
|
`DiagnosticsService::getCertificateInfo()` (src/Public/Controllers/Api/InternalApiController.php:323-326;
|
|
src/Core/Diagnostics/DiagnosticsService.php:32). The value is `null` when no certificate is configured or readable.
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- get_certificate_info
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Certificate info or null.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type:
|
|
- object
|
|
- 'null'
|
|
properties:
|
|
serial:
|
|
type: string
|
|
expiration:
|
|
type: integer
|
|
description: Unix time.
|
|
subject:
|
|
type: string
|
|
path:
|
|
type: string
|
|
example:
|
|
serial: 04A1B2C3D4E5F6
|
|
expiration: 1768000000
|
|
subject: CN=panel.example.com
|
|
path: /home/xc_vm/bin/certificates/fullchain.pem
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=get_archive_files:
|
|
post:
|
|
operationId: Internal_getArchiveFiles
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: TV-archive segment files of a stream
|
|
description: |
|
|
`glob(ARCHIVE_PATH/<stream_id>/*.ts)` (src/Public/Controllers/Api/InternalApiController.php:328-332).
|
|
File names are `Y-m-d:H-i.ts` (one per minute). HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- get_archive_files
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- stream_id
|
|
properties:
|
|
stream_id:
|
|
type: integer
|
|
responses:
|
|
'200':
|
|
description: Absolute paths.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
result:
|
|
type: boolean
|
|
const: true
|
|
data:
|
|
type: array
|
|
items:
|
|
type: string
|
|
example:
|
|
result: true
|
|
data:
|
|
- /home/xc_vm/content/archive/12/2026-10-17:08-00.ts
|
|
- /home/xc_vm/content/archive/12/2026-10-17:08-01.ts
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=kill_plex:
|
|
post:
|
|
operationId: Internal_killPlex
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: Kill the Plex sync process group
|
|
description: |
|
|
Kills the process group in `CACHE_TMP_PATH/plex_pid` and the `WATCH_TMP_PATH/*.ppid` children
|
|
(src/Public/Controllers/Api/InternalApiController.php:334-337, killProcessGroup :349). It always returns true.
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- kill_plex
|
|
description: Action name.
|
|
responses:
|
|
'200':
|
|
description: Always true.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/api.php?action=probe:
|
|
post:
|
|
operationId: Internal_probe
|
|
deprecated: true
|
|
tags:
|
|
- Internal Server API
|
|
summary: ffprobe a source URL from this server
|
|
description: |
|
|
`probeStream` (src/Public/Controllers/Api/InternalApiController.php:339-342, 462-501).
|
|
- An empty `url` gives `{"result":false}`. A target refused by `NetworkUtils::probeTargetAllowed` (SSRF guard)
|
|
gives `{"result":false,"error":"Invalid probe target."}`.
|
|
- Platform pages (YouTube, ...) are resolved first through `http_proxy`.
|
|
- The header `X-XC_VM-Prebuffer:1` is always appended to `headers`.
|
|
- `data` is ffprobe's raw JSON (`{streams, format}`) or `null` when ffprobe fails
|
|
(src/Streaming/Codec/FFprobeRunner.php:16, parse=false).
|
|
|
|
HTTP route removed in this build: 404.
|
|
parameters:
|
|
- $ref: '#/components/parameters/Internal_Password'
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- probe
|
|
description: Action name.
|
|
requestBody:
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- url
|
|
properties:
|
|
url:
|
|
type: string
|
|
http_proxy:
|
|
type: string
|
|
user_agent:
|
|
type: string
|
|
cookies:
|
|
type: string
|
|
headers:
|
|
type: string
|
|
description: CRLF-separated extra headers.
|
|
responses:
|
|
'200':
|
|
description: Probe result.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
result:
|
|
type: boolean
|
|
error:
|
|
type: string
|
|
data:
|
|
type:
|
|
- object
|
|
- 'null'
|
|
description: Raw ffprobe -print_format json output.
|
|
examples:
|
|
ok:
|
|
value:
|
|
result: true
|
|
data:
|
|
streams:
|
|
- index: 0
|
|
codec_name: h264
|
|
codec_type: video
|
|
width: 1920
|
|
height: 1080
|
|
format:
|
|
format_name: mpegts
|
|
bit_rate: '4500000'
|
|
refused:
|
|
value:
|
|
result: false
|
|
error: Invalid probe target.
|
|
noUrl:
|
|
value:
|
|
result: false
|
|
'404':
|
|
$ref: '#/components/responses/Internal_RouteRemoved'
|
|
/status:
|
|
get:
|
|
operationId: Internal_status
|
|
tags:
|
|
- Internal Server API
|
|
summary: Local status probe (loopback only)
|
|
description: |
|
|
nginx `location ~ ^/status$` allows 127.0.0.1 only and gives everything else nginx's 403
|
|
(src/bin/nginx/conf/nginx.conf:283-297). It passes `XC_STREAM=status` to src/Public/stream/index.php:24-27,
|
|
which runs `StreamingRequestBootstrap::init('status')` and exits with an empty 200.
|
|
`status` is exempt from the fail-closed settings gate (src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php:93).
|
|
The FPM pools also declare `pm.status_path = /status` (src/bin/php/etc/1.conf:17). Since nginx overrides
|
|
`SCRIPT_NAME` to `/public/stream/index.php`, the FPM status page is probably not what answers.
|
|
Read by the `fpm_status` action.
|
|
responses:
|
|
'200':
|
|
description: Empty body (or FPM's plain-text pool status, see description).
|
|
content:
|
|
text/plain:
|
|
schema:
|
|
type: string
|
|
'403':
|
|
description: nginx 403 for non-loopback clients.
|
|
/xfile:
|
|
get:
|
|
operationId: Internal_xfile
|
|
tags:
|
|
- Internal Server API
|
|
summary: Ticketed file chunk fetch between servers (replaces api getFile)
|
|
description: |
|
|
Route: nginx `location = /xfile` (`limit_req zone=xfile burst=100`, 429) sets `XC_ADMIN=xfile`
|
|
(src/bin/nginx/conf/nginx.conf:330-346). It reaches src/Public/admin/xfile.php, then
|
|
`FileTicketController::handle` (src/Public/Controllers/Api/FileTicketController.php:16-27), then
|
|
`FileTicketServer::serve` (src/Core/Cluster/FileTicketServer.php:57-130).
|
|
|
|
Auth:
|
|
- Method must be GET.
|
|
- `X-XCVM-File` carries a panel-signed `fil` ticket (fetcher_sid, fetcher_gen, owner_sid = this server, ref, file, tid).
|
|
- `X-XCVM-File-Auth` is the fetcher's Ed25519 proof `<ts_ms>.<32-hex nonce>.<86-char b64url sig>` over ticket,
|
|
method and request target. It must be within ±90 s, and the nonce is single-use (src/Core/Cluster/Crypto/RelayAuth.php:40).
|
|
|
|
File checks: the file must have an allowed extension (FileTicketServer.php:44: log gz zip m3u8 mp4 mkv avi mpg flv 3gp
|
|
m4v wmv mov ts srt sub sbv jpg png bmp jpeg gif tif) and lie under `lb_scan_roots` (or MAIN's own sealed paths).
|
|
|
|
Any auth or path failure gives a bare 404.
|
|
parameters:
|
|
- name: o
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
minimum: 0
|
|
description: Byte offset.
|
|
- name: 'n'
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 4194304
|
|
description: Chunk length (≤ FileDigest::CHUNK).
|
|
- name: rk
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: RelaySeal sealed session key. When given, the chunk is framed and encrypted under it.
|
|
- name: X-XCVM-File
|
|
in: header
|
|
required: true
|
|
schema:
|
|
type: string
|
|
description: Panel-signed `fil` ticket.
|
|
- name: X-XCVM-File-Auth
|
|
in: header
|
|
required: true
|
|
schema:
|
|
type: string
|
|
pattern: ^\d+\.[0-9a-f]{32}\.[A-Za-z0-9_-]{86}$
|
|
description: Fetcher proof.
|
|
responses:
|
|
'200':
|
|
description: The chunk (shorter than `n` at EOF; empty when `o` equals the size).
|
|
headers:
|
|
X-XCVM-File-Digest:
|
|
schema:
|
|
type: string
|
|
description: Server-signed digest over (tid, server, offset, total, bytes, time, nonce).
|
|
X-XCVM-Relay-Seal:
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- v1
|
|
description: Present when `rk` sealed the body.
|
|
Cache-Control:
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- no-store
|
|
content:
|
|
application/octet-stream:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'400':
|
|
description: Bad `o` or `n` (empty body).
|
|
'404':
|
|
description: Ticket, proof, nonce, path or seal refused (empty body).
|
|
'416':
|
|
description: '`o` > file size. Header `Content-Range: bytes */<size>`.'
|
|
'429':
|
|
description: nginx rate limit.
|
|
'503':
|
|
description: Server cannot sign the digest (empty body).
|
|
/admin/api?action=server&sub=list:
|
|
get:
|
|
operationId: LegacyAdmin_serverList
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: List servers
|
|
description: |
|
|
src/Public/admin/api.php:43-56. Auth (api.php:26-34) is shared by all `/admin/api` calls:
|
|
the client IP must be in `getAllowedIPs()` or the `api_ips` setting, and `api_pass` must match the setting when the setting is non-empty.
|
|
Otherwise the reply is 404 (`generate404`). Parameters come from GET or POST (RequestManager).
|
|
An unknown action/sub gives an empty 200.
|
|
parameters:
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- server
|
|
description: Action.
|
|
- name: sub
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- list
|
|
description: Sub-action.
|
|
- $ref: '#/components/parameters/LegacyAdmin_ApiPass'
|
|
responses:
|
|
'200':
|
|
description: One entry per server.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
server_name:
|
|
type: string
|
|
online:
|
|
type:
|
|
- string
|
|
- integer
|
|
description: '`server_online` as stored (often "1"/"0").'
|
|
info:
|
|
type:
|
|
- object
|
|
- 'null'
|
|
description: Decoded `server_hardware` JSON.
|
|
example:
|
|
- id: 1
|
|
server_name: Main Server
|
|
online: '1'
|
|
info:
|
|
total_ram: 16384000
|
|
cores: 8
|
|
kernel: 6.8.0
|
|
cpu_name: Intel Xeon
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/api?action=vod:
|
|
get:
|
|
operationId: LegacyAdmin_vodControl
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: Start/stop movies or episodes on servers
|
|
description: |
|
|
src/Public/admin/api.php:63-87. `action=movie` and `action=episode` are aliases of `vod`.
|
|
Ids come from `stream_ids[]`, else `stream_id`. Servers come from `servers[]`, else `server_id`, else all servers.
|
|
Unknown servers are dropped. The call is sent with `NodeRpc::broadcast(servers, {action:vod, function:<sub>, stream_ids[, force]})`:
|
|
a signed `node.rpc` to cluster nodes, the legacy `/api` POST to the rest. It always returns `{"result":true}`.
|
|
Same auth as server list. A `sub` other than start/stop gives an empty 200.
|
|
parameters:
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- vod
|
|
- movie
|
|
- episode
|
|
description: Action (aliases).
|
|
- name: sub
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- start
|
|
- stop
|
|
description: Sub-action.
|
|
- name: stream_ids[]
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: Stream ids.
|
|
- name: stream_id
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
description: Single id when stream_ids is empty.
|
|
- name: servers[]
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: Target servers.
|
|
- name: server_id
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
description: Single server when servers is empty.
|
|
- name: force
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: 'start only: truthy runs at once instead of queueing.'
|
|
- $ref: '#/components/parameters/LegacyAdmin_ApiPass'
|
|
responses:
|
|
'200':
|
|
description: Always true once dispatched.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/api?action=stream:
|
|
get:
|
|
operationId: LegacyAdmin_streamControl
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: Start/stop live streams on servers
|
|
description: |
|
|
src/Public/admin/api.php:93-107. Sends `NodeRpc::broadcast(servers, {action:stream, function:<sub>, stream_ids})`.
|
|
`servers[]` defaults to all servers. It always returns `{"result":true}`, even when `stream_ids` is empty
|
|
(the nodes then answer with stats via fall-through). Same auth as server list.
|
|
parameters:
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- stream
|
|
description: Action.
|
|
- name: sub
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- start
|
|
- stop
|
|
description: Sub-action.
|
|
- name: stream_ids[]
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: Stream ids.
|
|
- name: servers[]
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: integer
|
|
description: Target servers (default all).
|
|
- $ref: '#/components/parameters/LegacyAdmin_ApiPass'
|
|
responses:
|
|
'200':
|
|
description: Always true.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Internal_Result'
|
|
example:
|
|
result: true
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/api?action=stream&sub=list:
|
|
get:
|
|
operationId: LegacyAdmin_streamList
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: List non-movie streams
|
|
description: |
|
|
`SELECT id, stream_display_name FROM streams WHERE type <> 2` (src/Public/admin/api.php:109-118).
|
|
Values are DB strings. Same auth as server list.
|
|
parameters:
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- stream
|
|
description: Action.
|
|
- name: sub
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- list
|
|
description: Sub-action.
|
|
- $ref: '#/components/parameters/LegacyAdmin_ApiPass'
|
|
responses:
|
|
'200':
|
|
description: Streams.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type:
|
|
- string
|
|
- integer
|
|
description: Numeric string from the DB.
|
|
stream_name:
|
|
type: string
|
|
example:
|
|
- id: '12'
|
|
stream_name: News HD
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/api?action=stream&sub=online:
|
|
get:
|
|
operationId: LegacyAdmin_streamStatus
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: Online or offline streams per server
|
|
description: |
|
|
`sub=online`: streams_servers rows with `pid > 0 AND stream_status = 0`. `sub=offline`: rows with `stream_status <> 0`.
|
|
Movies (type 2) are excluded in both (src/Public/admin/api.php:120-142).
|
|
The result is a map of stream id to the list of server ids. Quirk: `[]` (empty array) when there are no rows.
|
|
Same auth as server list.
|
|
parameters:
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- stream
|
|
description: Action.
|
|
- name: sub
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- online
|
|
- offline
|
|
description: Sub-action.
|
|
- $ref: '#/components/parameters/LegacyAdmin_ApiPass'
|
|
responses:
|
|
'200':
|
|
description: Map stream_id to server_ids.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
oneOf:
|
|
- type: object
|
|
additionalProperties:
|
|
type: array
|
|
items:
|
|
type:
|
|
- string
|
|
- integer
|
|
- type: array
|
|
maxItems: 0
|
|
example:
|
|
'12':
|
|
- 1
|
|
- 3
|
|
'57':
|
|
- 1
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/api?action=line&sub=info:
|
|
get:
|
|
operationId: LegacyAdmin_lineInfo
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: Look up a line by credentials
|
|
description: |
|
|
`UserRepository::getUserInfo(false, username, password, true, true)` (src/Public/admin/api.php:147-167;
|
|
src/Domain/User/UserRepository.php:596-610, getUserInfo :639). `user_info` is the `lines` row (DB strings, JSON fields decoded) plus:
|
|
- `output_formats`
|
|
- `channel_ids`, `live_ids`, `vod_ids`, `radio_ids`, `series_ids`
|
|
- `active_cons`
|
|
- `category_ids`
|
|
|
|
Same auth as server list.
|
|
parameters:
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- line
|
|
description: Action.
|
|
- name: sub
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- info
|
|
description: Sub-action.
|
|
- $ref: '#/components/parameters/Username'
|
|
- $ref: '#/components/parameters/Password'
|
|
- $ref: '#/components/parameters/LegacyAdmin_ApiPass'
|
|
responses:
|
|
'200':
|
|
description: Line info or error.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
result:
|
|
type: boolean
|
|
user_info:
|
|
type: object
|
|
additionalProperties: true
|
|
error:
|
|
type: string
|
|
enum:
|
|
- NOT EXISTS
|
|
- PARAMETER ERROR (user/pass)
|
|
examples:
|
|
found:
|
|
value:
|
|
result: true
|
|
user_info:
|
|
id: '5'
|
|
username: john
|
|
password: secret
|
|
exp_date: '1767225600'
|
|
max_connections: '1'
|
|
is_trial: '0'
|
|
enabled: '1'
|
|
admin_enabled: '1'
|
|
bouquet:
|
|
- 1
|
|
- 2
|
|
allowed_outputs:
|
|
- 1
|
|
- 2
|
|
output_formats:
|
|
- m3u8
|
|
- ts
|
|
channel_ids:
|
|
- 12
|
|
- 57
|
|
live_ids:
|
|
- 12
|
|
- 57
|
|
vod_ids: []
|
|
radio_ids: []
|
|
series_ids: []
|
|
active_cons: 0
|
|
category_ids:
|
|
- 3
|
|
missing:
|
|
value:
|
|
result: false
|
|
error: NOT EXISTS
|
|
params:
|
|
value:
|
|
result: false
|
|
error: PARAMETER ERROR (user/pass)
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/api?action=reg_user&sub=list:
|
|
get:
|
|
operationId: LegacyAdmin_regUserList
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: List panel users (admins/resellers)
|
|
description: |
|
|
`users ⋈ users_groups` columns id, username, credits, group_id, group_name, last_login, date_registered, email, ip, status
|
|
(src/Public/admin/api.php:169-179). `credits` goes through `UserCredits::amounts`, so it is numeric (int, or a float
|
|
rounded to 4 places). The other values are DB strings. Same auth as server list.
|
|
parameters:
|
|
- name: action
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- reg_user
|
|
description: Action.
|
|
- name: sub
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- list
|
|
description: Sub-action.
|
|
- $ref: '#/components/parameters/LegacyAdmin_ApiPass'
|
|
responses:
|
|
'200':
|
|
description: Users.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
type: object
|
|
additionalProperties: true
|
|
example:
|
|
- id: '1'
|
|
username: admin
|
|
credits: 0
|
|
group_id: '1'
|
|
group_name: Administrators
|
|
last_login: '1760600000'
|
|
date_registered: '1700000000'
|
|
email: ''
|
|
ip: 10.0.0.5
|
|
status: '1'
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/live:
|
|
get:
|
|
operationId: LegacyAdmin_live
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: Server-to-server / admin-preview live stream
|
|
description: |
|
|
src/Public/admin/live.php. Sends `Access-Control-Allow-Origin: *`.
|
|
|
|
Auth (live.php:37-62):
|
|
- A `uitoken` forces `stream` (from the token) and `extension=m3u8`, with prebuffer `seg_time`.
|
|
- Otherwise `RelayGuard::admit(stream, password)`. A relay-ticket caller asking for `m3u8` gets 404.
|
|
- `prebuffer` or the header `X-XC_VM-Prebuffer` prebuffers `seg_time` seconds.
|
|
|
|
The stream must be assigned to this server, else 404. If it is not running and not on-demand, 404.
|
|
An on-demand stream is started and waited for up to 20 s.
|
|
|
|
Output:
|
|
- `odstart` returns the text `1` or `0` (:164-178). Quirk: the wait loop counts to 2000 polls but the timeout check
|
|
compares against 200, so `0` almost never appears and `1` can come after a timeout.
|
|
- `extension=m3u8` returns a playlist rewritten by `StreamUtils::generateAdminHLS`, with segment URLs
|
|
`/admin/live?password=<pass>&extension=m3u8&segment=<n>.ts&stream=<id>` or `...&uitoken=<t>` (:185-195).
|
|
`segment=` returns that `.ts` file (:196-206).
|
|
- Otherwise a continuous MPEG-TS feed (:212-341). It is RelaySeal-framed when a relay passes `rk` (:66-72).
|
|
parameters:
|
|
- name: stream
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
description: Stream id (required unless uitoken).
|
|
- name: extension
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- ts
|
|
- m3u8
|
|
description: Output; anything but m3u8 is TS.
|
|
- name: segment
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: 'With m3u8: segment file name.'
|
|
- name: prebuffer
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Presence enables prebuffer.
|
|
- name: odstart
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Presence returns 1/0 after starting.
|
|
- $ref: '#/components/parameters/LegacyAdmin_LivePassword'
|
|
- $ref: '#/components/parameters/LegacyAdmin_UIToken'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayKey'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayTicket'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayAuth'
|
|
- name: X-XC_VM-Prebuffer
|
|
in: header
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Any value enables prebuffer.
|
|
responses:
|
|
'200':
|
|
description: TS stream, HLS playlist, segment, or `1`/`0` for odstart.
|
|
content:
|
|
video/mp2t:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
application/vnd.apple.mpegurl:
|
|
schema:
|
|
type: string
|
|
example: |
|
|
#EXTM3U
|
|
#EXT-X-VERSION:3
|
|
#EXT-X-TARGETDURATION:10
|
|
#EXT-X-MEDIA-SEQUENCE:120
|
|
#EXTINF:10.000000,
|
|
/admin/live?password=PASS&extension=m3u8&segment=12_120.ts&stream=12
|
|
text/html:
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- '0'
|
|
- '1'
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/vod:
|
|
get:
|
|
operationId: LegacyAdmin_vod
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: Movie file with Range support
|
|
description: |
|
|
src/Public/admin/vod.php. Sends `Access-Control-Allow-Origin: *`.
|
|
Auth: a `uitoken` rewrites `stream` to `<id>.<container>`. Otherwise `RelayGuard::admit(<id from stream>, password)` (:27-38).
|
|
|
|
Serves `VOD_PATH/<id>.<ext>` with the container mime type. It always sends `Accept-Ranges: 0-<size>` (non-standard)
|
|
and `Content-Range`. A single range gives 206; a bad or multi-range request gives 416 with `Content-Range` (:56-110).
|
|
|
|
Quirks:
|
|
- Suffix ranges (`bytes=-N`) are mis-parsed: the `$range == '-'` test never matches them, so they become start=''.
|
|
- An unknown movie or missing file falls through to an **empty 200**.
|
|
parameters:
|
|
- name: stream
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: '`<id>.<ext>`, e.g. `345.mkv` (required unless uitoken).'
|
|
- $ref: '#/components/parameters/LegacyAdmin_LivePassword'
|
|
- $ref: '#/components/parameters/LegacyAdmin_UIToken'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayTicket'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayAuth'
|
|
- name: Range
|
|
in: header
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Single `bytes=a-b` range.
|
|
responses:
|
|
'200':
|
|
description: Whole file (or empty body when not found).
|
|
content:
|
|
video/mp4:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
video/x-matroska:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'206':
|
|
description: Partial content.
|
|
content:
|
|
application/octet-stream:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
'416':
|
|
description: Range not satisfiable; `Content-Range` set.
|
|
/admin/timeshift:
|
|
get:
|
|
operationId: LegacyAdmin_timeshift
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: TV-archive playback (HLS or concatenated TS)
|
|
description: |
|
|
src/Public/admin/timeshift.php. Sends `Access-Control-Allow-Origin: *`.
|
|
Auth: `uitoken`, or `RelayGuard::admit(stream, password)`. A relay asking for m3u8 gets 404 (:33-56).
|
|
|
|
Without `segment`, the first minute file `ARCHIVE_PATH/<id>/<Y-m-d:H-i>.ts` must exist, else 404. Up to `duration`
|
|
one-minute files are queued (missing ones skipped); none found gives 404 (:85-113).
|
|
|
|
Output:
|
|
- `extension=m3u8` returns a VOD playlist with `#EXTINF:60.0,` per minute. Segment URLs are
|
|
`/admin/timeshift?extension=m3u8&stream=<id>&segment=<file>&password=<live_streaming_pass>` or
|
|
`...&segment=<file>&uitoken=<t>` (:116-139). With `segment`, the file is served as video/mp2t, or 404.
|
|
- `extension=ts` returns the minutes concatenated, with single-range support (206/416) (:155-240).
|
|
|
|
Quirks:
|
|
- The uitoken branch updates RequestManager *after* `$rRequestData` was copied (:26 vs :40-49). The token's
|
|
stream, extension, start and duration are therefore ignored, and the caller must pass them explicitly.
|
|
- The stream not on this server gives 404.
|
|
parameters:
|
|
- name: stream
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
description: Stream id.
|
|
- name: extension
|
|
in: query
|
|
required: true
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- m3u8
|
|
- ts
|
|
description: Output format.
|
|
- name: start
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Unix time, `YYYYMMDD-HH`, or `YYYY-MM-DD:HH-MM` (StreamUtils::timeshiftStartTimestamp, src/Core/Util/StreamUtils.php:566-581). Required without segment.
|
|
- name: duration
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
description: Minutes. Required without segment.
|
|
- name: segment
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
description: Archive file name, e.g. `2026-10-17:08-00.ts`.
|
|
- $ref: '#/components/parameters/LegacyAdmin_LivePassword'
|
|
- $ref: '#/components/parameters/LegacyAdmin_UIToken'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayTicket'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayAuth'
|
|
responses:
|
|
'200':
|
|
description: Playlist, segment or TS.
|
|
content:
|
|
application/x-mpegurl:
|
|
schema:
|
|
type: string
|
|
example: |-
|
|
#EXTM3U
|
|
#EXT-X-VERSION:3
|
|
#EXT-X-TARGETDURATION:60
|
|
#EXT-X-MEDIA-SEQUENCE:0
|
|
#EXT-X-PLAYLIST-TYPE:VOD
|
|
#EXTINF:60.0,
|
|
/admin/timeshift?extension=m3u8&stream=12&segment=2026-10-17:08-00.ts&password=PASS
|
|
#EXT-X-ENDLIST
|
|
video/mp2t:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'206':
|
|
description: Partial TS.
|
|
content:
|
|
video/mp2t:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
'416':
|
|
description: Range not satisfiable.
|
|
/admin/thumb:
|
|
get:
|
|
operationId: LegacyAdmin_thumb
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: Live thumbnail
|
|
description: |
|
|
src/Public/admin/thumb.php. Auth: `uitoken`, or a relay ticket only. `password` is **not** accepted
|
|
(`RelayGuard::admit(..., null, ..., false)`, :28-39).
|
|
- On the stream's `vframes_server_id`: `STREAMS_PATH/<id>_.jpg` is returned if it is under 60 s old, with `Age`. Otherwise 404 (:52-61).
|
|
- On any other server: 302 to `<vframes site_url>admin/thumb?stream=<id>&aid=<aid>&uitoken=<t>&expires=<e>` (:63-66).
|
|
parameters:
|
|
- name: stream
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
description: Stream id (from uitoken if given).
|
|
- name: aid
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
description: Passed through on redirect.
|
|
- name: expires
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
description: Passed through on redirect.
|
|
- $ref: '#/components/parameters/LegacyAdmin_UIToken'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayTicket'
|
|
- $ref: '#/components/parameters/LegacyAdmin_RelayAuth'
|
|
responses:
|
|
'200':
|
|
description: JPEG (Content-Type `image/jpg` as sent).
|
|
headers:
|
|
Age:
|
|
schema:
|
|
type: integer
|
|
description: Seconds since the frame was written.
|
|
content:
|
|
image/jpg:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'302':
|
|
description: Redirect to the vframes server.
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|
|
/admin/proxy_api:
|
|
post:
|
|
operationId: LegacyAdmin_proxyApi
|
|
tags:
|
|
- Legacy Admin Stream Endpoints
|
|
summary: Proxy server stats report; returns pending signals
|
|
description: |
|
|
MAIN. src/Public/admin/proxy_api.php.
|
|
|
|
Signed form (:31-40):
|
|
- `X-XCVM-Proxy-Auth: <ts_ms>.<32-hex nonce>.<64-hex mac>`.
|
|
- mac = HMAC-SHA256(K, `xcvm proxy req v1|<server_id>|<ts>|<nonce>|<sha256(body)>`), within ±90 s, nonce single-use.
|
|
- K = HMAC-SHA256(proxy_secret, `xcvm proxy key v1|<server_id>|<gen>`) (src/Domain/Server/ProxyKey.php).
|
|
- After the first signed request the proxy is marked `proxy_signed` and unsigned requests are refused.
|
|
|
|
Unsigned (legacy) form (:41-54): the source address must be a known proxy IP, and `server_id` must resolve to it
|
|
(ProxyIdentity::resolve).
|
|
|
|
Side effects: inserts `servers_stats` and updates `servers` (connections, users, ping, server_hardware, interfaces,
|
|
watchdog_data). Then it returns and **deletes** the server's pending `signals` (excluding OPENSSL_EXTRA rows) (:103-118).
|
|
- Unsigned reply: a JSON array of the signals' `custom_data` objects.
|
|
- Signed reply: `{"payload":"<that array as a JSON string>","mac":"<hex>"}`, where mac = HMAC(K, `xcvm proxy resp v1|<server_id>|<nonce>|<payload>`).
|
|
|
|
Quirk: if the signals query fails, the body is empty. Unauthorised requests get 404.
|
|
parameters:
|
|
- name: X-XCVM-Proxy-Auth
|
|
in: header
|
|
required: false
|
|
schema:
|
|
type: string
|
|
pattern: ^\d+\.[0-9a-f]{32}\.[0-9a-f]{64}$
|
|
description: Signed-mode header.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/x-www-form-urlencoded:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- server_id
|
|
- stats
|
|
properties:
|
|
server_id:
|
|
type: integer
|
|
stats:
|
|
type: object
|
|
description: 'Sent as `stats[cpu]=...`. Keys used: cpu, cpu_cores, cpu_avg, total_mem, total_mem_free, total_mem_used, total_mem_used_percent, total_disk_space, uptime, total_running_streams, bytes_sent_total, bytes_received_total, cpu_load_average, kernel, cpu_name, network_speed, interfaces.'
|
|
additionalProperties: true
|
|
addresses:
|
|
description: Addresses the proxy sees itself on (telemetry, `ips_seen`).
|
|
responses:
|
|
'200':
|
|
description: Pending signals.
|
|
content:
|
|
application/json:
|
|
schema:
|
|
oneOf:
|
|
- type: array
|
|
items:
|
|
type: object
|
|
additionalProperties: true
|
|
- type: object
|
|
properties:
|
|
payload:
|
|
type: string
|
|
mac:
|
|
type: string
|
|
examples:
|
|
unsigned:
|
|
value:
|
|
- action: block_ip
|
|
ip: 203.0.113.9
|
|
signed:
|
|
value:
|
|
payload: '[]'
|
|
mac: 9f2c...e1
|
|
'404':
|
|
$ref: '#/components/responses/LegacyAdmin_NotFound'
|