diff --git a/MediaManager.Abstractions/IVideoPlayer.cs b/MediaManager.Abstractions/IVideoPlayer.cs index c5391533..c42bfe62 100644 --- a/MediaManager.Abstractions/IVideoPlayer.cs +++ b/MediaManager.Abstractions/IVideoPlayer.cs @@ -12,6 +12,11 @@ public interface IVideoPlayer : IPlaybackManager /// IVideoSurface RenderSurface { get; set; } + /// + /// True when RenderSurface has been initialized and ready for rendering + /// + bool IsReadyRendering { get; } + /// /// The aspect mode of the video /// diff --git a/MediaManager.Abstractions/IVideoSurface.cs b/MediaManager.Abstractions/IVideoSurface.cs index 3d5f322a..ffb2e92f 100644 --- a/MediaManager.Abstractions/IVideoSurface.cs +++ b/MediaManager.Abstractions/IVideoSurface.cs @@ -5,5 +5,6 @@ /// public interface IVideoSurface { + bool IsDisposed { get; } } } diff --git a/MediaManager.Abstractions/Implementations/MediaManagerBase.cs b/MediaManager.Abstractions/Implementations/MediaManagerBase.cs index 5b939d46..24772253 100644 --- a/MediaManager.Abstractions/Implementations/MediaManagerBase.cs +++ b/MediaManager.Abstractions/Implementations/MediaManagerBase.cs @@ -165,7 +165,7 @@ public async Task Play(IMediaFile mediaFile = null) if (!MediaQueue.Contains(mediaFile)) MediaQueue.Add(mediaFile); - MediaQueue.SetTrackAsCurrent(mediaFile); + MediaQueue.SetTrackAsCurrent(mediaFile); await RaiseMediaFileFailedEventOnException(async () => { @@ -385,9 +385,38 @@ private void RemoveEventHandlers() _currentPlaybackManager.StatusChanged -= OnStatusChanged; } - public virtual void Dispose() + #region IDisposable + // Flag: Has Dispose already been called? + bool disposed = false; + + // Public implementation of Dispose pattern callable by consumers. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + // Protected implementation of Dispose pattern. + protected virtual void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) + { + // Free any other managed objects here. + RemoveEventHandlers(); + } + + // Free any unmanaged objects here. + // + disposed = true; + } + + ~MediaManagerBase() { - RemoveEventHandlers(); + Dispose(false); } + #endregion } } diff --git a/MediaManager.Android/Audio/AudioPlayerBase.cs b/MediaManager.Android/Audio/AudioPlayerBase.cs index 8e926902..c25a7eff 100644 --- a/MediaManager.Android/Audio/AudioPlayerBase.cs +++ b/MediaManager.Android/Audio/AudioPlayerBase.cs @@ -10,7 +10,6 @@ using Plugin.MediaManager.Abstractions; using Plugin.MediaManager.Abstractions.Enums; using Plugin.MediaManager.Abstractions.EventArguments; -using Plugin.MediaManager.Abstractions.Implementations; using Plugin.MediaManager.MediaSession; namespace Plugin.MediaManager diff --git a/MediaManager.Android/Video/VideoPlayerImplementation.cs b/MediaManager.Android/Video/VideoPlayerImplementation.cs index d58b59ad..868a8474 100644 --- a/MediaManager.Android/Video/VideoPlayerImplementation.cs +++ b/MediaManager.Android/Video/VideoPlayerImplementation.cs @@ -1,9 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using Android.App; -using Android.Content.Res; using Android.Media; using Android.OS; using Android.Runtime; @@ -13,7 +10,6 @@ using Plugin.MediaManager.Abstractions; using Plugin.MediaManager.Abstractions.Enums; using Plugin.MediaManager.Abstractions.EventArguments; -using Plugin.MediaManager.Abstractions.Implementations; namespace Plugin.MediaManager { @@ -30,9 +26,9 @@ public VideoPlayerImplementation() StatusChanged += (sender, args) => OnPlayingHandler(args); } - private bool isPlayerReady = false; + private bool isPlayerReady = false; - private IScheduledExecutorService _executorService = Executors.NewSingleThreadScheduledExecutor(); + private IScheduledExecutorService _executorService = Executors.NewSingleThreadScheduledExecutor(); private IScheduledFuture _scheduledFuture; private void OnPlayingHandler(StatusChangedEventArgs args) @@ -45,7 +41,7 @@ private void OnPlayingHandler(StatusChangedEventArgs args) if (args.Status == MediaPlayerStatus.Stopped || args.Status == MediaPlayerStatus.Failed || args.Status == MediaPlayerStatus.Paused) CancelPlayingHandler(); } - + private void CancelPlayingHandler() { _scheduledFuture?.Cancel(false); @@ -56,14 +52,17 @@ private void StartPlayingHandler() var handler = new Handler(); var runnable = new Runnable(() => { handler.Post(OnPlaying); }); if (!_executorService.IsShutdown) - { - _scheduledFuture = _executorService.ScheduleAtFixedRate(runnable, 100, 1000, TimeUnit.Milliseconds); + { + _scheduledFuture = _executorService.ScheduleAtFixedRate(runnable, 100, 1000, TimeUnit.Milliseconds); } } private void OnPlaying() { - var progress = (Position.TotalSeconds / Duration.TotalSeconds); + if (!IsReadyRendering) + CancelPlayingHandler(); //RenderSurface is no longer valid => Cancel the periodic firing + + var progress = (Position.TotalSeconds / Duration.TotalSeconds); var position = Position; var duration = Duration; @@ -73,6 +72,13 @@ private void OnPlaying() duration.TotalSeconds >= 0 ? duration : TimeSpan.Zero)); } + /// + /// True when RenderSurface has been initialized and ready for rendering + /// + public bool IsReadyRendering => RenderSurface != null && !RenderSurface.IsDisposed; + + VideoView VideoViewCanvas => RenderSurface as VideoView; + private IVideoSurface _renderSurface; public IVideoSurface RenderSurface { get { @@ -82,8 +88,14 @@ public IVideoSurface RenderSurface { if (!(value is VideoSurface)) throw new ArgumentException("Not a valid video surface"); - var canvas = (VideoSurface)value; + if (_renderSurface == value) + return; + + var canvas = (VideoSurface)value; _renderSurface = canvas; + + //New canvas object => need initialization + isPlayerReady = false; } } @@ -97,9 +109,7 @@ public VideoAspectMode AspectMode { //TODO: Wrap videoplayer to respect aspectmode _aspectMode = value; } - } - - VideoView VideoViewCanvas => RenderSurface as VideoView; + } public IMediaFile CurrentFile { get; set; } private Android.Net.Uri currentUri { get; set; } @@ -150,13 +160,14 @@ protected virtual void OnMediaFileFailed(MediaFileFailedEventArgs e) { MediaFileFailed?.Invoke(this, e); } + + public TimeSpan Buffered => IsReadyRendering == false ? TimeSpan.Zero : TimeSpan.FromSeconds(VideoViewCanvas.BufferPercentage); - public TimeSpan Buffered => VideoViewCanvas == null ? TimeSpan.Zero : TimeSpan.FromSeconds(VideoViewCanvas.BufferPercentage); + public TimeSpan Duration => IsReadyRendering == false ? TimeSpan.Zero : TimeSpan.FromSeconds(VideoViewCanvas.Duration); + + public TimeSpan Position => IsReadyRendering == false ? TimeSpan.Zero : TimeSpan.FromSeconds(VideoViewCanvas.CurrentPosition); - public TimeSpan Duration => VideoViewCanvas == null ? TimeSpan.Zero : TimeSpan.FromSeconds(VideoViewCanvas.Duration); - - public TimeSpan Position => VideoViewCanvas == null ? TimeSpan.Zero : TimeSpan.FromSeconds(VideoViewCanvas.CurrentPosition); - private int lastPosition = 0; + private int lastPosition = 0; private MediaPlayerStatus _status = MediaPlayerStatus.Stopped; public MediaPlayerStatus Status @@ -187,17 +198,18 @@ public void Init() mediaController.SetAnchorView(VideoViewCanvas); VideoViewCanvas.SetMediaController(mediaController); } - + VideoViewCanvas.SetOnCompletionListener(this); VideoViewCanvas.SetOnErrorListener(this); VideoViewCanvas.SetOnPreparedListener(this); VideoViewCanvas.SetOnInfoListener(this); } - + public async Task Play(IMediaFile mediaFile = null) - { - if (VideoViewCanvas == null) - throw new System.Exception("No canvas set for video to play in"); + { + if (!IsReadyRendering) + //Android ViewRenderer might not initialize Control yet + return; if (isPlayerReady == false) { @@ -256,12 +268,16 @@ public async Task Stop() public void OnCompletion(MediaPlayer mp) { + Console.WriteLine($"OnCompletion"); + OnMediaFinished(new MediaFinishedEventArgs(CurrentFile)); } public bool OnError(MediaPlayer mp, MediaError what, int extra) { - Stop().Wait(); + Console.WriteLine($"OnError: {what}"); + + Stop().Wait(); Status = MediaPlayerStatus.Failed; OnMediaFailed(new MediaFailedEventArgs(what.ToString(), new System.Exception())); return true; @@ -269,7 +285,9 @@ public bool OnError(MediaPlayer mp, MediaError what, int extra) public void OnPrepared(MediaPlayer mp) { - if(Status == MediaPlayerStatus.Buffering) + Console.WriteLine($"OnPrepared: {Status}"); + + if (Status == MediaPlayerStatus.Buffering) VideoViewCanvas.Start(); Status = MediaPlayerStatus.Playing; @@ -277,7 +295,9 @@ public void OnPrepared(MediaPlayer mp) public bool OnInfo(MediaPlayer mp, [GeneratedEnum] MediaInfo what, int extra) { - switch (what) + Console.WriteLine($"OnInfo: {what}"); + + switch (what) { case MediaInfo.BadInterleaving: break; @@ -302,6 +322,6 @@ public bool OnInfo(MediaPlayer mp, [GeneratedEnum] MediaInfo what, int extra) } return true; - } + } } } diff --git a/MediaManager.Android/Video/VideoSurface.cs b/MediaManager.Android/Video/VideoSurface.cs index c4692f9a..6cf479be 100644 --- a/MediaManager.Android/Video/VideoSurface.cs +++ b/MediaManager.Android/Video/VideoSurface.cs @@ -34,5 +34,16 @@ protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) { base.OnMeasure(widthMeasureSpec, heightMeasureSpec); } + + #region IDisposable + public bool IsDisposed { get; private set; } + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + + base.Dispose(disposing); + } + #endregion } } diff --git a/MediaManager.Forms/MediaManager.Forms.Android/VideoViewRenderer.cs b/MediaManager.Forms/MediaManager.Forms.Android/VideoViewRenderer.cs index 3cac0c18..dc25e460 100644 --- a/MediaManager.Forms/MediaManager.Forms.Android/VideoViewRenderer.cs +++ b/MediaManager.Forms/MediaManager.Forms.Android/VideoViewRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using Plugin.MediaManager.Forms; using Plugin.MediaManager.Forms.Android; using Xamarin.Forms; @@ -11,7 +11,7 @@ public class VideoViewRenderer : ViewRenderer { private VideoSurface _videoSurface; - public static async void Init() + public static void Init() { var temp = DateTime.Now; } @@ -24,7 +24,7 @@ protected override void OnElementChanged(ElementChangedEventArgs e) _videoSurface = new VideoSurface(Context); SetNativeControl(_videoSurface); CrossMediaManager.Current.VideoPlayer.RenderSurface = _videoSurface; - } + } } protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) diff --git a/MediaManager.Forms/MediaManager.Forms/VideoView.cs b/MediaManager.Forms/MediaManager.Forms/VideoView.cs index 9e1c673a..a0a28a64 100644 --- a/MediaManager.Forms/MediaManager.Forms/VideoView.cs +++ b/MediaManager.Forms/MediaManager.Forms/VideoView.cs @@ -42,15 +42,16 @@ private static void OnAspectModeChanged(BindableObject bindable, object oldvalue { CrossMediaManager.Current.VideoPlayer.AspectMode = ((VideoAspectMode) newvalue); } - + private static void OnSourceChanged(BindableObject bindable, object oldvalue, object newvalue) - { + { var video = new MediaFile { Url = (string)newvalue, Type = MediaFileType.Video }; - + + //Auto play by adding video to the queue and then play CrossMediaManager.Current.Play(video); } } diff --git a/MediaManager.MacOS/VideoSurface.cs b/MediaManager.MacOS/VideoSurface.cs index e487d6d4..c5655ee9 100644 --- a/MediaManager.MacOS/VideoSurface.cs +++ b/MediaManager.MacOS/VideoSurface.cs @@ -5,5 +5,15 @@ namespace Plugin.MediaManager { public class VideoSurface : NSView, IVideoSurface { + #region IDisposable + public bool IsDisposed { get; private set; } + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + + base.Dispose(disposing); + } + #endregion } } diff --git a/MediaManager.UWP/MediaManagerImplementation.cs b/MediaManager.UWP/MediaManagerImplementation.cs index 21ec1650..d31f6351 100644 --- a/MediaManager.UWP/MediaManagerImplementation.cs +++ b/MediaManager.UWP/MediaManagerImplementation.cs @@ -39,10 +39,25 @@ public override IVideoPlayer VideoPlayer public override IVolumeManager VolumeManager { get; set; } = new VolumeManagerImplementation(); - public override void Dispose() + #region IDisposable + bool disposed = false; + protected override void Dispose(bool disposing) { - base.Dispose(); - _mediaButtonPlaybackController.UnsubscribeFromNotifications(); + if (disposed) + return; + + if (disposing) + { + // Free any other managed objects here. + _mediaButtonPlaybackController.UnsubscribeFromNotifications(); + } + + base.Dispose(disposing); + + // Free any unmanaged objects here. + // + disposed = true; } + #endregion } } \ No newline at end of file diff --git a/MediaManager.UWP/VideoPlayerImplementation.cs b/MediaManager.UWP/VideoPlayerImplementation.cs index 919b8ca7..799acb2c 100644 --- a/MediaManager.UWP/VideoPlayerImplementation.cs +++ b/MediaManager.UWP/VideoPlayerImplementation.cs @@ -203,6 +203,11 @@ public Task Stop() return Task.CompletedTask; } + /// + /// True when RenderSurface has been initialized and ready for rendering + /// + public bool IsReadyRendering => RenderSurface != null && !RenderSurface.IsDisposed; + public IVideoSurface RenderSurface { get { return _renderSurface; } diff --git a/MediaManager.UWP/VideoSurface.cs b/MediaManager.UWP/VideoSurface.cs index 39b8ba6a..d4cfc266 100644 --- a/MediaManager.UWP/VideoSurface.cs +++ b/MediaManager.UWP/VideoSurface.cs @@ -1,10 +1,44 @@ -using Windows.UI.Xaml.Controls; +using System; +using Windows.UI.Xaml.Controls; using Plugin.MediaManager.Abstractions; namespace Plugin.MediaManager { - public class VideoSurface : Canvas, IVideoSurface + public class VideoSurface : Canvas, IVideoSurface, IDisposable { - + #region IDisposable + public bool IsDisposed => disposed; + + // Flag: Has Dispose already been called? + bool disposed = false; + + // Public implementation of Dispose pattern callable by consumers. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + // Protected implementation of Dispose pattern. + protected virtual void Dispose(bool disposing) + { + if (disposed) + return; + + if (disposing) + { + // Free any other managed objects here. + } + + // Free any unmanaged objects here. + // + disposed = true; + } + + ~VideoSurface() + { + Dispose(false); + } + #endregion } } diff --git a/MediaManager.iOS/VideoPlayerImplementation.cs b/MediaManager.iOS/VideoPlayerImplementation.cs index 013ee2bf..e22b6ffc 100644 --- a/MediaManager.iOS/VideoPlayerImplementation.cs +++ b/MediaManager.iOS/VideoPlayerImplementation.cs @@ -10,7 +10,6 @@ using Plugin.MediaManager.Abstractions; using Plugin.MediaManager.Abstractions.Enums; using Plugin.MediaManager.Abstractions.EventArguments; -using Plugin.MediaManager.Abstractions.Implementations; namespace Plugin.MediaManager { @@ -319,6 +318,11 @@ private void ObserveLoadedTimeRanges() } } + /// + /// True when RenderSurface has been initialized and ready for rendering + /// + public bool IsReadyRendering => RenderSurface != null && !RenderSurface.IsDisposed; + private IVideoSurface _renderSurface; public IVideoSurface RenderSurface { diff --git a/MediaManager.iOS/VideoSurface.cs b/MediaManager.iOS/VideoSurface.cs index 6dfc792a..357e45d5 100644 --- a/MediaManager.iOS/VideoSurface.cs +++ b/MediaManager.iOS/VideoSurface.cs @@ -1,7 +1,4 @@ -using System; -using AVFoundation; -using CoreGraphics; -using Foundation; +using AVFoundation; using Plugin.MediaManager.Abstractions; using UIKit; @@ -22,5 +19,16 @@ public override void LayoutSubviews() avPlayerLayer.Frame = Bounds; } } + + #region IDisposable + public bool IsDisposed { get; private set; } + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + + base.Dispose(disposing); + } + #endregion } } diff --git a/MediaManager.tvOS/VideoSurface.cs b/MediaManager.tvOS/VideoSurface.cs index 347e86b7..46a3bf63 100644 --- a/MediaManager.tvOS/VideoSurface.cs +++ b/MediaManager.tvOS/VideoSurface.cs @@ -5,5 +5,15 @@ namespace Plugin.MediaManager { public class VideoSurface : UIView, IVideoSurface { + #region IDisposable + public bool IsDisposed { get; private set; } + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + + base.Dispose(disposing); + } + #endregion } } diff --git a/Samples/Forms/MediaForms/App.xaml.cs b/Samples/Forms/MediaForms/App.xaml.cs index 8a994093..f8a31ca0 100644 --- a/Samples/Forms/MediaForms/App.xaml.cs +++ b/Samples/Forms/MediaForms/App.xaml.cs @@ -10,7 +10,9 @@ public App() // Make sure it doesn't get stripped away by the linker var workaround = typeof(VideoView); InitializeComponent(); - MainPage = new MediaFormsPage(); + //MainPage = new MediaFormsPage(); + + MainPage = new NavigationPage(new HomePage()); } protected override void OnStart() diff --git a/Samples/Forms/MediaForms/HomePage.xaml b/Samples/Forms/MediaForms/HomePage.xaml new file mode 100644 index 00000000..62ccf979 --- /dev/null +++ b/Samples/Forms/MediaForms/HomePage.xaml @@ -0,0 +1,12 @@ + + + + +