Add Chromecast support#192
Conversation
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.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesChromecast Integration
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
src-tauri/Cargo.tomlsrc-tauri/src/commands/chromecast.rssrc-tauri/src/commands/mod.rssrc-tauri/src/engine/cast_proxy.rssrc-tauri/src/engine/chromecast.rssrc-tauri/src/engine/ffmpeg.rssrc-tauri/src/engine/mod.rssrc-tauri/src/lib.rssrc-tauri/src/models/chromecast.rssrc-tauri/src/models/mod.rssrc-tauri/src/state.rssrc/components/StreamPlayer.tsxsrc/components/ThumbnailPanel.tsxsrc/hooks/useChromecast.tssrc/lib/tauri.tssrc/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.rssrc-tauri/src/commands/chromecast.rssrc-tauri/src/engine/mod.rssrc-tauri/src/models/mod.rssrc-tauri/src/engine/ffmpeg.rssrc-tauri/src/lib.rssrc-tauri/src/state.rssrc-tauri/src/engine/cast_proxy.rssrc-tauri/src/engine/chromecast.rssrc-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.tssrc/components/ThumbnailPanel.tsxsrc/components/StreamPlayer.tsxsrc/lib/tauri.tssrc/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.tssrc/components/ThumbnailPanel.tsxsrc/components/StreamPlayer.tsxsrc/lib/tauri.tssrc/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-virtualfor virtualized lists when displaying 1000+ items
Files:
src/components/ThumbnailPanel.tsxsrc/components/StreamPlayer.tsx
🔇 Additional comments (7)
src/lib/tauri.ts (1)
292-309: LGTM — wrapper signatures match the Rust commands.
discoverChromecasts,castToDevice,stopCast, andgetCastStatusfaithfully mirrordiscover_chromecasts,cast_to_device,stop_cast, andget_cast_statusper 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
chromecastsubmodule is exposed forlib.rsregistration.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.
CastStatestorage withOption<ActiveCastSession>/Option<CastProxyHandle>behindMutexis the right shape for a single-active-session model, andcommands/chromecast.rscorrectlytake()s out of the guard before calling the asyncstop()/shutdown()so the lock isn't held across awaits.src-tauri/src/engine/mod.rs (1)
1-3: LGTM.
cast_proxyandchromecastare correctly registered alphabetically alongside the existing engine modules.src/lib/types.ts (1)
427-460: ⚡ Quick winNo action needed—Rust models are already correctly configured with proper serde renaming.
The
chromecast.rsRust models insrc-tauri/src/models/already carry#[serde(rename_all = "camelCase")]onChromecastDevice,CastSession, andCastMediaRequeststructs. TheCastSessionStateandCastStreamKindenums 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 throughtauri::invokewill work correctly.> Likely an incorrect or invalid review comment.src-tauri/Cargo.toml (1)
42-44: ⚡ Quick winAll dependency versions are current and stable.
rust_cast0.21.0 is the latest stable release with thethread_safefeature present and not yanked.mdns-sd0.19.1 andlocal-ip-address0.6.12 are also current releases. No security advisories are present for any of these crates.
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.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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
📒 Files selected for processing (7)
src-tauri/src/commands/chromecast.rssrc-tauri/src/engine/cast_proxy.rssrc-tauri/src/engine/chromecast.rssrc-tauri/src/state.rssrc/components/StreamPlayer.tsxsrc/components/ThumbnailPanel.tsxsrc/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.rssrc-tauri/src/engine/chromecast.rssrc-tauri/src/engine/cast_proxy.rssrc-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.tsxsrc/components/StreamPlayer.tsxsrc/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.tsxsrc/components/StreamPlayer.tsxsrc/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-virtualfor virtualized lists when displaying 1000+ items
Files:
src/components/ThumbnailPanel.tsxsrc/components/StreamPlayer.tsx
🔇 Additional comments (7)
src/hooks/useChromecast.ts (1)
84-101: Status listener correctly handles all session states.The branching is sound:
stoppedclears both session and error,errorpopulates 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
castMenuOpenis true (via thescheduleHideguard at line 76 and the dedicated effect at 87-95), the outside-click handler is scoped to the open state, andhandleCastDevicepauses 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 andcastRequestmemoization look correct.The fallback branch now strips query/fragment symmetrically before testing the extension, and
castRequestis memoized on the actual inputs so prop identity stays stable acrossThumbnailPanelre-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::Clientper session withcookie_store(true)and threading it throughhandle_connection/serve_upstreamcorrectly preserves auth cookies across manifest and segment requests. The earlycancel.cancel()onwait_for_playlistfailure prevents orphaned ffmpeg/temp dirs, andis_target_allowedtogether 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 keepsAppStateconsistent on both stop paths.The
manual_stopflag plus the uid-guardedclear_app_state_if_uid_matchescorrectly distinguishes the explicit stop path (caller already clearedAppState) from self-exit onConnection::Close/ receive errors, and the uid match prevents a successor session from being clobbered. Combined with theStoppedemit 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.
CastStatecorrectly couples the proxy handle with the active session, andcast_lifecycle_lockprovides the dedicated mutex needed to serialize start/stop transitions incommands/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_lockacross the full teardown →cast_proxy::start→chromecast::start_session→ store sequence (and the symmetricstop_castpath) closes the previous interleaving window. Releasing the innercast_statelock 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.
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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
src-tauri/Cargo.tomlsrc-tauri/src/engine/cast_proxy.rssrc-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.rssrc-tauri/src/engine/cast_proxy.rs
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).
There was a problem hiding this comment.
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 winNever 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 winBracket IPv6 hosts in the advertised cast URL.
detect_lan_ip()returnsIpAddr, but these URLs are still built ashttp://{lan_ip}:{port}. For IPv6 that produces an invalid authority, and it also mismatches the current IPv4-only bind on0.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 liftDon't advertise
HEAD/Rangeuntil 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. AdvertisingGET, HEAD, OPTIONSplusRangesupport 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 winRedact the cast token and encoded upstream URL from request logs.
pathincludes/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
📒 Files selected for processing (10)
src-tauri/src/commands/chromecast.rssrc-tauri/src/engine/cast_proxy.rssrc-tauri/src/engine/chromecast.rssrc/App.tsxsrc/components/CastMenu.tsxsrc/components/ChannelTable.tsxsrc/components/StreamPlayer.tsxsrc/components/ThumbnailPanel.tsxsrc/hooks/useChromecast.tssrc/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.tssrc/components/ThumbnailPanel.tsxsrc/components/StreamPlayer.tsxsrc/App.tsxsrc/components/ChannelTable.tsxsrc/components/CastMenu.tsxsrc/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.tssrc/components/ThumbnailPanel.tsxsrc/components/StreamPlayer.tsxsrc/App.tsxsrc/components/ChannelTable.tsxsrc/components/CastMenu.tsxsrc/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-virtualfor virtualized lists when displaying 1000+ items
Files:
src/components/ThumbnailPanel.tsxsrc/components/StreamPlayer.tsxsrc/components/ChannelTable.tsxsrc/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.rssrc-tauri/src/engine/chromecast.rssrc-tauri/src/engine/cast_proxy.rs
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
src-tauri/src/commands/chromecast.rssrc-tauri/src/engine/cast_proxy.rssrc/components/ThumbnailPanel.tsxsrc/hooks/useChromecast.tssrc/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.tsxsrc/lib/cast.tssrc/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.tsxsrc/lib/cast.tssrc/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-virtualfor 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.rssrc-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::Clientwithcookie_store(true)correctly persists auth cookies across manifest and segment fetches, thedetect_lan_ipIPv4-only selection avoids IPv6 advertising mismatch with the0.0.0.0bind, andcancel.cancel()onwait_for_manifestfailure 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_originorupstream_url, and the503 Remux not readyfallback 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_filenamerejects backslashes, colon (drive letter / UNC), absolute paths, and traversal segments before joining ontotmpdir, which closes the Windows-form bypass. The 206/416/200 branches and theis_headbody-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_lockheld across the whole sequence prevents interleaving withstop_cast, the same-device check-and-take is now atomic under onecast_stateacquisition (no moreexpect("checked above")panic window vs.clear_app_state_if_uid_matches), the proxy-start-failure branch correctly restores the prioractive+proxyso 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),stoppedclears both session and error, and the expliciterrorbranch 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
CastMenurenders regardless oflightboxOpen, so casting started from inside the lightbox (which unmounts the lightboxStreamPlayerviaonStopPlayer) 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
useMemoonresultkeepscastRequestreference-stable across parent re-renders, so any downstreamuseEffectinStreamPlayerthat depends oncastRequestwon't tear down spuriously.src/lib/cast.ts (2)
8-24: LGTM ondetectStreamKind.Both branches now strip query/fragment before matching
.m3u8, eliminating the prior asymmetry. Defaulting unknown URLs tompeg_tsis the right call given that IPTV endpoints frequently servevideo/mp2tfrom 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
stoppedanderroras terminal states (and the type predicate narrowing toCastSession) gives consumers (App.tsx,CastMenu,StreamPlayer) a single source of truth for the "cast is live" check.
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.
Summary
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 theStreamPlayeroverlay (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.-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.engine/chromecast.rs(discovery + session worker bridged viatokio::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 inAppState.cast_state; new sessions tear down prior ones cleanly.useChromecasthook (devices/session/error state,cast://statusevent subscription) + cast button UI inStreamPlayer.tsx.Ref #191. HEVC transcode fallback for old Chromecast dongles is intentionally deferred (Phase 3 in the original plan).
Test plan
curl http://<host>:<port>/cast/<wrong-token>/stream→ must 403Summary by CodeRabbit