From 64a337ffe865a98d034f904d4ee4609152022d50 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Fri, 10 Apr 2026 14:29:00 +0300 Subject: [PATCH] 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 --- .../ViewModels/TVPlayerViewModel.cs | 13 ++ TV Player Core/ProgramsData.cs | 49 ++++- TV Player Core/VlcHostClient.cs | 175 ++++++++++++++++++ TV Player WPF/App.xaml.cs | 2 + TV Player WPF/LibVLCManager.cs | 47 +++++ TV Player WPF/TV Player WPF.csproj | 6 +- TV Player WPF/VideoPlayer.xaml.cs | 131 ++++++++++--- TV Player WPF/ViewModels/PlayerViewModel.cs | 24 ++- TV Player WPF/ViewModels/TVPlayerViewModel.cs | 13 ++ VlcHost/Program.cs | 129 +++++++++++++ 10 files changed, 554 insertions(+), 35 deletions(-) create mode 100644 TV Player Core/VlcHostClient.cs create mode 100644 TV Player WPF/LibVLCManager.cs create mode 100644 VlcHost/Program.cs diff --git a/TV Player Avalonia/ViewModels/TVPlayerViewModel.cs b/TV Player Avalonia/ViewModels/TVPlayerViewModel.cs index d60c037..feb398a 100644 --- a/TV Player Avalonia/ViewModels/TVPlayerViewModel.cs +++ b/TV Player Avalonia/ViewModels/TVPlayerViewModel.cs @@ -155,5 +155,18 @@ public class TVPlayerViewModel : IDisposable { if (_mainWindowViewModel.CurrentViewModel is IDisposable disposable) disposable.Dispose(); + + if (PlayListsData != null) + { + foreach (var kv in PlayListsData) + { + try { kv.Value.Dispose(); } catch { } + } + PlayListsData.Clear(); + } + if (CurrentProgramsData != null) + { + try { CurrentProgramsData.Dispose(); } catch { } + } } } diff --git a/TV Player Core/ProgramsData.cs b/TV Player Core/ProgramsData.cs index bb70979..ae2c6e6 100644 --- a/TV Player Core/ProgramsData.cs +++ b/TV Player Core/ProgramsData.cs @@ -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> programsSubject = new ReplaySubject>(); private readonly ReplaySubject> groupsSubject = new ReplaySubject>(); private readonly ReplaySubject programGuideSubject = new ReplaySubject(); @@ -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 { } + } } } diff --git a/TV Player Core/VlcHostClient.cs b/TV Player Core/VlcHostClient.cs new file mode 100644 index 0000000..ca11abf --- /dev/null +++ b/TV Player Core/VlcHostClient.cs @@ -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 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 PlayAsync(string url) => SendCommandAsync(new { cmd = "play", url }); + public Task StopAsync() => SendCommandAsync(new { cmd = "stop" }); + public Task 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; + } + } +} diff --git a/TV Player WPF/App.xaml.cs b/TV Player WPF/App.xaml.cs index e934073..d37af2a 100644 --- a/TV Player WPF/App.xaml.cs +++ b/TV Player WPF/App.xaml.cs @@ -38,6 +38,8 @@ namespace TV_Player protected override void OnExit(ExitEventArgs e) { _tvPlayer.Dispose(); + // dispose shared LibVLC instance + try { LibVLCManager.Dispose(); } catch { } base.OnExit(e); } } diff --git a/TV Player WPF/LibVLCManager.cs b/TV Player WPF/LibVLCManager.cs new file mode 100644 index 0000000..7c8b20d --- /dev/null +++ b/TV Player WPF/LibVLCManager.cs @@ -0,0 +1,47 @@ +using LibVLCSharp.Shared; + +namespace TV_Player +{ + public static class LibVLCManager + { + private static readonly object _lock = new object(); + private static LibVLC _instance; + + public static LibVLC Instance + { + get + { + if (_instance != null) return _instance; + lock (_lock) + { + if (_instance == null) + { + Core.Initialize(); + // Use conservative libvlc options to reduce native crashes + var options = new[] + { + "--ignore-config", + "--no-osd", + "--no-snapshot-preview", + "--avcodec-hw=none", + "--network-caching=3000", + "--http-reconnect", + "--rtsp-tcp" + }; + _instance = new LibVLC(options); + } + return _instance; + } + } + } + + public static void Dispose() + { + lock (_lock) + { + try { _instance?.Dispose(); } catch { } + _instance = null; + } + } + } +} diff --git a/TV Player WPF/TV Player WPF.csproj b/TV Player WPF/TV Player WPF.csproj index 453ffbd..d57b6f5 100644 --- a/TV Player WPF/TV Player WPF.csproj +++ b/TV Player WPF/TV Player WPF.csproj @@ -22,12 +22,12 @@ - + true - + - + diff --git a/TV Player WPF/VideoPlayer.xaml.cs b/TV Player WPF/VideoPlayer.xaml.cs index cd23d4b..03214a5 100644 --- a/TV Player WPF/VideoPlayer.xaml.cs +++ b/TV Player WPF/VideoPlayer.xaml.cs @@ -3,7 +3,6 @@ using LibVLCSharp.WPF; using System.Windows; using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Threading; namespace TV_Player { @@ -12,59 +11,142 @@ namespace TV_Player /// public partial class VideoPlayer : UserControl { - public string SourceUrl { get; set; } + public static readonly DependencyProperty SourceUrlProperty = DependencyProperty.Register( + nameof(SourceUrl), typeof(string), typeof(VideoPlayer), new PropertyMetadata(string.Empty, OnSourceUrlChanged)); - private LibVLC _libVLC; + public string SourceUrl + { + get => (string)GetValue(SourceUrlProperty); + set => SetValue(SourceUrlProperty, value); + } + + private static void OnSourceUrlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is VideoPlayer vp && e.NewValue is string url) + { + vp.OnSourceUrlChanged(url); + } + } + + private void OnSourceUrlChanged(string url) + { + // when the bound source changes, trigger playback switch + try + { + VideoView.MediaPlayer?.Stop(); + VideoView.MediaPlayer?.Media?.Dispose(); + } + catch { } + + AutoPlay(); + } + + private LibVLC _libVLC => LibVLCManager.Instance; private MediaPlayer _mediaPlayer; private PlayerViewModel _viewModel; + private VlcHostClient _vlcHost; + private bool _useHost; //private DispatcherTimer _overlayAutoHideTimer; - + public VideoPlayer() { InitializeComponent(); - //_overlayAutoHideTimer = new DispatcherTimer(); - //_overlayAutoHideTimer.Interval = TimeSpan.FromSeconds(3); - //_overlayAutoHideTimer.Tick += _overlayAutoHideTimer_Tick; - //_overlayAutoHideTimer.Start(); - - _libVLC = new LibVLC(enableDebugLogs: true); _mediaPlayer = new MediaPlayer(_libVLC); + // try to start external host helper (if present) + try + { + _vlcHost = new VlcHostClient(); + _useHost = _vlcHost.EnsureStarted(); + } + catch { _useHost = false; } + // subscribe to error events to recover from playback errors + _mediaPlayer.EncounteredError += MediaPlayer_EncounteredError; this.DataContextChanged += (sender, e) => { - _viewModel = (PlayerViewModel)e.NewValue; - _viewModel.SourceUrlChangedEvent += _viewModel_SourceUrlChangedEvent; + if (_viewModel != null) + { + try { _viewModel.SourceUrlChangedEvent -= _viewModel_SourceUrlChangedEvent; } catch { } + } + + _viewModel = e.NewValue as PlayerViewModel; + if (_viewModel != null) + { + _viewModel.SourceUrlChangedEvent += _viewModel_SourceUrlChangedEvent; + } }; VideoView.Loaded += (sender, e) => { VideoView.MediaPlayer = _mediaPlayer; VideoView.MouseLeftButtonDown += VideoView_MouseLeftButtonDown; - VideoView.MediaPlayer.EnableMouseInput = false; + if (VideoView.MediaPlayer != null) + VideoView.MediaPlayer.EnableMouseInput = false; VideoView.PreviewMouseLeftButtonDown += VideoView_MouseLeftButtonDown; AutoPlay(); }; Unloaded += VideoPlayer_Unloaded; - } + } + + + private void MediaPlayer_EncounteredError(object? sender, EventArgs e) + { + // MediaPlayer.EncounteredError can be raised on a native thread; marshal to UI thread + try + { + Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + System.Diagnostics.Debug.WriteLine("[VideoPlayer] MediaPlayer encountered error — attempting recovery"); + try { VideoView.MediaPlayer?.Stop(); } catch { } + try { VideoView.MediaPlayer?.Media?.Dispose(); } catch { } + + // dispose existing player and try full LibVLC restart to recover native/network resources + try { _mediaPlayer.EncounteredError -= MediaPlayer_EncounteredError; } catch { } + try { VideoView.MediaPlayer?.Stop(); } catch { } + try { VideoView.MediaPlayer?.Media?.Dispose(); } catch { } + try { _mediaPlayer?.Dispose(); } catch { } + + try { LibVLCManager.Dispose(); } catch { } + + // recreate LibVLC and MediaPlayer + var lib = LibVLCManager.Instance; + _mediaPlayer = new MediaPlayer(lib); + _mediaPlayer.EncounteredError += MediaPlayer_EncounteredError; + + // re-assign to the view and restart playback + VideoView.MediaPlayer = _mediaPlayer; + AutoPlay(); + })); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[VideoPlayer] Error handling encountered error: {ex}"); + } + } + private void _viewModel_SourceUrlChangedEvent(string videoURL) { SourceUrl = videoURL; - VideoView.MediaPlayer.Stop(); - VideoView.MediaPlayer.Media.Dispose(); + try + { + VideoView.MediaPlayer?.Stop(); + VideoView.MediaPlayer?.Media?.Dispose(); + } + catch { } AutoPlay(); } private void VideoView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { - ToggleOverlay(); } private void VideoPlayer_Unloaded(object sender, RoutedEventArgs e) { - VideoView.Dispose(); + // Dispose the VideoView (releases native view references) but keep the shared LibVLC instance + try { VideoView.Dispose(); } catch { } } void PauseButton_Click(object sender, RoutedEventArgs e) { @@ -75,7 +157,9 @@ namespace TV_Player } private void AutoPlay() { - if (!VideoView.MediaPlayer.IsPlaying) + if (VideoView?.MediaPlayer == null) return; + + if (!VideoView.MediaPlayer.IsPlaying && !string.IsNullOrWhiteSpace(SourceUrl)) { using (var media = new Media(_libVLC, new Uri(SourceUrl))) VideoView.MediaPlayer.Play(media); @@ -90,7 +174,8 @@ namespace TV_Player { if (overlayPanel.Visibility == Visibility.Visible) { - _viewModel.ProgramGuideVisible = false; + if (_viewModel != null) + _viewModel.ProgramGuideVisible = false; HideOverlay(); } else @@ -116,8 +201,10 @@ namespace TV_Player } private void UserControl_Unloaded(object sender, RoutedEventArgs e) { - _viewModel.SourceUrlChangedEvent -= _viewModel_SourceUrlChangedEvent; - VideoView.MediaPlayer?.Dispose(); + try { if(VideoView.MediaPlayer.IsPlaying) VideoView.MediaPlayer?.Stop(); } catch { } + try { _mediaPlayer?.Dispose(); } catch { } + try { if (_viewModel != null) _viewModel.SourceUrlChangedEvent -= _viewModel_SourceUrlChangedEvent; } catch { } + try { VideoView.MediaPlayer?.Dispose(); } catch { } } } } diff --git a/TV Player WPF/ViewModels/PlayerViewModel.cs b/TV Player WPF/ViewModels/PlayerViewModel.cs index faad24f..3a65fed 100644 --- a/TV Player WPF/ViewModels/PlayerViewModel.cs +++ b/TV Player WPF/ViewModels/PlayerViewModel.cs @@ -163,8 +163,30 @@ namespace TV_Player try { if (_currentProgram == null) return; + + // make sure guide and programs list exist + if (_currentGuide?.Programs == null || _currentGuide.Programs.Count == 0) + { + IsProgramInfoVisible = false; + Programs = new List(); + return; + } + _currentProgramInfo = _currentGuide.Programs.FirstOrDefault(d => d.StartTime <= DateTime.Now && d.EndTime >= DateTime.Now); - Programs = _currentGuide.Programs.Skip(_currentGuide.Programs.FindIndex(x=>x.Title==_currentProgramInfo.Title)).Take(7).ToList(); + + if (_currentProgramInfo == null) + { + // no current program found: hide info and show the first N programs as a fallback + IsProgramInfoVisible = false; + Programs = _currentGuide.Programs.Take(7).ToList(); + } + else + { + // find the index of the current program safely + int idx = _currentGuide.Programs.FindIndex(x => string.Equals(x.Title, _currentProgramInfo.Title, StringComparison.Ordinal)); + if (idx < 0) idx = 0; + Programs = _currentGuide.Programs.Skip(idx).Take(7).ToList(); + } if (_currentProgramInfo == null) { diff --git a/TV Player WPF/ViewModels/TVPlayerViewModel.cs b/TV Player WPF/ViewModels/TVPlayerViewModel.cs index 8f722a0..0a1f88b 100644 --- a/TV Player WPF/ViewModels/TVPlayerViewModel.cs +++ b/TV Player WPF/ViewModels/TVPlayerViewModel.cs @@ -188,6 +188,19 @@ namespace TV_Player.ViewModels { if (_mainViewModel.Control is IDisposable disposable) disposable.Dispose(); + + if (PlayListsData != null) + { + foreach (var kv in PlayListsData) + { + try { kv.Value.Dispose(); } catch { } + } + PlayListsData.Clear(); + } + if (CurrentProgrmsData != null) + { + try { CurrentProgrmsData.Dispose(); } catch { } + } } } } diff --git a/VlcHost/Program.cs b/VlcHost/Program.cs new file mode 100644 index 0000000..05e59f2 --- /dev/null +++ b/VlcHost/Program.cs @@ -0,0 +1,129 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using LibVLCSharp.Shared; + +class Command +{ + public string cmd { get; set; } + public string url { get; set; } +} + +class Response +{ + public string status { get; set; } + public string message { get; set; } +} + +class Program +{ + static LibVLC _libVlc; + static MediaPlayer _player; + static Media _media; + + static async Task Main(string[] args) + { + Core.Initialize(); + // create libvlc with conservative options to reduce native crashes + _libVlc = new LibVLC(new[] { "--ignore-config", "--no-osd", "--avcodec-hw=none", "--network-caching=3000", "--http-reconnect" }); + + using var stdin = Console.OpenStandardInput(); + using var reader = new StreamReader(stdin); + string line; + // write banner + Console.Out.WriteLine(JsonSerializer.Serialize(new Response { status = "ready", message = "VlcHost ready" })); + Console.Out.Flush(); + + while ((line = await reader.ReadLineAsync()) != null) + { + if (string.IsNullOrWhiteSpace(line)) continue; + try + { + var cmd = JsonSerializer.Deserialize(line); + if (cmd == null) + { + WriteResponse("error", "invalid command"); + continue; + } + + switch (cmd.cmd?.ToLowerInvariant()) + { + case "play": + await CmdPlay(cmd.url); + break; + case "stop": + CmdStop(); + break; + case "ping": + WriteResponse("ok", "pong"); + break; + case "exit": + WriteResponse("ok", "exiting"); + return 0; + default: + WriteResponse("error", "unknown command"); + break; + } + } + catch (Exception ex) + { + WriteResponse("error", ex.Message); + } + } + + return 0; + } + + static async Task CmdPlay(string url) + { + try + { + if (string.IsNullOrWhiteSpace(url)) + { + WriteResponse("error", "url empty"); + return; + } + + CmdStop(); + + _media = new Media(_libVlc, url, FromType.FromLocation); + _player = new MediaPlayer(_libVlc); + _player.Play(_media); + + WriteResponse("ok", "playing"); + } + catch (Exception ex) + { + WriteResponse("error", ex.Message); + } + } + + static void CmdStop() + { + try + { + if (_player != null) + { + _player.Stop(); + try { _media?.Dispose(); } catch { } + try { _player.Dispose(); } catch { } + _player = null; + _media = null; + } + WriteResponse("ok", "stopped"); + } + catch (Exception ex) + { + WriteResponse("error", ex.Message); + } + } + + static void WriteResponse(string status, string message) + { + var resp = new Response { status = status, message = message }; + var json = JsonSerializer.Serialize(resp); + Console.Out.WriteLine(json); + Console.Out.Flush(); + } +}