if (typeof MistSkins == "undefined") { var MistSkins = {}; }
if ((typeof mistoptions != "undefined") && ("host" in mistoptions)) { var misthost = MistUtil.http.url.sanitizeHost(mistoptions.host); } else { var misthost = ".."; }
MistSkins["default"] = {
structure: {
main: {
if: function(){
return (!!this.info.hasVideo && (this.source.type.split("/")[1] != "audio"));
},
then: { //use this substructure when there is video
type: "placeholder",
classes: ["mistvideo"],
children: [{
type: "hoverWindow",
mode: "pos",
style: {position: "relative"},
transition: {
hide: "left: 0; right: 0; bottom: -43px;",
show: "bottom: 0;",
viewport: "left:0; right: 0; top: -1000px; bottom: 0;"
},
button: {type: "videocontainer"},
children: [{type: "loading"},{type: "error"}],
window: {type: "controls"}
}]
},
else: { //use this subsctructure for audio only
type: "container",
classes: ["mistvideo"],
style: {overflow: "visible"},
children: [
{
type: "controls",
classes: ["mistvideo-novideo"],
style: {width: "480px"}
},
{type: "loading"},
{type: "error"},
{
if: function(){
return (this.options.controls == "stock");
},
then: { //show the video element if its controls will be used
type: "video",
style: { position: "absolute" }
},
else: { //hide the video element
type: "video",
style: {
position: "absolute",
display: "none"
}
}
}
],
}
},
videocontainer: {type: "video"},
controls: {
if: function(){
return !!(this.player && this.player.api && this.player.api.play)
},
then: { //use this subsctructure for players that have an api with at least a play function available
type: "container",
classes: ["mistvideo-column"],
children: [
{
type: "progress",
classes: ["mistvideo-pointer"]
},
{
type: "container",
classes: ["mistvideo-main","mistvideo-padding","mistvideo-row","mistvideo-background"],
children: [
{
type: "play",
classes: ["mistvideo-pointer"]
},
{type: "currentTime"},
{type: "totalTime"},
{
type: "container",
classes: ["mistvideo-align-right"],
children: [
{
type: "container",
children: [
{
type: "container",
classes: ["mistvideo-volume_container"],
children: [{
type: "volume",
mode: "horizontal",
size: {height: 22},
classes: ["mistvideo-pointer"]
}]
},
{
type: "speaker",
classes: ["mistvideo-pointer"],
style: {"margin-left": "-2px"}
}
]
},
{
if: function(){
//show the fullscreen and loop buttons here if the player size is larger than 200px
if (("size" in this) && (this.size.width > 200) || ((!this.info.hasVideo || (this.source.type.split("/")[1] == "audio")))) {
return true;
}
return false;
},
then: {
type: "container",
children: [{
type: "loop",
classes: ["mistvideo-pointer"]
},
{
type: "fullscreen",
classes: ["mistvideo-pointer"]
}]
}
},
{
type: "hoverWindow",
mode: "pos",
transition: {
hide: "right: -1000px; bottom: 44px;",
show: "right: 5px;",
viewport: "right: 0; left: 0; bottom: 0; top: -1000px"
},
button: {type: "settings", classes: ["mistvideo-pointer"]},
window: {type: "submenu"}
}
]}
]
}
]
},
else: { //use this subsctructure for players that don't have an api with at least a play function available
if: function() { return !!(this.player && this.player.api); },
then: { //use this subsctructure if some sort of api does exist
type: "hoverWindow",
mode: "pos",
transition: {
hide: "right: -1000px; bottom: 44px;",
show: "right: 2.5px;",
viewport: "right: 0; left: -1000px; bottom: 0; top: -1000px"
},
style: { right: "5px", left: "auto" },
button: {
type: "settings",
classes: ["mistvideo-background","mistvideo-padding"],
},
window: { type: "submenu" }
}
}
},
submenu: {
type: "container",
style: {
"width": "80%",
"maxWidth": "25em",
"zIndex": 2
},
classes: ["mistvideo-padding","mistvideo-column","mistvideo-background"],
children: [
{type: "tracks"},
{
if: function(){
//only show the fullscreen and loop buttons here if the player size is less than 200px
if (("size" in this) && (this.size.width <= 200)) {
return true;
}
return false;
},
then: {
type: "container",
classes: ["mistvideo-center"],
children: [{
type: "loop",
classes: ["mistvideo-pointer"]
},
{
type: "fullscreen",
classes: ["mistvideo-pointer"]
}]
}
}
]
},
placeholder: {
type: "container",
classes: ["mistvideo","mistvideo-delay-display"],
children: [
{type: "placeholder"},
{type: "loading"},
{type: "error"}
]
},
secondaryVideo: function(switchThese){
return {
type: "hoverWindow",
classes: ["mistvideo"],
mode: "pos",
transition: {
hide: "left: 10px; bottom: -40px;",
show: "bottom: 10px;",
viewport: "left: 0; right: 0; top: 0; bottom: 0"
},
button: {
type: "container",
children: [{type: "videocontainer"}]
},
window: {
type: "switchVideo",
classes: ["mistvideo-controls","mistvideo-padding","mistvideo-background","mistvideo-pointer"],
containers: switchThese
}
};
}
},
css: {
skin: misthost+"/skins/default.css"
},
icons: {
blueprints: {
play: {
size: 45,
svg: ''
},
largeplay: {
size: 45,
svg: ''
},
pause: {
size: 45,
svg: ''
},
speaker: {
size: 45,
svg: ''
},
volume: {
size: {width:100, height:45},
svg: function(){
var uid = MistUtil.createUnique();
return ''; //rectangle added because Edge won't trigger mouse events above transparent areas
}
},
muted: {
size: 45,
svg: ''
},
fullscreen: {
size: 45,
svg: ''
},
loop: {
size: 45,
svg: ''
},
settings: {
size: 45,
svg: ''
},
loading: {
size: 100,
svg: ''
},
timeout: {
size: 25,
svg: function(options){
if ((!options) || (!options.delay)) {
options = {delay: 10};
}
var delay = options.delay;
var uid = MistUtil.createUnique();
return '';
}
},
popout: {
size: 45,
svg: ''
},
switchvideo: {
size: 45,
svg: ''
}
}
},
blueprints: {
container: function(){
var container = document.createElement("div");
return container;
},
video: function(){
var MistVideo = this;
//disable right click
MistUtil.event.addListener(MistVideo.video,"contextmenu",function(e){
e.preventDefault();
//also do something useful
//show submenu
MistVideo.container.setAttribute("data-show-submenu","");
MistVideo.container.removeAttribute("data-hide-submenu");
MistVideo.container.removeAttribute("data-hidecursor");
//onmouseout, hide submenu
var f = function(){
MistVideo.container.removeAttribute("data-show-submenu");
MistVideo.container.removeEventListener("mouseout",f);
}
MistUtil.event.addListener(MistVideo.container,"mouseout",f);
});
//hide the cursor after some time
MistVideo.video.hideTimer = false;
MistVideo.video.hideCursor = function(){
if (this.hideTimer) { clearTimeout(this.hideTimer); }
this.hideTimer = MistVideo.timers.start(function(){
MistVideo.container.setAttribute("data-hidecursor","");
var controlsContainer = MistVideo.container.querySelector(".mistvideo-controls");
if (controlsContainer) { controlsContainer.parentNode.setAttribute("data-hidecursor",""); }
},3e3);
};
MistUtil.event.addListener(MistVideo.video,"mousemove",function(){
MistVideo.container.removeAttribute("data-hidecursor");
var controlsContainer = MistVideo.container.querySelector(".mistvideo-controls");
if (controlsContainer) { controlsContainer.parentNode.removeAttribute("data-hidecursor"); }
MistVideo.video.hideCursor();
});
MistUtil.event.addListener(MistVideo.video,"mouseout",function(){
//stop the timer if no longer over the video element
if (MistVideo.video.hideTimer) { MistVideo.timers.stop(MistVideo.video.hideTimer); }
});
//improve autoplay behaviour
if (MistVideo.options.autoplay) {
//because Mist doesn't send data instantly (but real time), it can take a little while before canplaythrough is fired. Rather than wait, we can just start playing at the canplay event
MistUtil.event.addListener(MistVideo.video,"canplay",function(){
if (MistVideo.player.api.paused) {
var promise = MistVideo.player.api.play();
if (promise) {
promise.catch(function(e){
if (MistVideo.destroyed) { return; }
MistVideo.log("Autoplay failed. Retrying with muted audio..");
//play has failed
if (MistVideo.info.hasVideo) {
//try again with sound muted
MistVideo.player.api.muted = true;
//safari doesn't send this event themselves..
MistUtil.event.send("volumechange",null,MistVideo.video);
var promise = MistVideo.player.api.play();
if (promise) {
promise.catch(function(){
if (MistVideo.destroyed) { return; }
MistVideo.log("Autoplay failed even with muted video. Unmuting and showing play button.");
MistVideo.player.api.muted = false;
//play has failed
//show large centered play button
var largePlayButton = MistVideo.skin.icons.build("largeplay",150);
MistUtil.class.add(largePlayButton,"mistvideo-pointer");
MistVideo.container.appendChild(largePlayButton);
//start playing on click
MistUtil.event.addListener(largePlayButton,"click",function(){
if (MistVideo.player.api.paused) {
MistVideo.player.api.play();
}
});
//remove large button on play
var f = function (){
MistVideo.container.removeChild(largePlayButton);
MistVideo.video.removeEventListener("play",f);
};
MistUtil.event.addListener(MistVideo.video,"play",f);
}).then(function(){
if (MistVideo.destroyed) { return; }
MistVideo.log("Autoplay worked! Video will be unmuted on mouseover if the page has been interacted with.");
//show large "muted" icon
var largeMutedButton = MistVideo.skin.icons.build("muted",100);
MistUtil.class.add(largeMutedButton,"mistvideo-pointer");
MistVideo.container.appendChild(largeMutedButton);
MistUtil.event.addListener(largeMutedButton,"click",function(){
MistVideo.player.api.muted = false;
MistVideo.container.removeChild(largeMutedButton);
});
//listen for page interactions
var interacted = false;
var i = function(){
interacted = true;
document.body.removeEventListener("click",i);
};
MistUtil.event.addListener(document.body,"click",i,MistVideo.video);
//turn sound back on on mouseover
var f = function(){
if (interacted) {
MistVideo.player.api.muted = false;
MistVideo.video.removeEventListener("mouseenter",f);
MistVideo.log("Re-enabled sound");
}
};
MistUtil.event.addListener(MistVideo.video,"mouseenter",f);
//remove all the things when unmuted
var fu = function(){
if (!MistVideo.video.muted) {
if (largeMutedButton.parentNode) {
MistVideo.container.removeChild(largeMutedButton);
}
MistVideo.video.removeEventListener("volumechange",fu);
document.body.removeEventListener("click",i);
MistVideo.video.removeEventListener("mouseenter",f);
}
}
MistUtil.event.addListener(MistVideo.video,"volumechange",fu);
},function(){});
}
}
});
}
}
});
}
return this.video;
},
videocontainer: function(){
return this.UI.buildStructure(this.skin.structure.videocontainer);
},
secondaryVideo: function(o){
if (!o) { o = {}; }
if (!o.options) { o.options = {}; }
var MistVideo = this;
if (!("secondary" in MistVideo)) {
MistVideo.secondary = [];
}
var options = MistUtil.object.extend({},MistVideo.options);
options = MistUtil.object.extend(options,o.options);
MistVideo.secondary.push(options);
var pointer = {
primary: MistVideo,
secondary: false
};
options.target = document.createElement("div");
delete options.container;
var mvo = {};
options.MistVideoObject = mvo;
MistUtil.event.addListener(options.target,"initialized",function(){
var mv = mvo.reference;
//options.callback = function(mv){
options.MistVideo = mv; //tell the main video we exist
pointer.secondary = mv;
mv.player.api.muted = true; //disable sound
mv.player.api.loop = false; //disable looping, master will do that for us
//as all event listeners are tied to the video element (not the container), events don't bubble up and disturb higher players
//prevent clicks on the control container from bubbling down to underlying elements
var controlContainers = options.target.querySelectorAll(".mistvideo-controls");
for (var i = 0; i < controlContainers.length; i++) {
MistUtil.event.addListener(controlContainers[i],"click",function(e){
e.stopPropagation();
});
}
//ensure the state of the main player is copied
MistUtil.event.addListener(MistVideo.video,"play",function(){
if (mv.player.api.paused) { mv.player.api.play(); }
},options.target);
MistUtil.event.addListener(MistVideo.video,"pause",function(e){
if (!mv.player.api.paused) { mv.player.api.pause(); }
},options.target);
MistUtil.event.addListener(MistVideo.video,"seeking",function(){
mv.player.api.currentTime = this.currentTime;
},options.target);
MistUtil.event.addListener(MistVideo.video,"timeupdate",function(){
if (mv.player.api.pausedesync) { return; }
//sync
var desync = this.currentTime - mv.player.api.currentTime;
var adesync = Math.abs(desync);
if (adesync > 30) {
mv.player.api.pausedesync = true;
mv.player.api.currentTime = this.currentTime;
mv.log("Re-syncing with main video by seeking (desync: "+desync+"s)");
}
else if (adesync > 0.01) {
var rate = 0.1;
if (adesync < 1) {
rate = 0.05;
}
rate = 1 + rate * Math.sign(desync);
if (rate != mv.player.api.playbackRate) {
mv.log("Re-syncing by changing the playback rate (desync: "+Math.round(desync*1e3)+"ms, rate: "+rate+")");
}
mv.player.api.playbackRate = rate;
}
else if (mv.player.api.playbackRate != 1) {
mv.player.api.playbackRate = 1;
mv.log("Sync with main video achieved (desync: "+Math.round(desync*1e3)+"ms)");
}
},options.target);
MistUtil.event.addListener(mv.video,"seeked",function(){
//don't attempt to correct sync if we're already seeking
mv.player.api.pausedesync = false;
});
});
options.skin = MistUtil.object.extend({},MistVideo.skin,true);
options.skin.structure.main = MistUtil.object.extend({},MistVideo.skin.structure.secondaryVideo(pointer));
mistPlay(MistVideo.stream,options);
return options.target;
},
switchVideo: function(options){
var container = document.createElement("div");
container.appendChild(this.skin.icons.build("switchvideo"));
MistUtil.event.addListener(container,"click",function(){
var primary = options.containers.primary;
var secondary = options.containers.secondary;
function findVideo(startAt,matchTarget) {
if (startAt.video.currentTarget == matchTarget) {
return startAt.video;
}
if (startAt.secondary) {
for (var i = 0; i < startAt.secondary.length; i++) {
var result = findVideo(startAt.secondary[i].MistVideo,matchTarget);
if (result) { return result; }
}
}
return false;
}
//find video element in primary/secondary containers
var pv = findVideo(primary,primary.options.target);
var sv = findVideo(primary,secondary.options.target);
//prevent pausing the primary
var playit = !pv.paused;
//switch them
var place = document.createElement("div");
sv.parentElement.insertBefore(place,sv);
pv.parentElement.insertBefore(sv,pv);
place.parentElement.insertBefore(pv,place);
place.parentElement.removeChild(place);
if (playit) {
try {
pv.play();
sv.play();
} catch(e) {}
}
var tmp = {
width: pv.style.width,
height: pv.style.height,
currentTarget: pv.currentTarget
};/*
pv.style.width = sv.style.width;
pv.style.height = sv.style.height;
sv.style.width = tmp.width;
sv.style.height = tmp.height;*/
pv.currentTarget = sv.currentTarget;
sv.currentTarget = tmp.currentTarget;
primary.player.resizeAll();
});
return container;
},
controls: function(){
if ((this.options.controls) && (this.options.controls != "stock")) {
MistUtil.class.add(this.container,"hasControls");
var container = this.UI.buildStructure(this.skin.structure.controls);
if (MistUtil.isTouchDevice()) {
container.style.zoom = 1.5;
}
return container;
}
},
submenu: function(){
return this.UI.buildStructure(this.skin.structure.submenu);
},
hoverWindow: function(options){
//rewrite to a container with specific classes and continue the buildStructure call
var structure = {
type: "container",
classes: ("classes" in options ? options.classes : []),
children: ("children" in options ? options.children : [])
};
structure.classes.push("hover_window_container");
if (!("classes" in options.window)) { options.window.classes = []; }
options.window.classes.push("inner_window");
options.window.classes.push("mistvideo-container");
options.window = {
type: "container",
classes: ["outer_window"],
children: [options.window]
};
if (!("classes" in options.button)) { options.button.classes = []; }
options.button.classes.push("pointer");
switch (options.mode) {
case "left":
structure.classes.push("horizontal");
structure.children = [options.window,options.button];
break;
case "right":
structure.classes.push("horizontal");
structure.children = [options.button,options.window];
break;
case "top":
structure.classes.push("vertical");
structure.children = [options.button,options.window];
break;
case "bottom":
structure.classes.push("vertical");
structure.children = [options.window,options.button];
break;
case "pos":
structure.children = [options.button,options.window];
if (!("classes" in options.window)) { options.window.classes = []; }
break;
default:
throw "Unsupported mode for structure type hoverWindow";
break;
}
if ("transition" in options) {
if (!("css" in structure)) { structure.css = []; }
structure.css.push(
".hover_window_container:hover > .outer_window:not([data-hidecursor]) > .inner_window { "+options.transition.show+" }\n"+
".hover_window_container > .outer_window { "+options.transition.viewport+" }\n"+
".hover_window_container > .outer_window > .inner_window { "+options.transition.hide+" }"
);
}
structure.classes.push(options.mode);
return this.UI.buildStructure(structure);
},
draggable: function(options){
var container = this.skin.blueprints.container(options);
var MistVideo = this;
var button = this.skin.icons.build("fullscreen",16);
MistUtil.class.remove(button,"fullscreen");
MistUtil.class.add(button,"draggable-icon");
container.appendChild(button);
button.style.alignSelf = "flex-end";
button.style.position = "absolute";
button.style.cursor = "move";
var offset = {};
var move = function(e){
container.style.left = (e.clientX - offset.x)+"px";
container.style.top = (e.clientY - offset.y)+"px";
};
var stop = function(e){
window.removeEventListener("mousemove",move);
window.removeEventListener("click",stop);
MistUtil.event.addListener(button,"click",start);
};
var start = function(e){
e.stopPropagation();
button.removeEventListener("click",start);
offset.x = MistVideo.container.getBoundingClientRect().left - (container.getBoundingClientRect().left - e.clientX);
offset.y = MistVideo.container.getBoundingClientRect().top - (container.getBoundingClientRect().top - e.clientY);
container.style.position = "absolute";
container.style.right = "auto";
container.style.bottom = "auto";
MistVideo.container.appendChild(container);
move(e);
//container.style.resize = "both";
MistUtil.event.addListener(window,"mousemove",move,container);
MistUtil.event.addListener(window,"click",stop,container);
};
MistUtil.event.addListener(button,"click",start);
return container;
},
progress: function(){
//the outer container is div.progress, which contains div.bar and multiple div.buffer-s
var margincontainer = document.createElement("div");
var container = document.createElement("div");
margincontainer.appendChild(container);
container.kids = {};
container.kids.bar = document.createElement("div");
container.kids.bar.className = "bar";
container.appendChild(container.kids.bar);
var video = this.video;
var MistVideo = this;
//these functions update the states
container.updateBar = function(currentTime){
if (this.kids.bar) {
if (!isFinite(MistVideo.player.api.duration)) { this.kids.bar.style.display = "none"; return; }
else { this.kids.bar.style.display = ""; }
w = Math.min(1,Math.max(0,this.time2perc(currentTime)));
this.kids.bar.style.width = w*100+"%";
}
};
container.time2perc = function(time) {
if (!isFinite(MistVideo.player.api.duration)) { return 0; }
var result = 0;
if (MistVideo.info.type == "live") {
var buffer_window = MistVideo.info.meta.buffer_window * 1e-3;
result = (time - MistVideo.player.api.duration + buffer_window) / buffer_window;
}
else {
result = time / MistVideo.player.api.duration;
}
return Math.min(1,Math.max(0,result));
}
container.buildBuffer = function(start,end){
var buffer = document.createElement("div");
buffer.className = "buffer";
buffer.style.left = (this.time2perc(start)*100)+"%";
buffer.style.width = ((this.time2perc(end) - this.time2perc(start))*100)+"%";
return buffer;
};
container.updateBuffers = function(buffers){
//clear old buffer-divs
var old = this.querySelectorAll(".buffer");
for (var i = 0; i < old.length; i++) {
this.removeChild(old[i]);
}
//add new buffer-divs
if (buffers) {
for (var i = 0; i < buffers.length; i++) {
this.appendChild(this.buildBuffer(
buffers.start(i),
buffers.end(i)
));
}
}
};
//obey video states
var lastBufferUpdate = 0;
var bufferTimer = false;
MistUtil.event.addListener(video,"progress",function(){
function updateBuffers(){
//limit fire to once per second
if (new Date().getTime() - lastBufferUpdate > 1e3) {
container.updateBuffers(api.buffered);
lastBufferUpdate = new Date().getTime();
}
else if (!bufferTimer) {
bufferTimer = MistVideo.timers.start(function(){
updateBuffers();
bufferTimer = false;
},1e3);
}
}
updateBuffers();
},container);
var lastBarUpdate = 0;
var barTimer = false;
MistUtil.event.addListener(video,"timeupdate",function(){
function updateBar(){
//console.log(video.currentTime,"timeupdate");
//limit fire to once per 0.2 second
if ((new Date().getTime() - lastBarUpdate > 200) && (!dragging)) {
container.updateBar(api.currentTime);
lastBarUpdate = new Date().getTime();
}
else if (!barTimer) {
barTimer = MistVideo.timers.start(function(){
updateBar();
barTimer = false;
},1e3);
}
}
updateBar();
},container);
MistUtil.event.addListener(video,"seeking",function(){
container.updateBar(MistVideo.player.api.currentTime);
},container);
//control video states
container.getPos = function(e){
var perc = MistUtil.getPos(this,e);
if (MistVideo.info.type == "live") {
//live mode: seek in DVR window
var bufferWindow = MistVideo.info.meta.buffer_window * 1e-3; //buffer window in seconds
//assuming the "right" part or the progressbar is at true live
return (perc-1) * bufferWindow + MistVideo.player.api.duration;
}
else {
//VOD mode
if (!isFinite(MistVideo.player.api.duration)) { return false; }
return perc * MistVideo.player.api.duration;
}
};
//seeking
container.seek = function(e){
var pos = this.getPos(e);
MistVideo.player.api.currentTime = pos;
};
MistUtil.event.addListener(margincontainer,"mouseup",function(e){
if (e.which != 1) { return;} //only respond to left mouse clicks
container.seek(e);
});
//hovering
var tooltip = MistVideo.UI.buildStructure({type:"tooltip"});
tooltip.style.opacity = 0;
container.appendChild(tooltip);
MistUtil.event.addListener(margincontainer,"mouseout",function(){
if (!dragging) { tooltip.style.opacity = 0; }
});
container.moveTooltip = function(e){
var secs = this.getPos(e);
if (secs === false) {
//the tooltip isn't going to make sense
tooltip.style.opacity = 0;
return;
}
tooltip.setText(MistUtil.format.time(secs));
tooltip.style.opacity = 1;
var perc = MistUtil.getPos(this,e);// e.clientX - this.getBoundingClientRect().left;
var pos = {bottom:20};
//if the cursor is on the left side of the progress bar, show tooltip on the right of the cursor, otherwise, show it on the left side
if (perc > 0.5) {
pos.right = (1-perc)*100+"%";
tooltip.triangle.setMode("bottom","right");
}
else {
pos.left = perc*100+"%";
tooltip.triangle.setMode("bottom","left");
}
tooltip.setPos(pos);
};
MistUtil.event.addListener(margincontainer,"mousemove",function(e){
container.moveTooltip(e);
});
//TODO for live seeking, maybe show a tooltip at start and end with the apprioprate times as well?
//dragging
var dragging = false;
MistUtil.event.addListener(margincontainer,"mousedown",function(e){
if (e.which != 1) { return;} //only respond to left mouse clicks
dragging = true;
container.updateBar(container.getPos(e));
var moveListener = MistUtil.event.addListener(document,"mousemove",function(e){
container.updateBar(container.getPos(e));
container.moveTooltip(e);
},container);
var upListener = MistUtil.event.addListener(document,"mouseup",function(e){
if (e.which != 1) { return;} //only respond to left mouse clicks
dragging = false;
//remove mousemove and up
MistUtil.event.removeListener(moveListener);
MistUtil.event.removeListener(upListener);
tooltip.style.opacity = 0;
//trigger seek
if ((!e.path) || (MistUtil.array.indexOf(e.path,margincontainer) < 0)) { //it's not already triggered by onmouseup
container.seek(e);
}
},container);
});
return margincontainer;
},
play: function(){
var MistVideo = this;
var button = document.createElement("div");
button.appendChild(this.skin.icons.build("play"));
button.appendChild(this.skin.icons.build("pause"));
button.setState = function(state){
this.setAttribute("data-state",state);
};
button.setState("paused");
var video = this.video;
//obey video states
MistUtil.event.addListener(video,"playing",function(){
button.setState("playing");
MistVideo.options.autoplay = true;
},button);
MistUtil.event.addListener(video,"pause",function(){
button.setState("paused");
},button);
MistUtil.event.addListener(video,"paused",function(){
button.setState("paused");
},button);
MistUtil.event.addListener(video,"ended",function(){
button.setState("paused");
},button);
//control video states
MistUtil.event.addListener(button,"click",function(){
if (MistVideo.player.api.error) { MistVideo.player.api.load(); }
if (MistVideo.player.api.paused) {
MistVideo.player.api.play();
}
else {
MistVideo.player.api.pause();
MistVideo.options.autoplay = false;
}
});
//toggle play/pause on click on video container
if (MistVideo.player.api) {
MistUtil.event.addListener(MistVideo.video,"click",function(){
if (MistVideo.player.api.paused) { MistVideo.player.api.play(); }
else if (!MistUtil.isTouchDevice()) {
MistVideo.player.api.pause();
MistVideo.options.autoplay = false;
}
},button);
}
return button;
},
speaker: function(){
var hasaudio = false;
var tracks = this.info.meta.tracks;
for (var i in tracks) {
if (tracks[i].type == "audio") { hasaudio = true; break; }
}
if (!hasaudio) { return false; }
var button = this.skin.icons.build("speaker");
var MistVideo = this;
var video = this.video;
//obey video states
MistUtil.event.addListener(video,"volumechange",function(){
if ((MistVideo.player.api.volume) && (!MistVideo.player.api.muted)) {
MistUtil.class.remove(button,"off");
}
else {
MistUtil.class.add(button,"off");
}
},button);
//control video states
MistUtil.event.addListener(button,"click",function(e){
MistVideo.player.api.muted = !MistVideo.player.api.muted;
});
return button;
},
volume: function(options){
var hasaudio = false;
var tracks = this.info.meta.tracks;
for (var i in tracks) {
if (tracks[i].type == "audio") { hasaudio = true; break; }
}
if (!hasaudio) { return false; }
var container = document.createElement("div");
var button = this.skin.icons.build("volume",("size" in options ? options.size : false));
container.appendChild(button);
var MistVideo = this;
button.mode = ("mode" in options ? options.mode : "vertical");
if (button.mode == "vertical") { button.style.transform = "rotate(90deg)"; } //TODO do this properly
//pad values with this amount (to allow for line thickness)
button.margin = {
start: 0.15,
end: 0.1
};
var video = this.video;
//obey video states
button.set = function(perc){
perc = 100 - 100 * Math.pow(1 - perc/100,2); //transform back from quadratic
//add padding
if ((perc != 100) && (perc != 0)) {
perc = this.addPadding(perc/100) * 100;
}
var sliders = button.querySelectorAll(".slider");
for (var i = 0; i < sliders.length; i++) {
sliders[i].setAttribute(button.mode == "vertical" ? "height" : "width",perc+"%");
}
}
MistUtil.event.addListener(video,"volumechange",function(){
button.set(MistVideo.player.api.muted ? 0 : (MistVideo.player.api.volume*100));
},button);
//apply initial video state
var initevent = MistUtil.event.addListener(video,"loadstart",function(){
if (('localStorage' in window) && (localStorage != null) && ('mistVolume' in localStorage)) {
MistVideo.player.api.volume = localStorage['mistVolume'];
}
MistUtil.event.removeListener(initevent);
});
button.addPadding = function(actual){
return actual * (1 - (this.margin.start + this.margin.end)) + this.margin.start;
}
button.removePadding = function(padded){
var val = (padded - this.margin.start) / (1 - (this.margin.start + this.margin.end));
val = Math.max(val,0);
val = Math.min(val,1);
return val;
}
//control video states
button.getPos = function(e){
return this.addPadding(MistUtil.getPos(this,e));
};
//set volume
button.setVolume = function(e){
MistVideo.player.api.muted = false;
var val = this.removePadding(MistUtil.getPos(this,e));
val = 1 - Math.pow((1-val),0.5); //transform to quadratic range between 0 and 1
MistVideo.player.api.volume = val;
try {
localStorage["mistVolume"] = MistVideo.player.api.volume;
}
catch (e) {}
};
MistUtil.event.addListener(button,"mouseup",function(e){
if (e.which != 1) { return;} //only respond to left mouse clicks
button.setVolume(e);
});
//hovering
var tooltip = MistVideo.UI.buildStructure({type:"tooltip"});
tooltip.style.opacity = 0;
tooltip.triangle.setMode("bottom","right");
container.style.position = "relative";
container.appendChild(tooltip);
MistUtil.event.addListener(button,"mouseover",function(){
tooltip.style.opacity = 1;
});
MistUtil.event.addListener(button,"mouseout",function(){
if (!dragging) { tooltip.style.opacity = 0; }
});
button.moveTooltip = function(e){
tooltip.style.opacity = 1;
var pos = MistUtil.getPos(this,e);
tooltip.setText(Math.round(this.removePadding(pos)*100)+"%");
tooltip.setPos({
bottom: 46,
right: 100*(1-pos)+"%"
});
};
MistUtil.event.addListener(button,"mousemove",function(e){
button.moveTooltip(e);
});
//dragging
var dragging = false;
MistUtil.event.addListener(button,"mousedown",function(e){
if (e.which != 1) { return;} //only respond to left mouse clicks
dragging = true;
//button.set(button.getPos(e)*100);
button.setVolume(e);
tooltip.style.opacity = 1;
var moveListener = MistUtil.event.addListener(document,"mousemove",function(e){
//button.set(button.getPos(e)*100);
button.setVolume(e);
button.moveTooltip(e);
},button);
var upListener = MistUtil.event.addListener(document,"mouseup",function(e){
if (e.which != 1) { return;} //only respond to left mouse clicks
dragging = false;
//remove mousemove and up
MistUtil.event.removeListener(moveListener);
MistUtil.event.removeListener(upListener);
tooltip.style.opacity = 0;
//trigger volumechange
if ((!e.path) || (MistUtil.array.indexOf(e.path,button) < 0)) { //it's not already triggered by onmouseup
button.setVolume(e);
}
},button);
});
return container;
},
currentTime: function(){
var MistVideo = this;
var container = document.createElement("div");
var text = document.createTextNode("");
container.appendChild(text);
var video = MistVideo.player.api;
var formatTime = MistUtil.format.time;
container.set = function(){
var v = MistVideo.player.api.currentTime;
text.nodeValue = formatTime(v);
};
container.set();
MistUtil.event.addListener(MistVideo.video,"timeupdate",function(){
container.set();
},container);
MistUtil.event.addListener(MistVideo.video,"seeking",function(){
container.set();
},container);
return container;
},
totalTime: function(){
var MistVideo = this;
var container = document.createElement("div");
var text = document.createTextNode("");
container.appendChild(text);
var video = this.player.api;
if (MistVideo.info.type == "live") {
text.nodeValue = "live";
container.className = "live";
}
else {
container.set = function(duration){
if (isNaN(duration) || !isFinite(duration)) {
this.style.display = "none";
return;
}
this.style.display = "";
text.nodeValue = MistUtil.format.time(duration);
};
MistUtil.event.addListener(MistVideo.video,"durationchange",function(){
var v = MistVideo.player.api.duration;
container.set(v);
},container);
}
return container;
},
playername: function(){
if (!this.playerName || !(this.playerName in mistplayers)) { return; }
var container = document.createElement("span");
container.appendChild(document.createTextNode(mistplayers[this.playerName].name));
return container;
},
mimetype: function(){
if (!this.source) { return; }
var a = document.createElement("a");
a.href = this.source.url;
a.target = "_blank";
a.title = a.href+" ("+this.source.type+")";
a.appendChild(document.createTextNode(MistUtil.format.mime2human(this.source.type)));
return a;
},
logo: function(options){
if ("element" in options) {
return options.element;
}
if ("src" in options) {
var img = document.createElement("img");
img.src = options.src;
return img;
}
},
settings: function(){
var MistVideo = this;
var button = this.skin.icons.build("settings");
var touchmode = (typeof document.ontouchstart != "undefined");
MistUtil.event.addListener(button,"click",function(){
if (MistVideo.container.hasAttribute("data-show-submenu")) {
if (touchmode) {
MistVideo.container.setAttribute("data-hide-submenu",""); //don't show even when hovering
}
MistVideo.container.removeAttribute("data-show-submenu");
}
else {
MistVideo.container.setAttribute("data-show-submenu","");
MistVideo.container.removeAttribute("data-hide-submenu");
}
});
return button;
},
loop: function(){
if ((!("loop" in this.player.api)) || (this.info.type == "live")) { return; }
var MistVideo = this;
var button = this.skin.icons.build("loop");
var video = this.video;
var api = this.player.api;
button.set = function(){
if (api.loop) {
MistUtil.class.remove(this,"off");
}
else {
MistUtil.class.add(this,"off");
}
};
MistUtil.event.addListener(button,"click",function(e){
api.loop = !api.loop;
this.set();
});
button.set();
return button;
},
fullscreen: function(){
if ((!("setSize" in this.player)) || (!this.info.hasVideo) || (this.source.type.split("/")[1] == "audio")) { return; }
var MistVideo = this;
//determine which functions to use..
var requestfuncs = ["requestFullscreen","webkitRequestFullscreen","mozRequestFullScreen","msRequestFullscreen","webkitEnterFullscreen"];
var fullscreenableElements = [function(){ return MistVideo.container;},function(){ return MistVideo.video;}]; //if the functions are not available on the container div, try them again on the video element (for iphone)
var funcs = false;
main:
for (var j in fullscreenableElements) {
for (var i in requestfuncs) {
if (requestfuncs[i] in fullscreenableElements[j]()) {
funcs = {};
funcs.request = function(){ return funcs.fullscreenableElement()[requestfuncs[i]](); }
var cancelfuncs = ["exitFullscreen","webkitCancelFullScreen","mozCancelFullScreen","msExitFullscreen","webkitExitFullscreen"];
var elementfuncs = ["fullscreenElement","webkitFullscreenElement","mozFullScreenElement","msFullscreenElement","webkitFullscreenElement"];
var eventname = ["fullscreenchange","webkitfullscreenchange","mozfullscreenchange","MSFullscreenChange","webkitfullscreenchange"];
funcs.cancel = function() { return document[cancelfuncs[i]](); };
funcs.element = function() { return document[elementfuncs[i]]; };
funcs.event = eventname[i];
funcs.fullscreenableElement = fullscreenableElements[j];
break main; //break to the main loop
}
}
}
if (!funcs) {
//fake fullscreen mode!
funcs = {
event: "fakefullscreenchange",
fullscreenableElement: function (){ return MistVideo.container; },
};
var keydownfunc = function(e){
switch (e.key) {
case "Escape": {
funcs.cancel();
break;
}
}
};
funcs.request = function(){
funcs.element = function(){ return MistVideo.container; };
MistUtil.event.send(funcs.event,null,document);
document.addEventListener("keydown",keydownfunc);
return true;
}
funcs.cancel = function(){
funcs.element = function(){ return null; }
document.removeEventListener("keydown",keydownfunc);
MistUtil.event.send(funcs.event,null,document);
return true;
}
funcs.element = function(){ return null; }
}
var button = this.skin.icons.build("fullscreen");
function onclick(){
if (funcs.element()) {
funcs.cancel();
}
else {
funcs.request();
}
}
MistUtil.event.addListener(button,"click",onclick);
MistUtil.event.addListener(MistVideo.video,"dblclick",onclick);
MistUtil.event.addListener(document,funcs.event,function() {
if (funcs.element() == funcs.fullscreenableElement()) {
MistVideo.container.setAttribute("data-fullscreen","");
}
else if (MistVideo.container.hasAttribute("data-fullscreen")) {
MistVideo.container.removeAttribute("data-fullscreen");
}
MistVideo.player.resizeAll();
},button);
return button;
},
tracks: function(){
if ((!this.info) || (!this.video)) { return; }
var MistVideo = this;
var table = document.createElement("table");
function build(tracks) {
//empty table
MistUtil.empty(table);
tracks = MistUtil.tracks.parse(tracks);
var selections = {};
var checkboxes = {};
function changeToTracks(type,value){
MistVideo.log("User selected "+type+" track with id "+value);
if (!MistVideo.options.setTracks) { MistVideo.options.setTracks = {}; }
MistVideo.options.setTracks[type] = value;
if ((value === true) && selections[type]) {
MistUtil.event.send("change",null,selections[type]);
}
if ("setTrack" in MistVideo.player.api) {
return MistVideo.player.api.setTrack(type,value);
}
else {
//gather what tracks we should use
var usetracks = {};
for (var i in selections) {
if ((i == "subtitle") || (selections[i].value == "")) { continue; } //subtitle tracks are handled seperately
usetracks[i] = selections[i].value;
}
if (value != ""){ usetracks[type] = value; }
//use setTracks
if ("setTracks" in MistVideo.player.api) {
return MistVideo.player.api.setTracks(usetracks);
}
//use setSource
if ("setSource" in MistVideo.player.api) {
return MistVideo.player.api.setSource(
MistUtil.http.url.addParam(MistVideo.source.url,usetracks)
);
}
}
}
//sort the tracks to ["audio","video",..,"subtitle",..etc]
var tracktypes = MistUtil.object.keys(tracks,function(keya,keyb){
function order(value) {
switch (value) {
case "audio": return "aaaaaaa";
case "video": return "aaaaaab";
default: return value;
}
}
if (order(keya) > order(keyb)) { return 1; }
if (order(keya) < order(keyb)) { return -1; }
return 0;
});
for (var j in tracktypes) {
var type = tracktypes[j];
var t = tracks[type];
if (type == "subtitle") {
if ((!("player" in MistVideo)) || (!("api" in MistVideo.player)) || (!("setSubtitle" in MistVideo.player.api))) {
//this player does not support adding subtitles, don't show track selection in the interface
MistVideo.log("Subtitle selection was disabled as this player does not support it.");
continue;
}
//check if the VTT output is available
var subtitleSource = false;
for (var i in MistVideo.info.source) {
var source = MistVideo.info.source[i];
//this is a subtitle source, and it's the same protocol (HTTP/HTTPS) as the video source
if ((source.type == "html5/text/vtt") && (MistUtil.http.url.split(source.url).protocol == MistUtil.http.url.split(MistVideo.source.url).protocol)) {
subtitleSource = source.url.replace(/.srt$/,".vtt");
break;
}
}
if (!subtitleSource) {
//if we can't find a subtitle output, don't show track selection in the interface
MistVideo.log("Subtitle selection was disabled as an SRT source could not be found.");
continue;
}
//also add the option to disable subtitles
t[""] = { trackid: "", different: { none:"None" } };
}
var tr = document.createElement("tr");
tr.title = "The current "+type+" track";
table.appendChild(tr);
if ("decodingIssues" in MistVideo.skin.blueprints) { //this is dev mode
var cell = document.createElement("td");
tr.appendChild(cell);
if (type != "subtitle") {
var checkbox = document.createElement("input");
checkbox.setAttribute("type","checkbox");
checkbox.setAttribute("checked","");
checkbox.setAttribute("title","Whether or not to play "+type);
checkbox.trackType = type;
cell.appendChild(checkbox);
checkboxes[type] = checkbox;
if (MistVideo.options.setTracks && (MistVideo.options.setTracks[type])) {
if (MistVideo.options.setTracks[type] == "none") {
checkbox.checked = false;
}
else {
checkbox.checked = true;
}
}
MistUtil.event.addListener(checkbox,"change",function(){
//make sure at least one checkbox is checked
var n = 0;
for (var i in checkboxes) {
if (checkboxes[i].checked) {
n++;
}
}
if (n == 0) {
for (var i in checkboxes) {
if (i == this.trackType) { continue; }
if (!checkboxes[i].checked) {
checkboxes[i].checked = true;
changeToTracks(i,true);
break;
}
}
}
var value = "none";
if (this.checked) {
if (this.trackType in selections) {
value = selections[this.trackType].value;
}
else {
value = "auto";
}
}
else {
value = "none";
}
changeToTracks(this.trackType,(this.checked ? value : "none"));
});
MistUtil.event.addListener(MistVideo.video,"playerUpdate_trackChanged",function(e){
if (e.message.type != type) { return; }
if (e.message.value == "none") { this.checked = false; }
else { this.checked = true; }
},select);
}
}
var header = document.createElement("td");
tr.appendChild(header);
header.appendChild(document.createTextNode(MistUtil.format.ucFirst(type)+":"));
var cell = document.createElement("td");
tr.appendChild(cell);
var trackkeys = MistUtil.object.keys(t);
//var determine the display info for the tracks
function orderValues(trackinfoobj) {
var order = {
trackid: 0,
language: 1,
width: 2,
bps: 3,
fpks: 4,
channels: 5,
codec: 6,
rate: 7
};
return MistUtil.object.values(trackinfoobj,function(keya,keyb,valuea,valueb){
if (order[keya] > order[keyb]) { return 1; }
if (order[keya] < order[keyb]) { return -1; }
return 0;
});
}
//there is more than one track of this type, and the player supports switching tracks
if ((trackkeys.length > 1) && ("player" in MistVideo) && ("api" in MistVideo.player) && (("setTrack" in MistVideo.player.api) || ("setTracks" in MistVideo.player.api) || ("setSource" in MistVideo.player.api))) {
//show a select box
var select = document.createElement("select");
select.title = "Select another "+type+" track";
selections[type] = select;
select.trackType = type;
cell.appendChild(select);
if (type != "subtitle") {
var option = document.createElement("option");
select.appendChild(option);
option.value = "";
option.appendChild(document.createTextNode("Automatic"));
}
//display properties that are the same for all tracks
var same = orderValues(t[MistUtil.object.keys(t)[0]].same);
if (same.length) {
var span = document.createElement("span");
span.className = "mistvideo-description";
cell.appendChild(span);
cell.appendChild(document.createTextNode(same.join(" ")));
}
//add options to the select
function n(str) {
if (str == "") { return -1; }
return Number(str);
}
var options = MistUtil.object.keys(t,function(a,b){
return n(a) - n(b);
}); //sort them
for (var i in options) {
var track = t[options[i]];
var option = document.createElement("option");
select.appendChild(option);
option.value = ("idx" in track ? track.idx : track.trackid);
if (MistUtil.object.keys(track.different).length) {
option.appendChild(document.createTextNode(orderValues(track.different).join(" ")));
}
else {
//all the tracks are the same as far as the metadata is concerned, just show a track number
option.appendChild(document.createTextNode("Track "+(Number(i)+1)));
}
}
MistUtil.event.addListener(MistVideo.video,"playerUpdate_trackChanged",function(e){
if ((e.message.type != type) || (e.message.trackid == "none")) { return; }
select.value = e.message.trackid;
MistVideo.log("Player selected "+type+" track with id "+e.message.trackid);
},select);
if (type == "subtitle") {
MistUtil.event.addListener(select,"change",function(){
try {
localStorage["mistSubtitleLanguage"] = t[this.value].lang;
}
catch (e) {}
if (this.value != "") {
//gather metadata for this subtitle track here
var trackinfo = MistUtil.object.extend({},t[this.value]);
trackinfo.label = orderValues(trackinfo.describe).join(" ");
trackinfo.src = MistUtil.http.url.addParam(subtitleSource,{track:this.value});
MistVideo.player.api.setSubtitle(trackinfo);
}
else {
MistVideo.player.api.setSubtitle();
}
});
//load last used language if available
if (("localStorage" in window) && (localStorage != null) && ("mistSubtitleLanguage" in localStorage)) {
for (var i in t) {
if (t[i].lang == localStorage["mistSubtitleLanguage"]) {
select.value = i;
//trigger onchange
var e = document.createEvent("Event");
e.initEvent("change");
select.dispatchEvent(e);
break;
}
}
}
}
else {
MistUtil.event.addListener(select,"change",function(){
if (this.trackType in checkboxes) { //this is dev mode
checkboxes[this.trackType].checked = true;
}
if (!changeToTracks(this.trackType,this.value)) {
//trackchange failed, reset select to old value
}
});
//set to the track that plays by default
/*
if (MistVideo.info.type == "live") {
//for live, the default track is the highest index
select.value = MistUtil.object.keys(t).pop();
}
else {
//for vod, the default track is the lowest index
select.value = MistUtil.object.keys(t).shift();
}
*/
}
}
else {
//show as text
var span = document.createElement("span");
span.className = "mistvideo-description";
cell.appendChild(span);
span.appendChild(document.createTextNode(orderValues(t[trackkeys[0]].same).join(" ")));
}
}
}
build(this.info.meta.tracks);
MistUtil.event.addListener(MistVideo.video,"metaUpdate_tracks",function(e){
//reconstruct track selection interface
build(e.message.meta.tracks);
},table);
return table;
},
text: function(options){
var container = document.createElement("span");
container.appendChild(document.createTextNode(options.text));
return container;
},
placeholder: function(){
var placeholder = document.createElement("div");
var size = this.calcSize();
placeholder.style.width = size.width+"px";
placeholder.style.height = size.height+"px";
if (this.options.poster) placeholder.style.background = "url('"+this.options.poster+"') no-repeat 50%/contain";
return placeholder;
},
timeout: function(options){
if (!"function" in options) { return; }
var delay = ("delay" in options ? options.delay : 5);
var icon = this.skin.icons.build("timeout",false,{delay:delay});
icon.timeout = this.timers.start(function(){
options.function();
},delay*1e3);
return icon;
},
polling: function(){
var div = document.createElement("div");
var icon = this.skin.icons.build("loading");
div.appendChild(icon);
return div;
},
loading: function(){
var MistVideo = this;
var icon = this.skin.icons.build("loading",50);
if (("player" in MistVideo) && (MistVideo.player.api)) {
var timer = false;
function addIcon(e){
MistVideo.container.setAttribute("data-loading",e.type);
checkIfOk();
}
function removeIcon(){
MistVideo.container.removeAttribute("data-loading");
if (timer) { MistVideo.timers.stop(timer); }
timer = false;
}
function checkIfOk(){
if (!timer) {
//if everything is playing fine, remove the icon
timer = MistVideo.timers.start(function(){
timer = false;
if ((MistVideo.monitor.vars) && (MistVideo.monitor.vars.score >= 0.999)) { //it's playing just fine
removeIcon();
}
else {
checkIfOk();
}
},1e3);
}
}
//add loading icon
var events = ["waiting","seeking","stalled"];
for (var i in events) {
MistUtil.event.addListener(MistVideo.video,events[i],function(e){
if ((!this.paused) && ("container" in MistVideo)) {
addIcon(e);
}
},icon);
}
//remove loading icon
var events = ["seeked","playing","canplay","paused","ended"];
for (var i in events) {
MistUtil.event.addListener(MistVideo.video,events[i],function(e){
if ("container" in MistVideo) {
removeIcon();
}
},icon);
MistUtil.event.addListener(MistVideo.video,"progress",function(e){
if (("container" in MistVideo) && ("monitor" in MistVideo) && ("vars" in MistVideo.monitor) && ("score" in MistVideo.monitor.vars) && (MistVideo.monitor.vars.score > 0.99)) {
removeIcon();
}
},icon);
}
}
return icon;
},
error: function(){
var MistVideo = this;
var container = document.createElement("div");
container.message = function(message,details,options){
MistUtil.empty(this);
var message_container = document.createElement("div");
message_container.className = "message";
this.appendChild(message_container);
if (!options.polling && !options.passive && !options.hideTitle) {
var header = document.createElement("h3");
message_container.appendChild(header);
header.appendChild(document.createTextNode("The player has encountered a problem"));
}
var p = document.createElement("p");
message_container.appendChild(p);
message_container.update = function(message){
MistUtil.empty(p);
//p.appendChild(document.createTextNode(message));
p.innerHTML = message; //allow custom html messages (configured in MI/HTTP/nostreamtext)
};
if (message) {
if (MistVideo.info.on_error) {
message = MistVideo.info.on_error.replace(/\/,message);
}
message_container.update(message);
var d = document.createElement("p");
d.className = "details mistvideo-description";
message_container.appendChild(d);
if (details) {
d.appendChild(document.createTextNode(details));
}
else if ("decodingIssues" in MistVideo.skin.blueprints) { //dev mode
if (("player" in MistVideo) && ("api" in MistVideo.player) && (MistVideo.video)) {
details = [];
if (typeof MistVideo.state != "undefined") {
details.push(["Stream state:",MistVideo.state]);
}
if (typeof MistVideo.player.api.currentTime != "undefined") {
details.push(["Current video time:",MistUtil.format.time(MistVideo.player.api.currentTime)]);
}
if (("video" in MistVideo) && ("getVideoPlaybackQuality" in MistVideo.video)) {
var data = MistVideo.video.getVideoPlaybackQuality();
if (("droppedVideoFrames" in data) && ("totalVideoFrames" in data) && (data.totalVideoFrames)) {
details.push(["Frames dropped/total:",MistUtil.format.number(data.droppedVideoFrames)+"/"+MistUtil.format.number(data.totalVideoFrames)]);
}
if (("corruptedVideoFrames" in data) && (data.corruptedVideoFrames)) {
details.push(["Corrupted frames:",MistUtil.format.number(data.corruptedVideoFrames)]);
}
}
var networkstates = {
0: ["NETWORK EMPTY:","not yet initialized"],
1: ["NETWORK IDLE:","resource selected, but not in use"],
2: ["NETWORK LOADING:","data is being downloaded"],
3: ["NETWORK NO SOURCE:","could not locate source"]
};
details.push(networkstates[MistVideo.video.networkState]);
var readystates = {
0: ["HAVE NOTHING:","no information about ready state"],
1: ["HAVE METADATA:","metadata has been loaded"],
2: ["HAVE CURRENT DATA:","data for the current playback position is available, but not for the next frame"],
3: ["HAVE FUTURE DATA:","data for current and next frame is available"],
4: ["HAVE ENOUGH DATA:","can start playing"]
};
details.push(readystates[MistVideo.video.readyState]);
if (!options.passive) {
var table = document.createElement("table")
for (var i in details) {
var tr = document.createElement("tr");
table.appendChild(tr);
for (var j in details[i]) {
var td = document.createElement("td");
tr.appendChild(td);
td.appendChild(document.createTextNode(details[i][j]));
}
}
d.appendChild(table);
}
}
var c = document.createElement("div");
c.className = "mistvideo-container mistvideo-column";
c.style.textAlign = "left";
c.style.marginBottom = "1em";
message_container.appendChild(c);
var s = MistVideo.UI.buildStructure({type:"forcePlayer"});
if (s) { c.appendChild(s); }
var s = MistVideo.UI.buildStructure({type:"forceType"});
if (s) { c.appendChild(s); }
}
}
return message_container;
};
var countdown = false;
var showingError = false;
var since = false;
var message_global;
var ignoreThese = {};
//add control functions to overall MistVideo object
this.showError = function(message,options){
if (!options) {
options = {
softReload: !!(MistVideo.player && MistVideo.player.api && MistVideo.player.api.load),
reload: true,
nextCombo: !!MistVideo.info,
polling: false,
passive: false
};
}
var identifyer = (options.type ? options.type : message);
if (identifyer in ignoreThese) { return; }
if (options.reload === true) {
if ((MistVideo.options.reloadDelay) && (!isNaN(Number(MistVideo.options.reloadDelay)))) {
options.reload = Number(MistVideo.options.reloadDelay);
}
else {
options.reload = 10;
}
}
if (options.passive) {
if (showingError === true) { return; }
if (showingError) {
//only update the text, not the buttons or their countdowns
message_global.update(message);
since = (new Date()).getTime();
return;
}
container.setAttribute("data-passive","");
}
else {
container.removeAttribute("data-passive");
}
if (showingError) { container.clear(); } //stop any countdowns still running
showingError = (options.passive ? "passive" : true);
since = (new Date()).getTime();
var event = this.log(message,"error");
var message_container = container.message(message,false,options);
message_global = message_container;
var button_container = document.createElement("div");
button_container.className = "mistvideo-buttoncontainer";
message_container.appendChild(button_container);
MistUtil.empty(button_container);
if (options.softReload) {
var obj = {
type: "button",
label: "Reload video",
onclick: function(){
MistVideo.player.api.load();
}
};
if (!isNaN(options.softReload+"")) { obj.delay = options.softReload; }
button_container.appendChild(MistVideo.UI.buildStructure(obj));
}
if (options.reload) {
var obj = {
type: "button",
label: "Reload player",
onclick: function(){
MistVideo.reload();
}
};
if (!isNaN(options.reload+"")) { obj.delay = options.reload; }
button_container.appendChild(MistVideo.UI.buildStructure(obj));
}
if (options.nextCombo) {
var obj = {
type: "button",
label: "Next source",
onclick: function(){
MistVideo.nextCombo();
}
};
if (!isNaN(options.nextCombo+"")) { obj.delay = options.nextCombo; }
button_container.appendChild(MistVideo.UI.buildStructure(obj));
}
if (options.ignore) {
var obj = {
type: "button",
label: "Ignore",
onclick: function(){
this.clearError();
ignoreThese[identifyer] = true;
//stop showing this error
}
};
if (!isNaN(options.ignore+"")) { obj.delay = options.ignore; }
button_container.appendChild(MistVideo.UI.buildStructure(obj));
}
if (options.polling) {
button_container.appendChild(MistVideo.UI.buildStructure({type:"polling"}));
}
MistUtil.class.add(container,"show");
if ("container" in MistVideo) {
MistVideo.container.removeAttribute("data-loading");
}
if (event.defaultPrevented) {
container.clear();
}
};
container.clear = function(){
var countdowns = container.querySelectorAll("svg.icon.timeout");
for (var i = 0; i < countdowns.length; i++) {
MistVideo.timers.stop(countdowns[i].timeout);
}
MistUtil.empty(container);
MistUtil.class.remove(container,"show");
showingError = false;
};
this.clearError = container.clear;
//listener to clear error window
if ("video" in MistVideo) {
var events = ["timeupdate","playing","canplay"];//,"progress"];
for (var i in events) {
MistUtil.event.addListener(MistVideo.video,events[i],function(e){
if (!showingError) { return; }
if (e.type == "timeupdate") {
if (MistVideo.player.api.currentTime == 0) { return; }
if (((new Date()).getTime() - since) < 2e3) { return; }
}
MistVideo.log("Removing error window because of "+e.type+" event");
container.clear();
},container);
}
}
return container;
},
tooltip: function(){
var container = document.createElement("div");
var textNode = document.createTextNode("");
container.appendChild(textNode);
container.setText = function(text){
textNode.nodeValue = text;
};
var triangle = document.createElement("div");
container.triangle = triangle;
triangle.className = "triangle";
container.appendChild(triangle);
triangle.setMode = function(primary,secondary){
if (!primary) { primary = "bottom"; }
if (!secondary) { secondary = "left"; }
//reset styles
var sides = ["bottom","top","right","left"];
for (var i in sides) {
this.style[sides[i]] = ""; //bottom
var cap = MistUtil.format.ucFirst(sides[i]);
this.style["border"+cap] = ""; //borderBottom
this.style["border"+cap+"Color"] = ""; //borderBottomColor
}
var opposite = {
top: "bottom",
bottom: "top",
left: "right",
right: "left"
};
//set styles
this.style[primary] = "-10px"; //bottom
this.style["border"+MistUtil.format.ucFirst(opposite[primary])] = "none"; //borderTop
this.style["border"+MistUtil.format.ucFirst(primary)+"Color"] = "transparent"; //borderBottomColor
this.style[secondary] = 0; //left
this.style["border"+MistUtil.format.ucFirst(opposite[secondary])] = "none"; //borderRight
};
container.setPos = function(pos){
//also apply the "other" values, to reset if direction mode is switched
var set = {
left: "auto",
right: "auto",
top: "auto",
bottom: "auto"
};
MistUtil.object.extend(set,pos);
for (var i in set) {
if (!isNaN(set[i])) { set[i] += "px"; } //add px if the value is a number
this.style[i] = set[i];
}
};
return container;
},
button: function(options){ //label,onclick,timeout){
var button = document.createElement("button");
var MistVideo = this;
if (options.onclick) {
MistUtil.event.addListener(button,"click",function(){
options.onclick.call(MistVideo,arguments);
});
if (options.delay) {
var countdown = this.UI.buildStructure({
type: "timeout",
delay: options.delay,
function: options.onclick
});
if (countdown) {
button.appendChild(countdown);
}
}
}
button.appendChild(document.createTextNode(options.label));
return button;
}
},
colors: {
fill: "#fff",
semiFill: "rgba(255,255,255,0.5)",
stroke: "#fff",
strokeWidth: 1.5,
background: "rgba(0,0,0,0.8)",
progressBackground: "#333",
accent: "#0f0"
}
};
MistSkins.dev = {
structure: MistUtil.object.extend({},MistSkins["default"].structure,true),
blueprints: {
timeout: function(){ //don't use countdowns on buttons
return false;
},
log: function(){
var container = document.createElement("div");
container.appendChild(document.createTextNode("Logs"));
var logsc = document.createElement("div");//scroll this
logsc.className = "logs";
container.appendChild(logsc);
var logs = document.createElement("table");
logsc.appendChild(logs);
var MistVideo = this;
var lastmessage = {message: false};
var count = false;
var scroll = true;
function addMessage(time,message,data) {
if (!data) { data = {}; }
if (lastmessage.message == message) {
count++;
lastmessage.counter.nodeValue = count;
if ((count == 2) && (lastmessage.counter.parentElement)) {
lastmessage.counter.parentElement.style.display = "";
}
return;
}
count = 1;
var entry = document.createElement("tr");
entry.className = "entry";
if ((data.type) && (data.type != "log")) {
MistUtil.class.add(entry,"type-"+data.type);
message = MistUtil.format.ucFirst(data.type)+": "+message;
}
logs.appendChild(entry);
var timestamp = document.createElement("td");
timestamp.className = "timestamp";
entry.appendChild(timestamp);
var stamp = time.toLocaleTimeString(); //get current time in local format
//add miliseconds
var t = stamp.split(" ");
t[0] += "."+("00"+time.getMilliseconds()).slice(-3);
//t = t.join(" ");
timestamp.appendChild(document.createTextNode(t[0]));
if ("currentTime" in data) {
timestamp.title = "Video playback time: "+MistUtil.format.time(data.currentTime,{ms:true});
}
var td = document.createElement("td");
entry.appendChild(td);
var msg = document.createElement("span");
msg.className = "message";
td.appendChild(msg);
msg.appendChild(document.createTextNode(message));
var counter = document.createElement("span");
counter.style.display = "none";
counter.className = "counter";
td.appendChild(counter);
var countnode = document.createTextNode(count);
counter.appendChild(countnode);
if (scroll) { logsc.scrollTop = logsc.scrollHeight; }
lastmessage = {message: message, counter: countnode};
}
MistUtil.event.addListener(logsc,"scroll",function(){
//console.log(logsc.scrollTop + logsc.clientHeight,logsc.scrollHeight);
if (logsc.scrollTop + logsc.clientHeight >= logsc.scrollHeight - 5) {
scroll = true;
}
else {
scroll = false;
}
//console.log(scroll);
})
//add previously generated log messages
for (var i in MistVideo.logs) {
addMessage(MistVideo.logs[i].time,MistVideo.logs[i].message,MistVideo.logs[i].data);
}
MistUtil.event.addListener(MistVideo.options.target,"log",function(e){
if (!e.message) { return; }
var data = {};
if (MistVideo.player && MistVideo.player.api && ("currentTime" in MistVideo.player.api)) {
data.currentTime = MistVideo.player.api.currentTime;
}
addMessage(new Date(),e.message,data);
},container);
MistUtil.event.addListener(MistVideo.options.target,"error",function(e){
if (!e.message) { return; }
var data = {type:"error"};
if (MistVideo.player && MistVideo.player.api && ("currentTime" in MistVideo.player.api)) {
data.currentTime = MistVideo.player.api.currentTime;
}
addMessage(new Date(),e.message,data);
},container);
return container;
},
decodingIssues: function(){
if (!this.player) { return; }
var MistVideo = this;
var container = document.createElement("div");
function buildItem(options){
var label = document.createElement("label");
container.appendChild(label);
label.style.display = "none";
var text = document.createElement("span");
label.appendChild(text);
text.appendChild(document.createTextNode(options.name+":"));
text.className = "mistvideo-description";
var valuec = document.createElement("span");
label.appendChild(valuec);
var value = document.createTextNode((options.value ? options.value : ""));
valuec.appendChild(value);
var ele = document.createElement("span");
valuec.appendChild(ele);
label.set = function(val){
if (val !== 0) { this.style.display = ""; }
if (typeof val == "object") {
if (val instanceof Promise) {
val.then(function(val){
label.set(val)
},function(){});
return;
}
if ("val" in val) {
value.nodeValue = val.val;
valuec.className = "value";
}
//is there a graph already?
if (ele.children.length) {
var graph = ele.children[0];
return graph.addData(val);
}
else {
//create a graph
var graph = MistUtil.createGraph({x:[val.x],y:[val.y]},val.options);
//it's (probably) a DOM element, insert it
ele.style.display = "";
MistUtil.empty(ele);
return ele.appendChild(graph);
}
}
return value.nodeValue = val;
};
container.appendChild(label);
updates.push(function(){
var result = options.function();
label.set(result);
});
}
if (MistVideo.player.api) {
var videovalues = {
"Playback score": function(){
if ("monitor" in MistVideo) {
if (("vars" in MistVideo.monitor) && ("score" in MistVideo.monitor.vars)) {
if (MistVideo.monitor.vars.values.length) {
var last = MistVideo.monitor.vars.values[MistVideo.monitor.vars.values.length - 1];
if ("score" in last) {
var score = Math.min(1,Math.max(0,last.score));
return {
x: last.clock,
y: Math.min(1,Math.max(0,last.score)),
options: {
y: {
min: 0,
max: 1
},
x: {
count: 10
}
},
val: Math.round(Math.min(1,Math.max(0,MistVideo.monitor.vars.score))*100)+"%"
};
}
}
}
return 0;
}
},
"Corrupted frames": function(){
if ((MistVideo.player.api) && ("getVideoPlaybackQuality" in MistVideo.player.api)) {
var r = MistVideo.player.api.getVideoPlaybackQuality();
if (r) {
if (r.corruptedVideoFrames) {
return {
val: MistUtil.format.number(r.corruptedVideoFrames),
x: (new Date()).getTime()*1e-3,
y: r.corruptedVideoFrames,
options: {
x: { count: 10 }
}
};
}
return 0;
}
}
},
"Dropped frames": function(){
if ((MistVideo.player.api) && ("getVideoPlaybackQuality" in MistVideo.player.api)) {
var r = MistVideo.player.api.getVideoPlaybackQuality();
if (r) {
if (r.droppedVideoFrames) {
return MistUtil.format.number(r.droppedVideoFrames);
/* show a graph: return {
val: MistUtil.format.number(r.droppedVideoFrames),
x: (new Date()).getTime()*1e-3,
y: r.droppedVideoFrames,
options: {
x: { count: 10 },
differentiate: true,
reverseGradient: true
}
};*/
}
return 0;
}
}
},
"Total frames": function(){
if ((MistVideo.player.api) && ("getVideoPlaybackQuality" in MistVideo.player.api)) {
var r = MistVideo.player.api.getVideoPlaybackQuality();
if (r) { return MistUtil.format.number(r.totalVideoFrames); }
}
},
"Decoded audio": function(){
if (MistVideo.player.api) {
return MistUtil.format.bytes(MistVideo.player.api.webkitAudioDecodedByteCount);
}
},
"Decoded video": function(){
if (MistVideo.player.api) {
return MistUtil.format.bytes(MistVideo.player.api.webkitVideoDecodedByteCount);
}
},
"Negative acknowledgements": function(){
if (MistVideo.player.api) {
return MistUtil.format.number(MistVideo.player.api.nackCount);
}
},
"Picture losses": function(){
return MistUtil.format.number(MistVideo.player.api.pliCount);
},
"Packets lost": function(){
return MistUtil.format.number(MistVideo.player.api.packetsLost);
},
"Packets received": function(){
return MistUtil.format.number(MistVideo.player.api.packetsReceived);
},
"Bytes received": function(){
if (MistVideo.player.api) {
return MistUtil.format.bytes(MistVideo.player.api.bytesReceived);
}
},
"Local latency [ms]": function(){
if ((MistVideo.player.api) && ("getLatency" in MistVideo.player.api)) {
var p = MistVideo.player.api.getLatency();
if (p) {
return new Promise(function(resolve,reject) {
p.then(function(result){
var r = [];
for (var i in result) {
if (result[i]) {
r.push(i[0]+":"+Math.round(result[i]*1e3));
}
}
if (r.length) { resolve(r.join(" ")); }
else { resolve(); }
},reject)
});
}
return new Promise(function(resolve,reject){resolve();},function(){});
}
}
};
var updates = [];
for (var i in videovalues) {
if (typeof videovalues[i]() == "undefined") { continue; }
buildItem({
name: i,
function: videovalues[i]
});
}
container.update = function(){
for (var i in updates) {
updates[i]();
}
MistVideo.timers.start(function(){
container.update();
},1e3);
};
container.update();
}
return container;
},
forcePlayer: function(){
var container = document.createElement("label");
container.title = "Reload MistVideo and use the selected player";
var MistVideo = this;
var s = document.createElement("span");
container.appendChild(s);
s.appendChild(document.createTextNode("Force player: "));
var select = document.createElement("select");
container.appendChild(select);
var option = document.createElement("option");
select.appendChild(option);
option.value = "";
option.appendChild(document.createTextNode("Automatic"));
for (var i in mistplayers) {
var option = document.createElement("option");
select.appendChild(option);
option.value = i;
option.appendChild(document.createTextNode(mistplayers[i].name));
}
if (this.options.forcePlayer) { select.value = this.options.forcePlayer; }
MistUtil.event.addListener(select,"change",function(){
MistVideo.options.forcePlayer = (this.value == "" ? false : this.value);
if (MistVideo.options.forcePlayer != MistVideo.playerName) { //only reload if there is a change
MistVideo.reload();
}
});
return container;
},
forceType: function(){
if (!this.info) { return; }
var container = document.createElement("label");
container.title = "Reload MistVideo and use the selected protocol";
var MistVideo = this;
var s = document.createElement("span");
container.appendChild(s);
s.appendChild(document.createTextNode("Force protocol: "));
var select = document.createElement("select");
container.appendChild(select);
var option = document.createElement("option");
select.appendChild(option);
option.value = "";
option.appendChild(document.createTextNode("Automatic"));
var sofar = {};
for (var i in MistVideo.info.source) {
var source = MistVideo.info.source[i];
//skip doubles
if (source.type in sofar) { continue; }
sofar[source.type] = 1;
var option = document.createElement("option");
select.appendChild(option);
option.value = source.type;
option.appendChild(document.createTextNode(MistUtil.format.mime2human(source.type)));
}
if (this.options.forceType) { select.value = this.options.forceType; }
MistUtil.event.addListener(select,"change",function(){
MistVideo.options.forceType = (this.value == "" ? false : this.value);
if ((!MistVideo.source) || (MistVideo.options.forceType != MistVideo.source.type)) { //only reload if there is a change
MistVideo.reload();
}
});
return container;
},
forceSource: function(){
var container = document.createElement("label");
container.title = "Reload MistVideo and use the selected source";
var MistVideo = this;
var s = document.createElement("span");
container.appendChild(s);
s.appendChild(document.createTextNode("Force source: "));
var select = document.createElement("select");
container.appendChild(select);
var option = document.createElement("option");
select.appendChild(option);
option.value = "";
option.appendChild(document.createTextNode("Automatic"));
for (var i in MistVideo.info.source) {
var source = MistVideo.info.source[i];
var option = document.createElement("option");
select.appendChild(option);
option.value = i;
option.appendChild(document.createTextNode(source.url+" ("+MistUtil.format.mime2human(source.type)+")"));
}
if (this.options.forceSource) { select.value = this.options.forceSource; }
MistUtil.event.addListener(select,"change",function(){
MistVideo.options.forceSource = (this.value == "" ? false : this.value);
if (MistVideo.options.forceSource != MistVideo.source.index) { //only reload if there is a change
MistVideo.reload();
}
});
return container;
}
}
};
//MistSkins.dev.css = MistUtil.object.extend(MistSkins["default"].css);
MistSkins.dev.css = {skin: misthost+"/skins/dev.css"};
//prepend dev tools to settings window
MistSkins.dev.structure.submenu = MistUtil.object.extend({},MistSkins["default"].structure.submenu,true);
MistSkins.dev.structure.submenu.type = "draggable";
MistSkins.dev.structure.submenu.style.width = "25em";
MistSkins.dev.structure.submenu.children.unshift({
type: "container",
style: { flexShrink: 1 },
classes: ["mistvideo-column"],
children: [
{
if: function(){
return (this.playerName && this.source)
},
then: {
type: "container",
classes: ["mistvideo-description"],
style: { display: "block" },
children: [
{type: "playername", style: { display: "inline" }},
{type: "text", text: "is playing", style: {margin: "0 0.2em"}},
{type: "mimetype"}
]
}
},
{type:"log"},
{type:"decodingIssues"},
{
type: "container",
classes: ["mistvideo-column","mistvideo-devcontrols"],
style: {"font-size":"0.9em"},
children: [
{
type: "text",
text: "Player control"
},{
type: "container",
classes: ["mistvideo-devbuttons"],
style: {"flex-wrap": "wrap"},
children: [
{
type: "button",
title: "Build MistVideo again",
label: "MistVideo.reload();",
onclick: function(){
this.reload();
}
},{
type: "button",
title: "Switch to the next available player and source combination",
label: "MistVideo.nextCombo();",
onclick: function(){
this.nextCombo();
}
}
]
},
{type:"forcePlayer"},
{type:"forceType"}//,
//{type:"forceSource"}
]
}
]
});
// a skin has a structure
// a skin has formatting rules
// a skin has blueprints that build element types
function MistSkin(MistVideo) {
MistVideo.skin = this;
this.applySkinOptions = function(skinOptions) {
if ((typeof skinOptions == "string") && (skinOptions in MistSkins)) { skinOptions = MistUtil.object.extend({},MistSkins[skinOptions],true); }
var skinParent;
if (("inherit" in skinOptions) && (skinOptions.inherit) && (skinOptions.inherit in MistSkins)) {
skinParent = this.applySkinOptions(skinOptions.inherit);
}
else {
skinParent = MistSkins.default;
}
//structure should be shallow extended
this.structure = MistUtil.object.extend({},skinParent.structure);
if (skinOptions && ("structure" in skinOptions)) {
MistUtil.object.extend(this.structure,skinOptions.structure);
}
//blueprints should be shallow extended
this.blueprints = MistUtil.object.extend({},skinParent.blueprints);
if (skinOptions && ("blueprints" in skinOptions)) {
MistUtil.object.extend(this.blueprints,skinOptions.blueprints);
}
//icons should be shallow extended
this.icons = MistUtil.object.extend({},skinParent.icons,true);
if (skinOptions && ("icons" in skinOptions)) {
MistUtil.object.extend(this.icons.blueprints,skinOptions.icons);
}
this.icons.build = function(type,size,options){
if (!size) { size = 22; }
//return an svg
var d = this.blueprints[type];
var svg;
if (typeof d.svg == "function") {
svg = d.svg.call(MistVideo,options);
}
else {
svg = d.svg;
}
if (typeof size != "object") {
size = {
height: size,
width: size
};
}
if (typeof d.size != "object") {
d.size = {
height: d.size,
width: d.size
};
}
if ((!("width" in size) && ("height" in size)) || (!("height" in size) && ("width" in size))) {
if ("width" in size) {
size.height = size.width * d.size.height / d.size.width;
}
if ("height" in size) {
size.width = size.height * d.size.width / d.size.height;
}
}
var str = "";
str += '';
var container = document.createElement("div");
container.innerHTML = str;
return container.firstChild;
}
//colors should be deep extended
this.colors = MistUtil.object.extend({},skinParent.colors);
if (skinOptions && ("colors" in skinOptions)) {
MistUtil.object.extend(this.colors,skinOptions.colors,true);
}
//apply "general" css and skin specific css to structure
this.css = MistUtil.object.extend({},skinParent.css);
if (skinOptions && ("css" in skinOptions)) {
MistUtil.object.extend(this.css,skinOptions.css);
}
return this;
}
this.applySkinOptions("skin" in MistVideo.options ? MistVideo.options.skin : "default");
//load css
var styles = [];
for (var i in this.css) {
if (typeof this.css[i] == "string") {
var a = MistUtil.css.load(MistVideo.urlappend(this.css[i]),this.colors);
styles.push(a);
}
}
this.css = styles; //overwrite
return;
}