From 7a6bb5ef5d31c033287ae0b5132540474a0eca0d Mon Sep 17 00:00:00 2001 From: Ledrunning Date: Mon, 4 May 2026 22:06:13 +0200 Subject: [PATCH 1/2] Add hiding panel bug fixing --- TinyPlayer.Core/VideoPlayerCore.cs | 18 ++++ TinyPlayer.Desktop/Controls/VideoHost.cs | 46 +++++---- TinyPlayer.Desktop/View/MainWindow.xaml | 21 +++- TinyPlayer.Desktop/View/MainWindow.xaml.cs | 68 +++++++++---- TinyPlayer.Desktop/ViewModel/MainViewModel.cs | 95 +++++++++++++++---- 5 files changed, 188 insertions(+), 60 deletions(-) diff --git a/TinyPlayer.Core/VideoPlayerCore.cs b/TinyPlayer.Core/VideoPlayerCore.cs index fd59e82..fe5d56a 100644 --- a/TinyPlayer.Core/VideoPlayerCore.cs +++ b/TinyPlayer.Core/VideoPlayerCore.cs @@ -353,4 +353,22 @@ private void AnalyseStreams() StreamsAnalysed?.Invoke(this, new StreamsAnalysedEventArgs(metadata, sb.ToString())); } + + public void SetRenderSize(int w, int h) + { + if (_playbin == null) + { + return; + } + + var overlay = ((Bin)_playbin)?.GetByInterface(VideoOverlayAdapter.GType); + if (overlay == null) + { + return; + } + + var adapter = new VideoOverlayAdapter(overlay.Handle); + adapter.SetRenderRectangle(0, 0, w, h); + adapter.Expose(); + } } \ No newline at end of file diff --git a/TinyPlayer.Desktop/Controls/VideoHost.cs b/TinyPlayer.Desktop/Controls/VideoHost.cs index 1b8f3ca..a810897 100644 --- a/TinyPlayer.Desktop/Controls/VideoHost.cs +++ b/TinyPlayer.Desktop/Controls/VideoHost.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using System.Windows; using System.Windows.Interop; namespace TinyPlayer.Desktop.Controls; @@ -8,9 +9,7 @@ namespace TinyPlayer.Desktop.Controls; /// public class VideoHost : HwndHost { - // Win32 private const int WsChild = 0x40000000; - private const int WsVisible = 0x10000000; public new IntPtr Handle { get; private set; } @@ -18,19 +17,33 @@ public class VideoHost : HwndHost protected override HandleRef BuildWindowCore(HandleRef hwndParent) { Handle = CreateWindowEx( - 0, - "static", - "", + 0, "static", "", WsChild | WsVisible, 0, 0, 100, 100, hwndParent.Handle, - IntPtr.Zero, - IntPtr.Zero, - 0); + IntPtr.Zero, IntPtr.Zero, 0); return new HandleRef(this, Handle); } + // При ресайзе WPF элемента — двигаем нативное окно под новый размер + protected override void OnRenderSizeChanged(SizeChangedInfo info) + { + base.OnRenderSizeChanged(info); + if (Handle == IntPtr.Zero) + { + return; + } + + var w = (int)info.NewSize.Width; + var h = (int)info.NewSize.Height; + + if (w > 0 && h > 0) + { + MoveWindow(Handle, 0, 0, w, h, true); + } + } + protected override void DestroyWindowCore(HandleRef hwnd) { DestroyWindow(hwnd.Handle); @@ -38,17 +51,14 @@ protected override void DestroyWindowCore(HandleRef hwnd) [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr CreateWindowEx( - int exStyle, - string className, - string windowName, - int style, - int x, int y, - int width, int height, - IntPtr parentHandle, - IntPtr menu, - IntPtr instance, - object param); + int exStyle, string className, string windowName, int style, + int x, int y, int width, int height, + IntPtr parentHandle, IntPtr menu, IntPtr instance, object param); [DllImport("user32.dll")] private static extern bool DestroyWindow(IntPtr hwnd); + + [DllImport("user32.dll")] + private static extern bool MoveWindow( + IntPtr hwnd, int x, int y, int width, int height, bool repaint); } \ No newline at end of file diff --git a/TinyPlayer.Desktop/View/MainWindow.xaml b/TinyPlayer.Desktop/View/MainWindow.xaml index 80002ab..3bfddfa 100644 --- a/TinyPlayer.Desktop/View/MainWindow.xaml +++ b/TinyPlayer.Desktop/View/MainWindow.xaml @@ -16,7 +16,14 @@ WindowBackdropType="Mica" WindowCornerPreference="Round" WindowStartupLocation="CenterScreen" + WindowState="{Binding WindowState, Mode=TwoWay}" + WindowStyle="{Binding WindowStyle, Mode=TwoWay}" mc:Ignorable="d"> + + + + + @@ -27,7 +34,8 @@ + Margin="0,0,0,0" + Visibility="{Binding TitleBarVisibility}"> - + @@ -63,7 +75,8 @@ Grid.Row="2" Margin="12,0,12,12" Background="{DynamicResource ControlFillColorSecondaryBrush}" - CornerRadius="12,12,0,0"> + CornerRadius="12,12,0,0" + Visibility="{Binding IsControlsVisible, Converter={StaticResource BooleanToVisibilityConverter}}"> @@ -162,4 +175,4 @@ - + \ No newline at end of file diff --git a/TinyPlayer.Desktop/View/MainWindow.xaml.cs b/TinyPlayer.Desktop/View/MainWindow.xaml.cs index 0ebc56d..7c3a7a8 100644 --- a/TinyPlayer.Desktop/View/MainWindow.xaml.cs +++ b/TinyPlayer.Desktop/View/MainWindow.xaml.cs @@ -1,39 +1,65 @@ using System.Windows.Input; using TinyPlayer.Desktop.ViewModel; using Wpf.Ui.Controls; +using KeyEventArgs = System.Windows.Input.KeyEventArgs; +using MouseEventArgs = System.Windows.Input.MouseEventArgs; -namespace TinyPlayer.Desktop.View +namespace TinyPlayer.Desktop.View; + +/// +/// Interaction logic for MainWindow.xaml +/// +public partial class MainWindow : FluentWindow { - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class MainWindow : FluentWindow + public MainWindow(MainViewModel viewModel) { - public MainViewModel Vm { get; } + InitializeComponent(); - public MainWindow(MainViewModel viewModel) - { - InitializeComponent(); + Vm = viewModel; + DataContext = Vm; - Vm = viewModel; - DataContext = Vm; + Loaded += (_, _) => { Vm.SetVideoHandle(VideoSurface.Handle); }; + } - Loaded += (_, _) => - { - Vm.SetVideoHandle(VideoSurface.Handle); - }; - } + public MainViewModel Vm { get; } + + private void OnSeekStarted(object sender, MouseButtonEventArgs e) + { + Vm.BeginSeek(); + } + + private void OnSeekCompleted(object sender, MouseButtonEventArgs e) + { + Vm.EndSeek(); + } - private void OnSeekStarted(object sender, MouseButtonEventArgs e) + private void Window_Closed(object sender, EventArgs e) + { + Vm.Dispose(); + } + + private void VideoGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + Vm.OnVideoClick(); + + if (e.ClickCount == 2) { - Vm.BeginSeek(); + Vm.ToggleFullscreenCommand.Execute(null); } + } - private void OnSeekCompleted(object sender, MouseButtonEventArgs e) + protected override void OnKeyDown(KeyEventArgs e) + { + if (e.Key == Key.Escape) { - Vm.EndSeek(); + Vm.ToggleFullscreenCommand.Execute(null); } - private void Window_Closed(object sender, EventArgs e) => Vm.Dispose(); + base.OnKeyDown(e); + } + + private void VideoGrid_MouseMove(object sender, MouseEventArgs e) + { + Vm.OnVideoClick(); } } \ No newline at end of file diff --git a/TinyPlayer.Desktop/ViewModel/MainViewModel.cs b/TinyPlayer.Desktop/ViewModel/MainViewModel.cs index de375a9..b0252f4 100644 --- a/TinyPlayer.Desktop/ViewModel/MainViewModel.cs +++ b/TinyPlayer.Desktop/ViewModel/MainViewModel.cs @@ -1,4 +1,5 @@ using System.Windows; +using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Gst; @@ -17,11 +18,20 @@ public partial class MainViewModel : BaseViewModel private const int SeekDebounce = 100; private const int MaxVolumePercentage = 100; private readonly List _allSubtitleTracks = []; + + [ObservableProperty] private Visibility _controlsVisibility = Visibility.Visible; + private bool _coreUpdating; + [ObservableProperty] private long _duration; + private DispatcherTimer? _hideControlsTimer; + private nint _hwnd; + + [ObservableProperty] private bool _isControlsVisible = true; + private bool _isInternalUpdate; [ObservableProperty] private bool _isMuted; @@ -46,8 +56,37 @@ public partial class MainViewModel : BaseViewModel [ObservableProperty] private string _timeText = "00:00 / 00:00"; + [ObservableProperty] private Visibility _titleBarVisibility = Visibility.Visible; + [ObservableProperty] private double _volume = MaxVolumePercentage; + [ObservableProperty] private WindowState _windowState = WindowState.Normal; + + [ObservableProperty] private WindowStyle _windowStyle = WindowStyle.SingleBorderWindow; + + [RelayCommand] + private void ToggleFullscreen() + { + var isFullscreen = WindowState == WindowState.Maximized; + + if (!isFullscreen) + { + // Switch to fullscreen — first set to None, then Maximised; + // otherwise the taskbar won't be hidden + WindowStyle = WindowStyle.None; + WindowState = WindowState.Maximized; + TitleBarVisibility = Visibility.Collapsed; + ControlsVisibility = Visibility.Collapsed; + } + else + { + WindowStyle = WindowStyle.SingleBorderWindow; + WindowState = WindowState.Normal; + TitleBarVisibility = Visibility.Visible; + ControlsVisibility = Visibility.Visible; + } + } + [RelayCommand(CanExecute = nameof(HasCore))] private void TogglePlayPause() { @@ -128,11 +167,31 @@ private void LoadUri(string uri) ToggleMuteCommand.NotifyCanExecuteChanged(); } + public void OnVideoClick() + { + IsControlsVisible = true; + + _hideControlsTimer?.Stop(); + _hideControlsTimer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(3) + }; + _hideControlsTimer.Tick += (_, _) => + { + _hideControlsTimer.Stop(); + if (IsPlaying) + { + IsControlsVisible = false; + } + }; + _hideControlsTimer.Start(); + } + private void OnStreamsAnalysed(object? sender, StreamsAnalysedEventArgs e) { Application.Current.Dispatcher.Invoke(() => { - _streamsInitialized = false; // Temporarily suspending the response to changes whilst we compile the lists + _streamsInitialized = false; _isInternalUpdate = true; try @@ -141,7 +200,6 @@ private void OnStreamsAnalysed(object? sender, StreamsAnalysedEventArgs e) SubtitleTracks.Clear(); _allSubtitleTracks.Clear(); - // Fill in with pre-defined names from the metadata foreach (var track in e.Metadata.AudioTracks) { AudioTracks.Add(track); @@ -152,28 +210,31 @@ private void OnStreamsAnalysed(object? sender, StreamsAnalysedEventArgs e) _allSubtitleTracks.Add(track); } - // Restore the current audio track from the playbin + // Restore the audio track and apply it SelectedAudioTrack = AudioTracks .FirstOrDefault(t => t.Index == e.Metadata.CurrentAudioIndex) ?? AudioTracks.FirstOrDefault(); - // Subtitles - fill in the list but do NOT switch tracks - if (SubtitlesEnabled) + if (SelectedAudioTrack != null) + { + Core?.SetAudioTrack(SelectedAudioTrack.Index); + } + + // Subtitles — populate the list but do not enable them + // Enable only via the CC button using SubtitlesEnabled + foreach (var item in _allSubtitleTracks) { - foreach (var item in _allSubtitleTracks) - { - SubtitleTracks.Add(item); - } - - // Retrieve the current subtitle from the playbin - // If CurrentSubtitleIndex == -1, there are no subtitles - SelectedSubtitleTrack = e.Metadata.CurrentSubtitleIndex >= 0 - ? SubtitleTracks.FirstOrDefault(t => t.Index == e.Metadata.CurrentSubtitleIndex) - : SubtitleTracks.FirstOrDefault(); + SubtitleTracks.Add(item); } - else + + SelectedSubtitleTrack = SubtitleTracks.FirstOrDefault(); + + // Set the current state of SubtitlesEnabled + Core?.SetSubtitlesEnabled(SubtitlesEnabled); + + if (SubtitlesEnabled && SelectedSubtitleTrack != null) { - SelectedSubtitleTrack = null; + Core?.SetSubtitleTrack(SelectedSubtitleTrack.Index); } } finally From 2f9576aea32fa8c0768fcab9f6251d0370e27eff Mon Sep 17 00:00:00 2001 From: Ledrunning Date: Thu, 7 May 2026 21:52:05 +0200 Subject: [PATCH 2/2] Minor refactoring for beta version --- TinyPlayer.Core/Extensions/TrackResolver.cs | 15 +++---- TinyPlayer.Core/Models/MetadataLanguage.cs | 9 +++++ TinyPlayer.Core/VideoPlayerCore.cs | 16 +++++--- TinyPlayer.Desktop/Controls/VideoHost.cs | 2 +- TinyPlayer.Desktop/ViewModel/BaseViewModel.cs | 2 + TinyPlayer.Desktop/ViewModel/MainViewModel.cs | 39 ++++--------------- 6 files changed, 39 insertions(+), 44 deletions(-) create mode 100644 TinyPlayer.Core/Models/MetadataLanguage.cs diff --git a/TinyPlayer.Core/Extensions/TrackResolver.cs b/TinyPlayer.Core/Extensions/TrackResolver.cs index aa6b707..18c4341 100644 --- a/TinyPlayer.Core/Extensions/TrackResolver.cs +++ b/TinyPlayer.Core/Extensions/TrackResolver.cs @@ -1,10 +1,11 @@ using System.Globalization; +using TinyPlayer.Core.Models; namespace TinyPlayer.Core.Extensions; public static class TrackResolver { - public static string IsoToLanguageName(this string code, string? codec, uint bitrate, int index, string prefix) + public static string IsoToLanguageName(this string code, MetadataLanguage metadata) { string? name = null; @@ -18,14 +19,14 @@ public static string IsoToLanguageName(this string code, string? codec, uint bit return name; } - // Fallback — кодек + битрейт - if (!string.IsNullOrEmpty(codec)) + // Fallback codec + bitrate + if (!string.IsNullOrEmpty(metadata.Codec)) { - return bitrate > 0 - ? $"{codec} {bitrate / 1000}kbps" - : codec; + return metadata.Bitrate > 0 + ? $"{metadata.Codec} {metadata.Bitrate / 1000}kbps" + : metadata.Codec; } - return $"{prefix} {index}"; + return $"{metadata.Prefix} {metadata.Index}"; } } \ No newline at end of file diff --git a/TinyPlayer.Core/Models/MetadataLanguage.cs b/TinyPlayer.Core/Models/MetadataLanguage.cs new file mode 100644 index 0000000..3b88bef --- /dev/null +++ b/TinyPlayer.Core/Models/MetadataLanguage.cs @@ -0,0 +1,9 @@ +namespace TinyPlayer.Core.Models; + +public class MetadataLanguage +{ + public string? Codec { get; set; } + public int Index { get; set; } + public uint Bitrate { get; set; } + public string? Prefix { get; set; } +} \ No newline at end of file diff --git a/TinyPlayer.Core/VideoPlayerCore.cs b/TinyPlayer.Core/VideoPlayerCore.cs index fe5d56a..7a6e9f3 100644 --- a/TinyPlayer.Core/VideoPlayerCore.cs +++ b/TinyPlayer.Core/VideoPlayerCore.cs @@ -133,7 +133,7 @@ private void InitPipeline(string uri) _playbin.SetProperty("flags", new Value((uint)AvFlagsType.EnableAllFlags)); - // Waiting for the actual Paused evenе only then is the duration known + // Waiting for the actual Paused even only then is the duration known _playbin.SetState(State.Paused); _playbin.GetState(out _, out _, Constants.SECOND * 5); _playbin.SetState(State.Playing); @@ -286,8 +286,8 @@ private void ApplicationCb(object o, SignalArgs args) private static void TagsCb(object sender, SignalArgs args) { - var el = sender as Element; - el?.PostMessage(Message.NewApplication(el, new Structure("tags-changed"))); + var element = sender as Element; + element?.PostMessage(Message.NewApplication(element, new Structure("tags-changed"))); } private void AnalyseStreams() @@ -328,13 +328,19 @@ private void AnalyseStreams() } // Title: Language (if available), otherwise codec, otherwise "Track N" - var title = lang?.IsoToLanguageName(codec, rate, i, "Track"); + var title = lang?.IsoToLanguageName(new MetadataLanguage + { + Codec = codec, + Bitrate = rate, + Index = i, + Prefix = "Track" + }); metadata.AudioTracks.Add(new StreamItem { Index = i, Title = title }); sb.AppendLine($"Audio {i}: {title}"); } - // Subtitles + // Subtitles for (var i = 0; i < metadata.NumOfSubtitles; i++) { var tags = (TagList)_playbin.Emit("get-text-tags", i); diff --git a/TinyPlayer.Desktop/Controls/VideoHost.cs b/TinyPlayer.Desktop/Controls/VideoHost.cs index a810897..19312da 100644 --- a/TinyPlayer.Desktop/Controls/VideoHost.cs +++ b/TinyPlayer.Desktop/Controls/VideoHost.cs @@ -26,7 +26,7 @@ protected override HandleRef BuildWindowCore(HandleRef hwndParent) return new HandleRef(this, Handle); } - // При ресайзе WPF элемента — двигаем нативное окно под новый размер + // When resizing a WPF element, resize the native window to fit the new dimensions protected override void OnRenderSizeChanged(SizeChangedInfo info) { base.OnRenderSizeChanged(info); diff --git a/TinyPlayer.Desktop/ViewModel/BaseViewModel.cs b/TinyPlayer.Desktop/ViewModel/BaseViewModel.cs index c79eef6..42e58aa 100644 --- a/TinyPlayer.Desktop/ViewModel/BaseViewModel.cs +++ b/TinyPlayer.Desktop/ViewModel/BaseViewModel.cs @@ -12,6 +12,8 @@ public abstract class BaseViewModel : ObservableObject, IDisposable public ObservableCollection AudioTracks { get; } = []; public ObservableCollection SubtitleTracks { get; } = []; + public readonly List AllSubtitleTracks = []; + public void Dispose() { Core?.Dispose(); diff --git a/TinyPlayer.Desktop/ViewModel/MainViewModel.cs b/TinyPlayer.Desktop/ViewModel/MainViewModel.cs index b0252f4..95f7996 100644 --- a/TinyPlayer.Desktop/ViewModel/MainViewModel.cs +++ b/TinyPlayer.Desktop/ViewModel/MainViewModel.cs @@ -1,4 +1,5 @@ -using System.Windows; +using System.Diagnostics; +using System.Windows; using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -17,51 +18,28 @@ public partial class MainViewModel : BaseViewModel private const double MaxVolumeDelta = 100.0; private const int SeekDebounce = 100; private const int MaxVolumePercentage = 100; - private readonly List _allSubtitleTracks = []; [ObservableProperty] private Visibility _controlsVisibility = Visibility.Visible; - private bool _coreUpdating; - - [ObservableProperty] private long _duration; - private DispatcherTimer? _hideControlsTimer; - private nint _hwnd; - [ObservableProperty] private bool _isControlsVisible = true; - private bool _isInternalUpdate; - [ObservableProperty] private bool _isMuted; - [ObservableProperty] private bool _isPlaying; - private bool _isUserSeeking; - [ObservableProperty] private long _position; - private CancellationTokenSource? _seekCts; - [ObservableProperty] private StreamItem? _selectedAudioTrack; - [ObservableProperty] private StreamItem? _selectedSubtitleTrack; - [ObservableProperty] private string _streamInfo = string.Empty; - private bool _streamsInitialized; - [ObservableProperty] private bool _subtitlesEnabled; - [ObservableProperty] private string _timeText = "00:00 / 00:00"; - [ObservableProperty] private Visibility _titleBarVisibility = Visibility.Visible; - [ObservableProperty] private double _volume = MaxVolumePercentage; - [ObservableProperty] private WindowState _windowState = WindowState.Normal; - [ObservableProperty] private WindowStyle _windowStyle = WindowStyle.SingleBorderWindow; [RelayCommand] @@ -143,7 +121,6 @@ private void LoadUri(string uri) Core?.StreamsAnalysed -= OnStreamsAnalysed; Core?.StreamsAnalysed += OnStreamsAnalysed; - //TODO : add custom message box! Core?.ErrorOccurred += msg => Application.Current?.Dispatcher.Invoke(() => MessageBox.Show(msg, "Playback error", @@ -198,7 +175,7 @@ private void OnStreamsAnalysed(object? sender, StreamsAnalysedEventArgs e) { AudioTracks.Clear(); SubtitleTracks.Clear(); - _allSubtitleTracks.Clear(); + AllSubtitleTracks.Clear(); foreach (var track in e.Metadata.AudioTracks) { @@ -207,7 +184,7 @@ private void OnStreamsAnalysed(object? sender, StreamsAnalysedEventArgs e) foreach (var track in e.Metadata.SubtitleTracks) { - _allSubtitleTracks.Add(track); + AllSubtitleTracks.Add(track); } // Restore the audio track and apply it @@ -222,7 +199,7 @@ private void OnStreamsAnalysed(object? sender, StreamsAnalysedEventArgs e) // Subtitles — populate the list but do not enable them // Enable only via the CC button using SubtitlesEnabled - foreach (var item in _allSubtitleTracks) + foreach (var item in AllSubtitleTracks) { SubtitleTracks.Add(item); } @@ -258,7 +235,7 @@ private void ApplySubtitleFilter() return; } - foreach (var item in _allSubtitleTracks) + foreach (var item in AllSubtitleTracks) { SubtitleTracks.Add(item); } @@ -318,9 +295,9 @@ private async Task DebouncedSeek(long value) Core?.SeekTo((int)value); } } - catch (TaskCanceledException) + catch (TaskCanceledException e) { - // TODO: add log + Trace.TraceError(e.Message); } }