Skip to content

feat: add casting controls and expandable workspace navigation#1056

Open
SalemOurabi wants to merge 24 commits into
4gray:masterfrom
SalemOurabi:codex/casting-support
Open

feat: add casting controls and expandable workspace navigation#1056
SalemOurabi wants to merge 24 commits into
4gray:masterfrom
SalemOurabi:codex/casting-support

Conversation

@SalemOurabi

@SalemOurabi SalemOurabi commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add consistently available remote playback controls for AirPlay, Google Cast, browser Remote Playback, and Electron DLNA/UPnP
  • support casting across inline video, radio, embedded, and external playback surfaces when the stream is receiver-fetchable
  • add an expandable workspace navigation rail with compact icon and labeled modes, fixed Settings access, RTL-aware labels, and unclipped active markers
  • fix Electron development startup on Nx 22 with an owned web/Electron process coordinator
  • return structured Xtream IPC failures so background EPG errors no longer produce repeated Electron handler rejection logs

Security and behavior

  • this implements media URL handoff, not screen mirroring
  • reject receiver handoff for local/unsafe schemes, embedded credentials, and playback requiring protected request headers
  • pin DLNA HTTP requests to the discovered responder address, disable redirects, and enforce response-size and time limits
  • never expose provider header values or authorization credentials to the renderer
  • clear stale MPV request properties before loading a new stream in a reused process
  • restrict the CSP addition to the official Google Cast SDK origin
  • preserve Xtream cancellation metadata (AbortError, status 499) through Electron IPC, renderer normalization, and the Xtream API service

Latest review fixes

  • update Electron E2E navigation helpers after the brand changed from a dashboard link to the rail expand/collapse button
  • verify the active Sources route and brand button when Dashboard is disabled
  • preserve name and status on structured Xtream errors so cancelled imports remain classified as cancelled rather than failed
  • merge upstream remote-control volume support with casting in AudioPlayerComponent; both the playback cast metadata input and external volume/volumeChange contract are retained
  • update the M3U player test stub to cover the combined audio-player API
  • resolve all prior Greptile review threads

Validation

  • pnpm run typecheck:ci
  • pnpm nx test web --runInBand --runTestsByPath apps/web/src/app/services/electron.service.spec.ts apps/web/src/app/settings/settings.component.spec.ts (2 suites, 40 tests)
  • pnpm nx test portal-xtream-data-access --runInBand (12 suites, 93 tests)
  • pnpm nx test ui-playback --runInBand --runTestsByPath libs/ui/playback/src/lib/audio-player/audio-player.component.spec.ts (17 suites, 146 tests)
  • pnpm nx test playlist-m3u-feature-player --runInBand --runTestsByPath libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts (4 suites, 47 tests)
  • pnpm nx run electron-backend-e2e:e2e-ci--src/settings.e2e.ts (6 passed)
  • pnpm nx run electron-backend-e2e:e2e-ci--src/dashboard-activation.e2e.ts (1 passed)
  • affected project lint and exact-file ESLint checks pass; remaining warnings are pre-existing
  • git diff --check, conflict-marker scan, and git merge-tree --write-tree HEAD upstream/master pass

Upstream and conflicts

  • fork master is synchronized with 4gray/iptvnator:master at 1444047a
  • PR branch contains current upstream and is 0 commits behind
  • the only merge conflicts were in the audio player and its spec; they were resolved additively to retain both casting and upstream remote-volume behavior
  • no unresolved files or conflict markers remain

Notes

  • real AirPlay, Cast, and DLNA device validation still requires compatible hardware on the same network
  • the Electron build skips embedded MPV when no vendored Windows runtime is present
  • dependency audit findings are pre-existing; this update does not change package.json or pnpm-lock.yaml
  • documentation updated in README.md, docs/architecture/remote-playback.md, docs/architecture/electron-security.md, and docs/architecture/workspace-shell.md
  • wiki export skipped because IPTVNATOR_WIKI_VAULT is not configured

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds remote playback controls (AirPlay, Google Cast, browser Remote Playback, Electron DLNA/UPnP) and an expandable workspace navigation rail, while fixing two previously noisy runtime issues: repeated Electron IPC rejection logs from background Xtream EPG requests, and stale E2E navigation selectors after the dashboard brand became a rail-toggle button.

  • Structured Xtream error path: The backend now returns { type: 'ERROR', ... } instead of rejecting the IPC handler, preserving name/status through electron.service.ts and xtream-api.service.ts all the way to callers; cancellation (AbortError, status 499) and provider failures (status 520, etc.) are classified correctly, and tests cover all three layers.
  • Audio-player merge conflict: Both the upstream remote-control volume/volumeChange contract and the new casting playback/castPlayback signals are additively merged; CastControlComponent is added alongside the existing transport controls in the extracted HTML template.
  • DLNA/UPnP casting: New DlnaRendererService discovers UPnP renderers via SSDP, pins HTTP requests to the discovered responder address, enforces no-redirect and response-size limits; the preload bridge exposes discoverDlnaRenderers/startDlnaPlayback and is guarded by RuntimeCapabilitiesService.supportsDlnaCasting.

Confidence Score: 5/5

All changed paths are correct and well-tested; the error-flow, conflict resolution, and E2E selector updates are all verified.

The structured Xtream error path is correctly threaded through all three layers (backend to ElectronService to XtreamApiService), with unit tests at each hop. The audio-player merge retains both the upstream remote-volume contract and the new casting signals without conflict. No stale conflict markers remain. DLNA SSRF protections are consistent with the backend URL-safety pattern. The CSP change is tightly scoped to the official Cast SDK origin. Previously flagged issues (module-level handler registration in casting.events.ts, 0.0.0.0/localhost gaps in isDirectCastUrl, hasPlaybackHeaders duplication) are all resolved in this head.

No files require special attention.

Important Files Changed

Filename Overview
apps/electron-backend/src/app/events/xtream.events.ts Replaces throw with return createXtreamErrorResponse(error) to prevent rejected IPC handlers; correctly preserves name/status on all error branches; suppressErrorLog tested end-to-end.
apps/web/src/app/services/electron.service.ts New 'type' in response guard re-throws structured error responses so the existing catch block can normalize and re-emit them; name and status are preserved via spread into the return value.
libs/portal/xtream/data-access/src/lib/services/xtream-api.service.ts Correctly reconstructs a typed Error from the structured IPC response, attaching name (e.g. AbortError) and status so callers can classify cancellations vs. provider failures.
libs/ui/playback/src/lib/audio-player/audio-player.component.ts Merge conflict resolved additively: upstream volume/volumeChange contract and casting playback/castPlayback computed are both present; template extracted to separate HTML file.
apps/electron-backend/src/app/events/casting.events.ts Both ipcMain.handle calls are now inside bootstrapCastingEvents(); DlnaRendererService instantiation at module level is benign (no side effects, stubbed in tests).
apps/electron-backend/src/app/services/dlna-renderer.service.ts SSDP discovery with pinned HTTP requests, redirect-disabled fetch, response-size limits, and candidate count cap (32); SSRF guard via isReceiverFetchableUrl before SOAP playback.
libs/ui/playback/src/lib/casting/cast-media.utils.ts Blocks 0.0.0.0, .localhost subdomains, [::1], 127., embedded credentials, and non-HTTP/S schemes; hasPlaybackHeaders is no longer duplicated here - now lives in shared interfaces.
apps/electron-backend-e2e/src/settings.e2e.ts Selectors updated to getByRole('link', { name: 'Sources' }) and button.brand, correctly reflecting the new rail-toggle navigation.
apps/web/src/index.html CSP script-src extended with https://www.gstatic.com for the Google Cast SDK - correctly scoped to the official SDK origin only.
libs/ui/playback/src/lib/casting/cast.service.ts Google Cast SDK loaded lazily with timeout, callback restore, and promise-reset on failure; DLNA guarded by runtime capability check; canUseGoogleCast enforces secure context, direct URL, and no custom headers.
libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts CastControlVisibility timer correctly cleaned up in ngOnDestroy; overlay visibility driven by pointer/keyboard events on the host element.
libs/shared/interfaces/src/lib/electron-api.interface.ts Xtream response split into ElectronBridgeXtreamSuccessResponse / ElectronBridgeXtreamErrorResponse union; DLNA bridge methods added as optional on ElectronBridgeApi.

Sequence Diagram

sequenceDiagram
    participant Renderer as Angular Renderer
    participant ES as ElectronService
    participant IPC as Electron IPC
    participant XE as xtream.events.ts
    participant XAS as XtreamApiService

    Renderer->>XAS: getLiveStreams(credentials, opts)
    XAS->>ES: sendIpcEvent(XTREAM_REQUEST, payload)
    ES->>IPC: window.electron.xtreamRequest(payload)
    IPC->>XE: ipcMain.handle('XTREAM_REQUEST')

    alt Success
        XE-->>IPC: "return { payload, action }"
        IPC-->>ES: "resolves { payload, action }"
        ES-->>XAS: "returns { type: XTREAM_RESPONSE, payload }"
        XAS-->>Renderer: returns typed response
    else Error
        XE-->>IPC: return createXtreamErrorResponse(error)
        IPC-->>ES: "resolves { type: ERROR, name?, status, message }"
        ES->>ES: 'type' in response - throw response
        ES->>ES: catch: normalize name/status
        ES-->>XAS: "resolves { type: ERROR, name?, status, message }"
        XAS->>XAS: "type===ERROR - throw Error with .name/.status"
        XAS-->>Renderer: rejects Error
    end
Loading

Reviews (6): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger Greptile

Comment thread apps/electron-backend/src/app/services/dlna-protocol.ts
Comment thread apps/electron-backend/src/app/events/casting.events.ts
Comment thread libs/ui/playback/src/lib/casting/cast-media.utils.ts Outdated
Comment thread libs/ui/playback/src/lib/casting/cast-media.utils.ts
@codecov-commenter

codecov-commenter commented Jun 13, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 63.73457% with 235 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.87%. Comparing base (e91cbd5) to head (944e87e).
⚠️ Report is 1923 commits behind head on master.

Files with missing lines Patch % Lines
...-backend/src/app/services/dlna-renderer.service.ts 34.73% 59 Missing and 3 partials ⚠️
libs/ui/playback/src/lib/casting/cast.service.ts 54.90% 38 Missing and 8 partials ⚠️
...electron-backend/src/app/services/dlna-protocol.ts 57.89% 34 Missing and 6 partials ⚠️
...tron-backend/src/app/events/xtream-request.util.ts 43.58% 19 Missing and 3 partials ⚠️
...playback/src/lib/casting/cast-control.component.ts 81.66% 7 Missing and 4 partials ⚠️
...bs/ui/playback/src/lib/casting/cast-media.utils.ts 71.42% 8 Missing and 2 partials ⚠️
...c/lib/web-player-view/web-player-view.component.ts 75.00% 5 Missing and 4 partials ⚠️
apps/electron-backend/src/app/services/dlna-xml.ts 83.67% 2 Missing and 6 partials ⚠️
apps/electron-backend/src/app/api/main.preload.ts 0.00% 4 Missing ⚠️
...shared/interfaces/src/lib/portal-playback.utils.ts 0.00% 4 Missing ⚠️
... and 11 more
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

❗ There is a different number of reports uploaded between BASE (e91cbd5) and HEAD (944e87e). Click for more details.

HEAD has 4 uploads less than BASE
Flag BASE (e91cbd5) HEAD (944e87e)
4 0
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #1056       +/-   ##
===========================================
- Coverage   71.05%   54.87%   -16.19%     
===========================================
  Files          40      551      +511     
  Lines         691    30489    +29798     
  Branches       87     6519     +6432     
===========================================
+ Hits          491    16730    +16239     
- Misses        176    11235    +11059     
- Partials       24     2524     +2500     
Flag Coverage Δ
unit 54.87% <63.73%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor Author

@greptileai

Please re-review the updated head 441be837. The four prior P2 findings were addressed, their threads are resolved, and the follow-up also includes DLNA/Google Cast security hardening plus the Nx/Electron development-start fix.

Copy link
Copy Markdown
Contributor Author

@greptileai

Please perform the final re-review on head e49383cc. Since the previous 5/5 review, this follow-up hardens the Nx/Electron dev launcher with owned process coordination and seven regression tests, and adds an absolute deadline to pinned DLNA HTTP requests with regression coverage. Independent reviewer verdict: APPROVED.

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review the latest head b9007db1. The shared video-player cast overlay now auto-hides after inactivity, reappears on pointer/keyboard activity, and remains visible during hover, focus, or an open device menu. Please specifically check timer cleanup, menu/focus races, accessibility, reduced-motion styling, and conflicts with the existing player control visibility behavior.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

Copy link
Copy Markdown
Contributor Author

@greptileai please perform a final re-review of head 516755e7. The two non-blocking observations from the 5/5 review are now addressed: handleFocusOut has direct inside/outside-container regression tests, and the hidden cast overlay uses aria-hidden plus inert so it leaves assistive technology and keyboard navigation. The branch also merged current upstream 8d672f8b without conflicts. Please specifically verify accessibility, focus/menu behavior, timer cleanup, and the upstream merge.

@SalemOurabi SalemOurabi changed the title feat(playback): add AirPlay, Google Cast, and DLNA controls feat: add casting controls and expandable workspace navigation Jun 14, 2026
Introduce a structured ElectronBridgeXtreamErrorResponse and union response type, and update the Xtream IPC flow to return error objects instead of throwing. Added createXtreamErrorResponse to normalize Axios/network errors (including canceled requests -> AbortError with status 499) and fixed syscall/hostname extraction in formatXtreamError. The backend now returns the structured error to avoid rejected Electron handlers; the web renderer checks for a response.type and rethrows to preserve existing error handling. Docs updated to explain why IPC handlers must not be rejected.

Copy link
Copy Markdown
Contributor Author

@greptileai please perform a fresh final review of head cf901ee0.

Since the last Greptile-reviewed head, this update:

  • preserves structured Xtream AbortError/status metadata end-to-end while suppressing noisy background IPC handler failures;
  • repairs the stale Electron E2E dashboard selectors after the brand became a navigation-toggle button;
  • synchronizes the fork and PR branch with upstream 1444047a;
  • resolves the audio-player merge conflict additively, retaining both casting metadata and upstream remote-control volume/volumeChange behavior.

Please specifically check correctness/security of the structured error path, the casting + remote-volume conflict resolution, stale conflict artifacts, and compatibility with the latest dashboard/settings refactor. Local typecheck, unit suites, and targeted Electron E2E suites are green; the updated validation matrix is in the PR description.

Resolve the two xtream conflicts by unioning upstream's portal-
compatibility fix (254879f) with this branch's SSRF + structured-error
work, keeping both intents:

- xtream.events.ts: keep BOTH createXtreamErrorResponse (structured IPC
  error responses + AbortError/499 cancellation mapping) AND upstream's
  buildXtreamApiUrl (normalizeXtreamServerUrl base + username/password
  trim). The merged XTREAM_REQUEST handler already routes through
  requestWithValidatedRedirects (per-hop SSRF validation) and uses both
  helpers.
- xtream-api.service.ts: keep the richer thrown error that preserves
  name + status (needed for cancellation detection) over upstream's bare
  Error; upstream's normalize/trim sendRequest and multi-action
  getAccountInfo auto-merged around it.

Fixes the "Web E2E on Ubuntu Chromium" failure of
"@xtream playlist details edit is retained in the PWA browser context":
the PWA save/validate path needs upstream's normalizeXtreamServerUrl,
which this branch was missing. Verified locally: chromium passes;
electron-backend builds; portal-xtream-data-access (101), shared-
interfaces (29) and xtream.events.spec all green.
Move formatXtreamError, createXtreamErrorResponse and buildXtreamApiUrl
out of xtream.events.ts into a focused xtream-request.util.ts. Behavior
is unchanged; xtream.events.ts now holds only IPC registration and drops
from 380 to 273 lines, back under the repo's 300-line guideline
(CLAUDE.md TypeScript File Size Rule).

Verified: electron-backend build + lint clean, xtream.events.spec green
(372 passed; the 13 failures are the pre-existing Windows-only platform
suites unrelated to this change).
SalemOurabi added a commit to SalemOurabi/iptvnator that referenced this pull request Jun 15, 2026
The casting change to video-player.component.html binds [playback] on the
inline player child; the spec's mock needs the matching input() declared or
the template binding fails. (Carried over from the 4gray#1056 merge resolution
that the topic cherry-pick missed.)
@SalemOurabi

Copy link
Copy Markdown
Contributor Author

Closing in favor of three focused, independently reviewable PRs split out of this branch — each builds and tests green on top of the current master:

This branch had grown to ~20 commits spanning four unrelated concerns, which made review and merge hard and created a recurring conflict surface. Each topic is now isolated:

  • Shared files (the 18 i18n bundles, the workspace shell component, the Electron bridge interface) were separated at the hunk level, so each PR carries only its own changes.
  • The xtream PR is reconstructed on top of the SSRF + portal-compatibility work that has since landed on master.

No changes were lost: the union of the three PRs equals this branch's diff against master, minus the embedded-mpv / packaging changes that are already on master. They can be reviewed and merged in any order (a trivial i18n/shell rebase may be needed for whichever lands second).

@SalemOurabi

Copy link
Copy Markdown
Contributor Author

Reopened — continuing with the combined PR instead of the three-way split (#1069/#1070/#1071, now closed).

@SalemOurabi SalemOurabi reopened this Jun 15, 2026
@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown

Too many files changed for review. (101 files found, 100 file limit)

- CSP: restrict the Google Cast https://www.gstatic.com script-src to the PWA
  build (new index.pwa.html wired via the web:build:pwa `index` input/output
  option); the Electron renderer keeps the tighter `script-src 'self'`.
- dlna-protocol: set the absolute request deadline to 10s, longer than the 4s
  socket-inactivity timeout, so a trickle-data sender is still bounded in
  wall-clock time instead of both firing at the same instant.
- cast-media: scope <video>/<audio> discovery via an explicit [data-cast-scope]
  attribute on the player hosts (web-player-view host + radio-hero) instead of
  hardcoded CSS class names, so a layout/class refactor cannot silently break
  Cast.
- workspace-shell: convert toCastPlayback() to a castPlayback computed so the
  OnPush cast-control receives a stable object reference instead of a freshly
  allocated one on every change-detection pass.

Verified: web pwa build emits index.html with gstatic; production build emits
index.html with script-src 'self'; ui-playback 137/137, workspace-shell-feature
102/102, dlna-renderer.service.spec green; electron-backend build clean.
The VOD/series details view embeds the trailer via
<iframe src="https://www.youtube.com/embed/{id}">. The hardened CSP's
`frame-src 'none'` blocked it (ERR_BLOCKED_BY_CSP), so trailers failed to load
in both the Electron renderer and the PWA. Allow the YouTube frame origins
(youtube.com + youtube-nocookie.com) in frame-src for the base and PWA index;
no other directive is loosened.
…lscreen

The cast overlay re-show was wired to host pointer events on .web-player-view.
That fails when a player captures pointer events (ArtPlayer — the control hides
after a few seconds and never returns) or owns the fullscreen element (Video.js
— it hides in fullscreen and doesn't reappear until exiting). Drive the re-show
from document-level capture-phase pointer/key activity instead, gated to the
player surface (host bounding box) or any active fullscreen, plus re-show on
fullscreenchange. Listeners run outside Angular (rAF-throttled) so idle movement
doesn't churn change detection; only a gated re-show re-enters the zone.

Verified: ui-playback 137/137 (web-player-view spec updated for the new
document-level activity path).
…ullscreen

Each player fullscreened its own element (Video.js .video-js, ArtPlayer, or the
native <video>), leaving the cast-control overlay — a sibling of the player in
.web-player-view — outside the fullscreen context. The HTML5 player showed no
cast icon in fullscreen at all (a native <video> cannot host overlay siblings),
and the others only showed it intermittently.

Make .web-player-view (which holds both the active player and the cast overlay)
the single fullscreen target:
- Add a fullscreen toggle to the cast overlay that calls requestFullscreen/
  exitFullscreen on the host; isFullscreen tracks document.fullscreenElement via
  the existing fullscreenchange listener and drives the icon and label.
- Disable each player's own fullscreen so this is the only entry point:
  Video.js controlBar.fullscreenToggle=false, HTML5 controlsList="nofullscreen",
  ArtPlayer fullscreen/fullscreenWeb=false.

The cast control now shares one fullscreen context across Video.js, HTML5, and
ArtPlayer. Esc still exits fullscreen natively.

Verified: ui-playback 138/138 (added a web-player-view fullscreen test), web
production build clean. Runtime fullscreen behavior needs a manual rebuild — it
cannot be exercised from jsdom unit tests.
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.

2 participants