A high-performance, security-hardened media proxy server with automatic format detection, transcoding, and HLS/DASH playlist rewriting.
- Multi-format Support: Video (MP4, MKV, AVI, WebM, WMV, FLV, MPEG, VOB, TS, HLS, DASH) and Audio (MP3, AAC, M4A, FLAC, OGG, WAV, AIFF, WMA, AC3)
- Automatic Transcoding: Converts incompatible formats to web-compatible MP4/MP3
- HLS/DASH Proxy: Rewrites playlists and manifests with proxied segment URLs, preserving headers
- Security Hardened: SSRF protection, DNS rebinding prevention, header injection sanitization
- Production Ready: Rate limiting, CORS support, request logging, graceful shutdown
# Build and run
go build -o aio-proxy && ./aio-proxydocker build -t aio-proxy .
docker run -p 8080:8080 aio-proxyAll settings are configurable via environment variables, allowing runtime tuning without rebuilding.
| Environment Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Server port |
BASE_URL |
http://127.0.0.1:PORT |
Public URL for playlist rewriting |
ALLOW_INTERNAL_NETWORKS |
false |
Allow proxying to private IPs (dangerous) |
ENABLE_YTDLP |
true |
Enable type=ytdlp support |
ENABLE_HLS |
true |
Enable HLS playlist proxying and rewriting |
ENABLE_DASH |
true |
Enable DASH manifest proxying and rewriting |
ENABLE_DEFAULT_HEADERS |
true |
Add browser-like headers to upstream requests |
PROXY_MEDIA_URLS |
true |
Proxy embedded URLs in playlists (segments, subtitles, audio, keys). When false, converts to absolute URLs only |
CACHE_ENABLED |
false |
Enable caching for HLS segments |
CACHE_TYPE |
memory |
Cache backend: memory or disk |
CACHE_PATH |
/tmp/aio-proxy-cache |
Directory for disk cache |
CACHE_MAX_SIZE |
512M |
Maximum cache size (e.g., 512M, 1G) |
CACHE_TTL |
300 |
Cache entry TTL in seconds |
MAX_CONCURRENT_STREAMS |
100 |
Maximum simultaneous proxy connections (global) |
MAX_STREAMS_PER_IP |
10 |
Maximum simultaneous streams per client IP |
MAX_BANDWIDTH_PER_IP |
(unlimited) | Bandwidth limit per client IP (e.g., 10M, 500K, 1G) |
MAX_CONCURRENT_TRANSCODE |
5 |
Maximum simultaneous ffmpeg processes (global) |
MAX_TRANSCODE_PER_IP |
2 |
Maximum simultaneous transcodes per client IP |
HW_ACCEL |
auto |
Hardware acceleration: auto, vaapi, nvenc, qsv, off |
API_RATE_LIMIT |
0 |
Requests per minute per IP for /probe and /proxy (0 = unlimited, 60-300 recommended for production) |
API_RATE_BURST |
10 |
Burst allowance for API rate limiting |
TRANSCODE_QUEUE_TIMEOUT |
30 |
Seconds to wait for transcoder slot before 503 |
READ_TIMEOUT |
30 |
HTTP server read timeout (seconds) |
IDLE_TIMEOUT |
120 |
HTTP server idle timeout (seconds) |
DIAL_TIMEOUT |
30 |
Upstream connection timeout (seconds) |
HTTP_TIMEOUT |
0 |
Overall HTTP client timeout (0 = unlimited) |
MAX_REQUEST_BODY_SIZE |
0 |
Max request body size (e.g., 1M, 10K; 0 = unlimited) |
CORS_ORIGINS |
* |
CORS allowed origins (e.g., https://example.com) |
TRUSTED_PROXIES |
(none) | Comma-separated IPs/CIDRs to trust for client IP headers |
LOG_LEVEL |
info |
Logging verbosity: error, warn, info, debug |
Security Note: Proxy headers (
CF-Connecting-IP,X-Forwarded-For,X-Real-IP) are only trusted from IPs matchingTRUSTED_PROXIES. Direct connections always use the TCP source IP (unspoofable).
# High-capacity server with longer timeouts
MAX_CONCURRENT_TRANSCODE=10 \
TRANSCODE_QUEUE_TIMEOUT=60 \
DIAL_TIMEOUT=60 \
./aio-proxy
# Enable memory caching (default, fast but lost on restart)
docker run -p 8080:8080 \
-e CACHE_ENABLED=true \
-e CACHE_MAX_SIZE=1G \
-e CACHE_TTL=600 \
aio-proxy
# Enable disk caching (persists across restarts, good for NVMe SSDs)
docker run -p 8080:8080 \
-v /mnt/cache:/cache \
-e CACHE_ENABLED=true \
-e CACHE_TYPE=disk \
-e CACHE_PATH=/cache \
-e CACHE_MAX_SIZE=10G \
aio-proxy
# Limit bandwidth to 10 MB/s per stream (prevents single user hogging all bandwidth)
docker run -p 8080:8080 \
-e MAX_BANDWIDTH_PER_IP=10M \
-e MAX_STREAMS_PER_IP=5 \
aio-proxy
# Behind nginx at 10.0.0.1
docker run -p 8080:8080 \
-e TRUSTED_PROXIES=10.0.0.1 \
-e MAX_TRANSCODE_PER_IP=3 \
aio-proxy
# Behind Cloudflare (trust their IP ranges)
docker run -p 8080:8080 \
-e TRUSTED_PROXIES="173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22" \
aio-proxy
# Hardware acceleration with VA-API (AMD/Intel on Linux)
docker run -p 8080:8080 \
--device=/dev/dri:/dev/dri \
aio-proxy
# Hardware acceleration with NVIDIA (requires glibc-based image, not Alpine)
# docker run -p 8080:8080 \
# --runtime=nvidia \
# --gpus all \
# your-nvenc-image
# Force CPU encoding (disable hardware acceleration)
docker run -p 8080:8080 \
-e HW_ACCEL=off \
aio-proxyHardware Acceleration: The default Alpine image supports VA-API (AMD/Intel). For NVIDIA NVENC, you'll need a glibc-based image (e.g., Ubuntu) with NVIDIA drivers.
GET /probe?url=<target>&headers=<json>&h=<base64>&type=<mode>
Returns media metadata including duration, video, audio, and subtitle track information.
| Parameter | Description |
|---|---|
url |
Target media URL (required) |
headers |
Custom headers as JSON |
h |
Custom headers as base64-encoded JSON |
type |
Probe mode: native, fast, or full (default) |
# Full probe - all formats, all metadata (slowest)
curl "http://localhost:8080/probe?url=https://example.com/video.mkv"
curl "http://localhost:8080/probe?url=https://example.com/video.mkv&type=full"
# Fast probe - ffprobe with reduced buffer
curl "http://localhost:8080/probe?url=https://example.com/video.mkv&type=fast"
# Native probe - in-process parsing, no ffprobe spawn (fastest, MKV/MP4 only)
curl "http://localhost:8080/probe?url=https://example.com/video.mkv&type=native"| Mode | Time* | Formats | Returns |
|---|---|---|---|
full |
~400ms | All | Duration + all tracks |
fast |
~350ms | All | Duration + all tracks |
native |
~150ms | MKV, MP4 | Duration only |
*Times vary based on network latency and file structure
Response:
{
"url": "https://example.com/video.mkv",
"duration": 3847.234,
"duration_formatted": "1:04:07",
"video": [
{ "index": 0, "codec_name": "hevc", "width": 1920, "height": 1080 }
],
"audio": [
{
"index": 1,
"codec_name": "aac",
"language": "eng",
"title": "English",
"default": true
},
{ "index": 2, "codec_name": "aac", "language": "spa", "title": "Spanish" }
],
"subtitles": [
{
"index": 3,
"codec_name": "subrip",
"language": "eng",
"title": "English"
}
]
}GET /proxy?url=<target>&type=<type>&headers=<json>&name=<n>&method=<method>&download=<bool>&seek=<seconds>&raw=<bool>
| Parameter | Required | Description |
|---|---|---|
url |
Yes* | Target URL to proxy |
d |
Yes* | Segment params for HLS/DASH (see format below) |
type |
No | Force type: web, video, audio, hls, dash, ytdlp |
headers |
No | JSON object of headers to send upstream |
h |
No | Base64-encoded headers (alternative to headers) |
name |
No | Media name for playlist metadata and download filename |
method |
No | HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD |
body |
No | Base64-encoded request body (for POST/PUT/PATCH, max ~6KB due to URL limits) |
download |
No | Set to true for download mode with attachment header |
stream |
No | Set to true for HLS/DASH→MP4 streaming mode (not seekable, but starts immediately) |
media |
No | For type=ytdlp: video (default) or audio |
seek |
No | Start position in seconds (e.g., seek=3600 for 1 hour) |
raw |
No | Skip transcoding, pass through original format. Works with download=true for raw file downloads |
*Either url or d is required.
d= parameter format: <base64>,<key>=<value>,...
- Base64 part: JSON with
u(url),h(headers),n(name),t(token) - Template vars: comma-separated
key=valuepairs that replace$key$in the URL - Example:
d=eyJ1Ijo...,Number=$Number$→ player substitutes →d=eyJ1Ijo...,Number=5 - Uses comma separator to avoid
&which causes XML parsing issues in DASH manifests
| Header | Description |
|---|---|
X-Content-Duration |
Media duration in seconds (when available) |
Content-Type |
video/mp4 / audio/mpeg (transcoded), or original format type when raw=true |
Note: The
bodyparameter is transmitted via URL query string, which has browser/server limits (typically 2-8KB). For large payloads, make a direct POST request to the proxy endpoint with the body in the request itself.
# Video streaming (returns X-Content-Duration header)
curl -I "http://localhost:8080/proxy?url=https://example.com/video.mp4"
# Seek to 1 hour into movie
curl "http://localhost:8080/proxy?url=https://example.com/movie.mkv&download=true&seek=3600"
# HLS with custom headers (default: native HLS, seekable)
curl "http://localhost:8080/proxy?type=hls&url=https://cdn.example.com/stream.m3u8&headers={\"Referer\":\"https://example.com\"}"
# HLS streaming mode (converts to MP4, not seekable but starts immediately)
curl "http://localhost:8080/proxy?type=hls&url=https://cdn.example.com/stream.m3u8&stream=true"
# DASH with custom headers (native DASH, seekable)
curl "http://localhost:8080/proxy?type=dash&url=https://cdn.example.com/manifest.mpd&headers={\"Referer\":\"https://example.com\"}"
# DASH download mode (converts to MP4)
curl -o video.mp4 "http://localhost:8080/proxy?type=dash&url=https://cdn.example.com/manifest.mpd&download=true"
# Download video
curl -o movie.mp4 "http://localhost:8080/proxy?url=https://example.com/video.mkv&download=true&name=Movie"
# yt-dlp: Stream YouTube/supported sites as MP4
curl "http://localhost:8080/proxy?type=ytdlp&url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"
# yt-dlp: Extract audio only as MP3
curl "http://localhost:8080/proxy?type=ytdlp&media=audio&url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"
# POST request via proxy
curl "http://localhost:8080/proxy?type=web&method=POST&url=https://api.example.com/data&body=eyJrZXkiOiJ2YWx1ZSJ9"
# Download raw MKV file (no transcoding)
curl -O "http://localhost:8080/proxy?url=https://example.com/movie.mkv&download=true&raw=true"Nested URL Unwrapping: If the
urlparameter points back to the proxy itself (e.g., from a redirect or embedded player), it will be automatically unwrapped. Parameters from the inner URL override the outer request. Supports one level of nesting.
| Mode | Parameter | Browser | Seekable | Notes |
|---|---|---|---|---|
| Native HLS/DASH (default) | (none) | Player required | ✅ Yes | Playlist/manifest URLs rewritten to proxy |
| Streaming MP4 | stream=true |
Native | ❌ No | Starts immediately, no seeking |
| Download MP4 | download=true |
Native | ✅ Yes | Must buffer fully first |
Default (Native HLS/DASH): Returns a rewritten playlist/manifest with proxied segment URLs. Works with hls.js, dash.js, VLC, mpv, and any compatible player. Fully seekable.
http://localhost:8080/proxy?type=hls&url=https://cdn.example.com/stream.m3u8
http://localhost:8080/proxy?type=dash&url=https://cdn.example.com/manifest.mpd
Streaming Mode: Converts HLS/DASH to fragmented MP4 on-the-fly. Plays in any browser but cannot seek (duration unknown during transcoding).
http://localhost:8080/proxy?type=hls&url=https://cdn.example.com/stream.m3u8&stream=true
http://localhost:8080/proxy?type=dash&url=https://cdn.example.com/manifest.mpd&stream=true
Download Mode: Converts HLS/DASH to complete MP4 file. Seekable after fully buffered.
http://localhost:8080/proxy?type=hls&url=https://cdn.example.com/stream.m3u8&download=true
http://localhost:8080/proxy?type=dash&url=https://cdn.example.com/manifest.mpd&download=true
GET /
Returns:
{
"status": "healthy",
"service": "AIO Proxy",
"version": "0.0.1"
}The proxy automatically detects media format using:
- URL file extension
Content-Typeheader (via HEAD request)- Magic bytes (file signature)
Video (output: MP4)
- Transmux (fast, lossless): Any container with H.264/H.265/VP9/AV1 video + AAC/MP3/Opus/FLAC/Vorbis audio
- Transcode (slower, uses HW accel): Containers with incompatible codecs (MPEG-2, WMV, AC3, etc.)
Audio (output: MP3)
- Transmux: MP3 sources only
- Transcode: AAC, FLAC, OGG, WAV, AIFF, WMA, M4A, Opus, Vorbis
HLS
- Playlist rewriting with proxied segments
- Supports encrypted streams (KEY URIs rewritten)
- Multi-track audio/subtitle passthrough
DASH
- Manifest rewriting with proxied segments
- Supports SegmentTemplate with
$Number$ ,$Time$ ,$Bandwidth$ ,$RepresentationID$ - BaseURL, initialization, and media URL rewriting
- Multi-track audio/subtitle passthrough
The proxy automatically detects if the source codecs are web-compatible:
| Operation | Description | Speed | Quality |
|---|---|---|---|
| Transmux | Change container only, copy streams | Fast | Lossless |
| Transcode | Re-encode streams | Slower | Slight loss |
Web-compatible video codecs: H.264, H.265/HEVC, VP8, VP9, AV1 Web-compatible audio codecs: AAC, MP3, Opus, FLAC, Vorbis
When the source has compatible codecs, streams are copied (transmuxed) into MP4. When codecs are incompatible, hardware-accelerated encoding is used (transcode).
- Blocks connections to private IP ranges (10.x, 172.16-31.x, 192.168.x)
- Blocks localhost and link-local addresses
- Blocks cloud metadata endpoints (169.254.169.254)
- DNS rebinding prevention via connection-time IP validation
Stream limits (all proxy connections):
- Global limit: Maximum concurrent streams (default: 100, via
MAX_CONCURRENT_STREAMS) - Per-IP limit: Maximum streams per client (default: 10, via
MAX_STREAMS_PER_IP) - Bandwidth limit: Maximum bytes/sec per client IP across all their connections (via
MAX_BANDWIDTH_PER_IP)
Transcode limits (ffmpeg processes only):
- Global limit: Maximum concurrent transcodes (default: 5, via
MAX_CONCURRENT_TRANSCODE) - Per-IP limit: Maximum transcodes per client (default: 2, via
MAX_TRANSCODE_PER_IP) - Queue timeout: 503 response if slot unavailable (default: 30s, via
TRANSCODE_QUEUE_TIMEOUT)
HLS/DASH segments: Segment requests (identified by the d= parameter) bypass per-IP stream limits but remain subject to bandwidth limits. This allows HLS/DASH playback with many concurrent segment requests without triggering rate limits.
Bandwidth format accepts: 10M (10 MB/s), 500K (500 KB/s), 1G (1 GB/s), or raw bytes. The bandwidth limit is shared across all of a client's connections, preventing abuse even with many concurrent requests.
Stream limits protect connection count; bandwidth limits protect network saturation; transcode limits protect CPU. Set any *_PER_IP to 0 to disable per-IP limiting.
- CRLF injection prevention in forwarded headers
- Playlist name sanitization (control characters, quotes removed)
The proxy strips all IP-revealing headers before forwarding to upstream:
X-Forwarded-For,X-Real-IP,CF-Connecting-IP,True-Client-IP,X-Client-IPForwarded,Via,X-Forwarded-Host,X-Forwarded-Proto
This means upstream servers only see the proxy's IP address, not your clients' IPs. Useful for scenarios like shared streaming accounts where the upstream service restricts to a single IP.
# Standard build
go build -o aio-proxy
# Cross-compile for Linux
GOOS=linux GOARCH=amd64 go build -o aio-proxy-linuxThe version is defined in main.go:
var Version = "0.0.1"Update the Version variable to release a new version.
- Go 1.21+ (for
mapspackage) - FFmpeg (must be in PATH)
- yt-dlp (optional, for
type=ytdlpsupport)