diff --git a/.gitignore b/.gitignore index a57e89a..a84c072 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ SigningCertificate_Encoded.txt *.zip /QPlayerOld /api +/TestProj +/TestResults +/QPlayer.Tests/CueListTests2.cs +/QPlayer.Tests/CueListTests3.cs diff --git a/QPlayer.MagicQCTRLPlugin/MagicQCTRLPlugin.cs b/QPlayer.MagicQCTRLPlugin/MagicQCTRLPlugin.cs index 9012865..d3e79d7 100644 --- a/QPlayer.MagicQCTRLPlugin/MagicQCTRLPlugin.cs +++ b/QPlayer.MagicQCTRLPlugin/MagicQCTRLPlugin.cs @@ -61,7 +61,10 @@ private void MagicQCTRLTask() driver?.OnMessageReceived -= Driver_OnMessageReceived; MainViewModel.Log($"MagicQCTRL disconnected due to an error: {ex.Message}", MainViewModel.LogLevel.Warning); } - driver?.Dispose(); + finally + { + driver?.Dispose(); + } } } diff --git a/QPlayer.MagicQCTRLPlugin/QPlayer.MagicQCTRLPlugin.csproj b/QPlayer.MagicQCTRLPlugin/QPlayer.MagicQCTRLPlugin.csproj index 15442ab..3269823 100644 --- a/QPlayer.MagicQCTRLPlugin/QPlayer.MagicQCTRLPlugin.csproj +++ b/QPlayer.MagicQCTRLPlugin/QPlayer.MagicQCTRLPlugin.csproj @@ -7,7 +7,7 @@ true true - 0.1.1 + 0.1.2 Thomas Mathieson Thomas Mathieson ©️ Thomas Mathieson 2026 diff --git a/QPlayer.MagicQCTRLPlugin/USBDriver.cs b/QPlayer.MagicQCTRLPlugin/USBDriver.cs index 51329c5..db74c16 100644 --- a/QPlayer.MagicQCTRLPlugin/USBDriver.cs +++ b/QPlayer.MagicQCTRLPlugin/USBDriver.cs @@ -38,10 +38,12 @@ internal class USBDriver : IDisposable public void USBConnectAsync() { - DeviceList.Local.Changed += (o, e) => - { - USBConnect(); - }; + DeviceList.Local.Changed += OnDeviceListChangedHandler; + } + + private void OnDeviceListChangedHandler(object? sender, EventArgs e) + { + USBConnect(); } /// @@ -203,6 +205,11 @@ public void Dispose() usbRXTask?.Dispose(); } catch { } + try + { + DeviceList.Local.Changed -= OnDeviceListChangedHandler; + } + catch { } usbDevice = null; OnConnectionStatusChanged?.Invoke(false); isDisposing = false; diff --git a/QPlayer.OSCCuePlugin/OSCCueViewModel.cs b/QPlayer.OSCCuePlugin/OSCCueViewModel.cs index df45f6c..9ffddcf 100644 --- a/QPlayer.OSCCuePlugin/OSCCueViewModel.cs +++ b/QPlayer.OSCCuePlugin/OSCCueViewModel.cs @@ -41,8 +41,21 @@ private bool OSCMessageValid_Template } }*/ + [SkipView] + public override string NamePreview => string.IsNullOrEmpty(Name) ? $"OSC {command}" : Name; + public OSCCueViewModel(MainViewModel mainViewModel) : base(mainViewModel) - { } + { + PropertyChanged += (o, e) => + { + switch (e.PropertyName) + { + case nameof(Command): + OnPropertyChanged(nameof(NamePreview)); + break; + } + }; + } public override void Go() { diff --git a/QPlayer.OSCCuePlugin/QPlayer.OSCCuePlugin.csproj b/QPlayer.OSCCuePlugin/QPlayer.OSCCuePlugin.csproj index b3677d9..8085d95 100644 --- a/QPlayer.OSCCuePlugin/QPlayer.OSCCuePlugin.csproj +++ b/QPlayer.OSCCuePlugin/QPlayer.OSCCuePlugin.csproj @@ -7,7 +7,7 @@ true QPlayer OSC Cue Plugin - 0.1.0 + 0.1.1 Thomas Mathieson Thomas Mathieson ©️ Thomas Mathieson 2026 diff --git a/QPlayer.PyPlayPlugin/FramingCueView.xaml b/QPlayer.PyPlayPlugin/FramingCueView.xaml new file mode 100644 index 0000000..89735ef --- /dev/null +++ b/QPlayer.PyPlayPlugin/FramingCueView.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QPlayer.PyPlayPlugin/FramingCueView.xaml.cs b/QPlayer.PyPlayPlugin/FramingCueView.xaml.cs new file mode 100644 index 0000000..a6c4f78 --- /dev/null +++ b/QPlayer.PyPlayPlugin/FramingCueView.xaml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace QPlayer.PyPlayPlugin; + +/// +/// Interaction logic for FramingCueView.xaml +/// +public partial class FramingCueView : UserControl +{ + public FramingCueView() + { + InitializeComponent(); + } +} diff --git a/QPlayer.PyPlayPlugin/FramingCueViewModel.cs b/QPlayer.PyPlayPlugin/FramingCueViewModel.cs new file mode 100644 index 0000000..12e558b --- /dev/null +++ b/QPlayer.PyPlayPlugin/FramingCueViewModel.cs @@ -0,0 +1,31 @@ +using QPlayer.Audio; +using QPlayer.SourceGenerator; +using QPlayer.Utilities; +using QPlayer.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; + +namespace QPlayer.PyPlayPlugin; + +[Model(typeof(VideoFramingCue))] +//[GenerateView] +[View(typeof(FramingCueView))] +[DisplayName("Video Framing Cue")] +[Icon("IconPyFramingCue", typeof(Icons))] +public partial class FramingCueViewModel(MainViewModel mainViewModel) : CueViewModel(mainViewModel) +{ + [Reactive] private UndoableObservableCollection corners = [..Enumerable.Repeat(default, 4)]; + [Reactive] private UndoableObservableCollection framing = [.. Enumerable.Range(0, 4).Select(x => new FramingShutterViewModel())]; + [Reactive] private float fadeTime = 0; + [Reactive] private FadeType fadeType = FadeType.SCurve; +} + +public partial class FramingShutterViewModel : BindableViewModel +{ + [Reactive] private float rotation; + [Reactive] private float maskStart; + [Reactive] private float softness; +} diff --git a/QPlayer.PyPlayPlugin/FramingShutterControl.xaml b/QPlayer.PyPlayPlugin/FramingShutterControl.xaml new file mode 100644 index 0000000..cd0bac0 --- /dev/null +++ b/QPlayer.PyPlayPlugin/FramingShutterControl.xaml @@ -0,0 +1,47 @@ + + + + + + + F0 M50,-20 h-100 v20 A50,50 0 0 0 -35.3553,35.3553 A150,80 0 0 1 -15,75 v20 a5,5 0 0 0 5,5 h20 a5,5 0 0 0 5,-5 v-20 A150,80 0 0 1 35.3553,35.3553 A50,50 0 0 0 50,0 v-20z + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QPlayer.PyPlayPlugin/FramingShutterControl.xaml.cs b/QPlayer.PyPlayPlugin/FramingShutterControl.xaml.cs new file mode 100644 index 0000000..0d98b64 --- /dev/null +++ b/QPlayer.PyPlayPlugin/FramingShutterControl.xaml.cs @@ -0,0 +1,97 @@ +using QPlayer.Views; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace QPlayer.PyPlayPlugin; + +/// +/// Interaction logic for FramingShutterControl.xaml +/// +public partial class FramingShutterControl : UserControl +{ + bool isDragging = false; + Point lastPos; + + public FramingShutterControl() + { + InitializeComponent(); + } + + private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e) + { + isDragging = true; + LibraryImports.ShowCursor(false); + lastPos = e.GetPosition(ContainerGrid); + (sender as FrameworkElement)?.CaptureMouse(); + //Debug.WriteLine($"down left={e.LeftButton} right={e.RightButton} drag={isDragging}"); + } + + private void Rectangle_MouseUp(object sender, MouseButtonEventArgs e) + { + //Debug.WriteLine($"up left={e.LeftButton} right={e.RightButton} drag={isDragging}"); + } + + private void Rectangle_MouseMove(object sender, MouseEventArgs e) + { + //Debug.WriteLine($"move left={e.LeftButton} right={e.RightButton} drag={isDragging}"); + if (!isDragging || DataContext is not FramingShutterViewModel shutter) + return; + + if (e.LeftButton == MouseButtonState.Released && e.RightButton == MouseButtonState.Released) + { + (sender as FrameworkElement)?.ReleaseMouseCapture(); + isDragging = false; + ContainerGrid.Opacity = 0.7; + // Sometimes the MouseUp call gets eaten, make sure to unhide the mouse here... + int count; + do + { + count = LibraryImports.ShowCursor(true); + } while (count < 0); + while (count > 0) + { + count = LibraryImports.ShowCursor(false); + } + return; + } + + var pos = e.GetPosition(ContainerGrid); + var off = pos - lastPos; + lastPos = pos; + + var x = off.X / ContainerGrid.ActualWidth; + var y = off.Y / ContainerGrid.ActualHeight; + + shutter.Rotation = Math.Clamp(shutter.Rotation - (float)(x * 150), -120, 120); + if (e.RightButton == MouseButtonState.Pressed) + shutter.Softness = Math.Clamp(shutter.Softness + (float)(y*2), 0, 1); + else + shutter.MaskStart = Math.Clamp(shutter.MaskStart - (float)(y*2), 0, 1); + } + + private void Rectangle_MouseEnter(object sender, MouseEventArgs e) + { + ContainerGrid.Opacity = 1.0; + } + + private void Rectangle_MouseLeave(object sender, MouseEventArgs e) + { + // Debug.WriteLine($"leave left={e.LeftButton} right={e.RightButton} drag={isDragging}"); + if (isDragging) + return; + + ContainerGrid.Opacity = 0.7; + //LibraryImports.ShowCursor(true); + } +} diff --git a/QPlayer.PyPlayPlugin/Icons.xaml b/QPlayer.PyPlayPlugin/Icons.xaml new file mode 100644 index 0000000..837c3f7 --- /dev/null +++ b/QPlayer.PyPlayPlugin/Icons.xaml @@ -0,0 +1,24 @@ + + + F1 M512,512z M0,0z M0,96C0,60.7,28.7,32,64,32L448,32C483.3,32,512,60.7,512,96L512,416C512,451.3,483.3,480,448,480L64,480C28.7,480,0,451.3,0,416L0,96z M48,368L48,400C48,408.8,55.2,416,64,416L96,416C104.8,416,112,408.8,112,400L112,368C112,359.2,104.8,352,96,352L64,352C55.2,352,48,359.2,48,368z M416,352C407.2,352,400,359.2,400,368L400,400C400,408.8,407.2,416,416,416L448,416C456.8,416,464,408.8,464,400L464,368C464,359.2,456.8,352,448,352L416,352z M48,240L48,272C48,280.8,55.2,288,64,288L96,288C104.8,288,112,280.8,112,272L112,240C112,231.2,104.8,224,96,224L64,224C55.2,224,48,231.2,48,240z M416,224C407.2,224,400,231.2,400,240L400,272C400,280.8,407.2,288,416,288L448,288C456.8,288,464,280.8,464,272L464,240C464,231.2,456.8,224,448,224L416,224z M48,112L48,144C48,152.8,55.2,160,64,160L96,160C104.8,160,112,152.8,112,144L112,112C112,103.2,104.8,96,96,96L64,96C55.2,96,48,103.2,48,112z M416,96C407.2,96,400,103.2,400,112L400,144C400,152.8,407.2,160,416,160L448,160C456.8,160,464,152.8,464,144L464,112C464,103.2,456.8,96,448,96L416,96z M160,128L160,192C160,209.7,174.3,224,192,224L320,224C337.7,224,352,209.7,352,192L352,128C352,110.3,337.7,96,320,96L192,96C174.3,96,160,110.3,160,128z M192,288C174.3,288,160,302.3,160,320L160,384C160,401.7,174.3,416,192,416L320,416C337.7,416,352,401.7,352,384L352,320C352,302.3,337.7,288,320,288L192,288z + + + + + F1 M512,512z M0,0z M0,416C0,433.7,14.3,448,32,448L86.7,448C99,476.3 127.2,496 160,496 192.8,496 221,476.3 233.3,448L480,448C497.7,448 512,433.7 512,416 512,398.3 497.7,384 480,384L233.3,384C221,355.7 192.8,336 160,336 127.2,336 99,355.7 86.7,384L32,384C14.3,384,0,398.3,0,416z M128,416A32,32,0,1,1,192,416A32,32,0,1,1,128,416z M320,256A32,32,0,1,1,384,256A32,32,0,1,1,320,256z M352,176C319.2,176,291,195.7,278.7,224L32,224C14.3,224 0,238.3 0,256 0,273.7 14.3,288 32,288L278.7,288C291,316.3 319.2,336 352,336 384.8,336 413,316.3 425.3,288L480,288C497.7,288 512,273.7 512,256 512,238.3 497.7,224 480,224L425.3,224C413,195.7,384.8,176,352,176z M192,128A32,32,0,1,1,192,64A32,32,0,1,1,192,128z M265.3,64C253,35.7 224.8,16 192,16 159.2,16 131,35.7 118.7,64L32,64C14.3,64 0,78.3 0,96 0,113.7 14.3,128 32,128L118.7,128C131,156.3 159.2,176 192,176 224.8,176 253,156.3 265.3,128L480,128C497.7,128 512,113.7 512,96 512,78.3 497.7,64 480,64L265.3,64z + + + + + F1 M512,512z M0,0z M128,32C128,14.3 113.7,0 96,0 78.3,0 64,14.3 64,32L64,64 32,64C14.3,64 0,78.3 0,96 0,113.7 14.3,128 32,128L64,128 64,384C64,419.3,92.7,448,128,448L352,448 352,384 128,384 128,32z M384,480C384,497.7 398.3,512 416,512 433.7,512 448,497.7 448,480L448,448 480,448C497.7,448 512,433.7 512,416 512,398.3 497.7,384 480,384L448,384 448,128C448,92.7,419.3,64,384,64L160,64 160,128 384,128 384,480z + + + + + + + + + \ No newline at end of file diff --git a/QPlayer.PyPlayPlugin/Icons.xaml.cs b/QPlayer.PyPlayPlugin/Icons.xaml.cs new file mode 100644 index 0000000..079b0f9 --- /dev/null +++ b/QPlayer.PyPlayPlugin/Icons.xaml.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; + +namespace QPlayer.PyPlayPlugin; + +public partial class Icons : ResourceDictionary +{ + public Icons() + { + InitializeComponent(); + } +} diff --git a/QPlayer.PyPlayPlugin/PyPlayPlugin.cs b/QPlayer.PyPlayPlugin/PyPlayPlugin.cs new file mode 100644 index 0000000..0e1c9e1 --- /dev/null +++ b/QPlayer.PyPlayPlugin/PyPlayPlugin.cs @@ -0,0 +1,19 @@ +using QPlayer.Models; +using QPlayer.ViewModels; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +namespace QPlayer.PyPlayPlugin; + +[PluginName("PyPlay Plugin")] +[PluginAuthor("Thomas Mathieson")] +[PluginDescription("This plugin adds support for PyPlay video and shader cues. PyPlay is a flexible cross-platform video playback engine. https://github.com/dmathies/pyPlay")] +public class PyPlayPlugin : QPlayerPlugin +{ + public override void OnLoad(MainViewModel mainViewModel) + { + MainViewModel.Log("Loaded PyPlay plugin!"); + } +} diff --git a/QPlayer.PyPlayPlugin/PyVideoCueModel.cs b/QPlayer.PyPlayPlugin/PyVideoCueModel.cs new file mode 100644 index 0000000..5412efa --- /dev/null +++ b/QPlayer.PyPlayPlugin/PyVideoCueModel.cs @@ -0,0 +1,113 @@ +using QPlayer.Audio; +using QPlayer.Models; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Collections.ObjectModel; +using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace QPlayer.PyPlayPlugin; + +/// +/// Model for the Video Cue +/// +public record PyVideoCue : Cue +{ + public string path = string.Empty; + public string shader = string.Empty; + public int zIndex; + public string? alphaPath; + public AlphaMode alphaMode = AlphaMode.Video; + public TimeSpan startTime; + public TimeSpan duration; + public bool stompsOthers; + public float dimmer = 1; + public float volume = 1; + public float fadeIn = 1; + public float fadeOut = 1; + public FadeType fadeType = FadeType.SCurve; + public float brightness = 1; + public float contrast = 1; + public float gamma = 1; + public float scale = 1; + public float rotation = 0; + public Vector2 offset = Vector2.Zero; + public List shaderParameters = []; + // DMX address as a single integer (1..508). 0 means unset. + public int dmxAddress = 0; + + public PyVideoCue() : base() { } +} + +/// +/// Model for the Video Framing Cue +/// +[Serializable] +public record VideoFramingCue : Cue +{ + public List corners = [.. Enumerable.Repeat(default, 4)]; + public List framing = [.. Enumerable.Range(0, 4).Select(x => new FramingShutter())]; + public float fadeTime = 0; + public FadeType fadeType = FadeType.SCurve; + + public VideoFramingCue() : base() { } +} + +/// +/// Model for the Shader Parameters Cue +/// +[Serializable] +public record ShaderParamsCue : Cue +{ + /// + /// Can be numeric or the string "post" for post-processing target. + /// + public string targetQid = string.Empty; + public List shaderParameters = []; + public float fadeTime = 0; + public FadeType fadeType = FadeType.SCurve; + public bool postProcessing = false; + + public ShaderParamsCue() : base() { } +} + +/// +/// Enum for alpha blending modes +/// +public enum AlphaMode +{ + Opaque, + Video, + Alpha, + GradientWipe +} + +/// +/// Struct for framing shutter configuration +/// +public record FramingShutter +{ + public float rotation; + public float maskStart; + public float softness; +} + +/// +/// Struct for shader parameters +/// +[Serializable] +public record class ShaderParameter +{ + public string name = string.Empty; + public float value; + + public ShaderParameter() { } + + public ShaderParameter(string name, float value) + { + this.name = name; + this.value = value; + } +} diff --git a/QPlayer.PyPlayPlugin/PyVideoCueView.xaml b/QPlayer.PyPlayPlugin/PyVideoCueView.xaml new file mode 100644 index 0000000..b8fc28c --- /dev/null +++ b/QPlayer.PyPlayPlugin/PyVideoCueView.xaml @@ -0,0 +1,124 @@ + + + + + + + + + + + +