Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
7acec98
Merge remote-tracking branch 'origin/feat/remote-core' into feat/remo…
MaxMB15 Oct 18, 2026
fadea1c
Merge branch 'feat/discovery' into feat/remote-target
MaxMB15 Oct 18, 2026
754616d
feat(remote): run this device as a remote-control target on macOS and…
MaxMB15 Oct 18, 2026
340bdd4
Merge branch 'feat/discovery' into feat/remote-target
MaxMB15 Oct 18, 2026
db23f71
test(remote): drive the app's remote commands and events over loopback
MaxMB15 Oct 18, 2026
cf5f3be
feat(remote): control another Cinette device from macOS and Linux
MaxMB15 Oct 18, 2026
6a6fd08
Merge branch 'feat/discovery' into feat/remote-target
MaxMB15 Oct 18, 2026
efc4c9f
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 18, 2026
5badce6
feat(remote): add the device picker, consent dialog and remote contro…
MaxMB15 Oct 18, 2026
69bfee3
feat(remote): add remote control settings
MaxMB15 Oct 18, 2026
6eff837
test(remote): kill the UI mutants the first run missed
MaxMB15 Oct 18, 2026
fd02bcc
test(remote): read the catalog mock's first call through vi.mocked
MaxMB15 Oct 18, 2026
25e9c4b
test(remote): cover loading states, the picker's closed start and its…
MaxMB15 Oct 18, 2026
bd956fa
test(remote): catch what the mutation run missed in the target
MaxMB15 Oct 18, 2026
65e0f1d
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 18, 2026
3034dde
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 18, 2026
0ee0d2a
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 18, 2026
b6a9e69
test(remote): kill the settings card's mutants
MaxMB15 Oct 18, 2026
ef1a624
test(remote): check the discovery button's look
MaxMB15 Oct 18, 2026
197f15c
docs: add remote control to the changelog
MaxMB15 Oct 18, 2026
291f3a6
fix(remote): build the remote commands for iOS without unused-code er…
MaxMB15 Oct 18, 2026
7d0628d
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 18, 2026
d4e2ace
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 18, 2026
9b55ab8
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 18, 2026
d21ed0a
feat(remote): load a series' episodes from its provider when a contro…
MaxMB15 Oct 18, 2026
3e9ba4c
test(remote): catch the target's frontend mutants that CI found
MaxMB15 Oct 18, 2026
bac126e
test(remote): catch the controller mutants that CI found
MaxMB15 Oct 18, 2026
292b85f
Merge remote-tracking branch 'origin/feat/remote-core' into feat/remo…
MaxMB15 Oct 18, 2026
26d37d1
fix(remote): adapt the target to the core's review fixes
MaxMB15 Oct 18, 2026
e34e22b
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 18, 2026
c6f44aa
fix(remote): adapt the controller to the core's review fixes
MaxMB15 Oct 18, 2026
36de9ac
Merge commit '722465d' into feat/remote-target
MaxMB15 Oct 18, 2026
fbd73d2
feat(remote): say why Cinette looks on the local network on macOS
MaxMB15 Oct 18, 2026
d978996
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 18, 2026
27408db
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 18, 2026
ba682ab
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 18, 2026
3674d2f
feat(remote): say in the picker when macOS keeps Cinette off the loca…
MaxMB15 Oct 18, 2026
a98ef1f
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 18, 2026
06dec20
feat(remote): say plainly what Always allow lets in
MaxMB15 Oct 18, 2026
1e83b65
Merge branch 'feat/remote-groups-cursor' into feat/remote-target
MaxMB15 Oct 18, 2026
a8feb68
feat(remote): answer groups a page at a time
MaxMB15 Oct 18, 2026
28630dc
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 18, 2026
5806365
feat(remote): send a groups query with its limit
MaxMB15 Oct 18, 2026
e42b2f9
Merge branch 'feat/remote-groups-cursor' into feat/remote-target
MaxMB15 Oct 18, 2026
e8755fb
fix(remote): play a remote load through the player page when none is …
MaxMB15 Oct 18, 2026
7ea3422
fix(remote): harden the target against slow, busy and stale controllers
MaxMB15 Oct 18, 2026
6e1a3d7
test(remote): catch the mutants the hardening left
MaxMB15 Oct 18, 2026
a06b17f
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 18, 2026
0b264ce
fix(remote): tie the controller's commands and events to one link
MaxMB15 Oct 18, 2026
63f2ef6
fix(remote): load only logos on public hosts, and catch what the muta…
MaxMB15 Oct 18, 2026
16eb8cb
test(remote): wait for the player page without leftovers to clean up
MaxMB15 Oct 19, 2026
2b51bb4
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 19, 2026
554efd3
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 19, 2026
7270ddb
fix(remote): page the remote catalog safely and open series from search
MaxMB15 Oct 19, 2026
28359a1
fix(remote): make the remote control screens safer and clearer to use
MaxMB15 Oct 19, 2026
ecaf10e
test(remote): catch the UI mutants the review fixes left
MaxMB15 Oct 19, 2026
56ac594
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 19, 2026
a1afbae
fix(remote): keep remote control settings saves from undoing each other
MaxMB15 Oct 19, 2026
cefafb7
test(remote): catch the settings mutants the save queue left
MaxMB15 Oct 19, 2026
9604001
Merge branch 'feat/remote-groups-cursor' into feat/remote-target
MaxMB15 Oct 19, 2026
08421a8
Merge remote-tracking branch 'origin/feat/discovery' into feat/remote…
MaxMB15 Oct 19, 2026
3879ec4
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 19, 2026
4b2a91c
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 19, 2026
313a9f4
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 19, 2026
35dbfee
docs(changelog): put remote control back under Unreleased
MaxMB15 Oct 19, 2026
3aba723
Merge branch 'feat/remote-groups-cursor' into feat/remote-target
MaxMB15 Oct 19, 2026
9e497bf
fix(remote): answer groups with the core's group page
MaxMB15 Oct 19, 2026
533458b
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 19, 2026
b5d8915
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 19, 2026
5df0e9f
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 19, 2026
07fe877
fix(remote): build the logo checks only where a controller runs
MaxMB15 Oct 19, 2026
f0042b9
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 19, 2026
fe85799
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 19, 2026
005fc57
fix(remote): start remote control without ??=, which the React Compil…
MaxMB15 Oct 19, 2026
0d2c9f3
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 19, 2026
624d827
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 19, 2026
e1f333c
Merge remote-tracking branch 'origin/dev' into feat/remote-settings
MaxMB15 Oct 19, 2026
3300bd4
Merge remote-tracking branch 'origin/dev' into feat/remote-target
MaxMB15 Oct 19, 2026
ab1ec08
fix: keep dev's Cargo.lock versions and add only the new crates
MaxMB15 Oct 19, 2026
c130eed
fix(remote): keep the target to the local network and its limits
MaxMB15 Oct 19, 2026
93b871d
Merge remote-tracking branch 'origin/dev' into feat/remote-target
MaxMB15 Oct 19, 2026
202e21b
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 19, 2026
e65d8ee
fix(remote): show the code a restarted target asks with, and get past…
MaxMB15 Oct 19, 2026
e4f60e2
test(remote): check every field of the idle state a closing player sends
MaxMB15 Oct 19, 2026
e97b65f
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 19, 2026
d5a3888
fix(remote): leave only the link the webview names
MaxMB15 Oct 19, 2026
25a83f2
test(remote): read the last idle state without Array.at, which the ap…
MaxMB15 Oct 19, 2026
a1ec018
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 19, 2026
415d035
fix(remote): keep a newer state over a load's stand-in, and tidy the …
MaxMB15 Oct 19, 2026
f0bb688
Merge remote-tracking branch 'origin/dev' into feat/remote-target
MaxMB15 Oct 19, 2026
d279051
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 19, 2026
258790f
test(remote): catch the load mutants on a destination with no state yet
MaxMB15 Oct 20, 2026
9013e0e
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 20, 2026
0706c12
fix(remote): keep names from reordering the text around them, and sea…
MaxMB15 Oct 20, 2026
0eb7e89
test(remote): check the local network command, and skip its mutant th…
MaxMB15 Oct 20, 2026
3798465
fix(remote): send states in order, catch the fetch bucket's mutants
MaxMB15 Oct 20, 2026
8efec25
Merge branch 'feat/remote-target' into feat/remote-controller
MaxMB15 Oct 20, 2026
d778010
fix(remote): pass over any address that isn't the device, not only a …
MaxMB15 Oct 20, 2026
179a23d
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 20, 2026
ab2083e
fix(remote): make the remote control screens harder to answer by acci…
MaxMB15 Oct 20, 2026
8f0795a
test(remote): check that a drop with no session yet ends at once
MaxMB15 Oct 20, 2026
90e792e
Merge branch 'feat/remote-controller' into feat/remote-ui
MaxMB15 Oct 20, 2026
059e4c9
test(remote): catch the UI mutants the review fixes left
MaxMB15 Oct 20, 2026
8a4aff6
chore(remote): skip the pending consent clock mutant no test can tell…
MaxMB15 Oct 20, 2026
fc5255a
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 20, 2026
3eab4cf
fix(remote): keep the remote control settings truthful about what the…
MaxMB15 Oct 20, 2026
e60a1b1
test(remote): type the answers a settings test holds back
MaxMB15 Oct 20, 2026
0ffd779
test(remote): catch the settings mutants the review fixes left
MaxMB15 Oct 20, 2026
924dbc6
fix(remote): show a failed leave after the bar has gone, and let go o…
MaxMB15 Oct 20, 2026
23533a4
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 20, 2026
c488d26
fix(remote): take only the new identity from a reset's answer
MaxMB15 Oct 20, 2026
c1f8868
test(remote): check that the ended notice isn't raised
MaxMB15 Oct 20, 2026
5384820
Merge branch 'feat/remote-ui' into feat/remote-settings
MaxMB15 Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .cargo/mutants.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# SPDX-License-Identifier: GPL-3.0-only
# Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.

Expand Down Expand Up @@ -72,6 +72,10 @@
# Asks macOS whether it refuses local network access. On Linux, where
# the mutation runs build, it's always false.
'src-tauri/src/cast/mod\.rs:.*\blocal_network_denied\b',
# These bind remote control's real port and advertise this machine on the
# network. The tests start the target on loopback with start_on instead.
'src-tauri/src/remote/service\.rs:.*\b(start|advertise_on_network)\b',
'src-tauri/src/remote/target\.rs:.*\bbind_default\b',
]

# Without --in-place, cargo-mutants copies the tree to build each mutant. This
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to Cinette, formerly called Max Video Player, are listed her

- On iPhone and iPad, the player has an AirPlay button when an AirPlay TV is in reach. It sends the channel, movie or episode that's playing to the TV, which plays it straight from your provider, and a movie or episode carries on from where it was. Streams the TV can't play as they are, such as MKV files and most raw MPEG-TS channels, stay on the iPhone and Cinette says why (#323).
- The player answers a keyboard's media keys. Play/pause, play and pause work as you'd expect, fast forward and rewind skip by the lengths set in Settings, and next and previous change the episode (#312).
- On macOS and Linux, one Cinette can control another on the same network. Choose Devices in the sidebar and pick the other device; it plays there, and this one shows what's playing, with play, pause, stop and that device's channels, movies and series to choose from. Under Only when allowed, the default, the other device asks first, and both screens show the same code so you can tell your own request from someone else's. Settings > Remote control sets whether others can find this device, who may control it (Always allow, Only when allowed, Never allow), its name, lists the devices controlling it, each with Disconnect, and can give this device a new identity, which ends every session. macOS asks once whether Cinette may use the local network; if that was refused, the device list says where to allow it. Nothing leaves your network, and no stream address, login or playlist setting is sent to the other device (#317, #318, #319, #321, #322).

### Changed

Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions apps/player/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ chrono = { version = "0.4", features = ["serde"] }
reqwest = { workspace = true, features = ["json", "stream"] }
futures-util = "0.3"
tempfile = "3"
# Reads the logo addresses other Cinette devices send. The same version as mvp-core's.
url = "2"

[dev-dependencies]
# The mock runtime, so tests can build an app and call commands without a window.
Expand All @@ -42,10 +44,19 @@ libc = "0.2"
zbus = "5"
# DNS-SD for Cinette devices and Cast receivers, without needing Avahi.
mdns-sd = { version = "0.21", default-features = false }
# The interfaces' subnets, so remote control only takes local peers. The
# same version as mdns-sd's.
if-addrs = "0.15"
# The remote-control listener's dual-stack socket. The same version as tokio's.
socket2 = "0.6"

[target.'cfg(target_os = "macos")'.dependencies]
# poll and the socket types for Bonjour's dns_sd.h.
libc = "0.2"
# The interfaces' subnets, so remote control only takes local peers.
if-addrs = "0.15"
# The remote-control listener's dual-stack socket. The same version as tokio's.
socket2 = "0.6"

[target.'cfg(target_os = "ios")'.dependencies]
oslog = { version = "0.2", default-features = false }
Expand Down
2 changes: 1 addition & 1 deletion apps/player/src-tauri/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>Cinette finds Chromecasts, TVs and other Cinette devices on your local network so you can play on them.</string>
<string>Cinette finds Chromecasts, TVs and other Cinette devices on your local network, so you can play on them or control them.</string>
<key>NSBonjourServices</key>
<array>
<string>_cinette._tcp</string>
Expand Down
15 changes: 12 additions & 3 deletions apps/player/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,24 @@ pub async fn update_provider(
pub async fn get_xtream_series_episodes(
state: State<'_, AppState>,
channel_id: String,
) -> Result<Vec<Channel>, String> {
fetch_series_episodes(&state.cache, &channel_id).await
}

/// The episodes of the Xtream series `channel_id`, from its provider. The
/// cache is only locked while the series and its provider are read.
pub(crate) async fn fetch_series_episodes(
cache: &Mutex<CacheStore>,
channel_id: &str,
) -> Result<Vec<Channel>, String> {
let (channel, provider) = {
let cache = state.cache.lock().map_err(|e| e.to_string())?;
let cache = cache.lock().map_err(|e| e.to_string())?;
let ch = cache
.get_channel_by_id(&channel_id)
.get_channel_by_id(channel_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("Channel not found: {channel_id}"))?;
let p = cache
.get_provider_for_channel(&channel_id)
.get_provider_for_channel(channel_id)
.map_err(|e| e.to_string())?
.ok_or("Provider not found for channel")?;
(ch, p)
Expand Down
18 changes: 17 additions & 1 deletion apps/player/src-tauri/src/commands/tests/arguments.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,21 @@
},
"stop_recording": { "required": ["id"] },
"get_cast_receivers": {},
"hold_cast_receivers": { "required": ["ids"] }
"hold_cast_receivers": { "required": ["ids"] },
"remote_get_settings": {},
"remote_set_settings": { "required": ["settings"] },
"remote_reset_identity": {},
"remote_answer_consent": { "required": ["request", "allow"] },
"remote_sessions": {},
"remote_pending_consent": {},
"remote_disconnect": { "required": ["sessionId"] },
"remote_set_state": { "required": ["state"] },
"remote_finish_command": { "required": ["token", "outcome"] },
"remote_devices": {},
"remote_local_network_denied": {},
"remote_connect": { "required": ["deviceId", "link"] },
"remote_link": {},
"remote_leave": { "required": ["link"] },
"remote_command": { "required": ["command", "link"] },
"remote_catalog": { "required": ["catalogQuery", "link"] }
}
11 changes: 10 additions & 1 deletion apps/player/src-tauri/src/commands/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,16 @@ fn sample(argument: &str) -> Value {
| "source" | "generation" => {
json!(1)
}
"keep" => json!(true),
"keep" | "allow" => json!(true),
"request" | "token" => json!(1),
"outcome" => json!("ok"),
"command" => json!({ "action": "stop" }),
"catalogQuery" => json!({ "kind": "groups", "contentType": "live", "limit": 100 }),
"settings" => json!({ "discoverable": true, "remoteControl": "ask", "deviceName": null }),
"state" => json!({
"status": "idle", "item": null, "position": 0, "duration": 0, "volume": null,
"subtitleId": null, "audioId": null, "error": null
}),
"episode" | "channel" => serde_json::to_value(Channel::default()).unwrap(),
_ => json!("x"),
}
Expand Down
24 changes: 24 additions & 0 deletions apps/player/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
mod identifier;
mod logging;
mod progress;
mod remote;
mod streams;
#[cfg(test)]
mod android_project_tests;
Expand All @@ -25,6 +26,7 @@
mod test_support;

use commands::AppState;
use remote::commands as remote_commands;
use mvp_core::cache::store::CacheStore;
use std::sync::Mutex;
use tauri::Manager;
Expand Down Expand Up @@ -144,6 +146,22 @@
}

register_commands![
remote_commands::remote_get_settings,
remote_commands::remote_set_settings,
remote_commands::remote_reset_identity,
remote_commands::remote_answer_consent,
remote_commands::remote_sessions,
remote_commands::remote_pending_consent,
remote_commands::remote_disconnect,
remote_commands::remote_set_state,
remote_commands::remote_finish_command,
remote_commands::remote_devices,
remote_commands::remote_local_network_denied,
remote_commands::remote_connect,
remote_commands::remote_link,
remote_commands::remote_leave,
remote_commands::remote_command,
remote_commands::remote_catalog,
commands::load_m3u_playlist,
commands::load_m3u_file,
commands::load_xtream_provider,
Expand Down Expand Up @@ -356,66 +374,72 @@
app.manage(crate::streams::StreamsState::default());
crate::downloads::manager::restore_settings(app.handle());

// Listens for other Cinette devices and advertises this one.
#[cfg(any(target_os = "linux", target_os = "macos"))]
crate::remote::start(app.handle(), app_dir.clone());

#[cfg(desktop)]
{
use tauri_plugin_shell::ShellExt;
crate::downloads::retention::start(app.handle());
match app.shell().sidecar(crate::downloads::FFMPEG_SIDECAR) {
Ok(_) => {
tracing::info!("ffmpeg sidecar resolved");
// Start or miss the recordings that came due while the
// app was closed, before downloads take the connections.
let next_check = crate::downloads::recordings::run_check(app.handle());
crate::downloads::recordings::start(app.handle(), next_check);
// Start whatever is still queued from the last run.
if let Err(e) = crate::downloads::manager::pump(app.handle()) {
tracing::warn!("Failed to start queued downloads: {e}");
}
}
Err(e) => tracing::error!("ffmpeg sidecar missing: {e}"),
}
}

// Set the WebView's native background to fully transparent so the
// video surface (positioned below the WebView) is visible through it.
// CSS `background: transparent` alone is not sufficient on all Linux
// compositors (e.g. Pop!_OS/COSMIC with WebKit2GTK DMABUF renderer).
#[cfg(any(target_os = "macos", target_os = "linux"))]
if let Some(ref window) = app.get_webview_window("main") {
use tauri::webview::Color;
if let Err(e) = window.set_background_color(Some(Color(0, 0, 0, 0))) {
tracing::warn!("Failed to set WebView background transparent: {}", e);
}
}

#[cfg(debug_assertions)]
{
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
}

#[cfg(any(target_os = "macos", target_os = "linux"))]
{
if let Some(window) = app.get_webview_window("main") {
let handle = app.handle().clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::Resized(size) = event
&& let Some(state) = handle.try_state::<tauri_plugin_mpv::MpvState>()
{
state.resize(size.width, size.height);
}
});
}
}

Ok(())
})
.build(context)
.expect("error while building tauri application")
.run(|app, event| {
if let tauri::RunEvent::Exit = event {
crate::downloads::manager::stop_all_for_exit(app);
crate::downloads::recordings::stop_all_for_exit(app);
#[cfg(any(target_os = "linux", target_os = "macos"))]
crate::remote::stop_for_exit(app);

Check warning on line 442 in apps/player/src-tauri/src/lib.rs

View workflow job for this annotation

GitHub Actions / plan

Function left out

scripts/changed-code-exclude.toml, entry 7, leaves out lines 377-380, 441-442 of run in apps/player/src-tauri/src/lib.rs, which this change adds or changes, since run matches the function's name. Check that the entry means to leave out this function.
}
});
}
Expand Down
Loading
Loading