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;