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>

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)}
 });
}

Browser-aware player code, revisited again

It’s the code snippet that just won’t go away. I’ve updated the code for some additional functionality. This version takes server, port, and stream parameters via the URL, parses them in javascript, and then queries a streamcheck HTTPProvider on the server to see if a stream by that name is currently published. If it is, it will load the player, otherwise load a message, and check periodically to see if the stream is published, and load the player if the state changes to true, and unload it if it changes to false, returning to the message. The player is designed to scale to fit whatever window it’s in, so make an IFRAME of whatever size you want the player, and you’re off and running

<IFRAME SRC="player.html?streamer=wowza.nhicdn.net&port=1935&app=live&stream=livestream" WIDTH="640" HEIGHT="360" SCROLLING="NO" />


Without further ado, here’s the code:



<body style="margin: 0px; background: black; color: white; font-family: sans-serif;">
<div id="videoframe" style="text-align: center;font-size: 14;">The video stream is currently offline. Playback will resume as soon as a stream is available.</div>
<script type="text/javascript" src="/assets/jw5/jwplayer.js"></script> 
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script type='text/javascript' src='https://www.google.com/jsapi'></script>


<SCRIPT type="text/javascript">
// Browser-aware video player for JW Player and Wowza
 

plwd=self.innerWidth;
plht=self.innerHeight;
// var debugwindow=document.getElementById("debug")
// debugwindow.innerHTML='<P>Dimensions: '+plwd+'x'+plht+'</P>';
var streamer=getUrlVars()["streamer"];
var app=getUrlVars()["app"];
var port=getUrlVars()["port"];
var stream=getUrlVars()["stream"];
var server=streamer+':'+port+'/'+app;


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_playstation = (agent.indexOf('playstation')!=-1);
var is_safari = (agent.indexOf('safari')!=-1);
var is_blackberry = (agent.indexOf('blackberry')!=-1);
var is_android = (agent.indexOf('android')!=-1);
var streamstatus = false;
var prevstatus = false;
var curstatus = false;

streamcheck();
setInterval(function(){streamcheck()},10000);

function streamcheck() {
            $.ajax({
              type: "GET",
              url: "http://"+streamer+":8086/streamcheck?stream="+stream,
             dataType: "json",
             success: function(result){
		curstatus = Boolean(result);
		//if (result === "true") { curstatus = true;}
		//if (result === "false") { curstatus = false;}
		if (curstatus == prevstatus) {
		} 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);}
			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 = 'The video stream is currently offline. Playback will resume as soon as a stream is available.';
			
			
			console.log("Changed from true to false");
		}

		}		
			prevstatus = curstatus;
		}
           });
}

 
function getUrlVars() {
    var vars = {};
    var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
        vars[key] = value;
    });
    return vars;
}

function iOSPlayer(container,width,height,server,stream)
{
var player=document.getElementById(container)
player.innerHTML='<VIDEO controls '+
'HEIGHT="'+height+'" '+
'WIDTH="'+width+'" '+
'title="Live Stream">'+
'<SOURCE SRC="http://'+server+'/'+stream+'/playlist.m3u8"> '+
'</video>';
}
 
function rtspPlayer(container,width,height,server,stream)
{
var player=document.getElementById(container)
player.innerHTML='<A HREF="rtsp://'+server+'/'+stream+'">'+
'<IMG SRC="poster-play.png" '+
'ALT="Start Mobile Video" '+
'BORDER="0" '+
'HEIGHT="'+height+'" '+
'WIDTH="'+width+'">'+
'</A>';
}
 
function flashPlayer(container,wide,high,server,stream)
{
jwplayer(container).setup({
		height: high,
		width: wide,
		streamer: 'rtmp://'+server,
		file: stream,
		autostart: true,
		stretching: 'uniform'
		});
	
}
 
</SCRIPT>
</BODY>

The code for the streamcheck module is as follows:

package net.nerdherd.wms.http;

import java.io.*;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.wowza.wms.application.IApplicationInstance;
import com.wowza.wms.http.*;
import com.wowza.wms.logging.*;
import com.wowza.wms.vhost.*;

public class StreamCheck extends HTTProvider2Base {

    	public void onHTTPRequest(IVHost vhost, IHTTPRequest req, IHTTPResponse resp) {
		StringBuffer report = new StringBuffer();
		StringBuffer streamlist = new StringBuffer();
		if (!doHTTPAuthentication(vhost, req, resp))
			return;
		
		Map<String, List<String>> params = req.getParameterMap();
		
		String stream = "";
		boolean status = false;
		boolean listing = false;
				
		if (req.getMethod().equalsIgnoreCase("post")) {
			req.parseBodyForParams(true);
		}
		
		if (params.containsKey("stream"))
			stream = params.get("stream").get(0);
				
		try
		{
				if (vhost != null)
				{
					
					List<String> appNames = vhost.getApplicationNames();
					Iterator<String> appNameIterator = appNames.iterator();
					while (appNameIterator.hasNext())
					{
						try {
	                        String Name = appNameIterator.next();

	                        IApplicationInstance NowApp = vhost.getApplication(Name).getAppInstance("_definst_");
	                        List<String> PublishedNames = NowApp.getPublishStreamNames();
	                        Iterator<String> ThisPublished = PublishedNames.iterator();
	                        if ( PublishedNames.size()>0 )
	                                {
	                                while ( ThisPublished.hasNext() )
	                                        {
	                                        try {
	                                                String NowPublished = ThisPublished.next();
	                                                
	                                                	if (NowPublished.equals(stream)){
	                                                		status = true;
	                                                	}
	                                                	
	                                                 } catch (Exception e) {}
	                                        }
	                                
	                                }
	                        
	                        } catch (Exception e) {report.append(e.toString()); } 
	                        }					
					}
						
								
		}
		catch (Exception e)
		{
			WMSLoggerFactory.getLogger(HTTPServerVersion.class).error("StreamCheck: " + e.toString());
			e.printStackTrace();
		}

		if (!listing) { 
			if (status){
				report.append("true");
		    } else {
		    	report.append("false");
		    }
		}
		
		try {
			resp.setHeader("Content-Type", "text/plain");
			resp.setHeader("Access-Control-Allow-Origin","*");
			OutputStream out = resp.getOutputStream();
			byte[] outBytes = report.toString().getBytes();
			out.write(outBytes);
		} catch (Exception e) {
			WMSLoggerFactory.getLogger(null).error(
					"MediaCasterHTTP: " + e.toString());
		}
		
	
	}
}