Attention Minutes

by zanes

HTML

<script src="http://cdnjs.cloudflare.com/ajax/libs/bacon.js/0.6.19/Bacon.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/lodash.js/2.2.1/lodash.compat.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="http:////netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
<script src="http://www.youtube.com/player_api"></script>

<div>
  <span id="seconds"></span>
  <span id="label"></span>
</div>
<br />

<iframe id="QPKKQnijnsM" type="text/html" width="128" height="78" src="http://www.youtube.com/embed/QPKKQnijnsM?enablejsapi=1" frameborder="0">
</iframe>

<iframe id="pCkL9UlmCOE" type="text/html" width="128" height="78" src="http://www.youtube.com/embed/pCkL9UlmCOE?enablejsapi=1" frameborder="0">
</iframe>

<iframe id="buRLc2eWGPQ" type="text/html" width="128" height="78" src="http://www.youtube.com/embed/buRLc2eWGPQ?enablejsapi=1" frameborder="0">
</iframe>

JavaScript

/* global $, _, Bacon, YT */
function merge(stream1, stream2) {
  return stream1.merge(stream2);
}

function eventStream(eventName) {
  return $(window).asEventStream(eventName);
}

// A map to player state codes from their human-readable sting equivalents.
// https://developers.google.com/youtube/js_api_reference
var PLAYER_CODES = {
  beginning: -1,
  completed: 0,
  play: 1,
  pause: 2,
  buffer: 3,
  video: 5
};

var DECAY = 5000; // milliseconds
var EVENT_NAMES = ["focus", "click", "scroll", "mousemove", "touchstart", "touchend", "touchcancel", "touchleave", "touchmove"];

var playerEventStream = new Bacon.Bus();

var isPlaying = playerEventStream
    .scan({}, function(states, event) {
      var update = {};
      var id = event.target.getIframe().id;
      update[id] = event.data;
      return _.assign(states, update);
    })
    .map(_.values)
    .map(_.partialRight(_.contains, PLAYER_CODES.play));

var playerEvents = function(id) {
  var events = new Bacon.Bus();
  var player = new YT.Player(id, {
    events: {
      'onStateChange': function(event) {
        playerEventStream.push(event);
      }
    }
  });
  return events;
};

var isFocused = eventStream("focus").map(true)
    .merge(eventStream("blur").map(false))
    .toProperty(true);

var streams = _.map(EVENT_NAMES, eventStream);
var interactionStream = _.reduce(streams, merge);
var activityStream = interactionStream.merge(playerEventStream);

var recentlyActive = activityStream
    .map(true)
    .flatMapLatest(function() {
      return Bacon.once(true).merge(Bacon.once(false).delay(DECAY));
    })
    .toProperty(false);

var isActive = (recentlyActive.and(isFocused)).or(isPlaying);

var secondsActive = Bacon.mergeAll(isActive.changes(), isActive.sample(1000))
  .map(function(isActive) {
    return {
      isActive: isActive,
      timestamp: new Date().getTime()
    };
  })
  .slidingWindow(2,2)
  .filter(function(span) { return span[0].isActive; })
  .map(function(span) { return...