Encode videos to adaptive HLS streams, upload to Cloudflare R2, and generate embeddable player snippets.
- Encode single videos or entire directories to multi-bitrate HLS (H.264/AAC)
- Upload encoded files to Cloudflare R2 with proper MIME types and cache headers
- Preview uploaded videos locally via a built-in server streaming from R2
- Embed — generate copy-paste HTML snippets using Mux Player or HLS.js
The encoder automatically selects tiers at or below the source resolution:
| Tier | Resolution | Video Bitrate | Audio Bitrate |
|---|---|---|---|
| 360p | 640x360 | 800 kbps | 96 kbps |
| 480p | 854x480 | 1,400 kbps | 128 kbps |
| 720p | 1280x720 | 2,800 kbps | 128 kbps |
| 1080p | 1920x1080 | 5,000 kbps | 192 kbps |
| 1440p | 2560x1440 | 8,000 kbps | 192 kbps |
| 2160p | 3840x2160 | 14,000 kbps | 192 kbps |
Tiers above the source resolution are skipped (no upscaling).
- Python 3.10+
- ffmpeg — must be on PATH (or configured in
config.toml) - Cloudflare R2 bucket with public access enabled
Using uv (recommended)
# 1. Clone and install
git clone <repo-url> && cd hls-r2
uv sync
# 2. Configure R2 credentials
cp config.example.toml config.toml
# Edit config.toml with your R2 account details
# 3. Encode a video
uv run hls-r2 encode my-video.mp4
# 4. Upload to R2
uv run hls-r2 upload my-video
# Or upload everything:
uv run hls-r2 upload --all
# 5. Preview in browser
uv run hls-r2 preview my-video
# 6. Get embed HTML
uv run hls-r2 embed my-video# 1. Clone and install
git clone <repo-url> && cd hls-r2
pip install .
# 2. Configure R2 credentials
cp config.example.toml config.toml
# Edit config.toml with your R2 account details
# 3. Use the tool (same commands, no "uv run" prefix)
hls-r2 encode my-video.mp4
hls-r2 upload my-video
hls-r2 preview my-video
hls-r2 embed my-videoCopy config.example.toml to config.toml in your working directory:
[r2]
account_id = "your-account-id"
access_key_id = "your-access-key-id"
secret_access_key = "your-secret-access-key"
bucket_name = "my-video-bucket"
public_base_url = "https://cdn.example.com"
# prefix = "videos" # optional, default: "videos"
[encoding]
# output_dir = "output" # optional, default: "output"
# ffmpeg_path = "ffmpeg" # optional, if not on PATH
# ffprobe_path = "ffprobe" # optional, if not on PATHR2 credentials can also be set via environment variables, which take precedence over config.toml:
| Variable | Overrides |
|---|---|
R2_ACCOUNT_ID |
[r2] account_id |
R2_ACCESS_KEY_ID |
[r2] access_key_id |
R2_SECRET_ACCESS_KEY |
[r2] secret_access_key |
This is useful for CI/CD or when you don't want a config file on disk.
- Create an R2 bucket in the Cloudflare Dashboard
- Enable public access (Settings > Public Access) via custom domain or r2.dev subdomain
- Create an API token at R2 > Manage R2 API Tokens with read/write access to the bucket
- Copy the Access Key ID and Secret Access Key into
config.toml
These flags apply to all commands:
| Option | Description |
|---|---|
-v, --verbose |
Enable debug-level output (ffmpeg commands, detailed logging) |
-q, --quiet |
Suppress informational output, only show errors |
--version |
Show version and exit |
Encode a single video file or all videos in a directory to HLS.
# Single file
uv run hls-r2 encode video.mp4
# Directory (all supported formats)
uv run hls-r2 encode ./raw-videos/
# Custom output directory
uv run hls-r2 encode video.mp4 -o ./encoded/
# Overwrite existing encoded output
uv run hls-r2 encode video.mp4 --force| Option | Description |
|---|---|
-o, --output |
Output directory for encoded files (default: output) |
-c, --config |
Path to config file (default: ./config.toml) |
--force |
Overwrite existing encoded output for a slug |
Supported input formats: .mp4, .mkv, .mov, .avi, .webm, .flv, .wmv
Output structure:
output/
my-video/
master.m3u8 # Master playlist (adaptive bitrate)
360p/
stream.m3u8 # Per-tier playlist
segment_000.ts # MPEG-TS segments
segment_001.ts
720p/
stream.m3u8
segment_000.ts
...
1080p/
...
Upload encoded video(s) to R2. Validates credentials before uploading.
# Upload specific videos by slug (directory name)
uv run hls-r2 upload my-video another-video
# Upload all encoded videos
uv run hls-r2 upload --all
# Control upload concurrency
uv run hls-r2 upload --all --workers 4| Option | Description |
|---|---|
--all |
Upload all encoded videos in the output directory |
--workers |
Concurrent upload threads, 1–32 (default: 8) |
-o, --output |
Output directory containing encoded files (default: output) |
-c, --config |
Path to config file (default: ./config.toml) |
Start a local server to preview R2-hosted videos in the browser.
# List all videos and pick one
uv run hls-r2 preview
# Open directly to a specific video
uv run hls-r2 preview my-video
# Custom host/port
uv run hls-r2 preview --host 0.0.0.0 --port 3000Print HTML embed code to stdout. Pipe or copy-paste into your site.
# Both Mux Player and HLS.js snippets (default)
uv run hls-r2 embed my-video
# Mux Player only
uv run hls-r2 embed my-video --player mux
# HLS.js only
uv run hls-r2 embed my-video --player hlsjs
# Custom title
uv run hls-r2 embed my-video --title "My Great Video"
# Save to file
uv run hls-r2 embed my-video > embed.htmlList available videos (remote or local).
# List videos in R2
uv run hls-r2 list
# List locally encoded videos
uv run hls-r2 list --local| Option | Description |
|---|---|
--local |
List locally encoded videos instead of R2 |
-o, --output |
Output directory for local listing (default: output) |
-c, --config |
Path to config file (default: ./config.toml) |
src/hls_r2/
__init__.py # Package metadata
cli.py # Click CLI (entry point)
config.py # TOML config loading/validation
constants.py # Quality ladder, defaults, extensions
encoder.py # FFmpeg HLS encoding
uploader.py # R2 upload via boto3 (S3-compatible)
preview.py # Local HTTP preview server
embed.py # HTML snippet generation
py.typed # PEP 561 typed package marker
Uploaded files follow this pattern:
{prefix}/{slug}/master.m3u8
{prefix}/{slug}/360p/stream.m3u8
{prefix}/{slug}/360p/segment_000.ts
{prefix}/{slug}/720p/stream.m3u8
{prefix}/{slug}/720p/segment_000.ts
...
Default prefix is videos. The slug is derived from the source filename.
- Codec: H.264/AVC (libx264) + AAC audio
- Segments: MPEG-TS (.ts), 6 seconds each
- Playlist: HLS VOD with independent segments
- Scaling: Aspect ratio preserved with letterboxing/pillarboxing
- Single-pass: All tiers encoded in one ffmpeg invocation for speed
- No upscaling: Source resolution below a tier = that tier is skipped
MIT