B2C · Xtream · Reseller





Archive Storage Tiers
Three-tier archive system for 500+ concurrent clients. Configure in Config > Storage > Storage Tiers. Each tier has independent lifecycle, capacity calc and fallback behaviour.
Encoder
Write Buffer
2h hot window (NVMe or tmpfs)
→ mirror (continuous)
HDD RAID 5
50 TB, 14 days retention
→ prefetch on demand
Playback Cache
1 TB warm cache, 500+ clients
Clients
Segments older than mirror_min_age_minutes are COPIED to RAID continuously. The write_buffer holds a rolling 2h hot window so rewind-to-recent playback stays on fast storage. Only after evict_age_minutes (and only when verified-mirrored) are segments deleted from write_buffer. RAID always has the long-tail copy — no hourly burst transfers.
Tier
Purpose
Settings
Write Buffer
Receives segments 24/7, holds 2h hot window for fast recent-rewind playback, protects RAID from random IO. Device can be a physical NVMe partition (production) or tmpfs (dev/test box without physical NVMe — see tmpfs section below).
device_path (block device or 'tmpfs'), tmpfs_size_pct (10-50% MemAvailable when tmpfs), mirror_min_age_minutes, evict_age_minutes, flush_threshold_percent (force flush trigger)
HDD RAID Archive
Long-tail storage for full retention (default 14 days). Receives continuous mirror trickle from write_buffer — sequential write, no hourly bursts. Survives reboot. RAID survives single disk failure.
retention_days, raid_device, nginx_location, min_free_percent
Playback Cache (optional)
Read-side cache for popular files older than 2h (e.g. EPG catch-up). Populated from archive on demand after popularity threshold (3 hits / 5 min). Device can be NVMe or tmpfs.
prefetch_hours, max_sessions, eviction_policy (session/lru/fifo)
live writes × 2h = write_buffer hot window size. Example: 50 streams × 8 Mbps × 2h ≈ 36 GB. Add ~30% headroom for mirror trickle backlog → ~50 GB write_buffer is enough for 50 streams. RAID sized for full retention: 50 × 8 Mbps × 14 days ≈ 6 TB.
Stream.dvr_file_path/2 probes write_buffer → playback_cache → archive in order. A segment in the 2h hot window is served from write_buffer at native speeds even though it also exists on RAID. Nginx X-Accel-Redirect serves files directly without going through Phoenix.
Each tier can use a RAM-backed tmpfs virtual disk instead of a physical block device. Useful for dev boxes without spare NVMe, or for ephemeral test environments. Storage Tier card device dropdown contains a 'tmpfs (virtual disk from RAM)' option that replaces the device path selector with a percentage slider.
Allocation is specified as a percentage of MemAvailable (from /proc/meminfo) — step 10, valid values 10/20/30/40/50. Hard floor 10% (validation rejects below). Hard ceiling 50%. Default 30%. On a 100 GB MemAvailable host: 10% = 10 GB, 30% = 30 GB, 50% = 50 GB. Size is recomputed and remounted live when the slider is moved and saved.
If computed target would exceed 60% of current MemAvailable, the mount is refused and the tier remains offline (Stream.archive_path falls through to the next healthy tier). Prevents an aggressive setting from killing the BEAM via OOM. The guard runs both at boot and on every Save in the UI.
tmpfs lives in RAM — any in-flight buffer content is LOST when the host reboots. The fstab entry written at provision time re-mounts the empty tmpfs automatically on next boot, but old segments do not return. Mirror to archive continues normally for everything that was already mirrored before the reboot — only the last unmirrored window (typically < mirror_min_age_minutes worth) is lost. For production, use a physical NVMe device; tmpfs is intended as a dev/test substitute.
Use tmpfs for write_buffer when: (a) dev/test box has no spare NVMe but you still want the full DVR pipeline running for integration testing, (b) operator wants to validate mirror + archive behaviour without buying NVMe up-front, (c) ephemeral testing where loss-on-reboot is acceptable. Do NOT use tmpfs in production — RAM is volatile and orders of magnitude more expensive than NVMe per GB of retention.
When the write tier is tmpfs, StorageTierFlush uses tighter defaults — mirror_min_age 2 min, evict_age 15 min (vs 5 min / 120 min for physical NVMe). RAM is more expensive than disk so the rolling hot window is kept smaller. Override via MIRROR_MIN_AGE_MINUTES / EVICT_AGE_MINUTES env vars if needed.
Tiers are configured once via Config > Storage > Storage Tiers and provisioned automatically — no manual mkfs/mount/fstab editing on the host. StorageTier.ensure_all_provisioned/0 runs at every boot from SystemInitializer and is idempotent: already-mounted tiers are skipped, missing devices are no-ops, tmpfs tiers re-mount cleanly after reboot.
On first boot (no write tier in DB), the system auto-creates a tmpfs-backed write tier with 30% MemAvailable allocation, mount point /mnt/nvme_write. This gives a working DVR pipeline out-of-the-box on any host with enough RAM. Operator can later switch device to a physical NVMe via UI without losing other tier settings.
Stream.archive_path/1 returns the first 'healthy' tier — write_buffer if mounted+writable+free_pct above safety floor, otherwise falls back to archive (degraded mode). Safety floor 20% for tmpfs (RAM-strict), 2% for block devices. tier_health endpoint exposes per-tier mounted/writable/free_pct for monitoring.
Changing tmpfs_size_pct in UI triggers umount + remount with the new size on Save. Active DVR writes are briefly paused (mount transition < 1 s). Recommendation: change tmpfs size only when DVR write rate is low, and consider restarting streams after to ensure they pick up the new mount cleanly.
Base URL — OSTV Player
Package Fields
Packages API
Password Policy
Reseller API
Settings
User Fields
User Management
Base URL — OSTV Player
The OSTV Player application allows users to change the server base URL (API address). This feature is available only for users with provider set to root.
Reason: Changing the base URL is intended only for administrators/root users so they can switch between servers (e.g. production vs. testing). Regular users have a fixed base URL and cannot change it.
User Fields
Packages API
Returns a list of all active packages with prices and billing periods.
List of management users (resellers / providers).
Returns credit balance for a specific management user.
Package Fields
Reseller API
API for resellers/providers. Authorization: Authorization: Bearer mtk_... (token from Manage Users). Reseller sees only their own clients (owner_id).
Password Policy
Password is required when creating a subscriber. Rules:
Minimum 8 characters
At least 1 uppercase letter (A-Z)
At least 1 special character (!@#$%^&* etc.)
Applies to UI and API. On update, password is optional, but if provided must meet the rules.
Settings
Tier Auto-Provisioning & Safety
Capacity + filesystem auto-detection: when a free NVMe is selected from the dropdown, capacity (GB) is read from lsblk -bn -o SIZE and current FS from blkid (with whole-disk fallback). The form shows the values read-only — no manual entry, no mistakes.
Tier Flush Mechanism — Continuous Mirror
StorageTierFlush GenServer runs every 60s. Instead of waiting for an hour-dir to "age out" and moving it as a 5GB burst, it trickles segments to RAID continuously while keeping a 2h hot window on NVMe for fast "recent rewind" playback. The two phases are decoupled — copy and delete are separate, verified operations.
Why continuous mirror beats "wait 60min, batch-move": (1) RAID load is even — no 5GB bursts at hour boundaries, just trickle of 7MB segments; (2) Most hot data is already on RAID — disaster recovery only loses the last few minutes; (3) Eviction is verified per-file — a partial mirror failure is detected before any NVMe data is lost.
In-progress segment safety: the mirror only picks files older than MIRROR_MIN_AGE_MINUTES via find -mmin +5. A segment file still being appended by FFmpeg won't appear in the list — no chance of partial-write transfer.
Per-deployment Tuning (.env)
500–1000
Sizing rule:
DVR Preview Calendar
The DVR Preview tab shows a server-rendered calendar that highlights only the days for which recordings exist on disk. The scan runs across every tier (write_buffer + read_cache + archive + legacy) so a segment that was just flushed from NVMe to RAID stays visible without delay.
Days with recordings are clickable (blue) — clicking pre-fills the datetime input with that day at 00:00; the user then picks the hour. Days with no recording are dimmed and inert.
Implemented in Stream.available_dvr_days/1 via find -mindepth 4 -maxdepth 4 -type d on each tier mount; deduplicated across tiers (a day is shown once even if it has segments in multiple tiers during a flush window).
Overview
The B2C (Business-to-Consumer) API provides authentication and stream access for end-user player applications. Users authenticate with username/password and receive a time-limited auth token for subsequent requests.
Authenticate a user and obtain an auth_token for subsequent API calls.
Method
Endpoint
Request Body
Parameter
Type
Required
Description
Yes
User login name
User password
Example Request
Response Fields
Success Response (200)
Encrypted authentication token for subsequent API calls
Token expiration date/time in ISO 8601 format
Example Success Response
Error Response (403)
Error description message
HTTP status code
Example Error Response
Retrieve information about the authenticated user using the auth token.
Response Fields
Username of the authenticated user
Server message (empty if none)
Authentication status (1 = authenticated)
Account status: Active, Banned, Disabled, Trial
Account expiration date (null if unlimited)
Number of current active connections
Account creation date in ISO 8601 format
Maximum allowed simultaneous connections
List of allowed output stream formats
Provider name of the user's owner/reseller (null if none)
URL of the provider logo image (null if none)
URL of the provider collapse logo image 208x208 (null if none)
Current server time in ISO 8601 format
Example Request
Retrieve categories filtered by type. Available types: live (streams), vod (movies), series.
Category type: live, vod, or series
Category ID as string
Category display name
Parent category ID (0 if root)
MD5 hash of content for cache invalidation
Example Request
Retrieve detailed information for a specific VOD item, including metadata and stream links.
VOD stream ID (UUID)
Cover image URL
TMDB identifier
Backdrop image URL
Genre string
Plot / description
Cast members
Rating value (0-10)
Director name
Release date
Duration in seconds
Duration as formatted string
Array of category IDs
1 if adult content, 0 otherwise
VOD stream ID
VOD title
Date added (ISO 8601)
Stream link URLs
Video/audio codec metadata
Retrieve series information with full season and episode listings, grouped by season number.
Series ID (UUID)
Season number
Season display name
Number of episodes in season
Air date of the season
Season overview / description
Season cover image URL
Map keyed by season number string. Each value is an array of episode objects.
Episode Object Fields
Episode ID
TMDB episode ID
Episode number within season
Date added (ISO 8601)
Season number
Episode info object (same structure as VOD info)
Stream link URLs
Video/audio codec metadata
Token expiration depends on user role: admin = 365 days, other = 30 days|Disabled accounts cannot authenticate (returns 403)|Each successful login updates the user's last_login timestamp|Tokens are encrypted and contain user_id, username, role and expiration
Success Response (200)
Auth token obtained from /auth endpoint