Automatically starting and broadcasting in Wirecast

Recently, one of the local radio stations here in town wanted to stream their stuff live over the internet. Being their ISP, we helped them quite a bit. You can read about that and how it was done here: Multiple parallel audio streams from multiple audio sources on one Wirecast license.. Recently, however, we have uncovered a huge flaw in Wirecast: it’s inability to recover from pretty much any error automatically, start automatically, or automatically broadcast. If you don’t have a 24/7 technician who can sit in front of the server and watch Wirecast around the clock, this presents a problem.

A HUGE problem. And Wirecast’s official answer is “it’s on the wish list”.

Nice. I’m done wishing. So here’s what I did…

First, I started with Wirecast’s C# example for programmatically controlling their product. Not much. The documentation for the API, which you can find in the same directory as the sample code, showed that the API left much to be desired. But it’s all we have to work with…

The one part I did end up using from the sample code was the Late class. Here it is:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace WRHIStartup
{
///

/// Late binding helper class.
/// static bindings to help you get/set via OLE/COM.
///

class Late
{
public static void Set(object obj, string sProperty, object oValue)
{
object[] oParam = new object[1];
oParam[0] = oValue;
obj.GetType().InvokeMember(sProperty, BindingFlags.SetProperty, null, obj, oParam);
}
public static object Get(object obj, string sProperty, object oValue)
{
object[] oParam = new object[1];
oParam[0] = oValue;
return obj.GetType().InvokeMember(sProperty, BindingFlags.GetProperty, null, obj, oParam);
}
public static object Get(object obj, string sProperty, object[] oValue)
{
return obj.GetType().InvokeMember(sProperty, BindingFlags.GetProperty, null, obj, oValue);
}
public static object Get(object obj, string sProperty)
{
return obj.GetType().InvokeMember(sProperty, BindingFlags.GetProperty, null, obj, null);
}
public static object Invoke(object obj, string sProperty, params object[] oParam)
{
return obj.GetType().InvokeMember(sProperty, BindingFlags.InvokeMethod, null, obj, oParam);
}
public static object Invoke(object obj, string sProperty, object oValue)
{
object[] oParam = new object[1];
oParam[0] = oValue;
return obj.GetType().InvokeMember(sProperty, BindingFlags.InvokeMethod, null, obj, oParam);
}
public static object Invoke(object obj, string sProperty)
{
return obj.GetType().InvokeMember(sProperty, BindingFlags.InvokeMethod, null, obj, null);
}
}
}

All that really does is get us access to the Wirecast API bits in a semi-organized fashion. Better would be to create classes around each of the Wirecast interfaces, but let’s not look a $500 gift horse in the mouth.

My idea was simple:

  1. Get the Wirecast object
  2. Get all open documents
  3. Check to see if the open documents are broadcasting and start them if they are not
  4. Look for error message dialog boxes, close them if they exist, and then mark the streams to be restarted
  5. return to step 3, rinse and repeat

Sounds simple enough, doesn’t it? To get it to work on boot, I put shortcuts to all the saved Wirecast documents I wanted started automatically in the Startup menu as well as a shortcut to this program. The program does a 20 second wait at the beginning to be sure all the documents have time to load then it starts the cycle of checking them and starting them if necessary. Obviously, the first time around it is always necessary so it always starts them then.

So here we go… I am not going to say much, since the code is … semi-fairly-well commented. Ask any questions you want if you have any:


//Load the libraries we want...
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
// we're gonna need this...
using System.Threading;
// ooo, and this!
using System.Runtime.InteropServices;

namespace WRHIStartup
{
class Program
{
// We need to know the max length of a string returned by the system
const int MAXTITLE = 255;
// And the unsigned integer value for "Close, you stupid window!"
const UInt32 WM_CLOSE = 0x0010;

// Let's get a hash table for the list of open windows
private static Hashtable mTitlesList;
// and a list for those we know are errors and need to kill
private static ArrayList mBlockList;
// Gotta keep track of the Wirecast documents...
private static ArrayList documents;
// And a boolean to keep track of whether or not we need to restart the streams
private static bool validated = true;

// We need a delegate for one of the system calls we have to make
private delegate bool EnumDelegate(IntPtr hWnd, int lParam);

// And now we import some DLL calls to user32.dll
[DllImport("user32.dll", EntryPoint = "EnumDesktopWindows",
ExactSpelling = false, CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool _EnumDesktopWindows(IntPtr hDesktop,
EnumDelegate lpEnumCallbackFunction, IntPtr lParam);

[DllImport("user32.dll", EntryPoint = "GetWindowText",
ExactSpelling = false, CharSet = CharSet.Auto, SetLastError = true)]
private static extern int _GetWindowText(IntPtr hWnd,
StringBuilder lpWindowText, int nMaxCount);

[DllImport("user32.dll", EntryPoint = "SendMessage",
ExactSpelling = false, CharSet = CharSet.Auto, SetLastError = true)]
private static extern int SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern IntPtr GetParent(IntPtr hWnd);

///

/// This is just an alias to SendMessage so we don't have to
/// remember all the things it needs
///

/// The handle of the window we want to close /// true if the window closed
private static bool CloseWindow(IntPtr hWnd)
{
return (SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero) == 0);
}

///

/// This is used in the delegate to get the window information
///

/// /// ///
private static bool EnumWindowsProc(IntPtr hWnd, int lParam)
{
string title = GetWindowText(hWnd);
mTitlesList.Add(hWnd, title);
return true;
}

///

/// Returns the caption of a windows by given HWND identifier.
///

public static string GetWindowText(IntPtr hWnd)
{
StringBuilder title = new StringBuilder(MAXTITLE);
int titleLength = _GetWindowText(hWnd, title, title.Capacity + 1);
title.Length = titleLength;

return title.ToString();
}

///

/// Returns the caption of all desktop windows.
///

public static Hashtable GetDesktopWindowsCaptions()
{
mTitlesList = new Hashtable();
EnumDelegate enumfunc = new EnumDelegate(EnumWindowsProc);
IntPtr hDesktop = IntPtr.Zero; // current desktop
bool success = _EnumDesktopWindows(hDesktop, enumfunc, IntPtr.Zero);

if (success)
{
return mTitlesList;
}
else
{
// Get the last Win32 error code
int errorCode = Marshal.GetLastWin32Error();

string errorMessage = String.Format(
"EnumDesktopWindows failed with code {0}.", errorCode);
throw new Exception(errorMessage);
}
}

static void Main(string[] args)
{
Console.WriteLine("Waiting a few seconds to allow the documents to load...");
for (int i = 0; i < 20; i++) { Console.WriteLine("{0}", 20 - i); Thread.Sleep(1000); } Console.WriteLine("Alright, all documents should be started, let's go ahead get what we need."); documents = new ArrayList(); mBlockList = new ArrayList(); Console.Write("Getting Wirecast ... "); object oWirecast = GetWirecast(); Console.WriteLine("done."); object doc; int j = 1; Console.Write("Getting open documents "); while ((doc = Late.Invoke(oWirecast, "DocumentByIndex", j++)) != null) { Console.Write("."); object layer = Late.Invoke(doc, "LayerByName", "audio"); Late.Set(layer, "ActiveShotID", 0); documents.Add(doc); } Console.WriteLine(" {0} found.", documents.Count); SetCaptions(); while (true) { try { foreach (object curDoc in documents) { int isb = (int)Late.Invoke(curDoc, "IsBroadcasting"); if (isb != 1 || validated == false) { if (!validated) { Console.Write("Some stream is invalidated somewhere! stopping... "); Late.Invoke(curDoc, "Broadcast", "stop"); Console.WriteLine("done."); } Console.Write("Found non broadcasting stream! starting... "); Late.Invoke(curDoc, "Broadcast", "start"); object layer = Late.Invoke(doc, "LayerByName", "audio"); Late.Set(layer, "ActiveShotID", 0); Console.WriteLine("done."); } } validated = true; Hashtable desktopWindowsCaptions = GetDesktopWindowsCaptions(); foreach (DictionaryEntry caption in desktopWindowsCaptions) { //Console.WriteLine(caption); if (mBlockList.Contains(caption.Value)) { validated = false; if (CloseWindow((IntPtr)caption.Key)) { Console.WriteLine("Window {0} found and closed.", caption.Value); } else { int errorCode = Marshal.GetLastWin32Error(); string errorMessage = String.Format( "Destroying Window {0} failed with code {1}.", caption.Value, errorCode); throw new Exception(errorMessage); } } } Thread.Sleep(500); } catch (Exception ex) { DisplayError(ex); } } //Console.ReadLine(); } private static void SetCaptions() { mBlockList.Add("Wirecast"); } private static void DisplayError(Exception ex) { Console.WriteLine("Error Message: {0}", ex.Message); Console.WriteLine("{0}", ex.Source); Console.WriteLine(" {0}", ex.StackTrace); if (ex.InnerException != null) { Console.WriteLine("Inner Exception:"); DisplayError(ex.InnerException); } } ///

/// Get the active Wirecast object.
///

/// the active wirecast object
private static object GetWirecast()
{
try
{
return Marshal.GetActiveObject("Wirecast.Application");
}
catch
{
Type objClassType = Type.GetTypeFromProgID("Wirecast.Application");
return Activator.CreateInstance(objClassType);
}
}
}
}

Well, there it is. If you have any questions, ask, if you want binaries, ask as well and I’ll post a link or something. Hate to be short, but it’s after 6pm on Friday and frankly I am sick of looking at Wirecast. Their tech support says they are coming out with a new version very soon. Let’s hope they finally fixed the major issues this version has.

Leave a Reply