Video Control Room

Automating Video Workflows With PowerShell

Linking today to some great content from another Ian (ProTip: get to know an Ian, we’re full of useful knowledge). Ian Morrish posts about automating a variety of methods of automating A/V equipment using PowerShell. Lots of useful stuff in here.

No Windows? No worries, you can install PowerShell on MacOS and Linux too.

I’ve put some feelers out to some of my streaming equipment vendors to find out what kind of automation hooks and APIs they support.

Meanwhile, Wowza has a REST API for both its Streaming Engine and Cloud products. Integrating this into PowerShell should be relatively straightforward. Any PowerShell wizards wanna take a stab at it?

Stay tuned.

 

Microsoft Azure

Nonprofit Tech Deals: Microsoft Azure

Last week while I was at the Church IT Network National Conference in Anderson, SC, a colleague pointed me to a fantastic donation from Microsoft via TechSoup: $5000/year in Azure credit. At a hair over $400/month, this means you can run a pretty substantial amount of stuff. Microsoft just announced this program at the end of September, so it’s still very new. And very cool. Credits are good any time within the 12-month period, so you don’t have to split them up month by month. They do not, however, roll over to the following year.

The context of the conversation was for hosting the open-source RockRMS Church/Relationship Management System, but Wowza Streaming Engine is also available ready to go on Azure. And many other things. (and for those of us in the midwest, Microsoft’s biggest Azure datacenter is “US Central” located in Des Moines, as Iowa is currently a very business-friendly place to put a huge datacenter)

If you’re a registered 501c3 non-profit (or your local country’s equivalent if you’re outside the US), head on over to Tech Soup to take advantage of this fantastic deal.

As an added bonus, if you have Windows Server Datacenter licenses from TechSoup or that your organization purchased with Software Assurance, each 2-socket license can be run on up to two Azure compute instances each with up to 8 virtual cores, reducing the cost of your instances even further (as standard Windows instances include the cost of the Windows license at full nonprofit prices.). This also applies to SQL Server.

Here’s the process:

  1. Read the FAQ.
  2. Register your organization with TechSoup if you haven’t already done so.
  3. Head over to Microsoft’s Azure Product Donations page and hit “Get Started”
  4. At some point in the process you’ll also want to create an Azure account to associate the credits with. If you’re already using Office 365 for nonprofits, it’s best to tie an account to your O365 domain.
Wowza Ninja

Wowza Stream Scheduler Hacks: Google Calendar

One of Wowza’s most underutilized yet most powerful features is the stream scheduler. I’ve blogged about it extensively in the past, and I’ll return from a long hiatus to do it again.

To recap some of the things you can do with this add-on:

  • Create a virtual stream that plays a loop of server-side content
  • Play a sequence of video content (think TV programming)
  • A combination of both
  • Play portions of a video file (in/out points)
  • In combination with the LoopUntilLive module, do all that and then interrupt with a live stream

This gives you the ability to have a continuous 24/7 stream of programming including advertising. The output of this schedule is then treated by Wowza like any other stream, meaning it can be used as input to a transcoder, nDVR, or sent somewhere with Stream Targets.

The challenge we run into is that building the schedule in XML is not the most obvious thing in the world as there is not currently any integration of the module into the Wowza Streaming Engine Manager’s GUI.

As the schedule is written as a SMIL file (a specific XML schema) in an application’s content directory, It requires either logging in to the server and manipulating files with a text editor, or uploading into the content directory.

The other way is to build the schedule programmatically. Command-line PHP is an easy way to do this as PHP has some excellent PHP processing tools.

If you want to peek at the Java code for the scheduler module, Wowza has it up on GitHub.

A quick recap of the structure of the stream scheduler’s XML Schema:

  • The entire file is wrapped in <SMIL></SMIL> tags to indicate that this is in fact a SMIL file.
  • an empty <HEAD/> block – Wowza doesn’t currently make use of anything in here, but it’s a good place to put comments, and it makes for good XML.
  • The meat of the file, a <BODY></BODY> block that contains all the good stuff.
  • Within the body block, there are two key element types:
    1. One or more <STREAM> blocks that define the names of the virtual streams that are created by the schedule.
    2. One or more <PLAYLIST> blocks that define the content and timing of what gets published. Each playlist tag specifies the following attributes:
      • name : The name of the playlist. This is arbitrary but should be unique within the file
      • playOnStream: specifies which of the streams created in the <STREAM> block this playlist’s content will go to
      • repeat: a boolean (true/false) value that specifies if this playlist loops until something else happens. If it runs out of content, the virtual stream will stop.
      • scheduled: The date and time (based on server timezone) this playlist will be published to the stream. This is in ISO 8601 format without the T delimiter (YYYY-MM-DD HH:MM:SS)
    3. Within the <PLAYLIST> block are one or more <VIDEO> tags with the following attributes:
      • src: The path and filename (relative to the application’s content directory) of the video file to play. This should be prefixed with mp4: as you would any other video file within Wowza. You can also put in the name of a live stream published within the same application.
      • start: The offset (in seconds) from the beginning of the file where playback is to begin.
      • length: Play duration (in seconds) from the start point. A value of -1 will play to the end of the file. A value of -2 indicates that this is a live stream.
      • Once the end of this item is reached, it will move to the next element in the playlist. If there is no more content it will either loop (if repeat is set to true) or stop. If there is nothing further on the schedule, the stream will unpublish and stop. If this is not a repeating playlist, It’s generally a good idea to put a buffer video (a number of minutes of black video or a logo works just fine) at the end of it to fill any gaps to the next playlist.

So, the schedule is pretty straightforward, but it can get tedious to build. I previously posted about a way to generate this with a spreadsheet in Excel. This is clunky, but can save a lot of typing, and is good for repeating events.

But this lacked a good visual interface. As I was working on a project for a client to translate a schedule generated from their video content management system into the Wowza Stream Scheduler’s XML, it occurred to me that there was another structured schedule format that could be translated easily into XML: iCal. This calendar format is defined in RFC 2445 and is widely used by many calendaring systems.

Unfortunately, iCal is not XML to begin with (iCal/RFC2445 predates XML by a decade), which would be WAY too easy. Here is a sample of iCal data out of Google Calendar that contains two events (Google used to make their calendar shares available in XML but it seems that is no longer the case):

BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Wowza Event Scheduler Calendar
X-WR-TIMEZONE:America/Chicago
X-WR-CALDESC:
BEGIN:VEVENT
DTSTART:20161017T160000Z
DTEND:20161017T170000Z
DTSTAMP:20161016T164145Z
UID:xxxxxx@google.com
CREATED:20161012T212924Z
DESCRIPTION:mp4:video1.mp4\,0\,-1
LAST-MODIFIED:20161016T164138Z
LOCATION:teststream
SEQUENCE:3
STATUS:CONFIRMED
SUMMARY:11am Broadcast
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
DTSTART:20161017T170000Z
DTEND:20161017T180000Z
DTSTAMP:20161016T164145Z
UID:xxxxxx@google.com
CREATED:20161016T164116Z
DESCRIPTION:mp4:video2.mp4\,0\,1800\nmp4:video3.mp4\,0\,1800
LAST-MODIFIED:20161016T164118Z
LOCATION:teststream
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Noon Broadcast
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR

As you can see, this has some hints of XML: Opening and closing tags, attributes, and the like. Fortunately, Evert Pot wrote a handy little PHP function to make the conversion to XML.

One of the really nice things about JSON and XML in PHP is that the objects that contain them work just like any other nested arrays, and so extracting specific items is ridiculously easy. There’s a lot of data within the VEVENT block that we just aren’t interested in. We really only care about the start and stop times, and a few other fields like DESCRIPTION, LOCATION and SUMMARY, which we can hack to contain the names of the streams and content. In this example, I use DESCRIPTION to contain the names of the video files on each line (and additional comma-separated data regarding start and end points, and LOCATION to specify what stream it should be published on. SUMMARY can be used as the playlist name attribute There are a number of other iCal fields that can be used for this as well.

In order to use this data, we need to do the following:

  • Use the start/end times to calculate a duration
  • Make a list of the streams to publish to
  • figure out what video to play when
  • Convert datestamps to the local server time

For starters, we’re going to need to set a few defaults:

ini_set("allow_url_fopen", 1);
error_reporting(0);
date_default_timezone_set("US/Central");

Using Evert’s conversion function, we get the schedule into an XML object:

$calUrl = "https://calendar.google.com/calendar/ical/xxxxxxxxxxxxx8%40group.calendar.google.com/private-xxxxx/basic.ics";
// get your private calendar URL from the calendar settings. 
$CalData=file_get_contents($calUrl);
$xmlString=iCalendarToXML($CalData);
$xmlObj = simplexml_load_string($xmlString);

The object now looks like this:

SimpleXMLElement Object
(
    [PRODID] => -//Google Inc//Google Calendar 70.9054//EN
    [VERSION] => 2.0
    [CALSCALE] => GREGORIAN
    [METHOD] => PUBLISH
    [X-WR-CALNAME] => Wowza Event Scheduler Calendar
    [X-WR-TIMEZONE] => America/Chicago
    [X-WR-CALDESC] => SimpleXMLElement Object
        (
        )

    [VEVENT] => Array
        (
            [0] => SimpleXMLElement Object
                (
                    [DTSTART] => 20161017T160000Z
                    [DTEND] => 20161017T170000Z
                    [DTSTAMP] => 20161016T182755Z
                    [UID] => 72klt8s5ssrbjp9ofdk8ucovoo@google.com
                    [CREATED] => 20161012T212924Z
                    [DESCRIPTION] => mp4:video1.mp4\,0\,-1
                    [LAST-MODIFIED] => 20161016T164138Z
                    [LOCATION] => teststream
                    [SEQUENCE] => 3
                    [STATUS] => CONFIRMED
                    [SUMMARY] => 11am Broadcast
                    [TRANSP] => OPAQUE
                )

            [1] => SimpleXMLElement Object
                (
                    [DTSTART] => 20161017T170000Z
                    [DTEND] => 20161017T180000Z
                    [DTSTAMP] => 20161016T182755Z
                    [UID] => ac3lgjmjmijj2910au0fnv5vig@google.com
                    [CREATED] => 20161016T164116Z
                    [DESCRIPTION] => mp4:video2.mp4\,0\,1800\nmp4:video3.mp4\,0\,1800
                    [LAST-MODIFIED] => 20161016T164118Z
                    [LOCATION] => teststream
                    [SEQUENCE] => 1
                    [STATUS] => CONFIRMED
                    [SUMMARY] => Noon Broadcast
                    [TRANSP] => OPAQUE
                )

        )

)

So now we need to create another XML object for our schedule and give it the basic structure:

$smilXml = new SimpleXMLElement('<smil/>');
$smilHead = $smilXml->addChild('head');
$smilBody = $smilXml->addChild('body');

Now we need to iterate once through the VEVENT objects to get stream names:

$playonstream = [];

foreach ($xmlObj->VEVENT as $event) {
        $loc = $event->LOCATION;
        $playOnStream["$loc"]=true;
        // We don't really care about the value of this array element, as long as it exists.
        // This way we only get one array element for each unique stream name
}

// Iterate through the list of streams and create them in the SMIL
foreach ($playOnStream as $key => $value) {

$smilStream = $smilBody->addChild('stream');
$smilStream->addAttribute('name',$key);

}

So now we have the beginnings of a schedule:

<?xml version="1.0"?>
<smil>
  <head/>
  <body>
    <stream name="teststream"/>
  </body>
</smil>

We now need to iterate through the list again to add in the fallback items for each stream that starts when the stream starts (this is done as a separate loop to keep the output XML cleaner):

// Add in default fallback entries
foreach ($playOnStream as $key => $value) {
        $defaultPl=$smilBody->addChild('playlist');
        $defaultPl->addAttribute('name',"default-$key");
        $defaultPl->addAttribute('playOnStream',$key);
        $defaultPl->addAttribute('repeat','true');
        $defaultPl->addAttribute('scheduled',"2016-01-01 00:00:01");
        $contentItem = $defaultPl->addChild('video');
        $contentItem->addAttribute('src','mp4:padding.mp4');
        $contentItem->addAttribute('start','0');
        $contentItem->addAttribute('length','-1');

}

Which then gives us these new items:

<smil>
  <head/>
  <body>
    <stream name="teststream"/>
    <playlist name="default-teststream" playOnStream="teststream" repeat="true" scheduled="2016-01-01 00:00:01">
      <video src="mp4:padding.mp4" start="0" length="-1"/>
    </playlist>
  </body>
</smil>

And then we need to iterate again through the VEVENTS to create the actual schedule items:

foreach ($xmlObj->VEVENT as $event) {

        //parse the times into Unix time stamps using the ever-useful strtotime() function;
        $eventStart = strtotime($event->DTSTART);
        $eventEnd = strtotime($event->DTEND);

        //format them into the ISO 8601 format for use in the schedule
        //Note that we're using H:i:s rather than h:i:s because 24-hour time is important here
        $start = date("Y-m-d H:i:s", $eventStart);
        $end = date("Y-m-d H:i:s", $eventEnd);

        //extract summary for playlist name
        $plName = $event->SUMMARY;
        $plLoc = $event->LOCATION;

        //extract description for content
        $description = $event->DESCRIPTION;
        $videos=preg_split('/\\\\n/',$description);
        // add on a padding video at the end of this list
        $videos[]="mp4:padding.mp4";

        //create playlist
        $playlist = $smilBody->addChild('playlist');
        $playlist->addAttribute('name',$plName);
        $playlist->addAttribute('playOnStream',$plLoc);
        $playlist->addAttribute('repeat','false');
        $playlist->addAttribute('scheduled',$start);

        //iterate through playlist items
        foreach($videos as $plItem) {
                echo "$plItem\n";
                $attrs=preg_split('/\\\\,/',$plItem);
                // set defaults for stream start/duration if not specified
                // assume start at beginning and play all the way through
                if(!$attrs[1]) { $attrs[1] = 0; }
                if(!$attrs[2]) { $attrs[2] = -1; }

                $contentItem = $playlist->addChild('video');
                $contentItem->addAttribute('src',$attrs[0]);
                $contentItem->addAttribute('start',$attrs[1]);
                $contentItem->addAttribute('length',$attrs[2]);

        } // end of playlist loop

} // end of event loop

And, finally, we need to add a little bit of code to format the XML object for use with Wowza:

$dom = dom_import_simplexml($smilXml)->ownerDocument;
$dom->formatOutput = true;
$output=$dom->saveXML();
echo "$output\n"; // outputs to STDOUT
$dom->save('streamschedule.smil'); // save to file

For the purposes of this last section, I’ve created some additional events to add a secondary stream:

Schedule Overview

11am Broadcast Event

11am Alternate Broadcast Event

Noon Broadcast Event

Event Broadcast

The iCal looks like this:

BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:Wowza Event Scheduler Calendar
X-WR-TIMEZONE:America/Chicago
X-WR-CALDESC:
BEGIN:VEVENT
DTSTART:20161017T160000Z
DTEND:20161017T170000Z
DTSTAMP:20161016T182604Z
UID:8m4hcp98fmuosuoe48o20drl7k@google.com
CREATED:20161016T180813Z
DESCRIPTION:mp4:video5.mp4\nmp4:video6.mp4
LAST-MODIFIED:20161016T180813Z
LOCATION:altstream
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:11am alternate broadcast
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
DTSTART:20161017T180000Z
DTEND:20161017T190000Z
DTSTAMP:20161016T182604Z
UID:c1ijl48srikc22vkkvrgvpb718@google.com
CREATED:20161016T180725Z
DESCRIPTION:mp4:video4.mp4
LAST-MODIFIED:20161016T180725Z
LOCATION:teststream
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Event
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
DTSTART:20161017T160000Z
DTEND:20161017T170000Z
DTSTAMP:20161016T182604Z
UID:72klt8s5ssrbjp9ofdk8ucovoo@google.com
CREATED:20161012T212924Z
DESCRIPTION:mp4:video1.mp4\,0\,-1
LAST-MODIFIED:20161016T164138Z
LOCATION:teststream
SEQUENCE:3
STATUS:CONFIRMED
SUMMARY:11am Broadcast
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
DTSTART:20161017T170000Z
DTEND:20161017T180000Z
DTSTAMP:20161016T182604Z
UID:ac3lgjmjmijj2910au0fnv5vig@google.com
CREATED:20161016T164116Z
DESCRIPTION:mp4:video2.mp4\,0\,1800\nmp4:video3.mp4\,0\,1800
LAST-MODIFIED:20161016T164118Z
LOCATION:teststream
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:Noon Broadcast
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR

And when we run the process, we get this spiffy code coming out:

<smil>
  <head/>
  <body>
    <stream name="altstream"/>
    <stream name="teststream"/>
    <playlist name="default-altstream" playOnStream="altstream" repeat="true" scheduled="2016-01-01 00:00:01">
      <video src="mp4:padding.mp4" start="0" length="-1"/>
    </playlist>
    <playlist name="default-teststream" playOnStream="teststream" repeat="true" scheduled="2016-01-01 00:00:01">
      <video src="mp4:padding.mp4" start="0" length="-1"/>
    </playlist>
    <playlist name="11am alternate broadcast" playOnStream="altstream" repeat="false" scheduled="2016-10-17 11:00:00">
      <video src="mp4:video5.mp4" start="0" length="-1"/>
      <video src="mp4:video6.mp4" start="0" length="-1"/>
      <video src="mp4:padding.mp4" start="0" length="-1"/>
    </playlist>
    <playlist name="Event" playOnStream="teststream" repeat="false" scheduled="2016-10-17 13:00:00">
      <video src="mp4:video4.mp4" start="0" length="-1"/>
      <video src="mp4:padding.mp4" start="0" length="-1"/>
    </playlist>
    <playlist name="11am Broadcast" playOnStream="teststream" repeat="false" scheduled="2016-10-17 11:00:00">
      <video src="mp4:video1.mp4" start="0" length="-1"/>
      <video src="mp4:padding.mp4" start="0" length="-1"/>
    </playlist>
    <playlist name="Noon Broadcast" playOnStream="teststream" repeat="false" scheduled="2016-10-17 12:00:00">
      <video src="mp4:video2.mp4" start="0" length="1800"/>
      <video src="mp4:video3.mp4" start="0" length="1800"/>
      <video src="mp4:padding.mp4" start="0" length="-1"/>
    </playlist>
  </body>
</smil>

So there you have a relatively simple one-way hack to spit Google Calendar/iCal events out into a Wowza Schedule. You would still need to manually run this every time you wanted to update the broadcast schedule (and reload the Wowza server), and this does not send any confirmation back to your iCal that the event has been scheduled.

Stay tuned for a variation on this code that uses the Google Calendar API (a much more elegant approach)

Instant Replays on Wowza

One of the useful features of Wowza is its ability to record a stream to disk and then be able to use that recording for a replay. In version 3.5, it would simply take the stream name, slap an MP4 extension on the end, and version any previous ones with _0, _1, etc. In 3.6, the default naming scheme for these recordings was a timestamp, with a configuration option to use the legacy naming convention. In Version 4, it appears this legacy naming convention option has disappeared altogether, meaning you can’t set up a player to just play back “streamname.mp4” and it would always grab the most recent one. EDIT: It appears that this loss of functionality was unintentional and has been classified as a bug, which should be fixed very soon.

This became a problem for one of my clients after their Wowza server got updated to V4. It wasn’t practical to re-code the player every week, or to go into the server and manually rename the file. Since it’s on a Windows server, PowerShell to the rescue:

$basepath= "C:\Program Files (x86)\Wowza Media Systems\Wowza Streaming Engine 4.0.3\content\"
$replayfile =  gci $basepath\streamname*.mp4 | sort LastWriteTime | select -last 1
$link = $replayfile.Name

cmd /c del $basepath\replay.mp4
cmd /c mklink $basepath\replay.mp4 $basepath\$link

I then put this into a scheduled task, with time-based triggers. Powershell is a little tricky to get into a scheduled task, but I finally got the syntax right:

Action: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Arguments: -nologo -file “C:\Program Files (x86)\Wowza Media Systems\Wowza Streaming Engine 4.0.3\content\replay.ps1”

If you’re on Linux or OSX, you can do this in bash instead:

#!/bin/bash

basepath='/usr/local/WowzaStreamingEngine/content'
unset -v replayfile
for file in "$basepath"/streamname*.mp4
 do
  [[ -z $replayfile || $file -nt $replayfile ]] && replayfile=$file
 done;
rm -f $basepath/replay.mp4
ln -s $replayfile $basepath/replay.mp4

and put it in your crontab (this example is every sunday at 11:30am)

30 11 * * 0 /bin/bash /usr/local/WowzaMediaServer/content/replay.sh

Browser-Aware Player Code, 2014 Edition

Yet another installment in the never-ending series of dealing with different platforms. This is precipitated by the continued boneheadedness of Google when it comes to supporting any live streaming transports in the native browser (they just simply don’t). Some handset manufacturers are adding HLS support back, though.

You’ll notice there’s also code in here to reference an MPEG-DASH manifest as well as “sanjose” and “smooth” (in Wowza parlance). This script doesn’t make use of those capabilities at the moment, but once I get a good list of which browsers can support MSE and DASH.js, I’ll update it to be able to use that player.

This assumes that you’re using cloud-hosted JW Player with a key, but if you’re not, simply replace the first SCRIPT reference with your locally hosted JWPlayer file and comment out the jwplayer.key line.

If you wish to use a different player such as FlowPlayer, you can replace the appropriate code in the flashPlayer() function.


<HTML>
<HEAD>
<TITLE>Player</TITLE>


<script type="text/javascript" src="http://p.jwpcdn.com/6/8/jwplayer.js?ver=3.9"></script>
<script type="text/javascript">

jwplayer.key='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

jwplayer.defaults = { "ph": 2 };

</script>


</HEAD>
<BODY STYLE="margin: 0px; background-color: white">
<div id="videoframe"></div>

<SCRIPT type="text/javascript">
// Browser-aware video player for JW Player and Wowza
// V0.3 - May 19, 2014
// (c)2014 Ian Beyer
// Released under the GPL

var container='videoframe';
var width="100%";
var aspect="4:3";



//This section calculates actual sizes for player when using non-responsive elements.

var multiplier=aspect.split(":");
var fixedwidth = Math.floor(window.innerWidth*width.split("%")[0]/100);
var fixedheight = Math.floor(window.innerWidth*multiplier[1]/multiplier[0]*width.split("%")[0]/100);
console.log ('Size: '+width+', '+aspect+' ('+fixedwidth+' x '+fixedheight+')');

var streamserver='wowzanode1.tvwmedia.net';
var streamport='1935';
var streamapp='srcEncoders';
var streamname='TVW01';

var streambase=streamserver+':'+streamport+'/'+streamapp+'/_definst_/'+streamname;
var cupertinourl='http://'+streambase+'/playlist.m3u8';
var sanjoseurl='http://'+streambase+'/Manifest.f4m';
var smoothurl='http://'+streambase+'/Manifest';
var dashurl='http://'+streambase+'/manifest.mpd';
var rtmpurl='rtmp://'+streambase;
var rtspurl='rtsp://'+streambase;

var agent=navigator.userAgent.toLowerCase();
var is_iphone = (agent.indexOf('iphone')!=-1);
var is_ipad = (agent.indexOf('ipad')!=-1);
var is_ipod = (agent.indexOf('ipod')!=-1);
var is_safari = (agent.indexOf('safari')!=-1);
var is_iemobile = (agent.indexOf('iemobile')!=-1);
var is_blackberry = (agent.indexOf('blackberry')!=-1);
var is_android1= (agent.indexOf('android\ 1')!=-1);
var is_android2= (agent.indexOf('android\ 2')!=-1);
var is_android3= (agent.indexOf('android\ 3')!=-1);
var is_android4 = (agent.indexOf('android\ 4')!=-1);

var is_chrome = (agent.indexOf('chrome')!=-1);

if (is_iphone) { iosPlayer(); }
else if (is_ipad) { iosPlayer(); }
else if (is_ipod) { iosPlayer(); }
else if (is_android1) { rtspPlayer(); }
else if (is_android2) { rtspPlayer(); }
else if (is_android4) { a4Player(); }
else if (is_blackberry) { rtspPlayer(); }
else if (is_iemobile) { rtspPlayer(); }
else { flashPlayer(); }

function iosPlayer()
{
var player=document.getElementById(container)
player.innerHTML='<VIDEO '+
' SRC="'+cupertinourl+'"'+
' HEIGHT="'+fixedheight+'"'+
' WIDTH="'+fixedwidth+'"'+
' poster="poster.png"'+
' title="Live Stream"'+
' CONTROLS>'+
'</video>';
}

function windowsPlayer()

{

// Need to add code here for silverlight player in Windows Phone to support the 12 users that actually have one. Until then use rtspPlayer();

}

function a4Player()
{
var player=document.getElementById(container)
player.innerHTML='<A HREF="'+cupertinourl+'">'+
'<IMG SRC="player.png" '+
'ALT="Start Mobile Video" '+
'BORDER="1" '+
'WIDTH="'+width+'">'+
'</A>';
}

function dashPlayer()

{

// Reserved for future use

}

function rtspPlayer()
{
var player=document.getElementById(container)
player.innerHTML='<A HREF="'+rtspurl+'">'+
'<IMG SRC="player.png" '+
'ALT="Start Mobile Video" '+
'BORDER="0" '+
'WIDTH="'+width+'">'+
'</A>';
}

function flashPlayer()
{


//If using JW6 Premium or Enterprise, you can also use the cupertinourl here.

jwplayer(container).setup({
 width: width,
 aspectratio: aspect,
 file: rtmpurl,
 autostart: false
 });


}

</SCRIPT>
</BODY>
</HTML>

Multi-Tenant/IP Hosting for Wowza Streaming Engine

For most users, running Wowza Media Server/Wowza Streaming Engine (which I’ll refer to as just “Wowza”) are perfectly content running it out of the box as is on a dedicated server. Where it gets a little more interesting is when you have to co-exist with other server applications that want some of the same ports (I’m lookin’ at YOU, web servers!).

By default, Wowza binds itself to all available IP addresses on ports 1935 (RTMP, but will also take HTTP requests on that port), 8086 (for some basic management functions), 8083 and 8084 (for JMX), and with WSE4, 8087 (REST) and 8088 (WSE Manager). It won’t bind itself to port 80 specifically because of the common problem of co-existing with web servers. If you have Wowza enabled for IPv6, it will also bind to all available IPv6 addresses on the same ports.

This technique is also good for reducing surface area, where you can have remote management such as SSH or RDP listening on one address, and someone scanning your streaming server IP won’t find any open management ports to attempt to exploit.

Read more

HLS distribution with Amazon CloudFront

I’ve blogged extensively about Wowza RTMP distribution with edge/origin and load balancing, but streaming distribution is moving more to HTTP-based systems such as Apple’s HTTP Live Streaming (known inside Wowza as “cupertino”), Adobe’s HTTP Dynamic Streaming (Wowza: “sanjose”), and Microsoft’s Smooth Streaming (Wowza: “smooth”). Future trends suggest a move to MPEG-DASH, which is a standard based on all three proprietary methods (I’ll get into DASH in a future post as the standard coalesces – we’re talking bleeding edge here). The common element in all of them, however, is that they use HTTP as a distribution method, which makes it much easier to leverage CDNs that are geared towards non-live content on HTTP. One of these CDNs is Amazon’s CloudFront service. With edges in 41 locations around the world and 12 cents a gigabyte for transfer (pricing may vary by region), it’s a good way to get into an HTTP CDN without paying a huge amount of money or committing to a big contract with a provider like Akamai.

On the player side, JW Player V6 now supports HLS, and you can do Adobe HDS with the Strobe Media Player.

With the 3.5 release, Wowza Media Server can now act as an HTTP caching origin for any HTTP based CDN, including CloudFront. Doing so is exceedingly simple. First, configure your Wowza server as an HTTP caching origin, and then create a CloudFront distribution (use a “download” type rather than a streaming type – it seems counterintuitive, but trust me on this one!), and then under the origin domain name, put the hostname of your Wowza server. You can leave the rest as defaults, and it will work. It’ll take Amazon a few minutes to provision the distribution, but once it’s ready, you’ll get a URL that looks something like “d1ed7b1ghbj64o.cloudfront.net”. You can verify that the distribution is working by opening a browser to that address, and you should see the Wowza version information. Put that same CloudFront URL in the player URL in place of the Wowza server address, and your players will now start playing from the nearest CloudFront edge cache.

See? Easy.

Browser-Aware Player Code: Episode V, IE Strikes Back

Not so long ago, I updated my browser-aware player code to check for the presence of a stream. Recently, it’s come to light that Internet Explorer 9 doesn’t play nice with this particular snippet, because in IE9, the Javascript engine is rather brain-damaged when it comes to cross-site requests. In order to deal with this properly, we must alter the way we query the server for the presence of a stream:


console.log("Starting stream Check");
if (is_ie9) {
console.log ("Using XDR because IE9 is stupid");
streamcheckXDR();
setInterval(function(){streamcheckXDR()},10000);
}
else {
streamcheck();
setInterval(function(){streamcheck()},10000);
}
function streamcheckXDR() {
 console.log("Starting XDR");
 xdr = new XDomainRequest();
 if(xdr) {
 xdr.onerror = function(){ console.log("XDR Error"); };
 xdr.onload = function(){ startPlayer(xdr.responseText,"XDR"); };
 url = "http://"+streamer+":8086/streamcheck?stream="+stream;
 xdr.open("get",url);
 xdr.send();
 } else {
 console.log("Failed to create XDR object");
 }

}

function startPlayer(result,mode){

// for some inexplicable reason, running "Boolean" on the XDR output doesn't work

// so we have to call the function and tell it if we're dealing with XDR data or AJAX data.

&nbsp;

if(mode == "XDR") {
 if (result === "true") { curstatus = true;}
 if (result === "false") { curstatus = false;}
 } else {
 curstatus = Boolean(result);
 }

//console.log("Result: "+result);
 //console.log("Previous: "+prevstatus);
 //console.log("Current: "+curstatus);
 if (curstatus == prevstatus) {
 //console.log("No Change");
 } else {
 if (curstatus) {
 if (is_iphone || is_ipad || is_ipod) { iOSPlayer("videoframe",plwd,plht,server,stream);}
 else if (is_blackberry) { rtspPlayer("videoframe",plwd,plht,server,stream+'_+240p');}
 else { flashPlayer("videoframe",plwd,plht,server,stream); }
 console.log("Changed from false to true");
 } else {
 var vframe=document.getElementById("videoframe")
 if (is_iphone || is_ipad || is_ipod || is_blackberry) {
 } else {
 jwplayer("videoframe").remove();
 }
 vframe.innerHTML = '<IMG SRC="image.png" WIDTH="'+plwd+'" HEIGHT="'+plht+'">';


 console.log("Changed from true to false");
 }

}
 prevstatus = curstatus;
}

function streamcheck() {
 console.log("Starting AJAX");
 $.ajax({
 dataType: "json",
 contentType: "text/plain",

type: "GET",
 url: "http://"+streamer+":8086/streamcheck?stream="+stream,
 error: function(XMLHttpRequest, textStatus, errorThrown)
 {
 console.log('AJAX Failure:'+ textStatus+':'+errorThrown);
 //some stuff on failure
 },
 success: function(result){startPlayer(result)}
 });
}

Wowza EC2 Capacity Update

It’s been a while since Wowza has updated their EC2 performance numbers (they date back to about 2009), and both Amazon and Wowza have made great improvements to their products. Since I have access to a high-capacity system outside of Amazon’s cloud, I am able to use Wowza’s load test tool on a variety of instance sizes to see how they perform.

The test methodology was as follows:

  • Start up a Wowza instance on EC2 with no startup packages (us-east)
  • Install the server-side piece of Willow (from Solid Thinking Interactive)
  • Configure a 1Mbps stream in Wirecast
  • Monitor the stream in JWPlayer 5 with the Quality Monitor Plugin
  • Configure the Wowza Load Test Tool on one of my Wowza Hotrods located at Softlayer’s Washington DC datacenter
    • Server is 14 hops/2ms from us-east-1
  • Increase the load until:
    • the measured bandwidth on JW player drops below stream bandwidth
    • frame drops get frequent
    • Bandwidth levels out on the Willow Graphs while connection count increases
  • Let it run in that condition for a few minutes

In Willow, it basically looked like this (this was from the m1.small test). You can see ramping up to 100, 150, 200, 250, 275, and 300 streams. The last 3 look very similar because the server maxed out at 250 Mbps. (Yes, the graph says MBytes, that was a bug in Willow which Graeme fixed as soon as I told him about it)

Willow Bandwidth

Meanwhile, this is what happens on the server.. the CPU has maxed out.

EC2 CPU Usage

So that’s the basic methodology. Here are the results:

[table id=1 /]

There are a couple of things to note here. Naturally, if you’re not expecting a huge audience, stick to m1.small. But the best bang for the buck is the c1.medium (High-CPU Medium), which is a relatively new instance type, which gives you 4x the performance of a m1.small at less than 2x the price. The big surprise here was the m2.xlarge. It performs only marginally better than an m1.small at 4x the price.
All the instances that show 950 are effectively giving you the full benefit of the gigabit connection on that server and maxed out the interface long before the CPU maxes out. In the case of the c1.xlarge, there’s lots of CPU to spare for things like transcoding and such if you’re using a BYOL image. If you want to go faster, you’ll need to roll your own Cluster Quad or do a load-balanced set.

Disclaimers: Your mileage may vary, these are just guidelines, although I think they’re pretty close. I have not tested this anywhere but us-east-1, so if you’re using one of amazon’s other datacenters, you may get different results. I hope to test the other zones soon and see how the results compare.