@officebeats/matrix-iptv-cli 4.0.4 → 4.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -10
- package/bin/cli.js +73 -40
- package/matrix-iptv.exe +0 -0
- package/old_player.rs +404 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,10 +13,16 @@ This project is being actively optimized by **Ernesto "Beats"** with a primary f
|
|
|
13
13
|
## 📸 Gallery
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
<img src="./assets/
|
|
17
|
-
<img src="./assets/
|
|
16
|
+
<img src="./assets/selector.png" width="48%" />
|
|
17
|
+
<img src="./assets/categories.png" width="48%" />
|
|
18
18
|
<br />
|
|
19
|
-
<img src="./assets/
|
|
19
|
+
<img src="./assets/news.png" width="48%" />
|
|
20
|
+
<img src="./assets/nba.png" width="48%" />
|
|
21
|
+
<br />
|
|
22
|
+
<img src="./assets/vod.png" width="48%" />
|
|
23
|
+
<img src="./assets/playback.png" width="48%" />
|
|
24
|
+
<br />
|
|
25
|
+
<img src="./assets/movie_details.png" width="98%" />
|
|
20
26
|
</p>
|
|
21
27
|
|
|
22
28
|
---
|
|
@@ -45,9 +51,8 @@ If you have Node.js installed, this is the easiest way to stay updated:
|
|
|
45
51
|
npm install -g @officebeats/matrix-iptv-cli
|
|
46
52
|
```
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Alternatively, use these platform-specific one-liners:
|
|
54
|
+
<details>
|
|
55
|
+
<summary><strong>🚀 One-Click Install Scripts (Click to Expand)</strong></summary>
|
|
51
56
|
|
|
52
57
|
#### **Windows**
|
|
53
58
|
|
|
@@ -61,6 +66,8 @@ powershell -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.c
|
|
|
61
66
|
curl -sSL https://raw.githubusercontent.com/officebeats/matrix-iptv/main/install.sh -o install_matrix.sh && bash install_matrix.sh && rm install_matrix.sh
|
|
62
67
|
```
|
|
63
68
|
|
|
69
|
+
</details>
|
|
70
|
+
|
|
64
71
|
---
|
|
65
72
|
|
|
66
73
|
## 🎬 How to Run
|
|
@@ -125,6 +132,15 @@ We leverage advanced **MPV** flags to ensure professional-level video quality ev
|
|
|
125
132
|
|
|
126
133
|
---
|
|
127
134
|
|
|
135
|
+
## ⚙️ Advanced Features
|
|
136
|
+
|
|
137
|
+
Matrix IPTV CLI provides several core utilities to manage complex setups safely:
|
|
138
|
+
|
|
139
|
+
- **VLC Fallback Integration**: While MPV provides the ultimate playback tier, you can configure the app to fall back to launching VLC Media Player instead, maximizing compatibility.
|
|
140
|
+
- **Dynamic Background Sync**: Automatically handles large playlist parsing without hanging the UI, providing fluid progress updates and ETAs while it indexes tens of thousands of channels.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
128
144
|
## ⌨️ Common Controls
|
|
129
145
|
|
|
130
146
|
| Key | Action |
|
|
@@ -136,10 +152,10 @@ We leverage advanced **MPV** flags to ensure professional-level video quality ev
|
|
|
136
152
|
| **`v`** | **Toggle Favorite** |
|
|
137
153
|
| **`j` / `↓`** | Move Down |
|
|
138
154
|
| **`k` / `↑`** | Move Up |
|
|
139
|
-
| **`g`** | **
|
|
140
|
-
| **`G`** | **
|
|
141
|
-
| **`0`-`9`** | **Jump to Item
|
|
142
|
-
| **`m`** | **Playlist Mode**
|
|
155
|
+
| **`g`** | **Toggle Grid View / Group Menu** |
|
|
156
|
+
| **`G`** | **Manage Groups / Jump to Bottom (VOD)** |
|
|
157
|
+
| **`0`-`9`** | **Jump to Item / Switch Account (Home)** |
|
|
158
|
+
| **`m`** | **Settings: Playlist Mode** |
|
|
143
159
|
| **`x`** | **Settings** |
|
|
144
160
|
| **`n`** | **New Playlist** (Home Screen) |
|
|
145
161
|
| **`e`** | **Edit Playlist** (Home Screen) |
|
package/bin/cli.js
CHANGED
|
@@ -16,30 +16,38 @@ const platformMap = {
|
|
|
16
16
|
darwin: "macos",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.on(
|
|
19
|
+
const axios = require("axios");
|
|
20
|
+
|
|
21
|
+
async function download(url, dest) {
|
|
22
|
+
const writer = fs.createWriteStream(dest);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await axios({
|
|
26
|
+
method: 'get',
|
|
27
|
+
url: url,
|
|
28
|
+
responseType: 'stream',
|
|
29
|
+
headers: {
|
|
30
|
+
'Cache-Control': 'no-cache',
|
|
31
|
+
'User-Agent': 'matrix-iptv-cli-updater'
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
response.data.pipe(writer);
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
writer.on('finish', resolve);
|
|
39
|
+
writer.on('error', (err) => {
|
|
39
40
|
fs.unlink(dest, () => {});
|
|
40
41
|
reject(err);
|
|
41
42
|
});
|
|
42
|
-
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
fs.unlink(dest, () => {});
|
|
46
|
+
if (err.response && err.response.status === 404) {
|
|
47
|
+
throw new Error(`Asset not found (404). This usually means the GitHub release exists but the binary hasn't been uploaded yet.`);
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
async function performUpdate() {
|
|
@@ -75,22 +83,18 @@ async function performUpdate() {
|
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
// Replace old binary
|
|
78
|
-
// On Windows, sometimes the OS still has a lock for a split second or unlinking is restricted
|
|
79
86
|
let attempts = 0;
|
|
80
|
-
const maxAttempts =
|
|
87
|
+
const maxAttempts = 15;
|
|
81
88
|
while (attempts < maxAttempts) {
|
|
82
89
|
try {
|
|
83
90
|
if (fs.existsSync(binaryPath)) {
|
|
84
91
|
if (os.platform() === "win32") {
|
|
85
|
-
// "Rename-out" strategy for Windows: move the file to a temporary name first
|
|
86
|
-
// This is often allowed even if the file is lazily being released by the OS
|
|
87
92
|
const oldPath = binaryPath + ".old." + Date.now();
|
|
88
93
|
fs.renameSync(binaryPath, oldPath);
|
|
89
|
-
// Optionally try to delete the old one, but don't fail if we can't
|
|
90
94
|
try {
|
|
91
95
|
fs.unlinkSync(oldPath);
|
|
92
96
|
} catch (e) {
|
|
93
|
-
//
|
|
97
|
+
// Mark for deletion on next reboot or just ignore
|
|
94
98
|
}
|
|
95
99
|
} else {
|
|
96
100
|
fs.unlinkSync(binaryPath);
|
|
@@ -100,31 +104,42 @@ async function performUpdate() {
|
|
|
100
104
|
break;
|
|
101
105
|
} catch (e) {
|
|
102
106
|
attempts++;
|
|
103
|
-
console.log(`[!] Retry ${attempts}/${maxAttempts}: ${e.message}`);
|
|
104
107
|
if (attempts === maxAttempts) throw e;
|
|
105
|
-
|
|
106
|
-
await new Promise((r) => setTimeout(r, 500 + attempts * 200));
|
|
108
|
+
await new Promise((r) => setTimeout(r, 1000 + attempts * 500));
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
|
|
110
|
-
// Also clear quarantine on final binary path
|
|
111
112
|
if (os.platform() === "darwin") {
|
|
112
113
|
try {
|
|
113
114
|
const { execSync } = require("child_process");
|
|
114
|
-
execSync(
|
|
115
|
-
`xattr -d com.apple.quarantine "${binaryPath}" 2>/dev/null || true`
|
|
116
|
-
);
|
|
115
|
+
execSync(`xattr -d com.apple.quarantine "${binaryPath}" 2>/dev/null || true`);
|
|
117
116
|
} catch (e) {}
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
console.log(`[+] Update complete. Rebooting system...\n`);
|
|
120
|
+
|
|
121
|
+
if (os.platform() === "win32") {
|
|
122
|
+
const batchScript = `
|
|
123
|
+
@echo off
|
|
124
|
+
timeout /t 2 /nobreak > nul
|
|
125
|
+
start "" "${binaryPath}" %*
|
|
126
|
+
del "%~f0"
|
|
127
|
+
`;
|
|
128
|
+
const batchPath = path.join(os.tmpdir(), "matrix-relaunch.bat");
|
|
129
|
+
fs.writeFileSync(batchPath, batchScript);
|
|
130
|
+
spawn("cmd.exe", ["/c", batchPath, ...process.argv.slice(2)], {
|
|
131
|
+
detached: true,
|
|
132
|
+
stdio: "ignore",
|
|
133
|
+
}).unref();
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
121
136
|
} catch (err) {
|
|
122
137
|
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
123
138
|
throw err;
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
|
|
127
|
-
function launchApp() {
|
|
142
|
+
function launchApp(isUpdateRelaunch = false) {
|
|
128
143
|
if (!fs.existsSync(binaryPath)) {
|
|
129
144
|
console.error("\n❌ Matrix IPTV binary not found.");
|
|
130
145
|
console.log(
|
|
@@ -133,12 +148,27 @@ function launchApp() {
|
|
|
133
148
|
process.exit(1);
|
|
134
149
|
}
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
let child;
|
|
152
|
+
try {
|
|
153
|
+
child = spawn(binaryPath, process.argv.slice(2), {
|
|
154
|
+
stdio: "inherit",
|
|
155
|
+
windowsHide: false,
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (isUpdateRelaunch && (err.code === "EBUSY" || err.code === "EACCES")) {
|
|
159
|
+
console.log(`[*] Executable locked by OS. Retrying in 2 seconds...`);
|
|
160
|
+
setTimeout(() => launchApp(true), 2000);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
throw err;
|
|
164
|
+
}
|
|
140
165
|
|
|
141
166
|
child.on("error", (err) => {
|
|
167
|
+
if (isUpdateRelaunch && (err.code === "EBUSY" || err.code === "EACCES")) {
|
|
168
|
+
console.log(`[*] Executable locked by OS. Retrying in 2 seconds...`);
|
|
169
|
+
setTimeout(() => launchApp(true), 2000);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
142
172
|
console.error("Failed to start Matrix IPTV:", err);
|
|
143
173
|
process.exit(1);
|
|
144
174
|
});
|
|
@@ -147,7 +177,10 @@ function launchApp() {
|
|
|
147
177
|
if (code === 42) {
|
|
148
178
|
try {
|
|
149
179
|
await performUpdate();
|
|
150
|
-
|
|
180
|
+
// If not win32 (which handles its own relaunch), relaunch here
|
|
181
|
+
if (os.platform() !== "win32") {
|
|
182
|
+
launchApp(true);
|
|
183
|
+
}
|
|
151
184
|
} catch (err) {
|
|
152
185
|
console.error(`\n❌ Update failed: ${err.message}`);
|
|
153
186
|
process.exit(1);
|
package/matrix-iptv.exe
ADDED
|
Binary file
|
package/old_player.rs
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
use std::path::PathBuf;
|
|
2
|
+
use std::sync::Arc;
|
|
3
|
+
use std::sync::Mutex;
|
|
4
|
+
use crate::config::PlayerEngine;
|
|
5
|
+
|
|
6
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
7
|
+
use std::process::{Child, Command, Stdio};
|
|
8
|
+
|
|
9
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
#[cfg(target_arch = "wasm32")]
|
|
16
|
+
use web_sys::window;
|
|
17
|
+
|
|
18
|
+
#[derive(Clone)]
|
|
19
|
+
pub struct Player {
|
|
20
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
21
|
+
process: Arc<Mutex<Option<Child>>>,
|
|
22
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
23
|
+
ipc_path: Arc<Mutex<Option<PathBuf>>>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl Player {
|
|
27
|
+
pub fn new() -> Self {
|
|
28
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
29
|
+
{
|
|
30
|
+
Self {
|
|
31
|
+
process: Arc::new(Mutex::new(None)),
|
|
32
|
+
ipc_path: Arc::new(Mutex::new(None)),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
#[cfg(target_arch = "wasm32")]
|
|
36
|
+
{
|
|
37
|
+
Self {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Start the selected player engine and return the IPC pipe path for monitoring
|
|
42
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
43
|
+
pub async fn play(&self, url: &str, engine: PlayerEngine, use_default_mpv: bool, smooth_motion: bool) -> Result<(), anyhow::Error> {
|
|
44
|
+
// Pre-flight check: Verify stream is reachable before launching player
|
|
45
|
+
// This detects dead redirects/DNS issues early and notifies the user
|
|
46
|
+
self.check_stream_health(url).await?;
|
|
47
|
+
|
|
48
|
+
self.stop();
|
|
49
|
+
|
|
50
|
+
match engine {
|
|
51
|
+
PlayerEngine::Mpv => self.play_mpv(url, use_default_mpv, smooth_motion),
|
|
52
|
+
PlayerEngine::Vlc => self.play_vlc(url, smooth_motion),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async fn check_stream_health(&self, url: &str) -> Result<(), anyhow::Error> {
|
|
57
|
+
// Build a client that mimics the player's behavior (Chrome UA)
|
|
58
|
+
let client = reqwest::Client::builder()
|
|
59
|
+
.timeout(std::time::Duration::from_secs(10))
|
|
60
|
+
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
|
61
|
+
.danger_accept_invalid_certs(true)
|
|
62
|
+
.build()
|
|
63
|
+
.unwrap_or_else(|_| reqwest::Client::new());
|
|
64
|
+
|
|
65
|
+
// We use a stream request but abort immediately to check connectivity/headers.
|
|
66
|
+
// HEAD often fails on some IPTV servers, so a started GET is safer logic-wise.
|
|
67
|
+
let mut result = client.get(url).send().await;
|
|
68
|
+
|
|
69
|
+
// Resilience: Fallback to DoH if DNS fails for the stream health check
|
|
70
|
+
if let Err(ref e) = result {
|
|
71
|
+
if crate::doh::is_dns_error(e) {
|
|
72
|
+
#[cfg(debug_assertions)]
|
|
73
|
+
println!("DEBUG: Health check DNS error detected for {}. Trying DoH fallback...", url);
|
|
74
|
+
|
|
75
|
+
if let Some(resp) = crate::doh::try_doh_fallback(&client, url).await {
|
|
76
|
+
result = Ok(resp);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
match result {
|
|
82
|
+
Ok(resp) => {
|
|
83
|
+
if resp.status().is_success() || resp.status().is_redirection() {
|
|
84
|
+
Ok(())
|
|
85
|
+
} else {
|
|
86
|
+
Err(anyhow::anyhow!("Stream returned error status: {} (Server might be offline/blocking)", resp.status()))
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
Err(e) => {
|
|
90
|
+
// Provide a user-friendly error description using shared DNS detection
|
|
91
|
+
if crate::doh::is_dns_error(&e) {
|
|
92
|
+
Err(anyhow::anyhow!("Stream Server Unreachable. The host likely does not exist or is blocked (DNS Error). Details: {}", e))
|
|
93
|
+
} else if e.is_connect() || e.is_timeout() {
|
|
94
|
+
Err(anyhow::anyhow!("Stream Connection Failed. Server may be slow or offline. Details: {}", e))
|
|
95
|
+
} else {
|
|
96
|
+
Err(anyhow::anyhow!("Stream Check Failed: {}", e))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
103
|
+
fn play_mpv(&self, url: &str, use_default_mpv: bool, smooth_motion: bool) -> Result<(), anyhow::Error> {
|
|
104
|
+
|
|
105
|
+
// Find mpv executable, checking PATH and common installation locations
|
|
106
|
+
let mpv_path = crate::setup::get_mpv_path().ok_or_else(|| {
|
|
107
|
+
let hint = if cfg!(target_os = "macos") {
|
|
108
|
+
"\n\nHint: On macOS with Homebrew:\n\
|
|
109
|
+
- Apple Silicon: mpv is typically at /opt/homebrew/bin/mpv\n\
|
|
110
|
+
- Intel Mac: mpv is typically at /usr/local/bin/mpv\n\n\
|
|
111
|
+
To fix, add Homebrew to your PATH:\n\
|
|
112
|
+
export PATH=\"/opt/homebrew/bin:$PATH\"\n\
|
|
113
|
+
(Add this line to ~/.zshrc or ~/.bash_profile)"
|
|
114
|
+
} else {
|
|
115
|
+
""
|
|
116
|
+
};
|
|
117
|
+
anyhow::anyhow!(
|
|
118
|
+
"mpv not found. Please install mpv and ensure it's in your PATH.{}",
|
|
119
|
+
hint
|
|
120
|
+
)
|
|
121
|
+
})?;
|
|
122
|
+
|
|
123
|
+
// Create a unique IPC path for this session
|
|
124
|
+
let pipe_name = if cfg!(target_os = "windows") {
|
|
125
|
+
format!("\\\\.\\pipe\\mpv_ipc_{}", std::process::id())
|
|
126
|
+
} else {
|
|
127
|
+
format!("/tmp/mpv_ipc_{}", std::process::id())
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
let mut cmd = Command::new(&mpv_path);
|
|
131
|
+
cmd.arg(url)
|
|
132
|
+
.arg("--geometry=1280x720") // Start in 720p window (user preference)
|
|
133
|
+
.arg("--force-window") // Ensure window opens even if audio-only initially
|
|
134
|
+
.arg("--no-fs") // DISABLING FULLSCREEN - Force Windowed Mode
|
|
135
|
+
.arg("--osc=yes"); // Enable On Screen Controller for usability
|
|
136
|
+
|
|
137
|
+
// Apply smooth motion interpolation if enabled
|
|
138
|
+
if smooth_motion {
|
|
139
|
+
cmd.arg("--video-sync=display-resample") // Smooth motion sync (required for interpolation)
|
|
140
|
+
.arg("--interpolation=yes") // Frame generation / motion smoothing
|
|
141
|
+
.arg("--tscale=linear") // Soap opera effect - smooth motion blending (GPU friendly)
|
|
142
|
+
.arg("--tscale-clamp=0.0"); // Allow full blending for maximum smoothness
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Only apply optimizations if not using default MPV settings
|
|
146
|
+
if !use_default_mpv {
|
|
147
|
+
cmd.arg("--cache=yes")
|
|
148
|
+
.arg("--cache-pause=yes") // Pause when cache is starved
|
|
149
|
+
.arg("--cache-pause-wait=5") // Wait for 5 seconds of cache before resuming (builds a buffer against live edge)
|
|
150
|
+
.arg("--cache-pause-initial=yes") // Ensure we buffer 5 seconds initially
|
|
151
|
+
.arg("--network-timeout=10") // Increased timeout to prevent premature EOF closures
|
|
152
|
+
.arg("--hls-bitrate=max") // Force highest quality HLS to prevent bitrate switching loops
|
|
153
|
+
.arg("--tls-verify=no") // Ignore certificate errors for internal/sketchy HTTPS streams
|
|
154
|
+
.arg("--hr-seek=yes") // Precise seeking for better buffer recovery
|
|
155
|
+
// NETWORK TURBO MODE: Aggressive Caching for Stability
|
|
156
|
+
.arg("--demuxer-max-bytes=512MiB") // Doubled cache to 512MB
|
|
157
|
+
.arg("--demuxer-max-back-bytes=128MiB") // Increase back buffer for seeking/rewind
|
|
158
|
+
.arg("--demuxer-readahead-secs=60") // Buffer 1 full minute ahead (Adaptive Buffering)
|
|
159
|
+
.arg("--stream-buffer-size=8MiB") // Low-level socket buffer
|
|
160
|
+
.arg("--load-unsafe-playlists=yes") // Stay alive through malformed HLS fragments
|
|
161
|
+
.arg("--framedrop=vo") // Drop frames gracefully if GPU lags
|
|
162
|
+
.arg("--vd-lavc-fast") // Enable fast decoding optimizations
|
|
163
|
+
.arg("--vd-lavc-skiploopfilter=all") // Major CPU saver for low-end machines
|
|
164
|
+
.arg("--vd-lavc-threads=0") // Maximize thread usage for decoding
|
|
165
|
+
// LOW-END FRIENDLY UPSCALING (catmull_rom: good quality, low GPU cost)
|
|
166
|
+
.arg("--scale=catmull_rom") // Clean upscaling, ~25% faster than spline36
|
|
167
|
+
.arg("--cscale=catmull_rom") // Matching chroma scaler
|
|
168
|
+
.arg("--dscale=catmull_rom") // Consistent downscaling
|
|
169
|
+
.arg("--scale-antiring=0.7") // Reduce haloing
|
|
170
|
+
.arg("--cscale-antiring=0.7")
|
|
171
|
+
.arg("--hwdec=auto-copy") // More compatible hardware decoding
|
|
172
|
+
// RECONNECT TURBO: Auto-reconnect on network drops (avoiding aggressive EOF looping)
|
|
173
|
+
.arg("--stream-lavf-o=reconnect_on_http_error=4xx,5xx,reconnect_on_network_error=1,reconnect_streamed=1,reconnect_delay_max=5,fflags=+genpts+igndts");
|
|
174
|
+
|
|
175
|
+
if cfg!(target_os = "windows") {
|
|
176
|
+
cmd.arg("--d3d11-flip=yes") // Modern Windows presentation (faster)
|
|
177
|
+
.arg("--gpu-api=d3d11"); // Force D3D11 (faster than OpenGL on Windows)
|
|
178
|
+
} else if cfg!(target_os = "macos") {
|
|
179
|
+
cmd.arg("--gpu-api=opengl"); // Generally safe default for macOS mpv
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Common settings for both modes
|
|
184
|
+
cmd.arg("--msg-level=all=no")
|
|
185
|
+
.arg("--term-status-msg=no")
|
|
186
|
+
.arg("--input-terminal=no") // Ignore terminal for input
|
|
187
|
+
.arg("--terminal=no") // Completely disable terminal interactions
|
|
188
|
+
// USER AGENT MASQUERADE: Modern Chrome to avoid throttling
|
|
189
|
+
.arg("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
|
190
|
+
// Keep window open if playback fails to see error (optional, maybe off for prod)
|
|
191
|
+
.arg("--keep-open=no")
|
|
192
|
+
// Logging for troubleshooting
|
|
193
|
+
.arg("--log-file=mpv_playback.log")
|
|
194
|
+
// IPC for status monitoring
|
|
195
|
+
.arg(format!("--input-ipc-server={}", pipe_name));
|
|
196
|
+
|
|
197
|
+
// Disconnect from terminal input/output to prevent hotkey conflicts
|
|
198
|
+
cmd.stdin(Stdio::null())
|
|
199
|
+
.stdout(Stdio::null())
|
|
200
|
+
.stderr(Stdio::null());
|
|
201
|
+
|
|
202
|
+
let child = cmd.spawn();
|
|
203
|
+
|
|
204
|
+
match child {
|
|
205
|
+
Ok(child) => {
|
|
206
|
+
{
|
|
207
|
+
let mut guard = self.process.lock().map_err(|e| {
|
|
208
|
+
anyhow::anyhow!("Failed to lock process mutex: {}", e)
|
|
209
|
+
})?;
|
|
210
|
+
*guard = Some(child);
|
|
211
|
+
}
|
|
212
|
+
{
|
|
213
|
+
let mut ipc_guard = self.ipc_path.lock().map_err(|e| {
|
|
214
|
+
anyhow::anyhow!("Failed to lock IPC path mutex: {}", e)
|
|
215
|
+
})?;
|
|
216
|
+
*ipc_guard = Some(PathBuf::from(&pipe_name));
|
|
217
|
+
}
|
|
218
|
+
Ok(())
|
|
219
|
+
}
|
|
220
|
+
Err(e) => {
|
|
221
|
+
let hint = if cfg!(target_os = "macos") {
|
|
222
|
+
format!(
|
|
223
|
+
"\n\nSearched for mpv at: {}\n\
|
|
224
|
+
Hint: On Apple Silicon, Homebrew installs to /opt/homebrew/bin.\n\
|
|
225
|
+
Add this to your ~/.zshrc: export PATH=\"/opt/homebrew/bin:$PATH\"",
|
|
226
|
+
mpv_path
|
|
227
|
+
)
|
|
228
|
+
} else {
|
|
229
|
+
String::new()
|
|
230
|
+
};
|
|
231
|
+
Err(anyhow::anyhow!(
|
|
232
|
+
"Failed to start mpv: {}.{}",
|
|
233
|
+
e, hint
|
|
234
|
+
))
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
240
|
+
fn play_vlc(&self, url: &str, smooth_motion: bool) -> Result<(), anyhow::Error> {
|
|
241
|
+
// Find vlc executable
|
|
242
|
+
let vlc_path = crate::setup::get_vlc_path().ok_or_else(|| {
|
|
243
|
+
anyhow::anyhow!("VLC not found. Please install VLC.")
|
|
244
|
+
})?;
|
|
245
|
+
|
|
246
|
+
let mut cmd = Command::new(&vlc_path);
|
|
247
|
+
|
|
248
|
+
// Add Referrer validation (Common anti-scraping measure)
|
|
249
|
+
// Manual parsing to avoid adding 'url' crate dependency
|
|
250
|
+
if let Some(scheme_end) = url.find("://") {
|
|
251
|
+
let rest = &url[scheme_end + 3..];
|
|
252
|
+
if let Some(path_start) = rest.find('/') {
|
|
253
|
+
let host = &rest[..path_start];
|
|
254
|
+
let base = format!("{}://{}/", &url[..scheme_end], host);
|
|
255
|
+
cmd.arg(format!("--http-referrer={}", base));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
cmd.arg(url)
|
|
260
|
+
.arg("--no-video-title-show")
|
|
261
|
+
.arg("--http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
|
262
|
+
.arg("--http-reconnect")
|
|
263
|
+
.arg("--http-continuous")
|
|
264
|
+
.arg("--clock-jitter=500") // Allow more jitter in stream clock
|
|
265
|
+
.arg("--network-caching=15000") // 15 second buffer for TS streams
|
|
266
|
+
.arg("--gnutls-verify-trust-ee=no"); // For VLC HTTPS stability
|
|
267
|
+
|
|
268
|
+
// Apply smooth motion (deinterlacing) if enabled
|
|
269
|
+
if smooth_motion {
|
|
270
|
+
cmd.arg("--video-filter=deinterlace")
|
|
271
|
+
.arg("--deinterlace-mode=bob");
|
|
272
|
+
}
|
|
273
|
+
// DISCONNECT from terminal
|
|
274
|
+
cmd.stdin(Stdio::null())
|
|
275
|
+
.stdout(Stdio::null())
|
|
276
|
+
.stderr(Stdio::null());
|
|
277
|
+
|
|
278
|
+
#[cfg(windows)]
|
|
279
|
+
{
|
|
280
|
+
use std::os::windows::process::CommandExt;
|
|
281
|
+
const DETACHED_PROCESS: u32 = 0x00000008;
|
|
282
|
+
cmd.creation_flags(DETACHED_PROCESS);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let child = cmd.spawn()?;
|
|
286
|
+
|
|
287
|
+
{
|
|
288
|
+
let mut guard = self.process.lock().map_err(|e| {
|
|
289
|
+
anyhow::anyhow!("Failed to lock process mutex: {}", e)
|
|
290
|
+
})?;
|
|
291
|
+
*guard = Some(child);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
Ok(())
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Read the last few lines of the player logs to find errors
|
|
298
|
+
pub fn get_last_error_from_log(&self) -> Option<String> {
|
|
299
|
+
let logs = ["mpv_playback.log", "vlc_playback.log"];
|
|
300
|
+
|
|
301
|
+
for log_file in logs {
|
|
302
|
+
if let Ok(content) = std::fs::read_to_string(log_file) {
|
|
303
|
+
let lines: Vec<&str> = content.lines().rev().take(15).collect();
|
|
304
|
+
for line in lines {
|
|
305
|
+
let lower = line.to_lowercase();
|
|
306
|
+
if lower.contains("error") || lower.contains("failed") || lower.contains("fatal") {
|
|
307
|
+
// Clean up common VLC/MPV prefixes for cleaner UI display
|
|
308
|
+
let cleaned = line.split("]: ").last().unwrap_or(line);
|
|
309
|
+
return Some(cleaned.to_string());
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
None
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Check if MPV is still running (process alive)
|
|
318
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
319
|
+
pub fn is_running(&self) -> bool {
|
|
320
|
+
if let Ok(mut guard) = self.process.lock() {
|
|
321
|
+
if let Some(ref mut child) = *guard {
|
|
322
|
+
// try_wait returns Ok(Some(status)) if exited, Ok(None) if still running
|
|
323
|
+
match child.try_wait() {
|
|
324
|
+
Ok(Some(_)) => false, // Process has exited
|
|
325
|
+
Ok(None) => true, // Still running
|
|
326
|
+
Err(_) => false, // Error, assume not running
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
false
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
false // Mutex poisoned, assume not running
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// Wait for MPV to actually start playing by polling process status
|
|
337
|
+
/// Returns Ok(true) if playback confirmed, Ok(false) if process died, Err on timeout
|
|
338
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
339
|
+
pub async fn wait_for_playback(&self, timeout_ms: u64) -> Result<bool, anyhow::Error> {
|
|
340
|
+
use tokio::time::{sleep, Duration, Instant};
|
|
341
|
+
|
|
342
|
+
let start = Instant::now();
|
|
343
|
+
let timeout = Duration::from_millis(timeout_ms);
|
|
344
|
+
|
|
345
|
+
// Give MPV a moment to initialize
|
|
346
|
+
sleep(Duration::from_millis(500)).await;
|
|
347
|
+
|
|
348
|
+
// Poll until process is confirmed running and has had time to buffer
|
|
349
|
+
while start.elapsed() < timeout {
|
|
350
|
+
if !self.is_running() {
|
|
351
|
+
// Process died, playback failed
|
|
352
|
+
return Ok(false);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check if process has been alive for at least 2 seconds
|
|
356
|
+
// This indicates MPV successfully connected and is playing
|
|
357
|
+
if start.elapsed() > Duration::from_millis(2000) {
|
|
358
|
+
return Ok(true);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
sleep(Duration::from_millis(200)).await;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// If we reached here and process is still running, consider it a success
|
|
365
|
+
Ok(self.is_running())
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
#[cfg(target_arch = "wasm32")]
|
|
369
|
+
pub fn play(&self, url: &str, _engine: PlayerEngine, _use_default_mpv: bool, _smooth_motion: bool) -> Result<(), anyhow::Error> {
|
|
370
|
+
self.stop();
|
|
371
|
+
if let Some(win) = window() {
|
|
372
|
+
let _ = win.alert_with_message(&format!("Play stream: {}", url));
|
|
373
|
+
}
|
|
374
|
+
Ok(())
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
378
|
+
pub fn stop(&self) {
|
|
379
|
+
if let Ok(mut guard) = self.process.lock() {
|
|
380
|
+
if let Some(mut child) = guard.take() {
|
|
381
|
+
let _ = child.kill();
|
|
382
|
+
let _ = child.wait();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if let Ok(mut ipc_guard) = self.ipc_path.lock() {
|
|
387
|
+
*ipc_guard = None;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
#[cfg(target_arch = "wasm32")]
|
|
392
|
+
pub fn stop(&self) {
|
|
393
|
+
if let Some(_win) = window() {
|
|
394
|
+
web_sys::console::log_1(&"Stopping stream".into());
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
impl Drop for Player {
|
|
400
|
+
fn drop(&mut self) {
|
|
401
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
402
|
+
self.stop();
|
|
403
|
+
}
|
|
404
|
+
}
|