var MistUtil = { format: { time: function(secs,options){ if (isNaN(secs) || !isFinite(secs)) { return secs; } if (!options) { options = {}; } var ago = (secs < 0 ? " ago" : ""); secs = Math.abs(secs); var days = Math.floor(secs / 86400) secs = secs - days * 86400; var hours = Math.floor(secs / 3600); secs = secs - hours * 3600; var mins = Math.floor(secs / 60); var ms = Math.round((secs % 1)*1e3); secs = Math.floor(secs - mins * 60); var str = []; if (days) { days = days+" day"+(days > 1 ? "s" : "")+", "; } if ((hours) || (days)) { str.push(hours); str.push(("0"+mins).slice(-2)); } else { str.push(mins); //don't use 0 padding if there are no hours in front } str.push(("0"+Math.floor(secs)).slice(-2)); if (options.ms) { str[str.length-1] += "."+("000"+ms).slice(-3); } return (days ? days : "")+str.join(":")+ago; }, ucFirst: function(string){ return string.charAt(0).toUpperCase()+string.slice(1); }, number: function(num) { if ((isNaN(Number(num))) || (num == 0)) { return num; } //rounding //use a significance of three, but don't round "visible" digits var sig = Math.max(3,Math.ceil(Math.log(num)/Math.LN10)); var mult = Math.pow(10,sig - Math.floor(Math.log(num)/Math.LN10) - 1); num = Math.round(num * mult) / mult; //thousand seperation if (num >= 1e4) { var seperator = " "; number = num.toString().split("."); var regex = /(\d+)(\d{3})/; while (regex.test(number[0])) { number[0] = number[0].replace(regex,"$1"+seperator+"$2"); } num = number.join("."); } return num; }, bytes: function(val){ if (isNaN(Number(val))) { return val; } var suffix = ["bytes","KB","MB","GB","TB","PB"]; if (val == 0) { unit = suffix[0]; } else { var exponent = Math.floor(Math.log(Math.abs(val)) / Math.log(1024)); if (exponent < 0) { unit = suffix[0]; } else { val = val / Math.pow(1024,exponent); unit = suffix[exponent]; } } return this.number(val)+unit; }, mime2human: function(mime){ switch (mime) { case "html5/video/webm": { return "WebM"; break; } case "html5/application/vnd.apple.mpegurl": { return "HLS"; break; } case "flash/10": { return "Flash (RTMP)"; break; } case "flash/11": { return "Flash (HDS)"; break; } case "flash/7": { return "Flash (Progressive)"; break; } case "html5/video/mpeg": { return "TS"; break; } case "html5/application/vnd.ms-ss": { return "Smooth streaming"; break; } case "dash/video/mp4": { return "DASH"; break; } case "webrtc": { return "WebRTC"; break; } case "silverlight": { return "Smooth streaming (Silverlight)"; break; } case "html5/text/vtt": { return "VTT subtitles"; break; } case "html5/text/plain": { return "SRT subtitles"; break; } default: { return mime.replace("html5/","").replace("video/","").replace("audio/","").toLocaleUpperCase(); } } } }, class: { //reroute classList functionalities if not supported; also avoid indexOf add: function(DOMelement,item){ if ("classList" in DOMelement) { DOMelement.classList.add(item); } else { var classes = this.get(DOMelement); classes.push(item); this.set(DOMelement,classes); } }, remove: function(DOMelement,item){ if ("classList" in DOMelement) { DOMelement.classList.remove(item); } else { var classes = this.get(DOMelement); for (var i = classes.length-1; i >= 0; i--) { if (classes[i] == item) { classes.splice(i); } } this.set(DOMelement,classes); } }, get: function(DOMelement) { var classes; var className = DOMelement.getAttribute("class"); //DOMelement.className does not work on svg elements if ((!className) || (className == "")) { classes = []; } else { classes = className.split(" "); } return classes; }, set: function(DOMelement,classes) { DOMelement.setAttribute("class",classes.join(" ")); }, has: function(DOMelement,hasClass){ return (DOMelement.className.split(" ").indexOf(hasClass) >= 0) } }, object: { //extend object1 with object2 extend: function(object1,object2,deep) { for (var i in object2) { if (deep && (typeof object2[i] == "object") && (!("nodeType" in object2[i]))) { if (!(i in object1)) { if (MistUtil.array.is(object2[i])) { object1[i] = []; } else { object1[i] = {}; } } this.extend(object1[i],object2[i],true); } else { object1[i] = object2[i]; } } return object1; }, //replace Object.keys //if sorting: sort the keys alphabetically or use passed sorting function //sorting gets these arguments: keya,keyb,valuea,valueb keys: function(obj,sorting){ var keys = []; for (var i in obj) { keys.push(i); } if (sorting) { if (typeof sorting != "function") { sorting = function(keya,keyb){ if (keya > keyb) { return 1; } if (keya < keyb) { return -1; } return 0; }; } keys.sort(function(keya,keyb){ return sorting(keya,keyb,obj[keya],obj[keyb]); }); } return keys; }, //replace Object.values //if sorting: sort the keys alphabetically or use passed sorting function //sorting gets these arguments: keya,keyb,valuea,valueb values: function(obj,sorting){ var keys = this.keys(obj,sorting); values = []; for (var i in keys) { values.push(obj[keys[i]]); } return values; } }, array: { //replace [].indexOf indexOf: function(array,entry) { if (!(array instanceof Array)) { throw "Tried to use indexOf on something that is not an array"; } if ("indexOf" in array) { return array.indexOf(entry); } for (var i; i < array.length; i++) { if (array[i] == entry) { return i; } } return -1; }, //replace isArray is: function(array) { if ("isArray" in Array) { return Array.isArray(array); } return Object.prototype.toString.call(array) === '[object Array]'; }, multiSort: function (array,sortby) { /* MistUtil.array.multiSort([].concat(video.info.source),[ {type: ["html5/video/webm","silverlight"]} or ["type",["html5/video/webm","silverlight"]] ,{simul_tracks:-1} or ["simul_tracks",-1] ,function(a){ return a.priority * -1; } ,"url" ]); */ var sortfunc = function(a,b){ if (isNaN(a) || isNaN(b)) { return a.localeCompare(b); } return Math.sign(a-b); }; if (!sortby.length) { return array.sort(sortfunc); } function getValue(key,a) { function parseIt(item,key,sortvalue){ if (!(key in item)) { throw "Invalid sorting rule: "+JSON.stringify([key,sortvalue])+". \""+key+"\" is not a key of "+JSON.stringify(item); } if (typeof sortvalue == "number") { //deals with something like {priority: -1} if (key in item) { return item[key] * sortvalue; } } //deals with something like {type:["webrtc"]} var i = sortvalue.indexOf(item[key]) return (i >= 0 ? i : sortvalue.length); } //deals with something like function(a){ return a.foo + a.bar; } if (typeof key == "function") { return key(a); } if (typeof key == "object") { if (key instanceof Array) { //it's an array return parseIt(a,key[0],key[1]); } //it's an object for (var j in key) { //only listen to a single key return parseIt(a,j,key[j]); } } if (key in a) { return a[key]; } throw "Invalid sorting rule: "+key+". This should be a function, object or key of "+JSON.stringify(a)+"."; } array.sort(function(a,b){ var output = 0; for (var i in sortby) { var key = sortby[i]; output = sortfunc(getValue(key,a),getValue(key,b)); if (output != 0) { break; } } return output; }); return array; } }, createUnique: function() { var i = "uid"+Math.random().toString().replace("0.",""); if (document.querySelector("."+i)) { //if this is already used, try again return createUnique(); } return i; }, http: { getpost: function(type,url,data,callback,errorCallback) { var xhr = new XMLHttpRequest(); xhr.open(type, url, true); if (type == "POST") { xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); } if (errorCallback) { xhr.timeout = 8e3; } //go to timeout function after 8 seconds xhr.onload = function() { var status = xhr.status; if ((status >= 200) && (status < 300)) { callback(xhr.response); } else if (errorCallback) { xhr.onerror = function() { errorCallback(xhr); } } }; if (errorCallback) { xhr.onerror = function() { errorCallback(xhr); } xhr.ontimeout = xhr.onerror; } if (type == "POST") { var poststr; var post = []; for (var i in data) { post.push(i+"="+encodeURIComponent(data[i])); } if (post.length) { poststr = post.join("&"); } xhr.send(poststr); } else { xhr.send(); } }, get: function(url,callback,errorCallback){ this.getpost("GET",url,null,callback,errorCallback); }, post: function(url,data,callback,errorCallback){ this.getpost("POST",url,data,callback,errorCallback); }, url: { addParam: function(url,params){ var spliturl = url.split("?"); var ret = [spliturl.shift()]; var splitparams = []; if (spliturl.length) { splitparams = spliturl[0].split("&"); } for (var i in params) { splitparams.push(i+"="+params[i]); } if (splitparams.length) { ret.push(splitparams.join("&")); } return ret.join("?"); }, split: function(url){ var a = document.createElement("a"); a.href = url; return { protocol: a.protocol, host: a.hostname, hash: a.hash, port: a.port, path: a.pathname.replace(/\/*$/,"") }; }, sanitizeHost: function(host){ var split = MistUtil.http.url.split(host); return split.protocol + "//" + split.host + (split.port && (split.port != "") ? ":"+split.port : "") + (split.hash && (split.hash != "") ? "#"+split.hash : "") + (split.path ? split.path : ""); } } }, css: { cache: {}, load: function(url,colors,callback){ var style = document.createElement("style"); style.type = "text/css"; style.setAttribute("data-source",url); if (callback) { style.callback = callback; } var cache = this.cache; function onCSSLoad(d) { //parse rules and replace variables; expected syntax $abc[.abc] var css = MistUtil.css.applyColors(d,colors); if ("callback" in style) { style.callback(css); } else { style.textContent = css; } } if (url in cache) { if (cache[url] instanceof Array) { cache[url].push(onCSSLoad); } else { onCSSLoad(cache[url]); } } else { //retrieve file contents cache[url] = [onCSSLoad]; MistUtil.http.get(url,function(d){ for (var i in cache[url]) { cache[url][i](d); } cache[url] = d; },function(){ throw "Failed to load CSS from "+url; }); } return style; //its empty now, but will be filled on load }, applyColors: function(css,colors) { return css.replace(/\$([^\s^;^}]*)/g,function(str,variable){ var index = variable.split("."); var val = colors; for (var j in index) { val = val[index[j]]; } return val; }); }, createStyle: function(css,prepend,applyToChildren){ var style = document.createElement("style"); style.type = "text/css"; if (css) { if (prepend) { css = this.prependClass(css,prepend,applyToChildren); } style.textContent = css; } return style; }, prependClass: function (css,prepend,applyToChildren) { var style = false; if (typeof css != "string") { style = css; if (!("unprepended" in style)) { style.unprepended = style.textContent; } css = style.unprepended; } //remove all block comments css = css.replace(/\/\*.*?\*\//g,""); //save all @{} blocks var save = css.match(/@.*?{.*}/g); for (var i in save) { css = css.replace(save[i],"@@@@"); } //find and replace selectors css = css.replace(/[^@]*?{[^]*?}/g,function(match){ var split = match.split("{") var selectors = split[0].split(","); var properties = "{"+split.slice(1).join("}"); for (var i in selectors) { selectors[i] = selectors[i].trim(); var str = "."+prepend+selectors[i]; if (applyToChildren) { str += ",\n."+prepend+" "+selectors[i]; } selectors[i] = str; } return "\n"+selectors+" "+properties; }); //reinsert saved blocks for (var i in save) { css = css.replace(/@@@@/,save[i]); } if (style) { style.textContent = css; return; } return css; } }, empty: function(DOMelement) { while (DOMelement.lastChild){ if (DOMelement.lastChild.lastChild) { //also empty this child this.empty(DOMelement.lastChild); } if ("attachedListeners" in DOMelement.lastChild) { //remove attached event listeners for (var i in DOMelement.lastChild.attachedListeners) { MistUtil.event.removeListener(DOMelement.lastChild.attachedListeners[i]); } } DOMelement.removeChild(DOMelement.lastChild); } }, event: { send: function(type,message,target){ try { var event = new Event(type,{ bubbles: true, cancelable: true }); event.message = message; target.dispatchEvent(event); } catch (e) { try { var event = document.createEvent('Event'); event.initEvent(type,true,true); event.message = message; target.dispatchEvent(event); } catch (e) { return false; } } return true; }, addListener: function(DOMelement,type,callback,storeOnElement) { //add an event listener and store the handles, so they can be cleared DOMelement.addEventListener(type,callback); if (!storeOnElement) { storeOnElement = DOMelement; } if (!("attachedListeners" in storeOnElement)) { storeOnElement.attachedListeners = []; } var output = { element: DOMelement, type: type, callback: callback }; storeOnElement.attachedListeners.push(output); return output; }, removeListener: function(data) { data.element.removeEventListener(data.type, data.callback); } }, scripts: { list: {}, insert: function(src,onevent,MistVideo){ var scripts = this; if (MistVideo) { //register so we can remove it on unload MistVideo.errorListeners.push({ src: src, onevent: onevent }); } if (src in this.list) { //already present //register to error listening this.list[src].subscribers.push(onevent.onerror); //execute onload if ("onload" in onevent) { if (this.hasLoaded) { onevent.onload(); } else { MistUtil.event.addListener(this.list[src].tag,"load",onevent.onload); } } return; } var scripttag = document.createElement("script"); scripttag.hasLoaded = false; scripttag.setAttribute("src",src); scripttag.setAttribute("crossorigin","anonymous"); //must be set to get info about errors thrown document.head.appendChild(scripttag); scripttag.onerror = function(e){ onevent.onerror(e); } scripttag.onload = function(e){ this.hasLoaded = true; if (!MistVideo.destroyed) { onevent.onload(e); } } scripttag.addEventListener("error",function(e){ onevent.onerror(e); }); //error catching var oldonerror = false; if (window.onerror) { oldonerror = window.onerror; } window.onerror = function(message,source,line,column,error){ if (oldonerror) { oldonerror.apply(this,arguments); } if (source == src) { onevent.onerror(error); for (var i in scripts.list[src].subscribers) { scripts.list[src].subscribers[i](error); } } }; this.list[src] = { subscribers: [onevent.onerror], tag: scripttag }; return scripttag; } }, tracks: { parse: function(metaTracks){ var output = {}; for (var i in metaTracks) { var track = MistUtil.object.extend({},metaTracks[i]); if (track.type == "meta") { track.type = track.codec; track.codec = "meta"; } if (!(track.type in output)) { output[track.type] = {}; } output[track.type][track.trackid] = track; //make up something logical for the track displayname var name = {}; for (var j in track) { switch (j) { case "width": name[j] = track.width+"×"+track.height; break; case "bps": if (track.codec == "meta") { continue; } if (track.bps > 0) { var val; if (track.bps > 1024*1024/8) { val = Math.round(track.bps/1024/1024*8)+"mbps"; } else { val = Math.round(track.bps/1024*8)+"kbps"; } name[j] = val; } break; case "fpks": if (track.fpks > 0) { name[j] = track.fpks/1e3+"fps"; } break; case "channels": if (track.channels > 0) { name[j] = (track.channels == 1 ? "Mono" : (track.channels == 2 ? "Stereo" : "Surround ("+track.channels+"ch)")); } break; case "rate": name[j] = Math.round(track.rate)+"Khz"; break; case "language": if (track[j] != "Undetermined") { name[j] = track[j]; } break; case "codec": if (track.codec == "meta") { continue; } name[j] = track[j]; break; } } track.describe = name; } //filter what to display based on what is different for (var type in output) { var equal = false; for (var i in output[type]) { if (!equal) { //fill equal with all the keys and values of the first track of this type equal = MistUtil.object.extend({},output[type][i].describe); continue; } if (MistUtil.object.keys(output[type]).length > 1) { //if there is more than one track of this type for (var j in output[type][i].describe) { if (equal[j] != output[type][i].describe[j]) { //remove key from equal if not equal delete equal[j]; } } } } //apply for (var i in output[type]) { var different = {}; var same = {}; for (var j in output[type][i].describe) { if (!(j in equal)){ different[j] = output[type][i].describe[j]; } else { same[j] = output[type][i].describe[j]; } } output[type][i].different = different; output[type][i].same = same; var d = MistUtil.object.values(different); output[type][i].displayName = (d.length ? d.join(", ") : MistUtil.object.values(output[type][i].describe).join(" ")); } } return output; } }, isTouchDevice: function(){ return (('ontouchstart' in window) || (navigator.msMaxTouchPoints > 0)); //return true; }, getPos: function(element,cursorLocation){ var style = element.currentStyle || window.getComputedStyle(element, null); var zoom = 1; var node = element; while (node) { if ((node.style.zoom) && (node.style.zoom != "")) { zoom *= parseFloat(node.style.zoom,10); } node = node.parentElement; } var pos0 = element.getBoundingClientRect().left - (parseInt(element.borderLeftWidth,10) || 0); var width = element.getBoundingClientRect().width;; var perc = Math.max(0,((cursorLocation.clientX/zoom) - pos0) / width); perc = Math.min(perc,1); return perc; }, createGraph: function(data,options){ var ns = "http://www.w3.org/2000/svg"; var svg = document.createElementNS(ns,"svg"); svg.setAttributeNS(null,"height","100%"); svg.setAttributeNS(null,"width","100%"); svg.setAttributeNS(null,"class","mist icon graph"); svg.setAttributeNS(null,"preserveAspectRatio","none"); var x_correction = data.x[0]; var lasty = data.y[0]; if (options.differentiate) { for (var i = 1; i < data.y.length; i++) { var diff = data.y[i] - lasty; lasty = data.y[i]; data.y[i] = diff; } } var path = []; var area = { x: { min: data.x[0] - x_correction, max: data.x[0] - x_correction }, y: { min: data.y[0]*-1, max: data.y[0]*-1 } }; function updateMinMax(x,y) { if (arguments.length) { area.x.min = Math.min(area.x.min,x); area.x.max = Math.max(area.x.max,x); area.y.min = Math.min(area.y.min,y*-1); area.y.max = Math.max(area.y.max,y*-1); } else { //reprocess the entire path var d = path[0].split(","); area = { x: { min: d[0], max: d[0] }, y: { min: d[1], max: d[1] } }; for (var i = 1; i < path.length; i++) { var d = path[i].split(","); updateMinMax(d[0],d[1]*-1); } } } path.push([data.x[0] - x_correction,data.y[0]*-1].join(",")); for (var i = 1; i < data.y.length; i++) { updateMinMax(data.x[i] - x_correction,data.y[i]*-1); path.push("L "+[data.x[i] - x_correction,data.y[i]*-1].join(",")); } //define gradient var defs = document.createElementNS(ns,"defs"); svg.appendChild(defs); var gradient = document.createElementNS(ns,"linearGradient"); defs.appendChild(gradient); gradient.setAttributeNS(null,"id",MistUtil.createUnique()); gradient.setAttributeNS(null,"gradientUnits","userSpaceOnUse"); gradient.innerHTML += ''; gradient.innerHTML += ''; gradient.innerHTML += ''; gradient.innerHTML += ''; function updateViewBox() { if ("x" in options) { if ("min" in options.x) { area.x.min = options.x.min; } if ("max" in options.x) { area.x.max = options.x.max; } if ("count" in options.x) { area.x.min = area.x.max - options.x.count; } } if ("y" in options) { if ("min" in options.y) { area.y.min = options.y.max*-1; } if ("max" in options.y) { area.y.max = options.y.min*-1; } } svg.setAttributeNS(null,"viewBox",[area.x.min,area.y.min,area.x.max - area.x.min,area.y.max - area.y.min].join(" ")); gradient.setAttributeNS(null,"x1",0); gradient.setAttributeNS(null,"x2",0); if (options.reverseGradient) { gradient.setAttributeNS(null,"y1",area.y.max); gradient.setAttributeNS(null,"y2",area.y.min); } else { gradient.setAttributeNS(null,"y1",area.y.min); gradient.setAttributeNS(null,"y2",area.y.max); } } updateViewBox(); var line = document.createElementNS(ns,"path"); svg.appendChild(line); //line.setAttributeNS(null,"vector-effect","non-scaling-stroke"); line.setAttributeNS(null,"stroke-width","0.1"); line.setAttributeNS(null,"fill","none"); line.setAttributeNS(null,"stroke","url(#"+gradient.getAttribute("id")+")"); line.setAttributeNS(null,"d","M"+path.join(" L")); line.addData = function(newData) { if (options.differentiate) { var diff = newData.y - lasty; lasty = newData.y; newData.y = diff; } path.push([newData.x - x_correction,newData.y*-1].join(",")); if (options.x && options.x.count) { if (path.length > options.x.count) { path.shift(); updateMinMax(); } } updateMinMax(newData.x - x_correction,newData.y*-1); this.setAttributeNS(null,"d","M"+path.join(" L")); updateViewBox(); }; svg.addData = function(newData){ line.addData(newData); }; return svg; }, getBrowser: function(){ var ua = window.navigator.userAgent; if ((ua.indexOf("MSIE ") >= 0) || (ua.indexOf("Trident/") >= 0)) { return "ie"; } if (ua.indexOf("Edge/") >= 0) { return "edge"; } if ((ua.indexOf("Opera") >= 0) || (ua.indexOf('OPR') >= 0)) { return "opera"; } if (ua.indexOf("Chrome") >= 0) { return "chrome"; } if (ua.indexOf("Safari") >= 0) { return "safari"; } if (ua.indexOf("Firefox") >= 0) { return "firefox"; } return false; //unknown } };