Skip to content

Add Chromecast support#192

Merged
kristofferR merged 16 commits into
mainfrom
feat/chromecast-support
May 7, 2026
Merged

Add Chromecast support#192
kristofferR merged 16 commits into
mainfrom
feat/chromecast-support

Conversation

@kristofferR
Copy link
Copy Markdown
Owner

@kristofferR kristofferR commented May 6, 2026

Summary

  • Phase 1: mDNS Chromecast discovery + CASTV2 session control via rust_cast, with a LAN-bound HTTP proxy that's only reachable through a per-session 32-byte URL-safe token. HLS playlists are rewritten so segment URIs round-trip through the proxy. UI exposes a Cast button with a device picker dropdown inside the StreamPlayer overlay (sidebar and lightbox) plus a "Casting to X" badge in the corner of the player. On successful cast start, the local <video> is paused so the upstream connection isn't doubled.
  • Phase 2: When the source is detected as MPEG-TS, ffmpeg is spawned in copy mode (-c copy -f hls -hls_time 4 -hls_list_size 6 -hls_flags delete_segments+omit_endlist+independent_segments) into a sliding-window HLS playlist in a temp directory. The proxy serves the playlist and segments from disk so Chromecast (which doesn't natively play raw MPEG-TS over HTTP) can consume them. ffmpeg is killed and the temp directory removed when the cast session ends.
  • Backend: new engine/chromecast.rs (discovery + session worker bridged via tokio::task::spawn_blocking), engine/cast_proxy.rs (token-gated LAN proxy + HLS pass-through + remux mode), commands/chromecast.rs (discover_chromecasts, cast_to_device, stop_cast, get_cast_status). Cast session + proxy handle live in AppState.cast_state; new sessions tear down prior ones cleanly.
  • Frontend: useChromecast hook (devices/session/error state, cast://status event subscription) + cast button UI in StreamPlayer.tsx.

Ref #191. HEVC transcode fallback for old Chromecast dongles is intentionally deferred (Phase 3 in the original plan).

Test plan

  • Click cast button on an HLS test stream (e.g. Big Buck Bunny test HLS) → confirm playback on TV
  • Cast a real IPTV MPEG-TS stream → verify ffmpeg spawns, HLS playlist serves, TV plays
  • From a second LAN device, curl http://<host>:<port>/cast/<wrong-token>/stream → must 403
  • Stop cast → ffmpeg child exits, temp dir cleaned up, port closes
  • Cross-platform: macOS, Windows, Linux (mDNS reliability varies; Windows may need firewall opened on cast-proxy port)

Summary by CodeRabbit

  • New Features
    • Chromecast device discovery, device picker, refresh, and stop controls.
    • Cast controls in media toolbar, thumbnails, inline players, lightbox, and sidebar.
    • Background casting with persistent session status and “Casting to …” badge.
    • Hot-swap media onto an active cast session without reconnecting.
    • Cast-aware playback routing and channel‑surfing while casting.

Phase 1 of cast support (ref #191): mDNS discovery, CASTV2 session
control via rust_cast, and a LAN-bound token-gated stream proxy. UI
exposes a cast button + device picker in the StreamPlayer overlay.
MPEG-TS streams are detected and disabled pending Phase 2 (ffmpeg
remux).
Phase 2 of cast support (ref #191): when the source is detected as
MPEG-TS, spawn ffmpeg in copy mode to produce a sliding-window HLS
playlist in a temp directory. The cast proxy serves the playlist and
segments from disk so Chromecast (which doesn't natively support raw
MPEG-TS) can play them. ffmpeg is killed and the temp dir removed when
the cast session ends.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end Chromecast support: new Rust dependencies, models, a LAN HTTP cast proxy (HLS pass-through + optional ffmpeg remux), mDNS discovery and session engine, Tauri commands and AppState wiring, a React hook and UI components for discovery, cast start/stop, and status propagation.

Changes

Chromecast Integration

Layer / File(s) Summary
Dependencies & Manifest
src-tauri/Cargo.toml
Adds cast-sender = "0.3", mdns-sd = "0.19.1", local-ip-address = "0.6.12" to dependencies.
Data Models / Types
src-tauri/src/models/chromecast.rs, src/lib/types.ts
Adds ChromecastDevice, CastSession, CastSessionState, CastMediaRequest, CastStreamKind (Rust + TS).
AppState Wiring
src-tauri/src/state.rs, src-tauri/src/commands/mod.rs
Introduces CastState and a cast_state: Mutex<CastState> on AppState; registers commands::chromecast module.
Tauri Commands
src-tauri/src/commands/chromecast.rs, src-tauri/src/lib.rs
Adds commands: discover_chromecasts, cast_to_device, stop_cast, get_cast_status and integrates them into the invoke handler; serializes lifecycle transitions and stores session + proxy in state.
Chromecast Engine
src-tauri/src/engine/chromecast.rs
Implements mDNS discovery (discover), session manager (start_session), ActiveCastSession with stop/swap_media, worker loop, and status emission (CAST_STATUS_EVENT).
LAN Cast Proxy
src-tauri/src/engine/cast_proxy.rs
New proxy binding to 0.0.0.0:0 with tokenized /cast/<token>/ routing; HLS manifest rewriting, origin allowlisting, chunked upstream streaming, optional ffmpeg remux path with temp dir, codec probing, request validation, and unit tests.
FFmpeg Helpers Visibility
src-tauri/src/engine/ffmpeg.rs
Elevates GRACEFUL_KILL_TIMEOUT, graceful_kill, resolve_binary, and configure_background_process to pub(crate) (no behavior change).
Engine Module Exports
src-tauri/src/engine/mod.rs
Registers new cast_proxy and chromecast modules.
Commands → Frontend API
src/lib/tauri.ts
Adds TS bindings: discoverChromecasts, castToDevice, stopCast, getCastStatus.
Frontend Types & Utilities
src/lib/types.ts, src/lib/cast.ts
Adds Cast TS types and helpers: detectStreamKind, buildCastRequest, isCastSessionActive.
Frontend Hook
src/hooks/useChromecast.ts
Adds useChromecast hook exposing devices, discovering flag, session, error, and async actions refreshDevices, cast, stop; subscribes to cast://status.
UI Components & Wiring
src/components/CastMenu.tsx, src/components/StreamPlayer.tsx, src/components/ThumbnailPanel.tsx, src/components/ChannelTable.tsx, src/App.tsx
Adds CastMenu (popover/inline), integrates cast UI into StreamPlayer and ThumbnailPanel, threads chromecast hook through App, updates ChannelTable to debounce cast redirects, and routes play actions to cast when a session is active.

Sequence Diagram(s)

sequenceDiagram
    participant Frontend as Frontend UI
    participant Tauri as Tauri IPC
    participant Engine as Cast Engine
    participant mDNS as mDNS Network
    participant Cast as Chromecast Device
    participant Proxy as Local HTTP Proxy

    rect rgba(100, 150, 200, 0.5)
        Note over Frontend,mDNS: Discovery Flow
        Frontend->>Tauri: discoverChromecasts()
        Tauri->>Engine: discover(wait)
        Engine->>mDNS: Browse _googlecast._tcp.local.
        mDNS-->>Engine: Service responses
        Engine-->>Tauri: Vec<ChromecastDevice>
        Tauri-->>Frontend: Return devices
    end

    rect rgba(150, 200, 100, 0.5)
        Note over Frontend,Cast: Cast Flow
        Frontend->>Tauri: castToDevice(device, request)
        Tauri->>Proxy: start(upstream_url, stream_kind)
        Proxy-->>Tauri: proxy_url (tokenized)
        Tauri->>Engine: start_session(device, request, proxy_url, is_dash)
        Engine->>Cast: Connect & launch receiver, LOAD payload
        Cast->>Proxy: Fetch manifest/segments (via proxy_url)
        Proxy->>Proxy: Rewrite manifest / remux as needed
        Proxy-->>Cast: Serve segments
        Engine->>Tauri: emit cast://status (Playing)
        Tauri-->>Frontend: session update
    end

    rect rgba(200, 100, 150, 0.5)
        Note over Frontend,Proxy: Stop/Cleanup Flow
        Frontend->>Tauri: stopCast()
        Tauri->>Engine: stop session
        Engine->>Proxy: shutdown proxy
        Proxy->>Proxy: terminate ffmpeg, cleanup temp files
        Engine->>Tauri: emit cast://status (Stopped)
        Tauri-->>Frontend: clear session state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

🐰 I hopped through mDNS fields at night,

found tiny beacons blinking bright,
spun a proxy, remuxed a stream,
now Chromecasts sing the IPTV dream,
a carrot clap — the pixels take flight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Add Chromecast support' is concise and directly summarizes the main change—adding comprehensive Chromecast functionality across backend and frontend.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/chromecast-support

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/commands/chromecast.rs`:
- Around line 27-67: The cast lifecycle is not serialized: cast_to_device()
releases state.cast_state between tearing down the prior session and storing the
new proxy/session which allows interleaving with stop_cast() and
losing/abandoning sessions; fix by serializing the teardown/start/store sequence
using a dedicated lifecycle mutex or a generation ID on state.cast_state so the
entire sequence (take prior session/proxy, start new proxy via
cast_proxy::start, start new session via chromecast::start_session, and then set
guard.session and guard.proxy) is performed atomically — either by holding the
same async lock for the whole operation or by incrementing a generation counter
before release and validating the generation after async awaits (only storing
the new session/proxy if the generation matches), and ensure
proxy_handle.shutdown() / session.stop() are only called for the correct
generation to avoid shutting down a newer session (refer to cast_to_device(),
stop_cast(), state.cast_state, cast_proxy::start, chromecast::start_session,
guard.session, guard.proxy).

In `@src-tauri/src/engine/cast_proxy.rs`:
- Around line 697-776: The manifest rewrite currently accepts any absolute URI
and can rewrite to internal or non-HTTP(S) targets; update rewrite_manifest()
and rewrite_tag_uri() to validate resolved URLs: reject non-"http"/"https"
schemes and block hosts/addresses in loopback, private (RFC1918), link-local, or
IPv6 unique/local ranges unless the resolved target is explicitly allowed
because it shares origin with the original base URL (i.e., same host/port) or is
present in an allowlist derived from base. Implement a small helper (e.g.,
is_address_allowed(base: &Url, target: &Url) -> bool) that resolves
target.host_or_ip and checks scheme + IP ranges and same-origin, use it in both
rewrite_manifest (when joining trimmed segments) and rewrite_tag_uri (after
base.join(original)) to return the original line (no rewrite) when validation
fails; keep encode_segment usage for allowed targets. Ensure no network calls
are made during validation.
- Around line 307-351: After spawning the ffmpeg background task (which uses
cancel.clone()), ensure the remux worker is cancelled if wait_for_playlist(...)
fails: call cancel.cancel() immediately when wait_for_playlist returns an Err
(the call with playlist_path and REMUX_PLAYLIST_READY_TIMEOUT) before returning
the error so the tokio::spawn task (and its tmpdir) is signaled to terminate;
keep the rest of error propagation intact.
- Around line 519-540: serve_upstream builds a new reqwest::Client per call
which loses cookies/session state across requests; instead create and store a
single client on the cast session (e.g., add a reqwest::Client field to
CastProxyHandle or session state in AppState) and use that stored client in
serve_upstream rather than constructing a new one. Modify CastProxyHandle (or
session-level struct) to initialize the client once when the session is created,
ensure accept_invalid_certs/user_agent are set from settings when creating it,
and replace the local client construction in serve_upstream with a lookup of
that session client so cookies and redirects persist across manifest and segment
requests.

In `@src-tauri/src/engine/chromecast.rs`:
- Around line 291-303: The worker currently emits events on receiver Close and
receive() errors but never updates or removes the stored ActiveCastSession in
AppState.cast_state, so snapshot()/get_cast_status() still reports Playing; when
handling ChannelMessage::Connection::Close and in the Err(err) branch where you
call emit_state(..., CastSessionState::Stopped, ...) and emit_error(...), also
locate and update/remove the corresponding ActiveCastSession entry in
AppState.cast_state (the map that stores sessions/proxies) to mark it stopped or
delete it so snapshot() reflects the stopped state; perform this update using
the same device/session identifier used when inserting the session (the keys
used by ActiveCastSession storage) before breaking out of the loop.
- Around line 276-280: When handling the manual stop branch (if let
Ok(SessionCommand::Stop) = cmd_rx.try_recv()) you need to emit the final stopped
status before breaking so other windows/hooks receive the cast://status update;
after calling cast.media.stop(transport_id.clone(), 0) and
cast.receiver.stop_app(session_id.clone()) invoke the same status-emission
routine used elsewhere (e.g., the function or channel used to send cast://status
events from this module) with a "stopped" payload for
device.friendly_name/session_id, then log and break; ensure you reference the
existing status-sender used by this casting session so the stopped event is
published to all listeners.

In `@src/components/StreamPlayer.tsx`:
- Around line 108-111: openCastMenu currently opens the cast picker but doesn't
cancel or suspend the toolbar auto-hide, so the toolbar can still fade while the
picker is open; modify openCastMenu to suspend auto-hide when opening (e.g.,
clear the existing hide timeout via clearHideTimer or call a helper like
pauseAutoHide and set a flag like isAutoHidePaused true) and ensure that when
castMenuOpen becomes false the original auto-hide behavior is restored (restart
the hide timer via startHideTimer or resumeAutoHide). Update any related cleanup
so chromecast.refreshDevices() remains called but auto-hide is explicitly
paused/resumed around castMenuOpen state changes.

In `@src/components/ThumbnailPanel.tsx`:
- Around line 208-213: The castRequest object in ThumbnailPanel is reconstructed
on every render which breaks prop identity for downstream components like
StreamPlayer; wrap the castRequest creation in a useMemo inside ThumbnailPanel
keyed on resolvedUrl, result.url, result.name, result.tvg_logo and the output of
detectStreamKind(resolvedUrl ?? result.url) so the reference is stable between
renders; ensure you import/use React.useMemo and replace the inline const
castRequest with the memoized value so StreamPlayer effects that depend on
castRequest only run when the actual inputs change.
- Around line 13-24: In detectStreamKind (returning CastStreamKind) the catch
branch inspects the raw URL string and uses endsWith(".ts") which misses
query/fragment variants and uses includes(".m3u8") which overmatches; change the
fallback to strip query/fragment first (e.g., split on ? and # or use a regex to
test for /\.m3u8(?:$|\?|#)|\.ts(?:$|\?|#)/) and then use endsWith on that
stripped path so both ".m3u8" and ".ts" are detected symmetrically and tolerate
query/fragment parts.

In `@src/hooks/useChromecast.ts`:
- Around line 84-95: The listener callback for
listen<CastSession>("cast://status") only sets setError when next.state ===
"error", leaving stale error messages after healthy updates; update the callback
so that after you derive next = event.payload and handle "stopped"
(setSession(null)), you also clear any previous error for non-error states (call
setError(null) when next.state !== "error") and keep the existing behavior of
setting the explicit error message in the "error" branch; reference the listen
callback, the CastSession payload (next), setSession, setError, mounted, and
cancelledRef when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ce0de9b-cbc1-41b5-b2e5-fabf22596ffa

📥 Commits

Reviewing files that changed from the base of the PR and between 01003eb and 636f834.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • src-tauri/Cargo.toml
  • src-tauri/src/commands/chromecast.rs
  • src-tauri/src/commands/mod.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/engine/ffmpeg.rs
  • src-tauri/src/engine/mod.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/models/chromecast.rs
  • src-tauri/src/models/mod.rs
  • src-tauri/src/state.rs
  • src/components/StreamPlayer.tsx
  • src/components/ThumbnailPanel.tsx
  • src/hooks/useChromecast.ts
  • src/lib/tauri.ts
  • src/lib/types.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build (macos-latest)
  • GitHub Check: Build (windows-latest)
  • GitHub Check: Build (windows-latest)
  • GitHub Check: Build (ubuntu-22.04)
  • GitHub Check: Rust Tests
🧰 Additional context used
📓 Path-based instructions (5)
src-tauri/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src-tauri/src/**/*.rs: Use snake_case for variable and function names in Rust
Use 4-space indentation in Rust code
Use thiserror crate for error handling in Rust
Use serde for serialization in Rust code
Run cargo clean -p iptv-checker && cargo build in src-tauri/ before bun tauri dev after Rust code changes to ensure a fresh binary

Files:

  • src-tauri/src/commands/mod.rs
  • src-tauri/src/commands/chromecast.rs
  • src-tauri/src/engine/mod.rs
  • src-tauri/src/models/mod.rs
  • src-tauri/src/engine/ffmpeg.rs
  • src-tauri/src/lib.rs
  • src-tauri/src/state.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/models/chromecast.rs
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Define agent interfaces and implementations in TypeScript for agent systems
Use clear naming conventions for agent classes and methods
Document agent behavior and responsibilities in comments or docstrings
Implement error handling in agent methods

Files:

  • src/lib/types.ts
  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
  • src/lib/tauri.ts
  • src/hooks/useChromecast.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Use camelCase for variable and function names in TypeScript
Enable strict mode in TypeScript and avoid unused locals or parameters

Files:

  • src/lib/types.ts
  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
  • src/lib/tauri.ts
  • src/hooks/useChromecast.ts
src/lib/types.ts

📄 CodeRabbit inference engine (AGENTS.md)

Define types in lib/types.ts to mirror Rust models for type safety

Files:

  • src/lib/types.ts
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/components/**/*.{ts,tsx}: Use functional components with hooks in React
Use Tailwind CSS for styling, avoid CSS-in-JS in React components
Use lucide-react for icons in React components
Use @tanstack/react-virtual for virtualized lists when displaying 1000+ items

Files:

  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
🔇 Additional comments (7)
src/lib/tauri.ts (1)

292-309: LGTM — wrapper signatures match the Rust commands.

discoverChromecasts, castToDevice, stopCast, and getCastStatus faithfully mirror discover_chromecasts, cast_to_device, stop_cast, and get_cast_status per the cross-file context. Argument keys (device, request) are single words, so Tauri's snake↔camel conversion is a no-op and the payload shape will reach the Rust handlers unchanged.

src-tauri/src/commands/mod.rs (1)

1-1: LGTM.

Alphabetical ordering preserved; the new chromecast submodule is exposed for lib.rs registration.

src-tauri/src/models/mod.rs (1)

3-3: LGTM.

Module declaration is correctly placed and alphabetically ordered.

src-tauri/src/state.rs (1)

63-75: LGTM.

CastState storage with Option<ActiveCastSession>/Option<CastProxyHandle> behind Mutex is the right shape for a single-active-session model, and commands/chromecast.rs correctly take()s out of the guard before calling the async stop()/shutdown() so the lock isn't held across awaits.

src-tauri/src/engine/mod.rs (1)

1-3: LGTM.

cast_proxy and chromecast are correctly registered alphabetically alongside the existing engine modules.

src/lib/types.ts (1)

427-460: ⚡ Quick win

No action needed—Rust models are already correctly configured with proper serde renaming.

The chromecast.rs Rust models in src-tauri/src/models/ already carry #[serde(rename_all = "camelCase")] on ChromecastDevice, CastSession, and CastMediaRequest structs. The CastSessionState and CastStreamKind enums correctly use #[serde(rename_all = "snake_case")], producing the literals "hls", "mpeg_ts", and "other" which match the TypeScript enum. The TypeScript types properly mirror the serialized output, and deserialization through tauri::invoke will work correctly.

			> Likely an incorrect or invalid review comment.
src-tauri/Cargo.toml (1)

42-44: ⚡ Quick win

All dependency versions are current and stable. rust_cast 0.21.0 is the latest stable release with the thread_safe feature present and not yanked. mdns-sd 0.19.1 and local-ip-address 0.6.12 are also current releases. No security advisories are present for any of these crates.

Comment thread src-tauri/src/commands/chromecast.rs Outdated
Comment thread src-tauri/src/engine/cast_proxy.rs Outdated
Comment thread src-tauri/src/engine/cast_proxy.rs Outdated
Comment thread src-tauri/src/engine/cast_proxy.rs
Comment thread src-tauri/src/engine/chromecast.rs Outdated
Comment thread src-tauri/src/engine/chromecast.rs Outdated
Comment thread src/components/StreamPlayer.tsx Outdated
Comment thread src/components/ThumbnailPanel.tsx Outdated
Comment thread src/components/ThumbnailPanel.tsx Outdated
Comment thread src/hooks/useChromecast.ts
Backend:
- Serialize cast lifecycle via cast_lifecycle_lock so concurrent
  cast_to_device / stop_cast calls cannot interleave and orphan a session.
- Constrain HLS manifest rewrites to same-origin http(s) targets so a
  malicious playlist can't make the cast proxy SSRF loopback or RFC1918
  endpoints; also enforced at the /seg/<b64> fetch boundary.
- Reuse one reqwest::Client per cast session so cookies set on the
  manifest response persist into segment fetches (fixes 403s on
  cookie-gated streams).
- Cancel the remux worker if wait_for_playlist times out, so ffmpeg
  and the temp dir don't leak.
- Track a per-session UID and clear AppState from the worker on
  self-exit (heartbeat fail, Connection::Close), preventing stale
  Playing state in get_cast_status after a disconnect. The UID match
  guards against clearing a successor session.
- Emit a final Stopped event on the manual-stop path.

Frontend:
- Pin the player controls and cancel the auto-hide timer while the
  cast picker is open.
- Memoize castRequest so downstream effects don't re-fire on every
  parent render.
- Make detectStreamKind's fallback branch strip query/fragment before
  checking endsWith, so URLs like .../stream.ts?token=... still
  classify as MPEG-TS.
- Clear stale cast errors when a healthy status update arrives.
@kristofferR
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/engine/cast_proxy.rs`:
- Around line 478-520: The segment SSRF allowlist is being compared against the
original upstream_url instead of the manifest's final resolved origin, causing
valid rewritten segment URLs to be rejected; update the session to store the
manifest's final URL/origin (e.g. set a resolved_origin when
rewrite_manifest/serve_upstream records final_url) and change
handle_connection's segment-fetch branch to call
is_target_allowed(&resolved_origin_or_fallback, &decoded_url) (falling back to
upstream_url if resolved_origin is None); also add or update tests to cover a
redirected manifest/CDN-hosted segments case so
rewrite_manifest_drops_cross_origin_segments and related tests exercise the
redirect/CDN scenario.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3eec2b5f-8919-4131-b535-7fdf63c05fca

📥 Commits

Reviewing files that changed from the base of the PR and between 636f834 and d65ec34.

📒 Files selected for processing (7)
  • src-tauri/src/commands/chromecast.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/state.rs
  • src/components/StreamPlayer.tsx
  • src/components/ThumbnailPanel.tsx
  • src/hooks/useChromecast.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
src-tauri/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src-tauri/src/**/*.rs: Use snake_case for variable and function names in Rust
Use 4-space indentation in Rust code
Use thiserror crate for error handling in Rust
Use serde for serialization in Rust code
Run cargo clean -p iptv-checker && cargo build in src-tauri/ before bun tauri dev after Rust code changes to ensure a fresh binary

Files:

  • src-tauri/src/state.rs
  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/commands/chromecast.rs
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Define agent interfaces and implementations in TypeScript for agent systems
Use clear naming conventions for agent classes and methods
Document agent behavior and responsibilities in comments or docstrings
Implement error handling in agent methods

Files:

  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
  • src/hooks/useChromecast.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Use camelCase for variable and function names in TypeScript
Enable strict mode in TypeScript and avoid unused locals or parameters

Files:

  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
  • src/hooks/useChromecast.ts
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/components/**/*.{ts,tsx}: Use functional components with hooks in React
Use Tailwind CSS for styling, avoid CSS-in-JS in React components
Use lucide-react for icons in React components
Use @tanstack/react-virtual for virtualized lists when displaying 1000+ items

Files:

  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
🔇 Additional comments (7)
src/hooks/useChromecast.ts (1)

84-101: Status listener correctly handles all session states.

The branching is sound: stopped clears both session and error, error populates the error message, and any other healthy state drops a stale message — that fully addresses the previous concern about persistent errors after recovery.

src/components/StreamPlayer.tsx (1)

60-147: Cast picker integration looks solid.

Auto-hide is now correctly suspended while castMenuOpen is true (via the scheduleHide guard at line 76 and the dedicated effect at 87-95), the outside-click handler is scoped to the open state, and handleCastDevice pauses the local <video> when starting a session, which matches the PR’s “avoid duplicate upstream connections” intent.

src/components/ThumbnailPanel.tsx (1)

13-27: Stream-kind detection and castRequest memoization look correct.

The fallback branch now strips query/fragment symmetrically before testing the extension, and castRequest is memoized on the actual inputs so prop identity stays stable across ThumbnailPanel re-renders — both prior concerns are resolved.

Also applies to: 211-222

src-tauri/src/engine/cast_proxy.rs (1)

154-163: Session-scoped client, cancel-on-warmup-failure, and same-origin allowlist all look right.

Building one reqwest::Client per session with cookie_store(true) and threading it through handle_connection/serve_upstream correctly preserves auth cookies across manifest and segment requests. The early cancel.cancel() on wait_for_playlist failure prevents orphaned ffmpeg/temp dirs, and is_target_allowed together with the rewrite-side checks closes the SSRF surface for non-http(s) and cross-origin targets — all three previous concerns are addressed.

Also applies to: 374-378, 743-754

src-tauri/src/engine/chromecast.rs (1)

292-340: Worker lifecycle now keeps AppState consistent on both stop paths.

The manual_stop flag plus the uid-guarded clear_app_state_if_uid_matches correctly distinguishes the explicit stop path (caller already cleared AppState) from self-exit on Connection::Close / receive errors, and the uid match prevents a successor session from being clobbered. Combined with the Stopped emit on the manual branch, both prior concerns about stale Playing state and missing final events are addressed.

src-tauri/src/state.rs (1)

63-100: State wiring looks consistent.

CastState correctly couples the proxy handle with the active session, and cast_lifecycle_lock provides the dedicated mutex needed to serialize start/stop transitions in commands/chromecast.rs. Lock ordering (lifecycle → cast_state) is consistent across the call sites I see in this PR, so the documented "no interleaving" guarantee holds.

src-tauri/src/commands/chromecast.rs (1)

19-93: Lifecycle serialization looks correct.

Holding cast_lifecycle_lock across the full teardown → cast_proxy::startchromecast::start_session → store sequence (and the symmetric stop_cast path) closes the previous interleaving window. Releasing the inner cast_state lock between phases is intentional and safe because the lifecycle lock prevents any other start/stop call from sneaking in. Error path at lines 60-63 correctly shuts the freshly started proxy down before returning.

Comment thread src-tauri/src/engine/cast_proxy.rs
The previous fix pinned segment validation to the operator-supplied
upstream URL, but `rewrite_manifest` is called with `final_url` (the
URL after redirects). Whenever the manifest fetch lands on a different
host (302 to a CDN, or a master playlist that points sub-playlists at
a separate CDN host), the rewritten /seg/<b64> URLs encode CDN-rooted
URLs and the validator self-rejects them with 403.

Track the manifest's resolved origin in shared session state, populate
it from the first successful manifest fetch, and validate segments
against resolved_origin || upstream — accepting either origin when
both are known so master + sub-playlist splits across hosts still work.

Adds two tests: rewrite uses the post-redirect base, and the segment
acceptance helper accepts CDN segments after redirect while still
rejecting loopback.
rust_cast 0.19+ uses rustls, whose webpki parser rejects Chromecast's
self-signed X.509v1 certs with UnsupportedCertVersion before any
verifier runs — `dangerous_configuration` cannot bypass the parse-time
rejection. cast-sender uses async-native-tls (SChannel/Security/OpenSSL),
which accepts the certs out of the box.
Chromecast CAF receivers require CORS headers on adaptive media
manifests and segments per the Cast media specs; without them the
LOAD silently fails with no useful diagnostics. Also answer the
preflight OPTIONS directly (before the token check) and log each
incoming request so reachability is verifiable.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/engine/cast_proxy.rs`:
- Around line 460-466: The request parser currently only extracts the start-line
into method/path and drops all headers, so HEAD and Range requests are
advertised but not honored; update the parsing logic (where method is extracted
and parse_request_path is called) to also parse and retain request headers
(especially "Range") and propagate them into the handling code paths that serve
content; then modify the response paths that return full bodies to: for method
== "HEAD" return the same headers/status without a body, and for requests with a
valid "Range" header parse the byte-range, return 206 Partial Content with the
correct "Content-Range" and sliced response body (or 416 when invalid) while
preserving content length/accept-ranges, and ensure these changes are applied to
the other serving locations referenced (the other request-to-response handlers
in this module).
- Around line 116-124: detect_lan_ip() returns IpAddr but the advertised cast
URL is formed via simple string interpolation (http://{lan_ip}:{port}), which is
invalid for IPv6 and may mismatch an IPv4-only bind; update the logic so that
detect_lan_ip either prefers/returns an IPv4 address when the server binds to
0.0.0.0 or, when constructing the advertised URL, wrap IPv6 addresses in
brackets (e.g. [::1]) and/or fall back to the machine's IPv4 when available;
locate detect_lan_ip() and the code that builds the advertised cast URL (the
interpolation site near the cast proxy URL construction) and implement IPv6
bracketing and IPv4-preference fallback to ensure valid authority formatting and
compatibility with a 0.0.0.0 bind.
- Line 473: The log is currently emitting sensitive parts of the request path
(e.g., "/cast/<token>..." and "/seg/<b64>...") via log::info!("[CastProxy]
{method} {path} from {peer}"); — update the logging to redact those sensitive
segments before logging by transforming the path variable (e.g., replace the
token after "/cast/" and the base64/URL after "/seg/" with a constant like
"<REDACTED>" or strip query/last path segments), using a small helper or regex
inside the same module (cast_proxy.rs) so the printed line in the CastProxy
request handler still uses method, the sanitized path, and peer but never emits
raw tokens or encoded upstream URLs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0d353e1a-b2e9-4f5d-8292-438ee3bca3e5

📥 Commits

Reviewing files that changed from the base of the PR and between d65ec34 and 8a7f077.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • src-tauri/Cargo.toml
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/chromecast.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
src-tauri/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src-tauri/src/**/*.rs: Use snake_case for variable and function names in Rust
Use 4-space indentation in Rust code
Use thiserror crate for error handling in Rust
Use serde for serialization in Rust code
Run cargo clean -p iptv-checker && cargo build in src-tauri/ before bun tauri dev after Rust code changes to ensure a fresh binary

Files:

  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/engine/cast_proxy.rs

Comment thread src-tauri/src/engine/cast_proxy.rs
Comment thread src-tauri/src/engine/cast_proxy.rs
Comment thread src-tauri/src/engine/cast_proxy.rs Outdated
The cast UI was rendered identically in the cramped ~288px sidebar and the
~1500px lightbox, so the in-player popover overflowed the sidebar's bounds
and the bottom-strip cast button competed with playback controls. Extract
a single CastMenu component with popover (lightbox) and inline (sidebar)
modes, gate StreamPlayer's cast UI behind a `compact` prop, and lift
useChromecast to ThumbnailPanel so both render sites share one session.

Starting a cast also paused — but didn't stop — the local player, leaving
its upstream HTTP connection alive. Most IPTV servers enforce a single
connection limit, so the local proxy and the cast pipeline's ffmpeg kept
kicking each other off every few seconds. Each ffmpeg reconnect introduced
a backwards DTS jump that the receiver's HLS player couldn't recover from,
and the session went idle/error within a minute. The cast handler now
calls onStop instead of onTogglePause, and the inline cast row stays
mounted while a session is active so the user can still hit Stop after
local playback is gone.
Move onCastStart before the chromecast.cast() await in both popover and
inline modes. The parent uses it to flip playIntentActive=false; firing
it after the ~seconds-long handshake meant arrow-key navigation in the
channel table could still trigger ChannelTable auto-open mid-cast,
spawning a local stream that fought the cast ffmpeg for the IPTV
single-connection upstream slot and killed the session.
While a Chromecast session is active, all play paths (sidebar Play,
lightbox Play, double-click, Enter, arrow-key auto-open) now call
chromecast.cast(currentDevice, request) for the target channel instead
of streamPlayer.play(). This prevents the local stream_proxy from
opening a competing connection to single-connection IPTV upstreams,
which previously kicked the cast pipeline's ffmpeg off the upstream
within seconds.

Arrow-key navigation gets a 300ms debounce when casting so a key burst
coalesces into a single backend cast_to_device call instead of one per
keystroke. Explicit actions (Enter, double-click, Play button, context
menu) cancel the pending debounce and fire immediately.

useChromecast is lifted from ThumbnailPanel to App.tsx so handlePlayInApp
can read the session. Shared helpers (buildCastRequest,
isCastSessionActive, detectStreamKind) consolidated in src/lib/cast.ts.
A lastCastDeviceRef in App.tsx provides a fallback if mDNS discovery
drops the device while a session is live.

No backend changes — cast_to_device is already idempotent: it acquires
cast_lifecycle_lock, tears down the prior proxy/ffmpeg/CAF session, and
starts fresh.
Three fixes that came out of testing the cast redirect flow:

- Hot-swap on the live receiver session when redirecting to the same
  device, so the TV transitions between channels cleanly instead of
  flashing back to the launcher between LOADs.
- Plain row click redirects an active cast (same 300ms debounce as
  arrow keys), matching the existing double-click and Enter behavior.
- Hand-craft the LOAD payload via cast-sender's Custom namespace so the
  metadata discriminator goes on the wire as `metadataType: 0` (Cast V2
  numeric form) instead of cast-sender 0.3's default `type: "GENERIC"`.
  Default Media Receiver only honors the V2 form when picking the title
  it shows on screen — the old rust_cast crate emitted that form, which
  is why channel names rendered there but stopped after the swap.
The Chromecast Default Media Receiver's CAFv3 MPL doesn't support fMP4
HLS playback, so HEVC content (which can't ride MPEG-TS per Cast spec)
falls through to LoadFailed on the receiver. DASH on the same receiver
dispatches to Shaka, which handles fMP4+HEVC out of the box.

- Probe video codec; HEVC takes the DASH/fMP4 path, H.264 keeps HLS+TS
- Tag video as hvc1 (Cast-/Apple-standard) instead of hev1
- Transcode audio to AAC for the DASH path: copy-into-mp4 fails on
  MPEG-TS sources because the TS audio codec_tag (0x0F = MPEG-2 AAC OTI)
  is incompatible with mp4's expected mp4a, and many EU broadcast
  streams carry MP2/AC3 audio that mp4 can't hold at all
- Add aresample async drift correction and avoid_negative_ts make_zero
  so AAC encoder + fMP4 muxer survive timestamp jumps on upstream
  reconnects (single-conn IPTV servers kick ffmpeg every few seconds)
- Capture last 32 ffmpeg stderr lines and dump at WARN on non-zero exit
  so failure causes show up at the default log level
- Plumb is_dash through cast_proxy → chromecast::build_load_payload so
  the LOAD payload sets contentType application/dash+xml and skips the
  HLS-specific hlsSegmentFormat/hlsVideoSegmentFormat fields
Single-connection IPTV servers kick ffmpeg every few seconds. ffmpeg's
built-in -reconnect re-opens the HTTP response, but each reconnect
flushes the demuxer state and the DASH muxer rejects the resulting
mid-window PTS jumps, killing the cast within seconds on 4K HEVC.

Pipe upstream bytes into ffmpeg's stdin via a tokio task that
transparently re-establishes the upstream HTTP connection on EOF or
error. ffmpeg now sees one continuous TS source for the lifetime of
the cast, so +genpts+discardcorrupt smooths over the inevitable PTS
resets between connections instead of restarting the demuxer.

H.264 stays on ffmpeg's HTTP input — TS+HLS tolerates mid-stream
resets and the simpler path is well-tested.
Many IPTV channels carry EAC3, AC3, or MP2 audio in MPEG-TS. Default
Media Receiver's MPL emits LoadFailed on those codec_tags even though
the muxer accepts them. Probe audio alongside video and transcode to
AAC unless the upstream is already AAC; preserves the well-tested
H.264+AAC stream-copy path.

Also default unknown URLs in detectStreamKind to mpeg_ts. IPTV
endpoints frequently serve TS without a `.ts` extension (e.g.
`/live/<creds>/<id>` returning `video/mp2t`). Routing them through
the ffmpeg remux path lets the codec probe pick the right pipeline
instead of streaming raw bytes the receiver can't decode.
The proxy advertised HEAD/Range support in its CORS preflight but ignored
both — every method returned a full body, every Range request got a 200
with the whole file. Receivers that probe segments with HEAD or do partial
fetches saw mismatched framing and either stalled or aborted load.

- Add parse_request_header (case-insensitive, header-block-bounded) and
  parse_byte_range (single byte-unit form, clamped to file length).
- serve_remux_file now honors HEAD (headers only) and Range (206 +
  Content-Range, or 416 if unsatisfiable) and advertises Accept-Ranges.
- serve_upstream forwards HEAD upstream and pipes Range upstream as-is;
  preserves Content-Length / Content-Range / Accept-Ranges from the
  upstream response and only falls back to chunked when no length is
  known. HEAD writes headers and skips the body entirely.

Also fix detect_lan_ip on dual-stack hosts: local_ip() can return IPv6
which is unreachable from the IPv4-bound listener. Fall through to
list_afinet_netifas and pick the first non-loopback IPv4.

Tested via cargo test --lib cast_proxy (18 passing, including 9 new for
the parsers).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/App.tsx (1)

2499-2520: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Never fall through to local playback while a cast session is active.

Two paths here still violate the “redirect instead of local play” rule: the scan/single-provider gate runs before the cast redirect, and the unresolved-device branch falls through into streamPlayer.play(). In both cases a click during an active cast can still open a competing local stream.

Suggested restructuring
     (result: ChannelResult) => {
-      if (isScanActive(getStore().scanState) && getStore().playlist?.single_provider) {
-        getStore().setPendingPlaybackChannel(result);
-        return;
-      }
-      // While a cast session is active, redirect the cast to the new channel
-      // instead of starting a competing local stream. Single-connection IPTV
-      // upstreams can only feed one consumer; starting local play would kick
-      // the cast pipeline's ffmpeg off the upstream.
       if (isCastSessionActive(chromecast.session)) {
         const session = chromecast.session;
         const device =
           chromecast.devices.find((d) => d.id === session.deviceId) ??
           (lastCastDeviceRef.current?.id === session.deviceId
             ? lastCastDeviceRef.current
             : null);
-        if (device) {
-          void chromecast.cast(device, buildCastRequest(result));
-          return;
-        }
-        // Fall through to local play if we can't resolve the device — better
-        // than silently doing nothing.
+        if (!device) {
+          void chromecast.refreshDevices();
+          getStore().setPlaybackError("Cast session is active, but the target device is unavailable.");
+          return;
+        }
+        void chromecast.cast(device, buildCastRequest(result)).catch(() => {});
+        return;
+      }
+
+      if (isScanActive(getStore().scanState) && getStore().playlist?.single_provider) {
+        getStore().setPendingPlaybackChannel(result);
+        return;
       }
       getStore().setPlayIntentActive(true);
       streamPlayer.play(result);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` around lines 2499 - 2520, The current click handler checks
isScanActive/getStore().playlist?.single_provider before checking
isCastSessionActive, and the unresolved-device branch falls through to local
playback; move the cast-session guard to run before the scan/single-provider
branch so any active cast is handled first (use
isCastSessionActive(chromecast.session) early), and once a cast session is
detected always either call chromecast.cast(...) or return (do not let execution
reach streamPlayer.play()); in the unresolved-device case explicitly return (or
set a pending/cancel state) after attempting resolution or logging so local
playback cannot start.
♻️ Duplicate comments (3)
src-tauri/src/engine/cast_proxy.rs (3)

178-197: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Bracket IPv6 hosts in the advertised cast URL.

detect_lan_ip() returns IpAddr, but these URLs are still built as http://{lan_ip}:{port}. For IPv6 that produces an invalid authority, and it also mismatches the current IPv4-only bind on 0.0.0.0, so dual-stack hosts can advertise a cast URL the receiver cannot open.

Suggested fix
+fn format_advertised_host(ip: IpAddr) -> String {
+    match ip {
+        IpAddr::V4(v4) => v4.to_string(),
+        IpAddr::V6(v6) => format!("[{v6}]"),
+    }
+}
+
-            format!(
-                "http://{lan_ip}:{port}/cast/{token}/hls/{}",
-                started.manifest_filename
-            ),
+            format!(
+                "http://{}:{port}/cast/{token}/hls/{}",
+                format_advertised_host(lan_ip),
+                started.manifest_filename
+            ),
             started.is_dash,
         )
     } else {
-        (format!("http://{lan_ip}:{port}/cast/{token}/stream"), false)
+        (
+            format!(
+                "http://{}:{port}/cast/{token}/stream",
+                format_advertised_host(lan_ip)
+            ),
+            false,
+        )
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/engine/cast_proxy.rs` around lines 178 - 197, The advertised
cast URL is built as http://{lan_ip}:{port} which breaks for IPv6 addresses;
update the URL construction used around cast_url (the branches that set cast_url
when stream_kind == CastStreamKind::MpegTs and the else branch that yields
"/stream") to bracket IPv6 literals (e.g., produce http://[IPv6]:port) —
implement a small helper or inline logic that checks the IpAddr kind (or string
contains ':') and wraps the address in [ ] when IPv6 so start_remux and the
other URL formatting produce valid authorities.

816-823: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't advertise HEAD/Range until the proxy actually parses them.

This parser only keeps the request line, so downstream handlers never see Range, and both serving paths still send full bodies. Advertising GET, HEAD, OPTIONS plus Range support here makes receivers assume partial-content semantics the proxy does not implement.

Also applies to: 835-845

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/engine/cast_proxy.rs` around lines 816 - 823, The proxy
currently advertises support for HEAD and Range even though it only parses the
request line (the local variable method and parse_request_path are used) and
never implements partial/content-range semantics; remove HEAD from the
advertised methods and remove any Accept-Ranges/Range advertisement in the
response headers (the same change must be applied in the other block mentioned
around lines 835-845). Update the header-building code to only advertise the
actual, implemented verbs (e.g., GET and OPTIONS) and ensure the method parsing
(variable method derived from request and parse_request_path) is left unchanged.

830-830: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redact the cast token and encoded upstream URL from request logs.

path includes /cast/<token>/..., and /seg/<b64> also encodes the upstream URL. Logging it verbatim turns the request line into bearer credentials plus stream metadata.

Minimal redaction
-    log::info!("[CastProxy] {method} {path} from {peer}");
+    let sanitized_path = sanitize_logged_path(&path);
+    log::info!("[CastProxy] {method} {sanitized_path} from {peer}");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/engine/cast_proxy.rs` at line 830, The request log prints the
raw path (containing sensitive data like the cast token and base64-encoded
upstream URL); before logging in the CastProxy code (where
log::info!("[CastProxy] {method} {path} from {peer}"); is called), create a
sanitized_path by redacting the token and encoded URL segments (e.g., replace
"/cast/<token>/" with "/cast/<redacted>/" and "/seg/<b64>" with
"/seg/<redacted>") and use sanitized_path in the log call; update references to
method, path, and peer accordingly so logs no longer emit bearer-like
credentials or encoded upstream URLs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/commands/chromecast.rs`:
- Around line 34-50: The bug is a race between the initial same-device check and
the later take causing expect("checked above") to panic; fix by combining the
check-and-take under a single cast_state lock: instead of computing same_device
with one lock and then re-locking to take session/proxy, acquire
state.cast_state.lock().await once and use an if let Some(session) =
guard.session.as_ref() (or inspect session.session.device_id) to detect the
matching device, and if it matches call guard.session.take() and
guard.proxy.take() inside that same critical section to produce (active,
prior_proxy); this ensures atomic check-and-take and prevents races with
clear_app_state_if_uid_matches/drive_session without changing
cast_lifecycle_lock acquisition.

In `@src-tauri/src/engine/cast_proxy.rs`:
- Around line 946-951: The current guard only rejects '/' and '..' but allows
Windows absolute/UNC paths and backslashes; update validation around the
relative variable and tmpdir.join(relative) to: reject any backslashes, leading
drive-letter patterns (e.g. /^[A-Za-z]:/), and UNC forms (leading "\\"), and
reject any input that is empty or contains path-separators; after computing the
candidate path, canonicalize both tmpdir and the joined path and ensure the
canonicalized candidate starts_with the canonicalized tmpdir before serving; on
failure use write_simple to return a 400/403 as before.

In `@src/components/ThumbnailPanel.tsx`:
- Around line 385-390: The inline cast/stop UI is hidden whenever lightboxOpen
is true, but when starting a cast from the lightbox StreamPlayer unmounts and
the inline CastMenu/stop path must remain visible; update the conditional that
renders the inline cast row (the block using lightboxOpen, isPlaying, and
isCastSessionActive(chromecast.session)) to allow the CastMenu to render when a
cast session is active even if lightboxOpen is true (i.e., change the early
return for lightboxOpen to only return null when there is no active cast
session), ensuring the CastMenu/stop controls referenced by CastMenu,
StreamPlayer, and onStopPlayer remain accessible while casting from the
lightbox.

In `@src/hooks/useChromecast.ts`:
- Around line 62-68: The stop function currently clears session in the finally
block even when stopCast() fails; change it so session is only cleared when
stopCast succeeds (or when cancelledRef.current is true). Specifically, in
useChromecast's stop callback (function stop) remove the finally-based
setSession(null) and instead await stopCast() inside try, then after a
successful await (and if !cancelledRef.current) call setSession(null); on error
catch the exception, setError(err) and do not clear session so the UI still
reflects the active receiver; keep cancelledRef handling so cancellation still
clears session when appropriate.

---

Outside diff comments:
In `@src/App.tsx`:
- Around line 2499-2520: The current click handler checks
isScanActive/getStore().playlist?.single_provider before checking
isCastSessionActive, and the unresolved-device branch falls through to local
playback; move the cast-session guard to run before the scan/single-provider
branch so any active cast is handled first (use
isCastSessionActive(chromecast.session) early), and once a cast session is
detected always either call chromecast.cast(...) or return (do not let execution
reach streamPlayer.play()); in the unresolved-device case explicitly return (or
set a pending/cancel state) after attempting resolution or logging so local
playback cannot start.

---

Duplicate comments:
In `@src-tauri/src/engine/cast_proxy.rs`:
- Around line 178-197: The advertised cast URL is built as
http://{lan_ip}:{port} which breaks for IPv6 addresses; update the URL
construction used around cast_url (the branches that set cast_url when
stream_kind == CastStreamKind::MpegTs and the else branch that yields "/stream")
to bracket IPv6 literals (e.g., produce http://[IPv6]:port) — implement a small
helper or inline logic that checks the IpAddr kind (or string contains ':') and
wraps the address in [ ] when IPv6 so start_remux and the other URL formatting
produce valid authorities.
- Around line 816-823: The proxy currently advertises support for HEAD and Range
even though it only parses the request line (the local variable method and
parse_request_path are used) and never implements partial/content-range
semantics; remove HEAD from the advertised methods and remove any
Accept-Ranges/Range advertisement in the response headers (the same change must
be applied in the other block mentioned around lines 835-845). Update the
header-building code to only advertise the actual, implemented verbs (e.g., GET
and OPTIONS) and ensure the method parsing (variable method derived from request
and parse_request_path) is left unchanged.
- Line 830: The request log prints the raw path (containing sensitive data like
the cast token and base64-encoded upstream URL); before logging in the CastProxy
code (where log::info!("[CastProxy] {method} {path} from {peer}"); is called),
create a sanitized_path by redacting the token and encoded URL segments (e.g.,
replace "/cast/<token>/" with "/cast/<redacted>/" and "/seg/<b64>" with
"/seg/<redacted>") and use sanitized_path in the log call; update references to
method, path, and peer accordingly so logs no longer emit bearer-like
credentials or encoded upstream URLs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b108323-a7f6-469b-b05a-320b8c537705

📥 Commits

Reviewing files that changed from the base of the PR and between 8a7f077 and 98930f7.

📒 Files selected for processing (10)
  • src-tauri/src/commands/chromecast.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src-tauri/src/engine/chromecast.rs
  • src/App.tsx
  • src/components/CastMenu.tsx
  • src/components/ChannelTable.tsx
  • src/components/StreamPlayer.tsx
  • src/components/ThumbnailPanel.tsx
  • src/hooks/useChromecast.ts
  • src/lib/cast.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build (macos-latest)
  • GitHub Check: Build (windows-latest)
  • GitHub Check: Rust Tests
  • GitHub Check: Build (ubuntu-22.04)
  • GitHub Check: Build (windows-latest)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Define agent interfaces and implementations in TypeScript for agent systems
Use clear naming conventions for agent classes and methods
Document agent behavior and responsibilities in comments or docstrings
Implement error handling in agent methods

Files:

  • src/lib/cast.ts
  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
  • src/App.tsx
  • src/components/ChannelTable.tsx
  • src/components/CastMenu.tsx
  • src/hooks/useChromecast.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Use camelCase for variable and function names in TypeScript
Enable strict mode in TypeScript and avoid unused locals or parameters

Files:

  • src/lib/cast.ts
  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
  • src/App.tsx
  • src/components/ChannelTable.tsx
  • src/components/CastMenu.tsx
  • src/hooks/useChromecast.ts
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/components/**/*.{ts,tsx}: Use functional components with hooks in React
Use Tailwind CSS for styling, avoid CSS-in-JS in React components
Use lucide-react for icons in React components
Use @tanstack/react-virtual for virtualized lists when displaying 1000+ items

Files:

  • src/components/ThumbnailPanel.tsx
  • src/components/StreamPlayer.tsx
  • src/components/ChannelTable.tsx
  • src/components/CastMenu.tsx
src-tauri/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src-tauri/src/**/*.rs: Use snake_case for variable and function names in Rust
Use 4-space indentation in Rust code
Use thiserror crate for error handling in Rust
Use serde for serialization in Rust code
Run cargo clean -p iptv-checker && cargo build in src-tauri/ before bun tauri dev after Rust code changes to ensure a fresh binary

Files:

  • src-tauri/src/commands/chromecast.rs
  • src-tauri/src/engine/chromecast.rs
  • src-tauri/src/engine/cast_proxy.rs

Comment thread src-tauri/src/commands/chromecast.rs Outdated
Comment thread src-tauri/src/engine/cast_proxy.rs Outdated
Comment thread src/components/ThumbnailPanel.tsx
Comment thread src/hooks/useChromecast.ts
cast_proxy.rs:
- Redact /cast/<token> and /seg/<base64> from request logs. The token is
  a bearer credential for the proxy and the base64 segment encodes the
  upstream URL — log access shouldn't equal stream access.
- Replace ad-hoc relative-path check in serve_remux_file with
  is_safe_remux_filename, which also rejects backslashes, drive letters,
  and UNC roots. On Linux, Path::components treats "C:\Windows\win.ini"
  as a single Normal component, so without the string-level scan the
  cast token holder could read arbitrary files via tmpdir.join(relative).

commands/chromecast.rs:
- Combine the same-device check and the take into one cast_state critical
  section. The worker self-cleanup (clear_app_state_if_uid_matches) only
  acquires cast_state, not cast_lifecycle_lock, so a 5s drive_session
  tick detecting a disconnect could null guard.session between a
  detached check and the follow-up take().expect("checked above") and
  panic the command thread.

useChromecast.ts:
- Stop clearing session in finally{}. If stopCast() rejects the receiver
  may still be playing — clearing locally would lie to the rest of the
  UI and break follow-up flows like cast-redirect that gate on
  isCastSessionActive.

ThumbnailPanel.tsx:
- Keep the inline cast row mounted while a cast session is active even
  with the lightbox open. Starting a cast from the lightbox unmounts the
  lightbox StreamPlayer, so without this fallback the only way to stop
  or retarget the cast was to manually close the lightbox.

24 cast_proxy tests pass, including 6 new ones for the new helpers.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/src/engine/cast_proxy.rs`:
- Around line 1172-1191: The manifest buffering currently uses
response.bytes().await after looks_like_m3u8, which can OOM if the upstream is
misclassified; change this to enforce a hard cap (e.g., 2 MiB) before fully
buffering by first checking Content-Length if present and rejecting if > cap,
otherwise read the body via a limited stream reader (bytes_stream() or similar)
accumulating up to the cap and aborting if exceeded; on overflow or read error,
log the error via log::warn! and return the same write_simple(socket, 502, ...)
path used for manifest read failures; update the code around is_m3u8,
response.bytes().await, and the block that produces body/body_str/rewritten so
rewrite_manifest only runs on safely capped content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 059f92ba-8d6e-41cb-b0d6-3c871fefca9f

📥 Commits

Reviewing files that changed from the base of the PR and between 98930f7 and 9ac9cd2.

📒 Files selected for processing (5)
  • src-tauri/src/commands/chromecast.rs
  • src-tauri/src/engine/cast_proxy.rs
  • src/components/ThumbnailPanel.tsx
  • src/hooks/useChromecast.ts
  • src/lib/cast.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build (windows-latest)
  • GitHub Check: Build (macos-latest)
  • GitHub Check: Build (windows-latest)
  • GitHub Check: Rust Tests
  • GitHub Check: Build (ubuntu-22.04)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Define agent interfaces and implementations in TypeScript for agent systems
Use clear naming conventions for agent classes and methods
Document agent behavior and responsibilities in comments or docstrings
Implement error handling in agent methods

Files:

  • src/components/ThumbnailPanel.tsx
  • src/lib/cast.ts
  • src/hooks/useChromecast.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Use camelCase for variable and function names in TypeScript
Enable strict mode in TypeScript and avoid unused locals or parameters

Files:

  • src/components/ThumbnailPanel.tsx
  • src/lib/cast.ts
  • src/hooks/useChromecast.ts
src/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/components/**/*.{ts,tsx}: Use functional components with hooks in React
Use Tailwind CSS for styling, avoid CSS-in-JS in React components
Use lucide-react for icons in React components
Use @tanstack/react-virtual for virtualized lists when displaying 1000+ items

Files:

  • src/components/ThumbnailPanel.tsx
src-tauri/src/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

src-tauri/src/**/*.rs: Use snake_case for variable and function names in Rust
Use 4-space indentation in Rust code
Use thiserror crate for error handling in Rust
Use serde for serialization in Rust code
Run cargo clean -p iptv-checker && cargo build in src-tauri/ before bun tauri dev after Rust code changes to ensure a fresh binary

Files:

  • src-tauri/src/commands/chromecast.rs
  • src-tauri/src/engine/cast_proxy.rs
🔇 Additional comments (10)
src-tauri/src/engine/cast_proxy.rs (3)

169-294: LGTM on the lifecycle, cookies, and IPv4 selection.

The shared reqwest::Client with cookie_store(true) correctly persists auth cookies across manifest and segment fetches, the detect_lan_ip IPv4-only selection avoids IPv6 advertising mismatch with the 0.0.0.0 bind, and cancel.cancel() on wait_for_manifest failure properly tears down the spawned ffmpeg worker plus its temp dir. The atomic remux/listener teardown via the cancel token is clean.


854-1010: LGTM on the request handler.

Token gate, OPTIONS preflight short-circuit, redact_cast_path on log emission, redirect-aware segment validation against either resolved_origin or upstream_url, and the 503 Remux not ready fallback when state hasn't been populated all look correct. The unit test at Line 1714 (segment_accepted_after_redirect_to_cdn) exercises the redirect-CDN regression cleanly.


1012-1110: LGTM — HEAD/Range and path-traversal validation.

is_safe_remux_filename rejects backslashes, colon (drive letter / UNC), absolute paths, and traversal segments before joining onto tmpdir, which closes the Windows-form bypass. The 206/416/200 branches and the is_head body-suppression are RFC-compliant and the unit tests exercise them.

src-tauri/src/commands/chromecast.rs (1)

27-156: LGTM — lifecycle is now correctly serialized and race-free.

cast_lifecycle_lock held across the whole sequence prevents interleaving with stop_cast, the same-device check-and-take is now atomic under one cast_state acquisition (no more expect("checked above") panic window vs. clear_app_state_if_uid_matches), the proxy-start-failure branch correctly restores the prior active+proxy so an existing cast keeps playing, and the swap-failure branch falls through to a fresh launch with state already cleared so the take in the else branch is a safe no-op. Prior proxy is shut down only after the new session is wired up so the receiver doesn't lose its current segment source mid-swap.

src/hooks/useChromecast.ts (2)

62-77: LGTM on stop() error handling.

Session is now only cleared on a successful backend stop. On failure we surface the error and rethrow without lying to downstream consumers (e.g. isCastSessionActive-gated cast-redirect flows), which was the prior bug.


92-109: LGTM on the listener.

Stale errors are cleared on healthy status transitions (connecting/loading/playing), stopped clears both session and error, and the explicit error branch retains its message. The mounted/cancelledRef guards correctly avoid post-unmount setState.

src/components/ThumbnailPanel.tsx (2)

385-404: LGTM — cast controls remain reachable from the lightbox.

Once a session is active, the inline CastMenu renders regardless of lightboxOpen, so casting started from inside the lightbox (which unmounts the lightbox StreamPlayer via onStopPlayer) still leaves the user with a visible Stop / retarget control. The early-return ordering correctly preserves the original "no local player + no cast" idle case.


205-208: LGTM on the memoized cast request.

Keying useMemo on result keeps castRequest reference-stable across parent re-renders, so any downstream useEffect in StreamPlayer that depends on castRequest won't tear down spuriously.

src/lib/cast.ts (2)

8-24: LGTM on detectStreamKind.

Both branches now strip query/fragment before matching .m3u8, eliminating the prior asymmetry. Defaulting unknown URLs to mpeg_ts is the right call given that IPTV endpoints frequently serve video/mp2t from extensionless paths — the cast proxy / ffprobe path will gracefully fall through if the upstream isn't actually TS.


36-40: LGTM on the active-session type guard.

Treating stopped and error as terminal states (and the type predicate narrowing to CastSession) gives consumers (App.tsx, CastMenu, StreamPlayer) a single source of truth for the "cast is live" check.

Comment thread src-tauri/src/engine/cast_proxy.rs
A response classified as HLS (URL ending in .m3u8 or matching content-type)
was buffered fully via response.bytes() before rewriting. Misconfigured or
malicious upstreams whose bodies are actually long-running MPEG-TS or live
streams could OOM the proxy. Stream into a 2 MiB cap and refuse upfront when
Content-Length advertises larger than the cap.
@kristofferR kristofferR mentioned this pull request May 7, 2026
17 tasks
@kristofferR kristofferR merged commit ff08a12 into main May 7, 2026
7 of 8 checks passed
@kristofferR kristofferR deleted the feat/chromecast-support branch May 7, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant