You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

441 lines
15 KiB
JavaScript

var MediaFragments = (function(window) {
// "use strict";
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(fun /*, thisp */) {
"use strict";
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== "function") {
throw new TypeError();
}
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in t) {
fun.call(thisp, t[i], i, t);
}
}
};
}
// '&' is the only primary separator for key-value pairs
var SEPARATOR = '&';
// report errors?
var VERBOSE = true;
var logWarning = function(message) {
if (VERBOSE) {
console.log('Media Fragments URI Parsing Warning: ' + message);
}
}
// the currently supported media fragments dimensions are: t, xywh, track, id
// allows for O(1) checks for existence of valid keys
var dimensions = {
t: function(value) {
var components = value.split(',');
if (components.length > 2) {
return false;
}
var start = components[0]? components[0] : '';
var end = components[1]? components[1] : '';
if ((start === '' && end === '') ||
(start && !end && value.indexOf(',') !== -1)) {
return false;
}
// hours:minutes:seconds.milliseconds
var npt = /^((npt\:)?((\d+\:(\d\d)\:(\d\d))|((\d\d)\:(\d\d))|(\d+))(\.\d*)?)?$/;
if ((npt.test(start)) &&
(npt.test(end))) {
start = start.replace(/^npt\:/, '');
// replace a sole trailing dot, which is legal:
// npt-sec = 1*DIGIT [ "." *DIGIT ]
start = start.replace(/\.$/, '');
end = end.replace(/\.$/, '');
var convertToSeconds = function(time) {
if (time === '') {
return false;
}
// possible cases:
// 12:34:56.789
// 34:56.789
// 56.789
// 56
var hours;
var minutes;
var seconds;
time = time.split(':');
var length = time.length;
if (length === 3) {
hours = parseInt(time[0], 10);
minutes = parseInt(time[1], 10);
seconds = parseFloat(time[2]);
} else if (length === 2) {
var hours = 0;
var minutes = parseInt(time[0], 10);
var seconds = parseFloat(time[1]);
} else if (length === 1) {
var hours = 0;
var minutes = 0;
var seconds = parseFloat(time[0]);
} else {
return false;
}
if (hours > 23) {
logWarning('Please ensure that hours <= 23.');
return false;
}
if (minutes > 59) {
logWarning('Please ensure that minutes <= 59.');
return false;
}
if (seconds >= 60) {
logWarning('Please ensure that seconds < 60.');
return false;
}
return hours * 3600 + minutes * 60 + seconds;
};
var startNormalized = convertToSeconds(start);
var endNormalized = convertToSeconds(end);
if (start && end) {
if (startNormalized < endNormalized) {
return {
value: value,
unit: 'npt',
start: start,
end: end,
startNormalized: startNormalized,
endNormalized: endNormalized
};
} else {
logWarning('Please ensure that start < end.');
return false;
}
} else {
if ((convertToSeconds(start) !== false) ||
(convertToSeconds(end) !== false)) {
return {
value: value,
unit: 'npt',
start: start,
end: end,
startNormalized: startNormalized === false ? '' : startNormalized,
endNormalized: endNormalized === false ? '' : endNormalized,
};
} else {
logWarning('Please ensure that start or end are legal.');
return false;
}
}
}
// hours:minutes:seconds:frames.further-subdivison-of-frames
var smpte = /^(\d+\:\d\d\:\d\d(\:\d\d(\.\d\d)?)?)?$/;
var prefix = start.replace(/^(smpte(-25|-30|-30-drop)?).*/, '$1');
start = start.replace(/^smpte(-25|-30|-30-drop)?\:/, '');
if ((smpte.test(start)) && (smpte.test(end))) {
// we interpret frames as milliseconds, and further-subdivison-of-frames
// as microseconds. this allows for relatively easy comparison.
var convertToSeconds = function(time) {
if (time === '') {
return false;
}
// possible cases:
// 12:34:56
// 12:34:56:78
// 12:34:56:78.90
var hours;
var minutes;
var seconds;
var frames;
var subframes;
time = time.split(':');
var length = time.length;
if (length === 3) {
hours = parseInt(time[0], 10);
minutes = parseInt(time[1], 10);
seconds = parseInt(time[2], 10);
frames = 0;
subframes = 0;
} else if (length === 4) {
hours = parseInt(time[0], 10);
minutes = parseInt(time[1], 10);
seconds = parseInt(time[2], 10);
if (time[3].indexOf('.') === -1) {
frames = parseInt(time[3], 10);
subframes = 0;
} else {
var frameSubFrame = time[3].split('.');
frames = parseInt(frameSubFrame[0], 10);
subframes = parseInt(frameSubFrame[1], 10);
}
} else {
return false;
}
if (hours > 23) {
logWarning('Please ensure that hours <= 23.');
return false;
}
if (minutes > 59) {
logWarning('Please ensure that minutes <= 59.');
return false;
}
if (seconds > 59) {
logWarning('Please ensure that seconds <= 59.');
return false;
}
return hours * 3600 + minutes * 60 + seconds +
frames * 0.001 + subframes * 0.000001;
};
if (start && end) {
if (convertToSeconds(start) < convertToSeconds(end)) {
return {
value: value,
unit: prefix,
start: start,
end: end
};
} else {
logWarning('Please ensure that start < end.');
return false;
}
} else {
if ((convertToSeconds(start) !== false) ||
(convertToSeconds(end) !== false)) {
return {
value: value,
unit: prefix,
start: start,
end: end
};
} else {
logWarning('Please ensure that start or end are legal.');
return false;
}
}
}
// regexp adapted from http://delete.me.uk/2005/03/iso8601.html
var wallClock = /^((\d{4})(-(\d{2})(-(\d{2})(T(\d{2})\:(\d{2})(\:(\d{2})(\.(\d+))?)?(Z|(([-\+])(\d{2})\:(\d{2})))?)?)?)?)?$/;
start = start.replace('clock:', '');
if ((wallClock.test(start)) && (wallClock.test(end))) {
// the last condition is to ensure ISO 8601 date conformance.
// not all browsers parse ISO 8601, so we can only use date parsing
// when it's there.
if (start && end && !isNaN(Date.parse('2009-07-26T11:19:01Z'))) {
// if both start and end are given, then the start must be before
// the end
if (Date.parse(start) <= Date.parse(end)) {
return {
value: value,
unit: 'clock',
start: start,
end: end
};
} else {
logWarning('Please ensure that start < end.');
return false;
}
} else {
return {
value: value,
unit: 'clock',
start: start,
end: end
};
}
}
logWarning('Invalid time dimension.');
return false;
},
xywh: function(value) {
// "pixel:" is optional
var pixelCoordinates = /^(pixel\:)?\d+,\d+,\d+,\d+$/;
// "percent:" is obligatory
var percentSelection = /^percent\:\d+,\d+,\d+,\d+$/;
var values = value.replace(/(pixel|percent)\:/, '').split(',');
var x = values[0];
var y = values[1];
var w = values[2];
var h = values[3];
if (pixelCoordinates.test(value)) {
if (w > 0 && h > 0) {
return {
value: value,
unit: 'pixel',
x: x,
y: y,
w: w,
h: h
};
} else {
logWarning('Please ensure that w > 0 and h > 0');
return false;
}
} else if (percentSelection.test(value)) {
/**
* checks for valid percent selections
*/
var checkPercentSelection = (function checkPercentSelection(
x, y, w, h) {
if (!((0 <= x) && (x <= 100))) {
logWarning('Please ensure that 0 <= x <= 100.');
return false;
}
if (!((0 <= y) && (y <= 100))) {
logWarning('Please ensure that 0 <= y <= 100.');
return false;
}
if (!((0 <= w) && (w <= 100))) {
logWarning('Please ensure that 0 <= w <= 100.');
return false;
}
if (!((0 <= h) && (h <= 100))) {
logWarning('Please ensure that 0 <= h <= 100.');
return false;
}
return true;
});
if (checkPercentSelection(x, y, w, h)) {
return {
value: value,
unit: 'percent',
x: x,
y: y,
w: w,
h: h
};
}
logWarning('Invalid percent selection.');
return false;
} else {
logWarning('Invalid spatial dimension.');
return false;
}
},
track: function(value) {
return {
value: value,
name: value
};
},
id: function(value) {
return {
value: value,
name: value
};
},
chapter: function(value) {
return {
value: value,
chapter: value
};
}
}
/**
* splits an octet string into allowed key-value pairs
*/
var splitKeyValuePairs = function(octetString) {
var keyValues = {};
var keyValuePairs = octetString.split(SEPARATOR);
keyValuePairs.forEach(function(keyValuePair) {
// the key part is up to the first(!) occurrence of '=', further '='-s
// form part of the value
var position = keyValuePair.indexOf('=');
if (position < 1) {
return;
}
var components = [
keyValuePair.substring(0, position),
keyValuePair.substring(position + 1)];
// we require a value for each key
if (!components[1]) {
return;
}
// the key name needs to be decoded
var key = decodeURIComponent(components[0]);
// only allow keys that are currently supported media fragments dimensions
var dimensionChecker = dimensions[key];
// the value needs to be decoded
var value = decodeURIComponent(components[1]);
if (dimensionChecker) {
value = dimensionChecker(value);
} else {
// we had a key that is not part of media fragments
return;
}
if (!value) {
return;
}
// keys may appear more than once, thus store all values in an array,
// the exception being &t
if (!keyValues[key]) {
keyValues[key] = [];
}
if (key !== 't') {
keyValues[key].push(value);
} else {
keyValues[key][0] = value;
}
});
return keyValues;
}
return {
parse: function(opt_uri) {
return MediaFragments.parseMediaFragmentsUri(opt_uri);
},
parseMediaFragmentsUri: function(opt_uri) {
var uri = opt_uri? opt_uri : window.location.href;
// retrieve the query part of the URI
var indexOfHash = uri.indexOf('#');
var indexOfQuestionMark = uri.indexOf('?');
var end = (indexOfHash !== -1? indexOfHash : uri.length);
var query = indexOfQuestionMark !== -1?
uri.substring(indexOfQuestionMark + 1, end) : '';
// retrieve the hash part of the URI
var hash = indexOfHash !== -1? uri.substring(indexOfHash + 1) : '';
var queryValues = splitKeyValuePairs(query);
var hashValues = splitKeyValuePairs(hash);
return {
query: queryValues,
hash: hashValues,
toString: function() {
var buildString = function(name, thing) {
var s = '\n[' + name + ']:\n';
if(!Object.keys) Object.keys = function(o){
if (o !== Object(o)) {
throw new TypeError('Object.keys called on non-object');
}
var ret = [], p;
for (p in o) {
if (Object.prototype.hasOwnProperty.call(o,p)) ret.push(p);
}
return ret;
}
Object.keys(thing).forEach(function(key) {
s += ' * ' + key + ':\n';
thing[key].forEach(function(value) {
s += ' [\n';
Object.keys(value).forEach(function(valueKey) {
s += ' - ' + valueKey + ': ' + value[valueKey] + '\n';
});
s += ' ]\n';
});
});
return s;
}
var string =
buildString('Query', queryValues) +
buildString('Hash', hashValues);
return string;
}
};
}
}
})(window);