ATEM Mini Pro

Blackmagic Design makes some cool production switchers that are reasonably priced and work great. They even distribute an SDK to control them from Windows or Mac programs. Controlling from Crestron is tougher, but not impossible. Some third-party products have sprung up to make it easier, too. In this post, I’m going to walk through how I wrote a module to perform a couple of functions on the ATEM Mini Pro.

An Undocumented Protocol

As far as I can tell, ATEM doesn’t publish their protocol as far as which bits get put on the wire. They have an SDK, but it wraps around compiled libraries, so there’s no looking inside them to figure out what they’re doing. My only option was to run Wireshark and watch the dialog when the example programs attempted to do certain things.

For example, initiating communication with the switcher looked like this:

0000   10 14 6c 03 00 00 00 00 00 3f 00 00 01 00 00 00   ..l......?......
0010   00 00 00 00                                       ....

And the switcher would respond with:

0000   10 14 6c 03 00 00 00 00 00 fd 00 00 02 00 00 01   ..l.............
0010   00 00 00 00

And so on. I captured a few different sessions so that I could compare which values changed, which were constant, and tried to figure out what the different fields were in each packet. There’s a lot I don’t know, but I was able to get enough bits filled in correctly to make the switcher do things.

Our Options

What are our options for controlling a network device like the ATEM Mini Pro? Going from easiest to hardest would be:

  • TCP/IP Client or UDP/IP Client in SIMPL Windows
  • Direct sockets in SIMPL+
  • TCPClient or UDPServer in SIMPL#
  • Non-sandboxed C#

In the interests of saving time and dead ends, we’re going to start at the bottom of this list and work our way up. My initial testing with UDP/IP Clients suggested that the ATEM was pumping out a lot more traffic than SIMPL Windows wanted to deal with. Even going to direct sockets in SIMPL+, my buffers were filling up faster than I could clear them. Writing performant code in those languages is likely possible, but seems to be beyond my ability.

So I jumped to the C# end thinking it would have the best tools to help me write this thing.

Driver Program in C#

I forget how fast C# code compiles when I’ve been working in SIMPL Windows. It really helps maintain a good feedback loop when you can quickly compile and test your changes. And the quickest programs you can build are Windows Console apps. So that’s what we’ll use to start.

I did my testing in Visual Studio 2019, but you can use anything that builds Console Apps. I built mine on the .NET Framework, but .NET Core should work fine as well.

Lets add a new class to our project and name it AtemBase:

using System;
using System.Net;
using System.Net.Sockets;

namespace ATEM_Console
{
    class AtemBase : IDisposable
    {
        private UdpClient _udp;
        
        public AtemBase()
        {
        }

        public void Connect(string address, int portNumber)
        {
            _udp = new UdpClient();

            try
            {
                _udp.Connect(address, portNumber);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception in Connect: {0}", e.Message);
            }
        }

        public void Disconnect()
        {
            if (_udp != null)
            {
                _udp.Close();
            }
        }

        public void Dispose()
        {
            Disconnect();

            if (_udp != null)
            {
                _udp.Dispose();
            }

            _udp = null;
        }
    }
}

I’ve inherited from IDisposable largely because I want to make sure we clean up our UdpClient when we’re finished with it. Update the Program class to use it:

using System;
using System.Threading;

namespace ATEM_Console
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var mini = new AtemBase())
            {
                mini.Connect("172.20.10.205", 9910);
                Console.WriteLine("OK");

                Thread.Sleep(2000);
            }
        }
    }
}

Nothing happens yet, but we’re getting there. We’re going to add some properties to the AtemBase class and fill out the message loop:

using System;
using System.Net;
using System.Net.Sockets;

namespace ATEM_Console
{
    class AtemBase : IDisposable
    {
        private UdpClient _udp;

        public int SessionID { get; private set; }
        public int PacketID { get; private set; }
        public int Counter { get; private set; }
        
        public AtemBase()
        {
        }

        public void Connect(string address, int portNumber)
        {
            _udp = new UdpClient();

            SessionID = 0x1234; // temporary

            try
            {
                _udp.Connect(address, portNumber);
                _udp.BeginReceive(new AsyncCallback(OnReceiveData), _udp);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception in Connect: {0}", e.Message);
            }
        }

        public void Disconnect()
        {
            if (_udp != null)
            {
                _udp.Close();
            }
        }

        public void Dispose()
        {
            Disconnect();

            if (_udp != null)
            {
                _udp.Dispose();
            }

            _udp = null;
        }

        private void OnReceiveData(IAsyncResult result)
        {
            var sock = result.AsyncState as UdpClient;
            var src = new IPEndPoint(0, 0);
            var msg = sock.EndReceive(result, ref src);

            sock.BeginReceive(new AsyncCallback(OnReceiveData), sock);
        }
    }
}

This sets up a callback method for once we start receiving data. At the end of our callback, we setup another one to wait for the next packet (and so on). OnReceiveData will be called from a different thread, so we should be mindful of race conditions if we start accessing data within our class.

If we build this, nothing happens still. We haven’t initiated any communication to the ATEM Mini Pro yet. Lets add a few more methods to make that easier:

using System;
using System.Net;
using System.Net.Sockets;

namespace ATEM_Console
{
    class AtemBase : IDisposable
    {
        private UdpClient _udp;
        private int _state;

        public int SessionID { get; private set; }
        public int PacketID { get; private set; }
        public int Counter { get; private set; }
        
        public AtemBase()
        {
        }

        public void Connect(string address, int portNumber)
        {
            _udp = new UdpClient();

            SessionID = 0x1234; // temporary

            try
            {
                _udp.Connect(address, portNumber);
                _udp.BeginReceive(new AsyncCallback(OnReceiveData), _udp);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception in Connect: {0}", e.Message);
            }
        }

        public void Disconnect()
        {
            if (_udp != null)
            {
                _udp.Close();
            }
        }

        public void Dispose()
        {
            Disconnect();

            if (_udp != null)
            {
                _udp.Dispose();
            }

            _udp = null;
        }

        private void OnReceiveData(IAsyncResult result)
        {
            var sock = result.AsyncState as UdpClient;
            var src = new IPEndPoint(0, 0);
            var msg = sock.EndReceive(result, ref src);

            sock.BeginReceive(new AsyncCallback(OnReceiveData), sock);
        }

        public void Send(byte[] msg)
        {
            if (_udp != null)
            {
                _udp.Send(msg, msg.Length);
            }
        }

        private byte[] CreateMessage(byte type, int size)
        {
            byte[] msg = new byte[size];
            
            msg[0] = type;
            msg[1] = (byte)size;
            msg[2] = High(SessionID);
            msg[3] = Low(SessionID);

            return msg;
        }

        public void Hello()
        {
            byte[] msg = CreateMessage(0x10, 20);

            _state = 1;

            msg[9] = 0x3f;
            msg[12] = 0x01;

            Send(msg);
        }
    
        private void HelloAck()
        {
            byte[] msg = CreateMessage(0x80, 12);
            
            _state = 2;
            Counter = 1;

            msg[9] = 0xc3;

            Send(msg);
        }

        static byte Low(int n)
        {
            return (byte)(n & 0xFF);
        }

        static byte High(int n)
        {
            return (byte)(n >> 8);
        }
    }
}

Send and CreateMessage are both helper methods we’ll rely on a lot. CreateMessage builds a new byte array of the correct size and fills in the type and SessionID. We also create a couple static methods to quickly grab just the High or Low byte of an integer.

Hello builds the first packet we need to send to the ATEM switcher. HelloAck builds the first response. What’s that _state variable we set though? Remember, all of our incoming data passes into OnReceiveData, so it would be helpful to know where we are in the dialog. We’ll build a rudimentary state machine to help keep track:

private void OnReceiveData(IAsyncResult result)
{
    var sock = result.AsyncState as UdpClient;
    var src = new IPEndPoint(0, 0);
    var msg = sock.EndReceive(result, ref src);

    switch (_state)
    {
        case 1: // Hello
            HelloAck();
            break;
        case 2: // DeviceInfo
            if (msg[0] == 0x0d)
            {
                SessionID = msg[2] * 256 + msg[3];
            }
            else if (msg[0] == 0x88)
            {
                PacketID = msg[10] * 256 + msg[11];
                Ack();
            }
            break;
        case 3: // Idle
            if (msg[0] == 0x88)
            {
                PacketID = msg[10] * 256 + msg[11];
                Ack();
            }
            break;
        default:
            Console.WriteLine("Received {0} bytes in state {1}, type: {2:X2}", msg.Length, _state, msg[0]);
            break;
    }

    sock.BeginReceive(new AsyncCallback(OnReceiveData), sock);
}

If we enter an unknown state, the program prints out how many bytes we received as well as the message type (the first byte). This will help us identify where we’re missing code paths still. We also need to write the Ack method that gets called in states 2 and 3:

public void Ack()
{
    byte[] msg;

    _state = 3;

    msg = CreateMessage(0x80, 12);
    msg[4] = High(PacketID);
    msg[5] = Low(PacketID);
    msg[9] = 0x30;

    Send(msg);

    msg = CreateMessage(0x88, 12);
    msg[4] = High(PacketID);
    msg[5] = Low(PacketID);
    msg[11] = 0x01;

    Send(msg);
}

Our ack packets need to include the current PacketID which we grab in OnReceiveData. Eventually the dialog reaches state 3 which is kind of an idle state (maybe more akin to a heartbeat). This takes a couple seconds from the moment we initiate communication.

Returning to Program.cs, lets kick off the dialog:

using System;
using System.Threading;

namespace ATEM_Console
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var mini = new AtemBase("172.20.10.205", 9910))
            {
                mini.Connect();
                mini.Hello();

                Thread.Sleep(2000);
                Console.WriteLine("OK");

                Thread.Sleep(2000);
            }
        }
    }
}

The 2 second pause gives us a chance to reach state 3 before asking for too much. Obviously, we’ll look for a better way to signal readiness once we care about that. Lets fill in some more methods in AtemBase:

public void Cut()
{
    byte[] msg = CreateMessage(0x08, 24);;

    _state = 4;
    Counter += 1;

    msg[10] = High(Counter);
    msg[11] = Low(Counter);
    msg[13] = 0x0c;
    msg[14] = 0x4f;
    msg[15] = 0x03;
    msg[16] = 0x44;
    msg[17] = 0x43;
    msg[18] = 0x75;
    msg[19] = 0x74;
    msg[21] = 0x30;
    msg[22] = 0x73;
    msg[23] = 0x01;

    Send(msg);
}

private void CutAck()
{
    byte[] msg;

    _state = 3;

    msg = CreateMessage(0x80, 12);
    msg[4] = High(PacketID);
    msg[5] = Low(PacketID);
    msg[9] = 0x51;

    Send(msg);

    Counter += 1;

    msg = CreateMessage(0x88, 12);
    msg[4] = High(PacketID);
    msg[5] = Low(PacketID);
    msg[10] = High(Counter);
    msg[11] = Low(Counter);

    Send(msg);
}

public void Auto()
{
    byte[] msg = CreateMessage(0x08, 24);;

    _state = 5;
    Counter += 1;

    msg[10] = High(Counter);
    msg[11] = Low(Counter);
    msg[13] = 0x0c;
    msg[14] = 0x4f;
    msg[15] = 0x03;
    msg[16] = 0x44;
    msg[17] = 0x41;
    msg[18] = 0x75;
    msg[19] = 0x74;
    msg[21] = 0x9d;
    msg[22] = 0x0b;
    msg[23] = 0x01;

    Send(msg);
}

private void AutoAck()
{
    byte[] msg;

    _state = 3;

    msg = CreateMessage(0x80, 12);
    msg[4] = High(PacketID);
    msg[5] = Low(PacketID);
    msg[9] = 0x45;

    Send(msg);

    Counter += 1;

    msg = CreateMessage(0x88, 12);
    msg[4] = High(PacketID);
    msg[5] = Low(PacketID);
    msg[10] = High(Counter);
    msg[11] = Low(Counter);

    Send(msg);
}

public void SetPreview(int camera)
{
    byte[] msg = CreateMessage(0x88, 24);

    _state = 6;
    Counter += 1;

    msg[10] = High(Counter);
    msg[11] = Low(Counter);
    msg[13] = 0x0c;
    msg[14] = 0x01;
    msg[15] = 0x01;
    msg[16] = 0x43;
    msg[17] = 0x50;
    msg[18] = 0x76;
    msg[19] = 0x49;
    msg[21] = 0x47;
    msg[23] = (byte)camera;

    Send(msg);
}

private void SetPreviewAck()
{
    byte[] msg;

    _state = 3;

    msg = CreateMessage(0x80, 12);
    msg[4] = High(PacketID);
    msg[5] = Low(PacketID);
    msg[9] = 0xbb;

    Send(msg);

    Counter += 1;

    msg = CreateMessage(0x88, 12);
    msg[4] = High(PacketID);
    msg[5] = Low(PacketID);
    msg[10] = High(Counter);
    msg[11] = Low(Counter);

    Send(msg);
}

Don’t forget that we need to add these new states to OnReceiveData:

private void OnReceiveData(IAsyncResult result)
{
    var sock = result.AsyncState as UdpClient;
    var src = new IPEndPoint(0, 0);
    var msg = sock.EndReceive(result, ref src);

    switch (_state)
    {
        case 1: // Hello
            HelloAck();
            break;
        case 2: // DeviceInfo
            if (msg[0] == 0x0d)
            {
                SessionID = msg[2] * 256 + msg[3];
            }
            else if (msg[0] == 0x88)
            {
                PacketID = msg[10] * 256 + msg[11];
                Ack();
            }
            break;
        case 3: // Idle
            if (msg[0] == 0x88)
            {
                PacketID = msg[10] * 256 + msg[11];
                Ack();
            }
            break;
        case 4: // Cut
            if (msg[0] == 0x88)
            {
                PacketID = msg[10] * 256 + msg[11];
                CutAck();
            }
            break;
        case 5: // Auto
            if (msg[0] == 0x88)
            {
                PacketID = msg[10] * 256 + msg[11];
                AutoAck();
            }
            break;
        case 6: // SetPreview
            if (msg[0] == 0x88)
            {
                PacketID = msg[10] * 256 + msg[11];
                SetPreviewAck();
            }
            break;
        default:
            Console.WriteLine("Received {0} bytes in state {1}, type: {2:X2}", msg.Length, _state, msg[0]);
            break;
    }

    sock.BeginReceive(new AsyncCallback(OnReceiveData), sock);
}

Now we have Cut, Auto, and SetPreview that can be called from outside our class. Lets add those to our demo program:

using System;
using System.Threading;

namespace ATEM_Console
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var mini = new AtemBase("172.20.10.205", 9910))
            {
                mini.Connect();
                mini.Hello();

                Thread.Sleep(2000);
                Console.WriteLine("OK");

                mini.SetPreview(3);
                Thread.Sleep(1000);

                Console.WriteLine("Cut");
                mini.Cut();
                Thread.Sleep(1000);

                Console.WriteLine("Auto");
                mini.Auto();
                Thread.Sleep(2000);

                Console.WriteLine("Done");
                Thread.Sleep(2000);
            }
        }
    }
}

Run this beauty and it should work. Will it work with all ATEM switchers? Will it work with any other ATEM switchers besides the one I’m testing with? I don’t know, but it works with the one I’m borrowing!

Migrate to SIMPL#

We got switcher control working in Windows using a C# program, now lets make it work from a Crestron device! To add a further wrinkle, I’d like to make sure it works on 3-series processors so that means we’ll have to create our library in Visual Studio 2008.

Create a new SIMPL# Library project and copy the AtemBase.cs file into it. Lets rename the class (and file) to AtemMini since that’s really the only equipment I know for sure works at this point. Because we have to stick to Crestron’s sandbox on 3-series, there are a handful of changes that need to be made:

using System;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;

namespace AtemLib
{
    public class AtemMini : IDisposable
    {
        private UDPServer _udp;
        private int _state;

        public int SessionID { get; private set; }
        public int PacketID { get; private set; }
        public int Counter { get; private set; }

        public bool Ready { get; private set; }

        public event EventHandler ReadyEvent;

        public AtemMini()
        {
        }

        public void Initialize()
        {
            _state = 0;
        }

        public void Connect(string address, int port)
        {
            _udp = new UDPServer(address, port, 10000, EthernetAdapterType.EthernetLANAdapter);
            
            Ready = false;

            try
            {
                _udp.EnableUDPServer();
                _udp.ReceiveDataAsync(OnReceiveData);
            }
            catch (Exception e)
            {
                CrestronConsole.PrintLine("Exception in Connect: {0}", e.Message);
            }
        }

        public void Disconnect()
        {
            Ready = false;

            if (_udp != null)
            {
                _udp.DisableUDPServer();
            }
        }

        public void Dispose()
        {
            Disconnect();

            if (_udp != null)
            {
                _udp.Dispose();
            }

            _udp = null;
        }

        public void Send(byte[] msg)
        {
            _udp.SendData(msg, msg.Length);
        }

        public void OnReceiveData(UDPServer sock, int bytesReceived)
        {
            var msg = sock.IncomingDataBuffer;

            switch (_state)
            {
                case 1: // Hello
                    HelloAck();
                    break;
                case 2: // DeviceInfo
                    if (msg[0] == 0x0d)
                    {
                        SessionID = msg[2] * 256 + msg[3];
                    }
                    else if (msg[0] == 0x88)
                    {
                        PacketID = msg[10] * 256 + msg[11];
                        Ack();
                    }
                    break;
                case 3: // Idle
                    if (msg[0] == 0x88)
                    {
                        PacketID = msg[10] * 256 + msg[11];
                        Ack();

                        if (!Ready)
                        {
                            Ready = true;

                            if (ReadyEvent != null)
                            {
                                ReadyEvent(this, new EventArgs());
                            }
                        }
                    }
                    break;
                case 4: // Cut
                    if (msg[0] == 0x88)
                    {
                        PacketID = msg[10] * 256 + msg[11];
                        CutAck();
                    }
                    break;
                case 5: // Auto
                    if (msg[0] == 0x88)
                    {
                        PacketID = msg[10] * 256 + msg[11];
                        AutoAck();
                    }
                    break;
                case 6: // SetPreview
                    if (msg[0] == 0x88)
                    {
                        PacketID = msg[10] * 256 + msg[11];
                        SetPreviewAck();
                    }
                    break;
                default:
                    CrestronConsole.PrintLine("Received {0} bytes in state {1}, type: {2:X2}", msg.Length, _state, msg[0]);
                    break;
            }

            sock.ReceiveDataAsync(OnReceiveData);
        }

        public static byte Low(int n)
        {
            return (byte)(n & 0xFF);
        }

        public static byte High(int n)
        {
            return (byte)(n >> 8);
        }

        private byte[] CreateMessage(byte type, int size)
        {
            byte[] msg = new byte[size];

            msg[0] = type;
            msg[1] = (byte)size;
            msg[2] = High(SessionID);
            msg[3] = Low(SessionID);

            return msg;
        }

        public void Hello()
        {
            SessionID = 0x1234;
            Counter = 0;

            byte[] msg = CreateMessage(0x10, 20);

            _state = 1; // Hello

            msg[9] = 0x3f;
            msg[12] = 0x01;

            Send(msg);
        }

        private void HelloAck()
        {
            byte[] msg = CreateMessage(0x80, 12);

            _state = 2; // DeviceInfo
            Counter = 1;

            msg[9] = 0xc3;

            Send(msg);
        }

        private void Ack()
        {
            byte[] msg;

            _state = 3; // Idle

            msg = CreateMessage(0x80, 12);
            msg[4] = High(PacketID);
            msg[5] = Low(PacketID);
            msg[9] = 0x30;

            Send(msg);

            msg = CreateMessage(0x88, 12);
            msg[4] = High(PacketID);
            msg[5] = Low(PacketID);
            msg[11] = 0x01;

            Send(msg);
        }

        public void Cut()
        {
            byte[] msg = CreateMessage(0x08, 24);

            _state = 4; // Cut
            Counter += 1;

            msg[10] = High(Counter);
            msg[11] = Low(Counter);
            msg[13] = 0x0c;
            msg[14] = 0x4f;
            msg[15] = 0x03;
            msg[16] = 0x44; // D
            msg[17] = 0x43; // C
            msg[18] = 0x75; // u
            msg[19] = 0x74; // t
            msg[21] = 0x30;
            msg[22] = 0x73;
            msg[23] = 0x01;

            Send(msg);
        }

        private void CutAck()
        {
            byte[] msg;

            _state = 3;

            msg = CreateMessage(0x80, 12);
            msg[4] = High(PacketID);
            msg[5] = Low(PacketID);
            msg[9] = 0x51;

            Send(msg);

            Counter += 1;

            msg = CreateMessage(0x88, 12);
            msg[4] = High(PacketID);
            msg[5] = Low(PacketID);
            msg[10] = High(Counter);
            msg[11] = Low(Counter);

            Send(msg);
        }

        public void Auto()
        {
            byte[] msg = CreateMessage(0x08, 24);

            _state = 5; // Auto
            Counter += 1;

            msg[10] = High(Counter);
            msg[11] = Low(Counter);
            msg[13] = 0x0c;
            msg[14] = 0x4f;
            msg[15] = 0x03;
            msg[16] = 0x44; // D
            msg[17] = 0x41; // A
            msg[18] = 0x75; // u
            msg[19] = 0x74; // t
            msg[21] = 0x9d;
            msg[22] = 0x0b;
            msg[23] = 0x01;

            Send(msg);
        }

        private void AutoAck()
        {
            byte[] msg;

            _state = 3;

            msg = CreateMessage(0x80, 12);
            msg[4] = High(PacketID);
            msg[5] = Low(PacketID);
            msg[9] = 0x45;

            Send(msg);

            Counter += 1;

            msg = CreateMessage(0x88, 12);
            msg[4] = High(PacketID);
            msg[5] = Low(PacketID);
            msg[10] = High(Counter);
            msg[11] = Low(Counter);

            Send(msg);
        }

        public void SetPreview(int camera)
        {
            byte[] msg = CreateMessage(0x88, 24);

            _state = 6;
            Counter += 1;

            msg[10] = High(Counter);
            msg[11] = Low(Counter);
            msg[13] = 0x0c;
            msg[14] = 0x01;
            msg[15] = 0x01;
            msg[16] = 0x43; // C
            msg[17] = 0x50; // P
            msg[18] = 0x76; // v
            msg[19] = 0x49; // I
            msg[21] = 0x47; // G
            msg[23] = (byte)camera;

            Send(msg);
        }

        private void SetPreviewAck()
        {
            byte[] msg;

            _state = 3;

            msg = CreateMessage(0x80, 12);
            msg[4] = High(PacketID);
            msg[5] = Low(PacketID);
            msg[9] = 0xbb;

            Send(msg);

            Counter += 1;

            msg = CreateMessage(0x88, 12);
            msg[4] = High(PacketID);
            msg[5] = Low(PacketID);
            msg[10] = High(Counter);
            msg[11] = Low(Counter);

            Send(msg);
        }
    }
}

As you can see, the bulk of our class stays the same, there are just a couple of changes needed because Crestron’s UDPServer works a little differently. We also add an Initialize method since SIMPL+ can’t call anything but the default constructor. We’ve also added a ReadyEvent event to signal when our connection has stabilized.

Build this and it will spit out a CLZ library we can drop into a SIMPL+ module.

Wrapping It Up

To use our SIMPL# library in SIMPL Windows, we need to wrap it inside a SIMPL+ module. Here it is:

// COMPILER DIRECTIVES /////////////////////////////////////////////////////////////////////

#DEFAULT_VOLATILE
#ENABLE_STACK_CHECKING
#ENABLE_TRACE
// #DIGITAL_EXPAND 
// #ANALOG_SERIAL_EXPAND 

/*
#HELP_BEGIN
   (add additional lines of help lines)
#HELP_END
*/

// LIBRARIES ///////////////////////////////////////////////////////////////////////////////

#USER_SIMPLSHARP_LIBRARY "AtemLib"

// INPUTS //////////////////////////////////////////////////////////////////////////////////

DIGITAL_INPUT Connect;
DIGITAL_INPUT Disconnect;
DIGITAL_INPUT Cut;
DIGITAL_INPUT Auto;
ANALOG_INPUT  Preview_Source;

// OUTPUTS /////////////////////////////////////////////////////////////////////////////////

DIGITAL_OUTPUT Connect_Fb;

// SOCKETS /////////////////////////////////////////////////////////////////////////////////

// PARAMETERS //////////////////////////////////////////////////////////////////////////////

INTEGER_PARAMETER Port_Number;
STRING_PARAMETER  Address[100];

// STRUCTURES //////////////////////////////////////////////////////////////////////////////

AtemMini AtemConsole;

// GLOBAL VARIABLES ////////////////////////////////////////////////////////////////////////

INTEGER giReady;

// FUNCTIONS ///////////////////////////////////////////////////////////////////////////////

// EVENT HANDLERS //////////////////////////////////////////////////////////////////////////

PUSH Connect
{
	giReady = 0;
	AtemConsole.Connect(Address, Port_Number);
	AtemConsole.Hello();
}

PUSH Disconnect
{
	giReady = 0;
	Connect_Fb = giReady;
	AtemConsole.Disconnect();
}

PUSH Cut
{
	If (giReady)
	{
		AtemConsole.Cut();
	}
}

PUSH Auto
{
	If (giReady)
	{
		AtemConsole.Auto();
	}
}

CHANGE Preview_Source
{
	If (giReady)
	{
		AtemConsole.SetPreview(Preview_Source);
	}
}

// CALLBACKS AND DELEGATES /////////////////////////////////////////////////////////////////

EVENTHANDLER ReadyEventHandler(AtemMini sender, EventArgs args)
{
	giReady = 1;
	Connect_Fb = giReady;
}

// MAIN ////////////////////////////////////////////////////////////////////////////////////

FUNCTION Main()
{
	WaitForInitializationComplete();

	AtemConsole.Initialize();
	RegisterEvent(AtemConsole, ReadyEvent, ReadyEventHandler);
}

Build the SIMPL+ module, then drop it into a SIMPL Windows program. Give it appropriate signals and you can load and test to a processor:

If you’d like to grab a copy of the finished code, it’s available on my GitHub. Thanks for reading!

6 thoughts on “ATEM Mini Pro”

  1. Kiel, keep up the good work! Your posts are always great. I have learned so much from you. I truly appreciate it. Thanks for the effort!

    Like

  2. Would you be up to creating a version of your app that runs in Windows 10/11, that allows just the (YouTube) stream key to be entered via a dialog box and set into the switcher (I use the Extreme model, but guess it’s the same for all models that support streaming). Can make a contribution if needed. This is for our church livestream operators who are very hesitant at using the full switcher software, so looking for a very simple stream key entry app. Can email me back directly if you wish. Thank you.

    Like

    1. The good thing about running on Windows is you can use the SDK from Blackmagic: https://www.blackmagicdesign.com/developer/product/atem. They have example programs in a couple languages you can mess around with. The only reason I had to reverse engineer the protocol was because the SDK wouldn’t work on the 3-series platform. It’s free to register, then you can download it. I don’t have an ATEM Mini Pro to test with anymore, so I don’t think I’d be much help. Good luck!

      Like

Leave a comment