Streaming to multiple simultaneous destinations

Live streaming has been a “thing” for some time. I work with many churches to help them solve their streaming challenges and develop their technology strategy for streaming. One of the most frequent questions I hear is, “can I stream to Facebook Live and still keep my other stream?” Fortunately, this is a lot easier than it used to be. There are variations on this question, but they all boil down to wanting to know how to send one stream to multiple outlets to expand audience reach.

Method 1:

Multiple outputs from your encoder

Several software encoder platforms support multiple outputs. The easiest among these is probably Telestream’s Wirecast software. (The free/open-source Open Broadcast Studio does this as well, but I don’t have much experience with it, and I prefer the Wirecast interface, which is much more polished.) With Wirecast, it’s merely a matter of adding the additional outputs to the various streaming services that are supported. The downside to this approach is that you’ll need more bandwidth, as you are sending the same stream multiple times.

Screenshot 2017-04-16 09.56.42

Method 2:

The Cloud

1. Teradek Core

This is a vendor-specific approach that integrates with Teradek‘s pro-grade encoders (Cube, Bond, Slice, and T-Rax). It provides a single pane of glass that lets you manage your entire fleet of encoder devices (and control/configure them remotely), and then virtually patch the output of those encoders to one or more outputs. You can also use their Live::Air apps for iOS as an input (stay tuned for a post about using Live::Air). If you are using a Bond product, the input is via their Sputnik server, which allows you to spread the stream across multiple connections for extra bandwidth and redundancy, and then it’s reassembled before sending it on to the next step.

In this example, I’m taking an input stream from the Live::Air Solo app on my iPhone, and sending it to Wowza Streaming Cloud, and Facebook Live, all while recording the incoming stream:

Screenshot 2017-04-16 07.10.22

This is a simple drag and drop operation: Drag a source on the left into the workspace, and then drag one or more destinations from the left – this can be:

Teradek decoders (this is great for a multisite church scenario)

Channels (which are external stream destinations):

Screenshot 2017-04-16 09.49.05

Groups (a combination of the above):

Screenshot 2017-04-16 09.50.31

If you click the “Auto” box on the outputs, it will start that output automatically when the stream is available from the input.

When you create stream destinations for social sites, it will authenticate you against that site and keep that authentication.

You can manage a lot of inputs and outputs this way. This example from Teradek’s marketing department shows the scale:

core-management-platform-user-interface_e811873e-7cfb-4514-a465-1467d487d8d7

 

2. Wowza Streaming Engine/Streaming Cloud

Similar to Core, but not tied to a specific vendor, Wowza Streaming Engine provides Stream Targets as of version 4.4 (although the functionality has been in the software since sometime in version 2, as the PushPublish module, Stream Targets integrates it into the UI). Facebook Live support has been an option since almost the very beginning of Facebook Live. YouTube Live support is there, but as a standard RTMP destination.

Similarly, Wowza Streaming Cloud also offers this capability under the “Advanced Menu”:

Screenshot 2017-04-16 10.07.49

From there, you can create a stream target:

Screenshot 2017-04-16 10.08.02

 

Once that target is created, simply go into a transcoder output and add it (you can also create a target directly from there):

Screenshot 2017-04-16 10.12.26

 

As with Core, you can add multiple destinations to a transcoder output – Generally speaking you’ll want to send your best output to places like FB Live, YouTube, etc, as they do their own internal transcoding.

Screenshot 2017-04-16 10.13.31

 

Method 3:

Multiple Encoders

This is the obvious one, but also the least efficient both in terms of hardware and bandwidth. Each encoder goes to its own destination. This generally requires signal distribution amplifiers and other extra hardware.

 

 

Using Bitmovin Player with Church Online Platform

Today’s post will be a brief tutorial on using Bitmovin‘s excellent HTML5 video player with Church Online Platform.

If you’re a church that is wanting to go live, and you haven’t discovered COP, it’s a marvelous product. The fine folks on the life.church Digerati Team (who created the Bible App and made it available on just about every platform known to mankind). It’s a free hosted platform that lets you deliver church online. All you have to do is bring your own streaming provider and provide an embed code. You can use your provider’s player, or you can use your own player. The Digerati team are also a client of mine, and I really enjoy working with them – they’re talented, nerdy, and very good at what they do. (most recently, I helped them build out their Wowza Streaming Engine capability for automating the scheduling and delivery of simulated live events.)

One of my favorite video players out there right now is from Bitmovin, and they provide a CDN-hosted player that provides excellent analytics (complete with API access for the especially nerdy), and usage is free for the first 5000 impressions (and pricing is quite reasonable as you scale up from there). For this reason alone, it’s an excellent choice for churches getting started with streaming. Its other major benefit is that because it is written in HTML5 and Javascript, it will work on just about anything you can throw at it (for the really archaic devices, it still has a Flash component). It also is designed from the ground up to support the new MPEG-DASH standard, but if you’re using a streaming CDN or service that doesn’t provide DASH, no big deal, as the player also supports HLS, even for Flash delivery for those 3 devices that still haven’t discovered modern streaming technology or are running a particularly ancient version of Android. Added bonus, BitMovin’s player also supports VR and 360 streaming (as does Wowza Streaming Cloud).

For starters, you’ll need to sign up for an account, which will give you player information. One thing you’ll want to make sure you do is add your churchonline.org domain to the allowed domains for your license key. This is under Player/Overview:

Bitmovin Player Domain Config

If you forget to do this, the player will simply show an error telling you you need to do it.  This keeps someone from using your player key on their site, so be sure to use yourdomain.churchonline.org, not just churchonline.org.

To put this in your COP page, go to the event where you wish to use the player, and go to the Video tab:

ChurchOnline Event Settings

When you go to the Embed menu, you will see code to put it on the page (under Default video embed code). This is a little more involved than your standard embed code.

Bitmovin Embed Controls

A couple of key things to note here with regards to COP:

  1. In order to put the <script> stuff in your <head> section, you’d need to create a custom theme in COP. This is not necessary (in fact, putting that script statement in the head that way doesn’t work). What you’ll need to do is simply put the <script> piece just above the rest of it in the default embed code section.
  2. You’ll need to edit the source section in that code. If all you’re doing is HLS, you can remove the dash and progressive entries. Leave the HLS entry in place and put in the HLS URL provided by your streaming platform. In the case of Wowza Streaming Cloud, this is located at the bottom of the Overview tab of your streaming application under “Playback URLs”.
  3. The “poster” entry is the image the player shows when you’re not streaming any video.

So, for my test stream, the embed code looks like this:


<script type="text/javascript" src="https://bitmovin-a.akamaihd.net/bitmovin-player/stable/7/bitmovinplayer.js"></script>

<div id="player"></div>
<script type="text/javascript">
var conf = {
key: "d8XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX2e",
source: {
hls: "http://wowzaprod103-i.akamaihd.net/hls/live/######/########/playlist.m3u8",
poster: "http://dontfenceme.in/wp-content/uploads/2013/09/g-global-background.jpg"
}
};
var player = bitmovin.player("player");
player.setup(conf).then(function(value) {
// Success
console.log("Successfully created bitmovin player instance");
}, function(reason) {
// Error!
console.log("Error while creating bitmovin player instance");
});
</script>

The console.log lines aren’t necessary, but potentially useful when trying to debug why it can’t instantiate the player.

If you want to run a separate video when the doors aren’t open, put that under the offline video embed code section. You can leave the mobile and low sections empty, as your stream is probably already adaptive from your streaming provider.

Save it, and this is what you get:

BitMovin in ChurchOnline Platform

In order to remove the Bitmovin logo, edit the theme’s CSS and add the following lines:


/* Remove Bitmovin Logo on player */
.bmpui-ui-watermark {
display: none;
}

ChurchOnline Platform CSS Edit

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.

 

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 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.