Skip to content

feat(multiview): multiview grid for live TV - #1212

Open
SalemOurabi wants to merge 7 commits into
4gray:masterfrom
SalemOurabi:multiview-grid
Open

feat(multiview): multiview grid for live TV#1212
SalemOurabi wants to merge 7 commits into
4gray:masterfrom
SalemOurabi:multiview-grid

Conversation

@SalemOurabi

@SalemOurabi SalemOurabi commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Multiview page (/workspace/multiview, Electron and PWA) that plays several live TV channels simultaneously in a grid. This implements the "Multiview" part of the PiP/Multiview roadmap item.

  • Layout presets, switchable at runtime: 1×2, 2×2 (default), 1 large + 3 small, 3×3
  • All three sources: channels come from global favorites and recently viewed across M3U, Xtream and Stalker (UnifiedFavoritesDataService / UnifiedRecentDataService); playback URLs resolve through the existing StreamResolverService.resolvePlayback (so Stalker create_link and Xtream URL construction are reused, not duplicated)
  • Audio focus: exactly one tile has audio (highlighted border); click focuses a tile, double-click opens the channel in the regular full player via the existing global-collection handoff
  • Per-tile media engine: each tile runs a minimal standalone engine (MultiviewTileEngine: mpegts.js for raw TS, hls.js with small buffers, native <video> fallback). Deliberately not the full WebPlayerViewComponent stack, which is a per-app singleton (global keyboard shortcuts, shared volume persistence, single fullscreen/PiP)
  • Error handling: per-tile error state with retry (retry re-resolves the URL — important for expiring Xtream/Stalker tokens); a dismissible hint warns when two tiles use the same portal account (provider connection limits)
  • Persistence: layout, slots and audio focus survive reloads via localStorage (multiview-state-v1)
  • New rail entry in the workspace shell; live-only (radio/VOD/series excluded)

Implementation

  • New Nx library libs/workspace/multiview/feature (@iptvnator/workspace/multiview/feature), lazy-loaded route under /workspace
  • Layout presets are pure data (CSS grid templates; tiles bind grid-area), state is a component-provided signal service
  • i18n keys added to all 18 language files (MULTIVIEW.*, WORKSPACE.SHELL.RAIL_MULTIVIEW)
  • CLAUDE.md updated (routes, libs list, Key Features section)

Test plan

  • 58 unit tests in the new lib (layout presets, state service incl. persistence corruption, tile engine lifecycle, tile component, picker dialog, page orchestration incl. error/retry, dblclick handoff, connection-limit hint)
  • Workspace-shell tests (rail link) green
  • pnpm run lint and pnpm nx build web green
  • Manual PWA smoke test: grid renders, layout switching, picker dialog, persistence across reload

Known limitations (v1)

  • Most Xtream accounts allow only 1–2 concurrent streams; tiles from the same account may fail — surfaced via the connection-limit hint plus per-tile retry
  • EPG overlays on tiles are out of scope

@SalemOurabi

Copy link
Copy Markdown
Contributor Author

@greptileai Please review this pull request

@SalemOurabi SalemOurabi changed the title feat(multiview): TiviMate-style multiview grid for live TV feat(multiview): multiview grid for live TV Jul 19, 2026
@SalemOurabi

Copy link
Copy Markdown
Contributor Author

@greptileai Please review this pull request

@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a Multiview page (/workspace/multiview) that lets users watch up to nine live TV channels simultaneously in a CSS grid. The feature is available in both Electron and PWA builds and includes runtime layout switching, per-tile audio focus, a unified favorites/recent channel picker, and localStorage persistence.

  • New Nx library libs/workspace/multiview/feature: six new Angular components/services — page, tile, picker dialog, tile engine, layouts, and state service — each with unit tests (58 tests total).
  • Per-tile media engine (MultiviewTileEngine): mpegts.js for raw TS streams, hls.js with bounded buffers for HLS, native <video> fallback; muted by default with proper cleanup on destroy.
  • State management (MultiviewStateService): component-scoped, signal-based, persisted to localStorage with sanitized restoration that validates uid, name, playlistId, playlistName, contentType, and sourceType enum on every load.
  • Routing integration: lazy-loaded route at /workspace/multiview; multiview link added to the workspace rail via the shell route state service; 18 i18n files updated.

Confidence Score: 5/5

Safe to merge — the feature is entirely additive, isolated in its own Nx library, and guarded by lazy loading; it does not modify any existing playback or data-access paths.

All issues raised in prior review rounds have been addressed: the sanitizeSlot validation now covers contentType and sourceType enums; mpegts non-fatal errors are filtered; the picker double-open guard is in place; the request-ID counter is monotonically global; and the UID-split guard is present. No new functional defects were found in this pass.

No files require special attention. The new multiview library is self-contained and the only pre-existing file modifications (app.routes.ts, workspace-shell-route-state.service.ts) are minimal and clearly scoped.

Important Files Changed

Filename Overview
libs/workspace/multiview/feature/src/lib/multiview-page.component.ts Page orchestrator with signal-based resolution map and monotonic request IDs; picker guard, stale-request cancellation, and connection-limit hint all implemented correctly.
libs/workspace/multiview/feature/src/lib/multiview-tile-engine.ts Minimal per-tile engine selecting mpegts.js / hls.js / native fallback; mpegts non-fatal error filtering added via isNonFatalMpegtsErrorInfo; destroy guard prevents post-destroy callbacks.
libs/workspace/multiview/feature/src/lib/multiview-state.service.ts Component-scoped signal service with localStorage persistence; sanitizeSlot validates all required fields including contentType enum and sourceType enum; restore/persist lifecycle is correct.
libs/workspace/multiview/feature/src/lib/multiview-tile.component.ts Tile component with two isolated effects (playback/engine lifecycle and audio focus); destroyEngine called before creating new engine; host dblclick correctly propagates to page.
libs/workspace/multiview/feature/src/lib/multiview-channel-picker-dialog.component.ts Channel picker dialog with unified favorites/recent data loading; individual service failures handled gracefully; TranslateService injection removed (was flagged in prior review).
libs/workspace/multiview/feature/src/lib/multiview-layouts.ts Pure data file defining four CSS grid presets (1×2, 2×2, 1+3, 3×3); area names t0–t8 match the template's $index binding; CSS grid-template-areas strings are correctly quoted.
libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell-route-state.service.ts Multiview rail link added to workspaceLinks; railContext correctly returns null for the multiview route (no portal context needed); no guard required since multiview is available in both Electron and PWA.
apps/web/src/app/app.routes.ts New lazy-loaded route /workspace/multiview registered without a canActivate guard, consistent with the design intent of availability in both Electron and PWA contexts.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant Page as MultiviewPageComponent
    participant Dialog as ChannelPickerDialog
    participant State as MultiviewStateService
    participant Resolver as StreamResolverService
    participant Tile as MultiviewTileComponent
    participant Engine as MultiviewTileEngine

    User->>Page: click "Add Channel"
    Page->>Page: pickerOpen guard check
    Page->>Dialog: MatDialog.open()
    Dialog->>Dialog: loadItems() [UnifiedFavoritesDataService + UnifiedRecentDataService]
    User->>Dialog: select channel
    Dialog->>Page: afterClosed() → MultiviewSlotChannel
    Page->>State: assign(index, channel)
    State->>State: persist() → localStorage
    State->>Page: slots() signal change
    Page->>Page: syncResolutions(slots)
    Page->>Resolver: resolvePlayback(item)
    Resolver-->>Page: "{ streamUrl, userAgent, referer }"
    Page->>Page: "updateResolution(uid, {status:'ready', playback})"
    Page->>Tile: [playback] input binding
    Tile->>Engine: "new MultiviewTileEngine({ video, url, onError })"
    Engine->>Engine: start() — mpegts.js / hls.js / native fallback
    Engine-->>Tile: video plays (muted)
    User->>Tile: click (focusRequested)
    Tile->>Page: focusRequested.emit()
    Page->>State: focusAudio(index)
    State->>Tile: "audioFocused input = true"
    Tile->>Tile: "video.muted = false"
    User->>Tile: dblclick (openInPlayerRequested)
    Tile->>Page: openInPlayerRequested.emit()
    Page->>Page: openInPlayer(slot) → buildLiveCollectionNavigationTarget
    Page->>Page: router.navigate(target.link)
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 User
    participant Page as MultiviewPageComponent
    participant Dialog as ChannelPickerDialog
    participant State as MultiviewStateService
    participant Resolver as StreamResolverService
    participant Tile as MultiviewTileComponent
    participant Engine as MultiviewTileEngine

    User->>Page: click "Add Channel"
    Page->>Page: pickerOpen guard check
    Page->>Dialog: MatDialog.open()
    Dialog->>Dialog: loadItems() [UnifiedFavoritesDataService + UnifiedRecentDataService]
    User->>Dialog: select channel
    Dialog->>Page: afterClosed() → MultiviewSlotChannel
    Page->>State: assign(index, channel)
    State->>State: persist() → localStorage
    State->>Page: slots() signal change
    Page->>Page: syncResolutions(slots)
    Page->>Resolver: resolvePlayback(item)
    Resolver-->>Page: "{ streamUrl, userAgent, referer }"
    Page->>Page: "updateResolution(uid, {status:'ready', playback})"
    Page->>Tile: [playback] input binding
    Tile->>Engine: "new MultiviewTileEngine({ video, url, onError })"
    Engine->>Engine: start() — mpegts.js / hls.js / native fallback
    Engine-->>Tile: video plays (muted)
    User->>Tile: click (focusRequested)
    Tile->>Page: focusRequested.emit()
    Page->>State: focusAudio(index)
    State->>Tile: "audioFocused input = true"
    Tile->>Tile: "video.muted = false"
    User->>Tile: dblclick (openInPlayerRequested)
    Tile->>Page: openInPlayerRequested.emit()
    Page->>Page: openInPlayer(slot) → buildLiveCollectionNavigationTarget
    Page->>Page: router.navigate(target.link)
Loading

Reviews (8): Last reviewed commit: "merge: integrate latest upstream into mu..." | Re-trigger Greptile

Comment thread libs/workspace/multiview/feature/src/lib/multiview-state.service.ts
Comment thread libs/workspace/multiview/feature/src/lib/multiview-tile-engine.ts
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a TiviMate-style multiview grid page under /workspace/multiview as a new Nx library. Several live TV channels can be watched simultaneously in switchable layouts (1×2, 2×2, 1+3 focus, 3×3), with audio focus on a single tile, double-click handoff to the full player, per-tile error/retry, and localStorage persistence.

  • New libs/workspace/multiview/feature library: MultiviewStateService (signal-based state with localStorage round-trip), MultiviewTileEngine (mpegts.js / hls.js / native video path, parallel to the existing HTML5 player but without global singletons), MultiviewTileComponent (audio focus through muted property), MultiviewPageComponent (URL resolution, layout switching, connection-limit hint), and MultiviewChannelPickerDialogComponent (favorites + recent cross-source filter).
  • Route and shell integration: lazy-loaded route at /workspace/multiview added to app.routes.ts; rail entry added to WorkspaceShellRouteStateService.
  • i18n: MULTIVIEW.* keys added to all 18 language files; WORKSPACE.SHELL.RAIL_MULTIVIEW added to the shell rail.

Confidence Score: 3/5

The feature works correctly for HLS and native streams; the main risk is that TS-stream tiles in degraded network conditions will show spurious error overlays requiring manual retry, and rapid double-clicking the empty-slot button can open two overlapping dialogs.

The mpegts error-forwarding path discards the fatal/non-fatal distinction that the HLS path preserves, meaning any transient mpegts.js event — including ones the player would have self-recovered from — immediately tears down the engine and presents an error overlay. For users on TS-heavy playlists with variable network quality this will be a noticeable reliability gap. The double-dialog issue on the add button is a secondary usability defect. Both are straightforward to fix, but until they are the feature has rough edges that could frustrate users of TS streams.

multiview-tile-engine.ts (mpegts error handling) and multiview-page.component.html (add-button double-click guard)

Important Files Changed

Filename Overview
libs/workspace/multiview/feature/src/lib/multiview-tile-engine.ts New standalone media engine for multiview tiles. Mirrors the existing HTML5 player's mpegts/hls/native selection logic, but mpegts errors are not filtered by severity (unlike HLS which skips non-fatal events), risking spurious error states for TS streams.
libs/workspace/multiview/feature/src/lib/multiview-page.component.ts Page orchestrator: URL resolution with request-id-based stale-response guard, connection-limit hint, and navigation handoff. Logic is sound; the double-click issue is in the template, not here.
libs/workspace/multiview/feature/src/lib/multiview-page.component.html Layout template wiring state signals to tile and add-button slots. The 'Add Channel' button uses only (click), which fires twice on a double-click and can open two concurrent picker dialogs.
libs/workspace/multiview/feature/src/lib/multiview-state.service.ts Signal-based state with thorough localStorage round-trip: sanitizes restored slots, validates layout IDs, clamps audio focus index, and gracefully falls back to defaults on parse errors.
libs/workspace/multiview/feature/src/lib/multiview-tile.component.ts Per-tile component managing engine lifecycle via effects. Audio focus is handled correctly via untracked reads; DestroyRef cleanup is wired up.
libs/workspace/multiview/feature/src/lib/multiview-channel-picker-dialog.component.ts Cross-source channel picker with favorites/recent tabs and text search. Error handling via .catch(() => []) ensures loading always completes. The TranslateService injection is public but unused.
libs/workspace/multiview/feature/src/lib/multiview-layouts.ts Pure data module defining four CSS-grid layout presets. Type-safe guard and lookup helpers are straightforward and well-tested.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant PageComponent as MultiviewPageComponent
    participant StateService as MultiviewStateService
    participant Picker as ChannelPickerDialog
    participant Resolver as StreamResolverService
    participant TileComponent as MultiviewTileComponent
    participant TileEngine as MultiviewTileEngine

    User->>PageComponent: click Add Channel
    PageComponent->>Picker: dialog.open()
    Picker-->>PageComponent: result (MultiviewSlotChannel)
    PageComponent->>StateService: assign(index, channel)
    StateService-->>StateService: persist to localStorage
    StateService-->>PageComponent: slots() signal updated

    PageComponent->>Resolver: resolvePlayback(item)
    Resolver-->>PageComponent: streamUrl, title
    PageComponent->>TileComponent: [playback] input set

    TileComponent->>TileEngine: new MultiviewTileEngine
    TileEngine->>TileEngine: start() — mpegts / hls.js / native

    User->>TileComponent: click (focus audio)
    TileComponent->>PageComponent: focusRequested
    PageComponent->>StateService: focusAudio(index)

    User->>TileComponent: dblclick (open in player)
    TileComponent->>PageComponent: openInPlayerRequested
    PageComponent->>User: router.navigate (full player)

    alt Playback error
        TileEngine->>TileComponent: onError(diagnostic)
        TileComponent->>PageComponent: playbackFailed
        PageComponent->>PageComponent: "updateResolution status=error"
        User->>PageComponent: retry
        PageComponent->>Resolver: resolvePlayback re-resolves URL
    end
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 User
    participant PageComponent as MultiviewPageComponent
    participant StateService as MultiviewStateService
    participant Picker as ChannelPickerDialog
    participant Resolver as StreamResolverService
    participant TileComponent as MultiviewTileComponent
    participant TileEngine as MultiviewTileEngine

    User->>PageComponent: click Add Channel
    PageComponent->>Picker: dialog.open()
    Picker-->>PageComponent: result (MultiviewSlotChannel)
    PageComponent->>StateService: assign(index, channel)
    StateService-->>StateService: persist to localStorage
    StateService-->>PageComponent: slots() signal updated

    PageComponent->>Resolver: resolvePlayback(item)
    Resolver-->>PageComponent: streamUrl, title
    PageComponent->>TileComponent: [playback] input set

    TileComponent->>TileEngine: new MultiviewTileEngine
    TileEngine->>TileEngine: start() — mpegts / hls.js / native

    User->>TileComponent: click (focus audio)
    TileComponent->>PageComponent: focusRequested
    PageComponent->>StateService: focusAudio(index)

    User->>TileComponent: dblclick (open in player)
    TileComponent->>PageComponent: openInPlayerRequested
    PageComponent->>User: router.navigate (full player)

    alt Playback error
        TileEngine->>TileComponent: onError(diagnostic)
        TileComponent->>PageComponent: playbackFailed
        PageComponent->>PageComponent: "updateResolution status=error"
        User->>PageComponent: retry
        PageComponent->>Resolver: resolvePlayback re-resolves URL
    end
Loading
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/workspace/multiview/feature/src/lib/multiview-tile-engine.ts:110-127
**mpegts errors are not filtered by severity unlike the HLS path**

The HLS error handler guards with `if (!data.fatal) { return; }` so only fatal errors surface. The mpegts handler forwards every `mpegts.Events.ERROR` event unconditionally. mpegts.js can fire non-fatal ERROR events for transient network blips (e.g., temporary packet loss), which will immediately set the tile to the error state and destroy the engine — requiring a manual retry even for issues the player would have recovered from on its own. TS-stream tiles in poor-network environments will be far less resilient than HLS tiles.

### Issue 2 of 3
libs/workspace/multiview/feature/src/lib/multiview-page.component.html:57-65
**Double-click on "Add Channel" opens two concurrent picker dialogs**

`(click)="openPicker($index)"` fires twice during a native double-click (browsers emit `click``click``dblclick`). Each click triggers a separate `MatDialog.open()` call synchronously before the first dialog is even rendered, so the user ends up with two overlapping channel-picker dialogs. The second call could silently overwrite any channel selected in the first. Adding a guard like checking whether a dialog is already open for the same slot, or using `(click)` with a debounce, would prevent this.

### Issue 3 of 3
libs/workspace/multiview/feature/src/lib/multiview-channel-picker-dialog.component.ts:70
**Unused public injection of `TranslateService`**

`translate` is injected and exposed as a public field, but the template exclusively uses the `TranslatePipe` (`| translate`) and none of the component's methods reference `this.translate`. If it was intended for direct template use (e.g., `translate.instant(...)`) it is not currently wired up; if it was left over from an earlier implementation it can be removed to keep the component surface clean.

Reviews (2): Last reviewed commit: "chore(multiview): address lint findings ..." | Re-trigger Greptile

Comment thread libs/workspace/multiview/feature/src/lib/multiview-tile-engine.ts
@greptile-apps

greptile-apps Bot commented Jul 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a new MultiviewPageComponent under /workspace/multiview that lets users watch up to nine live TV channels simultaneously in a switchable CSS-grid layout. State (layout, slots, audio focus) is persisted to localStorage and restored on reload; playback uses a lean per-tile engine (mpegts.js / hls.js / native) rather than the full singleton player stack.

  • New Nx library libs/workspace/multiview/feature with lazy-loaded route, four layout presets, signal-based state service, per-tile engine, channel picker dialog, and 58 unit tests.
  • Rail nav entry added to the workspace shell; i18n keys added to all 18 language files.
  • Two correctness issues in MultiviewPageComponent: a request-ID counter reset that allows a stale URL resolution to overwrite a fresh one when the same channel is quickly removed then re-assigned, and an unguarded uid.split('::')[2] that can produce undefined as itemId on double-click navigation.

Confidence Score: 3/5

Mostly solid new feature, but the resolve-orchestration layer in MultiviewPageComponent has a counter-reset bug that can leave a tile playing a stale (or failed) stream URL after a quick remove-and-reassign cycle.

The state service, tile engine, layouts, and picker dialog are all well-structured and well-tested. The main concern is in MultiviewPageComponent: the requestIds map is cleared on slot removal, so immediately re-assigning the same channel resets the counter to 1, matching the still-inflight original request's ID. A stale error or stale (expiring) Xtream/Stalker token URL could overwrite the fresh resolve result depending on timing — a realistic scenario under slow or flaky network conditions. A second gap is the unguarded UID split used for full-player navigation, which silently passes undefined as itemId if the stored UID is malformed.

libs/workspace/multiview/feature/src/lib/multiview-page.component.ts — the request-ID counter reset in syncResolutions and the uid.split guard in openInPlayer both need attention before merging.

Important Files Changed

Filename Overview
libs/workspace/multiview/feature/src/lib/multiview-page.component.ts Orchestrates grid state, URL resolution, and player handoff. Contains a request-ID counter reset bug (when a channel is removed and immediately re-added, stale and fresh resolve calls share the same ID) and an unguarded UID split that can produce undefined itemId on navigation.
libs/workspace/multiview/feature/src/lib/multiview-state.service.ts Signal-based state service with localStorage persistence. Includes robust slot sanitization, audio focus invariant enforcement, and layout resize logic. Well-tested with 14 unit tests covering corruption and edge cases.
libs/workspace/multiview/feature/src/lib/multiview-tile-engine.ts Minimal per-tile engine (mpegts.js / hls.js / native fallback) with correct destroy lifecycle. Fatal HLS errors and mpegts errors are classified and forwarded; non-fatal HLS errors are intentionally suppressed.
libs/workspace/multiview/feature/src/lib/multiview-tile.component.ts Tile UI with engine lifecycle tied to the playback signal. Audio muting is managed by a dedicated effect; engine restart on playback change correctly preserves audio focus via untracked. Remove-button click is properly stopped from propagating to the host listener.
libs/workspace/multiview/feature/src/lib/multiview-channel-picker-dialog.component.ts Channel picker dialog filtering favorites and recent items (live TV only) with a text search. Errors from data services are silently caught and show an empty list, which is acceptable for a best-effort picker.
libs/workspace/multiview/feature/src/lib/multiview-layouts.ts Pure data module defining four CSS grid layout presets. All layout IDs, capacities, and grid templates are consistent and fully tested.
libs/workspace/multiview/feature/src/lib/multiview-page.component.html Template correctly guards tile rendering with @if (slot) and binds grid-area per index. Minor: playbackFailed binding drops $event, discarding the PlaybackDiagnostic.
apps/web/src/app/app.routes.ts Adds the /workspace/multiview lazy-loaded route at the expected position in the workspace child routes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant MultiviewPage as MultiviewPageComponent
    participant StateService as MultiviewStateService
    participant Resolver as StreamResolverService
    participant TileEngine as MultiviewTileEngine
    participant Player as Full Player (Router)

    User->>MultiviewPage: open picker (slot N)
    MultiviewPage->>MultiviewPage: open ChannelPickerDialog
    User->>MultiviewPage: select channel
    MultiviewPage->>StateService: assign(index, channel)
    StateService-->>MultiviewPage: slots() signal updated
    MultiviewPage->>Resolver: resolvePlayback(item)
    Resolver-->>MultiviewPage: streamUrl, userAgent, referer
    MultiviewPage->>MultiviewPage: updateResolution(uid, ready)
    MultiviewPage->>TileEngine: new MultiviewTileEngine
    TileEngine->>TileEngine: start() mpegts/hls/native branch
    TileEngine-->>MultiviewPage: onError(diagnostic) on fatal error
    User->>MultiviewPage: click tile
    MultiviewPage->>StateService: focusAudio(index)
    StateService-->>MultiviewPage: audioFocusIndex() updated
    MultiviewPage-->>TileEngine: "audioFocused=true, video.muted=false"
    User->>MultiviewPage: double-click tile
    MultiviewPage->>Player: router.navigate
    User->>MultiviewPage: retry error state
    MultiviewPage->>Resolver: resolvePlayback re-resolves URL
    Resolver-->>MultiviewPage: fresh streamUrl
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 User
    participant MultiviewPage as MultiviewPageComponent
    participant StateService as MultiviewStateService
    participant Resolver as StreamResolverService
    participant TileEngine as MultiviewTileEngine
    participant Player as Full Player (Router)

    User->>MultiviewPage: open picker (slot N)
    MultiviewPage->>MultiviewPage: open ChannelPickerDialog
    User->>MultiviewPage: select channel
    MultiviewPage->>StateService: assign(index, channel)
    StateService-->>MultiviewPage: slots() signal updated
    MultiviewPage->>Resolver: resolvePlayback(item)
    Resolver-->>MultiviewPage: streamUrl, userAgent, referer
    MultiviewPage->>MultiviewPage: updateResolution(uid, ready)
    MultiviewPage->>TileEngine: new MultiviewTileEngine
    TileEngine->>TileEngine: start() mpegts/hls/native branch
    TileEngine-->>MultiviewPage: onError(diagnostic) on fatal error
    User->>MultiviewPage: click tile
    MultiviewPage->>StateService: focusAudio(index)
    StateService-->>MultiviewPage: audioFocusIndex() updated
    MultiviewPage-->>TileEngine: "audioFocused=true, video.muted=false"
    User->>MultiviewPage: double-click tile
    MultiviewPage->>Player: router.navigate
    User->>MultiviewPage: retry error state
    MultiviewPage->>Resolver: resolvePlayback re-resolves URL
    Resolver-->>MultiviewPage: fresh streamUrl
Loading
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/workspace/multiview/feature/src/lib/multiview-page.component.ts:152-169
**Request-ID counter resets on slot removal, enabling stale writes**

When a channel is removed, `requestIds.delete(uid)` resets the counter to zero. If the same channel is immediately re-assigned, the new `resolveItem` call computes `requestId = (undefined ?? 0) + 1 = 1` — the same value as the original (now-stale) inflight request. Both calls pass the `this.requestIds.get(item.uid) !== requestId` guard (`1 !== 1` is false for both), so whichever completes last wins. If the stale call fails after the fresh call already marked the tile as `ready`, the tile reverts to `error` state; for Xtream/Stalker where each `resolvePlayback` mints a new expiring token, the stale URL (written last) would be used. Preserving the high-water-mark counter even through removal — e.g. storing a large monotonic value per UID instead of deleting the entry — prevents the collision.

### Issue 2 of 3
libs/workspace/multiview/feature/src/lib/multiview-page.component.ts:159-168
`item.uid.split('::')[2]` is `undefined` when the UID has fewer than three `::` segments. `sanitizeSlot` validates that `uid` is a string but not its format, so a persisted or injected UID like `"m3u::playlist-1"` (missing the item segment) would produce `undefined` as `itemId`. Depending on how `buildLiveCollectionNavigationTarget` handles that, navigation may silently route to a wrong or broken path. An early guard prevents this.

```suggestion
    openInPlayer(slot: MultiviewSlotChannel): void {
        const item = slot.item;
        const parts = item.uid.split('::');
        const itemId = parts[2];
        if (!itemId) {
            return;
        }
        const target = buildLiveCollectionNavigationTarget({
            mode: slot.origin,
            sourceType: item.sourceType,
            playlistId: item.playlistId,
            itemId,
            title: item.name,
            imageUrl: item.logo,
        });
```

### Issue 3 of 3
libs/workspace/multiview/feature/src/lib/multiview-page.component.html:58
The `playbackFailed` output emits a `PlaybackDiagnostic` but the binding omits `$event`, so the diagnostic is silently dropped. The component method `onTileFailed` currently ignores it entirely, which means there is no way to log or telemetry the actual failure reason. Even if the current error message is intentionally generic, passing the diagnostic through keeps the door open for future observability without a template change.

```suggestion
                    (playbackFailed)="onTileFailed(slot, $event)"
```

Reviews (3): Last reviewed commit: "chore(multiview): address lint findings ..." | Re-trigger Greptile

Comment thread libs/workspace/multiview/feature/src/lib/multiview-page.component.html Outdated
@SalemOurabi

Copy link
Copy Markdown
Contributor Author

@greptileai Please review this pull request

@codecov-commenter

codecov-commenter commented Jul 19, 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 95.46539% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.61%. Comparing base (e91cbd5) to head (bc7fc4e).
⚠️ Report is 2020 commits behind head on master.

Files with missing lines Patch % Lines
...tiview/feature/src/lib/multiview-page.component.ts 94.95% 3 Missing and 3 partials ⚠️
...ltiview/feature/src/lib/multiview-state.service.ts 94.73% 2 Missing and 3 partials ⚠️
apps/web/src/app/app.routes.ts 0.00% 3 Missing ⚠️
...tiview/feature/src/lib/multiview-tile.component.ts 95.08% 2 Missing and 1 partial ⚠️
...c/lib/multiview-channel-picker-dialog.component.ts 95.34% 2 Missing ⚠️
❗ 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 (bc7fc4e). Click for more details.

HEAD has 4 uploads less than BASE
Flag BASE (e91cbd5) HEAD (bc7fc4e)
4 0
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1212      +/-   ##
==========================================
- Coverage   71.05%   63.61%   -7.44%     
==========================================
  Files          40      680     +640     
  Lines         691    40017   +39326     
  Branches       87     8723    +8636     
==========================================
+ Hits          491    25458   +24967     
- Misses        176    11325   +11149     
- Partials       24     3234    +3210     
Flag Coverage Δ
unit 63.61% <95.46%> (?)

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.

…pegts errors

Persisted multiview slots now require contentType 'live', a known
sourceType enum value, and a playlistName string before restore, and the
mpegts.js error handler mirrors the hls.js fatal guard so explicitly
non-fatal payloads no longer put a tile into the error state.
@SalemOurabi

Copy link
Copy Markdown
Contributor Author

@greptileai Please review this pull request

@SalemOurabi

Copy link
Copy Markdown
Contributor Author

@greptileai Please review this pull request

- drop in-flight stream resolutions on destroy and never start new ones
  after navigating away, so no ephemeral portal sessions are created
- make request ids monotonic across slots so a removed-and-re-added
  channel cannot collide with a stale in-flight request
- track connection-limit hint dismissal per portal-account combination;
  a new same-account conflict after dismissal shows the hint again
- guard open-in-player against malformed persisted uids
- prevent double-click from opening two channel-picker dialogs
- pass the tile playback diagnostic through and log it for observability
- remove unused TranslateService injection from the picker dialog
@SalemOurabi

Copy link
Copy Markdown
Contributor Author

@greptileai Please review this pull request

@SalemOurabi

Copy link
Copy Markdown
Contributor Author

@greptileai Please review this pull request

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