hls-streamer
v4.6.2
Published
Lightweight HLS streaming from media files (MP3, AAC, M4A, OGG, FLAC, WAV, MP4, MOV, M4V) on demand, without temporary files or ffmpeg dependency
Maintainers
Readme

HLS Streamer
HLS Streamer converts audio and video files (MP3, AAC, M4A, OGG Vorbis, FLAC, WAV, MP4, MOV, M4V) into HTTP Live Streaming (HLS) playlists on the fly. It analyses the source file in-memory, builds frame-aligned byte ranges, and streams them without temporary files, native bindings, or external binaries like ffmpeg.
- Why HLS Streamer?
- How It Works
- Quick Start
- Serving Over HTTP
- Remote Storage (S3 / MinIO)
- Configuration Reference
- Playlist Anatomy
- Operational Tips
- Development
- Release Notes
- Support
Why HLS Streamer?
- Multi-format support – handles MP3, AAC, M4A, OGG Vorbis, FLAC, WAV, MP4, MOV, and M4V with automatic format detection.
- Audio + Video – audio files produce standard HLS v6 playlists; video files produce HLS v7 fMP4 playlists with an
EXT-X-MAPinit segment and keyframe-aligned boundaries. - Zero dependencies – no shared libraries, no ffmpeg, no native compilation. Pure TypeScript parsers for all formats. Drop it into Docker, serverless, or edge runtimes.
- Remote storage – stream directly from S3, MinIO, or any compatible object storage via the
IStorageProviderinterface — no local file required. - Accurate segments – real frame/packet parsing provides true durations,
#EXTINFmetadata, and target durations that match playback. - Frame-aligned byte ranges – every segment begins and ends on verified frame boundaries; video segments snap to keyframes to prevent decoding artifacts.
- No temp files – streams straight from the source file using byte-range reads.
- Fast-start aware – optional smaller first segments improve startup latency for constrained networks.
- TypeScript first – authored in TypeScript with full type definitions for your tooling and IDEs.
How It Works
- Storage abstraction – the source is either a local file (via
filePath) or any object storage (viastorageProvider). Both expose the same byte-range interface internally. - Format detection – automatically detects format from file content (magic bytes /
ftypbrand) or extension, with optional manual override. - Metadata analysis – format-specific parsers extract frame/packet tables with offsets, durations, and (for video) keyframe markers.
- Segment planning – boundaries are calculated from the frame table so each segment contains whole frames while respecting your target size. Video segments snap to I-frame boundaries.
- Playlist generation –
createM3U8()emits an#EXTM3Uplaylist. Audio files use HLS v6; video files use HLS v7 withEXT-X-MAPpointing to themoovinit segment. - On-demand byte ranges –
getFileBuffer(start, end)reads only the requested bytes — from disk or object storage — without buffering the full file.
┌──────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────────┐
│ Media Source │ ──▶ │ Format Detector │ ──▶ │ Format Parser │ ──▶ │ Segment Planner │
│ (any format) │ │ (magic bytes) │ │ (MP3/MP4/etc.) │ │ (frame/keyframe) │
└──────────────┘ └─────────────────┘ └─────────────────┘ └──────────┬───────────┘
│
▼
┌──────────────────────┐
│ HLS Playlist & Bytes │
└──────────────────────┘Quick Start
import { HlsStreamer } from 'hls-streamer';
// Audio file — auto-detected
const audioStreamer = new HlsStreamer({
filePath: '/media/library/song.mp3', // or .aac, .m4a, .ogg, .flac, .wav
segmentSizeKB: 512,
fileName: 'track',
baseUrl: 'audio/stream/session-42',
enableFastStart: true,
});
const audioPlaylist = await audioStreamer.createM3U8();
// Video file — same API
const videoStreamer = new HlsStreamer({
filePath: '/media/library/movie.mp4', // or .mov, .m4v
segmentSizeKB: 2048,
fileName: 'segment',
baseUrl: 'video/stream/session-42',
});
const videoPlaylist = await videoStreamer.createM3U8();
// Detect media type at runtime
const type = await videoStreamer.getMediaType(); // 'video' | 'audio'Serving Over HTTP
The example below shows an Express setup that handles both audio and video:
import express from 'express';
import { HlsStreamer } from 'hls-streamer';
const app = express();
app.get('/streams/:id/playlist.m3u8', async (req, res, next) => {
try {
const streamer = new HlsStreamer({
filePath: resolveMediaPath(req.params.id),
baseUrl: `streams/${req.params.id}`,
enableFastStart: true,
});
res.type('application/vnd.apple.mpegurl');
res.send(await streamer.createM3U8());
} catch (error) {
next(error);
}
});
app.get('/streams/:id/:start/:end/:filename', async (req, res, next) => {
try {
const streamer = new HlsStreamer({
filePath: resolveMediaPath(req.params.id),
baseUrl: `streams/${req.params.id}`,
});
const start = Number(req.params.start);
const end = Number(req.params.end);
const mediaType = await streamer.getMediaType();
res.type(mediaType === 'video' ? 'video/mp4' : 'audio/mpeg');
res.set('Accept-Ranges', 'bytes');
res.send(await streamer.getFileBuffer(start, end));
} catch (error) {
next(error);
}
});Segment URL Contract
Generated playlists follow this pattern:
/{baseUrl}/{startByte}/{endByte}/{fileName}{index}.{ext}For video files, an additional init segment URL is emitted as EXT-X-MAP:
/{baseUrl}/{moovOffset}/{moovEnd}/{fileName}init.mp4startByteis inclusive,endByteis exclusive.indexis zero-padded to three digits (000,001, ...).- Serve the exact byte range from the original file — no transcoding needed.
Remote Storage (S3 / MinIO)
HLS Streamer can stream directly from object storage — no local file needed. Pass a storageProvider instead of filePath. The built-in S3Provider works with AWS S3, MinIO, Wasabi, Backblaze B2, and any S3-compatible service.
Installation
The AWS SDK is a peer dependency and only required when using S3Provider. Install it separately:
npm install @aws-sdk/client-s3AWS S3
import { HlsStreamer, S3Provider } from 'hls-streamer';
const provider = new S3Provider({
bucket: 'my-media-bucket',
key: 'podcasts/episode-42.mp3',
clientConfig: {
region: 'us-east-1',
// credentials resolved automatically from env / IAM role
},
});
const streamer = new HlsStreamer({
storageProvider: provider,
fileName: 'episode',
baseUrl: 'streams/ep42',
segmentSizeKB: 512,
});
const playlist = await streamer.createM3U8();Passing explicit credentials:
import { S3Client } from '@aws-sdk/client-s3';
import { HlsStreamer, S3Provider } from 'hls-streamer';
const s3Client = new S3Client({
region: 'eu-west-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
const provider = new S3Provider({
bucket: 'my-media-bucket',
key: 'videos/clip.mp4',
client: s3Client, // reuse an existing client
});
const streamer = new HlsStreamer({ storageProvider: provider });MinIO
MinIO is fully S3-compatible. Point endpoint at your MinIO server and set forcePathStyle: true:
import { S3Client } from '@aws-sdk/client-s3';
import { HlsStreamer, S3Provider } from 'hls-streamer';
const minioClient = new S3Client({
endpoint: 'http://localhost:9000', // your MinIO server
region: 'us-east-1', // any non-empty string
forcePathStyle: true, // required for MinIO
credentials: {
accessKeyId: 'minioadmin',
secretAccessKey: 'minioadmin',
},
});
const provider = new S3Provider({
bucket: 'media',
key: 'audio/track.mp3',
client: minioClient,
});
const streamer = new HlsStreamer({
storageProvider: provider,
fileName: 'track',
baseUrl: 'streams/track',
});
const playlist = await streamer.createM3U8();Serving S3 / MinIO streams over HTTP (Express)
The HTTP handler is identical to the local-file version — only the streamer construction changes:
import express from 'express';
import { S3Client } from '@aws-sdk/client-s3';
import { HlsStreamer, S3Provider } from 'hls-streamer';
const app = express();
// Reuse one client for the lifetime of the process
const s3 = new S3Client({ region: 'us-east-1' });
function makeStreamer(bucket: string, key: string, sessionId: string) {
return new HlsStreamer({
storageProvider: new S3Provider({ bucket, key, client: s3 }),
baseUrl: `streams/${sessionId}`,
fileName: 'segment',
});
}
app.get('/streams/:session/playlist.m3u8', async (req, res, next) => {
try {
const streamer = makeStreamer('my-bucket', resolveKey(req.params.session), req.params.session);
res.type('application/vnd.apple.mpegurl');
res.send(await streamer.createM3U8());
} catch (err) { next(err); }
});
app.get('/streams/:session/:start/:end/:filename', async (req, res, next) => {
try {
const streamer = makeStreamer('my-bucket', resolveKey(req.params.session), req.params.session);
const mediaType = await streamer.getMediaType();
res.type(mediaType === 'video' ? 'video/mp4' : 'audio/mpeg');
res.set('Accept-Ranges', 'bytes');
res.send(await streamer.getFileBuffer(Number(req.params.start), Number(req.params.end)));
} catch (err) { next(err); }
});Custom storage provider
Implement IStorageProvider to connect any storage backend (GCS, Azure Blob, HTTP, etc.):
import { IStorageProvider } from 'hls-streamer';
class MyProvider implements IStorageProvider {
readonly resourceId = 'custom://my-source';
async getSize(): Promise<number> { /* return total file size */ }
async getRange(start: number, end: number): Promise<Buffer> { /* [start, end) bytes */ }
async getHeader(): Promise<Buffer> { return this.getRange(0, 64); }
async getBuffer(): Promise<Buffer> { /* full file */ }
}
const streamer = new HlsStreamer({ storageProvider: new MyProvider() });Tip:
getRangeis the hot path — use HTTP byte-range requests (Range: bytes=start-end) to avoid downloading the full file for each segment.
Error handling
import { StorageProviderError } from 'hls-streamer';
try {
const playlist = await streamer.createM3U8();
} catch (err) {
if (err instanceof StorageProviderError) {
console.error(`Storage error for ${err.resourceId}:`, err.message);
}
}Configuration Reference
Provide either filePath or storageProvider — not both.
| Option | Type | Default | Description |
| ------------------- | --------------------- | ------------ | ----------- |
| filePath | string | — | Path to a local media file. Supports: MP3, AAC, M4A, OGG, FLAC, WAV, MP4, MOV, M4V. |
| storageProvider | IStorageProvider | — | Remote storage provider (e.g. S3Provider). Mutually exclusive with filePath. |
| segmentSizeKB | number | 512 | Target segment size in kilobytes. |
| fileName | string | "file" | Base name for generated segment URLs. |
| baseUrl | string | "" | URL prefix inserted before each segment path. |
| enableFastStart | boolean | false | Smaller first two segments for faster playback start. |
| format | MediaFormat | auto-detect | Optional override: 'mp3', 'aac', 'm4a', 'ogg', 'flac', 'wav', 'mp4', 'mov', 'm4v'. |
API Surface
createM3U8(): Promise<string>– Full HLS playlist with frame-accurate durations. Audio → HLS v6; Video → HLS v7 +EXT-X-MAP.getFileBuffer(start: number, end: number): Promise<Buffer>– Byte-range read from the source file (used for both segments and the init segment). Validates the range against file size only — does not trigger a full parse.getSegmentDuration(index: number): Promise<number>– Duration in seconds for a specific segment.getMediaType(): Promise<'audio' | 'video'>– Returns'video'for MP4/MOV/M4V,'audio'for everything else. Reads only the file header (~64 bytes) when possible.getFileInfo(): Promise<MediaFileInfo>– Parsed metadata (size, duration, frame table, etc.). Cached on the instance.restoreFileInfo(fileInfo: MediaFileInfo): void– Hydrate a previously-parsedMediaFileInfointo a fresh instance, skipping the underlying parse. Pair with an external metadata cache (LRU, Redis) to avoid re-downloading + re-parsing the same file across short-lived instances. Safe to round-trip viaJSON.stringify/JSON.parse.
Custom error classes: FileNotFoundError, InvalidFileError, InvalidRangeError, InvalidParameterError, UnsupportedFormatError, StorageProviderError.
Supported Formats
| Format | Extensions | Container | Codec | Frame Parsing |
| ------- | ------------------- | --------- | ------------ | ------------------------------- |
| MP3 | .mp3 | — | MPEG Layer 3 | ✅ Full frame table |
| AAC | .aac | ADTS | AAC | ✅ ADTS frames |
| M4A | .m4a, .m4b | MP4 | AAC | ✅ MP4 box structure |
| OGG | .ogg, .oga | OGG | Vorbis | ✅ OGG pages |
| FLAC| .flac | — | FLAC | ✅ FLAC frames |
| WAV | .wav | RIFF | PCM | ⚠️ Synthetic 1-second frames |
| MP4 | .mp4 | fMP4 | H.264/H.265 | ✅ ISOBMFF box parse + keyframes |
| MOV | .mov | QuickTime | H.264/H.265 | ✅ ISOBMFF box parse + keyframes |
| M4V | .m4v | MP4 | H.264/H.265 | ✅ ISOBMFF box parse + keyframes |
Playlist Anatomy
Audio (HLS v6)
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:5.973,
/audio/session/0/260736/track000.mp3
#EXTINF:5.994,
/audio/session/260736/521472/track001.mp3
...
#EXT-X-ENDLISTVideo (HLS v7 fMP4)
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-MAP:URI="/video/session/0/28672/fileinit.mp4"
#EXTINF:4.000,
/video/session/28672/2125824/file000.mp4
#EXTINF:3.967,
/video/session/2125824/4194304/file001.mp4
...
#EXT-X-ENDLISTEXT-X-MAPpoints to themoovbox (init segment) inside the original file — no remuxing.- Video segment boundaries are snapped to keyframes for seamless decoding.
#EXTINFretains millisecond precision for smooth playback.
Operational Tips
Caching – Construct the streamer once per unique file and reuse it. Segment planning caches metadata after the first call.
External metadata cache – If you cannot keep
HlsStreamerinstances alive between requests (e.g. serverless, multi-process workers), usegetFileInfo()to capture the parsedMediaFileInfoonce andrestoreFileInfo()to hydrate fresh instances on subsequent requests. Subsequent calls tocreateM3U8(),getMediaType(), andgetFileBuffer()then issue zero parse-related reads against your storage backend:const cached = lruCache.get(s3Key); // your cache const streamer = new HlsStreamer({ storageProvider: new S3Provider({ bucket, key: s3Key, client: s3 }) }); if (cached) { streamer.restoreFileInfo(cached); } else { lruCache.set(s3Key, await streamer.getFileInfo()); } // From here, segment requests do one ranged GET, nothing more.Video segment size – Use a larger
segmentSizeKB(1024–4096) for video to avoid excessive HTTP requests. Audio works well at 512.CDN friendliness – Segment URLs are deterministic byte ranges, making them ideal for edge caching. Use
Cache-Control: public, max-age=86400.Serverless – Zero-dependency design works in Lambda/Cloud Functions.
getFileBufferreads only the bytes needed, keeping memory usage low.Content-Type – Serve audio segments as
audio/mpeg(or the appropriate codec MIME type) and video segments asvideo/mp4. UsegetMediaType()to branch at runtime.S3 client reuse – Create one
S3Clientfor the lifetime of your process and pass it to eachS3Provider. This avoids per-request connection overhead and respects connection pool limits.MinIO in Docker – Set
endpointto your MinIO container URL andforcePathStyle: true. The bucket must exist and the credentials must haves3:GetObjectands3:HeadObjectpermissions.Troubleshooting – Inspect
FileLib.analyzeMediaFile()to review parsing warnings and format-specific metadata.
Development
npm install
npm test
npm run buildTo run a single test file:
npx jest tests/Parsers/Mp4Parser.test.tsSupport
- 🐛 Bug reports: GitHub Issues
- 💬 Questions & ideas: GitHub Discussions
- 📦 npm registry: hls-streamer
Contributing
Contributions are welcome! Please open an issue to discuss substantial changes before submitting a pull request. Make sure npm test and npm run build pass prior to filing the PR.
Release Notes
For the complete history, see CHANGELOG.md.
Version 4.6.0
New Features
HlsStreamer.getFileInfo()is now public — returns the parsedMediaFileInfo(size, duration, frame table, etc.). Result is cached on the instance.HlsStreamer.restoreFileInfo(fileInfo)hydrates a previously-parsedMediaFileInfointo a fresh instance, skipping the underlying parse. Designed to pair with an external cache (LRU, Redis) so short-lived instances avoid redundant downloads + re-parsing of the same file. Round-trips cleanly throughJSON.stringify/JSON.parse.
Performance
getMediaType()no longer triggers a full file read in the common path. It now reads only the file header (~64 bytes) and classifies via magic bytes; falls back to a full parse only when the header is inconclusive. With aformatoverride, classification happens with no read at all.getFileBuffer()no longer triggers a full file parse. It validates the requested range using onlyprovider.getSize()(a HEAD-equivalent), then issues the ranged read. The size is memoized on the instance — repeated segment requests against the sameHlsStreamerissue exactly onegetSize()call regardless of how many ranges are fetched.
Migration
Fully backward compatible — no signatures or behaviors changed. All additions are additive.
Full details: CHANGELOG.md.
Version 4.5.0
New Features
- Remote storage support – stream directly from S3, MinIO, or any S3-compatible service without downloading files locally. Pass a
storageProvidertoHlsStreamerOptionsinstead offilePath. S3Provider– built-in provider for AWS S3 and S3-compatible services (MinIO, Wasabi, Backblaze B2, etc.). Accepts a pre-configuredS3Clientor creates one fromclientConfig. Uses HTTP byte-range requests for segments, avoiding full file downloads.@aws-sdk/client-s3is an optional peer dependency — only install it if you useS3Provider.IStorageProviderinterface – implement this to connect any custom storage backend (GCS, Azure Blob, HTTP, etc.).StorageProviderError– new typed error thrown when a storage provider operation fails, carrying theresourceIdof the failing resource.LocalFileProvider– the existing filesystem logic is now a first-class provider. Exported for use in custom wrappers or testing.
Migration
No changes needed for existing code. filePath continues to work exactly as before.
// v4.0 — unchanged, still works
new HlsStreamer({ filePath: '/local/audio.mp3' });
// v4.5 — new: remote storage
import { S3Provider } from 'hls-streamer';
new HlsStreamer({ storageProvider: new S3Provider({ bucket: 'b', key: 'audio.mp3', client: s3 }) });Version 4.0.0
Major release adding video support and completing the audio→media rename.
New Features
- Video support – MP4, MOV, and M4V files are now fully supported via a pure-JS ISOBMFF (MP4 box) parser. No ffmpeg, no native bindings, no vendored libraries.
- HLS v7 fMP4 playlists – video files produce
EXT-X-MAPinit segments and keyframe-aligned byte-range segments, fully compatible with Safari, Chrome, and HLS.js. - Keyframe-aware segmentation – video segment boundaries snap to I-frames, preventing decoding artifacts.
getMediaType()– new method returning'audio' | 'video'to simplify route/MIME-type logic.MediaFormat/MediaFileInfo/MediaFrameInfo– public types renamed fromAudio*toMedia*to reflect the broader scope.
Breaking Changes (from v3)
AudioFormat,AudioFileInfo,AudioFrameInfo,IAudioParserare now deprecated aliases — they still compile but will be removed in v5.FileLib.analyzeAudioFile()andanalyzeAudioBuffer()are deprecated; useanalyzeMediaFile()/analyzeMediaBuffer().UnsupportedFormatErrormessage changed from"Unsupported audio format"to"Unsupported media format".formatoption type widened fromAudioFormattoMediaFormat(superset — no change needed for existing callers).
Migration Guide
// v3.x
import { AudioFormat, AudioFileInfo } from 'hls-streamer';
const type: AudioFormat = 'mp3';
// v4.x — preferred
import { MediaFormat, MediaFileInfo } from 'hls-streamer';
const type: MediaFormat = 'mp3'; // same values, broader type
// v3.x names still compile in v4 (deprecated, removed in v5)
import { AudioFormat } from 'hls-streamer'; // ⚠️ deprecated alias
// Video — new in v4
const streamer = new HlsStreamer({ filePath: 'movie.mp4' });
const playlist = await streamer.createM3U8(); // HLS v7 + EXT-X-MAP
const type = await streamer.getMediaType(); // 'video'Version 3.1.0
- HLS compliance:
#EXTINFlines now include the required trailing comma. - AAC ADTS detection: ADTS buffers no longer misclassified as MP3.
- Extensionless files: constructor now accepts extensionless files when magic bytes identify a supported format.
Version 3.0.0
- Added AAC, M4A, OGG Vorbis, FLAC, and WAV support.
- Refactored MP3 parsing into a modular
Parsers/directory. InvalidFileErrorfor non-MP3 files replaced byUnsupportedFormatError.
Made with ❤️ by LordVersA
