From 5e115c90ac63f87e5d49d9c1af0fbaeb3a7bccab Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Sun, 29 Mar 2026 22:17:43 +0200 Subject: [PATCH 01/15] feat: replace Windows MediaPlayer with LibVLCSharp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows Media Foundation causes 30-second freezes when opening or seeking large .m4b files — the same freeze reproduces in Windows Media Player itself. VLC opens the same files instantly. Replace the audio backend with LibVLCSharp + VideoLAN.LibVLC.Windows: - LibVLC runs its own threaded demuxer/decoder, bypassing WMF entirely - Remove hidden MediaPlayerElement from PlayerControlGrid and LegacyPlayerPage - Add public Play(), Pause(), NaturalDuration on PlayerViewModel so external callers no longer reference the player backend directly - EndReached dispatches to UI thread to avoid LibVLC deadlock Co-Authored-By: Claude Sonnet 4.6 --- Audibly.App/Audibly.App.csproj | 10 +- .../UserControls/PlaySkipButtonsStack.xaml.cs | 8 +- .../UserControls/PlayerControlGrid.xaml | 5 - .../UserControls/PlayerControlGrid.xaml.cs | 1 - Audibly.App/ViewModels/MainViewModel.cs | 4 +- Audibly.App/ViewModels/PlayerViewModel.cs | 355 ++++++++++-------- .../Views/Legacy/LegacyPlayerPage.xaml | 7 - .../Views/Legacy/LegacyPlayerPage.xaml.cs | 9 +- Audibly.App/Views/LibraryCardPage.xaml.cs | 2 +- 9 files changed, 210 insertions(+), 191 deletions(-) diff --git a/Audibly.App/Audibly.App.csproj b/Audibly.App/Audibly.App.csproj index 9b1030e..bf1bd9b 100644 --- a/Audibly.App/Audibly.App.csproj +++ b/Audibly.App/Audibly.App.csproj @@ -106,10 +106,12 @@ - - - - + + + + + + diff --git a/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs b/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs index d3fae28..7cb4431 100644 --- a/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs +++ b/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs @@ -49,9 +49,9 @@ public double PlayButtonSize private void PlayPauseButton_OnClick(object sender, RoutedEventArgs e) { if (PlayerViewModel.PlayPauseIcon == Symbol.Play) - PlayerViewModel.MediaPlayer.Play(); + PlayerViewModel.Play(); else - PlayerViewModel.MediaPlayer.Pause(); + PlayerViewModel.Pause(); } private async void PreviousChapterButton_Click(object sender, RoutedEventArgs e) @@ -140,9 +140,9 @@ private async void SkipForwardButton_OnClick(object sender, RoutedEventArgs e) { // todo: might need to switch this to using the duration from the audiobook record PlayerViewModel.CurrentPosition = PlayerViewModel.CurrentPosition + _skipForwardButtonAmount <= - PlayerViewModel.MediaPlayer.PlaybackSession.NaturalDuration + PlayerViewModel.NaturalDuration ? PlayerViewModel.CurrentPosition + _skipForwardButtonAmount - : PlayerViewModel.MediaPlayer.PlaybackSession.NaturalDuration; + : PlayerViewModel.NaturalDuration; await PlayerViewModel.NowPlaying.SaveAsync(); } diff --git a/Audibly.App/UserControls/PlayerControlGrid.xaml b/Audibly.App/UserControls/PlayerControlGrid.xaml index 1aacba0..d180822 100644 --- a/Audibly.App/UserControls/PlayerControlGrid.xaml +++ b/Audibly.App/UserControls/PlayerControlGrid.xaml @@ -33,11 +33,6 @@ - - diff --git a/Audibly.App/ViewModels/MainViewModel.cs b/Audibly.App/ViewModels/MainViewModel.cs index 10ef5c4..8499e26 100644 --- a/Audibly.App/ViewModels/MainViewModel.cs +++ b/Audibly.App/ViewModels/MainViewModel.cs @@ -329,7 +329,7 @@ public async Task DeleteAudiobookAsync() if (SelectedAudiobook == App.PlayerViewModel.NowPlaying) _dispatcherQueue.TryEnqueue(() => { - App.PlayerViewModel.MediaPlayer.Pause(); + App.PlayerViewModel.Pause(); App.PlayerViewModel.NowPlaying.IsNowPlaying = false; App.PlayerViewModel.NowPlaying = null; }); @@ -366,7 +366,7 @@ public async void DeleteAudiobooksAsync() { await _dispatcherQueue.EnqueueAsync(() => { - App.PlayerViewModel.MediaPlayer.Pause(); + App.PlayerViewModel.Pause(); App.PlayerViewModel.NowPlaying = null; SelectedAudiobook = null; IsLoading = true; diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index df0c132..6516a6c 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -6,12 +6,11 @@ using System.Linq; using System.Threading.Tasks; using System.Timers; -using Windows.Media.Core; -using Windows.Media.Playback; using Audibly.App.Extensions; using Audibly.App.Helpers; using Audibly.App.Services; using CommunityToolkit.WinUI; +using LibVLCSharp.Shared; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Controls; @@ -21,10 +20,8 @@ public class PlayerViewModel : BindableBase, IDisposable { private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - /// - /// Gets the app-wide MediaPlayer instance. - /// - public readonly MediaPlayer MediaPlayer = new(); + private readonly LibVLC _libVLC; + private readonly LibVLCSharp.Shared.MediaPlayer _mediaPlayer; private int _chapterComboSelectedIndex; @@ -60,8 +57,12 @@ public class PlayerViewModel : BindableBase, IDisposable private string _volumeLevelGlyph = Constants.VolumeGlyph3; + private bool _mediaJustOpened; + public PlayerViewModel() { + _libVLC = new LibVLC("--no-video"); + _mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC); InitializeAudioPlayer(); } @@ -211,10 +212,16 @@ public int ChapterComboSelectedIndex /// public TimeSpan CurrentPosition { - get => MediaPlayer.PlaybackSession.Position; - set => MediaPlayer.PlaybackSession.Position = value > TimeSpan.Zero ? value : TimeSpan.Zero; + get => TimeSpan.FromMilliseconds(_mediaPlayer.Time >= 0 ? _mediaPlayer.Time : 0); + set => _mediaPlayer.Time = (long)Math.Max(value.TotalMilliseconds, 0); } + /// + /// Gets the natural duration of the currently loaded media. + /// + public TimeSpan NaturalDuration => + TimeSpan.FromMilliseconds(_mediaPlayer.Length >= 0 ? _mediaPlayer.Length : 0); + /// /// public bool IsTimerActive @@ -231,23 +238,174 @@ public string TimerRemainingText private set => OnPropertyChanged(); } + /// + /// Starts or resumes playback. + /// + public void Play() + { + _mediaPlayer.Play(); + } + + /// + /// Pauses playback. + /// + public void Pause() + { + _mediaPlayer.Pause(); + } + #region methods private void InitializeAudioPlayer() { - MediaPlayer.AutoPlay = false; - MediaPlayer.AudioCategory = MediaPlayerAudioCategory.Media; - MediaPlayer.AudioDeviceType = MediaPlayerAudioDeviceType.Multimedia; - MediaPlayer.CommandManager.IsEnabled = true; // todo: what is this? - MediaPlayer.MediaOpened += AudioPlayer_MediaOpened; - MediaPlayer.MediaEnded += AudioPlayer_MediaEnded; - MediaPlayer.MediaFailed += AudioPlayer_MediaFailed; - MediaPlayer.PlaybackSession.PositionChanged += PlaybackSession_PositionChanged; - MediaPlayer.PlaybackSession.PlaybackStateChanged += PlaybackSession_PlaybackStateChanged; + // LibVLC events fire on background threads — dispatch to UI thread. + _mediaPlayer.Playing += OnPlaying; + _mediaPlayer.Paused += OnPaused; + _mediaPlayer.EndReached += OnEndReached; + _mediaPlayer.EncounteredError += OnEncounteredError; + _mediaPlayer.TimeChanged += OnTimeChanged; + } + + private void OnPlaying(object? sender, EventArgs e) + { + _dispatcherQueue.TryEnqueue(() => + { + if (PlayPauseIcon == Symbol.Pause) return; + PlayPauseIcon = Symbol.Pause; + }); + + // Handle "media opened" logic on first Playing event after setting new media + if (_mediaJustOpened) + { + _mediaJustOpened = false; + HandleMediaOpened(); + } + } + + private void OnPaused(object? sender, EventArgs e) + { + _dispatcherQueue.TryEnqueue(() => + { + if (PlayPauseIcon == Symbol.Play) return; + PlayPauseIcon = Symbol.Play; + }); + } + + private void OnEndReached(object? sender, EventArgs e) + { + // CRITICAL: Must not call Play/SetMedia from EndReached handler — LibVLC deadlocks. + // Dispatch to UI thread to defer the operation. + _dispatcherQueue.TryEnqueue(() => HandleMediaEnded()); + } + + private void OnEncounteredError(object? sender, EventArgs e) + { + _dispatcherQueue.TryEnqueue(() => NowPlaying = null); + + App.ViewModel.EnqueueNotification(new Notification + { + Message = + "Unable to open the audiobook: media failed. Please verify that the file is not corrupted and try again.", + Severity = InfoBarSeverity.Error + }); + } + + private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) + { + if (NowPlaying == null) return; + + var currentPositionMs = e.Time; + + if (!NowPlaying.CurrentChapter.InRange(currentPositionMs)) + { + var newChapter = NowPlaying.Chapters.FirstOrDefault(c => + c.ParentSourceFileIndex == NowPlaying.CurrentSourceFileIndex && + c.InRange(currentPositionMs)); - // set volume level from settings - // UpdateVolume(UserSettings.Volume); - // UpdatePlaybackSpeed(UserSettings.PlaybackSpeed); + if (newChapter != null) + _ = _dispatcherQueue.EnqueueAsync(() => + { + NowPlaying.CurrentChapterIndex = ChapterComboSelectedIndex = newChapter.Index; + NowPlaying.CurrentChapterTitle = newChapter.Title; + ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); + }); + } + + _ = _dispatcherQueue.EnqueueAsync(async () => + { + ChapterPositionMs = (int)(currentPositionMs > NowPlaying.CurrentChapter.StartTime + ? currentPositionMs - NowPlaying.CurrentChapter.StartTime + : 0); + NowPlaying.CurrentTimeMs = (int)currentPositionMs; + + // TODO: this is gross + // calculate/update progress + double tmp = 0; + if (NowPlaying.CurrentSourceFileIndex != 0) + for (var i = 0; i < NowPlaying.CurrentSourceFileIndex; i++) + tmp += NowPlaying.SourcePaths[i].Duration; + tmp += currentPositionMs / 1000.0; + NowPlaying.Progress = Math.Ceiling(tmp / NowPlaying.Duration * 100); + NowPlaying.IsCompleted = NowPlaying.Progress >= 99.9; + }); + + _ = Task.Run(async () => await NowPlaying.SaveAsync()); + } + + private void HandleMediaOpened() + { + if (NowPlaying == null) return; + _dispatcherQueue.EnqueueAsync(async () => + { + if (NowPlaying.Chapters.Count == 0) + { + NowPlaying = null; + + await DialogService.ShowErrorDialogAsync("Error", + "An error occurred while trying to open the selected audiobook. " + + "The chapters could not be loaded. Please try importing the audiobook again."); + + return; + } + + ChapterComboSelectedIndex = NowPlaying.CurrentChapterIndex ?? 0; + + ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); + + ChapterPositionMs = + NowPlaying.CurrentTimeMs > NowPlaying.CurrentChapter.StartTime + ? (int)(NowPlaying.CurrentTimeMs - NowPlaying.CurrentChapter.StartTime) + : 0; + + // Seek to saved position + _mediaPlayer.Time = NowPlaying.CurrentTimeMs; + + _mediaPlayer.SetRate((float)PlaybackSpeed); + + if (_pendingAutoPlay) + { + _pendingAutoPlay = false; + // Already playing since we used _mediaPlayer.Play() to open + } + else + { + // We started playing to trigger media load — pause now since user didn't press play + _mediaPlayer.Pause(); + } + }); + } + + private void HandleMediaEnded() + { + // check if there is a next source file + if (NowPlaying == null || NowPlaying.CurrentSourceFileIndex >= NowPlaying.SourcePaths.Count - 1) return; + + // todo: log error here + if (NowPlaying.CurrentChapterIndex == null) return; + + _pendingAutoPlay = true; + + OpenSourceFile(NowPlaying.CurrentSourceFileIndex + 1, (int)NowPlaying.CurrentChapterIndex + 1); } public void SetTimer(double seconds) @@ -289,7 +447,7 @@ private void SleepTimer_Elapsed(object? sender, ElapsedEventArgs e) // Timer expired - pause playback _dispatcherQueue.TryEnqueue(() => { - MediaPlayer.Pause(); + _mediaPlayer.Pause(); IsTimerActive = false; _sleepTimer?.Stop(); _sleepTimer?.Dispose(); @@ -316,12 +474,16 @@ public void Dispose() { _sleepTimer?.Stop(); _sleepTimer?.Dispose(); + + _mediaPlayer.Stop(); + _mediaPlayer.Dispose(); + _libVLC.Dispose(); } public async void UpdateVolume(double volume) { VolumeLevel = volume; - MediaPlayer.Volume = volume / 100; + _mediaPlayer.Volume = (int)volume; VolumeLevelGlyph = volume switch { > 66 => Constants.VolumeGlyph3, @@ -341,7 +503,7 @@ public async void UpdateVolume(double volume) public async void UpdatePlaybackSpeed(double speed) { PlaybackSpeed = speed; - MediaPlayer.PlaybackRate = speed; + _mediaPlayer.SetRate((float)speed); // save playback speed for audiobook if (NowPlaying == null) return; @@ -364,7 +526,7 @@ public async Task OpenAudiobook(AudiobookViewModel audiobook) await NowPlaying.SaveAsync(); } - MediaPlayer.Pause(); + _mediaPlayer.Pause(); App.ViewModel.SelectedAudiobook = audiobook; @@ -407,7 +569,9 @@ await _dispatcherQueue.EnqueueAsync(async () => await NowPlaying.SaveAsync(); }); - MediaPlayer.Source = MediaSource.CreateFromUri(audiobook.CurrentSourceFile.FilePath.AsUri()); + _mediaJustOpened = true; + using var media = new Media(_libVLC, audiobook.CurrentSourceFile.FilePath.AsUri()); + _mediaPlayer.Play(media); } public async void OpenSourceFile(int index, int chapterIndex) @@ -426,7 +590,9 @@ await _dispatcherQueue.EnqueueAsync(() => await NowPlaying.SaveAsync(); - MediaPlayer.Source = MediaSource.CreateFromUri(NowPlaying.CurrentSourceFile.FilePath.AsUri()); + _mediaJustOpened = true; + using var media = new Media(_libVLC, NowPlaying.CurrentSourceFile.FilePath.AsUri()); + _mediaPlayer.Play(media); } /// @@ -446,140 +612,7 @@ public async Task SeekToPositionAsync(double sliderValue) #region event handlers - private void AudioPlayer_MediaOpened(MediaPlayer sender, object args) - { - if (NowPlaying == null) return; - _dispatcherQueue.EnqueueAsync(async () => - { - if (NowPlaying.Chapters.Count == 0) - { - NowPlaying = null; - - // note: content dialog - await DialogService.ShowErrorDialogAsync("Error", - "An error occurred while trying to open the selected audiobook. " + - "The chapters could not be loaded. Please try importing the audiobook again."); - - return; - } - - ChapterComboSelectedIndex = NowPlaying.CurrentChapterIndex ?? 0; - - ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); - - ChapterPositionMs = - NowPlaying.CurrentTimeMs > NowPlaying.CurrentChapter.StartTime - ? (int)(NowPlaying.CurrentTimeMs - NowPlaying.CurrentChapter.StartTime) - : 0; - - CurrentPosition = TimeSpan.FromMilliseconds(NowPlaying.CurrentTimeMs); - - MediaPlayer.PlaybackRate = PlaybackSpeed; - - if (_pendingAutoPlay) - { - _pendingAutoPlay = false; - MediaPlayer.Play(); - } - }); - } - - private void AudioPlayer_MediaEnded(MediaPlayer sender, object args) - { - // check if there is a next source file - if (NowPlaying == null || NowPlaying.CurrentSourceFileIndex >= NowPlaying.SourcePaths.Count - 1) return; - - var nextSourceFileIndex = NowPlaying.CurrentSourceFileIndex + 1; - - // find the first chapter belonging to the next source file - var nextChapter = NowPlaying.Chapters.FirstOrDefault(c => - c.ParentSourceFileIndex == nextSourceFileIndex); - - if (nextChapter == null) return; - - _pendingAutoPlay = true; - - OpenSourceFile(nextSourceFileIndex, nextChapter.Index); - } - - private void AudioPlayer_MediaFailed(MediaPlayer sender, MediaPlayerFailedEventArgs args) - { - _dispatcherQueue.TryEnqueue(() => NowPlaying = null); - - // note: content dialog - App.ViewModel.EnqueueNotification(new Notification - { - Message = - "Unable to open the audiobook: media failed. Please verify that the file is not corrupted and try again.", - Severity = InfoBarSeverity.Error - }); - } - - private void PlaybackSession_PlaybackStateChanged(MediaPlaybackSession sender, object args) - { - switch (sender.PlaybackState) - { - case MediaPlaybackState.Playing: - _dispatcherQueue.TryEnqueue(() => - { - if (PlayPauseIcon == Symbol.Pause) return; - PlayPauseIcon = Symbol.Pause; - }); - - break; - - case MediaPlaybackState.Paused: - _dispatcherQueue.TryEnqueue(() => - { - if (PlayPauseIcon == Symbol.Play) return; - PlayPauseIcon = Symbol.Play; - }); - - break; - } - } - - private async void PlaybackSession_PositionChanged(MediaPlaybackSession sender, object args) - { - if (NowPlaying == null) return; - - if (!NowPlaying.CurrentChapter.InRange(CurrentPosition.TotalMilliseconds)) - { - var newChapter = NowPlaying.Chapters.FirstOrDefault(c => - c.ParentSourceFileIndex == NowPlaying.CurrentSourceFileIndex && - c.InRange(CurrentPosition.TotalMilliseconds)); - - if (newChapter != null) - _ = _dispatcherQueue.EnqueueAsync(() => - { - NowPlaying.CurrentChapterIndex = ChapterComboSelectedIndex = newChapter.Index; - NowPlaying.CurrentChapterTitle = newChapter.Title; - ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); - }); - } - - if (IsUserSeeking) return; - - _ = _dispatcherQueue.EnqueueAsync(async () => - { - ChapterPositionMs = (int)(CurrentPosition.TotalMilliseconds > NowPlaying.CurrentChapter.StartTime - ? CurrentPosition.TotalMilliseconds - NowPlaying.CurrentChapter.StartTime - : 0); - NowPlaying.CurrentTimeMs = (int)CurrentPosition.TotalMilliseconds; - - // TODO: this is gross - // calculate/update progress - double tmp = 0; - if (NowPlaying.CurrentSourceFileIndex != 0) - for (var i = 0; i < NowPlaying.CurrentSourceFileIndex; i++) - tmp += NowPlaying.SourcePaths[i].Duration; - tmp += CurrentPosition.TotalSeconds; - NowPlaying.Progress = Math.Ceiling(tmp / NowPlaying.Duration * 100); - NowPlaying.IsCompleted = NowPlaying.Progress >= 99.9; - }); - - await NowPlaying.SaveAsync(); - } + // Event handlers are now private methods called from LibVLC event subscriptions above. #endregion -} \ No newline at end of file +} diff --git a/Audibly.App/Views/Legacy/LegacyPlayerPage.xaml b/Audibly.App/Views/Legacy/LegacyPlayerPage.xaml index 34177bc..626f1d4 100644 --- a/Audibly.App/Views/Legacy/LegacyPlayerPage.xaml +++ b/Audibly.App/Views/Legacy/LegacyPlayerPage.xaml @@ -36,13 +36,6 @@ - - { - PlayerViewModel.NowPlaying.PlaybackSpeed = e.NewValue; - PlayerViewModel.MediaPlayer.PlaybackRate = e.NewValue; + PlayerViewModel.UpdatePlaybackSpeed(e.NewValue); }); } private void VolumeSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e) { - DispatcherQueue.EnqueueAsync(async () => + DispatcherQueue.TryEnqueue(() => { - PlayerViewModel.NowPlaying.Volume = e.NewValue; - PlayerViewModel.MediaPlayer.Volume = e.NewValue / 100; - await PlayerViewModel.NowPlaying.SaveAsync(); + PlayerViewModel.UpdateVolume(e.NewValue); }); } } \ No newline at end of file diff --git a/Audibly.App/Views/LibraryCardPage.xaml.cs b/Audibly.App/Views/LibraryCardPage.xaml.cs index c619b12..f8d0b20 100644 --- a/Audibly.App/Views/LibraryCardPage.xaml.cs +++ b/Audibly.App/Views/LibraryCardPage.xaml.cs @@ -343,7 +343,7 @@ public void RestartAppButton_OnClick(object sender, RoutedEventArgs e) public void HideNowPlayingBarButton_OnClick(object sender, RoutedEventArgs e) { - PlayerViewModel.MediaPlayer.Pause(); + PlayerViewModel.Pause(); if (PlayerViewModel.NowPlaying != null) PlayerViewModel.NowPlaying.IsNowPlaying = false; PlayerViewModel.NowPlaying = null; From 73907ad6e53c1c517bd9454a675ba2f879cd8ea6 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Mon, 30 Mar 2026 09:07:29 +0200 Subject: [PATCH 02/15] use the proper path when creating the Media object --- Audibly.App/ViewModels/PlayerViewModel.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index 6516a6c..a953e30 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -2,6 +2,7 @@ // Updated: 08/02/2025 using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -213,7 +214,7 @@ public int ChapterComboSelectedIndex public TimeSpan CurrentPosition { get => TimeSpan.FromMilliseconds(_mediaPlayer.Time >= 0 ? _mediaPlayer.Time : 0); - set => _mediaPlayer.Time = (long)Math.Max(value.TotalMilliseconds, 0); + set => _mediaPlayer.Time = (long)value.TotalMilliseconds; } /// @@ -570,8 +571,11 @@ await _dispatcherQueue.EnqueueAsync(async () => }); _mediaJustOpened = true; - using var media = new Media(_libVLC, audiobook.CurrentSourceFile.FilePath.AsUri()); - _mediaPlayer.Play(media); + using (var media = new Media(_libVLC, audiobook.CurrentSourceFile.FilePath, FromType.FromPath)) + { + _mediaPlayer.Play(media); + } + } public async void OpenSourceFile(int index, int chapterIndex) @@ -591,8 +595,13 @@ await _dispatcherQueue.EnqueueAsync(() => await NowPlaying.SaveAsync(); _mediaJustOpened = true; - using var media = new Media(_libVLC, NowPlaying.CurrentSourceFile.FilePath.AsUri()); - _mediaPlayer.Play(media); + using (var media = new Media(_libVLC, NowPlaying.CurrentSourceFile.FilePath, FromType.FromPath)) + { + _mediaPlayer.Play(media); + + Debug.Print(media.Duration.ToString()); + } + } /// From 257f0e303e2634cda6194b397a84134c74b17ae4 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Mon, 30 Mar 2026 18:07:13 +0200 Subject: [PATCH 03/15] disable vlc chapter awarness (todo maybe actually use it later) and put all on OnTimeChanged on the main thread via dispatcher queue. --- Audibly.App/ViewModels/PlayerViewModel.cs | 50 +++++++++++++++-------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index a953e30..cf12f87 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -60,6 +60,8 @@ public class PlayerViewModel : BindableBase, IDisposable private bool _mediaJustOpened; + private bool _skipSeeking; + public PlayerViewModel() { _libVLC = new LibVLC("--no-video"); @@ -214,7 +216,12 @@ public int ChapterComboSelectedIndex public TimeSpan CurrentPosition { get => TimeSpan.FromMilliseconds(_mediaPlayer.Time >= 0 ? _mediaPlayer.Time : 0); - set => _mediaPlayer.Time = (long)value.TotalMilliseconds; + set { + if (!_skipSeeking) + { + _mediaPlayer.Time = (long)value.TotalMilliseconds; + } + } } /// @@ -313,27 +320,30 @@ private void OnEncounteredError(object? sender, EventArgs e) private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) { - if (NowPlaying == null) return; + _ = _dispatcherQueue.EnqueueAsync(() => + { + if (NowPlaying == null) return; - var currentPositionMs = e.Time; + var currentPositionMs = e.Time; - if (!NowPlaying.CurrentChapter.InRange(currentPositionMs)) - { - var newChapter = NowPlaying.Chapters.FirstOrDefault(c => - c.ParentSourceFileIndex == NowPlaying.CurrentSourceFileIndex && - c.InRange(currentPositionMs)); + if (!NowPlaying.CurrentChapter.InRange(currentPositionMs)) + { + var newChapter = NowPlaying.Chapters.FirstOrDefault(c => + c.ParentSourceFileIndex == NowPlaying.CurrentSourceFileIndex && + c.InRange(currentPositionMs)); - if (newChapter != null) - _ = _dispatcherQueue.EnqueueAsync(() => + if (newChapter != null) { + _skipSeeking = true; NowPlaying.CurrentChapterIndex = ChapterComboSelectedIndex = newChapter.Index; NowPlaying.CurrentChapterTitle = newChapter.Title; ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); - }); - } + _skipSeeking = false; + } + ; + } + - _ = _dispatcherQueue.EnqueueAsync(async () => - { ChapterPositionMs = (int)(currentPositionMs > NowPlaying.CurrentChapter.StartTime ? currentPositionMs - NowPlaying.CurrentChapter.StartTime : 0); @@ -348,9 +358,12 @@ private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) tmp += currentPositionMs / 1000.0; NowPlaying.Progress = Math.Ceiling(tmp / NowPlaying.Duration * 100); NowPlaying.IsCompleted = NowPlaying.Progress >= 99.9; + ; + + _ = Task.Run(async () => await NowPlaying.SaveAsync()); + }); - _ = Task.Run(async () => await NowPlaying.SaveAsync()); } private void HandleMediaOpened() @@ -573,6 +586,8 @@ await _dispatcherQueue.EnqueueAsync(async () => _mediaJustOpened = true; using (var media = new Media(_libVLC, audiobook.CurrentSourceFile.FilePath, FromType.FromPath)) { + //this makes vlc ignore chapters usually it's chapter aware but that breaks our existing logic assuming the player position is absolute from file start instead of chapter start. + media.AddOption(":demux=avformat"); _mediaPlayer.Play(media); } @@ -597,11 +612,10 @@ await _dispatcherQueue.EnqueueAsync(() => _mediaJustOpened = true; using (var media = new Media(_libVLC, NowPlaying.CurrentSourceFile.FilePath, FromType.FromPath)) { + media.AddOption(":demux=avformat"); _mediaPlayer.Play(media); - - Debug.Print(media.Duration.ToString()); } - + } /// From ee45b0b0aa77d52c327db2d13aa6db4bca61fe92 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Mon, 30 Mar 2026 19:01:25 +0200 Subject: [PATCH 04/15] allow restarting audio book after vlc ended it. --- Audibly.App/ViewModels/PlayerViewModel.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index cf12f87..4630ab2 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -251,6 +251,11 @@ public string TimerRemainingText /// public void Play() { + if (_mediaPlayer.State == VLCState.Ended) + { + // If media has ended, stop to get the player back into a state where it can play + _mediaPlayer.Stop(); + } _mediaPlayer.Play(); } @@ -412,7 +417,14 @@ await DialogService.ShowErrorDialogAsync("Error", private void HandleMediaEnded() { // check if there is a next source file - if (NowPlaying == null || NowPlaying.CurrentSourceFileIndex >= NowPlaying.SourcePaths.Count - 1) return; + if (NowPlaying == null || NowPlaying.CurrentSourceFileIndex >= NowPlaying.SourcePaths.Count - 1) + { + //We have no more media to play make sure to set the play button to play instead of pause! + if (PlayPauseIcon != Symbol.Play) + PlayPauseIcon = Symbol.Play; + return; + } + // todo: log error here if (NowPlaying.CurrentChapterIndex == null) return; From abac00e12b4a50d20e2153cf3c2929cacfe8bb19 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Wed, 1 Apr 2026 22:30:47 +0200 Subject: [PATCH 05/15] Guard media-open transitions and restore direct playback UI updates. Ignore stale time updates while a new VLC media is opening, and keep chapter/seek UI updates explicit without bringing back the broad SyncPlaybackState refactor. --- .../UserControls/PlaySkipButtonsStack.xaml.cs | 14 +- .../UserControls/PlayerControlGrid.xaml.cs | 6 +- Audibly.App/ViewModels/PlayerViewModel.cs | 266 +++++++++++------- 3 files changed, 174 insertions(+), 112 deletions(-) diff --git a/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs b/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs index 7cb4431..02a33e9 100644 --- a/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs +++ b/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs @@ -62,10 +62,8 @@ private async void PreviousChapterButton_Click(object sender, RoutedEventArgs e) .ParentSourceFileIndex != PlayerViewModel.NowPlaying?.CurrentSourceFileIndex) { var newChapterIdx = (int)PlayerViewModel.NowPlaying.CurrentChapterIndex - 1; - PlayerViewModel.OpenSourceFile(PlayerViewModel.NowPlaying.CurrentSourceFileIndex - 1, newChapterIdx); - PlayerViewModel.CurrentPosition = - TimeSpan.FromMilliseconds(PlayerViewModel.NowPlaying.Chapters[newChapterIdx].StartTime); - await PlayerViewModel.NowPlaying.SaveAsync(); + await PlayerViewModel.OpenSourceFile(PlayerViewModel.NowPlaying.CurrentSourceFileIndex - 1, newChapterIdx, + PlayerViewModel.NowPlaying.Chapters[newChapterIdx].StartTime); return; } @@ -98,10 +96,8 @@ private async void NextChapterButton_Click(object sender, RoutedEventArgs e) .ParentSourceFileIndex != PlayerViewModel.NowPlaying?.CurrentSourceFileIndex) { var newChapterIdx = (int)PlayerViewModel.NowPlaying.CurrentChapterIndex + 1; - PlayerViewModel.OpenSourceFile(PlayerViewModel.NowPlaying.CurrentSourceFileIndex + 1, newChapterIdx); - PlayerViewModel.CurrentPosition = - TimeSpan.FromMilliseconds(PlayerViewModel.NowPlaying.Chapters[newChapterIdx].StartTime); - await PlayerViewModel.NowPlaying.SaveAsync(); + await PlayerViewModel.OpenSourceFile(PlayerViewModel.NowPlaying.CurrentSourceFileIndex + 1, newChapterIdx, + PlayerViewModel.NowPlaying.Chapters[newChapterIdx].StartTime); return; } @@ -146,4 +142,4 @@ private async void SkipForwardButton_OnClick(object sender, RoutedEventArgs e) await PlayerViewModel.NowPlaying.SaveAsync(); } -} \ No newline at end of file +} diff --git a/Audibly.App/UserControls/PlayerControlGrid.xaml.cs b/Audibly.App/UserControls/PlayerControlGrid.xaml.cs index 8048e8f..1d91815 100644 --- a/Audibly.App/UserControls/PlayerControlGrid.xaml.cs +++ b/Audibly.App/UserControls/PlayerControlGrid.xaml.cs @@ -60,8 +60,8 @@ private async void ChapterCombo_SelectionChanged(object sender, SelectionChanged PlayerViewModel.NowPlaying.CurrentSourceFile.Index != newChapter.ParentSourceFileIndex) { // set the current source file index to the new source file index - PlayerViewModel.OpenSourceFile(newChapter.ParentSourceFileIndex, newChapter.Index); - PlayerViewModel.CurrentPosition = TimeSpan.FromMilliseconds(newChapter.StartTime); + await PlayerViewModel.OpenSourceFile(newChapter.ParentSourceFileIndex, newChapter.Index, + newChapter.StartTime); } else if (ChapterCombo.SelectedIndex != ChapterCombo.Items.IndexOf(PlayerViewModel.NowPlaying?.CurrentChapter)) { @@ -165,4 +165,4 @@ private void EndOfChapterTimerMenuItem_Click(object sender, RoutedEventArgs e) if (timerDuration > 0) PlayerViewModel.SetTimer(timerDuration); } -} \ No newline at end of file +} diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index 4630ab2..9430782 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -216,10 +216,43 @@ public int ChapterComboSelectedIndex public TimeSpan CurrentPosition { get => TimeSpan.FromMilliseconds(_mediaPlayer.Time >= 0 ? _mediaPlayer.Time : 0); - set { + set + { if (!_skipSeeking) { - _mediaPlayer.Time = (long)value.TotalMilliseconds; + var currentPositionMs = Math.Max(0, (long)value.TotalMilliseconds); + _mediaPlayer.Time = currentPositionMs; + + if (NowPlaying == null) return; + + if (!NowPlaying.CurrentChapter.InRange(currentPositionMs)) + { + var newChapter = NowPlaying.Chapters.FirstOrDefault(c => + c.ParentSourceFileIndex == NowPlaying.CurrentSourceFileIndex && + c.InRange(currentPositionMs)); + + if (newChapter != null) + { + _skipSeeking = true; + NowPlaying.CurrentChapterIndex = ChapterComboSelectedIndex = newChapter.Index; + NowPlaying.CurrentChapterTitle = newChapter.Title; + ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); + _skipSeeking = false; + } + } + + ChapterPositionMs = (int)(currentPositionMs > NowPlaying.CurrentChapter.StartTime + ? currentPositionMs - NowPlaying.CurrentChapter.StartTime + : 0); + NowPlaying.CurrentTimeMs = (int)currentPositionMs; + + double tmp = 0; + if (NowPlaying.CurrentSourceFileIndex != 0) + for (var i = 0; i < NowPlaying.CurrentSourceFileIndex; i++) + tmp += NowPlaying.SourcePaths[i].Duration; + tmp += currentPositionMs / 1000.0; + NowPlaying.Progress = Math.Ceiling(tmp / NowPlaying.Duration * 100); + NowPlaying.IsCompleted = NowPlaying.Progress >= 99.9; } } } @@ -285,14 +318,11 @@ private void OnPlaying(object? sender, EventArgs e) { if (PlayPauseIcon == Symbol.Pause) return; PlayPauseIcon = Symbol.Pause; - }); - // Handle "media opened" logic on first Playing event after setting new media - if (_mediaJustOpened) - { - _mediaJustOpened = false; - HandleMediaOpened(); - } + // Handle "media opened" logic on first Playing event after setting new media. + if (_mediaJustOpened) + HandleMediaOpened(); + }); } private void OnPaused(object? sender, EventArgs e) @@ -313,7 +343,11 @@ private void OnEndReached(object? sender, EventArgs e) private void OnEncounteredError(object? sender, EventArgs e) { - _dispatcherQueue.TryEnqueue(() => NowPlaying = null); + _dispatcherQueue.TryEnqueue(() => + { + _mediaJustOpened = false; + NowPlaying = null; + }); App.ViewModel.EnqueueNotification(new Notification { @@ -325,9 +359,9 @@ private void OnEncounteredError(object? sender, EventArgs e) private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) { - _ = _dispatcherQueue.EnqueueAsync(() => + _dispatcherQueue.TryEnqueue(() => { - if (NowPlaying == null) return; + if (NowPlaying == null || _mediaJustOpened) return; var currentPositionMs = e.Time; @@ -366,52 +400,50 @@ private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) ; _ = Task.Run(async () => await NowPlaying.SaveAsync()); - }); - } private void HandleMediaOpened() { if (NowPlaying == null) return; - _dispatcherQueue.EnqueueAsync(async () => - { - if (NowPlaying.Chapters.Count == 0) - { - NowPlaying = null; - await DialogService.ShowErrorDialogAsync("Error", - "An error occurred while trying to open the selected audiobook. " + - "The chapters could not be loaded. Please try importing the audiobook again."); + if (NowPlaying.Chapters.Count == 0) + { + _mediaJustOpened = false; + NowPlaying = null; - return; - } + _ = DialogService.ShowErrorDialogAsync("Error", + "An error occurred while trying to open the selected audiobook. " + + "The chapters could not be loaded. Please try importing the audiobook again."); - ChapterComboSelectedIndex = NowPlaying.CurrentChapterIndex ?? 0; + return; + } - ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); + ChapterComboSelectedIndex = NowPlaying.CurrentChapterIndex ?? 0; + NowPlaying.CurrentChapterTitle = NowPlaying.Chapters[ChapterComboSelectedIndex].Title; + ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); + ChapterPositionMs = + NowPlaying.CurrentTimeMs > NowPlaying.CurrentChapter.StartTime + ? (int)(NowPlaying.CurrentTimeMs - NowPlaying.CurrentChapter.StartTime) + : 0; - ChapterPositionMs = - NowPlaying.CurrentTimeMs > NowPlaying.CurrentChapter.StartTime - ? (int)(NowPlaying.CurrentTimeMs - NowPlaying.CurrentChapter.StartTime) - : 0; + // Seek to saved position. + _mediaPlayer.Time = NowPlaying.CurrentTimeMs; - // Seek to saved position - _mediaPlayer.Time = NowPlaying.CurrentTimeMs; + _mediaPlayer.SetRate((float)PlaybackSpeed); - _mediaPlayer.SetRate((float)PlaybackSpeed); + if (_pendingAutoPlay) + { + _pendingAutoPlay = false; + // Already playing since we used _mediaPlayer.Play() to open + } + else + { + // We started playing to trigger media load — pause now since user didn't press play + _mediaPlayer.Pause(); + } - if (_pendingAutoPlay) - { - _pendingAutoPlay = false; - // Already playing since we used _mediaPlayer.Play() to open - } - else - { - // We started playing to trigger media load — pause now since user didn't press play - _mediaPlayer.Pause(); - } - }); + _mediaJustOpened = false; } private void HandleMediaEnded() @@ -431,7 +463,7 @@ private void HandleMediaEnded() _pendingAutoPlay = true; - OpenSourceFile(NowPlaying.CurrentSourceFileIndex + 1, (int)NowPlaying.CurrentChapterIndex + 1); + _ = OpenSourceFile(NowPlaying.CurrentSourceFileIndex + 1, (int)NowPlaying.CurrentChapterIndex + 1); } public void SetTimer(double seconds) @@ -541,93 +573,127 @@ public async void UpdatePlaybackSpeed(double speed) public async Task OpenAudiobook(AudiobookViewModel audiobook) { - if (NowPlaying != null && NowPlaying.Equals(audiobook)) + if (_mediaJustOpened) return; - // todo: trying this out - if (NowPlaying != null) - { - NowPlaying.IsNowPlaying = false; - - await NowPlaying.SaveAsync(); - } + if (NowPlaying != null && NowPlaying.Equals(audiobook)) + return; - _mediaPlayer.Pause(); + _mediaJustOpened = true; + var playRequested = false; - App.ViewModel.SelectedAudiobook = audiobook; + try + { + if (NowPlaying != null) + { + NowPlaying.IsNowPlaying = false; - // verify that the file exists - // if there are multiple source files, check them all + await NowPlaying.SaveAsync(); + } - if (audiobook.SourcePaths.Any(sourceFile => !File.Exists(sourceFile.FilePath))) - { - // note: content dialog - await DialogService.ShowErrorDialogAsync("Error", - $"Unable to play audiobook: {audiobook.Title}. One or more of its source files were deleted or moved."); + _mediaPlayer.Pause(); - return; - } + App.ViewModel.SelectedAudiobook = audiobook; - await _dispatcherQueue.EnqueueAsync(async () => - { - NowPlaying = audiobook; + // verify that the file exists + // if there are multiple source files, check them all - if (NowPlaying.DateLastPlayed == null) + if (audiobook.SourcePaths.Any(sourceFile => !File.Exists(sourceFile.FilePath))) { - // use the global playback speed and volume level if they are set - // and this is the first time the audiobook is being played - UpdatePlaybackSpeed(UserSettings.PlaybackSpeed); - UpdateVolume(UserSettings.Volume); + // note: content dialog + await DialogService.ShowErrorDialogAsync("Error", + $"Unable to play audiobook: {audiobook.Title}. One or more of its source files were deleted or moved."); + + return; } - else + + await _dispatcherQueue.EnqueueAsync(() => { - // use the audiobook's playback speed and volume level - UpdatePlaybackSpeed(NowPlaying.PlaybackSpeed); - UpdateVolume(NowPlaying.Volume); - } + NowPlaying = audiobook; - NowPlaying.IsNowPlaying = true; - NowPlaying.DateLastPlayed = DateTime.Now; + if (NowPlaying.DateLastPlayed == null) + { + // use the global playback speed and volume level if they are set + // and this is the first time the audiobook is being played + UpdatePlaybackSpeed(UserSettings.PlaybackSpeed); + UpdateVolume(UserSettings.Volume); + } + else + { + // use the audiobook's playback speed and volume level + UpdatePlaybackSpeed(NowPlaying.PlaybackSpeed); + UpdateVolume(NowPlaying.Volume); + } + + NowPlaying.IsNowPlaying = true; + NowPlaying.DateLastPlayed = DateTime.Now; - ChapterComboSelectedIndex = NowPlaying.CurrentChapterIndex ?? 0; - NowPlaying.CurrentChapterTitle = NowPlaying.Chapters[ChapterComboSelectedIndex].Title; + ChapterComboSelectedIndex = NowPlaying.CurrentChapterIndex ?? 0; + NowPlaying.CurrentChapterTitle = NowPlaying.Chapters[ChapterComboSelectedIndex].Title; + ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); + ChapterPositionMs = + NowPlaying.CurrentTimeMs > NowPlaying.CurrentChapter.StartTime + ? (int)(NowPlaying.CurrentTimeMs - NowPlaying.CurrentChapter.StartTime) + : 0; + }); await NowPlaying.SaveAsync(); - }); - _mediaJustOpened = true; - using (var media = new Media(_libVLC, audiobook.CurrentSourceFile.FilePath, FromType.FromPath)) + using (var media = new Media(_libVLC, audiobook.CurrentSourceFile.FilePath, FromType.FromPath)) + { + //this makes vlc ignore chapters usually it's chapter aware but that breaks our existing logic assuming the player position is absolute from file start instead of chapter start. + media.AddOption(":demux=avformat"); + _mediaPlayer.Play(media); + } + + playRequested = true; + } + finally { - //this makes vlc ignore chapters usually it's chapter aware but that breaks our existing logic assuming the player position is absolute from file start instead of chapter start. - media.AddOption(":demux=avformat"); - _mediaPlayer.Play(media); + if (!playRequested) + _mediaJustOpened = false; } - } - public async void OpenSourceFile(int index, int chapterIndex) + public async Task OpenSourceFile(int index, int chapterIndex, long currentTimeMs = 0) { + if (_mediaJustOpened) + return; + if (NowPlaying == null || NowPlaying.CurrentSourceFileIndex == index) return; - NowPlaying.CurrentTimeMs = 0; - NowPlaying.CurrentSourceFileIndex = index; - NowPlaying.CurrentChapterIndex = chapterIndex; + _mediaJustOpened = true; + var playRequested = false; - await _dispatcherQueue.EnqueueAsync(() => + try { + NowPlaying.CurrentTimeMs = (int)currentTimeMs; + NowPlaying.CurrentSourceFileIndex = index; + NowPlaying.CurrentChapterIndex = chapterIndex; NowPlaying.CurrentChapterTitle = NowPlaying.Chapters[chapterIndex].Title; - }); + ChapterComboSelectedIndex = chapterIndex; + ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); + ChapterPositionMs = + currentTimeMs > NowPlaying.CurrentChapter.StartTime + ? (int)(currentTimeMs - NowPlaying.CurrentChapter.StartTime) + : 0; - await NowPlaying.SaveAsync(); + await NowPlaying.SaveAsync(); - _mediaJustOpened = true; - using (var media = new Media(_libVLC, NowPlaying.CurrentSourceFile.FilePath, FromType.FromPath)) + using (var media = new Media(_libVLC, NowPlaying.CurrentSourceFile.FilePath, FromType.FromPath)) + { + media.AddOption(":demux=avformat"); + _mediaPlayer.Play(media); + } + + playRequested = true; + } + finally { - media.AddOption(":demux=avformat"); - _mediaPlayer.Play(media); + if (!playRequested) + _mediaJustOpened = false; } - } /// From b2d10960185ffaf9c2dea1ae8651cc27b1383836 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Wed, 1 Apr 2026 22:33:25 +0200 Subject: [PATCH 06/15] use speex resampler to remove high piched noise --- Audibly.App/ViewModels/PlayerViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index 9430782..2a651da 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -64,7 +64,7 @@ public class PlayerViewModel : BindableBase, IDisposable public PlayerViewModel() { - _libVLC = new LibVLC("--no-video"); + _libVLC = new LibVLC("--no-video", "--audio-resampler=speex", "--speex-resampler-quality=10"); _mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC); InitializeAudioPlayer(); } From 7b1e48209afec0274916f28a452f6ef53a683589 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Wed, 1 Apr 2026 22:50:56 +0200 Subject: [PATCH 07/15] attempt to fix --- Audibly.App/ViewModels/PlayerViewModel.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index 2a651da..9360bcb 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -579,7 +579,6 @@ public async Task OpenAudiobook(AudiobookViewModel audiobook) if (NowPlaying != null && NowPlaying.Equals(audiobook)) return; - _mediaJustOpened = true; var playRequested = false; try @@ -643,10 +642,9 @@ await _dispatcherQueue.EnqueueAsync(() => { //this makes vlc ignore chapters usually it's chapter aware but that breaks our existing logic assuming the player position is absolute from file start instead of chapter start. media.AddOption(":demux=avformat"); - _mediaPlayer.Play(media); + _mediaJustOpened = true; + playRequested = _mediaPlayer.Play(media); } - - playRequested = true; } finally { @@ -663,7 +661,6 @@ public async Task OpenSourceFile(int index, int chapterIndex, long currentTimeMs if (NowPlaying == null || NowPlaying.CurrentSourceFileIndex == index) return; - _mediaJustOpened = true; var playRequested = false; try @@ -684,10 +681,9 @@ public async Task OpenSourceFile(int index, int chapterIndex, long currentTimeMs using (var media = new Media(_libVLC, NowPlaying.CurrentSourceFile.FilePath, FromType.FromPath)) { media.AddOption(":demux=avformat"); - _mediaPlayer.Play(media); + _mediaJustOpened = true; + playRequested = _mediaPlayer.Play(media); } - - playRequested = true; } finally { From d3bd1d44929ebcfa2cd3d41578e90a557e90eaf1 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Wed, 1 Apr 2026 23:00:18 +0200 Subject: [PATCH 08/15] use explcit pause --- Audibly.App/ViewModels/PlayerViewModel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index 9360bcb..8bb56c5 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -297,7 +297,7 @@ public void Play() /// public void Pause() { - _mediaPlayer.Pause(); + _mediaPlayer.SetPause(true); } #region methods @@ -440,7 +440,7 @@ private void HandleMediaOpened() else { // We started playing to trigger media load — pause now since user didn't press play - _mediaPlayer.Pause(); + _mediaPlayer.SetPause(true); } _mediaJustOpened = false; @@ -505,7 +505,7 @@ private void SleepTimer_Elapsed(object? sender, ElapsedEventArgs e) // Timer expired - pause playback _dispatcherQueue.TryEnqueue(() => { - _mediaPlayer.Pause(); + _mediaPlayer.SetPause(true); IsTimerActive = false; _sleepTimer?.Stop(); _sleepTimer?.Dispose(); @@ -590,7 +590,7 @@ public async Task OpenAudiobook(AudiobookViewModel audiobook) await NowPlaying.SaveAsync(); } - _mediaPlayer.Pause(); + _mediaPlayer.SetPause(true); App.ViewModel.SelectedAudiobook = audiobook; From 61fada012db50f8ab412ad44adb453182507b74e Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Sat, 4 Apr 2026 16:41:38 +0200 Subject: [PATCH 09/15] lower speex quality to 6 --- Audibly.App/ViewModels/PlayerViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index 8bb56c5..e0b9b58 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -64,7 +64,7 @@ public class PlayerViewModel : BindableBase, IDisposable public PlayerViewModel() { - _libVLC = new LibVLC("--no-video", "--audio-resampler=speex", "--speex-resampler-quality=10"); + _libVLC = new LibVLC("--no-video", "--audio-resampler=speex", "--speex-resampler-quality=6"); _mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC); InitializeAudioPlayer(); } From 0060782b369ac59186d023b82ef47a3e2b11e72d Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Sat, 4 Apr 2026 20:51:49 +0200 Subject: [PATCH 10/15] switch to GPL version of libvlc (fine becouse this is a gpl project), use samplerate resampler to fix stutter caused by speex --- Audibly.App/Audibly.App.csproj | 1 + Audibly.App/ViewModels/PlayerViewModel.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Audibly.App/Audibly.App.csproj b/Audibly.App/Audibly.App.csproj index bf1bd9b..c014eaf 100644 --- a/Audibly.App/Audibly.App.csproj +++ b/Audibly.App/Audibly.App.csproj @@ -136,6 +136,7 @@ + diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index e0b9b58..e6b6309 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -64,7 +64,7 @@ public class PlayerViewModel : BindableBase, IDisposable public PlayerViewModel() { - _libVLC = new LibVLC("--no-video", "--audio-resampler=speex", "--speex-resampler-quality=6"); + _libVLC = new LibVLC("--no-video", "--audio-resampler=samplerate", "--src-converter-type=0"); _mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC); InitializeAudioPlayer(); } From de5e9390cbc14818c00a47202b21ab06534d8b01 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Sat, 4 Apr 2026 20:52:47 +0200 Subject: [PATCH 11/15] enable hardware acceleration --- Audibly.App/ViewModels/PlayerViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index e6b6309..a08ff0e 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -304,6 +304,9 @@ public void Pause() private void InitializeAudioPlayer() { + //Enable hardware acceleartion. + _mediaPlayer.EnableHardwareDecoding = true; + // LibVLC events fire on background threads — dispatch to UI thread. _mediaPlayer.Playing += OnPlaying; _mediaPlayer.Paused += OnPaused; From 117be5fe9ba756a1b802b72ee43b1c7c3c44e17b Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Sat, 4 Apr 2026 20:58:07 +0200 Subject: [PATCH 12/15] lower --src-converter-type=1 --- Audibly.App/ViewModels/PlayerViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index a08ff0e..050c5b6 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -64,7 +64,7 @@ public class PlayerViewModel : BindableBase, IDisposable public PlayerViewModel() { - _libVLC = new LibVLC("--no-video", "--audio-resampler=samplerate", "--src-converter-type=0"); + _libVLC = new LibVLC("--no-video", "--audio-resampler=samplerate", "--src-converter-type=1"); _mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC); InitializeAudioPlayer(); } From 62a4a2c04dbac0b3eeb98df01a80532cebf2ba94 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Sat, 4 Apr 2026 21:08:39 +0200 Subject: [PATCH 13/15] fix rebase to get betterplaybar to work again --- Audibly.App/ViewModels/PlayerViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index 050c5b6..2cab097 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -364,7 +364,7 @@ private void OnTimeChanged(object? sender, MediaPlayerTimeChangedEventArgs e) { _dispatcherQueue.TryEnqueue(() => { - if (NowPlaying == null || _mediaJustOpened) return; + if (NowPlaying == null || _mediaJustOpened || IsUserSeeking ) return; var currentPositionMs = e.Time; From 645a17cb5bebce34fdeccf211bae37c6df2afb29 Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Sat, 4 Apr 2026 21:16:17 +0200 Subject: [PATCH 14/15] remove redundant LibVLC.Windows package --- Audibly.App/Audibly.App.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Audibly.App/Audibly.App.csproj b/Audibly.App/Audibly.App.csproj index c014eaf..5cc3429 100644 --- a/Audibly.App/Audibly.App.csproj +++ b/Audibly.App/Audibly.App.csproj @@ -108,7 +108,6 @@ - From 5fea7779115e1130e91eb3210f9261476462216f Mon Sep 17 00:00:00 2001 From: KeinNiemand Date: Wed, 22 Apr 2026 14:43:09 +0200 Subject: [PATCH 15/15] fix multi-file vlc chapter advance --- Audibly.App/ViewModels/PlayerViewModel.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Audibly.App/ViewModels/PlayerViewModel.cs b/Audibly.App/ViewModels/PlayerViewModel.cs index 2cab097..79596bf 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -459,14 +459,15 @@ private void HandleMediaEnded() PlayPauseIcon = Symbol.Play; return; } - + var nextSourceFileIndex = NowPlaying.CurrentSourceFileIndex + 1; + var nextChapter = NowPlaying.Chapters.FirstOrDefault(c => + c.ParentSourceFileIndex == nextSourceFileIndex); - // todo: log error here - if (NowPlaying.CurrentChapterIndex == null) return; + if (nextChapter == null) return; _pendingAutoPlay = true; - _ = OpenSourceFile(NowPlaying.CurrentSourceFileIndex + 1, (int)NowPlaying.CurrentChapterIndex + 1); + _ = OpenSourceFile(nextSourceFileIndex, nextChapter.Index); } public void SetTimer(double seconds)