diff --git a/Audibly.App/Audibly.App.csproj b/Audibly.App/Audibly.App.csproj index 9b1030e..5cc3429 100644 --- a/Audibly.App/Audibly.App.csproj +++ b/Audibly.App/Audibly.App.csproj @@ -106,10 +106,11 @@ - - - - + + + + + @@ -134,6 +135,7 @@ + diff --git a/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs b/Audibly.App/UserControls/PlaySkipButtonsStack.xaml.cs index d3fae28..02a33e9 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) @@ -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; } @@ -140,10 +136,10 @@ 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(); } -} \ No newline at end of file +} 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 @@ - - @@ -61,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)) { @@ -166,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/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..79596bf 100644 --- a/Audibly.App/ViewModels/PlayerViewModel.cs +++ b/Audibly.App/ViewModels/PlayerViewModel.cs @@ -2,16 +2,16 @@ // Updated: 08/02/2025 using System; +using System.Diagnostics; using System.IO; 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 +21,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 +58,14 @@ public class PlayerViewModel : BindableBase, IDisposable private string _volumeLevelGlyph = Constants.VolumeGlyph3; + private bool _mediaJustOpened; + + private bool _skipSeeking; + public PlayerViewModel() { + _libVLC = new LibVLC("--no-video", "--audio-resampler=samplerate", "--src-converter-type=1"); + _mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC); InitializeAudioPlayer(); } @@ -211,10 +215,54 @@ 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 + { + if (!_skipSeeking) + { + 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; + } + } } + /// + /// 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 +279,195 @@ public string TimerRemainingText private set => OnPropertyChanged(); } + /// + /// Starts or resumes playback. + /// + 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(); + } + + /// + /// Pauses playback. + /// + public void Pause() + { + _mediaPlayer.SetPause(true); + } + #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; + //Enable hardware acceleartion. + _mediaPlayer.EnableHardwareDecoding = true; + + // 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) + 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(() => + { + _mediaJustOpened = false; + 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) + { + _dispatcherQueue.TryEnqueue(() => + { + if (NowPlaying == null || _mediaJustOpened || IsUserSeeking ) return; + + var currentPositionMs = e.Time; + + 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; + + // 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; + + if (NowPlaying.Chapters.Count == 0) + { + _mediaJustOpened = false; + NowPlaying = null; + + _ = 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; + 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; - // set volume level from settings - // UpdateVolume(UserSettings.Volume); - // UpdatePlaybackSpeed(UserSettings.PlaybackSpeed); + // 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.SetPause(true); + } + + _mediaJustOpened = false; + } + + private void HandleMediaEnded() + { + // check if there is a next source file + 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; + } + var nextSourceFileIndex = NowPlaying.CurrentSourceFileIndex + 1; + var nextChapter = NowPlaying.Chapters.FirstOrDefault(c => + c.ParentSourceFileIndex == nextSourceFileIndex); + + if (nextChapter == null) return; + + _pendingAutoPlay = true; + + _ = OpenSourceFile(nextSourceFileIndex, nextChapter.Index); } public void SetTimer(double seconds) @@ -289,7 +509,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(); @@ -316,12 +536,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 +565,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; @@ -353,80 +577,123 @@ 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(); + 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.SetPause(true); - 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); - } - else - { - // use the audiobook's playback speed and volume level - UpdatePlaybackSpeed(NowPlaying.PlaybackSpeed); - UpdateVolume(NowPlaying.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; } - NowPlaying.IsNowPlaying = true; - NowPlaying.DateLastPlayed = DateTime.Now; + await _dispatcherQueue.EnqueueAsync(() => + { + NowPlaying = audiobook; - ChapterComboSelectedIndex = NowPlaying.CurrentChapterIndex ?? 0; - NowPlaying.CurrentChapterTitle = NowPlaying.Chapters[ChapterComboSelectedIndex].Title; + 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; + ChapterDurationMs = (int)(NowPlaying.CurrentChapter.EndTime - NowPlaying.CurrentChapter.StartTime); + ChapterPositionMs = + NowPlaying.CurrentTimeMs > NowPlaying.CurrentChapter.StartTime + ? (int)(NowPlaying.CurrentTimeMs - NowPlaying.CurrentChapter.StartTime) + : 0; + }); await NowPlaying.SaveAsync(); - }); - MediaPlayer.Source = MediaSource.CreateFromUri(audiobook.CurrentSourceFile.FilePath.AsUri()); + 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"); + _mediaJustOpened = true; + playRequested = _mediaPlayer.Play(media); + } + } + finally + { + 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; + 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(); - MediaPlayer.Source = MediaSource.CreateFromUri(NowPlaying.CurrentSourceFile.FilePath.AsUri()); + using (var media = new Media(_libVLC, NowPlaying.CurrentSourceFile.FilePath, FromType.FromPath)) + { + media.AddOption(":demux=avformat"); + _mediaJustOpened = true; + playRequested = _mediaPlayer.Play(media); + } + } + finally + { + if (!playRequested) + _mediaJustOpened = false; + } } /// @@ -446,140 +713,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;