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:
@@ -155,5 +155,18 @@ public class TVPlayerViewModel : IDisposable
|
|||||||
{
|
{
|
||||||
if (_mainWindowViewModel.CurrentViewModel is IDisposable disposable)
|
if (_mainWindowViewModel.CurrentViewModel is IDisposable disposable)
|
||||||
disposable.Dispose();
|
disposable.Dispose();
|
||||||
|
|
||||||
|
if (PlayListsData != null)
|
||||||
|
{
|
||||||
|
foreach (var kv in PlayListsData)
|
||||||
|
{
|
||||||
|
try { kv.Value.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
PlayListsData.Clear();
|
||||||
|
}
|
||||||
|
if (CurrentProgramsData != null)
|
||||||
|
{
|
||||||
|
try { CurrentProgramsData.Dispose(); } catch { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
using System;
|
using System.Reactive;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reactive;
|
|
||||||
using System.Reactive.Subjects;
|
using System.Reactive.Subjects;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace TV_Player
|
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<M3UInfo>> programsSubject = new ReplaySubject<List<M3UInfo>>();
|
||||||
private readonly ReplaySubject<List<GroupInfo>> groupsSubject = new ReplaySubject<List<GroupInfo>>();
|
private readonly ReplaySubject<List<GroupInfo>> groupsSubject = new ReplaySubject<List<GroupInfo>>();
|
||||||
private readonly ReplaySubject<Unit> programGuideSubject = new ReplaySubject<Unit>();
|
private readonly ReplaySubject<Unit> programGuideSubject = new ReplaySubject<Unit>();
|
||||||
@@ -21,11 +17,12 @@ namespace TV_Player
|
|||||||
public ProgramsData(string name,string playlistURL)
|
public ProgramsData(string name,string playlistURL)
|
||||||
{
|
{
|
||||||
_programName = name;
|
_programName = name;
|
||||||
Task.Run(() => GetPrograms(name,playlistURL));
|
Task.Run(() => GetPrograms(name,playlistURL), _cts.Token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GetPrograms(string name,string m3uLink)
|
private async Task GetPrograms(string name,string m3uLink)
|
||||||
{
|
{
|
||||||
|
if (_cts.IsCancellationRequested) return;
|
||||||
System.Diagnostics.Debug.WriteLine($"[ProgramsData] Starting download of: {m3uLink}");
|
System.Diagnostics.Debug.WriteLine($"[ProgramsData] Starting download of: {m3uLink}");
|
||||||
//string m3uLink = "http://pl.da-tv.vip/a71e77fa/835b3216/tv.m3u";
|
//string m3uLink = "http://pl.da-tv.vip/a71e77fa/835b3216/tv.m3u";
|
||||||
try
|
try
|
||||||
@@ -72,7 +69,7 @@ namespace TV_Player
|
|||||||
}
|
}
|
||||||
groupsSubject.OnNext(groupping);
|
groupsSubject.OnNext(groupping);
|
||||||
|
|
||||||
await Task.Run(() => GetProgramGuide(name, result.programGuide));
|
await Task.Run(() => GetProgramGuide(name, result.programGuide), _cts.Token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -88,10 +85,44 @@ namespace TV_Player
|
|||||||
|
|
||||||
private async Task GetProgramGuide(string name, string guideLink)
|
private async Task GetProgramGuide(string name, string guideLink)
|
||||||
{
|
{
|
||||||
|
if (_cts.IsCancellationRequested) return;
|
||||||
//guideLink = "http://epg.da-tv.vip/107-light.xml";
|
//guideLink = "http://epg.da-tv.vip/107-light.xml";
|
||||||
await M3UParser.DownloadGuideFromWebAsync(name,guideLink);
|
await M3UParser.DownloadGuideFromWebAsync(name,guideLink);
|
||||||
programGuideSubject.OnNext(Unit.Default);
|
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 { }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ namespace TV_Player
|
|||||||
protected override void OnExit(ExitEventArgs e)
|
protected override void OnExit(ExitEventArgs e)
|
||||||
{
|
{
|
||||||
_tvPlayer.Dispose();
|
_tvPlayer.Dispose();
|
||||||
|
// dispose shared LibVLC instance
|
||||||
|
try { LibVLCManager.Dispose(); } catch { }
|
||||||
base.OnExit(e);
|
base.OnExit(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,12 +22,12 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="LibVLCSharp" Version="3.9.4">
|
<PackageReference Include="LibVLCSharp" Version="3.9.6">
|
||||||
<TreatAsUsed>true</TreatAsUsed>
|
<TreatAsUsed>true</TreatAsUsed>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.4" />
|
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
<PackageReference Include="System.Reactive" Version="6.0.2" />
|
<PackageReference Include="System.Reactive" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using LibVLCSharp.WPF;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Threading;
|
|
||||||
|
|
||||||
namespace TV_Player
|
namespace TV_Player
|
||||||
{
|
{
|
||||||
@@ -12,34 +11,76 @@ namespace TV_Player
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class VideoPlayer : UserControl
|
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 MediaPlayer _mediaPlayer;
|
||||||
private PlayerViewModel _viewModel;
|
private PlayerViewModel _viewModel;
|
||||||
|
private VlcHostClient _vlcHost;
|
||||||
|
private bool _useHost;
|
||||||
//private DispatcherTimer _overlayAutoHideTimer;
|
//private DispatcherTimer _overlayAutoHideTimer;
|
||||||
|
|
||||||
public VideoPlayer()
|
public VideoPlayer()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
//_overlayAutoHideTimer = new DispatcherTimer();
|
|
||||||
//_overlayAutoHideTimer.Interval = TimeSpan.FromSeconds(3);
|
|
||||||
//_overlayAutoHideTimer.Tick += _overlayAutoHideTimer_Tick;
|
|
||||||
//_overlayAutoHideTimer.Start();
|
|
||||||
|
|
||||||
_libVLC = new LibVLC(enableDebugLogs: true);
|
|
||||||
_mediaPlayer = new MediaPlayer(_libVLC);
|
_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) =>
|
this.DataContextChanged += (sender, e) =>
|
||||||
{
|
{
|
||||||
_viewModel = (PlayerViewModel)e.NewValue;
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
try { _viewModel.SourceUrlChangedEvent -= _viewModel_SourceUrlChangedEvent; } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewModel = e.NewValue as PlayerViewModel;
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
_viewModel.SourceUrlChangedEvent += _viewModel_SourceUrlChangedEvent;
|
_viewModel.SourceUrlChangedEvent += _viewModel_SourceUrlChangedEvent;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
VideoView.Loaded += (sender, e) =>
|
VideoView.Loaded += (sender, e) =>
|
||||||
{
|
{
|
||||||
VideoView.MediaPlayer = _mediaPlayer;
|
VideoView.MediaPlayer = _mediaPlayer;
|
||||||
VideoView.MouseLeftButtonDown += VideoView_MouseLeftButtonDown;
|
VideoView.MouseLeftButtonDown += VideoView_MouseLeftButtonDown;
|
||||||
|
if (VideoView.MediaPlayer != null)
|
||||||
VideoView.MediaPlayer.EnableMouseInput = false;
|
VideoView.MediaPlayer.EnableMouseInput = false;
|
||||||
VideoView.PreviewMouseLeftButtonDown += VideoView_MouseLeftButtonDown;
|
VideoView.PreviewMouseLeftButtonDown += VideoView_MouseLeftButtonDown;
|
||||||
AutoPlay();
|
AutoPlay();
|
||||||
@@ -47,24 +88,65 @@ namespace TV_Player
|
|||||||
Unloaded += VideoPlayer_Unloaded;
|
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)
|
private void _viewModel_SourceUrlChangedEvent(string videoURL)
|
||||||
{
|
{
|
||||||
SourceUrl = videoURL;
|
SourceUrl = videoURL;
|
||||||
VideoView.MediaPlayer.Stop();
|
try
|
||||||
VideoView.MediaPlayer.Media.Dispose();
|
{
|
||||||
|
VideoView.MediaPlayer?.Stop();
|
||||||
|
VideoView.MediaPlayer?.Media?.Dispose();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
AutoPlay();
|
AutoPlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VideoView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
private void VideoView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
|
|
||||||
ToggleOverlay();
|
ToggleOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void VideoPlayer_Unloaded(object sender, RoutedEventArgs e)
|
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)
|
void PauseButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
@@ -75,7 +157,9 @@ namespace TV_Player
|
|||||||
}
|
}
|
||||||
private void AutoPlay()
|
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)))
|
using (var media = new Media(_libVLC, new Uri(SourceUrl)))
|
||||||
VideoView.MediaPlayer.Play(media);
|
VideoView.MediaPlayer.Play(media);
|
||||||
@@ -90,6 +174,7 @@ namespace TV_Player
|
|||||||
{
|
{
|
||||||
if (overlayPanel.Visibility == Visibility.Visible)
|
if (overlayPanel.Visibility == Visibility.Visible)
|
||||||
{
|
{
|
||||||
|
if (_viewModel != null)
|
||||||
_viewModel.ProgramGuideVisible = false;
|
_viewModel.ProgramGuideVisible = false;
|
||||||
HideOverlay();
|
HideOverlay();
|
||||||
}
|
}
|
||||||
@@ -116,8 +201,10 @@ namespace TV_Player
|
|||||||
}
|
}
|
||||||
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
|
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_viewModel.SourceUrlChangedEvent -= _viewModel_SourceUrlChangedEvent;
|
try { if(VideoView.MediaPlayer.IsPlaying) VideoView.MediaPlayer?.Stop(); } catch { }
|
||||||
VideoView.MediaPlayer?.Dispose();
|
try { _mediaPlayer?.Dispose(); } catch { }
|
||||||
|
try { if (_viewModel != null) _viewModel.SourceUrlChangedEvent -= _viewModel_SourceUrlChangedEvent; } catch { }
|
||||||
|
try { VideoView.MediaPlayer?.Dispose(); } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,8 +163,30 @@ namespace TV_Player
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_currentProgram == null) return;
|
if (_currentProgram == null) return;
|
||||||
|
|
||||||
|
// make sure guide and programs list exist
|
||||||
|
if (_currentGuide?.Programs == null || _currentGuide.Programs.Count == 0)
|
||||||
|
{
|
||||||
|
IsProgramInfoVisible = false;
|
||||||
|
Programs = new List<ProgramInfo>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_currentProgramInfo = _currentGuide.Programs.FirstOrDefault(d => d.StartTime <= DateTime.Now && d.EndTime >= DateTime.Now);
|
_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)
|
if (_currentProgramInfo == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -188,6 +188,19 @@ namespace TV_Player.ViewModels
|
|||||||
{
|
{
|
||||||
if (_mainViewModel.Control is IDisposable disposable)
|
if (_mainViewModel.Control is IDisposable disposable)
|
||||||
disposable.Dispose();
|
disposable.Dispose();
|
||||||
|
|
||||||
|
if (PlayListsData != null)
|
||||||
|
{
|
||||||
|
foreach (var kv in PlayListsData)
|
||||||
|
{
|
||||||
|
try { kv.Value.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
PlayListsData.Clear();
|
||||||
|
}
|
||||||
|
if (CurrentProgrmsData != null)
|
||||||
|
{
|
||||||
|
try { CurrentProgrmsData.Dispose(); } catch { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<int> 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<Command>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user