From 2cd782ee5fa64bc9e26e86ade3aa9613091b8ef7 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 22 Feb 2021 23:37:46 +0000 Subject: [PATCH] wip: initial commit --- .gitattributes | 63 +++++++ .gitignore | 263 +++++++++++++++++++++++++++++ .gitmodules | 3 + AtemProxy.sln | 28 ++++ AtemProxy/AtemProxy.csproj | 26 +++ AtemProxy/AtemServer.cs | 266 ++++++++++++++++++++++++++++++ AtemProxy/AtemServerConnection.cs | 41 +++++ AtemProxy/ClientConnectionList.cs | 90 ++++++++++ AtemProxy/Program.cs | 151 +++++++++++++++++ AtemProxy/ProxyConnection.cs | 213 ++++++++++++++++++++++++ AtemProxy/ProxyServer.cs | 137 +++++++++++++++ AtemProxy/UpstreamConnection.cs | 163 ++++++++++++++++++ AtemProxy/log4net.config | 25 +++ LibAtem | 1 + 14 files changed, 1470 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 AtemProxy.sln create mode 100644 AtemProxy/AtemProxy.csproj create mode 100644 AtemProxy/AtemServer.cs create mode 100644 AtemProxy/AtemServerConnection.cs create mode 100644 AtemProxy/ClientConnectionList.cs create mode 100644 AtemProxy/Program.cs create mode 100644 AtemProxy/ProxyConnection.cs create mode 100644 AtemProxy/ProxyServer.cs create mode 100644 AtemProxy/UpstreamConnection.cs create mode 100644 AtemProxy/log4net.config create mode 160000 LibAtem diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09c632e --- /dev/null +++ b/.gitignore @@ -0,0 +1,263 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0a5e5ed --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "LibAtem"] + path = LibAtem + url = https://github.com/LibAtem/LibAtem.git diff --git a/AtemProxy.sln b/AtemProxy.sln new file mode 100644 index 0000000..eaa64b8 --- /dev/null +++ b/AtemProxy.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AtemProxy", "AtemProxy\AtemProxy.csproj", "{00C82FAB-C255-4F6B-BC7E-6E0F798DCBF0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibAtem", "LibAtem\LibAtem\LibAtem.csproj", "{41BB562D-9C43-4A2A-8117-18DC4CDD4C7C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibAtem.Discovery", "LibAtem\LibAtem.Discovery\LibAtem.Discovery.csproj", "{EE6D9FE0-E821-460B-8E40-682075E32F39}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {00C82FAB-C255-4F6B-BC7E-6E0F798DCBF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00C82FAB-C255-4F6B-BC7E-6E0F798DCBF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00C82FAB-C255-4F6B-BC7E-6E0F798DCBF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00C82FAB-C255-4F6B-BC7E-6E0F798DCBF0}.Release|Any CPU.Build.0 = Release|Any CPU + {41BB562D-9C43-4A2A-8117-18DC4CDD4C7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41BB562D-9C43-4A2A-8117-18DC4CDD4C7C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41BB562D-9C43-4A2A-8117-18DC4CDD4C7C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41BB562D-9C43-4A2A-8117-18DC4CDD4C7C}.Release|Any CPU.Build.0 = Release|Any CPU + {EE6D9FE0-E821-460B-8E40-682075E32F39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE6D9FE0-E821-460B-8E40-682075E32F39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE6D9FE0-E821-460B-8E40-682075E32F39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE6D9FE0-E821-460B-8E40-682075E32F39}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/AtemProxy/AtemProxy.csproj b/AtemProxy/AtemProxy.csproj new file mode 100644 index 0000000..41642ea --- /dev/null +++ b/AtemProxy/AtemProxy.csproj @@ -0,0 +1,26 @@ + + + + Exe + netcoreapp3.1 + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/AtemProxy/AtemServer.cs b/AtemProxy/AtemServer.cs new file mode 100644 index 0000000..3820aae --- /dev/null +++ b/AtemProxy/AtemServer.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using LibAtem.Commands; +using LibAtem.Discovery; +using LibAtem.Net; +using log4net; +using Makaretu.Dns; + +namespace AtemProxy +{ + public class AtemServer + { + private static readonly ILog Log = LogManager.GetLogger(typeof(AtemServer)); + + private readonly ClientConnectionList _connections = new ClientConnectionList(); + private CommandQueue _state; + private bool _accept = false; + + private Socket _socket; + private readonly MulticastService _mdns = new MulticastService(); + + // TODO - remove this list, and replace with something more sensible... + private readonly List timers = new List(); + + public delegate void CommandHandler(object sender, List> commands); + + public event CommandHandler OnReceive; + + public AtemServer(CommandQueue state) + { + _state = state; + } + + public void RejectConnections() + { + _accept = false; + _connections.ClearAll(); + } + + public void AcceptConnections() + { + _accept = true; + } + + public void StartAnnounce(string modelName, string deviceId) + { + _mdns.UseIpv4 = true; + _mdns.UseIpv6 = false; + var safeModelName = modelName.Replace(' ', '-').ToUpper(); + var domain = new DomainName($"Mock {modelName}.{AtemDeviceInfo.ServiceName}"); + var deviceDomain = new DomainName($"MOCK-{safeModelName}-{deviceId}.local"); + + timers.Add(new Timer(o => + { + Log.Info("MDNS announce"); + DoAnnounce(deviceId, modelName, domain, deviceDomain); + }, null, 0, 10000)); + _mdns.QueryReceived += (s, e) => + { + var msg = e.Message; + if (msg.Questions.Any(q => q.Name == AtemDeviceInfo.ServiceName)) + { + Log.Debug("MDNS query"); + DoAnnounce(deviceId, modelName, domain, deviceDomain); + } + }; + _mdns.Start(); + } + private void DoAnnounce(string deviceId, string modelName, DomainName domain, DomainName deviceDomain) + { + var res = new Message(); + var addresses = MulticastService.GetIPAddresses() + .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork); + foreach (var address in addresses) + { + res.Answers.Add(new PTRRecord + { + Name = AtemDeviceInfo.ServiceName, + DomainName = domain + }); + res.AdditionalRecords.Add(new TXTRecord + { + Name = domain, + Strings = new List + { + "txtvers=1", + $"name=Blackmagic {modelName}", + "class=AtemSwitcher", + "protocol version=0.0", + "internal version=FAKE", + $"unique id={deviceId}" + } + }); + res.AdditionalRecords.Add(new ARecord + { + Address = address, + Name = deviceDomain, + }); + res.AdditionalRecords.Add(new SRVRecord + { + Name = domain, + Port = 9910, + Priority = 0, + Target = deviceDomain, + Weight = 0 + }); + /* + res.AdditionalRecords.Add(new NSECRecord + { + Name = domain + });*/ + } + _mdns.SendAnswer(res); + } + + public void StartPingTimer() + { + timers.Add(new Timer(o => + { + _connections.QueuePings(); + }, null, 0, AtemConstants.PingInterval)); + } + + private static Socket CreateSocket() + { + Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, 9910); + serverSocket.Bind(ipEndPoint); + + return serverSocket; + } + + public void StartReceive() + { + _socket = CreateSocket(); + + var thread = new Thread(async () => + { + while (true) + { + try + { + //Start receiving data + ArraySegment buff = new ArraySegment(new byte[2500]); + var end = new IPEndPoint(IPAddress.Any, 0); + SocketReceiveFromResult v = await _socket.ReceiveFromAsync(buff, SocketFlags.None, end); + + // Check if we can accept it + if (!_accept) continue; + + AtemServerConnection conn = _connections.FindOrCreateConnection(v.RemoteEndPoint, out _); + if (conn == null) + continue; + + byte[] buffer = buff.Array; + var packet = new ReceivedPacket(buffer); + + if (packet.CommandCode.HasFlag(ReceivedPacket.CommandCodeFlags.Handshake)) + { + conn.ResetConnStatsInfo(); + // send handshake back + byte[] test = + { + buffer[0], buffer[1], // flags + length + buffer[2], buffer[3], // session id + 0x00, 0x00, // acked pkt id + 0x00, 0x00, // retransmit request + buffer[8], buffer[9], // unknown2 + 0x00, 0x00, // server pkt id + 0x02, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00 + }; + + var sendThread = new Thread(o => + { + while (!conn.HasTimedOut) + { + conn.TrySendQueued(_socket); + Task.Delay(1).Wait(); + } + Console.WriteLine("send finished"); + }); + sendThread.Name = $"To {conn.Endpoint}"; + sendThread.Start(); + + await _socket.SendToAsync(new ArraySegment(test, 0, 20), SocketFlags.None, v.RemoteEndPoint); + + continue; + } + + if (!conn.IsOpened) + { + var recvThread = new Thread(o => + { + while (!conn.HasTimedOut || conn.HasCommandsToProcess) + { + List cmds = conn.GetNextCommands(); + + Log.DebugFormat("Recieved {0} commands", cmds.Count); + //conn.HandleInner(_state, connection, cmds); + } + }); + recvThread.Name = $"Receive {conn.Endpoint}"; + recvThread.Start(); + } + conn.Receive(_socket, packet); + + if (conn.ReadyForData) + QueueDataDumps(conn); + } + catch (SocketException) + { + // Reinit the socket as it is now unavailable + //_socket = CreateSocket(); + } + } + }); + thread.Name = "AtemServer"; + thread.Start(); + } + + private void QueueDataDumps(AtemConnection conn) + { + try + { + var queuedCommands = _state.Values(); + var count = queuedCommands.Count; + var sent = 0; + while (queuedCommands.Count > 0) + { + var builder = new OutboundMessageBuilder(); + + int removeCount = 0; + foreach (byte[] data in queuedCommands) + { + if (!builder.TryAddData(data)) + break; + + removeCount++; + } + + if (removeCount == 0) + { + throw new Exception("Failed to build message!"); + } + + queuedCommands.RemoveRange(0, removeCount); + conn.QueueMessage(builder.Create()); + Log.InfoFormat("Length {0} {1}", builder.currentLength , removeCount); + sent++; + } + + Log.InfoFormat("Sent all {1} commands to {0} in {2} packets", conn.Endpoint, count, sent); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + } + } +} \ No newline at end of file diff --git a/AtemProxy/AtemServerConnection.cs b/AtemProxy/AtemServerConnection.cs new file mode 100644 index 0000000..8b6a1ad --- /dev/null +++ b/AtemProxy/AtemServerConnection.cs @@ -0,0 +1,41 @@ +using System; +using System.Net; +using LibAtem.Commands; +using LibAtem.Net; + +namespace AtemProxy +{ + public class AtemServerConnection : AtemConnection + { + public AtemServerConnection(EndPoint endpoint, int sessionId) : base(endpoint, sessionId) + { + } + + private bool _sentDataDump; + + public bool ReadyForData + { + get + { + if (_sentDataDump) + return false; + + if (!IsOpened) + return false; + + return _sentDataDump = true; + } + } + + protected override OutboundMessage CompileNextMessage() + { + // TODO + return null; + } + + public override void QueueCommand(ICommand command) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/AtemProxy/ClientConnectionList.cs b/AtemProxy/ClientConnectionList.cs new file mode 100644 index 0000000..92ed0c4 --- /dev/null +++ b/AtemProxy/ClientConnectionList.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Net; +using log4net; + +namespace AtemProxy +{ + public class ClientConnectionList + { + private static readonly ILog Log = LogManager.GetLogger(typeof(ClientConnectionList)); + + private readonly Dictionary connections; + + public ClientConnectionList() + { + connections = new Dictionary(); + } + + public AtemServerConnection FindOrCreateConnection(EndPoint ep, out bool isNew) + { + lock (connections) + { + AtemServerConnection val; + if (connections.TryGetValue(ep, out val)) + { + isNew = false; + return val; + } + + val = new AtemServerConnection(ep, 0x8008);// TODO - make dynamic + connections[ep] = val; + val.OnDisconnect += RemoveTimedOut; + + Log.InfoFormat("New connection from {0}", ep); + + isNew = true; + return val; + } + } + + public void ClearAll() + { + lock (connections) + { + connections.Clear(); + } + } + + private void RemoveTimedOut(object sender) + { + var conn = sender as AtemServerConnection; + if (conn == null) + return; + + Log.InfoFormat("Lost connection to {0}", conn.Endpoint); + + lock (connections) + { + connections.Remove(conn.Endpoint); + } + } + + internal void QueuePings() + { + lock (connections) + { + var toRemove = new List(); + foreach (KeyValuePair conn in connections) + { + if (conn.Value.HasTimedOut) + { + toRemove.Add(conn.Key); + continue; + } + + if (conn.Value.IsOpened) + { + conn.Value.QueuePing(); + } + } + + foreach (var ep in toRemove) + { + Log.InfoFormat("Lost connection to {0}", ep); + connections.Remove(ep); + } + } + } + + } +} \ No newline at end of file diff --git a/AtemProxy/Program.cs b/AtemProxy/Program.cs new file mode 100644 index 0000000..44e5311 --- /dev/null +++ b/AtemProxy/Program.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using LibAtem.Commands; +using LibAtem.Util; +using log4net; +using log4net.Config; + +namespace AtemProxy +{ + public class Program + { + public static void Main(string[] args) + { + var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); + XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config")); + + var log = LogManager.GetLogger(typeof(Program)); + log.Info("Starting"); + + var currentStateCommands = new CommandQueue(); + var unknownCommandId = 1; + + var upstream = new UpstreamConnection("10.42.13.95"); + + var server = new AtemServer(currentStateCommands); + server.OnReceive += (sender, commands) => + { + // TODO + }; + + string deviceName = "Test Proxy"; + server.StartAnnounce(deviceName, deviceName.GetHashCode().ToString()); + + upstream.OnConnection += (sender) => + { + log.DebugFormat("Connected to atem"); + // TODO - state + server.AcceptConnections(); + }; + upstream.OnDisconnection += (sender) => + { + log.DebugFormat("Lost connection to atem"); + server.RejectConnections(); + currentStateCommands.Clear(); + }; + upstream.OnReceive += (sender, commands) => + { + foreach (var cmd in commands) + { + if (cmd.Item1 != null) + { + currentStateCommands.Set(new CommandQueueKey(cmd.Item1), cmd.Item2); + } + else + { + currentStateCommands.Set(unknownCommandId++, cmd.Item2); + } + } + }; + + upstream.Start(); + server.StartReceive(); + server.StartPingTimer(); + + + //var server = new ProxyServer("10.42.13.95"); + + /* + var client = new AtemClient("10.42.13.95"); + client.Connect(); + client.OnConnection += (sender) => + { + client.SendCommand(new FairlightMixerSourceSetCommand() + { + Mask = FairlightMixerSourceSetCommand.MaskFlags.Gain, + Index = AudioSource.Mic1, + SourceId = -256, + Gain = -10 + }); + Console.WriteLine("Sent"); + }; + + while(true){} + */ + + Console.WriteLine("Press Ctrl+C to terminate..."); + + AutoResetEvent waitHandle = new AutoResetEvent(false); + // Handle Control+C or Control+Break + Console.CancelKeyPress += (o, e) => + { + Console.WriteLine("Exit"); + + // Allow the manin thread to continue and exit... + waitHandle.Set(); + }; + + // Wait + waitHandle.WaitOne(); + + // Force the exit + System.Environment.Exit(0); + } + } + + public class CommandQueue + { + private readonly Dictionary _dict = new Dictionary(); + private readonly List _keys = new List(); + + public void Clear() + { + lock (_dict) + { + _dict.Clear(); + _keys.Clear(); + } + } + + public List Values() + { + lock (_dict) + { + return _keys.Select(k => _dict[k]).ToList(); + } + } + + public void Set(object key, byte[] value) + { + lock (_dict) + { + if (_dict.TryGetValue(key, out var tmpval)) + { + _dict[key] = value; + } + else + { + _dict.Add(key, value); + _keys.Add(key); + } + } + } + + } +} \ No newline at end of file diff --git a/AtemProxy/ProxyConnection.cs b/AtemProxy/ProxyConnection.cs new file mode 100644 index 0000000..8da2127 --- /dev/null +++ b/AtemProxy/ProxyConnection.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using LibAtem; +using LibAtem.Commands; +using LibAtem.Commands.DeviceProfile; +using LibAtem.Net; +using log4net; + +namespace AtemProxy +{ + public class ProxyConnection + { + private static readonly ILog Log = LogManager.GetLogger(typeof(ProxyConnection)); + + private readonly ConcurrentQueue _logQueue; + + private readonly Socket _serverSocket; + private readonly EndPoint _clientEndPoint; + + public IPEndPoint AtemEndpoint { get; } + public UdpClient AtemConnection { get; } + + public ProxyConnection(ConcurrentQueue logQueue, string address, Socket serverSocket, EndPoint clientEndpoint) + { + _logQueue = logQueue; + _serverSocket = serverSocket; + _clientEndPoint = clientEndpoint; + + AtemEndpoint = new IPEndPoint(IPAddress.Parse(address), 9910); + AtemConnection = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); + + StartReceivingFromAtem(address); + } + + private bool MutateServerCommand(ICommand cmd) + { + /* + if (cmd is MultiviewPropertiesGetCommand mvpCmd) + { + // mvpCmd.SafeAreaEnabled = false; + return true; + } + if (cmd is MultiviewerConfigCommand mvcCmd) + { + // mvcCmd.Count = 1; + // mvcCmd.WindowCount = 9; // < 10 works, no effect? + // mvcCmd.Tmp2 = 0; + // mvcCmd.CanRouteInputs = false; // Confirmed + // mvcCmd.CanToggleSafeArea = 0; // Breals + // mvcCmd.SupportsVuMeters = 0; // + return true; + } + else if (cmd is MixEffectBlockConfigCommand meCmd) + { + meCmd.KeyCount = 1; + return true; + } + else if (cmd is TopologyV8Command top8Cmd) + { + // topCmd.SuperSource = 2; // Breaks + // topCmd.TalkbackOutputs = 8; + // topCmd.SerialPort = 0; // < 1 Works + // topCmd.DVE = 2; // > 1 Works + top8Cmd.MediaPlayers = 1; // < 2 Works + // topCmd.Stingers = 0; // < 1 Works + top8Cmd.DownstreamKeyers = 1; // < 1 Works + // topCmd.Auxiliaries = 4; // Breaks + // topCmd.HyperDecks = 2; // Works + // topCmd.TalkbackOverSDI = 4; // + // top8Cmd.Tmp11 = 2; // < 1 breaks. > 1 is ok + // top8Cmd.Tmp12 = 0; // All work + // topCmd.Tmp14 = 1; // Breaks + // top8Cmd.Tmp20 = 1; + + Console.WriteLine("{0}", JsonConvert.SerializeObject(top8Cmd)); + return true; + } + else if (cmd is TopologyCommand topCmd) + { + // topCmd.SuperSource = 2; // Breaks + // topCmd.TalkbackOutputs = 8; + // topCmd.SerialPort = 0; // < 1 Works + // topCmd.DVE = 2; // > 1 Works + // topCmd.MediaPlayers = 1; // < 2 Works + // topCmd.Stingers = 0; // < 1 Works + topCmd.DownstreamKeyers = 1; // < 1 Works + // topCmd.Auxiliaries = 4; // Breaks + // topCmd.HyperDecks = 2; // Works + // topCmd.TalkbackOverSDI = 4; // + // topCmd.Tmp11 = 2; // < 1 breaks. > 1 is ok + // topCmd.Tmp12 = 0; // All work + // topCmd.Tmp14 = 1; // Breaks + // topCmd.Tmp20 = 0; + return true; + }*/ + return false; + } + + private byte[] ParsedCommandToBytes(ParsedCommandSpec cmd) + { + var build = new CommandBuilder(cmd.Name); + build.AddByte(cmd.Body); + return build.ToByteArray(); + } + + private byte[] CompileMessage(ReceivedPacket origPacket, byte[] payload) + { + byte opcode = (byte)origPacket.CommandCode; + byte len1 = (byte)((ReceivedPacket.HeaderLength + payload.Length) / 256 | opcode << 3); // opcode 0x08 + length + byte len2 = (byte)((ReceivedPacket.HeaderLength + payload.Length) % 256); + + byte[] buffer = + { + len1, len2, // Opcode & Length + (byte)(origPacket.SessionId / 256), (byte)(origPacket.SessionId % 256), // session id + 0x00, 0x00, // ACKed Pkt Id + 0x00, 0x00, // Unknown + 0x00, 0x00, // unknown2 + (byte)(origPacket.PacketId / 256), (byte)(origPacket.PacketId % 256), // pkt id + }; + + // If no payload, dont append it + if (payload.Length == 0) + return buffer; + + return buffer.Concat(payload).ToArray(); + } + + private void StartReceivingFromAtem(string address) + { + var thread = new Thread(() => + { + while (true) + { + try + { + IPEndPoint ep = AtemEndpoint; + byte[] data = AtemConnection.Receive(ref ep); + + //Log.InfoFormat("Got message from atem. {0} bytes", data.Length); + + var packet = new ReceivedPacket(data); + if (packet.CommandCode.HasFlag(ReceivedPacket.CommandCodeFlags.AckRequest) && + !packet.CommandCode.HasFlag(ReceivedPacket.CommandCodeFlags.Handshake)) + { + // Handle this further + var newPayload = new byte[0]; + bool changed = false; + foreach (var rawCmd in packet.Commands) + { + var cmd = CommandParser.Parse(ProxyServer.Version, rawCmd); + if (cmd != null) + { + if (cmd is VersionCommand vcmd) + { + ProxyServer.Version = vcmd.ProtocolVersion; + } + + var name = CommandManager.FindNameAndVersionForType(cmd); + // Log.InfoFormat("Recv {0} {1}", name.Item1, JsonConvert.SerializeObject(cmd)); + + if (MutateServerCommand(cmd)) + { + changed = true; + newPayload = newPayload.Concat(cmd.ToByteArray()).ToArray(); + } + else + { + newPayload = newPayload.Concat(ParsedCommandToBytes(rawCmd)).ToArray(); + } + + } + else + { + newPayload = newPayload.Concat(ParsedCommandToBytes(rawCmd)).ToArray(); + } + } + + if (changed) + { + data = CompileMessage(packet, newPayload); + } + } + + _logQueue.Enqueue(new LogItem() + { + IsSend = false, + Payload = data + }); + + try + { + _serverSocket.SendTo(data, SocketFlags.None, _clientEndPoint); + } + catch (ObjectDisposedException) + { + Log.ErrorFormat("{0} - Discarding message due to socket being disposed", _clientEndPoint); + } + } + catch (SocketException) + { + Log.ErrorFormat("Socket Exception"); + } + } + }); + thread.Start(); + } + } +} \ No newline at end of file diff --git a/AtemProxy/ProxyServer.cs b/AtemProxy/ProxyServer.cs new file mode 100644 index 0000000..36027c4 --- /dev/null +++ b/AtemProxy/ProxyServer.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using LibAtem; +using LibAtem.Commands; +using LibAtem.Net; +using log4net; +using Newtonsoft.Json; + +namespace AtemProxy +{ + public class LogItem + { + public bool IsSend { get; set; } + public byte[] Payload { get; set; } + } + + public class ProxyServer + { + private static readonly ILog Log = LogManager.GetLogger(typeof(ProxyServer)); + + private Socket _socket; + + private Dictionary _clients = new Dictionary(); + public static ProtocolVersion Version = ProtocolVersion.Minimum; + + private ConcurrentQueue _logQueue = new ConcurrentQueue(); + + public ProxyServer(string address) + { + // TODO - need to clean out stale clients + + StartLogWriter(); + StartReceivingFromClients(address); + } + + private void StartLogWriter() + { + var thread = new Thread(() => + { + while(true) + { + if (!_logQueue.TryDequeue(out LogItem item)) + { + Thread.Sleep(5); + continue; + } + + var packet = new ReceivedPacket(item.Payload); + if (packet.CommandCode.HasFlag(ReceivedPacket.CommandCodeFlags.AckRequest) && + !packet.CommandCode.HasFlag(ReceivedPacket.CommandCodeFlags.Handshake)) + { + string dirStr = item.IsSend ? "Send" : "Recv"; + // Handle this further + foreach (var rawCmd in packet.Commands) + { + var cmd = CommandParser.Parse(Version, rawCmd); + if (cmd != null) + { + Log.InfoFormat("{0} {1} {2} ({3})", dirStr, rawCmd.Name, JsonConvert.SerializeObject(cmd), BitConverter.ToString(rawCmd.Body)); + } else + { + Log.InfoFormat("{0} unknown {1} {2}", dirStr, rawCmd.Name, BitConverter.ToString(rawCmd.Body)); + } + } + } + } + }); + thread.Start(); + } + + private static Socket CreateSocket() + { + Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, 9910); + serverSocket.Bind(ipEndPoint); + + return serverSocket; + } + + private void StartReceivingFromClients(string address) + { + _socket = CreateSocket(); + + var thread = new Thread(async () => + { + while (true) + { + try + { + //Start receiving data + ArraySegment buff = new ArraySegment(new byte[2500]); + var end = new IPEndPoint(IPAddress.Any, 0); + SocketReceiveFromResult v = await _socket.ReceiveFromAsync(buff, SocketFlags.None, end); + + string epStr = v.RemoteEndPoint.ToString(); + if (!_clients.TryGetValue(epStr, out ProxyConnection client)) + { + Log.InfoFormat("Got connection from new client: {0}", epStr); + client = new ProxyConnection(_logQueue, address, _socket, v.RemoteEndPoint); + _clients.Add(epStr, client); + } + + //Log.InfoFormat("Got message from client. {0} bytes", v.ReceivedBytes); + + var resBuff = buff.ToArray(); + var resSize = v.ReceivedBytes; + + _logQueue.Enqueue(new LogItem() + { + IsSend = true, + Payload = resBuff + }); + + try + { + client.AtemConnection.Send(resBuff, resSize, client.AtemEndpoint); + } + catch (ObjectDisposedException) + { + Log.ErrorFormat("{0} - Discarding message due to socket being disposed", client.AtemEndpoint); + } + } + catch (SocketException) + { + // Reinit the socket as it is now unavailable + //_socket = CreateSocket(); + } + } + }); + thread.Start(); + } + } +} \ No newline at end of file diff --git a/AtemProxy/UpstreamConnection.cs b/AtemProxy/UpstreamConnection.cs new file mode 100644 index 0000000..a5b2d61 --- /dev/null +++ b/AtemProxy/UpstreamConnection.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using LibAtem; +using LibAtem.Commands; +using LibAtem.Commands.Audio.Fairlight; +using LibAtem.Commands.DataTransfer; +using LibAtem.Commands.DeviceProfile; +using LibAtem.Net; +using log4net; + +namespace AtemProxy +{ + public class UpstreamConnection + { + private static readonly ILog Log = LogManager.GetLogger(typeof(UpstreamConnection)); + + private readonly AtemClient _atem; + private readonly Thread _thread; + private readonly ConcurrentQueue _pktQueue = new ConcurrentQueue(); + + private ProtocolVersion _version = ProtocolVersion.Minimum; + + private static readonly Type[] AudioLevelCommands = new[] + { + typeof(FairlightMixerMasterLevelsCommand), typeof(FairlightMixerSourceLevelsCommand), + // typeof(FairlightMixerSendLevelsCommand) + }; + + private static readonly Type[] TransferCommands = new[] + { + typeof(DataTransferAbortCommand), typeof(DataTransferAckCommand), + typeof(DataTransferCompleteCommand), typeof(DataTransferDataCommand), typeof(DataTransferErrorCommand), + typeof(DataTransferDownloadRequestCommand), typeof(DataTransferUploadContinueCommand), + typeof(DataTransferUploadRequestCommand) + }; + + private static readonly Type[] LockCommands = new[] {typeof(LockObtainedCommand), typeof(LockStateSetCommand)}; + + public event AtemClient.ConnectedHandler OnConnection + { + add => _atem.OnConnection += value; + remove => _atem.OnConnection -= value; + } + public event AtemClient.DisconnectedHandler OnDisconnection + { + add => _atem.OnDisconnect += value; + remove => _atem.OnDisconnect -= value; + } + + public delegate void CommandHandler(object sender, List> commands); + public delegate void AudioLevelsHandler(object sender, List levels); + + public event CommandHandler OnReceive; + public event AudioLevelsHandler OnAudioLevels; + + private static byte[] ParsedCommandToBytes(ParsedCommandSpec cmd) + { + var build = new CommandBuilder(cmd.Name); + build.AddByte(cmd.Body); + return build.ToByteArray(); + } + + public UpstreamConnection(string address) + { + _atem = new AtemClient(address); + + _atem.OnDisconnect += (sender) => + { + // Discard any pending packets + _pktQueue.Clear(); + }; + + _atem.OnReceivePacket += (sender, pkt) => + { + _pktQueue.Enqueue(pkt); + }; + + _thread = new Thread(() => + { + while (true) + { + if (!_pktQueue.TryDequeue(out ReceivedPacket pkt)) + { + Thread.Sleep(5); + continue; + } + + var acceptedCommands = new List>(); + var audioLevels = new List(); + foreach (ParsedCommandSpec rawCmd in pkt.Commands) + { + var cmd = CommandParser.Parse(_version, rawCmd); + if (cmd != null) + { + // Ensure we know what version to parse with + if (cmd is VersionCommand vcmd) + _version = vcmd.ProtocolVersion; + + if (AudioLevelCommands.Contains(cmd.GetType())) + { + audioLevels.Add(cmd); + } + else if (LockCommands.Contains(cmd.GetType())) + { + } + else if (TransferCommands.Contains(cmd.GetType())) + { + + } + else + { + acceptedCommands.Add(Tuple.Create(cmd, ParsedCommandToBytes(rawCmd))); + } + } + else + { + // Unknown command, so forward it and hope! + // It is unlikely to break anything, but command-id logic wont handle it well + Log.WarnFormat("Atem gave unknown command {0} {1}", rawCmd.Name, BitConverter.ToString(rawCmd.Body)); + + // TODO - this is not very optimal... + //newPayload = newPayload.Concat(rawCmd.Body).ToArray(); + acceptedCommands.Add(Tuple.Create(null, ParsedCommandToBytes(rawCmd))); + } + } + + if (pkt.Commands.Count > 0) + { + Log.InfoFormat("Atem gave {0} forwardable commands of {1}", acceptedCommands.Count, + pkt.Commands.Count); + } + + if (acceptedCommands.Count > 0) + { + // Forward if anything was left + OnReceive?.Invoke(this, acceptedCommands); + } + + if (audioLevels.Count > 0) + { + // Forward audio levels commands to subscribed clients + OnAudioLevels?.Invoke(this, audioLevels); + } + } + }); + _thread.Name = "UpstreamConnection"; + } + + public void Start() + { + _atem.Connect(); + _thread.Start(); + } + + public void ForwardPacket(ReceivedPacket pkt) + { + // TODO + } + } +} \ No newline at end of file diff --git a/AtemProxy/log4net.config b/AtemProxy/log4net.config new file mode 100644 index 0000000..c4ec3f2 --- /dev/null +++ b/AtemProxy/log4net.config @@ -0,0 +1,25 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + diff --git a/LibAtem b/LibAtem new file mode 160000 index 0000000..cb5eb99 --- /dev/null +++ b/LibAtem @@ -0,0 +1 @@ +Subproject commit cb5eb99acb7f4379906bb7dcad7ffb115ba73e31