@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 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/playlists.png" width="45%" />
17
- <img src="./assets/pill.png" width="45%" />
16
+ <img src="./assets/selector.png" width="48%" />
17
+ <img src="./assets/categories.png" width="48%" />
18
18
  <br />
19
- <img src="./assets/live.png" width="90%" />
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
- ### **One-Click Scripts**
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`** | **Jump to Bottom** (Go to last item) |
140
- | **`G`** | **Jump to Top** (Go to first item) |
141
- | **`0`-`9`** | **Jump to Item** (Direct access to items 0-9) |
142
- | **`m`** | **Playlist Mode** (Quick Toggle/Switch) |
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
- function download(url, dest) {
20
- return new Promise((resolve, reject) => {
21
- const file = fs.createWriteStream(dest);
22
- https
23
- .get(url, (response) => {
24
- if (response.statusCode === 302 || response.statusCode === 301) {
25
- download(response.headers.location, dest).then(resolve).catch(reject);
26
- return;
27
- }
28
- if (response.statusCode !== 200) {
29
- reject(new Error(`Failed to download: ${response.statusCode}`));
30
- return;
31
- }
32
- response.pipe(file);
33
- file.on("finish", () => {
34
- file.close();
35
- resolve();
36
- });
37
- })
38
- .on("error", (err) => {
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 = 10;
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
- // It's okay if we can't delete it now, it'll just stay as a .old file
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
- // Increase delay on each attempt
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
- const child = spawn(binaryPath, process.argv.slice(2), {
137
- stdio: "inherit",
138
- windowsHide: false,
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
- launchApp(); // Relaunch
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);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officebeats/matrix-iptv-cli",
3
- "version": "4.0.4",
3
+ "version": "4.0.17",
4
4
  "description": "The premium Terminal IPTV Decoder",
5
5
  "main": "index.js",
6
6
  "bin": {