Adaptive Bitrate Streaming with DVR using Wowza Streaming Engine and OSMF Grind Player

Sat, Jun 14, 2014 5-minute read

A short article explaining how to configure Wowza Media Server and produce a live video stream with adaptive bit rate.

Today I spend some time playing with Wowza Streaming Engine 4 (previously called Wowza Media Server), and configured it for live adaptive bitrate streaming with DVR.

In order to do this, you need the following tools:

  1. Web server hosting a video player on a webcast page

  2. Wowza Streaming Engine 4 installed on your media streaming server (a 64 bit Linux or Windows machine, since the Transcoder addon is not available on OS X)

Media server provision

Since I work on OS X, I used Vagrant with VirtualBox to build an Ubuntu Precise 64 bit virtual machine for the media streaming server. I configured Vagrant to use a private network, so that all ports from Wowza would be accessible from the host machine (8088, 1935, etc.)

# Vagrantfile
...
config.vm.box = "precise64"
config.vm.network "private_network", ip: "192.168.50.14"
config.vm.synced_folder ".", "/vagrant", type: "nfs"
...

Video player

For a video player, I decided to try the OSMF Grind Player. Although there are a few forks available on Github, I used the original version. OSMF Grind Player is a Flash-based video player, something that you need to account for when designing a webcast page viewable on mobile devices (iOS and Android). Unfortunately the state of HTML5 is still not production ready for live video streaming on a desktop browser, so I sticked to a Flash-based player.

Configuring Wowza Streaming Engine

Wowza Streaming Engine 4 provides a browser-based administrative panel, available at http://vagrant-box-ip:8088/enginemanager. Once you are logged in, the first step is to enable the DVR and Transcoder addons and then create a DVR enabled application. This configures your /conf/application-name/Application.xml. The generated file should look like:

<Application>
  <Name>anton</Name>
  <AppType>Live</AppType>
...
  <Streams>
    <StreamType>live-record</StreamType>
    <!-- location of live stream recordings -->
    <StorageDir>${com.wowza.wms.context.VHostConfigHome}/content/anton</StorageDir>
    <KeyDir>${com.wowza.wms.context.VHostConfigHome}/keys</KeyDir>
    <!-- support for HDS, HLS, DVR, MPEG-DASH, etc. -->
    <LiveStreamPacketizers>cupertinostreamingpacketizer, dvrstreamingpacketizer, mpegdashstreamingpacketizer, sanjosestreamingpacketizer, smoothstreamingpacketizer</LiveStreamPacketizers>
    <Properties>
    </Properties>
  </Streams>
...
  <Transcoder>
    <LiveStreamTranscoder>transcoder</LiveStreamTranscoder>
    <!-- location of transrating profile -->
    <Templates>${SourceStreamName}.xml,transrate.xml</Templates>
    <ProfileDir>${com.wowza.wms.context.VHostConfigHome}/transcoder/profiles</ProfileDir>
    <TemplateDir>${com.wowza.wms.context.VHostConfigHome}/transcoder/templates</TemplateDir>
    <Properties>
    </Properties>
  </Transcoder>
...
  <DVR>
    <Recorders>dvrrecorder</Recorders>
    <Store>dvrfilestorage</Store>
    <WindowDuration>0</WindowDuration>
    <!-- location of saved DVR chunks -->
    <StorageDir>${com.wowza.wms.context.VHostConfigHome}/dvr</StorageDir>
    <ArchiveStrategy>append</ArchiveStrategy>
    <Properties>
    </Properties>
  </DVR>
...
</Application>

The Application.xml specifies all the important configurations and locations, in case you want to backup your streaming server configuration and use it on another server. For example:

  1. Type of generated live streams - you publish one incoming stream (for example via RTMP live video encoder), and Wowza produces a variety of outgoing streams like RTMP (for Flash-enabled browsers), HLS (for iOS and Android), HDS, RTSP (for Android), etc.

  2. DVR configuration and storage location - by default in the /dvr folder.

  3. Transrate profile - by default inside the /trancoders/templates/transrate.xml file, which looks like:

<Transcode>
  <Description>Default transrate.xml file</Description>
  <Encodes>
    <!-- Encode block for source stream. -->
    <Encode>
      <Enable>true</Enable>
      <Name>source</Name>
      <StreamName>mp4:${SourceStreamName}_source</StreamName>
      <Video>
        <Codec>PassThru</Codec>
        <Bitrate>${SourceVideoBitrate}</Bitrate>
      </Video>
      <Audio>
        <Codec>PassThru</Codec>
        <Bitrate>${SourceAudioBitrate}</Bitrate>
      </Audio>
    </Encode>

    <!-- Setup for 360p, high bandwith, main profile for desktop -->
    <Encode>
      <Enable>true</Enable>
      <Name>360p</Name>
      <StreamName>mp4:${SourceStreamName}_360p</StreamName>
      <Video>
        <Codec>H.264</Codec>
        <Implementation>default</Implementation>
        <GPUID>-1</GPUID>
        <FrameSize>
          <FitMode>stretch</FitMode>
          <Width>640</Width>
          <Height>360</Height>
          <Crop>0,0,0,0</Crop>
        </FrameSize>
        <Profile>main</Profile>
        <Bitrate>850000</Bitrate>
        <KeyFrameInterval>
          <FollowSource>true</FollowSource>
          <Interval>0</Interval>
        </KeyFrameInterval>
      </Video>
      <Audio>
        <Codec>PassThru</Codec>
        <Bitrate>${SourceAudioBitrate}</Bitrate>
      </Audio>
    </Encode>

    <!-- Setup for 160p, low bandwith, baseline profile for mobile devices -->
    <Encode>
      <Enable>true</Enable>
      <Name>160p</Name>
      <StreamName>mp4:${SourceStreamName}_160p</StreamName>
      <Video>
        <Codec>H.264</Codec>
        <Implementation>default</Implementation>
        <GPUID>-1</GPUID>
        <FrameSize>
          <FitMode>stretch</FitMode>
          <Width>284</Width>
          <Height>160</Height>
          <Crop>0,0,0,0</Crop>
        </FrameSize>
        <Profile>baseline</Profile>
        <Bitrate>200000</Bitrate>
        <KeyFrameInterval>
          <FollowSource>true</FollowSource>
          <Interval>0</Interval>
        </KeyFrameInterval>
      </Video>
      <Audio>
        <Codec>PassThru</Codec>
        <Bitrate>${SourceAudioBitrate}</Bitrate>
      </Audio>
    </Encode>
...
  </Encodes>
</Transcode>

Using this setup, I used Telestream’s Wirecast Pro to publish a live 720p ; 1280x720 ; 2mbps live stream, which was transrated live by Wowza to 360p and 160p for viewers with slow internet connection and viewers on mobile devices.

At this stage you have 3 incoming streams inside Wowza:

  1. stream-name_source (original 720p - 1280x720)

  2. stream-name_360p (transrated 360p - 640x360)

  3. stream-name_160p (transrated 160p - 284x160)

The next step is to generate a SMIL file and configure the video player. The SMIL file can be generated from the administrative panel of Wowza Streaming Engine (engine manager) or manually inside you application storage folder as configured in Application.xml, for example /content/application-name/stream-name.smil. If the file is placed at a wrong location, you would get an error #2032. The SMIL file looks like:

<smil>
  <body>
    <switch>
      <video width="1280" height="720" src="stream-name_source">
        <param name="videoBitrate" value="2000000" valuetype="data"></param>
      </video>
      <video width="640" height="360" src="stream-name_360p">
        <param name="videoBitrate" value="850000" valuetype="data"></param>
      </video>
      <video width="284" height="160" src="stream-name_160p">
        <param name="videoBitrate" value="200000" valuetype="data"></param>
      </video>
    </switch>
  </body>
</smil>

Configuring the webcast page with the video player

The last step is to provide the SMIL file to the OSMF grind player on your webcast page, which could be done with a simple JavaScript:

  SMIL_URL = "http://vagrant-ip:1935/application-name/smil:stream-name.smil/manifest.f4m?DVR"

  var pqs = new ParsedQueryString();
  var parameterNames = pqs.params(false);
  var parameters = {
      src: SMIL_URL,
      autoPlay: "true",
      streamType: "dvr",
      verbose: false,
      controlBarAutoHide: "true",
      controlBarPosition: "bottom"
  };

  for (var i = 0; i < parameterNames.length; i++) {
      var parameterName = parameterNames[i];
      parameters[parameterName] = pqs.param(parameterName) ||
      parameters[parameterName];
  }

  var wmodeValue = "direct";
  var wmodeOptions = ["direct", "opaque", "transparent", "window"];
  if (parameters.hasOwnProperty("wmode"))
  {
    if (wmodeOptions.indexOf(parameters.wmode) >= 0)
    {
      wmodeValue = parameters.wmode;
    }
    delete parameters.wmode;
  }

  // Embed the player SWF:
  swfobject.embedSWF(
    "/assets/GrindPlayer.swf"
    , "GrindPlayer"
    , 640
    , 360
    , "10.1.0"
    , "/assets/expressInstall.swf"
    , parameters
    , {
          allowFullScreen: "true",
          wmode: wmodeValue
      }
    , {
          name: "GrindPlayer"
      }
  );
}

Mobile devices support

Obviously the above JavaScript would embed the GrindPlayer SWF inside the given DOM container, which would only work for a Flash-enabled browser. In order to support mobile devices, I generally include Bootstrap (or other frontend framework) directives visible-desktop and hidden-desktop which shows/hides the respective container based on viewers device.

<!-- for flash enabled desktop browsers -->
<div class="visible-desktop" id="GrindPlayer">
  Flash player is to load here. Make sure you have Adobe Flash Player installed.
</div>

<!-- for mobile devices -->
<div class="hidden-desktop">
  <video controls src="http://vagrant-ip:1935/application-name/stream-name_160p/playlist.m3u8">
</div>

Conclusion

Following this workflow you can produce an adaptive bitrate live streaming of your event. Desktop users with good connections can enjoy 720p HD quality, whereas viewers with worse internet connection or using a mobile device could watch a lower quality stream.