ptp-proxy — IPTV, files and media processing IPTV, storage and managed media node
ptp-proxy combines IPTV delivery, shared restreaming, transcoding, VOD tunnelling, recordings and secure publication of local or remote files in one service. It can run by itself or connect to a PTP server as a managed node.
- Hide IPTV-provider and file-origin credentials
- Publish HLS channels behind your own URLs
- Share one restream connection among several players
- Apply software or hardware transcoding profiles
- Tunnel VOD and downloads with byte ranges and seek
- Serve local folders, disks and mounted drives
- Unify WebDAV, SFTP, SMB, NFS, FTP/FTPS and HTTP catalogues
- Connect external providers such as S3, BitTorrent, aMule, rclone, IPFS or SABnzbd
- Prepare content on demand through asynchronous operations
- Schedule recordings from channels or URLs
- Run as a local or remote PTP-managed node
- Run as a Windows Service or Linux daemon
Clients use ptp-proxy URLs for channels, VOD or files. The server validates identity and permissions, contacts the configured origin, transforms media when required and returns a uniform response without revealing origin secrets.
- It does not include third-party channels, films, series or files
- It does not discover services or credentials automatically: the administrator adds authorised sources
- It does not replace a VPN: it may use an existing network or tunnel but does not create one
- It does not make addon content public: every provider remains subject to permissions, limits and source policy
Operating modes
| Mode | What it solves | Data flow | FFmpeg |
|---|---|---|---|
| HLS proxy | Rewrites playlists, segments and keys without re-encoding video. | Each client keeps its own session with the origin. | No |
| Restream | Copies or transcodes a channel and distributes one shared output. | One channel session can serve several clients. | Yes |
| VOD tunnel | Delivers remote video files with size, download and seek support. | Each viewer receives an independent byte-range stream. | No |
| File server | Publishes local, mounted or remote files through one API. | Client → ptp-proxy → storage source. | No |
| On-demand preparation | Starts a remote job and publishes the result when it becomes available. | Client → operation → external provider → ready object. | Optional |
| PTP node | Exposes sources, profiles and operations to a central PTP server. | The node initiates control; delivery may be direct or relayed. | Task-dependent |
What you can build with ptp-proxy
The same features can be combined. One machine may simultaneously act as an IPTV proxy, file server, transcoding edge and remote PTP agent.
Publish playlists and streams with your own URLs and access credentials.
- HLS proxy
- HMAC claims
- Private headers
- Optional CONNECT
Reduce origin connections and adapt codec or bitrate to client devices.
- JSON profiles
- CPU or GPU
- disk, memory_fs or pipe_ram output
- Recordings
Turn folders and remote storage into seekable playback URLs.
- Local and mounted drives
- Remote protocols
- HTTP ranges
- Temporary links
Control slow jobs and publish a file only after it is ready.
- BitTorrent
- aMule/eD2k
- IPFS
- SABnzbd
Keep credentials and files near the origin while PTP coordinates the catalogue.
- Outbound connection
- Source inventory
- Available profiles
- Direct or relayed delivery
Add providers through plugins without changing the client-facing API.
- SDK and example
- Validated manifests
- Integrity checks
- Administrative status and restart
Main roles of a node
| Role | Typical input | Client output | Typical use |
|---|---|---|---|
| IPTV channels | Provider playlist or stream | Own HLS URL or restream | Live TV |
| VOD | Remote URL or claim token | Range stream or download | Films and episodes |
| Storage | Folder, remote protocol or catalogue | HTTP listing and objects | File library |
| External provider | Specialised service | Ready object through common API | S3, P2P, cloud |
| Recording | Channel or URL | File created on the node | Programmes and events |
| PTP node | Jobs from the central server | Inventory, results and delivery | Distributed installations |
Deployment models
| Deployment | Managed by | Connectivity | Choose it when |
|---|---|---|---|
| Standalone | Its own config.json | Clients connect directly to the proxy | Single machine or autonomous use |
| Local beside PTP | PTP starts and supervises the process | Loopback or LAN | Simple combined installation |
| Remote PTP node | Central PTP over outbound HTTPS | Works behind NAT or firewall | NAS, remote home, VPS or branch |
| Controlled public gateway | Node administrator | TLS, tokens and limits required | Direct delivery to external clients |
- IPTV only: configure
channelsand test/playlist.m3u. - File server: add
storage.sourcesand test/api/storage. - Transcoding: install profiles, configure FFmpeg and choose
profile. - On-demand content: enable a plugin source with
allow_prepare. - PTP integration: enable
managed_nodeor let PTP start the local node.
System requirements
- FFmpeg —
ffmpeg.exeexecutable in PATH or in the same folder - Visual C++ Redistributable 2022 (usually already installed)
- Without GPU: CPU only, no additional drivers
- With GPU: NVIDIA driver ≥ 570.0
- FFmpeg —
sudo apt install ffmpeg - glibc 2.31+ (included in Ubuntu 20.04 and later)
- Without GPU: CPU only, no additional drivers
- With GPU: NVIDIA driver + CUDA installed
Dependencies by feature
Install only what your deployment needs. The main binary can serve channels and native sources without Python or addon tools.
| Feature | Required | Optional | Notes |
|---|---|---|---|
| HLS proxy and VOD | ptp-proxy and network access | Own TLS endpoint | No FFmpeg when no re-encoding is required. |
| Restream, transcoding and recording | FFmpeg | GPU and drivers | The selected profile must exist on the node. |
| Local or remote file server | Access to the source | Private CA or mount | FFmpeg is not required to serve the file. |
| Python plugins | Python 3 | Provider-specific dependency | Bundled S3 and addons run outside the core. |
| PTP-managed node | Outbound HTTPS access to PTP | Direct public URL | Can work behind NAT and use relay as fallback. |
| memory_fs | Prepared memory-backed filesystem | System limits | ptp-proxy does not mount it automatically. |
- aMule/aMuled for the eD2k provider.
- rclone for administrator-configured cloud remotes.
- Kubo for IPFS/IPNS roots.
- SABnzbd for NZB jobs.
- aria2c when using the bundled BitTorrent provider.
- Optional dependencies are not required unless that provider is configured.
- The default HTTP listener is
127.0.0.1:8180. - For remote access, configure TLS or a secure reverse proxy and an access key.
- A managed node initiates its connection to PTP and does not need to expose its administrative API to the Internet.
- Remote sources must be reachable from the machine running ptp-proxy.
- Addon control APIs should remain on loopback whenever possible.
Important notes about FFmpeg
restream mode. If you only use proxy mode or VOD tunnel, FFmpeg is not needed, but having it available is recommended.Install FFmpeg on Windows
- Go to gyan.dev/ffmpeg/builds and download the release-full build
- Extract to e.g.
C:\ffmpeg\ - Add
C:\ffmpeg\binto the system PATH or copyffmpeg.exeto the same folder asptp-proxy.exe - Verify: open a terminal and run
ffmpeg -version
Install FFmpeg on Linux / WSL2
sudo apt update && sudo apt install ffmpeg
# Verify:
ffmpeg -version
Which FFmpeg version do I need?
| Feature | Minimum FFmpeg version |
|---|---|
| Copy / proxy / basic HLS | 4.0+ |
| CPU transcoding (libx264) | 4.0+ with libx264 compiled in |
| NVIDIA GPU (h264_nvenc) | 4.4+ with NVENC support + driver ≥ 570.0 |
| All of the above (recommended) | 6.0 or later |
Verify NVENC support (NVIDIA GPU)
# Should show "h264_nvenc" in the list
ffmpeg -hide_banner -encoders | grep nvenc
# Quick test — if it errors, the driver is not compatible
ffmpeg -hide_banner -f lavfi -i nullsrc -t 1 -c:v h264_nvenc -f null -
Architecture — connection flows
The first diagrams explain IPTV and restreaming. Additional flows show how ptp-proxy publishes files, coordinates asynchronous providers and works as a PTP-managed node.
1. Direct connection — no proxy
Users connect directly to the IPTV provider. Every client opens its own set of connections (control, video, EPG). Provider credentials travel in every request and are visible to the client.
2. ptp-proxy inserted
ptp-proxy sits between clients and the provider. Clients only see the proxy address. Credentials never leave the proxy. Each client still generates its own upstream connection (proxy mode). Dashed lines indicate flows that the server can individually enable or disable — control, video and web-player traffic can each be proxied or bypassed independently.
3. Restream mode — shared upstream
In restream mode, FFmpeg opens one upstream connection per channel. All clients receive the re-distributed stream. The provider sees exactly one connection regardless of how many viewers there are.
4. VPN scenario — proxy inside a tunnel
If ptp-proxy runs on a host that sits inside a VPN (or a Cloudflare tunnel, WireGuard, etc.). All upstream traffic to the provider exits through the VPN IP. Clients connect to ptp-proxy normally from the outside.
5. Storage gateway
↓ lists or requests an object
ptp-proxy — permissions, limits and temporary link
↓
Local / NAS / WebDAV / SFTP / SMB / NFS / FTP / HTTP / plugin
The client uses the same API without knowing where the file lives. ptp-proxy keeps credentials, publishes metadata and translates player seek into origin ranges.
6. PTP-managed node
PTP → inventory, browsing and jobs → node
node → results, profiles and operations → PTP
player → direct node or player → PTP → relay → node
PTP coordinates multiple nodes without treating them as generic tunnel proxies. Each node keeps its sources and plugins while PTP owns users, catalogue and location selection.
7. Asynchronous plugin preparation
ptp-proxy → external provider
queued → preparing → ready
ready result → /storage/<source>/<path>
client → normal Range playback
The operation controls content that is not yet a playable object. When it completes, the result uses the same API and permissions as any other source.
Responsibility boundaries
| Component | Primary responsibility |
|---|---|
| Client | Requests playlists, objects, ranges or authorised operations. |
| ptp-proxy | Origin access, local security, streaming, transcoding and plugins. |
| PTP | Users, catalogue, library, node coordination and delivery choice. |
| External provider | Connects to a specialised service and publishes the result. |
| FFmpeg | Copies, transcodes or records when a workflow requires it. |
Installation on Windows
- Download
ptp-proxy.exefrom the Downloads section of this guide - Create a folder, e.g.
C:\ptp-proxy\ - Copy
ptp-proxy.exeinto that folder - Copy
config.example.jsonto the same folder and rename it toconfig.json - Edit
config.jsonwith a text editor (Notepad++, VS Code...) - Open a terminal (
cmdor PowerShell) in that folder and run:
ptp-proxy.exe --config config.json
Listening on 0.0.0.0:8180 in the console (or in the log), the proxy is running.Run automatically on startup (optional)
To have the proxy start automatically with Windows, use the built-in service support (recommended — no extra tools needed) or Task Scheduler / NSSM (alternative).
Install as a Windows Service (recommended)
sc or the Services panel.# Install and start the service
ptp-proxy.exe --install-service --config "C:\ptp-proxy\config.json"
ptp-proxy.exe --start-service
sc query ptp-proxy
# Stop and uninstall the service
ptp-proxy.exe --stop-service
ptp-proxy.exe --uninstall-service
By default the service is installed with automatic startup. If you prefer to start it manually, add --demand:
# Manual start (only when explicitly requested)
ptp-proxy.exe --install-service --demand --config "C:\ptp-proxy\config.json"
Alternative: NSSM
NSSM is still valid if you already use it for other services.
# NSSM (https://nssm.cc)
nssm install PtpProxy "C:\ptp-proxy\ptp-proxy.exe" "--config C:\ptp-proxy\config.json"
nssm start PtpProxy
Windows vs Linux differences
| Aspect | Windows | Linux |
|---|---|---|
| Binary | ptp-proxy.exe | ptp-proxy (no extension) |
| Paths in config.json | C:/streams/ or ./streams/ | /home/user/streams/ or ./streams/ |
| NVIDIA GPU | Yes, with Windows driver | Yes, with Linux driver + CUDA |
| Job Object (kill child processes) | Automatic — FFmpeg is killed when the proxy closes | SIGTERM signal propagated |
| Path separator | / or \ (both valid in config.json) | / only |
| FFmpeg in PATH | Add C:\ffmpeg\bin to PATH or set "path": "C:/ffmpeg/bin/ffmpeg.exe" | Usually already in PATH after apt install |
Installation on Linux
- Download the binary
ptp-proxy(Linux x64) to e.g./opt/ptp-proxy/ - Grant execute permission:
chmod +x /opt/ptp-proxy/ptp-proxy
- Copy
config.example.jsonasconfig.jsonin the same folder and edit it - Run:
cd /opt/ptp-proxy
./ptp-proxy --config config.json
Run as a systemd service (recommended on Linux)
Create the file /etc/systemd/system/ptp-proxy.service
[Unit]
Description=ptp-proxy
After=network.target
[Service]
ExecStart=/opt/ptp-proxy/ptp-proxy --config /opt/ptp-proxy/config.json
WorkingDirectory=/opt/ptp-proxy
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now ptp-proxy
sudo systemctl status ptp-proxy
journalctl -u ptp-proxy -f
The binary is static (no extra dependencies)
Using the proxy on WSL2 (Linux inside Windows)
How WSL2 networking works
WSL2 has its own network interface, but Windows includes a mechanism called localhost forwarding: if an application inside WSL2 listens on 127.0.0.1, Windows automatically redirects connections to the host's 127.0.0.1 to the VM.
The proxy listens inside WSL2 Linux → Windows sees it at its own
127.0.0.1All outgoing proxy traffic uses the Linux/VPN interface, not Windows'
Step 1 — Install WSL2 and Ubuntu
wsl --install
wsl --install -d Ubuntu-24.04
Step 2 — Copy the binary to WSL2
wsl -d Ubuntu-24.04 -- bash -c "cp /mnt/c/path/ptp-proxy /home/user/ptp-proxy"
wsl -d Ubuntu-24.04 -- bash -c "chmod +x /home/user/ptp-proxy"
Or access WSL2 directly and move the files:
wsl
cp /mnt/c/path/to/binary/ptp-proxy ~/ptp-proxy
chmod +x ~/ptp-proxy
Step 3 — Configure config.json for WSL2
The key is to make the proxy listen on 127.0.0.1 inside WSL2. Windows handles the forwarding automatically.
"server": {
"listen": "127.0.0.1", // ← IMPORTANT: not 0.0.0.0
"port": 8180
}
0.0.0.0 in WSL2 if the goal is for only Windows to access the proxy via localhost forwarding. With 127.0.0.1, the proxy is only reachable from the host itself (Windows included via forwarding), not from the local network.Step 4 — Start the proxy from PowerShell
wsl -d Ubuntu-24.04 -- bash -c "cd /path/to/proxy ; nohup ./ptp-proxy > ptp-proxy.log 2>&1 &"
wsl -d Ubuntu-24.04 -- bash -c "tail -5 /path/to/proxy/ptp-proxy.log"
wsl -d Ubuntu-24.04 -- bash -c "pkill ptp-proxy"
Step 5 — Verify from Windows
Invoke-WebRequest http://127.0.0.1:8180/health
curl http://127.0.0.1:8180/health
ok as a response, the proxy in WSL2 is running and is accessible from Windows.Paths in config.json for WSL2
You can put the streams folder directly in Windows to easily see the files:
"storage_path": "/mnt/c/path/streams/" // Windows folder accessible from WSL2
FFmpeg in WSL2
sudo apt update && sudo apt install ffmpeg
libx264) in WSL2.Basic configuration (config.json)
The config.json file is the only configuration file. Edit it with any text editor. Each block is explained below.
Configuration map
The file is split by responsibility. Start with server, security and one source or channel, then add the remaining blocks as needed.
| Block | Controls | Needed when |
|---|---|---|
server | Listener, TLS and HTTP limits | Always |
security | Keys, management IPs, SSRF and rate limiting | Always |
ffmpeg | Executable path and HLS timing | Restream, transcode or record |
transcoding | Profile directory and default profile | Profiles are used |
restream | HLS output, directory and lifetime | Restream channels |
storage | Sources, permissions, tokens, plugins and limits | File server or addons |
core | Dynamic claims from a validation server | PTP or another core issues tokens |
managed_node | Enrollment and outbound connection to PTP | The proxy is a managed node |
channels | Static proxy/restream channels | They do not depend on a dynamic core |
recordings | Recording directory and policy | The recording API is used |
Full structure with default values
{
// ── Server ──────────────────────────────────────────────
"server": {
"listen": "127.0.0.1",
"port": 8180,
"worker_threads": 0,
"request_header_limit": 32768,
"request_body_limit": 1048576,
"request_timeout_sec": 30,
"write_timeout_sec": 60,
"keep_alive_timeout_sec": 30,
"max_requests_per_connection": 100,
"health_secret": "",
"tls": {
"enabled": false,
"port": 8443,
"cert": "",
"key": ""
}
},
// ── Logging ───────────────────────────────────────────────
"logging": {
"file": "ptp-proxy.log",
"json": true
},
// ── Security ─────────────────────────────────────────────
"security": {
"block_private_ips": true,
"management_ips": ["127.0.0.1", "::1"],
"access_key": "",
"localhost_bypass": true,
"proxy_raw_public": false,
"key_exempt_paths": [],
"allow_unauthenticated_remote": false,
"allow_access_key_query": false,
"allow_legacy_url_query": false,
"verify_upstream_tls": false,
"upstream_ca_file": "",
"rate_limit": {
"enabled": true,
"window_sec": 10,
"max_requests": 300
}
},
// ── FFmpeg ─────────────────────────────────────────────────
"ffmpeg": {
"path": "ffmpeg",
"hls_time_seconds": 4,
"hls_list_size": 10,
"extra_args": []
},
// ── Transcoding profiles ───────────────────────────────────
"transcoding": {
"profiles_dir": "./profiles",
"default_profile": "libopenh264_balanced",
"allow_legacy_extra_args": true
},
// ── Restream output ────────────────────────────────────────
"restream": {
"output": "disk",
"storage_path": "./streams/",
"startup_grace_ms": 30000,
"idle_timeout_sec": 120
},
// ── Storage sources ───────────────────────────────────────────
"storage": {
"sources": [
{
"id": "media",
"type": "local",
"root_path": "./media"
}
]
},
// ── Core (claims integration) ─────────────────────────────────────────────────────────────
"core": {
"url": "",
"secret": ""
},
// ── Channels ────────────────────────────────────────────────
"channels": []
}
<code>server.listen</code> options
| Value | Meaning | When to use |
|---|---|---|
"0.0.0.0" | Accepts connections on all network interfaces | Server accessible from the local network or internet |
"127.0.0.1" | Local connections only | WSL2 usage or behind nginx on the same server |
"192.168.1.x" | Connections from that network interface only | Serve only on the local network, not on the VPN |
HTTP limits and capacity
The limits protect against slow connections, unbounded bodies and excessive keep-alive sessions, keeping the service stable under load.
| Field | Default | Description |
|---|---|---|
worker_threads | Maximum number of simultaneous operations handled. | Available concurrency, with a minimum of 4. |
request_header_limit | 32768 | Maximum request-header bytes. |
request_body_limit | 1048576 | Maximum incoming body size. |
request_timeout_sec | 30 | Maximum time to read a request. |
write_timeout_sec | 60 | Maximum time to write a response. |
keep_alive_timeout_sec | 30 | Maximum wait between keep-alive requests. |
max_requests_per_connection | 100 | Maximum requests on one persistent connection. |
Storage sources and public HTTP access
Local, WebDAV, SFTP, SMB, NFS, FTP/FTPS and HTTP catalog sources share scoped authorization, opaque public IDs, temporary links, limits and ranged playback.
"storage": {
"sources": [
{
"id": "media",
"type": "local",
"root_path": "./media"
}
]
}
local or through mount_path. Localhost bypass remains disabled by default.| Field | Purpose |
|---|---|
id / public_id | Internal source name and optional opaque ID exposed in public URLs. |
type | local, webdav, sftp, smb, nfs, ftp or http_manifest. |
allow_list / allow_stream / allow_download | Per-source operations allowed for non-admin principals. |
admin_only | Restricts the source to storage:admin. |
access_tokens | Bearer tokens with storage permissions and optional internal source IDs. |
link_secret | HMAC secret for exact-path temporary playback/download links. |
read_buffer_bytes | Backpressure buffer between 16 KiB and 4 MiB. |
max_concurrent_reads* | Global, per-IP and per-source transfer limits. |
idle_timeout_sec / initial_retry_count | No-progress timeout and retries allowed only before response headers. |
root_path / base_url / mount_path | Local root, remote root, HTTP catalog or an already mounted SMB/NFS resource. |
verify_tls / ca_file | Certificate verification for TLS connections. |
known_hosts / host_key_sha256 | SFTP SSH host verification. |
domain / require_signing / require_encryption | SMB authentication and transport policy. |
connect_timeout_sec / transfer_timeout_sec | Positive remote connection and total transfer timeouts. |
TLS / HTTPS on the listener
The server can listen on HTTP and HTTPS simultaneously (two independent ports). TLS 1.2 and TLS 1.3 are supported; SSLv2 / SSLv3 are always disabled.
"server": {
"listen": "0.0.0.0",
"port": 8180,
"tls": {
"enabled": true,
"port": 8443,
"cert": "/etc/letsencrypt/live/mydomain.com/fullchain.pem",
"key": "/etc/letsencrypt/live/mydomain.com/privkey.pem"
}
}
| Field | Default | Description |
|---|---|---|
enabled | false | Activates the HTTPS listener. HTTP continues working on server.port. |
port | 8443 | HTTPS port. Clients must use https://host:8443/… when TLS is active. |
cert | "" | Path to the PEM file with the full certificate chain (e.g. fullchain.pem). |
key | "" | Path to the PEM file with the private key (e.g. privkey.pem). |
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"Then set
"cert": "./cert.pem" and "key": "./key.pem" in the config.certbot certonly --standalone -d mydomain.comFiles:
/etc/letsencrypt/live/mydomain.com/fullchain.pem and privkey.pem. Renew automatically with certbot renew.HLS output: disk, memory filesystem or integrated RAM
"output": "memory_fs" — recommended in-memory modeSegments are written to a memory-backed directory such as tmpfs or a RAM drive. Normal HLS behaviour is retained and configuration stays consistent across PTP and ptp-proxy.
Compatibility: pipe_ram keeps the integrated segmenter for small workloads; ram remains its legacy alias.
"output": "disk"Segments are stored under storage_path on a regular filesystem. This is the simplest option and may retain segments across a process restart.
The transcoding profile is selected independently with transcoding.default_profile or channels[].profile.
PTP-compatible transcoding profiles
ptp-proxy loads JSON profiles using the same format as PTP. Each profile has a stable identifier, labels, codec metadata and variants for direct playback, remuxing and HLS.
profile_mode (hlstrans or hlscopy). ptp-proxy validates both and never accepts arbitrary execution arguments from the core.Catalogue and default profile
Configure the profile directory and a default profile. Official packages include software and hardware-accelerated options.
"transcoding": {
"profiles_dir": "./profiles",
"default_profile": "libopenh264_balanced",
"allow_legacy_extra_args": true
}
ffmpeg.extra_args and channels[].extra_args remain only for migration and can be disabled with allow_legacy_extra_args=false.Per-channel profile and HLS variant
{
"id": "news",
"mode": "restream",
"url": "https://origin.example/live.m3u8",
"profile": "h264_nvenc_web",
"profile_mode": "hlstrans"
}
curl -H "Authorization: Bearer ACCESS_KEY" http://127.0.0.1:8180/api/transcode/profiles
What a profile defines
| Field | Public purpose |
|---|---|
id | Stable identifier shared with PTP. |
label | Localised name for user interfaces. |
codec | Output family seen by the client. |
mse_test | Browser compatibility probe. |
hlstrans | HLS variant with transcoding. |
hlscopy | HLS variant without re-encoding. |
direct / remux | Variants reserved for other playback workflows. |
How the profile is selected
- A channel may set
profileandprofile_mode. - A dynamic claim may request a profile by identifier.
- When omitted,
transcoding.default_profileis used. - PTP queries
/api/transcode/profilesand only offers profiles available on the node. - Legacy arguments may be disabled after migration is complete.
Where HLS output is generated
The profile defines encoding. The output mode defines where segments are stored; these are independent choices.
| Mode | What it does | Advantages | Recommended use |
|---|---|---|---|
disk | Writes playlists and segments to a normal filesystem. | Simple, inspectable and persistent until cleaned. | General use and fast disks. |
memory_fs | Writes normal HLS to a prepared tmpfs or RAM drive. | Keeps full HLS behaviour with low I/O latency. | Recommended option when RAM-backed output is wanted. |
pipe_ram | Keeps MPEG-TS segments in process memory. | Does not need a temporary filesystem. | Small, controlled workloads. |
ram | Legacy alias for pipe_ram. | Configuration compatibility. | Migrate to an explicit name. |
"restream": {
"output": "memory_fs",
"storage_path": "/dev/shm/ptp-proxy/streams"
}
memory_fs does not create or mount a RAM drive. Point restream.storage_path at an already prepared path and enforce its size in the operating system.Actual profile availability
A profile file does not guarantee that the encoder or driver exists on the machine. Test every profile on each node and let PTP store the availability announced by that node.
Hardware-accelerated profile
Before selecting the profile
- Confirm that the node exposes a compatible encoder
- Test the profile with a short restream before assigning it in PTP
- Keep a software profile available as a fallback choice
Included NVIDIA profile
"profile": "h264_nvenc_web"
Selection from PTP
PTP can query /api/transcode/profiles and offer only profiles installed on the selected node.
Included NVIDIA profiles
| Profile | Compatibility | Goal | Recommended use |
|---|---|---|---|
h264_nvenc_web | H.264 Baseline | Web compatibility | Browsers and mixed devices |
h264_nvenc | H.264 Main | General quality | Clients supporting Main profile |
Software transcoding profiles
Software profiles work without a dedicated video encoder. Performance depends on the processor, resolution and number of concurrent restreams.
Recommended software profile
"profile": "libopenh264_balanced"
Included software profiles
| Profile | Codec | Goal | Recommended use |
|---|---|---|---|
libopenh264_fast | H.264 | Lower load | Limited hosts or testing |
libopenh264_balanced | H.264 | Balance | Recommended general option |
libopenh264_quality | H.264 | Higher quality | Few concurrent channels |
libx264_fast | H.264 | Speed | Installations with GPL FFmpeg |
libx264_quality | H.264 | Quality | Installations with GPL FFmpeg |
libx265 | HEVC | Efficiency | HEVC-compatible clients only |
Channel configuration
Channel in proxy mode (simplest)
The proxy downloads the provider's playlist and rewrites the segments. Does not use FFmpeg.
"channels": [
{
"id": "my_channel",
"name": "My Channel 1",
"mode": "proxy",
"url": "http://panel.provider.com/live/USER/PASS/123.m3u8",
"headers": []
}
]
Channel in restream mode (with FFmpeg)
{
"id": "nvenc_channel",
"mode": "restream",
"url": "http://panel.provider.com/live/USER/PASS/456.m3u8",
"profile": "h264_nvenc_web",
"profile_mode": "hlstrans",
"skip_re": true
}
Channel with authentication headers
{
"id": "auth_channel",
"mode": "proxy",
"url": "https://cdn.provider.com/live/stream.m3u8",
"headers": [
{ "key": "Authorization", "value": "Bearer TOKEN_HERE" },
{ "key": "Referer", "value": "https://provider.com/" }
]
}
Once configured, the client accesses the channel at:
http://your-server:8180/channels/my_channel/playlist.m3u8
Static channels and dynamic flows
| Origin | Configuration | Advantage |
|---|---|---|
| Static proxy channel | channels[].mode=proxy | Simple setup without re-encoding. |
| Static restream channel | channels[].mode=restream | Shared output and controlled profile. |
| Dynamic live claim | Core returns URL and live type | Credentials never reach the client. |
| Dynamic restream claim | Core adds profile and profile_mode | PTP selects an installed profile without sending commands. |
| VOD/series claim | Core returns URL and content type | Range tunnel with seek and download. |
| Recording | /api/recordings API | Stores an authorised channel or URL as a managed file. |
Core integration
When core.url is configured, the client uses a short token. ptp-proxy validates it with the core and receives the real URL, content type and, where relevant, requested profile. Origin credentials are never returned to the player.
Recordings
The recording API can schedule a configured channel or authorised URL, query status and cancel the job. The result is written to the configured directory and is not automatically exposed as a storage source unless you add it.
- Schedule by start/end time or duration.
- Use a controlled output name and dedicated directory.
- Check free space and permissions before long jobs.
- Recordings and restreams have separate metrics.
PTP-managed node
ptp-proxy can act as an agent of a central PTP server. The node keeps physical access to channels, files, profiles and plugins while PTP coordinates users, catalogue, browsing and playback.
Three ways to use the same binary
| Mode | Configuration | Control | Delivery |
|---|---|---|---|
| Standalone | managed_node.enabled=false | Local config | Clients access the proxy directly. |
| Local PTP node | PTP starts the process and provides enrollment | Central PTP | Loopback or LAN. |
| Remote node | Proxy enrolls with a one-time token | Central PTP over outbound HTTPS | Direct, relay or both. |
ptp-proxy → heartbeat, sources, profiles and plugins → PTP
PTP → authorised jobs → ptp-proxy
ptp-proxy → typed results and operations → PTP
player → direct node URL or player → PTP → relay → node
- The proxy HTTP server keeps working with its local configuration.
- Already-started sources and plugins remain locally available.
- The node reconnects with backoff.
- Jobs requiring central control wait for reconnection.
- The node credential can be revoked independently from PTP.
What PTP can manage
| Capability | Result |
|---|---|
| Inventory | Available sources, profiles and providers. |
| Browsing | Listings, metadata and directory walks. |
| Catalogue | Import remote locations into the central library. |
| Protocol-v2 operations | Prepare, inspect and cancel on-demand content. |
| Plugins | Status and administrative restart of an external source. |
| Transcoding | Select a profile installed on the node. |
| Direct delivery | Temporary URL to a reachable node. |
| Reverse relay | Range reads when the client cannot reach the node. |
Node configuration
In an integrated installation PTP may provide these values through the environment. On a remote node they are stored in config.json; after enrollment the one-time token is replaced by a node-specific credential.
"managed_node": {
"enabled": true,
"server_url": "https://ptp.example.com",
"node_name": "NAS home",
"enrollment_token": "ONE_TIME_TOKEN",
"credential_file": "./state/ptp-node.json",
"public_url": "",
"direct_delivery": false,
"reverse_relay": true,
"verify_tls": true
}
Direct delivery and relay
| Mode | Advantage | Cost or limit |
|---|---|---|
| Direct | Video travels from node to client without crossing PTP. | The client must reach public_url. |
| Relay | Works even when the node accepts no inbound connections. | PTP transports bytes and consumes central bandwidth. |
| Mixed | PTP selects direct when possible and relay as fallback. | Both paths must be configured and tested. |
- Only known job types are accepted; there is no generic shell command.
- Source credentials remain on the node.
- Every job has identity, limits and a deadline.
- Relay reads are size-bounded and range-based.
- Plugin administration still requires explicit permissions.
Local and remote storage
ptp-proxy is also a file server and storage gateway. It publishes local folders, mounted drives, remote protocols, HTTP catalogues and external providers through the same listing, metadata and playback API.
Four ways to present content
The public API does not change whether the file is on the same machine, on a NAS, on a remote service or still needs to be prepared.
| Model | Origin | Availability | Example |
|---|---|---|---|
| Local server | Folder, disk or mounted drive | Immediate | type: local |
| Remote gateway | WebDAV, SFTP, SMB, NFS or FTP/FTPS | Immediate when the origin responds | type: webdav |
| HTTP catalogue | JSON manifest and HTTP/HTTPS objects | According to the catalogue | type: http_manifest |
| External provider | S3, P2P, cloud or download manager | Immediate or after an operation | type: plugin |
Each object is referenced by a public source ID and a relative path. The internal ID, credentials and real origin URL remain on the server.
source.id: administrator-facing internal identifier.source.public_id: opaque identifier used in public URLs.path: validated relative path inside the source.etagandmodified_at: detect changes.range_supported: tells the player whether seek is available.
The API describes folders and files. A PTP server may import those locations, associate them with films or episodes and maintain metadata, but ptp-proxy does not invent titles or artwork by itself.
- The client lists a source or receives a path from PTP.
- Authorization checks permission, source policy and concurrency limits.
- ptp-proxy reads metadata and decides whether ranges can be served.
- The response uses
GETorHEADwith ETag, size and MIME type. - The client may request another range to seek without knowing the real origin.
↓ HTTP + Bearer or signed link
ptp-proxy
↓ read-only connection
Local · WebDAV · SFTP · SMB · NFS · FTP/FTPS · HTTP catalog
- The same endpoints for every source
- HTTP ranges, seek, ETag and Last-Modified
- Tokens restricted by permission and source
- Opaque public IDs and temporary HMAC links
- Global, per-IP and per-source limits
- Timeouts, cancellation, stable streaming and metrics
Quick start
- Add a source to
storage.sources. - Create a token with
storage:listandstorage:read. - Request
GET /api/storage. - Browse with
/api/storage/<public_id>/list. - Play with
/storage/<public_id>/path.
Sources and available modes
Choose the source matching the remote service. Resources already mounted can be exposed as a directory or drive, while external providers add new services without changing the public API.
| Source | Direct connection | Mounted resource | Main requirement | Recommended use |
|---|---|---|---|---|
| Local | Yes | — | Directory readable by the process | Disks, folders and mounted drives. |
| WebDAV | Yes | Optional | Read-only account and TLS | Nextcloud, ownCloud, DAV servers and rclone. |
| SFTP | Yes | Optional | SSH host verification | File servers accessed through SSH. |
| SMB | Platform dependent | Yes | Reachable share and credentials | NAS and Windows/Samba shares. |
| NFS | Platform dependent | Yes | Export authorized for the host | Unix/Linux storage and NAS devices. |
| FTP / FTPS | Yes | Optional | FTPS recommended | Existing FTP servers and legacy systems. |
| HTTP catalog | Yes | — | Versioned JSON manifest | CDNs, static hosting and generated catalogs. |
| S3 compatible | Yes | — | Installed provider and read-only credentials | Amazon S3, MinIO, Ceph, Wasabi and compatible services. |
{
"id": "local-media",
"public_id": "media",
"type": "local",
"root_path": "D:/Media",
"allow_list": true,
"allow_stream": true,
"allow_download": false
}
D:/Media on Windows and /srv/media on Linux.{
"id": "remote-media",
"public_id": "cloud-a91e",
"type": "webdav",
"base_url": "https://dav.example.test/files/user/",
"username": "reader",
"password": "CHANGE_ME",
"auth_method": "any",
"verify_tls": true,
"allow_list": true,
"allow_stream": true,
"allow_download": false
}
verify_tls=true. Use ca_file for a private CA; redirects are restricted to the same origin.{
"id": "archive",
"public_id": "archive-a91e",
"type": "sftp",
"base_url": "sftp://files.example.test/media/",
"username": "reader",
"private_key": "./keys/id_ed25519",
"known_hosts": "./keys/known_hosts",
"allow_list": true,
"allow_stream": true
}
known_hosts or host_key_sha256 to verify the server. Reserve allow_unknown_host for controlled testing.{
"id": "nas",
"public_id": "nas-media",
"type": "smb",
"base_url": "smb://nas.example.test/video/",
"username": "reader",
"password": "CHANGE_ME",
"domain": "MEDIA",
"require_signing": true,
"require_encryption": false
}
{
"id": "nfs-media",
"public_id": "nfs",
"type": "nfs",
"server": "192.168.1.30",
"export_path": "/exports/media",
"remote_path": "/library",
"version": 3,
"uid": 1000,
"gid": 1000
}
mount_path. Mounting is recommended for advanced identity or Kerberos setups.{
"id": "legacy-media",
"public_id": "ftp-media",
"type": "ftp",
"base_url": "ftp://ftp.example.test/media/",
"username": "reader",
"password": "CHANGE_ME",
"tls_mode": "explicit",
"verify_tls": true,
"passive_mode": true,
"allow_list": true,
"allow_stream": true,
"allow_download": false
}
allow_insecure_ftp=true; the source is read-only and passive mode is the default.{
"id": "cdn-catalog",
"public_id": "cdn",
"type": "http_manifest",
"manifest_url": "https://catalog.example.test/media.json",
"base_url": "https://cdn.example.test/media/",
"refresh_sec": 300,
"max_manifest_bytes": 8388608,
"allowed_origins": ["https://cdn.example.test"],
"allow_list": true,
"allow_stream": true,
"allow_download": false
}
base_url or explicit allowed_origins.{
"id": "s3-media",
"public_id": "s3",
"type": "plugin",
"plugin_id": "s3-compatible",
"backend_type": "s3",
"plugin_config": {
"endpoint": "https://s3.example.test",
"bucket": "media",
"prefix": "library/",
"region": "us-east-1",
"access_key": "CHANGE_ME",
"secret_key": "CHANGE_ME",
"path_style": true,
"verify_tls": true
},
"allow_list": true,
"allow_stream": true,
"allow_download": false
}
Permissions, policies and temporary links
Storage has its own authorization layer. Localhost bypass is disabled by default and knowing a URL does not grant access.
"storage": {
"localhost_bypass": false,
"inherit_access_key": true,
"allow_unauthenticated_remote": false,
"link_secret": "CHANGE_ME_WITH_A_LONG_RANDOM_SECRET",
"access_tokens": [
{
"token": "CHANGE_ME_READER_TOKEN",
"permissions": ["storage:list", "storage:read", "storage:prepare"],
"sources": ["local-media"]
}
]
}
| Permission | Action |
|---|---|
storage:list | View authorized sources and list directories. |
storage:read | Use HEAD and stream with GET. |
storage:download | Use ?download=1 when the source allows it. |
storage:prepare | Start, inspect and cancel asynchronous preparation on sources that allow it. |
storage:admin | View every source and create temporary HMAC links. |
public_idhides the internal ID in public URLs.allow_list,allow_streamandallow_downloadseparate capabilities.admin_onlyreserves a source for administrators.max_concurrent_readslimits one source.- Restricted tokens list internal IDs, not
public_id.
POST /api/storage/sign creates a URL bound to source, path, action and expiration. Changing any of these values invalidates the signature and never bypasses source policy.
API and playback
Every source uses the same public paths and HTTP behaviour.
| Method and path | Purpose |
|---|---|
GET /api/storage | Lists sources visible to the principal. |
POST /api/storage/<source>/prepare | Starts asynchronous preparation; requires storage:prepare. |
GET /api/storage/<source>/operations/<id> | Returns state, progress and the resulting path. |
DELETE /api/storage/<source>/operations/<id> | Cancels an operation when supported by the provider. |
GET /api/storage/<source>/list?path=... | Lists one relative directory. |
POST /api/storage/sign | Creates an exact-path temporary URL. |
GET /api/storage/plugins | Shows external provider status; requires an explicit Bearer credential with storage:admin. |
GET /api/storage/plugin-providers | Inventories installed providers, versions, capabilities, duplicate IDs and integrity; requires storage:admin. |
POST /api/storage/plugins/<source>/restart | Restarts one external source; requires an explicit Bearer credential with storage:admin. |
GET /storage/<source>/<path> | Streams a complete object or one range. |
HEAD /storage/<source>/<path> | Returns metadata without a body. |
GET ...?download=1 | Forces attachment download when authorized. |
bytes range is supported, including open-ended and suffix ranges, plus If-Range, If-None-Match and If-Modified-Since. Multiple ranges return 416.curl examples
curl -H "Authorization: Bearer CHANGE_ME_READER_TOKEN" http://127.0.0.1:8180/api/storage
curl -H "Authorization: Bearer CHANGE_ME_READER_TOKEN" "http://127.0.0.1:8180/api/storage/media/list?path=films"
curl -I -H "Authorization: Bearer CHANGE_ME_READER_TOKEN" http://127.0.0.1:8180/storage/media/films/movie.mp4
curl -H "Authorization: Bearer CHANGE_ME_READER_TOKEN" -H "Range: bytes=1048576-2097151" http://127.0.0.1:8180/storage/media/films/movie.mp4
curl -X POST -H "Authorization: Bearer CHANGE_ME_ADMIN_TOKEN" -H "Content-Type: application/json" --data '{"source":"media","path":"films/movie.mp4","ttl_sec":300}' http://127.0.0.1:8180/api/storage/sign
Operations, metrics and troubleshooting
Limits and response codes
| Response | Common cause |
|---|---|
401 / 403 | Missing identity, permission or source policy. |
404 | Unknown source or object. |
416 | Invalid range or the source does not allow that operation. |
429 | Per-IP transfer limit. |
503 | Global/source limit or temporarily unavailable source. |
504 | Remote server timeout. |
The proxy exposes active transfers, bytes, duration, errors, cancellations, ranges, limit rejections and external-provider restarts/failures.
ptp_storage_active_transfers
ptp_storage_bytes_total
ptp_storage_transfer_duration_seconds_count
ptp_storage_transfer_duration_seconds_sum
ptp_storage_errors_total{code="..."}
ptp_storage_cancelled_total
ptp_storage_range_requests_total
ptp_storage_limit_rejections_total{reason="..."}
ptp_storage_plugin_restarts_total{plugin="...",reason="..."}
ptp_storage_plugin_failures_total{plugin="...",code="..."}
- Run
python3 tests/run_all.pyand CTest. - Test listing, full playback, seek and disconnect against real infrastructure.
- Confirm logs contain no credentials or tokens.
- Use
public_id, least privilege andallow_download=falseby default. - Verify certificates, remote server identity, share/export permissions and HTTP catalog allowed origins.
Protocol v2 extensions can prepare content that is not immediately available. Clients start the job, poll progress and stream the ordinary result path when it is ready.
curl -X POST -H "Authorization: Bearer CHANGE_ME_PREPARE_TOKEN" -H "Content-Type: application/json" -H "Idempotency-Key: example-1" --data '{"torrent_file":"sample.torrent"}' http://127.0.0.1:8180/api/storage/torrent/prepare
curl -H "Authorization: Bearer CHANGE_ME_PREPARE_TOKEN" http://127.0.0.1:8180/api/storage/torrent/operations/OPERATION_ID
curl -X DELETE -H "Authorization: Bearer CHANGE_ME_PREPARE_TOKEN" http://127.0.0.1:8180/api/storage/torrent/operations/OPERATION_ID
storage:prepare and allow_prepare: true. A read-only token cannot start downloads or remote jobs.Asynchronous operation states
| State | Meaning | Client action |
|---|---|---|
queued | Accepted and waiting for resources. | Poll again after the suggested interval. |
preparing | Provider is working and may report progress. | Display progress and keep polling. |
ready | Result now has a playable path. | Use the normal /storage URL. |
failed | Operation ended with a stable error. | Show the error or retry with a new key. |
cancelled | User or administrator stopped the job. | Do not attempt to play the result. |
Native, mounted and plugin sources
| Class | Who maintains the connection | Use it when |
|---|---|---|
| Native | ptp-proxy | Common protocols with first-party support. |
| Mounted | Operating system | Drives, FUSE, shares or exports already mounted. |
| v1 plugin | External provider | Content is immediately available. |
| v2 plugin | External provider with operations | Content must be downloaded, restored or generated. |
Prepares authorized BitTorrent content and publishes the completed file through the common API. Terminal operations survive restarts, with configurable capacity, free-space reserve, retention and partial cleanup.
Four optional providers connect already installed services without expanding the core or changing the public API.
| Addon | Dependency | Use | Seek |
|---|---|---|---|
| eD2k / aMule | aMule or aMuled and amulecmd | Prepares eD2k links and publishes completed files only. | After completion |
| rclone | rclone and a configured remote | Exposes cloud or file remotes as read-only storage. | Remote-dependent |
| IPFS / Kubo | Kubo node | Lists CID/IPNS roots and can prepare an authorized root. | Yes |
| Usenet / SABnzbd | SABnzbd | Prepares authorized NZBs and publishes only the final result. | After completion |
Administrators can inspect active instances, inventory installed providers and restart one source without exposing configuration, credentials or internal paths.
curl -H "Authorization: Bearer CHANGE_ME_ADMIN_TOKEN" http://127.0.0.1:8180/api/storage/plugins
curl -H "Authorization: Bearer CHANGE_ME_ADMIN_TOKEN" http://127.0.0.1:8180/api/storage/plugin-providers
curl -X POST -H "Authorization: Bearer CHANGE_ME_ADMIN_TOKEN" -H "Content-Type: application/json" --data '{}' http://127.0.0.1:8180/api/storage/plugins/s3/restart
External providers can now be installed to connect additional services without changing the public API. ptp-proxy continues to enforce authentication, permissions, limits, signed links and metrics.
- The package includes S3, BitTorrent, aMule/eD2k, rclone, IPFS/Kubo and SABnzbd providers.
- The Python SDK, local example provider and validator support independent extension development and verification.
- Each provider declares listing, metadata, ranges, seek and asynchronous preparation where applicable.
- Extensions use the same endpoints, permissions, links and limits as native sources.
- Install only trusted providers and restrict their credentials and control APIs.
Quick troubleshooting
| Symptom | Check |
|---|---|
| Source is not listed | Token, storage:list, source IDs, admin_only and allow_list. |
| Seek returns 416 | Range support, known size and remote server capabilities. |
| WebDAV does not list | Authentication, certificate, root URL, redirects and response limit. |
| SFTP does not start | Host verification, credentials, private key and SSH connectivity. |
| SMB does not connect | Credentials, domain, signing/encryption policy or mount_path. |
| NFS does not connect | Export permissions, version, identity or mount_path. |
| FTP/FTPS does not connect | TLS mode, certificate, passive mode, user and read permissions. |
| HTTP catalog is rejected | Schema/version, duplicate paths, sizes, URLs and allowed_origins. |
| External provider does not start | Check /api/storage/plugin-providers and /api/storage/plugins; review duplicate ID, integrity, Python 3, permissions, endpoint and credentials. |
| BitTorrent preparation is rejected | Total capacity, free-space reserve, maximum active operations and the size declared by the torrent. |
| Client disconnects | Write timeout, remote timeout, network and error metrics. |
The package includes a general guide, references for every native source, an external-provider guide and SDK/conformance documentation.
How to test the proxy
1. Verify the server starts
tail -f ptp-proxy.log
curl http://localhost:8180/health
2. Test a channel with ffplay
curl http://localhost:8180/api/channels
ffplay http://localhost:8180/channels/my_channel/playlist.m3u8
3. Test VOD tunnel with a static token (no validation server)
Useful for testing the proxy without a validation server set up. Add to config.json:
"core": {
"url": "", "secret": "",
"static_vod_tokens": { "test": "https://your-cdn.com/movie.mkv" }
}
ffplay http://localhost:8180/stream?t=test
4. Test with VLC
- Open VLC → Media → Open Network Stream
- Enter:
http://127.0.0.1:8180/channels/my_channel/playlist.m3u8 - Click Play
5. View metrics
curl http://localhost:8180/metrics
6. Test the file server
Use a token with storage:list and storage:read. Test listing, headers and a small range before opening a complete video.
curl -H "Authorization: Bearer CHANGE_ME_LIST_TOKEN" http://localhost:8180/api/storage
curl -H "Authorization: Bearer CHANGE_ME_LIST_TOKEN" "http://localhost:8180/api/storage/media/list?path="
curl -I -H "Authorization: Bearer CHANGE_ME_READ_TOKEN" http://localhost:8180/storage/media/video.mp4
curl -H "Authorization: Bearer CHANGE_ME_READ_TOKEN" -H "Range: bytes=0-1048575" http://localhost:8180/storage/media/video.mp4
7. Check transcoding profiles
curl -H "Authorization: Bearer CHANGE_ME" http://localhost:8180/api/transcode/profiles
8. Check a managed node
In PTP, verify that the node is connected, publishes sources and profiles and can complete a listing job. In the proxy logs, verify that heartbeat messages expose no secrets.
9. Run the automated suite
The suite creates local simulated services and verifies storage, security, plugins, TLS, CONNECT, profiles and asynchronous operations without relying on real providers.
cmake -S . -B build-tests -DPTP_BUILD_STORAGE_TESTS=ON
cmake --build build-tests
ctest --test-dir build-tests --output-on-failure
python3 tests/run_all.py
- Play a large file with VLC or ffplay and perform several seeks.
- Disconnect a client during transfer and confirm the active counter returns to zero.
- Test every remote protocol against the actual server you will use.
- Verify one CPU profile and, where relevant, one GPU profile.
- Restart a plugin and a v2 operation without losing the final catalogue result.
- Validate direct and relayed delivery for a remote PTP node.
- Review logs and metrics for leaked tokens or credentials.
Common troubleshooting
| Symptom | Likely cause | Solution |
|---|---|---|
| 503 when opening the channel | FFmpeg still starting | Normal for the first 5–30 s. Wait or increase startup_grace_ms |
| Permanent 503 | FFmpeg fails to start | Check the log — look for "ERROR" lines from FFmpeg |
| NVENC fails | Old driver or unsupported GPU | Update driver to ≥ 570.0 or switch to libx264 |
| Stream cuts / artifacts | Unstable source or incorrect GOP | Add -force_key_frames, try libx264 |
/health not responding | Proxy didn't start or port blocked | Check log, check firewall, verify port is correct |
curl: (7) Failed to connect | Proxy is not running | Start the proxy, verify listen and port |
| Source does not appear | Missing permission, policy or configuration | Check storage:list, allow_list, public_id and startup logs |
| File opens but cannot seek | Origin lacks ranges or size metadata | Test HEAD, inspect range_supported and the remote server |
| Operation remains preparing | External provider stopped or out of resources | Check plugin status, quota, free space and external dependency |
| PTP node appears offline | URL, TLS or node credential issue | Check managed_node, clock, CA and outbound connectivity |
| Profile unavailable | Encoder or driver missing | Query /api/transcode/profiles and test the profile on that node |
Reading logs
The proxy produces structured logs and redacts tokens, signatures, nonces, URLs and sensitive headers. Stream messages only show the upstream origin.
{"lvl":"INFO", "msg":"[stream] type=live mode=hls_proxy upstream=http://cdn:80"}
{"lvl":"WARN", "msg":"proxy_claim HTTP 403: Forbidden IP"}
{"lvl":"ERROR","msg":"ffmpeg exited with code 1"}
Important log fields
| Field | Description |
|---|---|
[stream] mode=hls_proxy | Request to /stream resolved as direct HLS proxy |
[stream] mode=ffmpeg_restream | Restream channel launched or reused |
[stream] mode=vod_tunnel | VOD tunnel activated with native seek |
[stream] mode=static_bypass | Static token, no validation server call |
proxy_claim HTTP 403 | The validation server rejected the token (expired, not found, IP not authorized) |
[connect] tunnel open | CONNECT tunnel opened for an HTTP client |
ffmpeg exited | FFmpeg closed — may be normal (stream ended) or an error |
[storage] | Listing, metadata or transfer from a file source. |
[plugin] | Startup, restart, failure or operation of an external provider. |
[managed-node] | Enrollment, heartbeat, jobs or reconnection to PTP. |
[recording] | Scheduling, start, completion or cancellation of a recording. |
rate limit | An identity or IP exceeded a configured limit. |
What to watch by feature
| Feature | Log | Metric or check |
|---|---|---|
| Proxy channels | Mode and redacted origin | Requests and HTTP status. |
| Restream | FFmpeg startup and exit | Active restreams and uptime. |
| Storage | Source and stable error code | Transfers, bytes, ranges and limits. |
| Plugins | State and restarts | Restarts and failures per provider. |
| PTP node | Heartbeat, job and reconnect | Node presence in PTP. |
| Recordings | Job state | Active recordings and free space. |
Useful metrics
ptp_storage_active_transfersand transferred bytes.- Duration, cancellations, ranges and limit rejections.
- Plugin restarts and failures.
- Active restreams and recordings.
- Uptime and HTTP errors.
Follow logs in real time
tail -f ptp-proxy.log
# Readable format (requires jq):
tail -f ptp-proxy.log | jq -r '.lvl + " | " + .msg'
Security — Protection layers
The proxy combines general HTTP protections with specialised permissions for streams, storage, plugins, CONNECT and managed nodes. Not every route uses the same credential or scope.
Layer 1 — Rate limiting (anti-hammering)
Limits how many requests an IP can make in a time window. If exceeded, returns 429 Too Many Requests.
"rate_limit": {
"enabled": true,
"window_sec": 10,
"max_requests": 60
}
Layer 2 — Management IPs
Sensitive endpoints (/metrics, /api/*, /proxy/raw) are only accessible from the listed IPs.
"management_ips": ["127.0.0.1", "::1"]
Layer 3 — Access key
When access_key is configured, protected endpoints require Authorization: Bearer. The ?key= query is accepted only when allow_access_key_query is explicitly enabled.
# HTTP header (curl, backends):
curl -H "Authorization: Bearer MY_KEY" http://proxy:8180/channels/my_channel/playlist.m3u8
allow_access_key_query and allow_legacy_url_query are disabled by default. Enable them only while migrating old clients.localhost_bypass: true, localhost may skip the key. When binding outside loopback, startup requires an access_key unless allow_unauthenticated_remote is explicitly enabled.Recommended production configuration
"security": {
"block_private_ips": true,
"management_ips": ["127.0.0.1", "::1"],
"access_key": "long-random-key-here",
"localhost_bypass": true,
"allow_unauthenticated_remote": false,
"allow_access_key_query": false,
"allow_legacy_url_query": false,
"verify_upstream_tls": false,
"rate_limit": { "enabled": true, "window_sec": 10, "max_requests": 300 }
}
Surfaces and credentials
| Surface | Typical protection | Note |
|---|---|---|
| Channels and playlists | Access key or claim | Do not expose provider URLs. |
| Dynamic VOD | HMAC claim token | Core selects URL, type and profile. |
| Storage | storage:* scopes | Policy is also evaluated per source. |
| Temporary links | HMAC signature and expiry | Bound to source, path and action. |
| Plugin administration | Explicit storage:admin | Local bypass is not sufficient. |
| Asynchronous preparation | storage:prepare | Also requires allow_prepare. |
| PTP node | Node-specific credential | Issued after one-time enrollment. |
| CONNECT | Dedicated secret or claim | Keep destinations and origins restricted. |
Every source decides whether listing, streaming, downloading or preparation is allowed. Tokens may be restricted to individual sources and public IDs avoid leaking internal identifiers.
The node connects to PTP, stores its own credential and only executes known jobs. It offers no shell, never returns source credentials and can be revoked without affecting other nodes.
Install trusted providers only. Verify manifests and integrity, use read-only credentials, limit operations and review the external services they control.
Additional protections
- SSRF without a second DNS lookup: connections use the exact endpoints that were validated.
- Every redirect is revalidated against the SSRF policy and
allow_hosts. - Playlists use opaque contexts; upstream URLs and headers remain in server memory.
- Secrets and sensitive URLs are redacted from logs; stream messages show only the upstream origin.
/healthnonces cannot be reused and the cache is bounded to 65,536 entries.- Outbound TLS verification is optional and disabled by default; enable it with
verify_upstream_tls.
Remote-exposure checklist
- Use TLS or a secure reverse proxy.
- Configure a long key and disable query-string compatibility.
- Restrict management IPs.
- Keep origin TLS verification enabled where possible.
- Create storage tokens by permission and source.
- Set global, per-IP and per-source limits.
- Do not publish addon control endpoints.
- Test node revocation and temporary-link expiry.
- Review logs, metrics and plugin integrity regularly.
Claim system — Secure token authentication
Why is it needed?
Without an authentication system, anyone who knows the proxy URL could:
- Guess or reuse tokens from other users
- Force the proxy to access URLs you didn't authorize (SSRF)
- Use the proxy as an open relay for their own requests
The claim system solves this: the proxy never trusts the client's token directly. It always verifies with your validation server that the token is valid, and authenticates the verification request itself with an HMAC signature.
The full flow for an IPTV client
↓
2. Your web service generates a short token (e.g.: UUID, 30s TTL)
returns to client: http://proxy:8180/stream?t=TOKEN
↓
3. Client calls the proxy: GET /stream?t=TOKEN
↓
4. Proxy builds a signed claim and POSTs to your service:
POST https://your-service.com/api/claim?proxy_claim=1
Body JSON: { token, ts, nonce, sig }
↓
5. Your server verifies the HMAC signature using the shared secret
if OK → returns { ok: true, url: "http://panel/live/USER/PASS/123.m3u8", type: "live" }
↓
6. Proxy serves content to client without exposing the real URL
The client never sees the real credentials. It only sees http://proxy:8180/stream?t=TOKEN.
Why it is secure
| Possible attack | Protection |
|---|---|
| Intercept and replay the claim | The claim includes a timestamp (ts) with a ±5-minute window. A captured claim expires quickly. |
| Brute-force the secret | The secret never travels in plaintext — only the HMAC result does. Without the secret, reversing HMAC-SHA256 is computationally infeasible. |
| Reuse an expired token | Your server controls the token TTL in its database. It can invalidate it after first use or after X seconds. |
| Forge a claim | Without the secret key (core.secret), it is impossible to compute a valid HMAC. All requests without a valid signature receive 403. |
HMAC claim calculation — Step by step
This section explains exactly how the proxy builds the claim it sends to your validation server. If you want to implement your own validation server (in any language), this is all you need.
Claim ingredients
| Field | Type | Description |
|---|---|---|
token | string | The token the client sent in ?t=TOKEN |
ts | int (Unix) | Current timestamp in seconds since epoch |
nonce | hex string | 16 cryptographically random bytes, hex-encoded. Example: a3f9c2b1d8e04f72 |
sig | hex string | HMAC-SHA256 of the message, in lowercase hex |
Exact formula
// 1. Build the message by concatenating with "|"
message = token + "|" + ts + "|" + nonce
// Concrete example:
// token = "e3df09b42ad23f8174eaf28bbe9ad96b"
// ts = 1740698753
// nonce = "a3f9c2b1d8e04f72"
message = "e3df09b42ad23f8174eaf28bbe9ad96b|1740698753|a3f9c2b1d8e04f72"
// 2. Sign with HMAC-SHA256 using the shared secret:
sig = HMAC-SHA256(secret, message) → result in lowercase hex
JSON the proxy sends to your server
POST https://your-php.com/api.php?proxy_claim=1
Content-Type: application/json
{
"token": "e3df09b42ad23f8174eaf28bbe9ad96b",
"ts": 1740698753,
"nonce": "a3f9c2b1d8e04f72",
"sig": "2b9f3e7d1c45a8f2..." // 64 hex chars
}
Response your server must return
// Valid token:
{ "ok": true, "url": "http://panel/live/USER/PASS/123.m3u8", "type": "live" }
// Invalid token:
{ "ok": false, "error": "Token not found" }
Stream types supported in the response
type | Proxy behavior |
|---|---|
live | Downloads and rewrites the HLS playlist from the provider. No FFmpeg. The client never sees the real URL. |
restream | Dynamically launches FFmpeg with -c copy. Generates its own HLS. Multiple clients share one FFmpeg process. |
vod | Range-aware tunnel for MP4/MKV. Native seek without buffering. |
series | Same as vod. |
Implement your own claims server
If you want to build your own integration, here are reference implementations in several languages.
<?php
// ── Endpoint: api.php?proxy_claim=1 ───────────────────────────────────────────
if (isset($_GET['proxy_claim'])) {
$body = json_decode(file_get_contents('php://input'), true);
$token = $body['token'] ?? null;
$ts = $body['ts'] ?? null;
$nonce = $body['nonce'] ?? null;
$sig = $body['sig'] ?? null;
if (!$token || !$ts || !$nonce || !$sig) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Missing fields']);
exit;
}
// Verify HMAC — the secret NEVER travels in the request
$secret = 'YOUR_SHARED_SECRET_HERE';
$msg = $token . '|' . $ts . '|' . $nonce;
$expected = hash_hmac('sha256', $msg, $secret);
// hash_equals hash_equals prevents timing attacks
if (!hash_equals($expected, $sig)) {
echo json_encode(['ok' => false, 'error' => 'Invalid signature']);
exit;
}
// Anti-replay: reject old timestamps (±5 min)
if (abs(time() - (int)$ts) > 300) {
echo json_encode(['ok' => false, 'error' => 'Timestamp expired']);
exit;
}
$info = buscar_token_en_db($token); // your function here
if (!$info) {
echo json_encode(['ok' => false, 'error' => 'Token not found']);
exit;
}
echo json_encode(['ok' => true, 'type' => $info['type'], 'url' => $info['url']]);
}
import hmac, hashlib, time, json
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = 'YOUR_SHARED_SECRET_HERE'
@app.route('/api', methods=['POST'])
def proxy_claim():
if request.args.get('proxy_claim') != '1':
return jsonify({'ok': False}), 404
body = request.get_json()
token, ts, nonce, sig = body.get('token'), body.get('ts'), body.get('nonce'), body.get('sig')
if not all([token, ts, nonce, sig]):
return jsonify({'ok': False, 'error': 'Missing fields'})
msg = f"{token}|{ts}|{nonce}"
expected = hmac.new(SECRET.encode(), msg.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return jsonify({'ok': False, 'error': 'Invalid signature'})
if abs(time.time() - int(ts)) > 300:
return jsonify({'ok': False, 'error': 'Timestamp expired'})
info = buscar_token(token)
if not info: return jsonify({'ok': False, 'error': 'Token not found'})
return jsonify({'ok': True, 'type': info['type'], 'url': info['url']})
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SECRET = 'YOUR_SHARED_SECRET_HERE';
app.post('/api', (req, res) => {
if (req.query.proxy_claim !== '1') return res.status(404).end();
const { token, ts, nonce, sig } = req.body;
if (!token || !ts || !nonce || !sig) return res.json({ ok: false, error: 'Missing fields' });
const msg = `${token}|${ts}|${nonce}`;
const expected = crypto.createHmac('sha256', SECRET).update(msg).digest('hex');
const a = Buffer.from(sig, 'utf8'), b = Buffer.from(expected, 'utf8');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b))
return res.json({ ok: false, error: 'Invalid signature' });
if (Math.abs(Date.now() / 1000 - +ts) > 300)
return res.json({ ok: false, error: 'Timestamp expired' });
const info = findToken(token);
if (!info) return res.json({ ok: false, error: 'Token not found' });
res.json({ ok: true, type: info.type, url: info.url });
});
package main
import (
"crypto/hmac"; "crypto/sha256"; "crypto/subtle"
"encoding/hex"; "encoding/json"; "fmt"; "math"; "net/http"; "time"
)
const secret = "YOUR_SHARED_SECRET_HERE"
type ClaimBody struct {
Token string `json:"token"`
Ts int64 `json:"ts"`
Nonce string `json:"nonce"`
Sig string `json:"sig"`
}
func proxyClaim(w http.ResponseWriter, r *http.Request) {
var b ClaimBody
json.NewDecoder(r.Body).Decode(&b)
msg := fmt.Sprintf("%s|%d|%s", b.Token, b.Ts, b.Nonce)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(msg))
expected := hex.EncodeToString(mac.Sum(nil))
if subtle.ConstantTimeCompare([]byte(expected), []byte(b.Sig)) != 1 {
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Invalid signature"})
return
}
if math.Abs(float64(time.Now().Unix()-b.Ts)) > 300 {
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "error": "Timestamp expired"})
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"ok": true, "type": "live", "url": "http://panel/live/USER/PASS/123.m3u8",
})
}
hash_equals in PHP, hmac.compare_digest in Python, timingSafeEqual in Node.js, subtle.ConstantTimeCompare in Go). Never compare strings with == for cryptographic data — an attacker can measure comparison timing.CONNECT proxy — Tunnel outgoing traffic through ptp-proxy
In addition to the claims system for IPTV clients, the proxy includes a handler for HTTP CONNECT requests. This allows any HTTP client (curl, your web app, or any service) to tunnel its outgoing connections through ptp-proxy, so all traffic exits through the network interface of the machine where ptp-proxy is running — regardless of OS. A typical use case is routing your service's requests through a different host or network.
Without CONNECT: client requests exit through its own network interface
With CONNECT: requests exit through ptp-proxy's network interface (its IP is what the provider sees)
Configuration in config.json
"connect_proxy": {
"enabled": true,
"use_claim": false,
"secret": "YOUR_SECRET_HERE",
"allowed_origins": ["127.0.0.1", "::1"],
"allowed_hosts": []
}
| Field | Description | Default |
|---|---|---|
enabled | Activates the CONNECT handler. false → 405 on any CONNECT | false |
use_claim | false → static secret; true → signed HMAC claim | false |
secret | Shared key. With use_claim: false it is the literal header value; with true it is the HMAC key | "" |
allowed_origins | Authorized source IPs. If the client IP is not here → 403 | ["127.0.0.1","::1"] |
allowed_hosts | Allowed destination hostnames. Empty = no restriction | [] |
Two authentication modes for CONNECT
Mode A — Static secret (<code>use_claim: false</code>)
The client sends the secret directly in each request. Simpler. Recommended when the client and the proxy are on the same host.
function curl_set_ptp_proxy_static(CurlHandle $ch): void {
curl_setopt($ch, CURLOPT_PROXY, 'http://127.0.0.1:8180');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROTO_HTTP);
curl_setopt($ch, CURLOPT_PROXYHEADER, ['X-Proxy-Secret: YOUR_SECRET_HERE']);
}
Mode B — HMAC claim (<code>use_claim: true</code>)
The client signs each CONNECT request with HMAC. The secret never travels in plaintext. Required if the proxy is exposed to the internet.
function curl_set_ptp_proxy_claim(CurlHandle $ch, string $url): void {
$parsed = parse_url($url);
$host = $parsed['host'];
$port = $parsed['port'] ?? ($parsed['scheme'] === 'https' ? 443 : 80);
$ts = time();
$nonce = bin2hex(random_bytes(8));
$msg = "connect|{$host}:{$port}|{$ts}|{$nonce}";
$sig = hash_hmac('sha256', $msg, 'YOUR_SECRET_HERE');
curl_setopt($ch, CURLOPT_PROXY, 'http://127.0.0.1:8180');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROTO_HTTP);
curl_setopt($ch, CURLOPT_PROXYHEADER, [
"X-Proxy-Ts: {$ts}",
"X-Proxy-Nonce: {$nonce}",
"X-Proxy-Sig: {$sig}",
]);
}
HMAC claim formula for CONNECT
message = "connect" + "|" + host + ":" + port + "|" + ts + "|" + nonce
sig = HMAC-SHA256(secret, message) → lowercase hex
Example: message = "connect|api.provider.com:443|1740698753|a3f9c2b1d8e04f72"
sig = hash_hmac('sha256', message, secret)
Safely exposing to the internet
If your service is on a remote server, you need the proxy to accept connections from the internet. With use_claim: true this is safe:
"connect_proxy": {
"enabled": true,
"use_claim": true,
"secret": "random-key-minimum-32-chars",
"allowed_origins": [],
"allowed_hosts": ["api.provider1.com", "api.provider2.com"]
}
use_claim: true- Secret never travels in plaintext
- Replay blocked (±60 s window)
- Destinations restricted by
allowed_hosts - No secret = no access
allowed_origins: []+use_claim: false+secret: ""→ public open proxyallowed_origins: []+use_claim: false→ secret exposed in transit
Generate a secure secret
# Linux / WSL2:
openssl rand -hex 32
API Endpoint Reference
Public reference for channels, VOD, recordings, profiles, storage and administration. Content routes, management routes and storage scopes follow different policies.
| Endpoint | Method | Access | Description |
|---|---|---|---|
/ or /health |
GET |
Public | Process liveness probe. It may use health protection when exposed beyond loopback. |
/playlist.m3u |
GET |
Public | M3U playlist containing enabled channels and their ptp-proxy HLS URLs. |
/channels/<id>/playlist.m3u8 |
GET |
Public | HLS playlist for a static proxy or restream channel. |
/stream?t=<token>[&dl=1] |
GET |
Public | Resolves a dynamic live, restream, VOD or series claim and returns authorised content. |
/download?t=<token> |
GET |
Public | Download alias for an authorised stream token. |
/api/transcode/profiles |
GET |
Mgmt only | Publishes installed profile IDs, labels, codec and variants without execution commands. |
/api/channels |
GET |
Mgmt only | Lists static channels and selected profiles. |
/api/recordings |
GET / POST |
Mgmt only | Lists or schedules recordings from authorised channels and URLs. |
/api/recordings/<id> |
GET / DELETE |
Mgmt only | Inspects or cancels one recording. |
/api/storage |
GET / HEAD |
Storage scope | Lists sources visible to an identity with storage:list. |
/api/storage/<source>/list?path=<relative> |
GET / HEAD |
Storage scope | Lists folders and files with size, timestamp, MIME, ETag and range support. |
/storage/<source>/<path>[?download=1] |
GET / HEAD |
Storage scope | Delivers an object with ranges, seek, HTTP conditionals and stream/download policy. |
/api/storage/sign |
POST |
Storage admin | Creates a temporary link bound to source, path, action and expiry. |
/api/storage/<source>/prepare |
POST |
Storage scope | Starts an idempotent v2 operation on a preparation-enabled source. |
/api/storage/<source>/operations/<id> |
GET / DELETE |
Storage scope | Reads progress or cancels an asynchronous operation. |
/api/storage/plugins |
GET / HEAD |
Storage admin | Non-sensitive status of configured plugin instances. |
/api/storage/plugin-providers |
GET / HEAD |
Storage admin | Inventory of installed providers, versions, capabilities and integrity. |
/api/storage/plugins/<source>/restart |
POST |
Storage admin | Administratively restarts a plugin source. |
/metrics |
GET |
Mgmt only | Prometheus metrics for server, restream, recording, storage and plugins. |
/proxy/raw?url=<encoded> |
GET |
Mgmt only | Diagnostic proxy for an allowed URL. Do not publish without management controls. |
CONNECT <host>:<port> |
CONNECT |
Public | HTTP CONNECT tunnel subject to its authentication and destination policy. |
PTP managed-node channel |
Outbound HTTPS |
Outbound channel | Agent enrolls, publishes heartbeat and receives jobs from PTP. It does not add an inbound managed API to the node. |
Management routes use general management IPs and key. Storage routes use storage:list, storage:read, storage:download, storage:prepare or storage:admin. The node channel uses its own credential and outbound connection.
PTP integration adds no public inbound management endpoint to ptp-proxy. The agent starts enrollment, heartbeat and long polling toward the PTP node API.
curl quick reference
# List channels
curl -H "Authorization: Bearer CHANGE_ME" http://127.0.0.1:8180/api/channels
# List storage sources
curl -H "Authorization: Bearer CHANGE_ME_LIST_TOKEN" http://127.0.0.1:8180/api/storage
# Read a file range
curl -H "Authorization: Bearer CHANGE_ME_READ_TOKEN" -H "Range: bytes=0-1048575" http://127.0.0.1:8180/storage/media/films/movie.mkv
# Inspect profiles
curl -H "Authorization: Bearer CHANGE_ME" http://127.0.0.1:8180/api/transcode/profiles
# Inspect plugins
curl -H "Authorization: Bearer CHANGE_ME_ADMIN_TOKEN" http://127.0.0.1:8180/api/storage/plugins
# Start preparation
curl -X POST -H "Authorization: Bearer CHANGE_ME_PREPARE_TOKEN" -H "Content-Type: application/json" --data '{"torrent_file":"sample.torrent"}' http://127.0.0.1:8180/api/storage/torrent/prepare