Parse, filter, build, and transform HLS & DASH manifests in Go. Zero dependencies. Ships as a library, HTTP proxy server, and CLI tool.
- Parse HLS Master Playlists (
.m3u8) and DASH MPDs (.mpd) from string, file, or URL - Filter variants/representations by codec, resolution, bandwidth, frame rate, audio language, MIME type
- Transform URIs — CDN rewrites, absolute URI resolution, auth token injection (variants, audio tracks, I-frame streams, and DASH
<BaseURL>elements) - Inject extra tracks — append subtitle tracks, alternate audio, or additional representations after filtering
- Build complete HLS or DASH manifests from scratch with a fluent builder API
- Serve as an HTTP proxy that filters manifests on the fly
- CLI tool for scripting and local use
- Zero non-stdlib dependencies
- Thread-safe —
Filter()is safe for concurrent use - HLS versions 3–7; DASH profiles
isoff-on-demandandisoff-live
go get github.com/alanzng/manifestorgo install github.com/alanzng/manifestor/cmd/manifestor@latestdocker pull ghcr.io/alanng/manifestor:latestimport (
manifestor "github.com/alanzng/manifestor"
"github.com/alanzng/manifestor/manifest"
)
filtered, err := manifest.Filter(content,
manifest.WithCodec(manifestor.H264),
manifest.WithMaxResolution(manifestor.Res1080p),
manifest.WithMaxBandwidth(5_000_000),
manifest.WithCDNBaseURL("https://cdn.example.com"),
)filtered, err := manifest.FilterFromURL("https://example.com/master.m3u8",
manifest.WithCodec(manifestor.H264),
manifest.WithAuthToken("token=abc123"),
)Take a Bento4-generated master playlist with mixed AVC1/HVC1 video and a single audio track, and produce a delivery manifest with H.265 only, max 720p, absolute CDN URLs, a dubbed audio track, and subtitles.
HLS:
import (
manifestor "github.com/alanzng/manifestor"
"github.com/alanzng/manifestor/hls"
"github.com/alanzng/manifestor/manifest"
)
const cdnBase = "https://vod-bp.vieon.vn/abc123/.../vod/2026/03/12/uuid/"
const dubbedBase = "https://vod-bp.vieon.vn/def456/.../vod/2026/03/24/uuid2/"
out, err := manifest.Filter(content,
manifest.WithCodec(manifestor.H265),
manifest.WithMaxResolution(manifestor.Res720p),
manifest.WithAbsoluteURIs(cdnBase),
manifest.WithHLSVariantSubtitleGroup("subs"),
manifest.WithHLSInjectSubtitle(hls.SubtitleTrackParams{
GroupID: "subs",
Name: "Tiếng Việt",
Language: "vi",
URI: "https://static.vieon.vn/subtitle/vi.m3u8",
Default: true,
}),
manifest.WithHLSInjectAudioTrack(hls.AudioTrackParams{
GroupID: "audio/mp4a",
Name: "Thuyết Minh",
Language: "tm",
URI: dubbedBase + "audio-tg-mp4a.m3u8",
}),
)DASH:
import (
manifestor "github.com/alanzng/manifestor"
"github.com/alanzng/manifestor/dash"
"github.com/alanzng/manifestor/manifest"
)
out, err := manifest.Filter(content,
manifest.WithCodec(manifestor.H265),
manifest.WithMaxResolution(manifestor.Res720p),
manifest.WithAbsoluteURIs(cdnBase),
manifest.WithDASHInjectAdaptationSet(dash.AdaptationSetParams{
MimeType: "audio/mp4",
Lang: "tm",
Name: "Thuyết Minh",
Representations: []dash.RepresentationParams{
{ID: "tm-audio", Bandwidth: 196728, Codecs: "mp4a.40.2",
BaseURL: dubbedBase + "media-audio-tg-mp4a.mp4"},
},
}),
manifest.WithDASHInjectAdaptationSet(dash.AdaptationSetParams{
ContentType: "text",
MimeType: "text/vtt",
Lang: "vi",
Roles: []dash.Role{{SchemeIDURI: "urn:mpeg:dash:role:2011", Value: "subtitle"}},
Representations: []dash.RepresentationParams{
{ID: "subtitles/vi", Bandwidth: 16,
BaseURL: "https://static.vieon.vn/subtitle/vi.vtt"},
},
}),
)import "github.com/alanzng/manifestor/hls"
b := hls.NewMasterBuilder()
b.SetVersion(6).
AddAudioTrack(hls.AudioTrackParams{
GroupID: "audio-en",
Name: "English",
Language: "en",
URI: "https://cdn.example.com/audio/en/index.m3u8",
Default: true,
AutoSelect: true,
}).
AddVariant(hls.VariantParams{
URI: "https://cdn.example.com/1080p/index.m3u8",
Bandwidth: 5_000_000,
Codecs: "avc1.640028,mp4a.40.2",
Width: 1920,
Height: 1080,
FrameRate: 29.97,
AudioGroupID: "audio-en",
}).
AddVariant(hls.VariantParams{
URI: "https://cdn.example.com/720p/index.m3u8",
Bandwidth: 2_800_000,
Codecs: "avc1.4d401f,mp4a.40.2",
Width: 1280,
Height: 720,
FrameRate: 29.97,
AudioGroupID: "audio-en",
})
playlist, err := b.Build()import "github.com/alanzng/manifestor/dash"
b := dash.NewMPDBuilder(dash.MPDConfig{
Profile: "isoff-on-demand",
Duration: "PT4M0.00S",
MinBufferTime: "PT1.5S",
})
b.AddAdaptationSet(dash.AdaptationSetParams{
MimeType: "video/mp4",
SegmentTemplate: &dash.SegmentTemplateParams{
Initialization: "$RepresentationID$/init.mp4",
Media: "$RepresentationID$/$Number$.m4s",
Timescale: 90000,
Duration: 270000,
},
Representations: []dash.RepresentationParams{
{ID: "v1", Bandwidth: 5_000_000, Codecs: "avc1.640028", Width: 1920, Height: 1080},
{ID: "v2", Bandwidth: 2_000_000, Codecs: "avc1.4d401f", Width: 1280, Height: 720},
},
})
b.AddAdaptationSet(dash.AdaptationSetParams{
MimeType: "audio/mp4",
Lang: "en",
Name: "English",
Representations: []dash.RepresentationParams{
{
ID: "a1",
Bandwidth: 128000,
Codecs: "mp4a.40.2",
BaseURL: "https://cdn.example.com/audio-en.mp4",
AudioChannelConfiguration: &dash.AudioChannelConfiguration{
SchemeIDURI: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
Value: "2",
},
},
},
})
mpd, err := b.Build()# Start the proxy server
manifestor serve --port 8080
# Filter a live manifest via HTTP
curl "http://localhost:8080/filter?url=https://example.com/master.m3u8&codec=h264&max_res=1920x1080"# Filter a local file
manifestor filter --input master.m3u8 --codec h264 --max-res 1920x1080
# Filter from URL and write to file
manifestor filter --url https://example.com/master.m3u8 --codec h264 --output filtered.m3u8
# Build from a JSON spec
manifestor build --format hls --variants spec.json --output master.m3u8| Option | Description |
|---|---|
WithCodec(manifestor.Codec) |
Keep only video variants matching codec: H264, H265, VP9, AV1. Audio tracks are always preserved. Use manifestor.ParseCodec(s) for string input. |
WithMaxResolution(manifestor.Resolution) |
Exclude variants wider or taller than the resolution. Presets: Res720p, Res1080p, Res4K, etc. |
WithMinResolution(manifestor.Resolution) |
Exclude variants smaller than the resolution |
WithExactResolution(manifestor.Resolution) |
Keep only variants matching exactly |
WithMaxBandwidth(bps) |
Exclude variants above bps bits/s |
WithMinBandwidth(bps) |
Exclude variants below bps bits/s |
WithMaxFrameRate(fps) |
Exclude variants with frame rate above fps |
WithAudioLanguage(lang) |
Keep only audio tracks matching BCP-47 lang |
WithMimeType(manifestor.MimeType) |
Keep only representations matching MIME type (DASH only). Constants: MimeVideoMP4, MimeAudioMP4, MimeTextVTT, etc. |
WithCDNBaseURL(base) |
Rewrite all URIs to use base as CDN origin |
WithAbsoluteURIs(origin) |
Resolve relative URIs to absolute using origin |
WithAuthToken(token) |
Append token= query parameter to all URIs |
WithURISigner(fn) |
Per-URI signing hook. fn(absoluteURL string) string is invoked for every absolute URI emitted by Filter and may return a rewritten replacement. Use when a signing scheme requires per-URL token computation that WithAuthToken cannot express. |
WithClearAudioTracks() |
Remove every parsed origin audio track (HLS) or audio AdaptationSet (DASH) before inject options run. Pair with WithHLSInjectAudioTrack / WithDASHInjectAdaptationSet to fully replace origin audio. |
URI rewriting covers: HLS variant URIs, audio track URIs, subtitle track URIs, I-frame stream URIs (including injected ones); DASH <BaseURL> elements (including injected ones).
out, err := manifest.Filter(content,
manifest.WithAbsoluteURIs("https://origin.example.com/bucket/"),
manifest.WithURISigner(func(u string) string {
return signWithCloudFront(u, keyPair)
}),
)| Option | Description |
|---|---|
WithHLSInjectVariant(p) |
Append a variant stream after filtering |
WithHLSInjectAudioTrack(p) |
Append an #EXT-X-MEDIA AUDIO track after filtering |
WithHLSInjectSubtitle(p) |
Append an #EXT-X-MEDIA SUBTITLES track after filtering |
WithHLSVariantSubtitleGroup(id) |
Set SUBTITLES="id" on all surviving variants |
| Option | Description |
|---|---|
WithDASHInjectAdaptationSet(p) |
Append an <AdaptationSet> to every Period after filtering |
| Option | Package | Description |
|---|---|---|
WithCustomFilter(fn) |
hls, dash |
User-defined filter: func(*Variant) bool / func(*Representation) bool |
WithCustomTransformer(fn) |
hls, dash |
User-defined transformer applied to each surviving variant/representation |
| Error | Condition |
|---|---|
ErrInvalidFormat |
Content is neither valid HLS nor DASH |
ErrNotMasterPlaylist |
HLS content is a media playlist, not a master |
ErrNoVariantsRemain |
All variants were filtered out |
ErrFetchFailed |
Upstream URL fetch failed |
ErrParseFailure |
Manifest could not be parsed |
ErrEmptyVariantList |
Build() called with no variants added |
ErrInvalidVariant |
A variant is missing URI or Bandwidth |
ErrOrphanedGroupID |
AudioGroupID references a non-existent #EXT-X-MEDIA group |
ErrInvalidLanguageTag |
DASH lang is not a valid BCP-47 tag |
Key fields parsed and round-tripped through Parse → Filter → Serialize:
| Element | Fields |
|---|---|
<MPD> |
Profile, Duration, MinBufferTime, MinUpdatePeriod |
<AdaptationSet> |
ID, ContentType, MimeType, Lang, Name (label attr), Roles, SegmentTemplate, SegmentBase |
<Representation> |
ID, Bandwidth, Codecs, Width, Height, FrameRate, MimeType, StartWithSAP, BaseURL, AudioChannelConfiguration |
<Role> |
SchemeIDURI, Value |
<AudioChannelConfiguration> |
SchemeIDURI, Value |
Fetches and filters an upstream manifest.
| Parameter | Required | Description |
|---|---|---|
url |
yes | Upstream manifest URL |
codec |
no | h264 | h265 | vp9 | av1 |
max_res |
no | e.g. 1920x1080 |
min_res |
no | e.g. 854x480 |
max_bw |
no | bits/s e.g. 5000000 |
min_bw |
no | bits/s e.g. 500000 |
fps |
no | max frame rate e.g. 30 |
cdn |
no | CDN base URL |
token |
no | Auth token string |
lang |
no | BCP-47 audio language |
Responses: 200 OK, 400 Bad Request, 422 Unprocessable Entity, 502 Bad Gateway
Builds a manifest from a JSON payload. See HTTP API docs for full schema.
| Operation | Target | Typical |
|---|---|---|
| Parse + filter + serialize 50 KB manifest | < 5 ms | ~1–2 ms |
| Build 100-variant manifest | < 2 ms | ~0.5 ms |
Tested against real-world output from:
- Bento4
mp4-dash.py - Shaka Packager
- AWS MediaConvert
- Azure Media Services
- Vieon VOD platform
| Name | Logo | Website | Description |
|---|---|---|---|
| Vieon |
|
vieon.vn | Vietnam's leading OTT streaming platform. Uses manifestor to filter and transform HLS & DASH manifests for multi-codec VOD delivery (AVC1 + HVC1) with CDN rewriting and per-request auth tokens. |
Contributions are welcome! Please read CONTRIBUTING.md before opening a pull request.
MIT — see LICENSE.