Skip to content

feat(playback): add Ferrite (WASM/WebCodecs) inline player for software HEVC#1098

Closed
multiduplikator wants to merge 3 commits into
4gray:masterfrom
multiduplikator:feat/ferrite-1.3.2
Closed

feat(playback): add Ferrite (WASM/WebCodecs) inline player for software HEVC#1098
multiduplikator wants to merge 3 commits into
4gray:masterfrom
multiduplikator:feat/ferrite-1.3.2

Conversation

@multiduplikator

Copy link
Copy Markdown

Ferrite (WASM/WebCodecs) inline player for software HEVC

Adds Ferrite as a new inline web player — a <canvas> + WebAssembly software decoder with an automatic WebCodecs hardware tier — so the web/PWA build can play HEVC where the browser has no hardware decoder (and H.264/MPEG-2, 8/10-bit, interlaced). Requires a cross-origin-isolated context (COOP/COEP); falls back to the default inline player for HLS (.m3u/.m3u8).

This supersedes #1092 — rebased onto current master (that PR had drifted into conflict) and updated to the just-released ferrite.js 1.3.2. All earlier greptile findings from #1091/#1092 remain fixed (canvas key keyed on url + epgParams; teardown via the facade's canonical destroy() which releases the transferred canvas and clears listeners; no resume listener survives teardown).

What's in it

  • ferrite-player component + session/event wiring (live + VOD), IPTVnator-owned Material controls and a long-press diagnostic overlay. Teardown goes through destroy() only.
  • Routing/settings/command-palette/i18n registration; HLS falls back to the default inline player.
  • Engine + worker asset bundling (ferrite.{mjs,wasm}, worker.js, present-worker.js) into apps/web assets, plus an opt-in COOP/COEP isolated dev-serve configuration (nx serve web --configuration=ferrite).

New in this revision (ferrite.js 1.3.2)

  • Bumped ferrite.js ^1.3.0^1.3.2. The 1.3.2 audio-path overhaul (clean 5.1→stereo downmix + device-rate resample, loudness leveling), all-codec anamorphic SAR, and correct HEVC sizing on browsers with bogus VideoFrame dims (Edge) are internal to ferrite.js and apply automatically via the re-bundled engine.
  • New "Dyna" audio-dynamics control (Line / RF / Night) in the player controls, driving the facade's setDrc(). Always shown (audio is always decoded on both tiers); resets to Line per stream like the deint mode. Night is a built-in compressor that tames loud peaks on any codec — useful for late-night listening.

Deliberately left out (no public ferrite.js API / not appropriate as app settings): the fast non-conformant decode toggle and a persisted default-deinterlace setting.

Validation

  • pnpm nx lint ui-playback
  • pnpm nx test ui-playback ✓ (140 tests; +2 Dyna control tests)
  • pnpm nx test workspace-shell-feature ✓ (command-palette registration)
  • pnpm nx build web ✓ (1.3.2 engine assets bundled into dist/apps/web/assets)

multiduplikator and others added 2 commits June 28, 2026 14:22
…re HEVC

Adds Ferrite as a new inline web player — canvas + WASM software decode, with an
automatic WebCodecs hardware tier — for software HEVC where the browser has no
hardware decoder (web/PWA; requires a cross-origin-isolated context).

- ferrite-player component + session/event wiring (live + VOD), IPTVnator-owned
  Material controls (play/mute/volume/fullscreen, VOD seek bar, software-tier
  deinterlace select) and a long-press diagnostic overlay; teardown via the facade's
  canonical destroy() (releases the transferred canvas + clears all listeners).
- web-player-view routing (HLS m3u/m3u8 falls back to the default inline player),
  settings + command-palette registration, i18n, docs, and the engine/worker asset
  bundling with an opt-in COOP/COEP isolated dev-serve configuration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s control

ferrite.js 1.3.2 brings an audio-path overhaul (clean 5.1→stereo downmix + device-rate
resample, loudness leveling) and the new "Dyna" dynamics control, all-codec anamorphic
SAR, and correct HEVC sizing on browsers with bogus VideoFrame dims (Edge). The audio
overhaul, SAR, and HEVC sizing are internal to ferrite.js and apply automatically via the
re-bundled 1.3.2 engine assets.

- bump ferrite.js ^1.3.0 → ^1.3.2 (lockfile + bundled engine/worker assets).
- add a "Dyna" select (Line / RF / Night) to the Ferrite controls, driving the facade's
  setDrc(0|1|2). Always shown (audio is always decoded on both tiers); resets to Line per
  stream like the deint mode. Night is a built-in compressor that tames peaks on any codec.
- i18n keys (FERRITE_PLAYER.DYNA*) across all locales; component + control specs extended.

Deliberately left out (no public ferrite.js API / not appropriate as app settings): the
fast non-conformant decode toggle and a persisted default-deinterlace setting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown

Greptile Summary

Adds Ferrite — a <canvas> + WASM/WebCodecs software decoder — as a fourth inline player option for the web/PWA build, enabling HEVC playback where no hardware decoder is available. HLS URLs (.m3u/.m3u8) automatically fall back to Video.js. The player requires a cross-origin-isolated context (COOP/COEP) and surfaces a long-press diagnostic overlay to surface isolation status on devtools-less devices.

  • New FerritePlayerComponent plus supporting modules (ferrite-session, ferrite-event-wiring, ferrite-controls, ferrite-debug-overlay, overlay-interactions) wired into WebPlayerViewComponent's player-selection ladder.
  • Ferrite assets (ferrite.{mjs,wasm}, worker.js, present-worker.js) bundled via project.json; an opt-in nx serve web --configuration=ferrite serve config adds COOP/COEP headers for local testing.
  • Settings, command-palette, i18n (23 locales), and diagnostics model updated to register the new player.

Confidence Score: 4/5

Safe to merge; the Ferrite player lifecycle, teardown, and HLS fallback are all correctly implemented with no data loss or crash paths.

The implementation is well-structured: teardown always goes through destroy(), the OffscreenCanvas re-keying pattern is correct, zone isolation is applied consistently, and tests cover the key lifecycle paths. The two open items — formatPlayer missing the 'ferrite' arm and the ferrite serve config pointing at the production build — are cosmetic gaps that don't affect runtime correctness.

web-player-view.component.ts (formatPlayer gap) and apps/web/project.json (production build target in the dev-serve config).

Important Files Changed

Filename Overview
libs/ui/playback/src/lib/ferrite-player/ferrite-player.component.ts Core Ferrite player component; 346 lines (over 300-line soft limit). Logic is sound — effect-based play/zap lifecycle, untracked guards, proper teardown via destroy(), and zone isolation for canvas/worker work. File size approaching the project's hard limit.
libs/ui/playback/src/lib/ferrite-player/ferrite-session.ts Session factory and teardown helpers. VOD resume handler is registered before load() and removed by destroy(), avoiding stale seeks. Clean zone isolation pattern.
libs/ui/playback/src/lib/ferrite-player/ferrite-event-wiring.ts Wires Ferrite facade events to component signal sinks. TIME_UPDATE is throttled to ~4 Hz; fatal vs. recoverable error distinction is correct; all listeners cleared by destroy().
libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts Adds Ferrite to the player-selection ladder with correct HLS fallback. formatPlayer is missing the 'ferrite' arm, leaving the player label blank in diagnostic detail panels when Ferrite errors.
apps/web/project.json Adds Ferrite asset glob rules and an opt-in ferrite serve configuration with COOP/COEP headers. The ferrite serve config uses buildTarget: web:build:production, which makes dev-serve iteration slow; switching to the development build target would be faster without losing the COOP/COEP headers.
libs/ui/playback/src/lib/ferrite-player/ferrite-controls.component.ts Presentational controls bar with scrub-while-dragging state. Signal-based inputs/outputs, correct seekTo / deintChange / dynaChange wiring.
libs/ui/playback/src/lib/ferrite-player/overlay-interactions.ts Framework-free idle-hide + long-press wiring. Timer cleanup is complete; no leaks across channel zaps. Long-press correctly scoped to the canvas only.
libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.model.ts Adds Ferrite: 'ferrite' to InlinePlaybackPlayer. Clean, minimal change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant WPV as WebPlayerViewComponent
    participant FPC as FerritePlayerComponent
    participant FS as ferrite-session
    participant FEW as ferrite-event-wiring
    participant FP as FerritePlayer (facade)
    participant OI as overlay-interactions

    WPV->>WPV: "selectedPlayer() == 'ferrite'?"
    Note over WPV: HLS fallback: m3u/m3u8 → VideoJs
    WPV->>FPC: "@defer renders app-ferrite-player"

    FPC->>FPC: effect(): channel + canvasRef ready
    FPC->>FS: startFerriteSession(opts)
    FS->>FP: "Ferrite.createPlayer({mpegts, isLive, url})"
    FS->>FEW: wireFerriteEvents(player, url, sinks)
    FEW->>FP: player.on(ERROR, MEDIA_INFO, TIME_UPDATE, ...)
    FS->>FP: player.attachCanvas(canvas)
    FS->>FP: player.load() + player.play()
    FS-->>FPC: returns FerritePlayer

    FPC->>OI: wireOverlayInteractions(shell, canvas, cb)
    Note over OI: idle-hide timer + long-press debug toggle

    FP->>FEW: MEDIA_INFO event
    FEW->>FPC: sinks.tier.set / sinks.format.set (outside zone)

    FP->>FEW: TIME_UPDATE (throttled 4Hz)
    FEW->>FPC: zone.run → emitTimeUpdate

    FP->>FEW: ERROR (fatal)
    FEW->>WPV: zone.run → emitIssue → handlePlaybackIssue

    Note over FPC: channel zap / ngOnDestroy
    FPC->>FS: stopFerriteSession(player)
    FS->>FP: player.destroy() (outside zone)
    Note over FP: releases OffscreenCanvas, terminates workers, clears all listeners
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant WPV as WebPlayerViewComponent
    participant FPC as FerritePlayerComponent
    participant FS as ferrite-session
    participant FEW as ferrite-event-wiring
    participant FP as FerritePlayer (facade)
    participant OI as overlay-interactions

    WPV->>WPV: "selectedPlayer() == 'ferrite'?"
    Note over WPV: HLS fallback: m3u/m3u8 → VideoJs
    WPV->>FPC: "@defer renders app-ferrite-player"

    FPC->>FPC: effect(): channel + canvasRef ready
    FPC->>FS: startFerriteSession(opts)
    FS->>FP: "Ferrite.createPlayer({mpegts, isLive, url})"
    FS->>FEW: wireFerriteEvents(player, url, sinks)
    FEW->>FP: player.on(ERROR, MEDIA_INFO, TIME_UPDATE, ...)
    FS->>FP: player.attachCanvas(canvas)
    FS->>FP: player.load() + player.play()
    FS-->>FPC: returns FerritePlayer

    FPC->>OI: wireOverlayInteractions(shell, canvas, cb)
    Note over OI: idle-hide timer + long-press debug toggle

    FP->>FEW: MEDIA_INFO event
    FEW->>FPC: sinks.tier.set / sinks.format.set (outside zone)

    FP->>FEW: TIME_UPDATE (throttled 4Hz)
    FEW->>FPC: zone.run → emitTimeUpdate

    FP->>FEW: ERROR (fatal)
    FEW->>WPV: zone.run → emitIssue → handlePlaybackIssue

    Note over FPC: channel zap / ngOnDestroy
    FPC->>FS: stopFerriteSession(player)
    FS->>FP: player.destroy() (outside zone)
    Note over FP: releases OffscreenCanvas, terminates workers, clears all listeners
Loading

Comments Outside Diff (1)

  1. libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts, line 377-388 (link)

    P2 formatPlayer missing 'ferrite' case

    InlinePlaybackPlayer.Ferrite = 'ferrite' is now a valid value for PlaybackDiagnostic.player, but the formatPlayer switch has no matching arm. When Ferrite emits a fatal diagnostic (e.g. crossOriginIsolated = false), the "Player" row in the diagnostic detail panel will be blank, making it harder to identify the failing player from the detail dump.

    Add case 'ferrite': return 'Ferrite'; to the switch before the default arm.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts
    Line: 377-388
    
    Comment:
    **`formatPlayer` missing 'ferrite' case**
    
    `InlinePlaybackPlayer.Ferrite = 'ferrite'` is now a valid value for `PlaybackDiagnostic.player`, but the `formatPlayer` switch has no matching arm. When Ferrite emits a fatal diagnostic (e.g. `crossOriginIsolated = false`), the "Player" row in the diagnostic detail panel will be blank, making it harder to identify the failing player from the detail dump.
    
    Add `case 'ferrite': return 'Ferrite';` to the switch before the `default` arm.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts:377-388
**`formatPlayer` missing 'ferrite' case**

`InlinePlaybackPlayer.Ferrite = 'ferrite'` is now a valid value for `PlaybackDiagnostic.player`, but the `formatPlayer` switch has no matching arm. When Ferrite emits a fatal diagnostic (e.g. `crossOriginIsolated = false`), the "Player" row in the diagnostic detail panel will be blank, making it harder to identify the failing player from the detail dump.

Add `case 'ferrite': return 'Ferrite';` to the switch before the `default` arm.

```suggestion
    private formatPlayer(player: PlaybackDiagnostic['player']): string {
        switch (player) {
            case 'videojs':
                return 'Video.js';
            case 'html5':
                return 'HTML5';
            case 'artplayer':
                return 'ArtPlayer';
            case 'ferrite':
                return 'Ferrite';
            default:
                return '';
        }
    }
```

### Issue 2 of 3
libs/ui/playback/src/lib/ferrite-player/ferrite-player.component.ts:1-10
**File size approaching hard limit**

`ferrite-player.component.ts` is 346 lines. The project's TypeScript file-size rule (from `CLAUDE.md`) sets a soft limit of 300 lines and a hard maximum of 350–400 lines. This file is already 46 lines over the soft limit. The existing split into `ferrite-session.ts`, `ferrite-event-wiring.ts`, and `overlay-interactions.ts` is good, but the remaining volume in the component itself (event-handler methods, the fullscreen host-listener, `applyVolume`, `playChannel`, and teardown) could be extracted into a small `FerritePlayerStateService` or a dedicated `ferrite-player.actions.ts` helper if a follow-up feature adds more methods.

### Issue 3 of 3
apps/web/project.json:162-172
**`ferrite` serve configuration rebuilds with the production target**

`"buildTarget": "web:build:production"` means `nx serve web --configuration=ferrite` performs a full production build (full optimization, no source maps by default) before serving, making development iteration significantly slower. Unless the production build is specifically required for the COOP/COEP test (e.g. to validate the bundled worker assets), consider using `"web:build:development"` to keep fast rebuilds. The `headers` block that sets COOP/COEP is independent of the build target and will work with either.

Reviews (1): Last reviewed commit: "feat(playback): bump ferrite.js to 1.3.2..." | Re-trigger Greptile

Comment on lines +1 to +10
import {
ChangeDetectionStrategy,
Component,
computed,
effect,
ElementRef,
HostListener,
inject,
input,
NgZone,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 File size approaching hard limit

ferrite-player.component.ts is 346 lines. The project's TypeScript file-size rule (from CLAUDE.md) sets a soft limit of 300 lines and a hard maximum of 350–400 lines. This file is already 46 lines over the soft limit. The existing split into ferrite-session.ts, ferrite-event-wiring.ts, and overlay-interactions.ts is good, but the remaining volume in the component itself (event-handler methods, the fullscreen host-listener, applyVolume, playChannel, and teardown) could be extracted into a small FerritePlayerStateService or a dedicated ferrite-player.actions.ts helper if a follow-up feature adds more methods.

Context Used: CLAUDE.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: libs/ui/playback/src/lib/ferrite-player/ferrite-player.component.ts
Line: 1-10

Comment:
**File size approaching hard limit**

`ferrite-player.component.ts` is 346 lines. The project's TypeScript file-size rule (from `CLAUDE.md`) sets a soft limit of 300 lines and a hard maximum of 350–400 lines. This file is already 46 lines over the soft limit. The existing split into `ferrite-session.ts`, `ferrite-event-wiring.ts`, and `overlay-interactions.ts` is good, but the remaining volume in the component itself (event-handler methods, the fullscreen host-listener, `applyVolume`, `playChannel`, and teardown) could be extracted into a small `FerritePlayerStateService` or a dedicated `ferrite-player.actions.ts` helper if a follow-up feature adds more methods.

**Context Used:** CLAUDE.md ([source](https://app.greptile.com/iptvnator/github/4gray/iptvnator/-/custom-context?memory=d7399d0f-9d03-4561-9aed-4525c6d78ad1))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread apps/web/project.json
Comment on lines 162 to 172
"buildTarget": "web:build:development"
},
"ferrite": {
"buildTarget": "web:build:production",
"host": "0.0.0.0",
"allowedHosts": true,
"headers": {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "credentialless"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 ferrite serve configuration rebuilds with the production target

"buildTarget": "web:build:production" means nx serve web --configuration=ferrite performs a full production build (full optimization, no source maps by default) before serving, making development iteration significantly slower. Unless the production build is specifically required for the COOP/COEP test (e.g. to validate the bundled worker assets), consider using "web:build:development" to keep fast rebuilds. The headers block that sets COOP/COEP is independent of the build target and will work with either.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/project.json
Line: 162-172

Comment:
**`ferrite` serve configuration rebuilds with the production target**

`"buildTarget": "web:build:production"` means `nx serve web --configuration=ferrite` performs a full production build (full optimization, no source maps by default) before serving, making development iteration significantly slower. Unless the production build is specifically required for the COOP/COEP test (e.g. to validate the bundled worker assets), consider using `"web:build:development"` to keep fast rebuilds. The `headers` block that sets COOP/COEP is independent of the build target and will work with either.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

… faster ferrite dev-serve

- web-player-view: add the 'ferrite' arm to formatPlayer so the diagnostics detail panel
  shows "Ferrite" instead of a blank label when a Ferrite playback issue is surfaced.
- project.json: the opt-in `ferrite` dev-serve config now builds web:build:development
  (was :production) — faster local iteration and the correct baseHref="/" for serving;
  the COOP/COEP headers live on the serve config, so isolation is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@multiduplikator

Copy link
Copy Markdown
Author

Thanks for the review! Addressed both cosmetic findings in the latest commit:

  • formatPlayer missing the 'ferrite' arm — added; the diagnostics detail panel now shows "Ferrite" instead of a blank label.
  • ferrite dev-serve using the production build target — switched to web:build:development (faster iteration + correct baseHref="/"); the COOP/COEP headers are on the serve config, so isolation is unchanged.

(Note: greptile only reviews on PR open, so this push won't re-trigger it — flagging the fixes here for the manual pass.)

@multiduplikator

Copy link
Copy Markdown
Author

Superseded by #1111 — rebased onto current master and updated to ferrite.js 1.3.4 (four-worker topology: off-main-thread audio + the mpv-faithful A/V-sync corrector). Re-submitted as a single clean commit so the review bot does a fresh pass. All prior review findings remain addressed; see #1111.

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