Note: This project is a collaborative effort between human creativity and AI assistance. The majority of the codebase was generated and refined through neural network collaboration, demonstrating the potential of human-AI pair programming.
A TypeScript application that converts HLS VOD (Video on Demand) streams into live streaming format. This tool allows you to simulate live streaming behavior from static VOD content by manipulating M3U8 playlists with proper media sequencing and discontinuity handling.
- VOD-to-Live Conversion: Transforms static VOD manifests into live streaming playlists
- Multi-Variant Support: Handles master playlists with multiple quality variants
- Sliding Window: Implements proper HLS sliding window mechanism for live streams
- Discontinuity Handling: Correctly manages discontinuity tags when looping content
- Duration Control: Extend or limit VOD content to specific durations
- Manifest Caching: Caches manifests for improved performance
- CORS Enabled: Supports cross-origin requests for web players
All test streams used in this repository are sourced from the Mux test streams page. The following pre-configured streams are available:
- BigBuckBunny - Standard test stream
- ARTEChina - Alternative test content
- DKTurntable - PTS shifted by 2.3s
- TearsOfSteal - Includes IMSC Captions
- Node.js >= 18.0.0
- npm >= 9.0.0
npm installStart the development server with auto-reload:
npm run devThe server will start on port 3000 by default (configurable via PORT environment variable).
The default docker-compose.yaml is configured for local development. It mounts the current directory into the container and runs npm run dev with hot-reload, alongside Prometheus and Grafana for metrics:
docker compose up| Service | URL |
|---|---|
| Streamer | http://localhost:3000 |
| Prometheus | http://localhost:9090 |
| Grafana | http://localhost:3001 |
Grafana default credentials: admin / admin.
Build the application:
npm run buildStart the production server:
npm startdocker-compose-prod.yaml builds the app from the Dockerfile and runs a single optimised container (no Prometheus/Grafana):
docker compose -f docker-compose-prod.yaml up -dGenerate or manipulate a VOD (Video on Demand) manifest.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
stream |
string | Yes | Stream identifier (e.g., "BigBuckBunny") or full URL to HLS manifest |
variant |
number | No | Variant index for quality selection. If omitted, returns master manifest |
duration |
number | No | Target duration in seconds. Content will loop to fill duration if needed |
Examples:
# Get master manifest for BigBuckBunny
curl "http://localhost:3000/vod.m3u8?stream=BigBuckBunny"
# Get specific variant (variant 0) for BigBuckBunny
curl "http://localhost:3000/vod.m3u8?stream=BigBuckBunny&variant=0"
# Get 120-second duration VOD (will loop if original is shorter)
curl "http://localhost:3000/vod.m3u8?stream=BigBuckBunny&variant=0&duration=120"
# Use custom stream URL
curl "http://localhost:3000/vod.m3u8?stream=https://example.com/vod/master.m3u8"Response:
- Content-Type:
application/vnd.apple.mpegurl - Body: M3U8 playlist with
EXT-X-ENDLISTtag (VOD)
Convert a VOD stream to live streaming format with sliding window behavior.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
stream |
string | Yes | Stream identifier (e.g., "BigBuckBunny") or full URL to HLS manifest |
variant |
number | No | Variant index for quality selection. If omitted, returns master manifest |
start |
number | No | Stream start timestamp in milliseconds (epoch). Defaults to current time |
now |
number | No | Current timestamp in milliseconds (epoch). Defaults to current time |
windowSize |
number | No | Number of segments in the sliding window (default: 3) |
Examples:
# Get live master manifest
curl "http://localhost:3000/live.m3u8?stream=BigBuckBunny"
# Get live variant with default sliding window
curl "http://localhost:3000/live.m3u8?stream=BigBuckBunny&variant=0"
# Simulate live stream that started 5 minutes ago
START_TIME=$(($(date +%s) * 1000 - 300000))
curl "http://localhost:3000/live.m3u8?stream=BigBuckBunny&variant=0&start=${START_TIME}"
# Custom window size (5 segments instead of default 3)
curl "http://localhost:3000/live.m3u8?stream=BigBuckBunny&variant=0&windowSize=5"
# Custom stream URL in live mode
curl "http://localhost:3000/live.m3u8?stream=https://example.com/vod/master.m3u8&variant=0"Response:
- Content-Type:
application/vnd.apple.mpegurl - Body: M3U8 playlist with
EXT-X-MEDIA-SEQUENCEandEXT-X-DISCONTINUITY-SEQUENCEtags (Live)
Live Stream Behavior:
The live endpoint simulates a live stream by:
- Calculating elapsed time since
starttimestamp - Creating a sliding window of segments from the VOD content
- Looping the VOD content with proper discontinuity markers
- Updating
EXT-X-MEDIA-SEQUENCEto reflect the current position - Omitting
EXT-X-ENDLISTtag to indicate ongoing stream
Health check endpoint for monitoring and load balancers.
Response:
- Status: 200 OK
Example:
curl "http://localhost:3000/health"Prometheus metrics endpoint. Returns all metrics in the standard Prometheus text exposition format.
Response:
- Content-Type:
text/plain; version=0.0.4; charset=utf-8 - Body: Prometheus text format metrics
Example:
curl "http://localhost:3000/metrics"| Metric | Type | Labels | Description |
|---|---|---|---|
streamer_requests_total |
Counter | stream, endpoint, type, result |
Total incoming manifest requests. endpoint: live|vod; type: master|variant; result: success|error |
streamer_request_duration_seconds |
Histogram | stream, endpoint, type |
End-to-end latency of manifest request handling. Buckets: 10ms–5s |
streamer_upstream_fetches_total |
Counter | stream, type, status |
Upstream manifest fetches from origin. status is the HTTP status code (e.g. 200, 404) or error for network failures |
streamer_upstream_fetch_duration_seconds |
Histogram | stream, type |
Latency of upstream manifest downloads (cache hits excluded). Buckets: 50ms–10s |
streamer_cache_hits_total |
Counter | stream, type |
Manifest requests served from in-memory cache |
streamer_manifest_requests_total |
Counter | endpoint, type |
Manifest requests by endpoint and variant track (e.g. type: master, variant0, variant1, …) |
streamer_cache_size |
Gauge | — | Number of manifest entries currently held in the in-memory cache |
In addition to the custom metrics above, the standard Node.js default metrics (event loop lag, GC, heap, etc.) are collected automatically.
<!DOCTYPE html>
<html>
<head>
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
</head>
<body>
<video id="my-video" class="video-js" controls preload="auto" width="640" height="264">
<source src="http://localhost:3000/live.m3u8?stream=BigBuckBunny&variant=0" type="application/x-mpegURL">
</video>
<script src="https://vjs.zencdn.net/8.10.0/video.min.js"></script>
<script>
var player = videojs('my-video');
</script>
</body>
</html>Note: This example uses
https://localhostto work in browser environment, which requires SSL to be enabled. See SSL configuration for setup instructions.
# Play live stream
ffplay "http://localhost:3000/live.m3u8?stream=BigBuckBunny&variant=0"
# Record live stream to file
ffmpeg -i "http://localhost:3000/live.m3u8?stream=BigBuckBunny&variant=0" \
-c copy output.mp4- Fetches the master manifest from the source URL
- Optionally fetches a specific variant manifest
- If
durationis specified, loops segments to reach target duration - Adds VOD-specific tags (
EXT-X-PLAYLIST-TYPE:VOD,EXT-X-ENDLIST) - Returns the processed manifest
- Fetches the VOD manifest from the source
- Calculates elapsed time since the
starttimestamp - Determines which segments should be in the current sliding window
- Adds discontinuity tags when looping back to the beginning
- Sets proper
EXT-X-MEDIA-SEQUENCEandEXT-X-DISCONTINUITY-SEQUENCE - Removes VOD-specific tags to indicate live streaming
- Returns a live manifest that updates over time
streamer/
├── src/
│ ├── streamer.ts # Core streaming logic
│ └── m3u8.ts # M3U8 parser and encoder
├── streams.ts # Predefined test streams
├── main.ts # Express server setup
└── package.json
- Streamer Class (src/streamer.ts) - Main logic for VOD and live conversion
- M3U8 Parser (src/m3u8.ts) - Handles parsing and encoding of M3U8 playlists
- Express Server (main.ts) - HTTP API endpoints
| Variable | Description | Default |
|---|---|---|
PORT |
Server port | 3000 |
SSL |
Any non-empty value enables HTTPS; leave empty to disable | (empty) |
SSL_KEY_PATH |
Path to the private key file | cert/key.pem |
SSL_CERT_PATH |
Path to the certificate file | cert/cert.pem |
SSL_PASSPHRASE |
Passphrase for the private key (if set during generation) | (none) |
Browsers require HTTPS to play HLS streams. In production, TLS is typically terminated at a reverse proxy (e.g. nginx with Let's Encrypt), which is why the Docker Compose setup runs with SSL= (disabled).
For local development, enable SSL in your .env:
SSL=1
# Optional — override defaults:
# SSL_KEY_PATH=cert/key.pem
# SSL_CERT_PATH=cert/cert.pem
# SSL_PASSPHRASE=your-passphraseYou can use your own certificate or generate a self-signed one:
mkdir -p cert && openssl req -x509 -newkey rsa:4096 \
-keyout cert/key.pem -out cert/cert.pem -sha256 -days 365- Caching is in-memory only (will not persist across restarts)
- No authentication or rate limiting included
This project demonstrates AI-assisted development. Contributions are welcome to enhance functionality, add features, or improve documentation.
CC-BY-4.0
- Test streams provided by Mux - https://test-streams.mux.dev/
- Developed through collaborative effort between human direction and AI code generation (
claude-opus-4-6)