Skip to content

feat: honor codec preference for multicodec HLS sources (closes #65) - #89

Closed
birme wants to merge 2 commits into
mainfrom
issue-65/multicodec-codec-preference
Closed

birme wants to merge 2 commits into
mainfrom
issue-65/multicodec-codec-preference

Conversation

@birme

@birme birme commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the codec-preference filter from #65 — filtering a multicodec (HEVC+AVC) source master down to a single video codec family so advertised CODECS matches served segments. The filter logic is complete and tested.

Important

Runtime integration is BLOCKED ON UPSTREAM — #65 is NOT yet fixed in the packaged service. See "Runtime status" below. This PR lands the tested filter utility + honest wiring shape; it does not (and cannot, with the currently installed engine) filter a real source at request time.

Changes

  • src/plugins/codec_filter.ts (new): filterMasterManifestByCodec() drops non-preferred video variants while keeping audio-only variants and #EXT-X-MEDIA groups; createCodecFilteringMasterLoader() builds a master-manifest injector shaped for HLSVod.load(_injectMasterManifest, …).
  • src/plugins/utils.ts: getPreferredVideoCodecProfile() resolves the preference from OPTS_PREFERRED_VIDEO_CODEC; createConfiguredMasterLoader() builds the loader. Its doc comment now clearly marks it blocked on upstream (not live runtime wiring) and names the exact hook the engine would need.
  • @eyevinn/m3u8 declared as a direct dependency (^0.5.8, the already-resolved transitive version) — it was used via require() but only present transitively. One-line lockfile change.
  • Fixture fix: added the missing src/plugins/__tests__/fixtures/multicodec/av1_720p.m3u8 media playlist that master_no_avc.m3u8 references, and added a test that prefers av01 so the fixture is exercised end to end (fixture set is now self-consistent).
  • Updated the chore: obtain multicodec HEVC+AVC HLS fixture and add a failing repro test #63 characterization test to assert the FIXED filter behavior; added fixtures + tests.

Config

OPTS_PREFERRED_VIDEO_CODEC — comma-separated HLS codec-family prefixes (e.g. hvc1,avc1). Default avc1,hvc1 (prefer AVC over HEVC). Single-codec sources pass through unchanged.

Runtime status (why #65 is not yet fixed in the service)

docker-fast never calls HLSVod.load()eyevinn-channel-engine@5.0.0 does, from inside Session:

  • dist/engine/session.js L1737 / L1972: new HLSVod(vodResponse.uri, ...) then currentVod.load() (L1756/L1760) with no _injectMasterManifest argument, so HLSVod self-fetches the master (@eyevinn/hls-vodtolive@4.1.5 index.js L490).
  • ChannelEngineOpts (dist/engine/server.d.ts L1-37) exposes no master-manifest injector/loader option.
  • VodResponse (server.d.ts L59-71) carries only uri: string — no loader/stream field.

So the only lever docker-fast has over a source is the uri string. HLSVod.load() does support an _injectMasterManifest seam (index.js L198, L489-503), but the engine does not surface it. Filtering via a self-hosted proxy would require a new HTTP route plus absolute-URL rewriting of every (currently relative) variant URI — a separate subsystem, out of scope here.

Upstream hook needed to unblock: eyevinn-channel-engine should thread a caller-supplied master-manifest loader into HLSVod.load(...) — e.g. a masterManifestLoader on ChannelEngineOpts, or a VodResponse.masterManifestLoader callback returned from IAssetManager.getNextVod. Once that exists, createConfiguredMasterLoader() wires straight in.

Verification

  • PROOF: npm run lint -> 0 errors (5 pre-existing no-explicit-any warnings on untyped @eyevinn/m3u8 objects)
  • PROOF: npm run pretty -> All matched files use Prettier code style!
  • PROOF: npm test -> Test Suites: 2 passed, Tests: 14 passed
  • PROOF: npm run build -> tsc EXIT=0

Relates to #65 (filter + tests landed; runtime fix blocked on the upstream hook above).

Filter a source HLS master to a single video codec family before it reaches
HLSVod, so bandwidth-keyed variant selection never mixes codecs and the
advertised CODECS always matches the served segments.

- Add src/plugins/codec_filter.ts: parse the master with @eyevinn/m3u8, drop
  non-preferred video codec-family variants (keeping audio-only variants and
  EXT-X-MEDIA groups), and re-serialize. Single-codec masters pass through
  unchanged. If the preferred family is absent, fall back to the source's
  first-seen family so the master is never emptied.
- Expose preference via OPTS_PREFERRED_VIDEO_CODEC (default: prefer avc1 over
  hvc1), surfaced from utils.ts.
- Flip the #63 characterization test to assert the fixed behavior (advertised
  CODECS matches served-segment codec for every variant) and add tests for
  single-codec pass-through, inverted preference, and graceful fallback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@birme birme left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pr-reviewer verdict: CHANGES REQUESTED

Solid, well-documented, well-tested filter. The core codec_filter.ts logic is correct: it filters only video StreamItems, preserves audio-only STREAM-INF variants and #EXT-X-MEDIA groups, normalizes avc3/hev1 onto their families, and gracefully falls back to the first-seen family. I verified the audio-preservation behavior directly against @eyevinn/m3u8 and it holds. However there is a blocking gap: the fix is not wired into docker-fast's runtime path, so the bug is not actually fixed in the shipped product — only in an isolated unit test.

Deterministic proofs

  • PROOF: npm run lint -> 0 errors, 5 warnings (all pre-existing-style no-explicit-any on the untyped @eyevinn/m3u8 objects). Acceptable.
  • PROOF: npm run pretty -> All matched files use Prettier code style!
  • PROOF: npm test -> Test Suites: 2 passed; Tests: 13 passed, 13 total.
  • PROOF: npm run build -> tsc EXIT=0.

Acceptance criteria

  1. Repro test now passes with 0 mismatches — MET (in unit test); NOT met at runtime (see major finding below).
  2. Single-codec sources unaffected — MET. filterMasterManifestByCodec returns undefined and mutates nothing when <2 video families; covered by a dedicated "no StreamItem removed" test.
  3. No trademark/product names — MET. Only standard HLS CODECS prefixes (avc1/hvc1/av01/mp4a) in code/config; "AVC"/"HEVC" appear only as standard-body names in doc comments.

Findings

File: src/server.ts:45 & src/plugins/utils.ts:289 · Severity: major · Finding: createConfiguredMasterLoader() / getPreferredVideoCodecProfile() and env OPTS_PREFERRED_VIDEO_CODEC are never invoked in production. docker-fast runs new ChannelEngine(plugin.newAssetManager(), {...}); it never calls HLSVod.load() itself — channel-engine loads VODs internally, and the ChannelEngine options object exposes no master-manifest injector hook. So a multicodec source still reaches HLSVod unfiltered at runtime and the advertised-CODECS/segment mismatch persists in the packaged service. Acceptance criterion #1 is proven only by the unit test that calls the seam directly. · Recommendation: Either wire the loader into the actual VOD-load path (e.g. via the asset manager / whatever injection point channel-engine supports) so OPTS_PREFERRED_VIDEO_CODEC genuinely takes effect end-to-end, or — if channel-engine currently offers no such seam — say so explicitly in the PR and split this into (a) this library + tests and (b) a channel-engine change that exposes the injection point, and do not close #65 until the runtime path is covered. As-is the new public functions are dead code.

File: src/plugins/tests/fixtures/multicodec/master_no_avc.m3u8:10 · Severity: minor · Finding: References variant av1_720p.m3u8, which does not exist in the fixtures dir. Tests pass only because the av01 variant is filtered out (hvc1 chosen) before its media playlist is ever fetched — so the fallback test never actually exercises serving an av01 segment, and an inverted-preference test would break. Slightly overstates the graceful-fallback coverage. · Recommendation: Add the missing av1_720p.m3u8 fixture (mirroring the others) so the fixture is self-consistent and the fallback path can be exercised for av01 too.

File: package.json · Severity: minor · Finding: @eyevinn/m3u8 is used via require() but is a transitive dependency (resolved to v0.5.8 through eyevinn-channel-engine/@eyevinn/hls-vodtolive), not declared in docker-fast's own dependencies. This works today via hoisting but is fragile: a future channel-engine bump could drop/relocate it and break docker-fast at runtime with no lockfile signal. · Recommendation: Declare @eyevinn/m3u8 as a direct dependency (pinned to the resolved range) in a follow-up so the runtime contract is explicit. Not blocking on its own.

File: src/plugins/codec_filter.ts:189 · Severity: suggestion · Finding: fetch(source) sends the master URL with no timeout and no request options; non-ok status is handled and stream errors propagate via out.destroy(err), which is good. No unsanitized user input reaches the URL here (the URI comes from the asset manager). · Recommendation: Consider a fetch timeout / abort to avoid a hung loader on a stalled origin, consistent with other fetch call sites. Optional.

Verdict

CHANGES — the filter itself is correct and merge-quality, but because it is not connected to the runtime path the PR does not actually fix #65 in the shipped product. Wire it in (or clearly descope + keep #65 open) before merging. The missing av1 fixture and the undeclared transitive dep should be cleaned up as well.

… loader blocked-on-upstream

The codec-preference filter cannot be wired into runtime: the installed
eyevinn-channel-engine calls HLSVod.load() internally with no master-manifest
injector, and neither ChannelEngineOpts nor VodResponse exposes a loader seam.
Document createConfiguredMasterLoader as blocked-on-upstream instead of leaving
it as dead code implying a runtime fix.

- Declare @eyevinn/m3u8 as a direct dependency (was transitive-only via require).
- Add missing av1_720p.m3u8 media fixture referenced by master_no_avc.m3u8 and a
  test that prefers av01 so the fixture set is self-consistent and exercised.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@birme

birme commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Closing unmerged: the codec-preference filter here is correct and tested, but its runtime integration is blocked on an upstream channel-engine master-manifest loader hook (see #65). Merging as-is would land dead code and prematurely close #65. Branch issue-65/multicodec-codec-preference is preserved for pickup once the engine exposes the hook.

@birme birme closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant