using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Threading; using LibVLCSharp.Shared; using System.Diagnostics; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using System.Web; using TV_Player.AvaloniaApp.ViewModels; namespace TV_Player.AvaloniaApp.Views; public partial class PlayerView : UserControl { private LibVLC? _libVlc; private MediaPlayer? _mediaPlayer; private Media? _currentMedia; private PlayerViewModel? _viewModel; private bool _initialized; private CancellationTokenSource? _bufferingWatchdogCts; private string? _activeStreamUrl; private bool _fallbackAttempted; public PlayerView() { AvaloniaXamlLoader.Load(this); Log("PlayerView ctor"); DataContextChanged += OnDataContextChanged; AttachedToVisualTree += OnAttachedToVisualTree; DetachedFromVisualTree += OnDetachedFromVisualTree; } private void OnAttachedToVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e) { if (_initialized) return; Log("Attach to visual tree -> initializing VLC"); try { Log("Core.Initialize() start"); Core.Initialize(); Log("Core.Initialize() completed"); _libVlc = new LibVLC(enableDebugLogs: true); Log("LibVLC instance created"); _mediaPlayer = new MediaPlayer(_libVlc) { // Software decode is more reliable for mixed IPTV codecs on macOS. EnableHardwareDecoding = !OperatingSystem.IsMacOS() }; Log($"MediaPlayer created. HW decoding enabled: {_mediaPlayer.EnableHardwareDecoding}"); _mediaPlayer.Buffering += OnMediaPlayerBuffering; _mediaPlayer.Playing += OnMediaPlayerPlaying; _mediaPlayer.EncounteredError += OnMediaPlayerEncounteredError; _mediaPlayer.EndReached += OnMediaPlayerEndReached; _mediaPlayer.Stopped += OnMediaPlayerStopped; VideoView.MediaPlayer = _mediaPlayer; _initialized = true; Log("VideoView.MediaPlayer assigned"); PlayCurrentStream(); } catch (System.Exception ex) { Log($"VLC initialization failed: {ex}"); _viewModel?.SetPlaybackStatus($"VLC init failed: {ex.Message}"); } } private void OnDetachedFromVisualTree(object? sender, Avalonia.VisualTreeAttachmentEventArgs e) { Log("Detach from visual tree -> disposing player resources"); if (_viewModel != null) { _viewModel.PropertyChanged -= OnViewModelPropertyChanged; } if (_mediaPlayer != null) { _mediaPlayer.Buffering -= OnMediaPlayerBuffering; _mediaPlayer.Playing -= OnMediaPlayerPlaying; _mediaPlayer.EncounteredError -= OnMediaPlayerEncounteredError; _mediaPlayer.EndReached -= OnMediaPlayerEndReached; _mediaPlayer.Stopped -= OnMediaPlayerStopped; } _mediaPlayer?.Stop(); _bufferingWatchdogCts?.Cancel(); _bufferingWatchdogCts?.Dispose(); _currentMedia?.Dispose(); _mediaPlayer?.Dispose(); _libVlc?.Dispose(); _bufferingWatchdogCts = null; _currentMedia = null; _mediaPlayer = null; _libVlc = null; _activeStreamUrl = null; _fallbackAttempted = false; _initialized = false; } private void OnDataContextChanged(object? sender, System.EventArgs e) { Log($"DataContext changed. New type: {DataContext?.GetType().Name ?? "null"}"); if (_viewModel != null) { _viewModel.PropertyChanged -= OnViewModelPropertyChanged; } _viewModel = DataContext as PlayerViewModel; if (_viewModel != null) { _viewModel.PropertyChanged += OnViewModelPropertyChanged; } PlayCurrentStream(); } private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PlayerViewModel.StreamUrl)) { Log("ViewModel StreamUrl changed -> replaying stream"); PlayCurrentStream(); } } private void PlayCurrentStream() { if (!_initialized || _viewModel == null || _mediaPlayer == null || _libVlc == null) { Log($"PlayCurrentStream skipped: initialized={_initialized}, hasVm={_viewModel != null}, hasPlayer={_mediaPlayer != null}, hasLibVlc={_libVlc != null}"); return; } if (string.IsNullOrWhiteSpace(_viewModel.StreamUrl)) { Log("PlayCurrentStream aborted: empty StreamUrl"); _viewModel.SetPlaybackStatus("No stream URL available for this channel."); return; } try { var streamUrl = _viewModel.StreamUrl.Trim(); Log($"PlayCurrentStream start. Url={SanitizeUrlForLog(streamUrl)}"); _activeStreamUrl = streamUrl; _fallbackAttempted = false; _mediaPlayer.Stop(); _currentMedia?.Dispose(); _currentMedia = null; _viewModel.SetPlaybackStatus("Buffering stream..."); StartBufferingWatchdog(); var started = StartPlayback(streamUrl, fallback: false); Log($"Primary playback start result: {started}"); if (!started) { _viewModel.SetPlaybackStatus("Stream failed to start. Try another channel or Open Externally."); } } catch (System.Exception ex) { Log($"PlayCurrentStream exception: {ex}"); _viewModel.SetPlaybackStatus($"Embedded playback failed: {ex.Message}. Use Open Externally as a fallback."); } } private static Media BuildMediaFromStreamUrl(LibVLC libVlc, string rawStreamUrl) { // Common IPTV format: http://.../stream.m3u8|user-agent=...&referer=... var splitIndex = rawStreamUrl.IndexOf('|'); if (splitIndex <= 0) { return new Media(libVlc, rawStreamUrl, FromType.FromLocation); } var baseUrl = rawStreamUrl[..splitIndex].Trim(); var optionsSegment = rawStreamUrl[(splitIndex + 1)..].Trim(); var media = new Media(libVlc, baseUrl, FromType.FromLocation); if (string.IsNullOrWhiteSpace(optionsSegment)) { return media; } var pairs = optionsSegment.Split('&', System.StringSplitOptions.RemoveEmptyEntries | System.StringSplitOptions.TrimEntries); foreach (var pair in pairs) { var equalsIndex = pair.IndexOf('='); if (equalsIndex <= 0 || equalsIndex == pair.Length - 1) { continue; } var key = pair[..equalsIndex].Trim().ToLowerInvariant(); var value = HttpUtility.UrlDecode(pair[(equalsIndex + 1)..].Trim()); if (string.IsNullOrWhiteSpace(value)) { continue; } switch (key) { case "user-agent": case "http-user-agent": media.AddOption($":http-user-agent={value}"); break; case "referer": case "referrer": case "http-referrer": case "http-referer": media.AddOption($":http-referrer={value}"); break; case "origin": media.AddOption($":http-header=Origin={value}"); break; case "cookie": case "cookies": media.AddOption($":http-header=Cookie={value}"); break; } } return media; } private bool StartPlayback(string streamUrl, bool fallback) { if (_mediaPlayer == null || _libVlc == null) return false; Log($"StartPlayback called. fallback={fallback}, url={SanitizeUrlForLog(streamUrl)}"); _currentMedia?.Dispose(); _currentMedia = BuildMediaFromStreamUrl(_libVlc, streamUrl); _currentMedia.AddOption(":network-caching=1500"); _currentMedia.AddOption(":live-caching=1500"); _currentMedia.AddOption(":http-reconnect=true"); _currentMedia.AddOption(":codec=avcodec"); if (OperatingSystem.IsMacOS()) { _currentMedia.AddOption(":avcodec-hw=none"); } if (fallback) { _currentMedia.AddOption(":demux=any"); _currentMedia.AddOption(":hls-segment-threads=4"); _currentMedia.AddOption(":http-continuous=true"); Log("Fallback options applied: demux=any, hls-segment-threads=4, http-continuous=true"); } var started = _mediaPlayer.Play(_currentMedia); Log($"MediaPlayer.Play returned: {started}"); return started; } private void StartBufferingWatchdog() { _bufferingWatchdogCts?.Cancel(); _bufferingWatchdogCts?.Dispose(); _bufferingWatchdogCts = new CancellationTokenSource(); var token = _bufferingWatchdogCts.Token; Log("Buffering watchdog started (12s)"); _ = Task.Run(async () => { try { await Task.Delay(TimeSpan.FromSeconds(12), token); } catch (TaskCanceledException) { return; } if (token.IsCancellationRequested) return; if (_mediaPlayer == null || _viewModel == null || string.IsNullOrWhiteSpace(_activeStreamUrl)) return; if (_mediaPlayer.IsPlaying || _fallbackAttempted) return; _fallbackAttempted = true; Log("Buffering watchdog triggered fallback attempt"); Dispatcher.UIThread.Post(() => { if (_viewModel == null) return; _viewModel.SetPlaybackStatus("Still buffering, retrying with fallback settings..."); var started = StartPlayback(_activeStreamUrl!, fallback: true); Log($"Fallback playback start result: {started}"); if (!started) { _viewModel.SetPlaybackStatus("Fallback playback failed. Try Open Externally for this channel."); } }); }, token); } private void OnMediaPlayerBuffering(object? sender, MediaPlayerBufferingEventArgs e) { if (_viewModel == null) return; if (e.Cache < 1 || ((int)e.Cache % 10) == 0) { Log($"MediaPlayer buffering: {e.Cache:0}%"); } Dispatcher.UIThread.Post(() => { _viewModel.SetPlaybackStatus($"Buffering stream... {e.Cache:0}%"); }); } private void OnMediaPlayerPlaying(object? sender, System.EventArgs e) { if (_viewModel == null) return; Dispatcher.UIThread.Post(() => { _bufferingWatchdogCts?.Cancel(); Log("MediaPlayer event: Playing"); _viewModel.SetPlaybackStatus(string.Empty); }); } private void OnMediaPlayerEncounteredError(object? sender, System.EventArgs e) { if (_viewModel == null) return; Dispatcher.UIThread.Post(() => { _bufferingWatchdogCts?.Cancel(); Log("MediaPlayer event: EncounteredError"); _viewModel.SetPlaybackStatus("Playback error. The stream may be unavailable or unsupported."); }); } private void OnMediaPlayerEndReached(object? sender, System.EventArgs e) { if (_viewModel == null) return; Dispatcher.UIThread.Post(() => { _bufferingWatchdogCts?.Cancel(); Log("MediaPlayer event: EndReached"); _viewModel.SetPlaybackStatus("Stream ended."); }); } private void OnMediaPlayerStopped(object? sender, System.EventArgs e) { if (_viewModel == null) return; Dispatcher.UIThread.Post(() => { _bufferingWatchdogCts?.Cancel(); Log("MediaPlayer event: Stopped"); if (string.IsNullOrWhiteSpace(_viewModel.PlaybackStatus)) { _viewModel.SetPlaybackStatus("Playback stopped."); } }); } private static string SanitizeUrlForLog(string url) { if (string.IsNullOrWhiteSpace(url)) return ""; var trimmed = url.Trim(); var tokenIndex = trimmed.IndexOf("token=", System.StringComparison.OrdinalIgnoreCase); if (tokenIndex >= 0) { return ""; } var splitIndex = trimmed.IndexOf('|'); return splitIndex > 0 ? trimmed[..splitIndex] + "|" : trimmed; } private static void Log(string message) { var line = $"[PlayerView] {DateTime.Now:HH:mm:ss.fff} {message}"; Debug.WriteLine(line); Console.WriteLine(line); } }