380 lines
14 KiB
JavaScript
380 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
jQuery plugin to embed video from a pan.do/ra instance
|
|
Usage:
|
|
html:
|
|
<div id="video" data-pandora-id="AA"></div>
|
|
js:
|
|
$('#video').pandoravideo();
|
|
|
|
Options can be passed as data- attributes in the html or in an options object - read code for docs
|
|
*/
|
|
|
|
(function($) {
|
|
$.fn.pandoravideo = function(options) {
|
|
|
|
//get options, giving preference, in order, to data- attributes defined on the html element, options passed when instantiatiing $(element).pandoravideo(options), defaults.
|
|
var options = options || {},
|
|
namespace = options.namespace || "pandora",
|
|
|
|
optionTypes = {
|
|
'strings': ['api', 'base', 'resolution', 'action', 'id'],
|
|
'integers': ['width', 'interval'],
|
|
'floats': ['in', 'out'],
|
|
'arrays': ['layers', 'keys'],
|
|
'booleans': []
|
|
//'functions': ['callback'] FIXME: lets not.
|
|
};
|
|
|
|
|
|
return this.each(function() {
|
|
|
|
var $this = $(this),
|
|
dataOptions = $.extend(options, $this.getDataOptions(optionTypes, namespace)),
|
|
opts = $.extend({
|
|
'id': 'ABC', //FIXME: throw an error if id is undefined at this point
|
|
'layers': ['transcripts', 'descriptions', 'keywords', 'places'],
|
|
'keys': ['layers'], //data keys to fetch from API call. FIXME: add more apt keys
|
|
'api': "//pad.ma/api/", //pandora instance api end-point - see http://pad.ma/api
|
|
'in': 0, //in point (float, in seconds) of clip
|
|
'out': 0, //out point of clip
|
|
'pandora_base': 'pad.ma/', //pandora instance from where to fetch video and image data
|
|
'resolution': '480p', //resolution of video (96p, 240p, or 480p)
|
|
'width': '640', //display (css) width
|
|
'interval': 300, //interval (in ms) to run updatePlayer polling loop
|
|
'action': 'get', //action POST param to send to url
|
|
'callback': function() { $.noop(); } //function to call after done instantiating pandoraVideo object, called with the object.
|
|
}, dataOptions),
|
|
id = opts.id,
|
|
$loading = $('<div />').addClass("pandoraLoading").text("Loading video...").appendTo($this),
|
|
sendData = JSON.stringify({'id': id, 'keys': opts.keys});
|
|
|
|
//get the pandora id and instantiate a pandoraVideo object with the current element and render it and execute optional callback
|
|
var deferred = $.post(opts.api, {'action': opts.action, 'data': sendData}, function(response) {
|
|
$loading.hide().remove();
|
|
var pandora = new PandoraVideo(id, response.data, $this, opts);
|
|
pandora.render();
|
|
opts.callback(pandora);
|
|
}, "json");
|
|
|
|
deferred.error(function(data) {
|
|
alert("failed to load video data");
|
|
});
|
|
});
|
|
};
|
|
/*
|
|
pandoraVideo class
|
|
Parameters:
|
|
id: <string> pandora video id
|
|
data: <object> data for the video object
|
|
$el: <jQuery element>
|
|
opts: <object> options object
|
|
|
|
*/
|
|
var PandoraVideo = function(id, data, $el, opts) {
|
|
var that = this;
|
|
this.id = id;
|
|
this.data = data;
|
|
this.$el = $el;
|
|
this.o = opts;
|
|
this.annotPoint = -1;
|
|
this.getVideoURL = function() {
|
|
var rand = parseInt(Math.random() * 10000);
|
|
return "//video" + rand + "." + opts.pandora_base + id + "/" + opts.resolution + ".webm";
|
|
};
|
|
|
|
//empties element, appends video widget
|
|
this.render = function() {
|
|
var that = this;
|
|
this.$el.empty();
|
|
this.$el.append(that.getWidget());
|
|
};
|
|
|
|
/*
|
|
Get points
|
|
*/
|
|
var flattenedPoints = [];
|
|
$.each(that.o.layers, function(i,layerType) {
|
|
$.each(that.data.layers[layerType], function(j,layer) {
|
|
flattenedPoints.push(layer['in']);
|
|
flattenedPoints.push(layer.out);
|
|
});
|
|
});
|
|
this.points = makeArrayUnique(flattenedPoints);
|
|
|
|
|
|
this.crossesPoint = function(newPos) {
|
|
var that = this;
|
|
var positions = [that.annotPoint, newPos].sort();
|
|
return this.points.some(function(point) {
|
|
return point >= positions[0] && point <= positions[1];
|
|
});
|
|
};
|
|
|
|
function makeArrayUnique(arr) {
|
|
var o = {}, i, l = arr.length, r = [];
|
|
for(i=0; i<l;i+=1) o[arr[i]] = arr[i];
|
|
for(i in o) r.push(o[i]);
|
|
return r;
|
|
};
|
|
|
|
/*
|
|
Use this to set options on the player from the outside.
|
|
Example:
|
|
pandoraVideoObject.setOption("width", 250);
|
|
*/
|
|
this.setOption = function(key, val) {
|
|
var that = this;
|
|
this.o[key] = val;
|
|
if ($.inArray(key, ['resolution']) != -1) {
|
|
this.destroy();
|
|
this.render();
|
|
return this;
|
|
}
|
|
if ($.inArray(key, ['in', 'out', 'layers']) != -1) {
|
|
this.updatePlayer();
|
|
return this;
|
|
}
|
|
if ($.inArray(key, ['width']) != -1) {
|
|
this.$video.animate({'width': that.o.width + "px"});
|
|
return this;
|
|
}
|
|
console.log("attempt to set invalid option or option which will make no difference to player state");
|
|
};
|
|
|
|
|
|
/*
|
|
Returns currently defined option for key specified
|
|
Parameters:
|
|
key: <string> eg. 'width'
|
|
*/
|
|
this.getOption = function(key) {
|
|
return this.o[key] || "invalid option";
|
|
};
|
|
|
|
|
|
//returns <jQuery element> widget for this pandoraVideo object
|
|
this.getWidget = function() {
|
|
var that = this;
|
|
var $container = this.$container = $('<div />').addClass("pandoraContainer");
|
|
var $video = this.$video = $('<video />')
|
|
.appendTo($container)
|
|
.attr("src", that.getVideoURL())
|
|
.attr("controls", "controls")
|
|
.addClass("pandoraVideo")
|
|
.animate({'width': that.o.width})
|
|
.load()
|
|
.bind("loadedmetadata", function() {
|
|
this.currentTime = that.o['in'];
|
|
that.updatePlayer();
|
|
|
|
})
|
|
.bind("play", function() {
|
|
that.interval = setInterval(function() {
|
|
that.updatePlayer();
|
|
}, that.o.interval)
|
|
})
|
|
.bind("pause", function() {
|
|
clearInterval(that.interval);
|
|
})
|
|
.bind("ended", function() {
|
|
clearInterval(that.interval);
|
|
})
|
|
.bind("seeked", function() {
|
|
that.updatePlayer();
|
|
});
|
|
var $annotations = this.$annotations = $('<div />')
|
|
.addClass("pandoraAnnotations")
|
|
.appendTo($container);
|
|
return $container;
|
|
};
|
|
|
|
//update annotations, etc. based on video currentTime
|
|
this.updatePlayer = function() {
|
|
var that = this;
|
|
var currentTime = this.$video[0].currentTime;
|
|
|
|
//first, handle if video has crossed out-point or is before in-point
|
|
if (that.o.out != 0) {
|
|
if (currentTime > (that.o.out + 2)) {
|
|
that.$video[0].currentTime = that.o.out;
|
|
that.$video[0].pause();
|
|
}
|
|
if (currentTime < (that.o['in'] - 2)) {
|
|
that.$video[0].currentTime = that.o['in'];
|
|
that.$video[0].pause();
|
|
}
|
|
}
|
|
|
|
|
|
//if layers are the same as last update, return
|
|
if (!that.crossesPoint(currentTime)) { return false; }
|
|
//console.log("annot point changed");
|
|
|
|
//else, set new annotPoint, construct DOM elements for currently matched layers, etc.
|
|
that.annotPoint = currentTime;
|
|
//now get all matching layers at current time code
|
|
var layerNames = this.o.layers,
|
|
matchedLayers = {};
|
|
|
|
$.each(layerNames, function(i, layerName) {
|
|
|
|
matchedLayers[layerName] = that.getLayersAtTimecode(layerName, currentTime)
|
|
});
|
|
that.currentLayers = matchedLayers;
|
|
that.$annotations.empty();
|
|
for (var layer in matchedLayers) {
|
|
if (matchedLayers.hasOwnProperty(layer)) {
|
|
var theseLayers = matchedLayers[layer];
|
|
//console.log(theseLayers);
|
|
if (theseLayers.length > 0) {
|
|
var $annotsForLayer = getElemForLayer(layer, theseLayers);
|
|
$annotsForLayer.appendTo(that.$annotations);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
Parameters:
|
|
layerName: <string> eg. 'transcripts'
|
|
currentTime: <float> in seconds
|
|
Returns <array> of matched layer objects
|
|
*/
|
|
this.getLayersAtTimecode = function(layerName, currentTime) {
|
|
var ret = [];
|
|
var theseLayers = this.data.layers[layerName];
|
|
$.each(theseLayers, function(i,layer) {
|
|
if (layer['in'] < currentTime && layer.out > currentTime) {
|
|
ret.push(layer);
|
|
}
|
|
});
|
|
return ret;
|
|
};
|
|
|
|
this.destroy = function() {
|
|
this.$video[0].pause();
|
|
this.$video.remove();
|
|
this.$el.empty();
|
|
};
|
|
};
|
|
|
|
/*
|
|
Parameters:
|
|
layerName: <string> eg. 'transcripts'
|
|
layers: <array> layer objects to render
|
|
Returns <jQuery element> for an annotation type - i.e. all 'transcripts', or all 'descriptions'
|
|
*/
|
|
function getElemForLayer(layerName, layers) {
|
|
if (layers.length === 0) {
|
|
return $('<div />'); //FIXME
|
|
}
|
|
var $elem = $('<div />').addClass("pandoraLayer").addClass(layerName);
|
|
var title = layerName.substr(0,1).toUpperCase() + layerName.substr(1, layerName.length);
|
|
var $title = $('<div />').addClass("pandoraLayerTitle").text(title).appendTo($elem);
|
|
|
|
$.each(layers, function(i,v) {
|
|
var $annot = $('<div />').addClass("pandoraAnnot");
|
|
//TODO: add time-code div
|
|
var $annotText = $('<div />')
|
|
.addClass("pandoraAnnotText")
|
|
.html(v.value)
|
|
.appendTo($annot);
|
|
$annot.appendTo($elem);
|
|
});
|
|
return $elem;
|
|
}
|
|
|
|
/*
|
|
Silly function to check if two layer arrays are the same (i.e. to check if matchedLayers have changed)
|
|
Parameters:
|
|
layers1: <array> of layer objects
|
|
layers2: <array> of layer objects to compare with
|
|
Returns <boolean> true if layer arrays are the same, false if different
|
|
FIXME: this id concatenation string comparison is a bit weird, but it seemed like a non-expensive simple way to do it
|
|
*/
|
|
function isSameLayers(layers1, layers2) {
|
|
var idString1 = '',
|
|
idString2 = '';
|
|
for (var l in layers1) {
|
|
if (layers1.hasOwnProperty(l)) {
|
|
$.each(layers1[l], function(i,v) {
|
|
idString1 += v.id;
|
|
});
|
|
}
|
|
}
|
|
for (var l in layers2) {
|
|
if (layers2.hasOwnProperty(l)) {
|
|
$.each(layers2[l], function(i,v) {
|
|
idString2 += v.id;
|
|
});
|
|
}
|
|
}
|
|
return idString1 == idString2
|
|
}
|
|
|
|
//Returns <boolean> true or false based on whether the browser can play pandora video
|
|
//FIXME: actually implement this function
|
|
function canPlayVideo() {
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
Get options from data- attributes
|
|
Parameters:
|
|
optionTypes: <object>
|
|
example: {
|
|
'strings': ['option1', 'option2', 'option3'],
|
|
'integers': ['fooint', 'barint'],
|
|
'arrays': ['list1', 'list2'],
|
|
'booleans': ['bool1']
|
|
}
|
|
|
|
namespace: <string>
|
|
example: 'pandora'
|
|
namespace for data- attributes
|
|
|
|
example html:
|
|
<div id="blah" data-pandora-option1="foobar" data-pandora-fooint="23" data-pandora-list2="apples, oranges" data-pandora-bool1="true">
|
|
|
|
usage:
|
|
var dataOptions = $('#blah').getDataOptions(optionTypes, namespace);
|
|
*/
|
|
$.fn.getDataOptions = function(optionTypes, namespace) {
|
|
var $element = this;
|
|
var prefix = "data-" + namespace + "-",
|
|
options = {};
|
|
$.each(optionTypes['strings'], function(i,v) {
|
|
var attr = prefix + v;
|
|
options[v] = $element.hasAttr(attr) ? $element.attr(attr) : undefined;
|
|
});
|
|
$.each(optionTypes['integers'], function(i,v) {
|
|
var attr = prefix + v;
|
|
options[v] = $element.hasAttr(attr) ? parseInt($element.attr(attr)) : undefined;
|
|
});
|
|
$.each(optionTypes['floats'], function(i,v) {
|
|
var attr = prefix + v;
|
|
options[v] = $element.hasAttr(attr) ? parseFloat($element.attr(attr)) : undefined;
|
|
});
|
|
$.each(optionTypes['arrays'], function(i,v) {
|
|
var attr = prefix + v;
|
|
options[v] = $element.hasAttr(attr) ? $.map($element.attr(attr).split(","), $.trim) : undefined;
|
|
});
|
|
$.each(optionTypes['booleans'], function(i,v) {
|
|
var attr = prefix + v;
|
|
options[v] = $element.hasAttr(attr) ? $element.attr(attr) == 'true' : false;
|
|
});
|
|
return options;
|
|
}
|
|
|
|
/*
|
|
FIXME: possibly improve - http://stackoverflow.com/questions/1318076/jquery-hasattr-checking-to-see-if-there-is-an-attribute-on-an-element#1318091
|
|
*/
|
|
$.fn.hasAttr = function(attr) {
|
|
return this.attr(attr) != undefined;
|
|
};
|
|
|
|
})(jQuery);
|