diff --git a/MauiAudio.Sample/MainPageViewModel.cs b/MauiAudio.Sample/MainPageViewModel.cs index 36e2e71..0e4c18b 100644 --- a/MauiAudio.Sample/MainPageViewModel.cs +++ b/MauiAudio.Sample/MainPageViewModel.cs @@ -33,7 +33,8 @@ void Play() async void PlayInStream() { var stream = await FileSystem.OpenAppPackageFileAsync("sample.mp3"); - await playerService.PlayAsync(new() { Stream=stream, Name = "TempUrl", Author = "TempAuthor" }); + var img = await GetCoverImage(); + await playerService.PlayAsync(new() { Stream=stream, Name = "TempUrl", Author = "TempAuthor", ImageBytes = img, DurationInMs = 188000 }); } private void OnTimeChanging(object sender, EventArgs e) @@ -56,4 +57,13 @@ async void StopPlay() { await playerService.dispose(); } + async Task? GetCoverImage(string filePath = null) + { + using var imageStream = await FileSystem.OpenAppPackageFileAsync("shadow.png"); + + using var memStream = new MemoryStream(); + imageStream.CopyTo(memStream); + //doing return File.ReadAllBytes(IMAGE_FULL_PATH_AS_STRING); Will work as well + return memStream.ToArray(); + } } diff --git a/MauiAudio.Sample/MauiAudio.Sample.csproj b/MauiAudio.Sample/MauiAudio.Sample.csproj index 2e606f4..7921f30 100644 --- a/MauiAudio.Sample/MauiAudio.Sample.csproj +++ b/MauiAudio.Sample/MauiAudio.Sample.csproj @@ -1,10 +1,10 @@  - net6.0-android;net6.0-ios;net6.0-maccatalyst - $(TargetFrameworks);net6.0-windows10.0.19041.0 + net8.0-android;net8.0-ios;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041.0 - + Exe MauiAudio.Sample true @@ -48,6 +48,10 @@ + + + + @@ -61,4 +65,8 @@ + + + + diff --git a/MauiAudio.Sample/Resources/Raw/shadow.png b/MauiAudio.Sample/Resources/Raw/shadow.png new file mode 100644 index 0000000..641831c Binary files /dev/null and b/MauiAudio.Sample/Resources/Raw/shadow.png differ diff --git a/MauiAudio/MauiAudio.csproj b/MauiAudio/MauiAudio.csproj index 1795067..a356477 100644 --- a/MauiAudio/MauiAudio.csproj +++ b/MauiAudio/MauiAudio.csproj @@ -1,15 +1,15 @@  - net6.0;net6.0-android;net6.0-ios;net6.0-maccatalyst - $(TargetFrameworks);net6.0-windows10.0.19041.0 + net8.0;net8.0-android;net8.0-ios;net8.0-maccatalyst + $(TargetFrameworks);net8.0-windows10.0.19041.0 true true enable - 14.2 - 14.0 - 21.0 + 14.2 + 14.0 + 21.0 10.0.17763.0 10.0.17763.0 1.2.2 @@ -36,13 +36,13 @@ true - + - + diff --git a/MauiAudio/MediaPlay.cs b/MauiAudio/MediaPlay.cs index 64f80dc..a8feca6 100644 --- a/MauiAudio/MediaPlay.cs +++ b/MauiAudio/MediaPlay.cs @@ -13,5 +13,13 @@ public class MediaPlay public string URL { get; set; } public Stream Stream { get; set; } public string Image { get; set; } + /// + /// Get/Set Album Cover in Byte[] to put on Notification + /// + public byte[] ImageBytes { get; set; } + /// + /// Get/Set the VALUE of the Duration, solely used for the Notification bar to show Progress. + /// + public long DurationInMs { get; set; } = 0; } } diff --git a/MauiAudio/NativeAudioService.windows.cs b/MauiAudio/NativeAudioService.windows.cs index 1c63026..5dfccac 100644 --- a/MauiAudio/NativeAudioService.windows.cs +++ b/MauiAudio/NativeAudioService.windows.cs @@ -77,10 +77,28 @@ private MediaPlaybackItem mediaPlaybackItem(MediaPlay media) if (media.Name != null) props.MusicProperties.Title = media.Name; if (media.Author != null) props.MusicProperties.Artist = media.Author; if (media.Image != null) + { props.Thumbnail = RandomAccessStreamReference.CreateFromUri(new Uri(media.Image)); + } + else if(media.ImageBytes != null) + { + props.Thumbnail = ConvertToRandomAccessStreamReference(media.ImageBytes); + } + mediaItem.ApplyDisplayProperties(props); return mediaItem; } + + private RandomAccessStreamReference ConvertToRandomAccessStreamReference(byte[] imageData) + { + using var stream = new MemoryStream(imageData); + var randomAccessStream = new InMemoryRandomAccessStream(); + var writer = new DataWriter(randomAccessStream.GetOutputStreamAt(0)); + writer.WriteBytes(imageData); + writer.StoreAsync().GetResults(); + return RandomAccessStreamReference.CreateFromStream(randomAccessStream); + } + public async Task InitializeAsync(MediaPlay media) { if (mediaPlayer == null) diff --git a/MauiAudio/Platforms/Android/MediaPlayerService.cs b/MauiAudio/Platforms/Android/MediaPlayerService.cs index 8f2e514..fec6a7f 100644 --- a/MauiAudio/Platforms/Android/MediaPlayerService.cs +++ b/MauiAudio/Platforms/Android/MediaPlayerService.cs @@ -1,18 +1,18 @@ using Android.App; using Android.Content; +using Android.Graphics; using Android.Media; +using Android.Media.Session; using Android.Net; using Android.Net.Wifi; using Android.OS; -using Android.Media.Session; using AndroidNet = Android.Net; -using Android.Graphics; namespace MauiAudio.Platforms.Android; [Service(Exported = true)] -[IntentFilter(new[] { ActionPlay, ActionPause, ActionStop, ActionTogglePlayback, ActionNext, ActionPrevious })] +[IntentFilter(new[] { ActionPlay, ActionPause, ActionStop, ActionTogglePlayback, ActionNext, ActionPrevious, ActionSeekTo })] public class MediaPlayerService : Service, AudioManager.IOnAudioFocusChangeListener, MediaPlayer.IOnBufferingUpdateListener, @@ -27,6 +27,7 @@ public class MediaPlayerService : Service, public const string ActionTogglePlayback = "com.xamarin.action.TOGGLEPLAYBACK"; public const string ActionNext = "com.xamarin.action.NEXT"; public const string ActionPrevious = "com.xamarin.action.PREVIOUS"; + public const string ActionSeekTo = "com.xamarin.action.ActionSeekTo"; public MediaPlayer mediaPlayer; private AudioManager audioManager; @@ -104,7 +105,7 @@ protected virtual void OnStatusChanged(EventArgs e) protected virtual void OnPlayingChanged(bool e) { PlayingChanged?.Invoke(this, e); - IsPlayingChanged?.Invoke(this,e); + IsPlayingChanged?.Invoke(this, e); } protected virtual void OnCoverReloaded(EventArgs e) @@ -279,8 +280,8 @@ public object Cover set { cover = value as Bitmap; - if(cover!=null) - OnCoverReloaded(EventArgs.Empty); + if (cover != null) + OnCoverReloaded(EventArgs.Empty); } } @@ -327,18 +328,18 @@ private async Task PrepareAndPlayMediaPlayerAsync() MediaMetadataRetriever metaRetriever = new MediaMetadataRetriever(); AndroidNet.Uri uri; - if(mediaPlay.Stream!= null) + if (mediaPlay.Stream != null) { - var fileStream = File.Create(FileSystem.Current.CacheDirectory+"temp.wav"); + var fileStream = File.Create(FileSystem.Current.CacheDirectory + "temp.wav"); mediaPlay.Stream.CopyTo(fileStream); fileStream.Close(); - uri = AndroidNet.Uri.Parse( FileSystem.Current.CacheDirectory + "temp.wav"); + uri = AndroidNet.Uri.Parse(FileSystem.Current.CacheDirectory + "temp.wav"); } else { uri = AndroidNet.Uri.Parse(mediaPlay.URL); } - await mediaPlayer.SetDataSourceAsync(ApplicationContext, uri); + await mediaPlayer.SetDataSourceAsync(ApplicationContext, uri); //If Uri Scheme is not set its a local file so there's no metadata to fetch if (!string.IsNullOrWhiteSpace(uri.Scheme)) @@ -367,6 +368,10 @@ private async Task PrepareAndPlayMediaPlayerAsync() { Cover = await GetImageBitmapFromUrl(mediaPlay.Image); } + else if (mediaPlay.ImageBytes is not null) + { + Cover = await GetImageBitmapFromUrlBytesAsync(mediaPlay.ImageBytes); + } else if (metaRetriever != null && !string.IsNullOrWhiteSpace(metaRetriever.ExtractMetadata(MetadataKey.Album))) { byte[] imageByteArray = metaRetriever.GetEmbeddedPicture(); @@ -403,6 +408,17 @@ private async Task GetImageBitmapFromUrl(string url) return imageBitmap; } + async Task GetImageBitmapFromUrlBytesAsync(byte[] imageBytes) + { + Bitmap imageBitmap = null; + if (imageBytes != null && imageBytes.Length > 0) + { + + imageBitmap = await BitmapFactory.DecodeByteArrayAsync(imageBytes, 0, imageBytes.Length); + } + + return imageBitmap; + } public async Task Seek(int position) { await Task.Run(() => @@ -416,7 +432,7 @@ await Task.Run(() => public async Task PlayNext() { - TaskPlayNext?.Invoke(this,EventArgs.Empty); + TaskPlayNext?.Invoke(this, EventArgs.Empty); //if (mediaPlayer != null) //{ // mediaPlayer.Reset(); @@ -511,11 +527,14 @@ public void UpdatePlaybackStateStopped() } } - private void UpdatePlaybackState(PlaybackStateCode state) + private void UpdatePlaybackState(PlaybackStateCode state, int SeekedPosition = 0) { if (mediaSession == null || mediaPlayer == null) return; - + if (SeekedPosition == 0) + { + SeekedPosition = Position; + } try { PlaybackState.Builder stateBuilder = new PlaybackState.Builder() @@ -525,9 +544,10 @@ private void UpdatePlaybackState(PlaybackStateCode state) PlaybackState.ActionPlayPause | PlaybackState.ActionSkipToNext | PlaybackState.ActionSkipToPrevious | - PlaybackState.ActionStop + PlaybackState.ActionStop | + PlaybackState.ActionSeekTo ) - .SetState(state, Position, 1.0f, SystemClock.ElapsedRealtime()); + .SetState(state, SeekedPosition, 1.0f, SystemClock.ElapsedRealtime()); mediaSession.SetPlaybackState(stateBuilder.Build()); @@ -548,7 +568,6 @@ private void StartNotification() { if (mediaSession == null) return; - NotificationHelper.StartNotification( ApplicationContext, mediaController.Metadata, @@ -558,7 +577,7 @@ private void StartNotification() } /// - /// Updates the metadata on the lock screen + /// Updates the metadata on the lock screen and on notification bar /// private void UpdateMediaMetadataCompat(MediaMetadataRetriever metaRetriever = null) { @@ -570,16 +589,20 @@ private void UpdateMediaMetadataCompat(MediaMetadataRetriever metaRetriever = nu if (metaRetriever != null) { builder - .PutString(MediaMetadata.MetadataKeyAlbum,metaRetriever.ExtractMetadata(MetadataKey.Album)) + .PutString(MediaMetadata.MetadataKeyAlbum, metaRetriever.ExtractMetadata(MetadataKey.Album)) .PutString(MediaMetadata.MetadataKeyArtist, mediaPlay.Author ?? metaRetriever.ExtractMetadata(MetadataKey.Artist)) - .PutString(MediaMetadata.MetadataKeyTitle, mediaPlay.Name ?? metaRetriever.ExtractMetadata(MetadataKey.Title)); + .PutString(MediaMetadata.MetadataKeyTitle, mediaPlay.Name ?? metaRetriever.ExtractMetadata(MetadataKey.Title)) + .PutLong(MediaMetadata.MetadataKeyDuration, mediaPlay.DurationInMs); + // using this metaRetriever.ExtractMetadata(MetadataKey.Duration)) doesn't work } else { builder .PutString(MediaMetadata.MetadataKeyAlbum, mediaSession.Controller.Metadata.GetString(MediaMetadata.MetadataKeyAlbum)) .PutString(MediaMetadata.MetadataKeyArtist, mediaSession.Controller.Metadata.GetString(MediaMetadata.MetadataKeyArtist)) - .PutString(MediaMetadata.MetadataKeyTitle, mediaSession.Controller.Metadata.GetString(MediaMetadata.MetadataKeyTitle)); + .PutString(MediaMetadata.MetadataKeyTitle, mediaSession.Controller.Metadata.GetString(MediaMetadata.MetadataKeyTitle)) + .PutLong(MediaMetadata.MetadataKeyDuration, mediaSession.Controller.Metadata.GetLong(MediaMetadata.MetadataKeyDuration)); + } builder.PutBitmap(MediaMetadata.MetadataKeyAlbumArt, Cover as Bitmap); @@ -619,6 +642,10 @@ private void HandleIntent(Intent intent) { mediaController.GetTransportControls().Stop(); } + else if (action.Equals(ActionSeekTo)) + { + mediaController.GetTransportControls().SeekTo(Position); + } } /// @@ -731,17 +758,19 @@ public MediaSessionCallback(MediaPlayerServiceBinder service) { mediaPlayerService = service; } - + bool isPlaying = true; public override async void OnPause() { mediaPlayerService.GetMediaPlayerService().OnPlayingChanged(false); base.OnPause(); + isPlaying = false; } public override async void OnPlay() { mediaPlayerService.GetMediaPlayerService().OnPlayingChanged(true); base.OnPlay(); + isPlaying = true; } public override async void OnSkipToNext() @@ -761,13 +790,21 @@ public override async void OnStop() await mediaPlayerService.GetMediaPlayerService().Stop(); base.OnStop(); } + public override async void OnSeekTo(long position) + { + + await mediaPlayerService.GetMediaPlayerService().Seek((int)position); + mediaPlayerService.GetMediaPlayerService().UpdatePlaybackState( isPlaying == true ? PlaybackStateCode.Playing : PlaybackStateCode.Paused + , (int)position); + base.OnSeekTo(position); + } } } public class MediaPlayerServiceBinder : Binder { private readonly MediaPlayerService service; - + public MediaPlayerServiceBinder(MediaPlayerService service) { this.service = service;