Swap M3 primary palette from violet to azure so checkboxes, radio
buttons, raised CTAs, and active states read as the same blue used by
the rail selection token. Cascading template + SCSS updates align the
remaining hand-rolled surfaces (Add Playlist dialog, VOD play button,
radio player, multi-EPG, empty-state CTAs) with the unified system.
LIVE stays red (broadcast role), cyan stays on EPG "now" indicator,
green stays on completed-download — semantic colors keep their meaning;
only the indiscriminate accent uses get folded into the blue primary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(playback): handle IPTV proxy URLs without file extensions
IPTV proxy servers (Acexy, Xtream Codes, etc.) use query-string URLs
like `https://proxy.example.com/ace/getstream?infohash=abc` that return
raw MPEG-TS streams without a file extension in the path.
`getExtensionFromUrl()` was returning garbage for these URLs (e.g.
`com/ace/getstream` from splitting on dots across path segments). All
four player backends then misrouted the stream — typically to HLS.js
which failed because the response is raw MPEG-TS, not an HLS manifest.
Root fix: rewrite `getExtensionFromUrl()` to extract the extension from
the last path segment only, returning `undefined` when there is no
dot-separated extension.
Player adjustments: treat `undefined` extension as MPEG-TS (the most
common format for live IPTV proxy streams) across all four backends
(HTML5, ArtPlayer, VideoJs, web-player-view).
* test(m3u-utils): cover URL extension parsing
* fix(playback): preserve query-declared stream extensions
---------
Co-authored-by: 4gray <serega05@gmail.com>
* fix(playback): avoid redundant VideoJS source resets
* fix(playback): clean up cleared VideoJS sources
* refactor(playback): use signal APIs in VideoJS player
closes#608
Audit followup to the support-loop and volume-restart fixes. Two more
effects pulled in transitive signal deps that would have caused the same
class of regression the next time the surrounding helpers grew.
1. Component session-fan-out effect: the body called
scheduleControlsHide(), which reads isPlaying/menus.anyOpen/statusLabel/
controlsVisible. Those became tracked deps of the effect, so opening a
popover, pausing, or hovering re-ran the whole body — re-emitting
timeUpdate. If a parent ever wires timeUpdate back into
playback.startTime as a "resume where I left off" feature, this would
have been the next stream-restart bug. Wrap the side-effect block in
untracked() so the effect only listens to session changes.
2. Controller stalled-tracker effect: tracked the full session signal
even though only status was needed. The session payload updates ~2 Hz
during playback (positionSeconds advances), making the effect re-run
constantly to call a no-op. Add a sessionStatus computed and track
that instead — fires only on real status transitions.
No behavior change for current users; both fixes are preventative.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
Same root cause as the loading-loop fix: the session-creation effect
read this.volume() while building startSession's initialVolume argument,
which made volume a tracked dependency of the effect. Each volume tick
re-ran the effect, the cleanup disposed the active session, and a fresh
one was created — which for VOD/series meant restarting playback from
the beginning.
Read volume via untracked() inside the effect. The value is only needed
once at session creation; subsequent volume changes flow through
controller.applyVolume() and never go near the effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
Regression introduced in the bundle 3 refactor. After calling
prepareEmbeddedMpv, the controller wrote the response back into
this.support — but the component's session-creation effect tracks
this.support(), so updating it cleaned up the just-created session and
ran startSession again, which prepared again, set support again, and so
on. Net effect: endless "Loading stream…" spinner because every session
was disposed before MPV could finish loading the file.
The initial loadSupport() in the constructor already populates support
(including capabilities). Drop the redundant set in startSession; leave
a comment explaining why so it does not get re-added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
The timeline and volume sliders were rendering invisibly because:
- --embedded-mpv-accent resolved to var(--mat-sys-primary) with no
fallback. If the Material 3 theme tokens did not cascade into the
component scope, the variable was empty and both the thumb and the
Firefox progress fill rendered with no color.
- The track contrast was 22% of var(--mat-sys-on-surface) which has the
same scoping fragility on top of being subtle even when it does
resolve.
- The thumb had no border, so even when accent was present it could
blend into similarly-toned popover backgrounds.
- The played portion of the timeline had no fill in Chromium because
::-webkit-slider-runnable-track does not support a native progress
state.
Fixes:
- Add hard fallbacks on every theme-token reference (accent falls back to
#7dd3fc, surface to #0f1620, outline-variant to a translucent white).
- Add --embedded-mpv-track and --embedded-mpv-thumb-ring as theme-
independent CSS variables so the slider stays legible regardless of
the parent theme provider.
- Give the thumb a 2 px white ring border + a stronger drop shadow so it
reads on any background (panel glass, dark backdrop, accent-coloured
area).
- Implement played-progress fill via a linear-gradient on
::-webkit-slider-runnable-track keyed off a CSS variable
--slider-progress, set per-slider from the template based on the
current playback position / volume. Firefox uses native
::-moz-range-progress.
- Tidy disabled state styling so live-stream timelines (no duration)
are dim but still visible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
The renderer was reading session.id and forwarding it to the addon, but
during the loading window that id is the placeholder
"embedded-mpv-starting" set by createLoadingSession. If the user adjusted
volume, seeked, or toggled audio/subtitle/speed/aspect before the addon's
createSession returned the real id, that placeholder id reached the
addon — and the addon's getSessionOrThrow threw a raw std::runtime_error
which libc++abi terminated the process on.
Two fixes, defense in depth:
1. Renderer (session controller): use the canonical sessionId() signal,
which is null until the addon hands back a real id, as the gate for all
IPC calls. Wrap every IPC call in a guardIpc helper that swallows
addon-side throws so a torn-down session or race won't surface as an
uncaught promise rejection.
2. Native (embedded_mpv.mm): change getSessionOrThrow to take a
Napi::Env and throw Napi::Error::New(env, ...) instead of
std::runtime_error. node-addon-api converts Napi::Error to a JS
exception cleanly; the previous std::runtime_error escaped the C++
frame and aborted the process when the addon was built without
NAPI_CPP_EXCEPTIONS translation. Refactor splits findSession (returns
nullptr) from getSessionOrThrow (env-aware) so call paths that just
probe a session's existence don't pay the throw cost.
The native fix needs an addon rebuild to take effect; the renderer fix
prevents the crash trigger immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
Same root cause as the modal-dialog occlusion: control popovers (volume,
audio, subtitle, speed, aspect) extend upward from the controls strip into
the area covered by the MPV NSView. They render in DOM but the native view
paints over them.
Replace the simple boolean overlayActiveProvider with a richer
boundsProvider closure on the session controller. The component drives it
from both the modal overlay state and the popover menu state:
- Modal dialog open (command palette, MatDialog) -> HIDDEN_BOUNDS, MPV
fully off-screen so the dialog has the whole window.
- Popover menu open -> shrink MPV from the bottom by 300 px so the popover
region lives in DOM-receiving space; video keeps playing in the upper
region instead of disappearing entirely.
- Otherwise -> full host bounds.
Bounds-resync effect now tracks menus.anyOpen() in addition to
overlayActive() so opening or closing a popover triggers an immediate
re-sync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
The component had grown to ~1000 lines with session lifecycle, IPC plumbing,
keyboard shortcuts, popover state, formatters, and view orchestration all
living in one file. Per CLAUDE.md's 350–400 line hard cap, split into:
- embedded-mpv-format.utils.ts (~100): pure helpers (formatTime, track
labels, volume icon/label, persisted-volume access, measureBounds) and
preset constants (SPEED_PRESETS, ASPECT_PRESETS, HIDDEN_BOUNDS).
- embedded-mpv-shortcuts.ts (~90): EmbeddedMpvShortcuts class that owns the
document keydown listener and routes through a handler interface; the
component just provides callbacks.
- embedded-mpv-ui-state.ts (~110): EmbeddedMpvMenuState (single-open
popover state machine with toggle/open/close/closeAll + anyOpen signal)
and EmbeddedMpvFeedback (transient overlay with auto-clearing flash).
- embedded-mpv-session-controller.ts (~395): component-scoped Injectable
that owns support/session/sessionId/stalled/retryToken signals, the
session-update IPC subscription, bounds-sync (resize, scroll, overlay
state), the stalled timer, and all per-session IPC operations
(togglePaused, seekBy, seekTo, applyVolume, setAudioTrack,
setSubtitleTrack, setSpeed, setAspect, retry).
- embedded-mpv-player.component.ts (now ~540, was ~1000): view-only
orchestration — view children, derived computed signals, DOM event
listeners (pointerdown/pointermove/fullscreenchange/dblclick), and three
effects (session start/teardown, overlay-active bounds sync, session
payload → volume/timeUpdate fan-out).
No behavior changes. Build clean, electron-backend tests still pass.
Component is still over the 400-line cap but the bulk of its size is now
the necessarily-coupled-to-view computed signals and constructor effects;
the remaining over-cap delta is structural to a player root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
Native addon (apps/electron-backend/native/src/embedded_mpv.mm)
- Extend SessionSnapshot with subtitleTracks, selectedSubtitleTrackId,
playbackSpeed, aspectOverride.
- Refactor track parsing into a shared updateTracksFromNode helper that
filters by mpv "type" so audio and subtitle tracks share the code path.
- Observe sid, speed, video-aspect-override; clear sub state on
MPV_EVENT_START_FILE.
- Export setSubtitleTrack (handles trackId === -1 as "no" to disable),
setSpeed (clamped to 0.25–4.0), setAspect (passthrough string for
video-aspect-override).
- Snapshot output now includes the new fields.
Service (embedded-mpv-native.service.ts) + IPC + preload
- Mirror methods on EmbeddedMpvNativeService with capability detection: each
method throws a descriptive error if the loaded addon doesn't expose the
underlying native function (i.e. user is running an older build).
- New IPC channels EMBEDDED_MPV_SET_SUBTITLE_TRACK, _SET_SPEED, _SET_ASPECT
registered in events file and exposed via preload.
- Extend EmbeddedMpvSupport with a capabilities probe so the renderer can
hide controls for features the current addon build doesn't ship.
Renderer (embedded-mpv-player.component.{ts,html})
- Three new popovers anchored above their buttons (subtitle / speed /
aspect), gated by capabilities() and (for subtitles) by track count.
- Subtitle popover includes an Off entry; speed/aspect use fixed presets.
- Error state now surfaces the same overlay as the stalled state, with a
Retry button that bumps the existing retryNonce signal — covers #8 from
the audit.
- All session-payload defaults (loading stub, error stub, refresh fallback,
dispose payload, native createSession default) updated for the new
required fields.
NOTE: Existing addon binaries do not expose the new methods. Until the
addon is rebuilt (pnpm run serve:backend:embedded-mpv or the release
build script), capabilities will report subtitles/playbackSpeed/
aspectOverride as false and the new buttons will simply not appear.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
- Volume button now toggles mute on click; the slider opens via hover/focus
in an absolute popover anchored above the button (with hover-bridge so the
cursor can reach it without crossing a dead zone). Wheel over the volume
area adjusts volume.
- Audio track menu likewise becomes a click-toggle popover anchored to its
button instead of a panel takeover; the back-arrow / mode-panel
scaffolding is gone.
- Slider gets explicit thumb/track styling for both WebKit and Firefox so
it reads as part of the design system instead of a raw native control;
aria-valuetext on the timeline announces formatted time.
- Stalled overlay: if status remains "loading" for 30 s, surface a centered
warning with a Retry button that disposes the session and recreates it
via a retryNonce signal the playback effect tracks.
- Keypress feedback overlay: ←/→ seek, ↑/↓ volume, M mute now flash a
centered pill with the icon + delta so the action is visible (especially
important in fullscreen where the controls are hidden).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
Add the standard set of player shortcuts to match the audio/HTML video
players already in the app:
- Space, K — play/pause
- F — toggle fullscreen
- Left/Right — seek -5 s / +5 s
- Up/Down — volume +/- 5 %
- M — toggle mute (restores prior volume)
Shortcuts are ignored while focus is in an input/select/textarea or while
any MatDialog (e.g. command palette) is open, so the palette's filter and
dialog inputs keep their keys. Volume changes route through a shared
applyVolume helper so keyboard/wheel/slider all persist to localStorage
and the active session.
Also bind dblclick on the player root to toggle fullscreen, with a guard
so double-clicks on buttons/sliders don't trigger it. Note: the native
MPV NSView intercepts mouse events over the video itself, so dblclick
only fires on DOM-receiving regions (currently the controls strip).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
The command palette is a MatDialog with a transparent backdrop, and the
prior MutationObserver-only detector did not reliably catch it across
overlay-container instantiation timing — the player kept rendering at full
bounds while the palette was open and continued occluding it. Subscribe
directly to MatDialog.afterOpened / afterAllClosed for the authoritative
"any modal dialog open" signal, and keep the OverlayContainer mutation
observer as a fallback for non-dialog CDK overlays. Either signal flips
overlayActive, hiding the embedded MPV view as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
The native MPV NSView sits above the WebContents and is sized to the
viewport rect, so any DOM region it covers never receives pointer events.
Bundle 1 made the viewport fill the whole player, leaving no DOM strip to
catch pointermove — once the controls faded out there was no way to bring
them back via hover. Reapply the bottom inset on the viewport, scoped to
the slimmer 64 px panel, so the bottom strip is DOM-only and hover wakes
the controls. Swap the gradient for the glass surface treatment now that
the panel no longer needs to fade into video.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
- Switch palette and surfaces to Material 3 tokens (--mat-sys-*) so the
player respects the app theme instead of a hardcoded cyan/sky scheme.
- Stop reserving 96 px below the viewport for the control panel; the panel
now floats over the bottom with a gradient fade so the video fills the
frame (especially in fullscreen).
- Drop control panel height from 96 to 64 px (88 px on narrow widths).
- Replace the corner "Loading stream in MPV..." pill with a centered
spinner + concise label during load; move the transient status toast
to the top-right and add role="status" aria-live.
- Add a red badge with pulsing dot for live streams in place of the plain
"LIVE" text label.
- Apply font-variant-numeric: tabular-nums to time/volume readouts so
digits stop jittering as playback advances.
- Slightly loosen transport/actions gaps for a less cramped rhythm and
use ease-out easing on control fade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
The embedded MPV player is a native NSView that sits above the WebContents
layer, so DOM dialogs (command palette, MatDialog, etc.) always paint behind
it regardless of z-index. Track CDK overlay backdrops via OverlayContainer +
MutationObserver and send off-screen bounds to the native view while any
backdrop is present, restoring real bounds on close. Playback continues
uninterrupted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
- Introduced tooling for building and staging the macOS `libmpv` runtime for IPTVnator's embedded MPV player.
- Added `build-macos-runtime.mjs` for building an LGPL-compatible runtime from source.
- Created `stage-macos-runtime.mjs` for staging the built runtime artifacts.
- Implemented validation for the packaged embedded MPV runtime in `electron-after-pack.cjs` and `embedded-mpv-macos.cjs`.
- Updated packaging scripts to ensure the embedded MPV runtime is correctly integrated and validated during the build process.
- Added README files to document the expected layout and usage for the embedded MPV runtime artifacts.
Entire-Checkpoint: c6e522b4276c
- Implemented a loading state for channel lists in various components, enhancing user experience during data fetching.
- Added `ChannelListLoadingStateComponent` to display loading skeletons for channels and groups.
- Updated `M3uWorkspaceRouteSession` to manage loading states more effectively when fetching playlists.
- Modified `VideoPlayerComponent`, `SidebarComponent`, and `ChannelListContainerComponent` to utilize the new loading state.
- Enhanced tests to cover new loading behaviors and ensure proper dispatching of loading actions.
- Updated video player component to conditionally render audio player for radio channels.
- Added radio-specific styles to video player and unified live tab components.
- Introduced new layout classes for radio and external players.
- Enhanced audio player component with improved UI and volume controls.
- Implemented logic to preserve radio flag in M3U playback details.
- Updated tests to cover new radio functionality and ensure proper rendering.
- Updated PlaylistErrorViewComponent to use PlaylistContextFacade for accessing the current playlist.
- Refactored StalkerSearchComponent to utilize PlaylistContextFacade for retrieving the active playlist.
- Modified StalkerWorkspaceRouteSession to sync route context using PlaylistContextFacade.
- Adjusted XtreamStore to remove NgRx store dependencies and use PlaylistContextFacade for current playlist access.
- Updated ChannelListContainerComponent to resolve playlist ID using PlaylistContextFacade.
- Refactored SidebarComponent and ToolbarComponent to eliminate NgRx store usage for playlist-related data.
- Enhanced unit tests in with-content.feature to ensure proper loading and cancellation of content imports.
- Introduced M3uRecentlyViewedItem interface to define structure for recently viewed items.
- Implemented utility functions to handle mapping and building of recently viewed items from playlists.
- Updated Playlist interface to use PlaylistRecentlyViewedItem type for recently viewed items.
- Enhanced PortalActivityItem interface to include 'm3u' as a possible source.
- Added UI components for displaying recently viewed channels, including clear functionality.
- Integrated recent view functionality into the channel list container and sidebar components.
- Updated dashboard data service to manage M3U favorites and recently viewed items.
- Added tests to ensure correct behavior of recently viewed items in the dashboard data service.