diff --git a/HighPrecisionTimer/HighPrecisionTimer.csproj b/HighPrecisionTimer/HighPrecisionTimer.csproj
index 2fc9fb0..338e500 100644
--- a/HighPrecisionTimer/HighPrecisionTimer.csproj
+++ b/HighPrecisionTimer/HighPrecisionTimer.csproj
@@ -9,7 +9,7 @@
Properties
HighPrecisionTimer
HighPrecisionTimer
- v4.6.2
+ v4.7.2
512
@@ -58,4 +58,4 @@
-->
-
\ No newline at end of file
+
diff --git a/Popcron.Sheets/Popcron.Sheets.csproj b/Popcron.Sheets/Popcron.Sheets.csproj
index 469efc7..0ce1cea 100644
--- a/Popcron.Sheets/Popcron.Sheets.csproj
+++ b/Popcron.Sheets/Popcron.Sheets.csproj
@@ -9,7 +9,7 @@
Properties
Popcron.Sheets
Popcron.Sheets
- v4.6.2
+ v4.7.2
512
true
@@ -216,4 +216,4 @@
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index d800333..7a1e841 100644
--- a/README.md
+++ b/README.md
@@ -2,3 +2,15 @@
vMix Universal Title Controller (UTC) is program, which work with vMix through official vMix API.
It can replace some paid or expensive software for making titles/scoreboards/lower thirds (by using pure vMix XAML/GT titles)
and add ability of base scripting.
+
+---
+
+## Plugins disponíveis
+
+### SRT Monitor Data Provider
+
+Monitore streams SRT diretamente dentro do vMixUTC, em modo Listener ou Caller, com preview de vídeo embutido.
+
+**[⬇ Baixar SrtMonitorDataProvider.zip](https://github.com/rafalau/vMixUTC/releases/download/v1.1.0-srt/SrtMonitorDataProvider.zip)** (~86 MB — já inclui VLC) — *v1.1: Enable Auto Sync*
+
+Instruções de instalação: [vMixUTCSRTDataProvider/README.md](vMixUTCSRTDataProvider/README.md)
diff --git a/vMixAPI/State.cs b/vMixAPI/State.cs
index 2e0e95f..eab0500 100644
--- a/vMixAPI/State.cs
+++ b/vMixAPI/State.cs
@@ -363,7 +363,7 @@ public bool Update()
}
}
- public void UpdateAsync()
+ public void UpdateAsync(bool ignoreCache = false)
{
SendFunction("", true, x =>
{
@@ -453,7 +453,7 @@ public void UpdateAsync()
else
Dispatcher.BeginInvoke(updateAction);
- });
+ }, ignoreCache: ignoreCache);
}
// Hold callback state strongly until async response is received and callback is dispatched.
diff --git a/vMixController/Classes/MainWindowSettings.cs b/vMixController/Classes/MainWindowSettings.cs
index 0a891df..b8cd880 100644
--- a/vMixController/Classes/MainWindowSettings.cs
+++ b/vMixController/Classes/MainWindowSettings.cs
@@ -772,6 +772,19 @@ public void UpdatePages()
RaisePropertyChanged(PagesPropertyName);
}
+ private bool _autoSync = false;
+
+ public bool AutoSync
+ {
+ get { return _autoSync; }
+ set
+ {
+ if (_autoSync == value) return;
+ _autoSync = value;
+ RaisePropertyChanged(nameof(AutoSync));
+ }
+ }
+
internal void AddRecentFile(string fileName)
{
if (RecentFiles == null)
diff --git a/vMixController/Classes/VmixTcpSubscriber.cs b/vMixController/Classes/VmixTcpSubscriber.cs
new file mode 100644
index 0000000..255c348
--- /dev/null
+++ b/vMixController/Classes/VmixTcpSubscriber.cs
@@ -0,0 +1,91 @@
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace vMixController.Classes
+{
+ internal sealed class VmixTcpSubscriber : IDisposable
+ {
+ public event EventHandler ActsReceived;
+
+ private CancellationTokenSource _cts;
+ private Task _loopTask;
+ private Timer _debounce;
+ private const int TcpPort = 8099;
+ private const int DebounceMs = 300;
+
+ public void Start(string host)
+ {
+ Stop();
+ _cts = new CancellationTokenSource();
+ var token = _cts.Token;
+ _loopTask = Task.Run(() => LoopAsync(host, token));
+ }
+
+ public void Stop()
+ {
+ _cts?.Cancel();
+ _loopTask = null;
+ _cts?.Dispose();
+ _cts = null;
+ }
+
+ private async Task LoopAsync(string host, CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ using (var client = new TcpClient())
+ {
+ client.ReceiveTimeout = 2000;
+ await client.ConnectAsync(host, TcpPort).ConfigureAwait(false);
+
+ using (var stream = client.GetStream())
+ using (var writer = new StreamWriter(stream) { AutoFlush = true, NewLine = "\r\n" })
+ using (var reader = new StreamReader(stream))
+ {
+ await writer.WriteLineAsync("SUBSCRIBE ACTS").ConfigureAwait(false);
+
+ while (!ct.IsCancellationRequested)
+ {
+ string line;
+ try
+ {
+ line = await reader.ReadLineAsync().ConfigureAwait(false);
+ }
+ catch (IOException) { break; }
+
+ if (line == null) break;
+
+ // "SUBSCRIBE OK ACTS" is the subscription confirmation — skip it
+ // actual events arrive as "ACTS OK ..."
+ if (line.StartsWith("ACTS ", StringComparison.Ordinal))
+ FireDebounced();
+ }
+ }
+ }
+ }
+ catch (OperationCanceledException) { break; }
+ catch { /* connection failed — retry after delay */ }
+
+ try { await Task.Delay(3000, ct).ConfigureAwait(false); }
+ catch (OperationCanceledException) { break; }
+ }
+ }
+
+ private void FireDebounced()
+ {
+ _debounce?.Dispose();
+ _debounce = new Timer(_ => ActsReceived?.Invoke(this, EventArgs.Empty), null, DebounceMs, Timeout.Infinite);
+ }
+
+ public void Dispose()
+ {
+ Stop();
+ _debounce?.Dispose();
+ }
+ }
+}
diff --git a/vMixController/Controls/vMixControlContainer.xaml b/vMixController/Controls/vMixControlContainer.xaml
index 858f12f..0a8bf4d 100644
--- a/vMixController/Controls/vMixControlContainer.xaml
+++ b/vMixController/Controls/vMixControlContainer.xaml
@@ -249,6 +249,7 @@
+
diff --git a/vMixController/MainWindow.xaml b/vMixController/MainWindow.xaml
index c791e91..4eda31c 100644
--- a/vMixController/MainWindow.xaml
+++ b/vMixController/MainWindow.xaml
@@ -498,6 +498,14 @@
+
diff --git a/vMixController/Skins/ControlTemplates.xaml b/vMixController/Skins/ControlTemplates.xaml
index e184734..6d87991 100644
--- a/vMixController/Skins/ControlTemplates.xaml
+++ b/vMixController/Skins/ControlTemplates.xaml
@@ -915,8 +915,8 @@
-
-
+
+
diff --git a/vMixController/ViewModel/MainViewModel.cs b/vMixController/ViewModel/MainViewModel.cs
index 8e44886..9ab9973 100644
--- a/vMixController/ViewModel/MainViewModel.cs
+++ b/vMixController/ViewModel/MainViewModel.cs
@@ -389,6 +389,7 @@ public MainWindowSettings WindowSettings
_windowSettings.PropertyChanged += WindowSettings_PropertyChanged;
RaisePropertyChanged(nameof(WindowSettings));
+ UpdateAutoSync();
}
}
@@ -400,6 +401,24 @@ private void WindowSettings_PropertyChanged(object sender, System.ComponentModel
CheckvMixConnection(null, new EventArgs());
});
+ if (e.PropertyName == nameof(Classes.MainWindowSettings.AutoSync))
+ UpdateAutoSync();
+ }
+
+ private void UpdateAutoSync()
+ {
+ if (WindowSettings?.AutoSync == true)
+ _autoSyncTimer.Start();
+ else
+ _autoSyncTimer.Stop();
+ }
+
+ private void AutoSyncTick(object sender, EventArgs e)
+ {
+ if (Model != null)
+ Model.UpdateAsync(ignoreCache: true);
+ else
+ SyncTovMixState();
}
private void LocalizationManager_CultureChanged(object sender, EventArgs e)
@@ -2374,6 +2393,10 @@ private void Model_OnStateUpdated(object sender, vMixAPI.StateSyncedEventArgs e)
bool ProcessHotkey(Key key, Key systemKey, ModifierKeys modifiers, bool onPress = true)
{
+ if (Keyboard.FocusedElement is System.Windows.Controls.Primitives.TextBoxBase ||
+ Keyboard.FocusedElement is System.Windows.Controls.PasswordBox)
+ return false;
+
FocusManager.SetFocusedElement(App.Current.MainWindow, (IInputElement)App.Current.MainWindow);
var result = false;
@@ -2660,6 +2683,8 @@ public RelayCommand ResetScalingCommand
DispatcherTimer _connectTimer = new DispatcherTimer();
DispatcherTimer _metricsTimer = new DispatcherTimer();
+ DispatcherTimer _autoSyncTimer = new DispatcherTimer();
+ Classes.VmixTcpSubscriber _tcpSubscriber = new Classes.VmixTcpSubscriber();
string _documentsPath;
@@ -2744,6 +2769,9 @@ public MainViewModel()
_connectTimer.Tick += CheckvMixConnection;
_connectTimer.Start();
+ _autoSyncTimer.Interval = TimeSpan.FromSeconds(1);
+ _autoSyncTimer.Tick += AutoSyncTick;
+
_metricsTimer.Interval = TimeSpan.FromSeconds(30);
_metricsTimer.Tick += (sender, args) =>
{
@@ -2754,7 +2782,7 @@ public MainViewModel()
_metricsTimer.Start();
//For loading NCalc before adding buttons to avoid throttle at button click
- var _expression = new NCalc.Expression("1+1");
+ var _expression = new NCalc.SafeExpression("1+1");
_expression.TryEvaluate(out _, out _);
_logger.Info("Loading mapped functions.");
@@ -3220,6 +3248,9 @@ protected virtual void Dispose(bool managed)
_connectTimer.Stop();
_connectTimer.Tick -= CheckvMixConnection;
_metricsTimer.Stop();
+ _autoSyncTimer.Stop();
+ _autoSyncTimer.Tick -= AutoSyncTick;
+ _tcpSubscriber.Dispose();
LocalizationManager.Instance.CultureChanged -= LocalizationManager_CultureChanged;
vMixAPI.StateFabrique.OnStateCreated -= State_OnStateCreated;
}
diff --git a/vMixController/Widgets/vMixControlExternalData.cs b/vMixController/Widgets/vMixControlExternalData.cs
index 3a7d6e9..b9a1878 100644
--- a/vMixController/Widgets/vMixControlExternalData.cs
+++ b/vMixController/Widgets/vMixControlExternalData.cs
@@ -19,6 +19,8 @@ namespace vMixController.Widgets
[Serializable]
public class vMixControlExternalData : vMixControlTextField, IvMixAutoUpdateWidget
{
+ public override bool IsResizeableVertical => true;
+
[NonSerialized]
DispatcherTimer _timer = new DispatcherTimer();
diff --git a/vMixController/vMixController.csproj b/vMixController/vMixController.csproj
index c450ffb..e98574a 100644
--- a/vMixController/vMixController.csproj
+++ b/vMixController/vMixController.csproj
@@ -63,6 +63,7 @@
prompt
4
false
+ false
settings-gears %281%29.ico
@@ -423,6 +424,7 @@
+
diff --git a/vMixControllerMultiState.sln b/vMixControllerMultiState.sln
index f9d6c19..3046d6c 100644
--- a/vMixControllerMultiState.sln
+++ b/vMixControllerMultiState.sln
@@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "vMixStreamDeckLibrary", "vM
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystemDataProvider", "FileSystemDataProvider\FileSystemDataProvider.csproj", "{65524B0F-6D11-4BA7-9D79-1A9884E60126}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SrtMonitorDataProvider", "vMixUTCSRTDataProvider\vMixUTCSRTDataProvider.csproj", "{F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -264,6 +266,18 @@ Global
{65524B0F-6D11-4BA7-9D79-1A9884E60126}.Release|x64.Build.0 = Release|Any CPU
{65524B0F-6D11-4BA7-9D79-1A9884E60126}.Release|x86.ActiveCfg = Release|Any CPU
{65524B0F-6D11-4BA7-9D79-1A9884E60126}.Release|x86.Build.0 = Release|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Debug|x64.Build.0 = Debug|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Debug|x86.Build.0 = Debug|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Release|x64.ActiveCfg = Release|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Release|x64.Build.0 = Release|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Release|x86.ActiveCfg = Release|Any CPU
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -281,6 +295,7 @@ Global
{C16DECA7-0A86-4285-9B3A-99DFD499B4FD} = {E322E6DA-3818-4CBD-A2F6-A94D6C8393BE}
{252E5F34-FA82-488A-939D-EF8DDAAB7D2A} = {E322E6DA-3818-4CBD-A2F6-A94D6C8393BE}
{65524B0F-6D11-4BA7-9D79-1A9884E60126} = {3F500D83-895F-4D44-9CA7-251D4B17B6FE}
+ {F8A24B3C-E1D2-4F56-A78B-C90D12EF5678} = {3F500D83-895F-4D44-9CA7-251D4B17B6FE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {27068E02-1682-44D7-819A-74759A261167}
diff --git a/vMixUTCSRTDataProvider/FodyWeavers.xml b/vMixUTCSRTDataProvider/FodyWeavers.xml
new file mode 100644
index 0000000..bd034a3
--- /dev/null
+++ b/vMixUTCSRTDataProvider/FodyWeavers.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/vMixUTCSRTDataProvider/FodyWeavers.xsd b/vMixUTCSRTDataProvider/FodyWeavers.xsd
new file mode 100644
index 0000000..f2dbece
--- /dev/null
+++ b/vMixUTCSRTDataProvider/FodyWeavers.xsd
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+ A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks
+
+
+
+
+ A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.
+
+
+
+
+ A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks
+
+
+
+
+ A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.
+
+
+
+
+ Obsolete, use UnmanagedWinX86Assemblies instead
+
+
+
+
+ A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks.
+
+
+
+
+ Obsolete, use UnmanagedWinX64Assemblies instead.
+
+
+
+
+ A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks.
+
+
+
+
+ A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks.
+
+
+
+
+ The order of preloaded assemblies, delimited with line breaks.
+
+
+
+
+
+ This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.
+
+
+
+
+ Controls if .pdbs for reference assemblies are also embedded.
+
+
+
+
+ Controls if runtime assemblies are also embedded.
+
+
+
+
+ Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.
+
+
+
+
+ Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.
+
+
+
+
+ As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.
+
+
+
+
+ The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events.
+
+
+
+
+ Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.
+
+
+
+
+ Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.
+
+
+
+
+ A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |
+
+
+
+
+ A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.
+
+
+
+
+ A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |
+
+
+
+
+ A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.
+
+
+
+
+ Obsolete, use UnmanagedWinX86Assemblies instead
+
+
+
+
+ A list of unmanaged X86 (32 bit) assembly names to include, delimited with |.
+
+
+
+
+ Obsolete, use UnmanagedWinX64Assemblies instead
+
+
+
+
+ A list of unmanaged X64 (64 bit) assembly names to include, delimited with |.
+
+
+
+
+ A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |.
+
+
+
+
+ The order of preloaded assemblies, delimited with |.
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/vMixUTCSRTDataProvider/OnWidgetUI.xaml b/vMixUTCSRTDataProvider/OnWidgetUI.xaml
new file mode 100644
index 0000000..3071e8f
--- /dev/null
+++ b/vMixUTCSRTDataProvider/OnWidgetUI.xaml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Listener (receber)
+ Caller (conectar)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 16 bytes (AES-128)
+ 24 bytes (AES-192)
+ 32 bytes (AES-256)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vMixUTCSRTDataProvider/OnWidgetUI.xaml.cs b/vMixUTCSRTDataProvider/OnWidgetUI.xaml.cs
new file mode 100644
index 0000000..6f9d19c
--- /dev/null
+++ b/vMixUTCSRTDataProvider/OnWidgetUI.xaml.cs
@@ -0,0 +1,207 @@
+using LibVLCSharp.Shared;
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using VlcMediaPlayer = LibVLCSharp.Shared.MediaPlayer;
+
+namespace UTCSRTDataProvider
+{
+ public partial class OnWidgetUI : UserControl, IDisposable
+ {
+ private static int _nextId = 0;
+ private readonly int _id = Interlocked.Increment(ref _nextId);
+
+ private VlcMediaPlayer _mediaPlayer;
+ private LibVLC _vlc;
+ private string _lastUrl;
+ private bool _isConnected;
+ private bool _reconnecting;
+ private bool _disposed;
+
+ static OnWidgetUI()
+ {
+ Trace.Listeners.Remove("Default");
+ }
+
+ public OnWidgetUI()
+ {
+ InitializeComponent();
+
+ if (DesignerProperties.GetIsInDesignMode(this))
+ return;
+
+ VideoPlaceholder.Text = $"Aguardando... [#{_id}]";
+ DataContextChanged += OnDataContextChanged;
+ }
+
+ private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ if (e.NewValue is SRTDataProvider neu)
+ AudioCheckBox.IsChecked = neu.IsAudioEnabled;
+ }
+
+ private SRTDataProvider Provider => DataContext as SRTDataProvider;
+
+ private void AudioCheckBox_Checked(object sender, RoutedEventArgs e)
+ {
+ if (Provider != null) Provider.IsAudioEnabled = true;
+ if (_isConnected) DoReconnect();
+ }
+
+ private void AudioCheckBox_Unchecked(object sender, RoutedEventArgs e)
+ {
+ if (Provider != null) Provider.IsAudioEnabled = false;
+ if (_isConnected) DoReconnect();
+ }
+
+ private Media BuildMedia()
+ {
+ var media = new Media(_vlc, _lastUrl, FromType.FromLocation);
+ media.AddOption(":network-caching=300");
+ if (AudioCheckBox.IsChecked != true)
+ media.AddOption(":no-audio");
+ return media;
+ }
+
+ // Reconnect without explicit Stop — Play(newMedia) makes VLC transition internally.
+ // _reconnecting suppresses the intermediate Stopped event so video stays visible.
+ private void DoReconnect()
+ {
+ if (_mediaPlayer == null || _vlc == null || string.IsNullOrEmpty(_lastUrl)) return;
+ _reconnecting = true;
+ SetStatus("Reconectando...", null);
+ var media = BuildMedia();
+ _mediaPlayer.Play(media);
+ media.Dispose();
+ }
+
+ private void ShowVideo()
+ {
+ VideoView.Visibility = Visibility.Visible;
+ VideoPlaceholder.Visibility = Visibility.Collapsed;
+ }
+
+ private void HideVideo()
+ {
+ VideoView.Visibility = Visibility.Collapsed;
+ VideoPlaceholder.Visibility = Visibility.Visible;
+ }
+
+ private void ConnectButton_Click(object sender, RoutedEventArgs e)
+ {
+ _vlc = Provider?.LibVLCInstance;
+ if (_vlc == null)
+ {
+ SetStatus(Provider?.VlcInitError ?? "VLC não inicializado", false);
+ return;
+ }
+
+ _lastUrl = Provider?.BuildUrl();
+ if (string.IsNullOrEmpty(_lastUrl))
+ {
+ SetStatus(Provider?.Mode == SRTMode.Caller
+ ? "Preencha IP e Porta"
+ : "Preencha a Porta", false);
+ return;
+ }
+
+ if (_mediaPlayer == null)
+ {
+ _mediaPlayer = new VlcMediaPlayer(_vlc);
+ _mediaPlayer.Playing += VlcMediaPlayer_Playing;
+ _mediaPlayer.EncounteredError += VlcMediaPlayer_Error;
+ _mediaPlayer.Stopped += VlcMediaPlayer_Stopped;
+ _mediaPlayer.EndReached += VlcMediaPlayer_Stopped;
+ }
+
+ ShowVideo();
+ VideoView.MediaPlayer = _mediaPlayer;
+ _mediaPlayer.Stop();
+ SetStatus("Conectando...", null);
+
+ var media = BuildMedia();
+ _mediaPlayer.Play(media);
+ media.Dispose();
+ }
+
+ private void DisconnectButton_Click(object sender, RoutedEventArgs e)
+ {
+ _reconnecting = false;
+ _isConnected = false;
+ _mediaPlayer?.Stop();
+ SetStatus("Desconectado", false);
+ HideVideo();
+ }
+
+ private void VlcMediaPlayer_Playing(object sender, EventArgs e)
+ {
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ _reconnecting = false;
+ _isConnected = true;
+ SetStatus("Conectado", true);
+ }));
+ }
+
+ private void VlcMediaPlayer_Error(object sender, EventArgs e)
+ {
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ _reconnecting = false;
+ _isConnected = false;
+ SetStatus("Erro de conexão", false);
+ HideVideo();
+ }));
+ }
+
+ private void VlcMediaPlayer_Stopped(object sender, EventArgs e)
+ {
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ if (_reconnecting) return;
+ if (StatusText.Text == "Conectado")
+ {
+ _isConnected = false;
+ SetStatus("Desconectado", false);
+ HideVideo();
+ }
+ }));
+ }
+
+ private void SetStatus(string text, bool? connected)
+ {
+ StatusText.Text = text;
+ var color = connected == true
+ ? Colors.LimeGreen
+ : connected == false
+ ? Colors.OrangeRed
+ : Colors.Orange;
+ var brush = new SolidColorBrush(color);
+ StatusText.Foreground = brush;
+ StatusIndicator.Fill = brush;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ DataContextChanged -= OnDataContextChanged;
+
+ if (_mediaPlayer != null)
+ {
+ _mediaPlayer.Playing -= VlcMediaPlayer_Playing;
+ _mediaPlayer.EncounteredError -= VlcMediaPlayer_Error;
+ _mediaPlayer.Stopped -= VlcMediaPlayer_Stopped;
+ _mediaPlayer.EndReached -= VlcMediaPlayer_Stopped;
+ _mediaPlayer.Stop();
+ _mediaPlayer.Dispose();
+ _mediaPlayer = null;
+ }
+ }
+ }
+}
diff --git a/vMixUTCSRTDataProvider/Properties/AssemblyInfo.cs b/vMixUTCSRTDataProvider/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..b35c68a
--- /dev/null
+++ b/vMixUTCSRTDataProvider/Properties/AssemblyInfo.cs
@@ -0,0 +1,17 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("vMixUTCSRTDataProvider")]
+[assembly: AssemblyDescription("SRT Monitor")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("vMixUTCSRTDataProvider")]
+[assembly: AssemblyCopyright("Copyright © 2025")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+[assembly: ComVisible(false)]
+[assembly: Guid("f8a24b3c-e1d2-4f56-a78b-c90d12ef5678")]
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/vMixUTCSRTDataProvider/README.md b/vMixUTCSRTDataProvider/README.md
new file mode 100644
index 0000000..79cee48
--- /dev/null
+++ b/vMixUTCSRTDataProvider/README.md
@@ -0,0 +1,61 @@
+# SRT Monitor — Data Provider para vMixUTC
+
+Widget de monitoramento de streams SRT integrado ao [vMixUTC](https://github.com/elgarf/vMixUTC), desenvolvido como um **Data Provider** externo.
+
+---
+
+## Download e Instalação
+
+**[⬇ Baixar SrtMonitorDataProvider.zip](https://github.com/rafalau/vMixUTC/releases/download/v1.1.0-srt/SrtMonitorDataProvider.zip)** (~86 MB — já inclui VLC)
+
+1. Extraia o ZIP
+2. Copie a pasta `DataProviders` para dentro da pasta de instalação do **vMixUTC**, mesclando com a pasta existente
+3. Reinicie o vMixUTC — o widget **SRT Monitor** aparecerá na lista de Data Providers
+
+---
+
+## O que é
+
+O **SRT Monitor** permite receber e visualizar streams de vídeo via protocolo **SRT (Secure Reliable Transport)** diretamente dentro do vMixUTC, sem precisar abrir um player externo. Ideal para monitorar sinais de entrada antes de colocá-los no ar no vMix.
+
+---
+
+## Funcionalidades
+
+| Recurso | Descrição |
+|---|---|
+| Modo Listener | Aguarda conexão de entrada na porta configurada |
+| Modo Caller | Conecta ativamente em um host remoto (IP + Porta) |
+| Preview de vídeo | Janela de preview embutida no widget com redimensionamento vertical |
+| Controle de áudio | Checkbox por instância — isola completamente o áudio de cada widget |
+| IP padrão automático | Pré-preenche o IP do host vMix já configurado no UTC |
+| Configurações avançadas | Latência, AES passphrase, key length (128/192/256 bits), Stream ID |
+| Ícones ▶ / ■ | Botões conectar/desconectar com ícones Font Awesome |
+| Múltiplas instâncias | Cada widget opera de forma completamente independente |
+
+---
+
+## Enable Auto Sync *(v1.1)*
+
+O vMixUTC não atualiza automaticamente quando algo muda no vMix — é necessário clicar em **Sync** manualmente. A partir da v1.1, o menu do UTC conta com o toggle **Enable Auto Sync**.
+
+Quando ativado, o UTC consulta o vMix a cada segundo e detecta qualquer alteração — inputs adicionados, textos modificados, configurações trocadas — atualizando a interface automaticamente.
+
+---
+
+## Tecnologias utilizadas
+
+| Biblioteca | Versão | Finalidade |
+|---|---|---|
+| [LibVLCSharp](https://github.com/videolan/libvlcsharp) | 3.8.5 | Wrapper .NET para libvlc |
+| [LibVLCSharp.WPF](https://github.com/videolan/libvlcsharp) | 3.8.5 | Controle WPF para preview de vídeo |
+| libvlc (VLC) | 3.x | Engine de reprodução / SRT decoder |
+| .NET Framework | 4.7.2 | Target framework do vMixUTC |
+| Costura.Fody | — | Embute as DLLs gerenciadas no assembly final |
+
+---
+
+## Créditos
+
+Idealizado por **[rafalau](https://github.com/rafalau)**.
+Implementado com assistência de **Claude Sonnet 4.6** (Anthropic).
diff --git a/vMixUTCSRTDataProvider/SRTDataProvider.cs b/vMixUTCSRTDataProvider/SRTDataProvider.cs
new file mode 100644
index 0000000..9437e13
--- /dev/null
+++ b/vMixUTCSRTDataProvider/SRTDataProvider.cs
@@ -0,0 +1,231 @@
+using LibVLCSharp.Shared;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.IO;
+using System.Windows;
+using vMixAPI;
+using vMixControllerDataProvider;
+
+namespace UTCSRTDataProvider
+{
+ public enum SRTMode
+ {
+ Listener = 0,
+ Caller = 1
+ }
+
+ [Description("SRT Monitor")]
+ public class SRTDataProvider : IvMixDataProvider, IDisposable, INotifyPropertyChanged
+ {
+ // Core.Initialize only needs to run once; LibVLC instances are per-provider
+ // so each SRT Monitor has fully isolated audio state.
+ private static bool _coreInitialized;
+ private static readonly object _initLock = new object();
+ private static string _vlcDir;
+
+ internal LibVLC LibVLCInstance { get; private set; }
+ internal string VlcInitError { get; private set; }
+
+ private static string GetDefaultIp()
+ {
+ try
+ {
+ var url = StateFabrique.GetUrl();
+ var uri = new Uri(url);
+ return uri.Host;
+ }
+ catch
+ {
+ return "127.0.0.1";
+ }
+ }
+
+ internal void EnsureVlcInitialized()
+ {
+ if (LibVLCInstance != null) return;
+ lock (_initLock)
+ {
+ if (!_coreInitialized)
+ {
+ try
+ {
+ var dir = Path.GetDirectoryName(typeof(SRTDataProvider).Assembly.Location);
+ var arch = Environment.Is64BitProcess ? "win-x64" : "win-x86";
+ _vlcDir = Path.Combine(dir, "libvlc", arch);
+ Core.Initialize(_vlcDir);
+ _coreInitialized = true;
+ }
+ catch (Exception ex)
+ {
+ VlcInitError = $"Core.Initialize falhou em '{_vlcDir}': {ex.GetType().Name}: {ex.Message}";
+ return;
+ }
+ }
+ }
+ try
+ {
+ LibVLCInstance = new LibVLC(enableDebugLogs: false);
+ }
+ catch (Exception ex)
+ {
+ VlcInitError = $"new LibVLC() falhou: {ex.GetType().Name}: {ex.Message}";
+ }
+ }
+
+ private OnWidgetUI _ui;
+ private SRTMode _mode = SRTMode.Listener;
+ private string _ip = GetDefaultIp();
+ private int _port = 4000;
+ private int _latency = 200;
+ private int _keyLength = 32;
+ private string _passphrase = "";
+ private string _streamId = "";
+ private bool _isAudioEnabled = true;
+
+ public SRTMode Mode
+ {
+ get => _mode;
+ set
+ {
+ if (_mode == value) return;
+ _mode = value;
+ Notify(nameof(Mode));
+ Notify(nameof(IsCallerMode));
+ Notify(nameof(ModeIndex));
+ }
+ }
+
+ public bool IsCallerMode => Mode == SRTMode.Caller;
+
+ public int ModeIndex
+ {
+ get => (int)Mode;
+ set { Mode = (SRTMode)value; }
+ }
+
+ public string IP
+ {
+ get => _ip;
+ set { if (_ip == value) return; _ip = value; Notify(nameof(IP)); }
+ }
+
+ public int Port
+ {
+ get => _port;
+ set { if (_port == value) return; _port = value; Notify(nameof(Port)); }
+ }
+
+ public int Latency
+ {
+ get => _latency;
+ set { if (_latency == value) return; _latency = value; Notify(nameof(Latency)); }
+ }
+
+ public int KeyLength
+ {
+ get => _keyLength;
+ set
+ {
+ if (_keyLength == value) return;
+ _keyLength = value;
+ Notify(nameof(KeyLength));
+ Notify(nameof(KeyLengthIndex));
+ }
+ }
+
+ public int KeyLengthIndex
+ {
+ get => _keyLength == 16 ? 0 : _keyLength == 24 ? 1 : 2;
+ set { KeyLength = value == 0 ? 16 : value == 1 ? 24 : 32; }
+ }
+
+ public string Passphrase
+ {
+ get => _passphrase;
+ set { if (_passphrase == value) return; _passphrase = value; Notify(nameof(Passphrase)); }
+ }
+
+ public string StreamId
+ {
+ get => _streamId;
+ set { if (_streamId == value) return; _streamId = value; Notify(nameof(StreamId)); }
+ }
+
+ public bool IsAudioEnabled
+ {
+ get => _isAudioEnabled;
+ set { if (_isAudioEnabled == value) return; _isAudioEnabled = value; Notify(nameof(IsAudioEnabled)); }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ private void Notify(string n) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
+
+ public SRTDataProvider()
+ {
+ EnsureVlcInitialized();
+ _ui = new OnWidgetUI { DataContext = this };
+ }
+
+ internal string BuildUrl()
+ {
+ if (_port <= 0 || _port > 65535) return null;
+ if (Mode == SRTMode.Caller && string.IsNullOrWhiteSpace(_ip)) return null;
+
+ var sb = new System.Text.StringBuilder("srt://");
+
+ if (Mode == SRTMode.Caller)
+ sb.Append(_ip.Trim());
+
+ sb.Append($":{_port}");
+ sb.Append($"?mode={Mode.ToString().ToLower()}");
+ sb.Append($"&latency={_latency}");
+
+ if (!string.IsNullOrWhiteSpace(_passphrase))
+ {
+ sb.Append($"&passphrase={Uri.EscapeDataString(_passphrase.Trim())}");
+ sb.Append($"&pbkeylen={_keyLength}");
+ }
+
+ if (!string.IsNullOrWhiteSpace(_streamId))
+ sb.Append($"&streamid={Uri.EscapeDataString(_streamId.Trim())}");
+
+ return sb.ToString();
+ }
+
+ public int Period { get; set; }
+ public bool IsProvidingCustomProperties => false;
+ public UIElement CustomUI => _ui;
+ public string[] Values => new string[0];
+ public void ShowProperties(Window owner) { }
+
+ public List