Improve LibVLC stability and add external playback host

- Add LibVLCManager singleton for safer LibVLC lifetime management
- Introduce VlcHostClient and VlcHost.exe for external playback via JSON commands
- Enhance VideoPlayer with error recovery and dependency property for SourceUrl
- Implement IDisposable in ProgramsData and ViewModels for better cleanup
- Update NuGet packages: LibVLCSharp to 3.9.6, System.Reactive to 6.1.0
- Add robust error handling and resource disposal throughout
- Improve program guide handling in PlayerViewModel
This commit is contained in:
2026-04-10 14:29:00 +03:00
parent 24ca481b64
commit 64a337ffe8
10 changed files with 554 additions and 35 deletions
+40 -9
View File
@@ -1,15 +1,11 @@
using System;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive;
using System.Reactive.Subjects;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace TV_Player
{
public class ProgramsData
public class ProgramsData : IDisposable
{
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private readonly ReplaySubject<List<M3UInfo>> programsSubject = new ReplaySubject<List<M3UInfo>>();
private readonly ReplaySubject<List<GroupInfo>> groupsSubject = new ReplaySubject<List<GroupInfo>>();
private readonly ReplaySubject<Unit> programGuideSubject = new ReplaySubject<Unit>();
@@ -21,11 +17,12 @@ namespace TV_Player
public ProgramsData(string name,string playlistURL)
{
_programName = name;
Task.Run(() => GetPrograms(name,playlistURL));
Task.Run(() => GetPrograms(name,playlistURL), _cts.Token);
}
private async Task GetPrograms(string name,string m3uLink)
{
if (_cts.IsCancellationRequested) return;
System.Diagnostics.Debug.WriteLine($"[ProgramsData] Starting download of: {m3uLink}");
//string m3uLink = "http://pl.da-tv.vip/a71e77fa/835b3216/tv.m3u";
try
@@ -72,7 +69,7 @@ namespace TV_Player
}
groupsSubject.OnNext(groupping);
await Task.Run(() => GetProgramGuide(name, result.programGuide));
await Task.Run(() => GetProgramGuide(name, result.programGuide), _cts.Token);
}
catch (Exception ex)
{
@@ -88,10 +85,44 @@ namespace TV_Player
private async Task GetProgramGuide(string name, string guideLink)
{
if (_cts.IsCancellationRequested) return;
//guideLink = "http://epg.da-tv.vip/107-light.xml";
await M3UParser.DownloadGuideFromWebAsync(name,guideLink);
programGuideSubject.OnNext(Unit.Default);
}
public void Dispose()
{
try
{
_cts.Cancel();
}
catch { }
try
{
_cts.Dispose();
}
catch { }
try
{
programGuideSubject.OnCompleted();
programGuideSubject.Dispose();
}
catch { }
try
{
programsSubject.OnCompleted();
programsSubject.Dispose();
}
catch { }
try
{
groupsSubject.OnCompleted();
groupsSubject.Dispose();
}
catch { }
}
}
}
+175
View File
@@ -0,0 +1,175 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace TV_Player
{
// Simple supervisor client to talk to an external VlcHost process via stdio lines (JSON per line).
// This is a minimal implementation: it starts the helper if present and provides async SendCommand/GetResponse.
public class VlcHostClient : IDisposable
{
private Process _process;
private StreamWriter _writer;
private StreamReader _reader;
private readonly object _lock = new object();
private readonly string _exePath;
private CancellationTokenSource _restartCts;
public bool IsRunning => _process != null && !_process.HasExited;
public VlcHostClient(string exePath = null)
{
_exePath = exePath ?? Path.Combine(AppContext.BaseDirectory ?? string.Empty, "VlcHost.exe");
}
public bool EnsureStarted()
{
lock (_lock)
{
if (IsRunning) return true;
if (!File.Exists(_exePath)) return false;
StartProcess();
return IsRunning;
}
}
private void StartProcess()
{
var psi = new ProcessStartInfo(_exePath)
{
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
_process = Process.Start(psi);
if (_process == null) return;
_process.EnableRaisingEvents = true;
_process.Exited += Process_Exited;
_writer = new StreamWriter(_process.StandardInput.BaseStream, Encoding.UTF8) { AutoFlush = true };
_reader = new StreamReader(_process.StandardOutput.BaseStream, Encoding.UTF8);
// optionally read initial banner asynchronously
Task.Run(async () =>
{
try
{
for (int i = 0; i < 5; i++)
{
if (_reader.Peek() >= 0)
{
var line = await _reader.ReadLineAsync().ConfigureAwait(false);
Debug.WriteLine($"[VlcHostClient] banner: {line}");
}
else break;
}
}
catch { }
});
}
private void Process_Exited(object sender, EventArgs e)
{
Debug.WriteLine("[VlcHostClient] helper process exited, scheduling restart");
lock (_lock)
{
try { _writer?.Dispose(); } catch { }
try { _reader?.Dispose(); } catch { }
_writer = null;
_reader = null;
}
// start a restart loop
_restartCts?.Cancel();
_restartCts = new CancellationTokenSource();
var token = _restartCts.Token;
Task.Run(async () =>
{
int attempt = 0;
while (!token.IsCancellationRequested && attempt < 20)
{
attempt++;
Debug.WriteLine($"[VlcHostClient] restart attempt {attempt}");
try
{
if (File.Exists(_exePath))
{
StartProcess();
if (IsRunning)
{
Debug.WriteLine("[VlcHostClient] helper restarted");
return;
}
}
}
catch { }
await Task.Delay(TimeSpan.FromSeconds(2), token).ContinueWith(_ => { });
}
Debug.WriteLine("[VlcHostClient] restart attempts exhausted");
}, token);
}
public async Task<string> SendCommandAsync(object command, int timeoutMs = 2000)
{
if (!EnsureStarted()) return null;
var json = JsonSerializer.Serialize(command);
try
{
await _writer.WriteLineAsync(json).ConfigureAwait(false);
}
catch
{
return null;
}
using var cts = new CancellationTokenSource(timeoutMs);
try
{
var t = _reader.ReadLineAsync();
var completed = await Task.WhenAny(t, Task.Delay(timeoutMs, cts.Token)).ConfigureAwait(false);
if (completed == t)
{
return await t.ConfigureAwait(false);
}
}
catch { }
return null;
}
public Task<string> PlayAsync(string url) => SendCommandAsync(new { cmd = "play", url });
public Task<string> StopAsync() => SendCommandAsync(new { cmd = "stop" });
public Task<string> PingAsync() => SendCommandAsync(new { cmd = "ping" });
public void Dispose()
{
try
{
_restartCts?.Cancel();
}
catch { }
try
{
if (IsRunning)
{
try { _writer?.WriteLine("{\"cmd\":\"exit\"}"); } catch { }
try { _process?.Kill(); } catch { }
}
}
catch { }
try { _writer?.Dispose(); } catch { }
try { _reader?.Dispose(); } catch { }
try { _process?.Dispose(); } catch { }
_process = null;
}
}
}