feat(playback): add Ferrite (WASM/WebCodecs) inline player for software HEVC#1111
feat(playback): add Ferrite (WASM/WebCodecs) inline player for software HEVC#1111multiduplikator wants to merge 1 commit into
Conversation
Greptile SummaryThis PR adds Ferrite — a
Confidence Score: 4/5The new Ferrite player path is additive and fully isolated from all existing player paths — no existing playback flow is modified. The canvas-keyed channel-zap teardown, outside-zone player creation, and graceful degradation when crossOriginIsolated is absent are all implemented correctly and covered by the new test suite. The component file sits above the project's 300-line style guideline (346 lines), and the dev-serve config opens the dev server to all network interfaces with unrestricted host matching. Neither is a runtime correctness issue, but both warrant follow-up before the config pattern is copied elsewhere. ferrite-player.component.ts (line-budget overage) and apps/web/project.json (the ferrite serve config's host/allowedHosts settings) are the two spots worth a second look before merge. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as User
participant WPV as WebPlayerViewComponent
participant FPC as FerritePlayerComponent
participant FSS as ferrite-session.ts
participant FEW as ferrite-event-wiring.ts
participant Lib as ferrite.js facade
U->>WPV: select channel (Ferrite player)
WPV->>WPV: selectedPlayer() computed (falls back to VideoJs if .m3u/.m3u8)
WPV->>FPC: "@defer load + [channel] input"
FPC->>FPC: "canvasKey() recomputes - @for renders canvas"
FPC->>FSS: "startFerriteSession({url, canvas, ...})"
FSS->>Lib: Ferrite.createPlayer() outside Angular zone
FSS->>FEW: wireFerriteEvents(player, url, sinks)
FEW->>Lib: player.on(ERROR/MEDIA_INFO/TIME_UPDATE/...)
FSS->>Lib: attachCanvas(canvas)
FSS->>Lib: load() + play()
Lib-->>FEW: MEDIA_INFO event
FEW-->>FPC: tier/format signals updated (zone.run)
Lib-->>FEW: TIME_UPDATE (throttled ~4Hz)
FEW-->>FPC: currentTime/duration signals + timeUpdate output (zone.run)
Lib-->>FEW: ERROR (fatal)
FEW-->>FPC: playbackIssue output (zone.run)
U->>FPC: channel zap
FPC->>FPC: teardown() - stopFerriteSession()
FPC->>Lib: player.destroy() (terminates workers, releases OffscreenCanvas)
FPC->>FPC: "canvasKey() recomputes - @for destroys old canvas, creates new"
FPC->>FSS: startFerriteSession (new URL + fresh canvas)
%%{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 U as User
participant WPV as WebPlayerViewComponent
participant FPC as FerritePlayerComponent
participant FSS as ferrite-session.ts
participant FEW as ferrite-event-wiring.ts
participant Lib as ferrite.js facade
U->>WPV: select channel (Ferrite player)
WPV->>WPV: selectedPlayer() computed (falls back to VideoJs if .m3u/.m3u8)
WPV->>FPC: "@defer load + [channel] input"
FPC->>FPC: "canvasKey() recomputes - @for renders canvas"
FPC->>FSS: "startFerriteSession({url, canvas, ...})"
FSS->>Lib: Ferrite.createPlayer() outside Angular zone
FSS->>FEW: wireFerriteEvents(player, url, sinks)
FEW->>Lib: player.on(ERROR/MEDIA_INFO/TIME_UPDATE/...)
FSS->>Lib: attachCanvas(canvas)
FSS->>Lib: load() + play()
Lib-->>FEW: MEDIA_INFO event
FEW-->>FPC: tier/format signals updated (zone.run)
Lib-->>FEW: TIME_UPDATE (throttled ~4Hz)
FEW-->>FPC: currentTime/duration signals + timeUpdate output (zone.run)
Lib-->>FEW: ERROR (fatal)
FEW-->>FPC: playbackIssue output (zone.run)
U->>FPC: channel zap
FPC->>FPC: teardown() - stopFerriteSession()
FPC->>Lib: player.destroy() (terminates workers, releases OffscreenCanvas)
FPC->>FPC: "canvasKey() recomputes - @for destroys old canvas, creates new"
FPC->>FSS: startFerriteSession (new URL + fresh canvas)
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
libs/ui/playback/src/lib/ferrite-player/ferrite-player.component.ts:1-10
**File exceeds the 300-line policy**
`ferrite-player.component.ts` is 346 lines. The repo's `CLAUDE.md` engineering rule sets a 300-line soft limit for TypeScript files, with 350–400 as the hard maximum. The PR description notes the concerns are already split into five sibling files, but the component itself still sits 46 lines over the soft target. Possible extractions to close the gap: the three `effect()` bodies (playback, overlay-wiring, volume) could move into a dedicated `ferrite-lifecycle.ts` helper that accepts the component's signals/refs as arguments, or the set of overlay action handlers (`togglePlay`, `toggleMute`, `applyVolume`, `toggleFullscreen`, `onSeek`, `onDeintChange`, `onDynaChange`) could be grouped into a small `ferrite-overlay-actions.ts` file. Either would bring the component down near the target without changing any public API.
### Issue 2 of 2
apps/web/project.json:164-168
`allowedHosts: true` accepts requests from any hostname, which combined with `host: "0.0.0.0"` makes the dev server reachable from—and impersonatable by—any origin on the same network. The preferred hardening for a local COOP/COEP test is to leave `host` at the loopback default (`localhost`) and enumerate only the hostnames actually needed. If LAN testing across devices is genuinely required, scoping to a single LAN hostname is safer than opening to all.
```suggestion
"ferrite": {
"buildTarget": "web:build:development",
"headers": {
```
Reviews (1): Last reviewed commit: "feat(playback): add Ferrite (WASM/WebCod..." | Re-trigger Greptile |
| import { | ||
| ChangeDetectionStrategy, | ||
| Component, | ||
| computed, | ||
| effect, | ||
| ElementRef, | ||
| HostListener, | ||
| inject, | ||
| input, | ||
| NgZone, |
There was a problem hiding this comment.
File exceeds the 300-line policy
ferrite-player.component.ts is 346 lines. The repo's CLAUDE.md engineering rule sets a 300-line soft limit for TypeScript files, with 350–400 as the hard maximum. The PR description notes the concerns are already split into five sibling files, but the component itself still sits 46 lines over the soft target. Possible extractions to close the gap: the three effect() bodies (playback, overlay-wiring, volume) could move into a dedicated ferrite-lifecycle.ts helper that accepts the component's signals/refs as arguments, or the set of overlay action handlers (togglePlay, toggleMute, applyVolume, toggleFullscreen, onSeek, onDeintChange, onDynaChange) could be grouped into a small ferrite-overlay-actions.ts file. Either would bring the component down near the target without changing any public API.
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 exceeds the 300-line policy**
`ferrite-player.component.ts` is 346 lines. The repo's `CLAUDE.md` engineering rule sets a 300-line soft limit for TypeScript files, with 350–400 as the hard maximum. The PR description notes the concerns are already split into five sibling files, but the component itself still sits 46 lines over the soft target. Possible extractions to close the gap: the three `effect()` bodies (playback, overlay-wiring, volume) could move into a dedicated `ferrite-lifecycle.ts` helper that accepts the component's signals/refs as arguments, or the set of overlay action handlers (`togglePlay`, `toggleMute`, `applyVolume`, `toggleFullscreen`, `onSeek`, `onDeintChange`, `onDynaChange`) could be grouped into a small `ferrite-overlay-actions.ts` file. Either would bring the component down near the target without changing any public API.
**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!
| "ferrite": { | ||
| "buildTarget": "web:build:development", | ||
| "host": "0.0.0.0", | ||
| "allowedHosts": true, | ||
| "headers": { |
There was a problem hiding this comment.
allowedHosts: true accepts requests from any hostname, which combined with host: "0.0.0.0" makes the dev server reachable from—and impersonatable by—any origin on the same network. The preferred hardening for a local COOP/COEP test is to leave host at the loopback default (localhost) and enumerate only the hostnames actually needed. If LAN testing across devices is genuinely required, scoping to a single LAN hostname is safer than opening to all.
| "ferrite": { | |
| "buildTarget": "web:build:development", | |
| "host": "0.0.0.0", | |
| "allowedHosts": true, | |
| "headers": { | |
| "ferrite": { | |
| "buildTarget": "web:build:development", | |
| "headers": { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/project.json
Line: 164-168
Comment:
`allowedHosts: true` accepts requests from any hostname, which combined with `host: "0.0.0.0"` makes the dev server reachable from—and impersonatable by—any origin on the same network. The preferred hardening for a local COOP/COEP test is to leave `host` at the loopback default (`localhost`) and enumerate only the hostnames actually needed. If LAN testing across devices is genuinely required, scoping to a single LAN hostname is safer than opening to all.
```suggestion
"ferrite": {
"buildTarget": "web:build:development",
"headers": {
```
How can I resolve this? If you propose a fix, please make it concise.…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, "Dyna" audio-dynamics 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.
- ferrite.js 1.3.4: four self-contained worker chunks (demux, video-decode,
audio-decode, present) are copied into the served assets and fed to the facade via
the {worker,presentWorker,audioWorker,demuxWorker}Url overrides — the esbuild
builder does not rewrite a dependency's internal new Worker(new URL(...)). Audio
runs off the main thread (an AudioWorklet-driven clock) and A/V sync uses the
mpv-faithful display-sync corrector.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a9b23a4 to
d4b824c
Compare
|
Thanks for the review! Addressed the two P2 findings:
|
What
Adds Ferrite — a
<canvas>+ WebAssembly/WebCodecs player (ferrite.js) — as a fourth inline web/PWA player, so HEVC plays where the browser has no hardware decoder (the current gap:<video>+ hls.js/mpegts.js can't software-decode HEVC). It has an automatic WebCodecs hardware tier and falls back to the default inline player for HLS (.m3u/.m3u8).Requires a cross-origin-isolated context (COOP/COEP) for
SharedArrayBuffer; when isolation is off it degrades gracefully with an explicit unsupported banner (no black screen).How it's wired
FerritePlayerComponent(same component I/O asHtmlVideoPlayerComponent, rendering to a canvas) with the separable concerns split out —ferrite-session,ferrite-event-wiring,ferrite-controls,ferrite-debug-overlay,overlay-interactions.WebPlayerViewComponent'sselectedPlayer()ladder asVideoPlayer.Ferrite; teardown always via the facade's canonicaldestroy()(releases the transferredOffscreenCanvas+ clears all listeners).isolated: NOon a devtools-less PWA).nx serve web --configuration=ferritethat adds the COOP/COEP headers for local testing. No change to any existing player path.ferrite.js 1.3.4
Bumps the engine to
1.3.4, which moves to a four-worker topology (demux · video-decode · audio-decode · present). The four self-contained worker chunks are copied into the served assets and fed to the facade via its{worker,presentWorker,audioWorker,demuxWorker}Urloverrides — the esbuild builder does not rewrite a dependency's internalnew Worker(new URL('./x.js', import.meta.url)), so the explicit same-origin URLs are required for the isolated context. Audio now runs off the main thread (an AudioWorklet-driven master clock), and A/V sync uses an mpv-faithful display-sync corrector (tight lip-sync after pause/resume).Review history
Supersedes #1098 (and the earlier #1092 / #1091). This is a clean, rebased, single-commit re-submission so the review bot gets a fresh pass. All prior findings are incorporated: no hardcoded
BACKEND_URL;canvasKeykeys on the effective URL (url + epgParams);formatPlayerhas the'ferrite'arm; theferritedev-serve uses the development build. On the earlier soft note aboutferrite-player.component.tslength — the separable concerns are already extracted into the five sibling files above, and what remains is the cohesive lifecycle core (it passes lint; there is nomax-linesrule).Testing
nx lint ui-playbackclean;nx test ui-playbackgreen (140 tests, incl. the ferrite specs — the component spec locks the four-worker wiring).