Skip to content
| Marketplace
Sign in
Visual Studio Code>Linters>HLS LensNew to Visual Studio Code? Get it now.
HLS Lens

HLS Lens

Allan Nava

| (0) | Free
Read HLS manifests in VS Code: inline diagnostics on m3u8 playlists, the bitrate ladder as a tree, and a deep segment check with segcheck.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

HLS Lens

HLS Lens

Read HLS manifests in VS Code — with the manifest telling you what is wrong with it.


An .m3u8 file is a list of claims, and a text editor shows you all of them equally. HLS Lens reads the manifest the way someone who has debugged a stream reads it: a wildcard where a hostname should be, a segment longer than the target duration it declares, a PROGRAM-DATE-TIME that walks away from the media timeline, an fMP4 playlist with no init segment, a content key fetched over plaintext HTTP.

81 rules, on the line you have to fix, while you edit:

#EXTM3U
#EXT-X-VERSION:3                        ← error   syntax/version-too-low
#EXT-X-TARGETDURATION:6                          EXT-X-MAP needs 6
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-KEY:METHOD=AES-128,URI="http://…" ← error  media/key-over-http
#EXT-X-MAP:URI="init.mp4"
#EXTINF:8.500,                          ← error   media/extinf-exceeds-target
seg-01205.m4s                                    8.5s against a 6s target

Plus the ladder as a tree, clickable child playlists, and — when you point it at a live URL — a deep check that hands the segment-level findings of segcheck to the same Problems panel.

What it does

  • Diagnostics while you type. Every rule reports the line, the reason and the fix. Rule ids are stable (media/extinf-exceeds-target), so a rule you disagree with goes in hlsLens.diagnostics.skip and stays gone. The full reference is docs/RULES.md, and HLS Lens: Show Rule Reference opens it inside the editor.
  • The manifest as a tree. The bitrate ladder in ascending order (with I-frame streams kept out of it, where they belong), the alternate audio and subtitle renditions, the segments with their durations and discontinuity/gap marks, the init segment and the keys, the low-latency vocabulary — the parts, the preload hint, the rendition reports — and the findings. Clicking a row reveals its line.
  • Open a manifest from a URL. HLS Lens: Open Manifest URL… fetches a playlist into a read-only editor with the diagnostics already on it. Redirects are followed and the final URL is what child URIs resolve against, so a CDN redirect does not send you hunting on the wrong host.
  • Follow the links. Variant, rendition, segment and EXT-X-MAP URIs are document links: on disk they open the file, on a CDN they open where they live.
  • Deep check with segcheck. The manifest rules read claims. HLS Lens: Deep Check Segments runs segcheck check --output json on a URL, so the findings that need the actual bytes — a gap no EXT-X-DISCONTINUITY declares, a rung that codes a lower resolution than it promises, a segment whose real duration drifts from its EXTINF — land next to them in the Problems panel. Without the binary the extension still does everything else; the deep check is the only feature that needs it.
  • The spec on hover. Hovering a tag says what it does, which EXT-X-VERSION it needs, where it is legal and every attribute it accepts with its enumerated values — the reference in the editor instead of a browser tab.
  • Completions that know the tag. # offers the tags that belong in this kind of playlist, , the attributes the tag accepts and not the ones already on the line, = the legal values (YES/NO, VOD/EVENT, AUDIO/VIDEO/SUBTITLES/CLOSED-CAPTIONS).
  • Quick fixes for the mechanical findings. Bump EXT-X-VERSION to what the playlist already uses, append a missing EXT-X-ENDLIST, raise EXT-X-TARGETDURATION to the longest segment, change a misspelled tag to the one it was meant to be, set AUTOSELECT=YES on a default rendition, drop a stray FORCED. Only those: a fix that needs a judgement call is not offered, and a tag more than two edits from a real one is a vendor extension rather than a typo, so it gets no guess.
  • The renditions compared with each other. HLS Lens: Check Renditions Together loads every rung of the open master — from disk or from the CDN — and reports what they disagree about: a different EXT-X-VERSION, segment counts that do not match, boundaries that drift, discontinuities one segment out, one rung that already ended while the others are live. Every rendition is a valid playlist on its own; these defects only exist between them, and they are what a player hits the moment it switches rungs.
  • Watch a live playlist. HLS Lens: Watch Live Playlist reloads the manifest on its own target duration and says what changed each time: the new segments, what slid off the front, a discontinuity that appeared, an EXT-X-ENDLIST that arrived. A window that stops moving for two reloads is reported — that is the packager falling over, and it looks identical to a healthy stream in any single snapshot.
  • The timeline as a picture. HLS Lens: Show Timeline draws the segments as a strip — discontinuities, EXT-X-GAP holes and the ad breaks an EXT-X-DATERANGE declares, each marked — and on a master it stacks every rung on one axis. A boundary that not every rung shares is a dashed rule straight through them, and only the rung that drifts is called out of step: with one rung out of five it is that rung that is wrong, not the four that agree. Clicking a segment reveals its line. The whole page is rendered in the core, so the drawing has tests rather than a screenshot.
  • Low latency, checked as a whole. A low-latency playlist asks a player to act on it before the media exists, so a declaration that does not hold costs a request it cannot take back. Eleven rules read the parts against the PART-TARGET the playlist declares, the preload hint against what the playlist already publishes (hinting a part it just listed is a wasted round trip, not a preload), the rendition reports against the position this playlist is at, and CAN-SKIP-UNTIL against the six target durations below which no conforming client may ask for a delta.
  • This manifest against another one. HLS Lens: Compare With… takes a path or a URL and says what changed: rungs added, removed or re-rated, rendition groups that came and went, a target duration that moved. Rungs are matched by URI, because a packager keeps the path of a rendition stable far more often than its bitrate — which is what tells "the same rung, re-rated" apart from "a new rung". A text diff answers the same question in a form nobody can read.
  • A report you can send someone. HLS Lens: Export Findings as a Report turns the findings — of the open manifest, or of the last workspace scan — into markdown for a ticket or JSON for whatever reads it next. The JSON carries a schema number so a consumer has something to pin, and lines are 1-based in both: 0-based is an editor's convention, and a report is read by people and by CI.
  • The whole workspace, not just the open file. HLS Lens: Check All Manifests in Workspace reads every .m3u8 and .mpd in the folder and fills the Problems panel with what it finds — including files nobody has opened, which is where the defect usually is. The scan is cancellable, and when it stops at its 2000-file cap it says so rather than reporting a partial result as if it were the whole one.
  • Rendition groups, resolved by name. Alternate audio and subtitles are the one part of a master a player looks up purely by string, and nothing fails loudly when the string is wrong: the stream plays without the track and the viewer reports "no Italian audio" for a manifest that looks well formed. Eight rules read the group from both sides — the renditions in it, and the variants that are supposed to name it.
  • One template, several deployments. EXT-X-DEFINE variables are substituted as the manifest is parsed, so the tree, the document links and every rule see the URI that will actually be requested. A {$name} nothing declares is not guessed at: substitution is textual and has no error path, so the braces stay in the URL a player requests — and that is reported on the line that uses it, which is the only clue you get from a 404 with a { in the hostname.
  • DASH, read the same way. Open an .mpd and it gets the same treatment as a playlist: the tree (periods, adaptation sets and their representations with bitrate, resolution and frame rate), a status bar line, and eleven dash/* rules reporting on it — a @mediaPresentationDuration the segment timeline does not fill, <S> elements that do not chain (a hole in the presentation, or two segments claiming the same seconds), a dynamic manifest with no <UTCTiming> for clients to synchronise their clock to, an adaptation set that never declares @segmentAlignment, a @media template with no $Number$. The XML reader is written here, like everything else: still no dependencies.
  • A status bar line that says what the open manifest is: 4 variants · 360p→1080p · 0.88 Mbps–6.10 Mbps · 3 alternate renditions.

The rules, in one paragraph

Seven structure rules (missing #EXTM3U, a BOM before it, a file that is both master and media, a misspelled tag — which players silently ignore, so a typo'd EXT-X-TARGETDURATON reads as no target duration at all — an EXT-X-VERSION lower than the tags in use, a malformed EXT-X-DEFINE, and a {$variable} nothing declares). Twenty-four master playlist rules (missing BANDWIDTH/RESOLUTION/CODECS, duplicate BANDWIDTH, a variant with no URI, an AUDIO/SUBTITLES group nothing declares, groups with no default or two defaults, no I-frame playlist for trick play, plaintext child URIs, missing AVERAGE-BANDWIDTH, a ladder that is not in ascending order, a CODECS level that cannot carry the declared RESOLUTION and FRAME-RATE, rungs so close together that ABR cannot tell them apart — or so far apart that there is nothing to fall back to — malformed EXT-X-SESSION-DATA, and content steering with no server or a pathway no variant belongs to) — eight of them on rendition groups (an EXT-X-MEDIA with no TYPE/GROUP-ID/NAME, subtitles with no URI, closed captions with one — the spec forbids it — or with no INSTREAM-ID, FORCED on something that is not subtitles, DEFAULT=YES with AUTOSELECT=NO, two renditions of a group sharing a NAME, an audio group that mixes stereo and 5.1, a group no variant references, and variants that do not agree on which groups they use). Thirty media playlist rules (no TARGETDURATION, a segment longer than it, a target duration far above the real segments, PLAYLIST-TYPE:VOD with no EXT-X-ENDLIST, fMP4 with no EXT-X-MAP, a content key over HTTP, PROGRAM-DATE-TIME going backwards or drifting from the EXTINF sum, a live playlist with no wall clock, discontinuities with no DISCONTINUITY-SEQUENCE, an EXTINF with no URI, EXT-X-GAP segments, a live window under three target durations, plaintext segment URIs, malformed or overlapping EXT-X-DATERANGE ad breaks, a live window a single content key covers, encryption switched off part-way through, an I-frames-only playlist that addresses whole segments instead of byte ranges, and an EXT-X-START that lands outside the playlist or inside the live edge) — eleven of them low latency (EXT-X-PART without the SERVER-CONTROL that makes parts worth serving, a hold-back under three target durations, parts with no EXT-X-PART-INF or longer than the PART-TARGET they declare, a part target as long as a segment, CAN-SKIP-UNTIL under six target durations, a malformed or duplicated preload hint, a hint for something the playlist already publishes, a rendition report with no position or several segments out of step, and a low-latency playlist that reports no other rendition at all).

Each one is documented with why it matters, not just what it matches: see docs/RULES.md.

Install

Documentation: allan-nava.github.io/hls-lens — usage, the full rule reference and the roadmap.

From the Marketplace: search HLS Lens. Or build the .vsix yourself:

npm install
npm run package        # → hls-lens-0.9.0.vsix
code --install-extension hls-lens-0.9.0.vsix

For the deep check, install segcheck (brew install --cask allan-nava/tap/segcheck, or a binary from its releases) and point hlsLens.segcheck.path at it if it is not on your PATH.

Commands

Command What it does
HLS Lens: Open Manifest URL… Fetch a playlist into a read-only editor, diagnostics included
HLS Lens: Check Renditions Together Load every rung of the master and report what they disagree about
HLS Lens: Show Timeline The segments as a strip, the rungs stacked on one axis, the drift drawn
HLS Lens: Check All Manifests in Workspace Analyse every manifest in the folder, opened or not
HLS Lens: Compare With… What this manifest declares that another one did not
HLS Lens: Export Findings as a Report The findings as markdown or JSON, to attach to a ticket
HLS Lens: Watch Live Playlist Reload the live playlist and report what changes; click the status bar to stop
HLS Lens: Deep Check This Rendition Run segcheck against one rung picked in the tree, not the whole master
HLS Lens: Deep Check Segments (segcheck) Download and parse the segments, bring the findings back
HLS Lens: Show Rule Reference The rule catalogue, from the extension itself
HLS Lens: Copy Resolved URI Absolute URI of the selected tree row
HLS Lens: Refresh Re-read the active manifest

Settings

Setting Default What it is for
hlsLens.diagnostics.enabled true Turn the squiggles off without disabling the extension
hlsLens.diagnostics.minSeverity hint warning hides the advisory rules
hlsLens.diagnostics.skip [] Rule ids or whole categories to skip
hlsLens.diagnostics.severity {} Re-grade a rule or a category: {"master/ladder-spacing": "warning"}
hlsLens.diagnostics.profile none apple or low-latency: a house style, graded under your own settings
hlsLens.pdtDriftToleranceMs 500 How far PROGRAM-DATE-TIME may drift from the EXTINF durations
hlsLens.targetDurationSlack 1.5 When TARGETDURATION counts as overstated
hlsLens.request.headers {} Extra headers for fetching a manifest (a token, a Host override)
hlsLens.request.timeoutMs 15000 Fetch timeout
hlsLens.segcheck.path segcheck Where the binary is
hlsLens.segcheck.segments 6 Segments sampled per rendition in the deep check
hlsLens.segcheck.renditions 0 Video renditions to inspect (0 = all)
hlsLens.segcheck.from auto Sample at the live edge, at the start, or let segcheck decide
hlsLens.segcheck.insecure false Skip TLS verification — lab servers only
hlsLens.watch.intervalSeconds 0 Reload interval for the watch; 0 follows EXT-X-TARGETDURATION
hlsLens.workspace.exclude **/node_modules/** Glob skipped by the workspace scan

Design notes

  • The logic is a pure core. src/core/ never imports vscode: the parser, the 81 rules, the ladder model, URI resolution, the segcheck bridge — and even the backlog parser and the icon generator — are plain TypeScript with tests. src/extension.ts only translates that model into diagnostics, tree items and links, and the scripts in scripts/ are I/O over the same core.
  • Line numbers everywhere, 0-based. The parser keeps the line index of every tag, URI, EXTINF and PROGRAM-DATE-TIME it decodes, because a finding that cannot point at a line is just a linter you have to read twice.
  • Attribute lists are parsed, not split. CODECS="avc1.4d401f,mp4a.40.2" is one value with a comma in it; splitting the line on commas is how a manifest gets reported as codec-less.
  • No dependencies. Not one runtime dependency; the fetcher is node:http(s) and even the Marketplace icon is generated (npm run icon) rather than pulled from a toolchain.
  • Nothing in the tests touches the network. Fixtures for the manifests, a throwaway local HTTP server for the fetcher, and the segcheck bridge tested against the JSON shape rather than by spawning the binary.

Development

npm install
npm run watch      # esbuild in watch mode, then F5 for the Extension Host
npm test           # the core: parser, rules, ladder, URIs, segcheck bridge, fetcher
npm run typecheck
npm run docs       # regenerate docs/RULES.md from the catalogue (CI checks this)
npm run icon:check # verify the committed icon against its generator, pixel for pixel
npm run site       # build site/ from docs/ (what GitHub Pages publishes)
npm run roadmap    # regenerate docs/ROADMAP.md from BACKLOG.md (CI checks this)

Releasing

A pushed v* tag is the whole release process. ci.yml runs the tests, refuses a tag that disagrees with package.json, packages the .vsix, attaches it to the GitHub release, and then publishes that same file — not a fresh package — to the VS Code Marketplace and to Open VSX:

# after the changelog entry and the version bump
git tag -a v0.9.0 -m "Release 0.9.0" && git push origin main --follow-tags

The two store credentials live in the marketplace environment, which is also where you can require a manual approval before a tag reaches users:

Secret Where it comes from Missing?
VSCE_PAT Azure DevOps PAT, scope Marketplace › Manage, for the allannava95 publisher Warns and skips the Marketplace step
OVSX_PAT Open VSX access token, namespace allannava95 Warns nothing, skips Open VSX

A missing PAT never fails the run: the .vsix is still built and attached to the release, so a tag is releasable before the store accounts exist. Once VSCE_PAT is set, every tag publishes — the project tags every commit, so bump the version deliberately.

The Marketplace step retries three times. Its API times out often enough to have already cost this repository a release — v0.11.0 died on Request timeout — and a tag is pushed once, so a transient failure there is a version nobody ever gets. A retry that comes back with already exists is treated as success: a request that timed out may still have been accepted.

Roadmap

BACKLOG.md is the plan, and the only place work is tracked. Two things are generated from it, so none of them can drift:

  • docs/ROADMAP.md — npm run roadmap, with CI failing if the committed file is not what the backlog produces.
  • GitHub milestones and issues — the backlog-sync workflow runs on every push that touches the backlog and makes the tracker a mirror of the file: a ## heading is a milestone, an item is an issue labelled backlog, - [x] closes it. A section is named after a release once it has shipped (v0.3.0 — Publishing automation, closed) and after a theme while it is planned (Editor); an item moves from the theme to the release that shipped it, which is the only way to answer both questions with the single milestone an issue has. Each issue is anchored to its stable id (HL-7) by a marker in the body, so renaming an item retitles its issue instead of opening a second one, and the whole thing is idempotent — it writes only what diverges. workflow_dispatch takes a dry_run input that reports what it would change.

Editing an issue on GitHub is therefore pointless: the next sync overwrites it. Edit the file.

Related

Part of a family of domain-specific tooling: segcheck (what your HLS/DASH segments actually contain), checkfleet (infrastructure health checks), keycloak-doctor (Keycloak realm audit), and the other lenses: nomad-lens, nats-lens, ansible-vars-lens.

License

MIT — see LICENSE.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft