AWS signed URLs on demand · Flowplayer

Now playing:

resolved video.url will be displayed here

Flowplayer requires a valid video URL at load time. However, the AWS signed URL must be generated and is not available in time. As Github pages do not allow PHP the delay is even more pronunced as we have to make an Ajax call to a remote origin. Therefore the player must be install asynchronously.

To highlight the challenge we install Flowplayer in a splash setup. As we cannot install the player on page load we mimick Flowplayer's CSS directives to implement a placeholder which deals with flexible dimensions as Flowplayer does with the player container.

The player is configured to go back into splash state on finish. This time the clip must be loaded asynchronously with a fresh signed URL as the URLs returned have an expiry time of only 5 seconds. We disable the API in splash state to make a click on the player area retrieve the new URL first.

To save time consuming Ajax calls we also mimick Flowplayer's video format picking.

<head/>

<style>

/* normal splash preparation */
#player {
   background: #777 url(http://flowplayer.org/media/img/demos/minimalist.jpg) no-repeat;
   background-size: contain;
}

/* placeholder splash */
#player.splash {
   position: relative;
   width: 100%;
   cursor: pointer;
}
/* placeholder ratio */
#player.splash .ratio {
   /* aspect ratio 12/5 -> placeholder ratio 5/12 */
   padding-top: 41.67%
}
/* placeholder play button */
#player.splash .playbutton {
   position: absolute;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background: url(//releases.flowplayer.org/5.3.2/skin/img/play_white.png) center no-repeat;
   background-size: 12%;
}

<script>

// global configuration
flowplayer.conf = {
  splash: true,
  ratio: 5/12
};

// global api setup
flowplayer(function (api) {
  api.bind("ready", function (e, api, video) {
    // show video info, for demo only
    var expiry = new Date(video.src.replace(/.*Expires=(\d+).*/, "$1") * 1000);
    $(".expires").text(" (expires at " + expiry.toLocaleTimeString() + "):");
    $(".signed pre").text(video.url);
  }).bind("finish", function (e, api) {
    // before going into splash state
    // prevent click on container from reloading same url immediately
    api.disable(true);
    api.unload();
  });
});

$(function () {
  var video = $("<video/>"),
      globaltype = "flash", // fallback to flash
      globalapi;

  // determine video type so we need to retrieve only 1 source via ajax
  // as 3 ajax calls would be very slow and expensive
  if (flowplayer.support.video) {
    $(["mp4", "webm", "ogg"]).each(function (i, type) {
      if (!!video[0].canPlayType("video/" + type).replace("no", "")) {
        globaltype = type;
        return false;
      }
    });
  }

  $("#player").click(function () {

    // retrieve url only in (placeholder) splash state
    if (globalapi === undefined || globalapi.splash) {

      if (globalapi === undefined) {
        // fade out placeholder play button to bridge first load period
        $("#player .playbutton").fadeOut(500);
      }

      $.ajax({
        // github does not offer php, therefore remote origin
        url: "http://flowplayer.blacktrash.org/getsignedurl.php",
        data: {
          "movie": "bauhaus/624x260." + (globaltype !== "flash" ?
              (globaltype !== "ogg" ? globaltype : "ogv") : "mp4")
        },
        dataType: "text",

        success: function (data) {

          if (globalapi === undefined) {

            // insert video tag with the src
            video.append($("<source/>").attr({
              type: "video/" + globaltype,
              src: data
            }));

            // replace placeholder with video tag
            $("#player .playbutton, #player .ratio").replaceWith(video);

            // install the player
            $("#player").removeClass("splash").flowplayer();

            // grab global api handle
            globalapi = $("#player").data("flowplayer");
            globalapi.load();

          } else {
            // player is already installed, only load clip with new url

            globalapi.disable(false);

            // circumvent issue #172:
            // load() does not handle a string argument containing a query
            // switch needed as variable values cannot be used as properties
            switch (globaltype) {
              case "webm":
                globalapi.load([{webm: data}]);
                break;
              case "ogg":
                globalapi.load([{ogg: data}]);
                break;
              case "flash":
                globalapi.load([{flash: data}]);
                break;
              default:
                globalapi.load([{mp4: data}]);
            }
          }
        }
      });
    }
  });
});

<body/>

<div id="player" class="splash">
   <div class="ratio"></div>
   <div class="playbutton"></div>
</div>

getsignedurl.php

<?php
header('Access-Control-Allow-Origin: *');

$resource = 'http://d2yz3vc7rxs49u.cloudfront.net/' . $_GET['movie'];
// default expiry after 5 seconds
$expires = time() + (isset($_GET['expires']) ? $_GET['expires'] : 5);

// key pair generated for cloudfront
$keyPairId = 'APKAJJZ2ZVCTME3I4VSQ';

$json = '{"Statement":[{"Resource":"' . $resource . '","Condition":{"DateLessThan":{"AWS:EpochTime":' . $expires . '}}}]}';

// read cloudfront private key pair
$fp = fopen('/home/user/awskeys/pk-' . $keyPairId . '.pem', 'r');
$priv_key = fread($fp, 8192);
fclose($fp);

// create the private key
$key = openssl_get_privatekey($priv_key);

// sign the policy with the private key
// depending on your php version you might have to use
// openssl_sign($json, $signed_policy, $key, OPENSSL_ALGO_SHA1)
openssl_sign($json, $signed_policy, $key);

openssl_free_key($key);

// create url safe signed policy
$base64_signed_policy = base64_encode($signed_policy);
$signature = str_replace(array('+', '=', '/'), array('-', '_', '~'), $base64_signed_policy);

// construct the url
$url = $resource . '?Expires=' . $expires . '&Signature=' . $signature . '&Key-Pair-Id=' . $keyPairId;

echo $url;
?>