Merge pull request #77 from microsoft/feature/use-native-player-on-blazor-app

Feature/use native player on blazor app
pull/89/head
Miguel Angel Barrera Muñoz 2022-04-25 08:40:40 +02:00 committed by GitHub
commit 8153149ff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1020 additions and 341 deletions

View File

@ -0,0 +1,24 @@
namespace SharedMauiLib;
public interface INativeAudioService
{
Task InitializeAsync(string audioURI);
Task PlayAsync(double position = 0);
Task PauseAsync();
Task SetMuted(bool value);
Task SetVolume(int value);
Task SetCurrentTime(double value);
ValueTask DisposeAsync();
bool IsPlaying { get; }
double CurrentPosition { get; }
event EventHandler<bool> IsPlayingChanged;
}

View File

@ -0,0 +1,13 @@
namespace SharedMauiLib.Platforms.Android.CurrentActivity
{
public enum ActivityEvent
{
Created,
Resumed,
Paused,
Destroyed,
SaveInstanceState,
Started,
Stopped
}
}

View File

@ -0,0 +1,16 @@
using Android.App;
namespace SharedMauiLib.Platforms.Android.CurrentActivity
{
public class ActivityEventArgs : EventArgs
{
internal ActivityEventArgs(Activity activity, ActivityEvent ev)
{
Event = ev;
Activity = activity;
}
public ActivityEvent Event { get; }
public Activity Activity { get; }
}
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SharedMauiLib.Platforms.Android.CurrentActivity
{
public class CrossCurrentActivity
{
static Lazy<ICurrentActivity> implementation = new Lazy<ICurrentActivity>(() => CreateCurrentActivity(), System.Threading.LazyThreadSafetyMode.PublicationOnly);
/// <summary>
/// Current settings to use
/// </summary>
public static ICurrentActivity Current
{
get
{
var ret = implementation.Value;
if (ret == null)
{
throw NotImplementedInReferenceAssembly();
}
return ret;
}
}
static ICurrentActivity CreateCurrentActivity()
{
#if NETSTANDARD1_0 || NETSTANDARD2_0
return null;
#else
return new CurrentActivityImplementation();
#endif
}
internal static Exception NotImplementedInReferenceAssembly()
{
return new NotImplementedException("This functionality is not implemented in the portable version of this assembly. You should reference the NuGet package from your main application project in order to reference the platform-specific implementation.");
}
}
}

View File

@ -0,0 +1,162 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using System;
using System.Threading;
using System.Threading.Tasks;
using AndroidApp = Android.App;
namespace SharedMauiLib.Platforms.Android.CurrentActivity
{
/// <summary>
/// Implementation for Feature
/// </summary>
[Preserve(AllMembers = true)]
public class CurrentActivityImplementation : ICurrentActivity
{
/// <summary>
/// Gets or sets the activity.
/// </summary>
/// <value>The activity.</value>
public Activity Activity
{
get => lifecycleListener?.Activity;
set
{
if (lifecycleListener == null)
Init(value, null);
}
}
/// <summary>
/// Activity state changed event handler
/// </summary>
public event EventHandler<ActivityEventArgs> ActivityStateChanged;
/// <summary>
/// Waits for an activity to be ready
/// </summary>
/// <returns></returns>
public async Task<Activity> WaitForActivityAsync(CancellationToken cancelToken = default)
{
if (Activity != null)
return Activity;
var tcs = new TaskCompletionSource<Activity>();
var handler = new EventHandler<ActivityEventArgs>((sender, args) =>
{
if (args.Event == ActivityEvent.Created || args.Event == ActivityEvent.Resumed)
tcs.TrySetResult(args.Activity);
});
try
{
using (cancelToken.Register(() => tcs.TrySetCanceled()))
{
ActivityStateChanged += handler;
return await tcs.Task.ConfigureAwait(false);
}
}
finally
{
ActivityStateChanged -= handler;
}
}
internal void RaiseStateChanged(Activity activity, ActivityEvent ev)
=> ActivityStateChanged?.Invoke(this, new ActivityEventArgs(activity, ev));
ActivityLifecycleContextListener lifecycleListener;
/// <summary>
/// Gets the current application context
/// </summary>
public Context AppContext =>
AndroidApp.Application.Context;
/// <summary>
/// Initialize current activity with application
/// </summary>
/// <param name="application">The main application</param>
public void Init(AndroidApp.Application application)
{
if (lifecycleListener != null)
return;
lifecycleListener = new ActivityLifecycleContextListener();
application.RegisterActivityLifecycleCallbacks(lifecycleListener);
}
/// <summary>
/// Initialize current activity with activity!
/// </summary>
/// <param name="activity">The main activity</param>
/// <param name="bundle">Bundle for activity </param>
public void Init(Activity activity, Bundle bundle)
{
Init(activity.Application);
lifecycleListener.Activity = activity;
}
}
[Preserve(AllMembers = true)]
class ActivityLifecycleContextListener : Java.Lang.Object, AndroidApp.Application.IActivityLifecycleCallbacks
{
WeakReference<Activity> currentActivity = new WeakReference<Activity>(null);
public Context Context =>
Activity ?? AndroidApp.Application.Context;
public Activity Activity
{
get => currentActivity.TryGetTarget(out var a) ? a : null;
set => currentActivity.SetTarget(value);
}
CurrentActivityImplementation Current =>
(CurrentActivityImplementation)(CrossCurrentActivity.Current);
public void OnActivityCreated(Activity activity, Bundle savedInstanceState)
{
Activity = activity;
Current.RaiseStateChanged(activity, ActivityEvent.Created);
}
public void OnActivityDestroyed(Activity activity)
{
Current.RaiseStateChanged(activity, ActivityEvent.Destroyed);
}
public void OnActivityPaused(Activity activity)
{
Activity = activity;
Current.RaiseStateChanged(activity, ActivityEvent.Paused);
}
public void OnActivityResumed(Activity activity)
{
Activity = activity;
Current.RaiseStateChanged(activity, ActivityEvent.Resumed);
}
public void OnActivitySaveInstanceState(Activity activity, Bundle outState)
{
Current.RaiseStateChanged(activity, ActivityEvent.SaveInstanceState);
}
public void OnActivityStarted(Activity activity)
{
Current.RaiseStateChanged(activity, ActivityEvent.Started);
}
public void OnActivityStopped(Activity activity)
{
Current.RaiseStateChanged(activity, ActivityEvent.Stopped);
}
}
}

View File

@ -0,0 +1,48 @@
using Android.Content;
using Android.OS;
using AndroidApp = Android.App;
namespace SharedMauiLib.Platforms.Android.CurrentActivity
{
/// <summary>
/// Current Activity Interface
/// </summary>
public interface ICurrentActivity
{
/// <summary>
/// Gets or sets the activity.
/// </summary>
/// <value>The activity.</value>
AndroidApp.Activity Activity { get; set; }
/// <summary>
/// Gets the current Application Context.
/// </summary>
/// <value>The app context.</value>
Context AppContext { get; }
/// <summary>
/// Fires when activity state events are fired
/// </summary>
event EventHandler<ActivityEventArgs> ActivityStateChanged;
/// <summary>
/// Waits for an activity to be ready for use
/// </summary>
/// <returns></returns>
Task<AndroidApp.Activity> WaitForActivityAsync(CancellationToken cancelToken = default);
/// <summary>
/// Initialize Current Activity Plugin with Application
/// </summary>
/// <param name="application"></param>
void Init(AndroidApp.Application application);
/// <summary>
/// Initialize the current activity with activity and bundle
/// </summary>
/// <param name="activity"></param>
/// <param name="bundle"></param>
void Init(AndroidApp.Activity activity, Bundle bundle);
}
}

View File

@ -1,4 +1,4 @@
namespace Microsoft.NetConf2021.Maui.Platforms.Android.Services;
namespace SharedMauiLib.Platforms.Android;
public delegate void StatusChangedEventHandler(object sender, EventArgs e);
@ -7,3 +7,5 @@ public delegate void BufferingEventHandler(object sender, EventArgs e);
public delegate void CoverReloadedEventHandler(object sender, EventArgs e);
public delegate void PlayingEventHandler(object sender, EventArgs e);
public delegate void PlayingChangedEventHandler(object sender, bool e);

View File

@ -0,0 +1,15 @@
namespace SharedMauiLib.Platforms.Android
{
public interface IAudioActivity
{
public MediaPlayerServiceBinder Binder { get; set; }
public event StatusChangedEventHandler StatusChanged;
public event CoverReloadedEventHandler CoverReloaded;
public event PlayingEventHandler Playing;
public event BufferingEventHandler Buffering;
}
}

View File

@ -5,16 +5,15 @@ using Android.Net;
using Android.Net.Wifi;
using Android.OS;
using Android.Media.Session;
using Microsoft.NetConf2021.Maui.Platforms.Android.Receivers;
using AndroidNet = Android.Net;
using Android.Graphics;
using Microsoft.Maui.Platform;
namespace Microsoft.NetConf2021.Maui.Platforms.Android.Services;
namespace SharedMauiLib.Platforms.Android;
[Service(Exported = true)]
[IntentFilter(new[] { ActionPlay, ActionPause, ActionStop, ActionTogglePlayback, ActionNext, ActionPrevious })]
public class MediaPlayerService : Service,
public class MediaPlayerService : Service,
AudioManager.IOnAudioFocusChangeListener,
MediaPlayer.IOnBufferingUpdateListener,
MediaPlayer.IOnCompletionListener,
@ -46,6 +45,12 @@ public class MediaPlayerService : Service,
public event BufferingEventHandler Buffering;
public event PlayingChangedEventHandler PlayingChanged;
public string AudioUrl;
public bool isCurrentEpisode = true;
private readonly Handler PlayingHandler;
private readonly Java.Lang.Runnable PlayingHandlerRunnable;
@ -55,9 +60,9 @@ public class MediaPlayerService : Service,
{
get
{
return (mediaController.PlaybackState != null
? mediaController.PlaybackState.State
: PlaybackStateCode.None);
return mediaController.PlaybackState != null
? mediaController.PlaybackState.State
: PlaybackStateCode.None;
}
}
@ -66,7 +71,8 @@ public class MediaPlayerService : Service,
PlayingHandler = new Handler(Looper.MainLooper);
// Create a runnable, restarting itself if the status still is "playing"
PlayingHandlerRunnable = new Java.Lang.Runnable(() => {
PlayingHandlerRunnable = new Java.Lang.Runnable(() =>
{
OnPlaying(EventArgs.Empty);
if (MediaPlayerState == PlaybackStateCode.Playing)
@ -76,7 +82,8 @@ public class MediaPlayerService : Service,
});
// On Status changed to PLAYING, start raising the Playing event
StatusChanged += (object sender, EventArgs e) => {
StatusChanged += (sender, e) =>
{
if (MediaPlayerState == PlaybackStateCode.Playing)
{
PlayingHandler.PostDelayed(PlayingHandlerRunnable, 0);
@ -89,6 +96,11 @@ public class MediaPlayerService : Service,
StatusChanged?.Invoke(this, e);
}
protected virtual void OnPlayingChanged(bool e)
{
PlayingChanged?.Invoke(this, e);
}
protected virtual void OnCoverReloaded(EventArgs e)
{
if (CoverReloaded != null)
@ -131,7 +143,7 @@ public class MediaPlayerService : Service,
{
if (mediaSession == null)
{
Intent nIntent = new Intent(ApplicationContext, typeof(MainActivity));
Intent nIntent = new Intent(ApplicationContext, typeof(Activity));
remoteComponentName = new ComponentName(PackageName, new RemoteControlBroadcastReceiver().ComponentName);
@ -203,15 +215,13 @@ public class MediaPlayerService : Service,
UpdatePlaybackState(PlaybackStateCode.Playing);
}
public string AudioUrl ;
public int Position
{
get
{
if (mediaPlayer == null
|| (MediaPlayerState != PlaybackStateCode.Playing
&& MediaPlayerState != PlaybackStateCode.Paused))
|| MediaPlayerState != PlaybackStateCode.Playing
&& MediaPlayerState != PlaybackStateCode.Paused)
return -1;
else
return mediaPlayer.CurrentPosition;
@ -223,8 +233,8 @@ public class MediaPlayerService : Service,
get
{
if (mediaPlayer == null
|| (MediaPlayerState != PlaybackStateCode.Playing
&& MediaPlayerState != PlaybackStateCode.Paused))
|| MediaPlayerState != PlaybackStateCode.Playing
&& MediaPlayerState != PlaybackStateCode.Paused)
return 0;
else
return mediaPlayer.Duration;
@ -256,7 +266,7 @@ public class MediaPlayerService : Service,
get
{
if (cover == null)
cover = BitmapFactory.DecodeResource(Resources, Resource.Drawable.player_play);
cover = BitmapFactory.DecodeResource(Resources, Resource.Drawable.abc_ab_share_pack_mtrl_alpha); //TODO player_play
return cover;
}
private set
@ -289,12 +299,14 @@ public class MediaPlayerService : Service,
if (mediaSession == null)
InitMediaSession();
if (mediaPlayer.IsPlaying)
if (mediaPlayer.IsPlaying && isCurrentEpisode)
{
UpdatePlaybackState(PlaybackStateCode.Playing);
return;
}
isCurrentEpisode = true;
await PrepareAndPlayMediaPlayerAsync();
}
@ -330,18 +342,14 @@ public class MediaPlayerService : Service,
byte[] imageByteArray = metaRetriever.GetEmbeddedPicture();
if (imageByteArray == null)
Cover = await BitmapFactory.DecodeResourceAsync(Resources, Resource.Drawable.player_play);
Cover = await BitmapFactory.DecodeResourceAsync(Resources, Resource.Drawable.abc_ab_share_pack_mtrl_alpha); //TODO player_play
else
Cover = await BitmapFactory.DecodeByteArrayAsync(imageByteArray, 0, imageByteArray.Length);
}
}
catch (Exception ex)
{
UpdatePlaybackState(PlaybackStateCode.Stopped);
mediaPlayer.Reset();
mediaPlayer.Release();
mediaPlayer = null;
UpdatePlaybackStateStopped();
// Unable to start playback log error
Console.WriteLine(ex);
@ -350,7 +358,8 @@ public class MediaPlayerService : Service,
public async Task Seek(int position)
{
await Task.Run(() => {
await Task.Run(() =>
{
if (mediaPlayer != null)
{
mediaPlayer.SeekTo(position);
@ -396,7 +405,7 @@ public class MediaPlayerService : Service,
public async Task PlayPause()
{
if (mediaPlayer == null || (mediaPlayer != null && MediaPlayerState == PlaybackStateCode.Paused))
if (mediaPlayer == null || mediaPlayer != null && MediaPlayerState == PlaybackStateCode.Paused)
{
await Play();
}
@ -408,7 +417,8 @@ public class MediaPlayerService : Service,
public async Task Pause()
{
await Task.Run(() => {
await Task.Run(() =>
{
if (mediaPlayer == null)
return;
@ -421,7 +431,8 @@ public class MediaPlayerService : Service,
public async Task Stop()
{
await Task.Run(() => {
await Task.Run(() =>
{
if (mediaPlayer == null)
return;
@ -439,6 +450,18 @@ public class MediaPlayerService : Service,
});
}
public void UpdatePlaybackStateStopped()
{
UpdatePlaybackState(PlaybackStateCode.Stopped);
if (mediaPlayer != null)
{
mediaPlayer.Reset();
mediaPlayer.Release();
mediaPlayer = null;
}
}
private void UpdatePlaybackState(PlaybackStateCode state)
{
if (mediaSession == null || mediaPlayer == null)
@ -485,6 +508,16 @@ public class MediaPlayerService : Service,
MediaPlayerState == PlaybackStateCode.Playing);
}
internal void SetMuted(bool value)
{
mediaPlayer.SetVolume(0, 0);
}
internal void SetVolume(int value)
{
mediaPlayer.SetVolume(value, value);
}
/// <summary>
/// Updates the metadata on the lock screen
/// </summary>
@ -662,13 +695,13 @@ public class MediaPlayerService : Service,
public override async void OnPause()
{
await mediaPlayerService.GetMediaPlayerService().Pause();
mediaPlayerService.GetMediaPlayerService().OnPlayingChanged(false);
base.OnPause();
}
public override async void OnPlay()
{
await mediaPlayerService.GetMediaPlayerService().Play();
mediaPlayerService.GetMediaPlayerService().OnPlayingChanged(true);
base.OnPlay();
}

View File

@ -0,0 +1,33 @@
using Android.Content;
using Android.OS;
namespace SharedMauiLib.Platforms.Android
{
public class MediaPlayerServiceConnection : Java.Lang.Object, IServiceConnection
{
readonly IAudioActivity instance;
public MediaPlayerServiceConnection(IAudioActivity mediaPlayer)
{
this.instance = mediaPlayer;
}
public void OnServiceConnected(ComponentName name, IBinder service)
{
if (service is MediaPlayerServiceBinder binder)
{
instance.Binder = binder;
var mediaPlayerService = binder.GetMediaPlayerService();
//mediaPlayerService.CoverReloaded += (object sender, EventArgs e) => { instance.CoverReloaded?.Invoke(sender, e); };
//mediaPlayerService.StatusChanged += (object sender, EventArgs e) => { instance.StatusChanged?.Invoke(sender, e); };
//mediaPlayerService.Playing += (object sender, EventArgs e) => { instance.Playing?.Invoke(sender, e); };
//mediaPlayerService.Buffering += (object sender, EventArgs e) => { instance.Buffering?.Invoke(sender, e); };
}
}
public void OnServiceDisconnected(ComponentName name)
{
}
}
}

View File

@ -0,0 +1,93 @@
using Android.Media;
using AndroidApp = Android.App;
namespace SharedMauiLib.Platforms.Android
{
public class NativeAudioService : INativeAudioService
{
IAudioActivity instance;
private MediaPlayer mediaPlayer => instance != null &&
instance.Binder.GetMediaPlayerService() != null ?
instance.Binder.GetMediaPlayerService().mediaPlayer : null;
public bool IsPlaying => mediaPlayer?.IsPlaying ?? false;
public double CurrentPosition => mediaPlayer?.CurrentPosition / 1000 ?? 0;
public event EventHandler<bool> IsPlayingChanged;
public Task InitializeAsync(string audioURI)
{
if (instance == null)
{
var activity = CurrentActivity.CrossCurrentActivity.Current;
instance = activity.Activity as IAudioActivity;
}
else
{
instance.Binder.GetMediaPlayerService().isCurrentEpisode = false;
instance.Binder.GetMediaPlayerService().UpdatePlaybackStateStopped();
}
this.instance.Binder.GetMediaPlayerService().PlayingChanged += (object sender, bool e) =>
{
Task.Run(async () => {
if (e)
{
await this.PlayAsync();
}
else
{
await this.PauseAsync();
}
});
IsPlayingChanged?.Invoke(this, e);
};
instance.Binder.GetMediaPlayerService().AudioUrl = audioURI;
return Task.CompletedTask;
}
public Task PauseAsync()
{
if (IsPlaying)
{
return instance.Binder.GetMediaPlayerService().Pause();
}
return Task.CompletedTask;
}
public async Task PlayAsync(double position = 0)
{
await instance.Binder.GetMediaPlayerService().Play();
await instance.Binder.GetMediaPlayerService().Seek((int)position * 1000);
}
public Task SetMuted(bool value)
{
instance?.Binder.GetMediaPlayerService().SetMuted(value);
return Task.CompletedTask;
}
public Task SetVolume(int value)
{
instance?.Binder.GetMediaPlayerService().SetVolume(value);
return Task.CompletedTask;
}
public Task SetCurrentTime(double position)
{
return instance.Binder.GetMediaPlayerService().Seek((int)position * 1000);
}
public ValueTask DisposeAsync()
{
instance.Binder?.Dispose();
return ValueTask.CompletedTask;
}
}
}

View File

@ -8,7 +8,8 @@ using static Android.App.Notification;
using static Android.Resource;
using AndroidMedia = Android.Media;
namespace Microsoft.NetConf2021.Maui.Platforms.Android.Services;
namespace SharedMauiLib.Platforms.Android;
public static class NotificationHelper
{
@ -37,7 +38,7 @@ public static class NotificationHelper
nm.CancelAll();
}
internal static void CreateNotificationChannel(Context context)
public static void CreateNotificationChannel(Context context)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
{
@ -59,50 +60,50 @@ public static class NotificationHelper
}
internal static void StartNotification(
Context context,
Context context,
MediaMetadata mediaMetadata,
AndroidMedia.Session.MediaSession mediaSession,
Object largeIcon,
object largeIcon,
bool isPlaying)
{
var pendingIntent = PendingIntent.GetActivity(
context,
0,
new Intent(context, typeof(MainActivity)),
new Intent(context, typeof(Activity)),
PendingIntentFlags.UpdateCurrent | PendingIntentFlags.Mutable);
MediaMetadata currentTrack = mediaMetadata;
MediaStyle style = new MediaStyle();
style.SetMediaSession(mediaSession.SessionToken);
var builder = new Notification.Builder(context, CHANNEL_ID)
var builder = new Builder(context, CHANNEL_ID)
.SetStyle(style)
.SetContentTitle(currentTrack.GetString(MediaMetadata.MetadataKeyTitle))
.SetContentText(currentTrack.GetString(MediaMetadata.MetadataKeyArtist))
.SetSubText(currentTrack.GetString(MediaMetadata.MetadataKeyAlbum))
.SetSmallIcon(Resource.Drawable.player_play)
.SetSmallIcon(Resource.Drawable.abc_ab_share_pack_mtrl_alpha) //TODO player_play
.SetLargeIcon(largeIcon as Bitmap)
.SetContentIntent(pendingIntent)
.SetShowWhen(false)
.SetOngoing(isPlaying)
.SetVisibility(NotificationVisibility.Public);
builder.AddAction(NotificationHelper.GenerateActionCompat(context, Drawable.IcMediaPrevious, "Previous", MediaPlayerService.ActionPrevious));
builder.AddAction(GenerateActionCompat(context, Drawable.IcMediaPrevious, "Previous", MediaPlayerService.ActionPrevious));
AddPlayPauseActionCompat(builder, context, isPlaying);
builder.AddAction(NotificationHelper.GenerateActionCompat(context, Drawable.IcMediaNext, "Next", MediaPlayerService.ActionNext));
builder.AddAction(GenerateActionCompat(context, Drawable.IcMediaNext, "Next", MediaPlayerService.ActionNext));
style.SetShowActionsInCompactView(0, 1, 2);
NotificationManagerCompat.From(context).Notify(NotificationId, builder.Build());
}
private static void AddPlayPauseActionCompat(
Notification.Builder builder,
Builder builder,
Context context,
bool isPlaying)
{
if (isPlaying)
builder.AddAction(NotificationHelper.GenerateActionCompat(context, Drawable.IcMediaPause, "Pause", MediaPlayerService.ActionPause));
builder.AddAction(GenerateActionCompat(context, Drawable.IcMediaPause, "Pause", MediaPlayerService.ActionPause));
else
builder.AddAction(NotificationHelper.GenerateActionCompat(context, Drawable.IcMediaPlay, "Play", MediaPlayerService.ActionPlay));
builder.AddAction(GenerateActionCompat(context, Drawable.IcMediaPlay, "Play", MediaPlayerService.ActionPlay));
}
}

View File

@ -0,0 +1,68 @@
using Android.App;
using Android.Content;
using Android.Views;
namespace SharedMauiLib.Platforms.Android;
[BroadcastReceiver(Exported = true)]
[IntentFilter(new[] { Intent.ActionMediaButton })]
public class RemoteControlBroadcastReceiver : BroadcastReceiver
{
/// <summary>
/// gets the class name for the component
/// </summary>
/// <value>The name of the component.</value>
public string ComponentName { get { return Class.Name; } }
/// <Docs>The Context in which the receiver is running.</Docs>
/// <summary>
/// When we receive the action media button intent
/// parse the key event and tell our service what to do.
/// </summary>
/// <param name="context">Context.</param>
/// <param name="intent">Intent.</param>
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action != Intent.ActionMediaButton)
return;
//The event will fire twice, up and down.
// we only want to handle the down event though.
var key = (KeyEvent)intent.GetParcelableExtra(Intent.ExtraKeyEvent);
if (key.Action != KeyEventActions.Down)
return;
string action;
switch (key.KeyCode)
{
case Keycode.Headsethook:
case Keycode.MediaPlayPause:
action = MediaPlayerService.ActionTogglePlayback;
break;
case Keycode.MediaPlay:
action = MediaPlayerService.ActionPlay;
break;
case Keycode.MediaPause:
action = MediaPlayerService.ActionPause;
break;
case Keycode.MediaStop:
action = MediaPlayerService.ActionStop;
break;
case Keycode.MediaNext:
action = MediaPlayerService.ActionNext;
break;
case Keycode.MediaPrevious:
action = MediaPlayerService.ActionPrevious;
break;
default:
return;
}
var remoteIntent = new Intent(action);
context.StartService(remoteIntent);
}
}

View File

@ -0,0 +1,74 @@
using AVFoundation;
using Foundation;
namespace SharedMauiLib.Platforms.MacCatalyst;
public class NativeAudioService : INativeAudioService
{
AVPlayer avPlayer;
string _uri;
public bool IsPlaying => avPlayer != null
? avPlayer.TimeControlStatus == AVPlayerTimeControlStatus.Playing
: false;
public double CurrentPosition => avPlayer?.CurrentTime.Seconds ?? 0;
public event EventHandler<bool> IsPlayingChanged;
public async Task InitializeAsync(string audioURI)
{
_uri = audioURI;
NSUrl fileURL = new NSUrl(_uri.ToString());
if (avPlayer != null)
{
await PauseAsync();
}
avPlayer = new AVPlayer(fileURL);
}
public Task PauseAsync()
{
avPlayer?.Pause();
return Task.CompletedTask;
}
public async Task PlayAsync(double position = 0)
{
await avPlayer.SeekAsync(new CoreMedia.CMTime((long)position, 1));
avPlayer?.Play();
}
public Task SetCurrentTime(double value)
{
return avPlayer.SeekAsync(new CoreMedia.CMTime((long)value, 1));
}
public Task SetMuted(bool value)
{
if (avPlayer != null)
{
avPlayer.Muted = value;
}
return Task.CompletedTask;
}
public Task SetVolume(int value)
{
if (avPlayer != null)
{
avPlayer.Volume = value;
}
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
{
avPlayer?.Dispose();
return ValueTask.CompletedTask;
}
}

View File

@ -0,0 +1,91 @@
using Windows.Media.Core;
using Windows.Media.Playback;
namespace SharedMauiLib.Platforms.Windows;
public class NativeAudioService : INativeAudioService
{
string _uri;
MediaPlayer mediaPlayer;
public bool IsPlaying => mediaPlayer != null
&& mediaPlayer.CurrentState == MediaPlayerState.Playing;
public double CurrentPosition => mediaPlayer?.Position.TotalSeconds ?? 0;
public event EventHandler<bool> IsPlayingChanged;
public async Task InitializeAsync(string audioURI)
{
_uri = audioURI;
if (mediaPlayer == null)
{
mediaPlayer = new MediaPlayer
{
Source = MediaSource.CreateFromUri(new Uri(_uri)),
AudioCategory = MediaPlayerAudioCategory.Media
};
}
if (mediaPlayer != null)
{
await PauseAsync();
mediaPlayer.Source = MediaSource.CreateFromUri(new Uri(_uri));
}
}
public Task PauseAsync()
{
mediaPlayer?.Pause();
return Task.CompletedTask;
}
public Task PlayAsync(double position = 0)
{
if (mediaPlayer != null)
{
mediaPlayer.Position = TimeSpan.FromSeconds(position);
mediaPlayer.Play();
}
return Task.CompletedTask;
}
public Task SetCurrentTime(double value)
{
if (mediaPlayer != null)
{
mediaPlayer.Position = TimeSpan.FromSeconds(value);
}
return Task.CompletedTask;
}
public Task SetMuted(bool value)
{
if (mediaPlayer != null)
{
mediaPlayer.IsMuted = value;
}
return Task.CompletedTask;
}
public Task SetVolume(int value)
{
if (mediaPlayer != null)
{
mediaPlayer.Volume = value != 0
? value / 100d
: 0;
}
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
{
mediaPlayer?.Dispose();
return ValueTask.CompletedTask;
}
}

View File

@ -1,18 +1,19 @@
using AVFoundation;
using Foundation;
namespace Microsoft.NetConf2021.Maui.Platforms.iOS;
namespace SharedMauiLib.Platforms.iOS;
public class AudioService : IAudioService
public class NativeAudioService : INativeAudioService
{
AVPlayer avPlayer;
string _uri;
public bool IsPlaying => avPlayer != null
? avPlayer.TimeControlStatus == AVPlayerTimeControlStatus.Playing
: false; //TODO
: false;
public double CurrentPosition => avPlayer?.CurrentTime.Seconds ?? 0;
public event EventHandler<bool> IsPlayingChanged;
public async Task InitializeAsync(string audioURI)
{
@ -30,7 +31,7 @@ public class AudioService : IAudioService
public Task PauseAsync()
{
avPlayer?.Pause();
return Task.CompletedTask;
}
@ -39,4 +40,35 @@ public class AudioService : IAudioService
await avPlayer.SeekAsync(new CoreMedia.CMTime((long)position, 1));
avPlayer?.Play();
}
}
public Task SetCurrentTime(double value)
{
return avPlayer.SeekAsync(new CoreMedia.CMTime((long)value, 1));
}
public Task SetMuted(bool value)
{
if (avPlayer != null)
{
avPlayer.Muted = value;
}
return Task.CompletedTask;
}
public Task SetVolume(int value)
{
if (avPlayer != null)
{
avPlayer.Volume = value;
}
return Task.CompletedTask;
}
public ValueTask DisposeAsync()
{
avPlayer?.Dispose();
return ValueTask.CompletedTask;
}
}

View File

Before

Width:  |  Height:  |  Size: 637 B

After

Width:  |  Height:  |  Size: 637 B

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!--<TargetFrameworks>net6.0-android</TargetFrameworks>-->
<TargetFrameworks>net6.0;net6.0-android;net6.0-ios;net6.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows')) and '$(MSBuildRuntimeType)' == 'Full'">$(TargetFrameworks);net6.0-windows10.0.19041</TargetFrameworks>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<SupportedOSPlatformVersion Condition="'$(TargetFramework)' == 'net6.0-ios'">14.2</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="'$(TargetFramework)' == 'net6.0-maccatalyst'">14.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="'$(TargetFramework)' == 'net6.0-android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$(TargetFramework.Contains('-windows'))">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$(TargetFramework.Contains('-windows'))">10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>
<ItemGroup>
<MauiImage Include="Resources\Images\*" />
</ItemGroup>
</Project>

View File

@ -56,6 +56,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lib\SharedMauiLib\SharedMauiLib.csproj" />
<ProjectReference Include="..\Web\Components\Podcast.Components.csproj" />
</ItemGroup>

View File

@ -1,46 +0,0 @@
using Android.Media;
namespace Microsoft.NetConf2021.Maui.Platforms.Android;
public class AudioService : IAudioService
{
MainActivity instance;
private MediaPlayer mediaPlayer => (instance != null &&
instance.binder.GetMediaPlayerService() != null ) ?
instance.binder.GetMediaPlayerService().mediaPlayer : null;
public bool IsPlaying => mediaPlayer?.IsPlaying ?? false;
public double CurrentPosition => mediaPlayer?.CurrentPosition/1000 ?? 0;
public async Task InitializeAsync(string audioURI)
{
if (this.instance == null)
{
this.instance = MainActivity.instance;
}
else
{
await this.instance.binder.GetMediaPlayerService().Stop();
}
this.instance.binder.GetMediaPlayerService().AudioUrl = audioURI;
}
public Task PauseAsync()
{
if (IsPlaying)
{
return this.instance.binder.GetMediaPlayerService().Pause();
}
return Task.CompletedTask;
}
public async Task PlayAsync(double position = 0)
{
await this.instance.binder.GetMediaPlayerService().Play();
await this.instance.binder.GetMediaPlayerService().Seek((int)position * 1000);
}
}

View File

@ -2,7 +2,8 @@
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Microsoft.NetConf2021.Maui.Platforms.Android.Services;
using SharedMauiLib.Platforms.Android;
using SharedMauiLib.Platforms.Android.CurrentActivity;
namespace Microsoft.NetConf2021.Maui;
@ -10,24 +11,21 @@ namespace Microsoft.NetConf2021.Maui;
Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)]
public class MainActivity : MauiAppCompatActivity
public class MainActivity : MauiAppCompatActivity , IAudioActivity
{
internal static MainActivity instance;
public MediaPlayerServiceBinder binder;
MediaPlayerServiceConnection mediaPlayerServiceConnection;
public MediaPlayerServiceBinder Binder { get; set; }
public event StatusChangedEventHandler StatusChanged;
public event CoverReloadedEventHandler CoverReloaded;
public event PlayingEventHandler Playing;
public event BufferingEventHandler Buffering;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
instance = this;
CrossCurrentActivity.Current.Init(this, savedInstanceState);
NotificationHelper.CreateNotificationChannel(ApplicationContext);
if (mediaPlayerServiceConnection == null)
InitializeMedia();
@ -39,32 +37,4 @@ public class MainActivity : MauiAppCompatActivity
var mediaPlayerServiceIntent = new Intent(ApplicationContext, typeof(MediaPlayerService));
BindService(mediaPlayerServiceIntent, mediaPlayerServiceConnection, Bind.AutoCreate);
}
class MediaPlayerServiceConnection : Java.Lang.Object, IServiceConnection
{
readonly MainActivity instance;
public MediaPlayerServiceConnection(MainActivity mediaPlayer)
{
this.instance = mediaPlayer;
}
public void OnServiceConnected(ComponentName name, IBinder service)
{
if (service is MediaPlayerServiceBinder binder)
{
instance.binder = binder;
var mediaPlayerService = binder.GetMediaPlayerService();
mediaPlayerService.CoverReloaded += (object sender, EventArgs e) => { instance.CoverReloaded?.Invoke(sender, e); };
mediaPlayerService.StatusChanged += (object sender, EventArgs e) => { instance.StatusChanged?.Invoke(sender, e); };
mediaPlayerService.Playing += (object sender, EventArgs e) => { instance.Playing?.Invoke(sender, e); };
mediaPlayerService.Buffering += (object sender, EventArgs e) => { instance.Buffering?.Invoke(sender, e); };
}
}
public void OnServiceDisconnected(ComponentName name)
{
}
}
}

View File

@ -1,69 +0,0 @@
using Android.App;
using Android.Content;
using Android.Views;
using Microsoft.NetConf2021.Maui.Platforms.Android.Services;
namespace Microsoft.NetConf2021.Maui.Platforms.Android.Receivers;
[BroadcastReceiver(Exported = true)]
[IntentFilter(new[] { Intent.ActionMediaButton })]
public class RemoteControlBroadcastReceiver : BroadcastReceiver
{
/// <summary>
/// gets the class name for the component
/// </summary>
/// <value>The name of the component.</value>
public string ComponentName { get { return this.Class.Name; } }
/// <Docs>The Context in which the receiver is running.</Docs>
/// <summary>
/// When we receive the action media button intent
/// parse the key event and tell our service what to do.
/// </summary>
/// <param name="context">Context.</param>
/// <param name="intent">Intent.</param>
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action != Intent.ActionMediaButton)
return;
//The event will fire twice, up and down.
// we only want to handle the down event though.
var key = (KeyEvent)intent.GetParcelableExtra(Intent.ExtraKeyEvent);
if (key.Action != KeyEventActions.Down)
return;
string action;
switch (key.KeyCode)
{
case Keycode.Headsethook:
case Keycode.MediaPlayPause:
action = MediaPlayerService.ActionTogglePlayback;
break;
case Keycode.MediaPlay:
action = MediaPlayerService.ActionPlay;
break;
case Keycode.MediaPause:
action = MediaPlayerService.ActionPause;
break;
case Keycode.MediaStop:
action = MediaPlayerService.ActionStop;
break;
case Keycode.MediaNext:
action = MediaPlayerService.ActionNext;
break;
case Keycode.MediaPrevious:
action = MediaPlayerService.ActionPrevious;
break;
default:
return;
}
var remoteIntent = new Intent(action);
context.StartService(remoteIntent);
}
}

View File

@ -1,43 +0,0 @@
using AVFoundation;
using Foundation;
namespace Microsoft.NetConf2021.Maui.Platforms.MacCatalyst;
public class AudioService : IAudioService
{
AVPlayer avPlayer;
string _uri;
public bool IsPlaying => avPlayer != null
? avPlayer.TimeControlStatus == AVPlayerTimeControlStatus.Playing
: false; //TODO
public double CurrentPosition => avPlayer?.CurrentTime.Seconds ?? 0;
public async Task InitializeAsync(string audioURI)
{
_uri = audioURI;
NSUrl fileURL = new NSUrl(_uri.ToString());
if (avPlayer != null)
{
await PauseAsync();
}
avPlayer = new AVPlayer(fileURL);
}
public Task PauseAsync()
{
avPlayer?.Pause();
return Task.CompletedTask;
}
public async Task PlayAsync(double position = 0)
{
await avPlayer.SeekAsync(new CoreMedia.CMTime((long)position, 1));
avPlayer?.Play();
}
}

View File

@ -1,52 +0,0 @@
using Windows.Media.Core;
using Windows.Media.Playback;
namespace Microsoft.NetConf2021.Maui.Platforms.Windows;
public class AudioService : IAudioService
{
string _uri;
MediaPlayer mediaPlayer;
public bool IsPlaying => mediaPlayer != null
&& mediaPlayer.CurrentState == MediaPlayerState.Playing;
public double CurrentPosition => (long)mediaPlayer?.Position.TotalSeconds;
public async Task InitializeAsync(string audioURI)
{
_uri = audioURI;
if(this.mediaPlayer == null)
{
this.mediaPlayer = new MediaPlayer
{
Source = MediaSource.CreateFromUri(new Uri(_uri)),
AudioCategory = MediaPlayerAudioCategory.Media
};
}
if (this.mediaPlayer != null)
{
await PauseAsync();
this.mediaPlayer.Source = MediaSource.CreateFromUri(new Uri(_uri));
}
}
public Task PauseAsync()
{
this.mediaPlayer?.Pause();
return Task.CompletedTask;
}
public Task PlayAsync(double position = 0)
{
if (this.mediaPlayer != null)
{
mediaPlayer.Position = TimeSpan.FromSeconds(position);
mediaPlayer.Play();
}
return Task.CompletedTask;
}
}

View File

@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Components", "..\Web\Components\Podcast.Components.csproj", "{39AD8B68-B8D3-43D4-86D4-3589E4AE81AD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedMauiLib", "..\Lib\SharedMauiLib\SharedMauiLib.csproj", "{772830C6-56B3-4A57-A8E1-50A715F149F5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -31,6 +33,10 @@ Global
{39AD8B68-B8D3-43D4-86D4-3589E4AE81AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39AD8B68-B8D3-43D4-86D4-3589E4AE81AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39AD8B68-B8D3-43D4-86D4-3589E4AE81AD}.Release|Any CPU.Build.0 = Release|Any CPU
{772830C6-56B3-4A57-A8E1-50A715F149F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{772830C6-56B3-4A57-A8E1-50A715F149F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{772830C6-56B3-4A57-A8E1-50A715F149F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{772830C6-56B3-4A57-A8E1-50A715F149F5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,10 +0,0 @@
namespace Microsoft.NetConf2021.Maui.Services;
public interface IAudioService
{
Task InitializeAsync(string audioURI);
Task PlayAsync(double position = 0);
Task PauseAsync();
bool IsPlaying { get; }
double CurrentPosition { get; }
}

View File

@ -1,8 +1,10 @@
namespace Microsoft.NetConf2021.Maui.Services;
using SharedMauiLib;
namespace Microsoft.NetConf2021.Maui.Services;
public class PlayerService
{
private readonly IAudioService audioService;
private readonly INativeAudioService audioService;
private readonly WifiOptionsService wifiOptionsService;
public Episode CurrentEpisode { get; set; }
@ -14,10 +16,16 @@ public class PlayerService
public event EventHandler NewEpisodeAdded;
public event EventHandler IsPlayingChanged;
public PlayerService(IAudioService audioService, WifiOptionsService wifiOptionsService)
public PlayerService(INativeAudioService audioService, WifiOptionsService wifiOptionsService)
{
this.audioService = audioService;
this.wifiOptionsService = wifiOptionsService;
this.audioService.IsPlayingChanged += (object sender, bool e) =>
{
IsPlaying = e;
IsPlayingChanged?.Invoke(this, EventArgs.Empty);
};
}
public async Task PlayAsync(Episode episode, Show show, bool isPlaying, double position = 0)
@ -39,27 +47,13 @@ public class PlayerService
await audioService.InitializeAsync(CurrentEpisode.Url.ToString());
if (isPlaying)
{
await InternalPlayAsync(initializePlayer: false, position);
}
else
{
await InternalPauseAsync();
}
await InternalPlayPauseAsync(isPlaying, position);
NewEpisodeAdded?.Invoke(this, EventArgs.Empty);
}
else
{
if (isPlaying)
{
await InternalPlayAsync(initializePlayer: false, position);
}
else
{
await InternalPauseAsync();
}
await InternalPlayPauseAsync(isPlaying, position);
}
IsPlayingChanged?.Invoke(this, EventArgs.Empty);
@ -75,13 +69,25 @@ public class PlayerService
return PlayAsync(episode, show, isPlaying, position);
}
private async Task InternalPlayPauseAsync(bool isPlaying, double position)
{
if (isPlaying)
{
await InternalPlayAsync(position);
}
else
{
await InternalPauseAsync();
}
}
private async Task InternalPauseAsync()
{
await audioService.PauseAsync();
IsPlaying = false;
}
private async Task InternalPlayAsync(bool initializePlayer = false, double position = 0)
private async Task InternalPlayAsync(double position = 0)
{
var canPlay = await wifiOptionsService.HasWifiOrCanPlayWithOutWifiAsync();
@ -90,11 +96,6 @@ public class PlayerService
return;
}
if (initializePlayer)
{
await audioService.InitializeAsync(CurrentEpisode.Url.ToString());
}
await audioService.PlayAsync(position);
IsPlaying = true;
}

View File

@ -11,14 +11,14 @@ public static class ServicesExtensions
builder.Services.AddSingleton<ShowsService>();
builder.Services.AddSingleton<ListenLaterService>();
#if WINDOWS
builder.Services.TryAddSingleton<IAudioService, Platforms.Windows.AudioService>();
builder.Services.TryAddSingleton<SharedMauiLib.INativeAudioService, SharedMauiLib.Platforms.Windows.NativeAudioService>();
#elif ANDROID
builder.Services.TryAddSingleton<IAudioService, Platforms.Android.AudioService>();
builder.Services.TryAddSingleton<SharedMauiLib.INativeAudioService, SharedMauiLib.Platforms.Android.NativeAudioService>();
#elif MACCATALYST
builder.Services.TryAddSingleton<IAudioService, Platforms.MacCatalyst.AudioService>();
builder.Services.TryAddSingleton<SharedMauiLib.INativeAudioService, SharedMauiLib.Platforms.MacCatalyst.NativeAudioService>();
builder.Services.TryAddSingleton< Platforms.MacCatalyst.ConnectivityService>();
#elif IOS
builder.Services.TryAddSingleton<IAudioService, Platforms.iOS.AudioService>();
builder.Services.TryAddSingleton<SharedMauiLib.INativeAudioService, SharedMauiLib.Platforms.iOS.NativeAudioService>();
#endif
builder.Services.TryAddTransient<WifiOptionsService>();

View File

@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Pages", "..\Web\Pag
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Podcast.Components", "..\Web\Components\Podcast.Components.csproj", "{BFA06A28-839A-4324-87EE-2FF6B8D522A2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedMauiLib", "..\Lib\SharedMauiLib\SharedMauiLib.csproj", "{4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -38,6 +40,10 @@ Global
{BFA06A28-839A-4324-87EE-2FF6B8D522A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFA06A28-839A-4324-87EE-2FF6B8D522A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFA06A28-839A-4324-87EE-2FF6B8D522A2}.Release|Any CPU.Build.0 = Release|Any CPU
{4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A34B6DA-AE7F-4C6B-B8A8-CDC14A2CA178}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Components.WebView.Maui;
using NetPodsMauiBlazor.Services;
using Podcast.Components;
using Podcast.Pages.Data;
using Podcast.Shared;
@ -25,8 +25,19 @@ public static class MauiProgram
{
client.BaseAddress = new Uri(APIUrl);
});
#if WINDOWS
builder.Services.AddSingleton<SharedMauiLib.INativeAudioService, SharedMauiLib.Platforms.Windows.NativeAudioService>();
#elif ANDROID
builder.Services.AddSingleton<SharedMauiLib.INativeAudioService, SharedMauiLib.Platforms.Android.NativeAudioService>();
#elif MACCATALYST
builder.Services.AddSingleton<SharedMauiLib.INativeAudioService, SharedMauiLib.Platforms.MacCatalyst.NativeAudioService>();
#elif IOS
builder.Services.AddSingleton<SharedMauiLib.INativeAudioService, SharedMauiLib.Platforms.iOS.NativeAudioService>();
#endif
builder.Services.AddScoped<ThemeInterop>();
builder.Services.AddScoped<AudioInterop>();
builder.Services.AddScoped<IAudioInterop, AudioInteropService>();
builder.Services.AddScoped<LocalStorageInterop>();
builder.Services.AddScoped<ClipboardInterop>();
builder.Services.AddScoped<SubscriptionsService>();

View File

@ -47,6 +47,7 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Lib\SharedMauiLib\SharedMauiLib.csproj" />
<ProjectReference Include="..\..\Web\Pages\Podcast.Pages.csproj" />
</ItemGroup>
</Project>

View File

@ -4,4 +4,5 @@
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true" android:networkSecurityConfig="@xml/network_security_config"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>

View File

@ -1,10 +1,37 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Microsoft.Maui;
using Android.OS;
using SharedMauiLib.Platforms.Android;
using SharedMauiLib.Platforms.Android.CurrentActivity;
namespace NetPodsMauiBlazor;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)]
public class MainActivity : MauiAppCompatActivity
public class MainActivity : MauiAppCompatActivity, IAudioActivity
{
MediaPlayerServiceConnection mediaPlayerServiceConnection;
public MediaPlayerServiceBinder Binder { get; set; }
public event StatusChangedEventHandler StatusChanged;
public event CoverReloadedEventHandler CoverReloaded;
public event PlayingEventHandler Playing;
public event BufferingEventHandler Buffering;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
CrossCurrentActivity.Current.Init(this, savedInstanceState);
NotificationHelper.CreateNotificationChannel(ApplicationContext);
if (mediaPlayerServiceConnection == null)
InitializeMedia();
}
private void InitializeMedia()
{
mediaPlayerServiceConnection = new MediaPlayerServiceConnection(this);
var mediaPlayerServiceIntent = new Intent(ApplicationContext, typeof(MediaPlayerService));
BindService(mediaPlayerServiceIntent, mediaPlayerServiceConnection, Bind.AutoCreate);
}
}

View File

@ -0,0 +1,76 @@
using SharedMauiLib;
using Microsoft.AspNetCore.Components;
using Podcast.Components;
using Podcast.Pages.Data;
using System.Timers;
namespace NetPodsMauiBlazor.Services;
internal class AudioInteropService : IAudioInterop
{
private readonly INativeAudioService _nativeAudioService;
private readonly PlayerService _playerService;
private readonly System.Timers.Timer currentTimeTimer;
public AudioInteropService(
INativeAudioService nativeAudioService,
PlayerService playerService)
{
_nativeAudioService = nativeAudioService;
_playerService = playerService;
currentTimeTimer = new System.Timers.Timer(TimeSpan.FromSeconds(1).TotalMilliseconds);
currentTimeTimer.Elapsed += OnCurrentTimeEvent;
}
public Task Pause(ElementReference element)
{
return _nativeAudioService.PauseAsync();
}
public async Task Play(ElementReference element)
{
await _nativeAudioService.PlayAsync(_nativeAudioService.CurrentPosition);
}
public async Task SetCurrentTime(ElementReference element, double value)
{
await _nativeAudioService.SetCurrentTime(value);
}
public Task SetMuted(ElementReference element, bool value)
{
return _nativeAudioService.SetMuted(value);
}
public void SetUri(string audioURI)
{
if (audioURI != null)
{
_nativeAudioService.InitializeAsync(audioURI).Wait();
currentTimeTimer.Start();
}
}
public Task SetVolume(ElementReference element, int value)
{
return _nativeAudioService.SetVolume(value);
}
public Task Stop(ElementReference element)
{
return _nativeAudioService.PauseAsync();
}
public ValueTask DisposeAsync()
{
currentTimeTimer.Dispose();
return _nativeAudioService.DisposeAsync();
}
private void OnCurrentTimeEvent(Object source, ElapsedEventArgs e)
{
_playerService.CurrentTime = _nativeAudioService.CurrentPosition;
}
}

View File

@ -10,7 +10,7 @@ builder.Services.AddHttpClient<PodcastService>(client =>
client.BaseAddress = new Uri(builder.Configuration["PodcastApi:BaseAddress"]!);
});
builder.Services.AddScoped<ThemeInterop>();
builder.Services.AddScoped<AudioInterop>();
builder.Services.AddScoped<IAudioInterop, AudioInterop>();
builder.Services.AddScoped<LocalStorageInterop>();
builder.Services.AddScoped<ClipboardInterop>();
builder.Services.AddScoped<SubscriptionsService>();

View File

@ -3,7 +3,7 @@ using Microsoft.JSInterop;
namespace Podcast.Components;
public class AudioInterop : IAsyncDisposable
public class AudioInterop : IAudioInterop ,IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
@ -57,4 +57,9 @@ public class AudioInterop : IAsyncDisposable
await module.DisposeAsync();
}
}
public void SetUri(string? audioURI)
{
//no action is necessary
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Components;
namespace Podcast.Components;
public interface IAudioInterop
{
void SetUri(string? audioURI);
Task Play(ElementReference element);
Task Pause(ElementReference element);
Task Stop(ElementReference element);
Task SetMuted(ElementReference element, bool value);
Task SetVolume(ElementReference element, int value);
Task SetCurrentTime(ElementReference element, double value);
ValueTask DisposeAsync();
}

View File

@ -1,6 +1,6 @@
@implements IDisposable
@inject PlayerService PlayerService
@inject AudioInterop AudioJsInterop
@inject IAudioInterop AudioJsInterop
<audio @ref="AudioElementRef"
src="@url"
@ -25,6 +25,7 @@
PlayerService.TimeSought += OnTimeSought;
url = PlayerService.Episode?.Url;
AudioJsInterop.SetUri(url);
}
public void Dispose()
@ -112,6 +113,7 @@
{
url = newValue;
loadingUrl = true;
AudioJsInterop.SetUri(url);
await InvokeAsync(StateHasChanged);
}
}

View File

@ -14,7 +14,7 @@ builder.Services.AddHttpClient<PodcastService>(client =>
client.BaseAddress = new Uri(builder.Configuration["PodcastApi:BaseAddress"]!);
});
builder.Services.AddScoped<ThemeInterop>();
builder.Services.AddScoped<AudioInterop>();
builder.Services.AddScoped<IAudioInterop, AudioInterop>();
builder.Services.AddScoped<LocalStorageInterop>();
builder.Services.AddScoped<ClipboardInterop>();
builder.Services.AddScoped<SubscriptionsService>();