diff --git a/CHANGELOG.md b/CHANGELOG.md index 9009432..bca3431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ -# 2.2.2 (prerelease) +# 3.0.0 (prerelease) +- BREAKING CHANGE : Created a new WPF control using ImageSource [#365](https://github.com/ZeBobo5/Vlc.DotNet/pull/365) see how to use in wiki! - FIXED the type of the `NewTitle` property (int -> string) in `VlcMediaPlayerTitleChangedEventArgs` [#364](https://github.com/ZeBobo5/Vlc.DotNet/pull/364) - FIXED Dispose() in WPF project [#358](https://github.com/ZeBobo5/Vlc.DotNet/pull/358) - CHANGED name of internal handle class from `SafeUnmanagedMemoryHandle` to `Utf8StringHandle` [#337](https://github.com/ZeBobo5/Vlc.DotNet/pull/337) diff --git a/README.md b/README.md index 6d02691..80c5a32 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,27 @@ It can work on any .net framework version starting from .net 2.0 and .net standa On the front-end side, two components are currently available to make it easy to integrate in your apps. One is for WinForms, the other for WPF. +Migrating WPF control from 2.x +---------- + +The WPF control has been rewritten from scratch from 2.x. + +The old WPF control was just a wrapper around the WinForms control. +This led to some issues (Airspace issue...) and lacked some WPF-ish features. + +That's why a new control has been written. To be fair, first versions of Vlc.DotNet +were built with that techique, but back then, there were issues in the .net framework +causing the memory usage to explode. As of 2018, this issue is resolved. + +You have in fact two options: +- Use the new WPF control. You might notice a performance impact when reading, for example, a 4k @ 60 fps video on a low-end computer. However, you can do whatever you like, just as a normal ImageSource in WPF. +- Wrap the Vlc.DotNet.WinForms control in a WinFormHost . It offers better performance, but you will experience Airspace issues (see [#296](https://github.com/ZeBobo5/Vlc.DotNet/issues/296)) if you need to write over the video. + +The right option to use depends on your needs. + +See the discussion [#249](https://github.com/ZeBobo5/Vlc.DotNet/issue/249) and pull request : [#365](https://github.com/ZeBobo5/Vlc.DotNet/pull/365) + + How to use ---------- It all starts with those three steps : diff --git a/appveyor.yml b/appveyor.yml index 0e111b7..e02efe5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ environment: - version_prefix: 2.2.2 + version_prefix: 3.0.0 version: $(version_prefix)-{branch}{build} image: Visual Studio 2017 diff --git a/src/Samples/Vlc.DotNet.Wpf.Samples/MainWindow.xaml.cs b/src/Samples/Vlc.DotNet.Wpf.Samples/MainWindow.xaml.cs index ff5c7c1..0499fbb 100644 --- a/src/Samples/Vlc.DotNet.Wpf.Samples/MainWindow.xaml.cs +++ b/src/Samples/Vlc.DotNet.Wpf.Samples/MainWindow.xaml.cs @@ -29,29 +29,33 @@ public MainWindow() vlcLibDirectory = new DirectoryInfo(Path.Combine(currentDirectory, @"..\..\..\lib\x64\")); } } - + + protected override void OnClosing(CancelEventArgs e) + { + this.control?.Dispose(); + base.OnClosing(e); + } + private void OnPlayButtonClick(object sender, RoutedEventArgs e) { this.control?.Dispose(); this.control = new VlcControl(); - this.control.MediaPlayer.VlcLibDirectory = this.vlcLibDirectory; - this.control.MediaPlayer.EndInit(); - this.ControlContainer.Content = this.control; + this.control.SourceProvider.CreatePlayer(this.vlcLibDirectory); // This can also be called before EndInit - this.control.MediaPlayer.Log += (_, args) => + this.control.SourceProvider.MediaPlayer.Log += (_, args) => { - string message = string.Format("libVlc : {0} {1} @ {2}", args.Level, args.Message, args.Module); + string message = $"libVlc : {args.Level} {args.Message} @ {args.Module}"; System.Diagnostics.Debug.WriteLine(message); }; - control.MediaPlayer.Play(new Uri("http://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_surround-fix.avi")); + + control.SourceProvider.MediaPlayer.Play(new Uri("http://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_480p_surround-fix.avi")); //control.MediaPlayer.Play(new FileInfo(@"..\..\..\Vlc.DotNet\Samples\Videos\BBB trailer.mov")); } private void OnStopButtonClick(object sender, RoutedEventArgs e) { - this.control?.MediaPlayer.Stop(); // This isn't strictly needed if Dispose() is called, but this is a demo... this.control?.Dispose(); this.control = null; } @@ -63,7 +67,7 @@ private void OnForwardButtonClick(object sender, RoutedEventArgs e) return; } - this.control.MediaPlayer.Rate = 2; + this.control.SourceProvider.MediaPlayer.Rate = 2; } private void GetLength_Click(object sender, RoutedEventArgs e) @@ -73,7 +77,7 @@ private void GetLength_Click(object sender, RoutedEventArgs e) return; } - GetLength.Content = this.control.MediaPlayer.Length + " ms"; + GetLength.Content = this.control.SourceProvider.MediaPlayer.Length + " ms"; } private void GetCurrentTime_Click(object sender, RoutedEventArgs e) @@ -83,7 +87,7 @@ private void GetCurrentTime_Click(object sender, RoutedEventArgs e) return; } - GetCurrentTime.Content = this.control.MediaPlayer.Time + " ms"; + GetCurrentTime.Content = this.control.SourceProvider.MediaPlayer.Time + " ms"; } private void SetCurrentTime_Click(object sender, RoutedEventArgs e) @@ -93,8 +97,8 @@ private void SetCurrentTime_Click(object sender, RoutedEventArgs e) return; } - this.control.MediaPlayer.Time = 5000; - SetCurrentTime.Content = this.control.MediaPlayer.Time + " ms"; + this.control.SourceProvider.MediaPlayer.Time = 5000; + SetCurrentTime.Content = this.control.SourceProvider.MediaPlayer.Time + " ms"; } } } diff --git a/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_format_cb.cs b/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_format_cb.cs index 3d9bc6a..bbbaba2 100644 --- a/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_format_cb.cs +++ b/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_format_cb.cs @@ -43,5 +43,5 @@ namespace Vlc.DotNet.Core.Interops.Signatures /// [LibVlcFunction("libvlc_video_format_cb")] [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate uint VideoFormatCallback(ref IntPtr userData, IntPtr chroma, ref uint width, ref uint height, ref uint pitches, ref uint lines); + public delegate uint VideoFormatCallback(out IntPtr userData, IntPtr chroma, ref uint width, ref uint height, ref uint pitches, ref uint lines); } diff --git a/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_lock_cb.cs b/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_lock_cb.cs index 0666937..642c8dd 100644 --- a/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_lock_cb.cs +++ b/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_lock_cb.cs @@ -10,8 +10,9 @@ namespace Vlc.DotNet.Core.Interops.Signatures /// Private pointer as passed to . /// /// - /// start address of the pixel planes (LibVLC allocates the array - /// of void pointers, this callback must initialize the array) + /// The pointer to the array of pointers to the pixel planes (LibVLC allocates the array + /// of pointers, this callback must initialize the array) + /// If you only need the first plane, then you have to initialize the address pointed by planes /// /// /// Whenever a new video frame needs to be decoded, the lock callback is @@ -21,5 +22,5 @@ namespace Vlc.DotNet.Core.Interops.Signatures /// [LibVlcFunction("libvlc_video_lock_cb")] [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate IntPtr LockVideoCallback(IntPtr userData, ref IntPtr planes); + public delegate IntPtr LockVideoCallback(IntPtr userData, IntPtr planes); } diff --git a/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_unlock_cb.cs b/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_unlock_cb.cs index 1ff9b1b..f987d0d 100644 --- a/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_unlock_cb.cs +++ b/src/Vlc.DotNet.Core.Interops/Signatures/libvlc_media_player.h/libvlc_video_unlock_cb.cs @@ -23,8 +23,10 @@ namespace Vlc.DotNet.Core.Interops.Signatures /// /// pixel planes as defined by the /// callback (this parameter is only for convenience) + /// + /// Its size is always PICTURE_PLANE_MAX, which is 5 in my experience. Only the number of planes required by chroma are usable, which is 1 for RV32. /// [LibVlcFunction("libvlc_video_lock_cb")] [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void UnlockVideoCallback(IntPtr userData, IntPtr picture, IntPtr planes); + public delegate void UnlockVideoCallback(IntPtr userData, IntPtr picture, [MarshalAs(UnmanagedType.LPArray, SizeConst = 5)]IntPtr[] planes); } diff --git a/src/Vlc.DotNet.Core.Interops/VlcManager.SetVideoCallbacks.cs b/src/Vlc.DotNet.Core.Interops/VlcManager.SetVideoCallbacks.cs new file mode 100644 index 0000000..628486c --- /dev/null +++ b/src/Vlc.DotNet.Core.Interops/VlcManager.SetVideoCallbacks.cs @@ -0,0 +1,24 @@ +using System; +using Vlc.DotNet.Core.Interops.Signatures; + +namespace Vlc.DotNet.Core.Interops +{ + public sealed partial class VlcManager + { + private LockVideoCallback _lockVideoCallbackReference; + private UnlockVideoCallback _unlockVideoCallbackReference; + private DisplayVideoCallback _displayVideoCallbackReference; + + public void SetVideoCallbacks(VlcMediaPlayerInstance mediaPlayerInstance, LockVideoCallback lockVideoCallback, UnlockVideoCallback unlockVideoCallback, DisplayVideoCallback displayVideoCallback, IntPtr userData) + { + if (mediaPlayerInstance == IntPtr.Zero) + throw new ArgumentException("Media player instance is not initialized."); + + this._lockVideoCallbackReference = lockVideoCallback; + this._unlockVideoCallbackReference = unlockVideoCallback; + this._displayVideoCallbackReference = displayVideoCallback; + + GetInteropDelegate().Invoke(mediaPlayerInstance, this._lockVideoCallbackReference, this._unlockVideoCallbackReference, this._displayVideoCallbackReference, userData); + } + } +} diff --git a/src/Vlc.DotNet.Core.Interops/VlcManager.SetVideoFormatCallbacks.cs b/src/Vlc.DotNet.Core.Interops/VlcManager.SetVideoFormatCallbacks.cs new file mode 100644 index 0000000..577ca7b --- /dev/null +++ b/src/Vlc.DotNet.Core.Interops/VlcManager.SetVideoFormatCallbacks.cs @@ -0,0 +1,22 @@ +using System; +using Vlc.DotNet.Core.Interops.Signatures; + +namespace Vlc.DotNet.Core.Interops +{ + public sealed partial class VlcManager + { + private VideoFormatCallback _videoFormatCallbackReference; + private CleanupVideoCallback _cleanupCallbackReference; + + public void SetVideoFormatCallbacks(VlcMediaPlayerInstance mediaPlayerInstance, VideoFormatCallback videoFormatCallback, CleanupVideoCallback cleanupCallback) + { + if (mediaPlayerInstance == IntPtr.Zero) + throw new ArgumentException("Media player instance is not initialized."); + + this._videoFormatCallbackReference = videoFormatCallback; + this._cleanupCallbackReference = cleanupCallback; + + GetInteropDelegate().Invoke(mediaPlayerInstance, this._videoFormatCallbackReference, this._cleanupCallbackReference); + } + } +} diff --git a/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.SetCallbacks.cs b/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.SetCallbacks.cs deleted file mode 100644 index 10b9daf..0000000 --- a/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.SetCallbacks.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Vlc.DotNet.Core.Interops.Signatures; - -namespace Vlc.DotNet.Core.Interops -{ - public sealed partial class VlcMediaPlayerInstance - { - public void SetVideoCallbacks(LockVideoCallback lockVideoCallback, UnlockVideoCallback unlockVideoCallback, DisplayVideoCallback displayVideoCallback, IntPtr userData) - { - myManager.GetInteropDelegate().Invoke(this.Pointer, lockVideoCallback, unlockVideoCallback, displayVideoCallback, userData); - } - } -} diff --git a/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.SetFormatCallbacks.cs b/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.SetFormatCallbacks.cs deleted file mode 100644 index 0e67e90..0000000 --- a/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.SetFormatCallbacks.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Vlc.DotNet.Core.Interops.Signatures; - -namespace Vlc.DotNet.Core.Interops -{ - public sealed partial class VlcMediaPlayerInstance - { - public void SetVideoFormatCallbacks(VideoFormatCallback videoFormatCallback, CleanupVideoCallback cleanupCallback) - { - myManager.GetInteropDelegate().Invoke(this.Pointer, videoFormatCallback, cleanupCallback); - } - } -} diff --git a/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.cs b/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.cs index 865ddcc..f3892f2 100644 --- a/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.cs +++ b/src/Vlc.DotNet.Core.Interops/VlcMediaPlayerInstance.cs @@ -3,7 +3,7 @@ namespace Vlc.DotNet.Core.Interops { - public sealed partial class VlcMediaPlayerInstance : InteropObjectInstance + public sealed class VlcMediaPlayerInstance : InteropObjectInstance { private readonly VlcManager myManager; diff --git a/src/Vlc.DotNet.Core/VlcMediaPlayer/VlcMediaPlayer.cs b/src/Vlc.DotNet.Core/VlcMediaPlayer/VlcMediaPlayer.cs index 3e73b1a..5bc0627 100644 --- a/src/Vlc.DotNet.Core/VlcMediaPlayer/VlcMediaPlayer.cs +++ b/src/Vlc.DotNet.Core/VlcMediaPlayer/VlcMediaPlayer.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; using System.Runtime.InteropServices; using Vlc.DotNet.Core.Interops; using Vlc.DotNet.Core.Interops.Signatures; @@ -143,6 +142,50 @@ public void Play() Manager.Play(myMediaPlayerInstance); } + /// + /// Overload, provided for convenience that calls before + /// + /// The file to play + /// The options to be given + public void Play(FileInfo file, params string[] options) + { + this.SetMedia(file, options); + this.Play(); + } + + /// + /// Overload, provided for convenience that calls before + /// + /// The uri to play + /// The options to be given + public void Play(Uri uri, params string[] options) + { + this.SetMedia(uri, options); + this.Play(); + } + + /// + /// Overload, provided for convenience that calls before + /// + /// The mrl to play + /// The options to be given + public void Play(string mrl, params string[] options) + { + this.SetMedia(mrl, options); + this.Play(); + } + + /// + /// Overload, provided for convenience that calls before + /// + /// The stream to play + /// The options to be given + public void Play(Stream stream, params string[] options) + { + this.SetMedia(stream, options); + this.Play(); + } + public void Pause() { Manager.Pause(myMediaPlayerInstance); diff --git a/src/Vlc.DotNet.Core/VlcMediaPlayer/VlcMediaPlayerVideo.cs b/src/Vlc.DotNet.Core/VlcMediaPlayer/VlcMediaPlayerVideo.cs new file mode 100644 index 0000000..edd1efe --- /dev/null +++ b/src/Vlc.DotNet.Core/VlcMediaPlayer/VlcMediaPlayerVideo.cs @@ -0,0 +1,56 @@ +using Vlc.DotNet.Core.Interops.Signatures; + +namespace Vlc.DotNet.Core +{ + using System; + + public sealed partial class VlcMediaPlayer + { + /// + /// Sets the video callbacks to render decoded video to a custom area in memory. + /// The media player will hold a reference on the IVideoCallbacks parameter + /// + /// + /// Rendering video into custom memory buffers is considerably less efficient than rendering in a custom window as normal. + /// See libvlc_video_set_callbacks for detailed explanations + /// + /// + /// Callback to lock video memory (must not be NULL) + /// + /// + /// Callback to unlock video memory (or NULL if not needed) + /// + /// + /// Callback to display video (or NULL if not needed) + /// + /// + /// Private pointer for the three callbacks (as first parameter). + /// This parameter will be overriden if is used + /// + public void SetVideoCallbacks(LockVideoCallback lockVideo, UnlockVideoCallback unlockVideo, DisplayVideoCallback display, IntPtr userData) + { + if (lockVideo == null) + { + throw new ArgumentNullException(nameof(lockVideo)); + } + + this.Manager.SetVideoCallbacks(this.myMediaPlayerInstance, lockVideo, unlockVideo, display, userData); + } + + /// + /// Set decoded video chroma and dimensions. This only works in combination with + /// + /// + /// Callback to select the video format (cannot be NULL) + /// Callback to release any allocated resources (or NULL) + public void SetVideoFormatCallbacks(VideoFormatCallback videoFormat, CleanupVideoCallback cleanup) + { + if (videoFormat == null) + { + throw new ArgumentNullException(nameof(videoFormat)); + } + + this.Manager.SetVideoFormatCallbacks(this.myMediaPlayerInstance, videoFormat, cleanup); + } + } +} \ No newline at end of file diff --git a/src/Vlc.DotNet.Forms/VlcControl.cs b/src/Vlc.DotNet.Forms/VlcControl.cs index 507a9e8..a4db14c 100644 --- a/src/Vlc.DotNet.Forms/VlcControl.cs +++ b/src/Vlc.DotNet.Forms/VlcControl.cs @@ -146,60 +146,32 @@ public DirectoryInfo OnVlcLibDirectoryNeeded() public void Play() { - //EndInit(); - if (myVlcMediaPlayer != null) - { - myVlcMediaPlayer.Play(); - } + myVlcMediaPlayer?.Play(); } public void Play(FileInfo file, params string[] options) { - //EndInit(); - if (myVlcMediaPlayer != null) - { - myVlcMediaPlayer.SetMedia(file, options); - Play(); - } + myVlcMediaPlayer?.Play(file, options); } public void Play(Uri uri, params string[] options) { - //EndInit(); - if (myVlcMediaPlayer != null) - { - myVlcMediaPlayer.SetMedia(uri, options); - Play(); - } + myVlcMediaPlayer?.Play(uri, options); } public void Play(string mrl, params string[] options) { - //EndInit(); - if (myVlcMediaPlayer != null) - { - myVlcMediaPlayer.SetMedia(mrl, options); - Play(); - } + myVlcMediaPlayer?.Play(mrl, options); } public void Play(Stream stream, params string[] options) { - //EndInit(); - if (myVlcMediaPlayer != null) - { - myVlcMediaPlayer.SetMedia(stream, options); - Play(); - } + myVlcMediaPlayer?.Play(stream, options); } public void Pause() { - //EndInit(); - if (myVlcMediaPlayer != null) - { - myVlcMediaPlayer.Pause(); - } + myVlcMediaPlayer?.Pause(); } public void Stop() diff --git a/src/Vlc.DotNet.Wpf/Vlc.DotNet.Wpf.csproj b/src/Vlc.DotNet.Wpf/Vlc.DotNet.Wpf.csproj index 300383c..6fd12be 100644 --- a/src/Vlc.DotNet.Wpf/Vlc.DotNet.Wpf.csproj +++ b/src/Vlc.DotNet.Wpf/Vlc.DotNet.Wpf.csproj @@ -9,20 +9,18 @@ - - Vlc.DotNet.pfx.snk - - - - + + + + + - diff --git a/src/Vlc.DotNet.Wpf/VlcControl.cs b/src/Vlc.DotNet.Wpf/VlcControl.cs index 467a5a2..fbe3bd0 100644 --- a/src/Vlc.DotNet.Wpf/VlcControl.cs +++ b/src/Vlc.DotNet.Wpf/VlcControl.cs @@ -1,26 +1,55 @@ -using System.Windows.Forms.Integration; +using System; +using System.Windows.Data; +using System.Windows.Media; namespace Vlc.DotNet.Wpf { - public class VlcControl : WindowsFormsHost + using System.Windows.Controls; + + /// + /// The Wpf component that allows to display a video in a Wpf way + /// + public class VlcControl: UserControl, IDisposable { - public Forms.VlcControl MediaPlayer { get; } + /// + /// The Viewbox that contains the video image + /// + private Viewbox viewBox; + + /// + /// The image that displays the video + /// + private readonly Image videoContent = new Image { ClipToBounds = true }; + /// + /// The class that provides video source + /// + private readonly VlcVideoSourceProvider sourceProvider; + + public VlcVideoSourceProvider SourceProvider => sourceProvider; + + /// + /// The constructor + /// public VlcControl() { - MediaPlayer = new Forms.VlcControl(); - this.Child = MediaPlayer; + sourceProvider = new VlcVideoSourceProvider(this.Dispatcher); + this.viewBox = new Viewbox + { + Child = this.videoContent, + Stretch = Stretch.Uniform + }; + + this.Content = this.viewBox; + this.Background = Brushes.Black; + // Binds the VideoSource to the Image.Source property + this.videoContent.SetBinding(Image.SourceProperty, new Binding(nameof(VlcVideoSourceProvider.VideoSource)) { Source = sourceProvider }); } - protected override void Dispose(bool disposing) + /// + public void Dispose() { - if (disposing) - { - this.Child = null; - this.MediaPlayer.Dispose(); - } - - base.Dispose(disposing); + sourceProvider.Dispose(); } } } \ No newline at end of file diff --git a/src/Vlc.DotNet.Wpf/VlcLibDirectoryNeededEventArgs.cs b/src/Vlc.DotNet.Wpf/VlcLibDirectoryNeededEventArgs.cs deleted file mode 100644 index e99987a..0000000 --- a/src/Vlc.DotNet.Wpf/VlcLibDirectoryNeededEventArgs.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.IO; - -namespace Vlc.DotNet.Wpf -{ - public sealed class VlcLibDirectoryNeededEventArgs : EventArgs - { - public DirectoryInfo VlcLibDirectory { get; set; } - - public VlcLibDirectoryNeededEventArgs() - { - - } - } -} \ No newline at end of file diff --git a/src/Vlc.DotNet.Wpf/VlcVideoSourceProvider.cs b/src/Vlc.DotNet.Wpf/VlcVideoSourceProvider.cs new file mode 100644 index 0000000..1d5bcd2 --- /dev/null +++ b/src/Vlc.DotNet.Wpf/VlcVideoSourceProvider.cs @@ -0,0 +1,267 @@ +namespace Vlc.DotNet.Wpf +{ + using System; + using System.IO; +#if NET45 + using System.IO.MemoryMappedFiles; +#endif + using System.Runtime.InteropServices; + using System.Windows; + using System.Windows.Media; + using Vlc.DotNet.Core; + using System.Windows.Interop; + using System.ComponentModel; + using System.Windows.Threading; + + /// + /// The class that can provide a Wpf Image Source to display the video. + /// + public class VlcVideoSourceProvider : INotifyPropertyChanged, IDisposable + { +#if NET45 + /// + /// The memory mapped file that contains the picture data + /// + private MemoryMappedFile memoryMappedFile; + + /// + /// The view that contains the pointer to the buffer that contains the picture data + /// + private MemoryMappedViewAccessor memoryMappedView; +#else + /// + /// The memory mapped file handle that contains the picture data + /// + private IntPtr memoryMappedFile; + + /// + /// The pointer to the buffer that contains the picture data + /// + private IntPtr memoryMappedView; +#endif + + private ImageSource videoSource; + + private Dispatcher dispatcher; + + public VlcVideoSourceProvider(Dispatcher dispatcher) + { + this.dispatcher = dispatcher; + } + + /// + /// The Image source that represents the video. + /// + public ImageSource VideoSource + { + get + { + return this.videoSource; + } + + private set + { + if (!Object.ReferenceEquals(this.videoSource, value)) + { + this.videoSource = value; + this.OnPropertyChanged(nameof(VideoSource)); + } + } + } + + /// + /// The media player instance. You must call before using this. + /// + public VlcMediaPlayer MediaPlayer { get; private set; } + + /// + /// Creates the player. This method must be called before using + /// + /// The directory where to find the vlc library + public void CreatePlayer(DirectoryInfo vlcLibDirectory) + { + var directoryInfo = vlcLibDirectory ?? throw new ArgumentNullException(nameof(vlcLibDirectory)); + + this.MediaPlayer = new VlcMediaPlayer(directoryInfo); + + this.MediaPlayer.SetVideoFormatCallbacks(this.VideoFormat, this.CleanupVideo); + this.MediaPlayer.SetVideoCallbacks(LockVideo, null, DisplayVideo, IntPtr.Zero); + } + + /// + /// Aligns dimension to the next multiple of mod + /// + /// The dimension to be aligned + /// The modulus + /// The aligned dimension + private uint GetAlignedDimension(uint dimension, uint mod) + { + var modResult = dimension % mod; + if (modResult == 0) + { + return dimension; + } + + return dimension + mod - (dimension % mod); + } + +#region Vlc video callbacks + /// + /// Called by vlc when the video format is needed. This method allocats the picture buffers for vlc and tells it to set the chroma to RV32 + /// + /// The user data that will be given to the callback. It contains the pointer to the buffer + /// The chroma + /// The visible width + /// The visible height + /// The buffer width + /// The buffer height + /// The number of buffers allocated + private uint VideoFormat(out IntPtr userdata, IntPtr chroma, ref uint width, ref uint height, ref uint pitches, ref uint lines) + { + var pixelFormat = PixelFormats.Bgr32; + Marshal.WriteByte(chroma, (byte)'R'); + Marshal.WriteByte(chroma, 1, (byte)'V'); + Marshal.WriteByte(chroma, 2, (byte)'3'); + Marshal.WriteByte(chroma, 3, (byte)'2'); + + pitches = this.GetAlignedDimension((uint)(width*pixelFormat.BitsPerPixel) / 8, 32); + lines = this.GetAlignedDimension(height, 32); + + var size = pitches * lines; +#if NET45 + this.memoryMappedFile = MemoryMappedFile.CreateNew(null, size); + var handle = this.memoryMappedFile.SafeMemoryMappedFileHandle.DangerousGetHandle(); +#else + this.memoryMappedFile = Win32Interop.CreateFileMapping(new IntPtr(-1), IntPtr.Zero, + Win32Interop.PageAccess.ReadWrite, 0, (int)size, null); + var handle = this.memoryMappedFile; +#endif + var args = new + { + width = width, + height = height, + pixelFormat = pixelFormat, + pitches = pitches + }; + + this.dispatcher.Invoke((Action)(() => + { + this.VideoSource = (InteropBitmap)Imaging.CreateBitmapSourceFromMemorySection(handle, + (int)args.width, (int)args.height, args.pixelFormat, (int)args.pitches, 0); + })); + +#if NET45 + this.memoryMappedView = this.memoryMappedFile.CreateViewAccessor(); + var viewHandle = this.memoryMappedView.SafeMemoryMappedViewHandle.DangerousGetHandle(); +#else + this.memoryMappedView = Win32Interop.MapViewOfFile(this.memoryMappedFile, Win32Interop.FileMapAccess.AllAccess, 0, 0, size); + var viewHandle = this.memoryMappedView; +#endif + userdata = viewHandle; + return 1; + } + + /// + /// Called by Vlc when it requires a cleanup + /// + /// The parameter is not used + private void CleanupVideo(ref IntPtr userdata) + { + // This callback may be called by Dispose in the Dispatcher thread, in which case it deadlocks if we call RemoveVideo again in the same thread. + if (!disposedValue) + { + this.dispatcher.Invoke((Action)this.RemoveVideo); + } + } + + /// + /// Called by libvlc when it wants to acquire a buffer where to write + /// + /// The pointer to the buffer (the out parameter of the callback) + /// The pointer to the planes array. Since only one plane has been allocated, the array has only one value to be allocated. + /// The pointer that is passed to the other callbacks as a picture identifier, this is not used + private IntPtr LockVideo(IntPtr userdata, IntPtr planes) + { + Marshal.WriteIntPtr(planes, userdata); + return userdata; + } + + /// + /// Called by libvlc when the picture has to be displayed. + /// + /// The pointer to the buffer (the out parameter of the callback) + /// The pointer returned by the callback. This is not used. + private void DisplayVideo(IntPtr userdata, IntPtr picture) + { + // Invalidates the bitmap + this.dispatcher.BeginInvoke((Action) (() => + { + (this.VideoSource as InteropBitmap)?.Invalidate(); + })); + } + #endregion + + /// + /// Removes the video (must be called from the Dispatcher thread) + /// + private void RemoveVideo() + { + this.VideoSource = null; + +#if NET45 + this.memoryMappedView?.Dispose(); + this.memoryMappedView = null; + this.memoryMappedFile?.Dispose(); + this.memoryMappedFile = null; +#else + if (this.memoryMappedFile != IntPtr.Zero) + { + Win32Interop.UnmapViewOfFile(this.memoryMappedView); + this.memoryMappedView = IntPtr.Zero; + Win32Interop.CloseHandle(this.memoryMappedFile); + this.memoryMappedFile = IntPtr.Zero; + } +#endif + } + + #region IDisposable Support + private bool disposedValue = false; + + /// + /// Disposes the control. + /// + /// The parameter is not used. + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + disposedValue = true; + this.MediaPlayer?.Dispose(); + this.MediaPlayer = null; + this.dispatcher.Invoke((Action)this.RemoveVideo); + } + } + + /// + /// The destructor + /// + ~VlcVideoSourceProvider() { + Dispose(false); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +#endregion + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/Vlc.DotNet.Wpf/Win32Interop.cs b/src/Vlc.DotNet.Wpf/Win32Interop.cs new file mode 100644 index 0000000..c22d216 --- /dev/null +++ b/src/Vlc.DotNet.Wpf/Win32Interop.cs @@ -0,0 +1,75 @@ +#if !NET45 +namespace Vlc.DotNet.Wpf +{ + using System; + using System.Runtime.InteropServices; + + public static class Win32Interop + { + /// + /// Creates or opens a named or unnamed file mapping object for a specified file. + /// + /// A handle to the file from which to create a file mapping object. + /// A pointer to a SECURITY_ATTRIBUTES structure that determines whether a returned handle can be inherited by child processes. The lpSecurityDescriptor member of the SECURITY_ATTRIBUTES structure specifies a security descriptor for a new file mapping object. + /// Specifies the page protection of the file mapping object. All mapped views of the object must be compatible with this protection. + /// The high-order DWORD of the maximum size of the file mapping object. + /// The low-order DWORD of the maximum size of the file mapping object. + /// The name of the file mapping object. + /// The value is a handle to the newly created file mapping object. + [DllImport("kernel32", SetLastError = true)] + public static extern IntPtr CreateFileMapping(IntPtr hFile, IntPtr lpAttributes, PageAccess flProtect, int dwMaximumSizeLow, int dwMaximumSizeHigh, string lpName); + + /// + /// Maps a view of a file mapping into the address space of a calling process. + /// + /// A handle to a file mapping object. The CreateFileMapping and OpenFileMapping functions return this handle. + /// The type of access to a file mapping object, which determines the protection of the pages. This parameter can be one of the following values. + /// A high-order DWORD of the file offset where the view begins. + /// A low-order DWORD of the file offset where the view is to begin. The combination of the high and low offsets must specify an offset within the file mapping. + /// The number of bytes of a file mapping to map to the view. All bytes must be within the maximum size specified by CreateFileMapping. If this parameter is 0 (zero), the mapping extends from the specified offset to the end of the file mapping. + /// The value is the starting address of the mapped view. + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject, FileMapAccess dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, uint dwNumberOfBytesToMap); + + /// + /// Unmaps a mapped view of a file from the calling process's address space. + /// + /// A pointer to the base address of the mapped view of a file that is to be unmapped. This value must be identical to the value returned by a previous call to the MapViewOfFile or MapViewOfFileEx function. + /// If the function succeeds, the return value is nonzero. + [DllImport("kernel32", SetLastError = true)] + public static extern bool UnmapViewOfFile(IntPtr lpBaseAddress); + + /// + /// Closes an open object handle. + /// + /// A valid handle to an open object. + /// If the function succeeds, the return value is nonzero. + [DllImport("kernel32", SetLastError = true)] + public static extern bool CloseHandle(IntPtr handle); + + public enum PageAccess + { + NoAccess = 0x01, + ReadOnly = 0x02, + ReadWrite = 0x04, + WriteCopy = 0x08, + Execute = 0x10, + ExecuteRead = 0x20, + ExecuteReadWrite = 0x40, + ExecuteWriteCopy = 0x80, + Guard = 0x100, + NoCache = 0x200, + WriteCombine = 0x400 + } + + public enum FileMapAccess : uint + { + Write = 0x00000002, + Read = 0x00000004, + AllAccess = 0x000f001f, + Copy = 0x00000001, + Execute = 0x00000020 + } + } +} +#endif \ No newline at end of file