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",
classes: ["mistvideo-maincontainer"],
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: "container",
children: [
{type: "videobackground", alwaysDisplay: false, delay: 5 },
{type: "video"},
{type: "subtitles"}
]
},
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"},
{
if: function(){
//show the total time if the player size is larger than 300px
if (("size" in this) && (this.size.width > 300) || ((!this.info.hasVideo || (this.source.type.split("/")[1] == "audio")))) {
return true;
}
return false;
},
then: {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 300px
if (("size" in this) && (this.size.width > 300) || ((!this.info.hasVideo || (this.source.type.split("/")[1] == "audio")))) {
return true;
}
return false;
},
then: {
type: "container",
children: [{
type: "chromecast",
classes: ["mistvideo-pointer"]
},{
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 <= 300)) {
return true;
}
return false;
},
then: {
type: "container",
classes: ["mistvideo-center"],
children: [{
type: "chromecast",
classes: ["mistvideo-pointer"]
},{
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
var canplay = MistUtil.event.addListener(MistVideo.video,"canplay",function(){
if (MistVideo.player.api && 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.then(function(){
if (MistVideo.reporting) { MistVideo.reporting.stats.d.autoplay = "success"; }
}).then(function(){
if (MistVideo.destroyed) { return; }
MistVideo.log("Autoplay worked! Video will be unmuted on mouseover if the page has been interacted with.");
if (MistVideo.reporting) { MistVideo.reporting.stats.d.autoplay = "muted"; }
//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.player.api.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);
}).catch(function(){
if (MistVideo.destroyed) { return; }
MistVideo.log("Autoplay failed even with muted video. Unmuting and showing play button.");
//wait 5 seconds and then pause the download
MistVideo.timers.start(function(){
if (MistVideo.player.api.paused) {
//don't question it
//if the video is paused, also request the player api to pause
//for example, for mews, this would pause the download
MistVideo.player.api.pause();
if (MistVideo.monitor) { MistVideo.monitor.destroy(); }
}
},5e3);
if (MistVideo.reporting) { MistVideo.reporting.stats.d.autoplay = "failed"; }
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);
});
}
}
else if (MistVideo.reporting) { MistVideo.reporting.stats.d.autoplay = "failed"; }
});
}
}
else if (MistVideo.reporting) { MistVideo.reporting.stats.d.autoplay = "success"; }
MistUtil.event.removeListener(canplay); //only fire once
});
}
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() && this.size && (this.size.width > 300)) {
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;
var first = Infinity;
if (MistVideo.info && MistVideo.info.meta && MistVideo.info.meta.tracks) {
for (var i in MistVideo.info.meta.tracks) {
if (MistVideo.info.meta.tracks[i].firstms*1e-3 < first) {
first = MistVideo.info.meta.tracks[i].firstms*1e-3;
}
}
}
if (first == Infinity) {
first = 0;
}
function firsts() {
if (MistVideo.player.api.duration < first) { return 0; }
return first;
}
function getBufferWindow() {
var buffer_window = MistVideo.info.meta.buffer_window;
if (typeof buffer_window == "undefined") {
//for some reason, buffer_window is not defined (liveDVR?)
//just assume we can seek in our own buffer
if (MistVideo.player && MistVideo.player.api && MistVideo.player.api.buffered && MistVideo.player.api.buffered.length) {
buffer_window = (MistVideo.player.api.duration - MistVideo.player.api.buffered.start(0))*1e3;
}
else {
//no buffer of our own either? we'll just assume we have a minute
buffer_window = 60e3;
}
}
return buffer_window *= 1e-3;
}
//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 = getBufferWindow();
result = (time - MistVideo.player.api.duration + buffer_window) / buffer_window;
}
else {
result = (time - firsts()) / (MistVideo.player.api.duration - firsts());
}
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(MistVideo.player.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(MistVideo.player.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 = getBufferWindow();
//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 - firsts()) + firsts();
}
};
//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;
}
if (MistVideo.options.useDateTime && MistVideo.info && MistVideo.info.unixoffset) {
tooltip.setText(MistUtil.format.ago(new Date(MistVideo.info.unixoffset + secs*1e3)));
}
else {
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(){
if (!this.player.api || !("muted" in this.player.api)) { return false; }
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
if ((!MistVideo.player.api.volume) || (MistVideo.player.api.muted)) {
MistUtil.class.add(button,"off");
}
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){
if (!this.player.api || !("volume" in this.player.api)) { return false; }
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
button.set(MistVideo.player.api.muted ? 0 : (MistVideo.player.api.volume*100));
//apply stored volume
var initevent = MistUtil.event.addListener(video,"loadedmetadata",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;
var t;
if (MistVideo.options.useDateTime && MistVideo.info && MistVideo.info.unixoffset) {
var d = new Date(MistVideo.info.unixoffset + v*1e3);
t = MistUtil.format.ago(d);
container.setAttribute("title",MistUtil.format.ago(d,34560e6));
}
else {
t = formatTime(v);
container.setAttribute("title",t);
}
text.nodeValue = t;
};
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 = "";
if (MistVideo.options.useDateTime && MistVideo.info && MistVideo.info.unixoffset) {
var t = new Date(duration*1e3 + MistVideo.info.unixoffset)
text.nodeValue = MistUtil.format.ago(t);
container.setAttribute("title",MistUtil.format.ago(t,34560e6)); //format as if more than a year ago
}
else {
text.nodeValue = MistUtil.format.time(duration);
container.setAttribute("title",text.nodeValue);
}
};
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;
button.set = function(){
if (MistVideo.player.api.loop) {
MistUtil.class.remove(this,"off");
}
else {
MistUtil.class.add(this,"off");
}
};
MistUtil.event.addListener(button,"click",function(e){
MistVideo.player.api.loop = !MistVideo.player.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){
if (value) { MistVideo.log("User selected "+type+" track with id "+value); }
else {
MistVideo.log("User selected automatic track selection for "+type);
MistUtil.event.send("trackSetToAuto",type,MistVideo.video);
}
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 (MistUtil.array.indexOf(["video","audio","subtitle"],type) <= -1) {
//Do not display this track type
continue;
}
if (type == "subtitle") {
if ((!("player" in MistVideo)) || (!("api" in MistVideo.player)) || (!("setWSSubtitle" in MistVideo.player.api) && !("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;
}
var mime = "html5/text/vtt"
if ("setWSSubtitle" in MistVideo.player.api) {
mime = "html5/text/javascript";
}
//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 == mime) && (MistUtil.http.url.split(source.url).protocol == MistUtil.http.url.split(MistVideo.source.url).protocol.replace(/^ws/,"http"))) {
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 a 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 ("setWSSubtitle" in MistVideo.player.api) {
MistVideo.player.api.setWSSubtitle(this.value == "" ? undefined : this.value);
}
else {
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 ((MistVideo.player.api && !MistVideo.player.api.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 "+(MistVideo.casting ? "chromecast" : "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;
if (!MistVideo.casting) {
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 (MistVideo.casting && !options.passive) {
var obj = {
type: "button",
label: "Stop casting",
onclick: function(){
MistVideo.detachFromCast();
}
};
if (!isNaN(options.softReload+"")) { obj.delay = options.softReload; }
button_container.appendChild(MistVideo.UI.buildStructure(obj));
}
if (options.softReload && !MistVideo.casting) {
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("Reloading because reload button was clicked.");
}
};
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 && event.defaultPrevented) {
MistVideo.log("Error event was defaultPrevented, not showing.");
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;
},
videobackground: function(options) {
/* options.alwaysDisplay : if true, always draw the video on the canvas */
/* options.delay : delay of the draw timeout in seconds */
if (!options) { options = {}; }
if (!options.delay) { options.delay = 5; }
var ele = document.createElement("div");
var MistVideo = this;
var canvasses = [];
for (var n = 0; n < 2; n++) {
var c = document.createElement("canvas");
c._context = c.getContext("2d");
ele.appendChild(c);
canvasses.push(c);
}
var index = 0;
var drawing = false;
function draw() {
//only draw if the element is visible, don't waste cpu
if (options.alwaysDisplay || (MistVideo.video.videoWidth/MistVideo.video.videoHeight != ele.clientWidth / ele.clientHeight)) {
canvasses[index].removeAttribute("data-front"); //put last one behind again
//console.log(new Date().toLocaleTimeString(),"draw");
index++;
if (index >= canvasses.length) { index = 0; }
var c = canvasses[index];
var ctx = c._context;
c.width = MistVideo.video.videoWidth;
c.height = MistVideo.video.videoHeight;
ctx.drawImage(MistVideo.video,0,0);
c.setAttribute("data-front","");
}
if (!MistVideo.player.api.paused) {
MistVideo.timers.start(function(){
draw();
},options.delay * 1e3);
}
else {
drawing = false;
}
}
MistUtil.event.addListener(MistVideo.video,"playing",function(){
if (!drawing) {
draw();
drawing = true;
}
});
return ele;
},
subtitles: function(options){
if (!("WebSocket" in window)) { return false; }
var MistVideo = this;
if (!("player" in MistVideo) || !("api" in MistVideo.player) || !("currentTime" in MistVideo.player.api)) { return false; }
if (!("metaTrackSubscriptions" in MistVideo)) { return false; }
function clearFormatting(str) {
str = str.replace(/\<\/?[bui]\>/gi,""); //remove ,,,,,
str = str.replace(/{\/?[bui]}/gi,""); //remove {b},{/b},{u},{/u},{i},{/i}
str = str.replace(/{\\a\d+}/gi,""); //remove {\a3} (line position)
str = str.replace(/\<\/?font[^>]*?\>/gi,""); //remove ,
return str;
}
var container = document.createElement("div");
var c = document.createElement("span");
container.appendChild(c);
var textNode = document.createTextNode("");
c.appendChild(textNode);
var timer = false;
function displayMessage(message) {
textNode.nodeValue = clearFormatting(message.data);
if (timer) {
//a previous message is still being displayed, remove the timer so that it doesn't remove the new message
MistVideo.timers.stop(timer);
timer = null;
}
function setTimer(delay) {
timer = MistVideo.timers.start(function(){
if (MistVideo.player.api.paused) {
//leave the subtitle for now, and start a new timer once the video starts playing
var playing = MistUtil.event.addListener(MistVideo.video,"playing",function(){
setTimer(message.time + ("duration" in message ? message.duration : 5e3) - MistVideo.player.api.currentTime*1e3);
MistUtil.event.removeListener(playing);
});
return;
}
textNode.nodeValue = "";
},delay);
}
setTimer("duration" in message ? message.duration : 5e3);
}
//when seeking, clear the current subtitle message
MistUtil.event.addListener(MistVideo.video,"seeked",function(){
textNode.nodeValue = "";
if (timer) { MistVideo.timers.stop(timer); }
timer = null;
});
if (!("setWSSubtitle" in MistVideo.player.api)) {
//insert generic subtitle function unless it already exists
var trackid = false;
MistVideo.player.api.setWSSubtitle = function(id){
if (id == trackid) { return; } //already selected
//first add, then remove: this prevents the websocket closing because no tracks are selected
if (typeof id != "undefined") {
MistVideo.metaTrackSubscriptions.add(id,displayMessage);
}
if (id != trackid) {
MistVideo.metaTrackSubscriptions.remove(trackid,displayMessage);
}
trackid = (id == "undefined" ? false : id);
}
}
return container;
},
chromecast: function(){
var MistVideo = this;
if ((!"".indexOf) || (MistVideo.options.host.indexOf("localhost") > -1) || (MistVideo.options.host.indexOf("::1") > -1)) { return; } //it's old IE or the Mist host is localhost and so it won't work ^_^
var ele = document.createElement("div");
var casting_ele = document.createElement("div");
casting_ele.className = "mistvideo-casting";
var api, reload, nextCombo, unload;
var selectedTracks = {};
var remoteData = {
currentTime: false,
paused: true,
volume: 1,
muted: false,
buffer: [],
loop: (MistVideo.player.api ? MistVideo.player.api.loop : MistVideo.options.loop)
};
MistVideo.casting = false;
function onCastLoad(tries) {
if (!window.chrome || !window.chrome.cast || !window.chrome.cast.isAvailable || (tries > 5)) {
if (ele.parentNode) { ele.parentNode.removeChild(ele); }
MistVideo.log("Chromecast is not supported");
console.warn(chrome,chrome.cast,chrome.cast ? chrome.cast.isAvailable : undefined,cast);
return;
}
if (!window.cast) {
if (!tries) { tries = 0; }
MistVideo.log("Casting api loaded but cast function not yet available, retrying..");
MistVideo.timers.start(function(){
onCastLoad(tries++);
},200)
return;
}
MistVideo.log("Chromecast API loaded");
if (!window.loadedCastApi || (window.loadedCastApi == "loading")) {
window.loadedCastApi = true; //so we don't try this multiple times
}
var cast_ele = document.createElement("google-cast-launcher");
ele.appendChild(cast_ele);
cast.framework.CastContext.getInstance().setOptions({
receiverApplicationId: "E5F1558C",
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
});
function detachFromCast(keepSession) {
MistUtil.class.remove(cast_ele,"active");
MistUtil.class.remove(MistVideo.container,"casting");
if (casting_ele.parentNode) { casting_ele.parentNode.removeChild(casting_ele) }
if (!keepSession && (cast.framework.CastContext.getInstance().getCurrentSession())) {
cast.framework.CastContext.getInstance().getCurrentSession().endSession(true);
}
if (api) {
MistVideo.player.api = api;
api.currentTime = remoteData.currentTime;
api.play();
MistVideo.reload = reload;
MistVideo.nextCombo = nextCombo;
MistVideo.unload = unload;
}
else {
MistVideo.player.api.play();
}
if (MistVideo.player.api && MistVideo.player.api.setTracks && MistUtil.object.keys(selectedTracks).length) {
MistVideo.player.api.setTracks(selectedTracks);
}
MistVideo.casting = false;
MistVideo.log("Detached chromecast session");
}
MistVideo.detachFromCast = detachFromCast;
cast_ele.addEventListener("click",function(e){
e.stopPropagation();
//if (false) {
if (MistUtil.class.has(cast_ele,"active")) {
detachFromCast();
}
else {
MistUtil.class.add(cast_ele,"active");
casting_ele.innerHTML = "Select a device to cast to";
MistVideo.container.appendChild(casting_ele);
MistUtil.class.add(MistVideo.container,"casting");
MistVideo.log("chromecast: pausing player")
MistVideo.player.api.pause();
MistVideo.container.setAttribute("data-loading","waiting for cast");
MistVideo.casting = true;
if (!window.MistCast) {
window.MistCast = {
send: function(obj){
cast.framework.CastContext.getInstance().getCurrentSession().sendMessage("urn:x-cast:mistcaster",JSON.stringify(obj));
}
};
}
function loadStream() {
cast.framework.CastContext.getInstance().getCurrentSession().addMessageListener("urn:x-cast:mistcaster",function(ns,message){
if (MistVideo.destroyed) { detachFromCast(); }
message = JSON.parse(message);
//console.log("chromecast message received:",message);
if (message.type) {
switch(message.type){
case "log":
case "error": {
MistVideo.log("[Chromecast] "+message.message,message.type);
break;
}
case "showError":{
MistVideo.showError.apply(MistVideo,message.args);
break;
}
case "event": {
switch (message.event) {
case "timeupdate": {
remoteData.currentTime = message.currentTime;
MistUtil.event.send(message.event,"chromecast",MistVideo.video);
break;
}
case "progress": {
remoteData.buffer = message.buffer;
MistUtil.event.send(message.event,"chromecast",MistVideo.video);
break;
}
case "pause":
case "paused":
case "ended":
case "play":
case "playing": {
remoteData.paused = message.paused;
MistUtil.event.send(message.event,"chromecast",MistVideo.video);
break;
}
case "volumechange": {
remoteData.volume = message.volume;
remoteData.muted = message.muted;
MistUtil.event.send(message.event,"chromecast",MistVideo.video);
break;
}
default: {
MistUtil.event.send(message.event,"chromecast",MistVideo.video);
}
}
break;
}
case "detach": {
if (message.n == MistVideo.n) {
detachFromCast(true); //don't murder the session
}
break;
}
default: {
console.log("Unknown chromecast message type",message);
}
}
}
});
var d = {
type: "load",
n: MistVideo.n, //indentify which player instance we are
options: {
host: MistVideo.options.host,
loop: MistVideo.options.loop, //will be overwritten with the current value if there is a player api
poster: MistVideo.options.poster, //should be an absolute url, because the location will be different
streaminfo: MistVideo.options.streaminfo,
urlappend: MistVideo.options.urlappend,
forcePriority: MistVideo.options.forcePriority,
setTracks: MistVideo.options.setTracks, //when the track selection is changed through the UI, the selected track is saved in the options, so this passes on the currently enforced tracks
controls: false,
skin: "default" //TODO: right now the skin can't really be transfered because there are functions in there that won't be in the JSON. At some point we should fix this, probably by having the Mist backend include a custom skin definition with the player code.
},
stream: MistVideo.stream
};
if (MistVideo.info && (MistVideo.info.type != "live")) {
d.time = MistVideo.player.api.currentTime;
}
if (MistVideo.options.skin == "dev") {
d.options.skin = MistVideo.options.skin;
}
if (MistVideo.player && MistVideo.player.api) {
d.volume = MistVideo.player.api.volume;
d.muted = MistVideo.player.api.muted;
d.options.loop = MistVideo.player.api.loop;
}
MistCast.send(d);
api = MistVideo.player.api;
reload = MistVideo.reload;
nextCombo = MistVideo.nextCombo;
unload = MistVideo.unload;
selectedTracks = MistVideo.options.setTracks ? MistVideo.options.setTracks : {};
MistVideo.player.api = new Proxy(api,{
get: function(target,key,receiver){
var property = target[key];
switch (key) {
case "muted":
case "volume":
case "currentTime":
case "paused":
case "loop": {
return remoteData[key];
}
case "buffered": {
return new function(){
this.length = remoteData.buffer.length;
this.start = function(i){
return remoteData.buffer[i][0];
}
this.end = function(i){
return remoteData.buffer[i][1];
}
}
}
case "setTracks":
case "play":
case "pause": {
return function() {
//put the arguments into an array so JSON doesn't turn it into an object
var a = [];
for (var i = 0; i < arguments.length; i++) {
a.push(arguments[i]);
}
if (key == "setTracks") {
//save the selected tracks so that we can restore them when casting detaches
MistUtil.object.extend(selectedTracks,arguments[0]);
}
MistCast.send({type: "cmd", cmd: key, args: a});
}
}
}
return property;
},
set: function(target,key,value){
if (key == "loop") {
remoteData[key] = value; //just assume chromecast will apply it properly
}
//console.log("set",key,value);
MistCast.send({type:"set",cmd:key,value:value});
}
});
api.pause();
MistVideo.reload = function(){
MistCast.send({type:"reload"});
};
MistVideo.nextCombo = function(){
MistCast.send({type:"nextCombo"});
};
MistVideo.unload = function(){
//this one should actually unload the current player, but detach the chromecast session first
detachFromCast();
return unload.apply(this,arguments);
};
var d = cast.framework.CastContext.getInstance().getCurrentSession().getCastDevice();
if (d && d.friendlyName) { casting_ele.innerHTML = "Casting to "+d.friendlyName; }
var on_session_end = function(){
if (cast.framework.CastContext.getInstance().getSessionState() == "SESSION_ENDED") {
if (MistUtil.class.has(cast_ele,"active")) {
detachFromCast();
}
cast.framework.CastContext.getInstance().removeEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED,on_session_end);
}
};
cast.framework.CastContext.getInstance().addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED,on_session_end);
/*for (let i of ["sessionstatechanged","caststatechanged"]) {
cast.framework.CastContext.getInstance().addEventListener(i,function(){
console.warn(i,cast.framework.CastContext.getInstance().getSessionState());
});
}*/
MistVideo.log("Attached chromecast session");
}
if (cast.framework.CastContext.getInstance().getCurrentSession()) {
loadStream();
}
else {
cast.framework.CastContext.getInstance().requestSession().then(function(){
//console.log("Session requested");
if (!cast.framework.CastContext.getInstance().getCurrentSession()) { throw "Could not connect to the cast device"; }
loadStream();
},function(e){
MistVideo.log("Chromecast session ended: "+e);
detachFromCast();
});
}
}
},true); //capture so that chrome's own handler doesn't fire
}
if ((!window.chrome || !window.chrome.cast) && (!window.loadedCastApi)) {
window['__onGCastApiAvailable'] = function(loaded, errorInfo) {
if (!loaded) {
MistVideo.log("Error while loading chromecast API: "+errorInfo);
}
onCastLoad();
};
window.loadedCastApi = "loading";
var script = document.createElement("script");
script.setAttribute("src","//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1");
document.head.appendChild(script);
MistVideo.log("Appending chromecast script");
}
else {
if (window.loadedCastApi == "loading") {
MistVideo.log("Not appending chromecast script - still loading");
MistVideo.timers.start(function(){
onCastLoad();
},200);
}
else {
MistVideo.log("Not appending chromecast script - already loaded");
onCastLoad();
}
}
return ele;
}
},
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 unless MistVideo.options.reloadDelay is set
if (this.options.reloadDelay !== false) {
return MistSkins.default.blueprints.timeout.apply(this,arguments);
}
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") {
try {
if (val instanceof Promise) {
val.then(function(val){
label.set(val)
},function(){});
return;
}
}
catch (e) {}
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) {
if ("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;
}
}
if ("webkitDroppedFrameCount" in MistVideo.player.api) {
return MistVideo.player.api.webkitDroppedFrameCount;
}
}
},
"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(){});
}
},
"Current bitrate": function(){
if (MistVideo.player.monitor && ("currentBps" in MistVideo.player.monitor)) {
var out = MistUtil.format.bits(MistVideo.player.monitor.currentBps);
return out ? out+"ps" : out;
}
if (MistVideo.player.api && "currentBps" in MistVideo.player.api) {
var out = MistUtil.format.bits(MistVideo.player.api.currentBps());
return out ? out+"ps" : out;
}
},
"Framerate in": function(){
if (MistVideo.player.api && "framerate_in" in MistVideo.player.api) {
return MistUtil.format.number(MistVideo.player.api.framerate_in());
}
},
"Framerate out": function(){
if (MistVideo.player.api && "framerate_out" in MistVideo.player.api) {
return MistUtil.format.number(MistVideo.player.api.framerate_out());
}
}
};
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("Reloading to force player.");
}
});
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("Reloading to force new type.");
}
});
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("Reloading to force new source.");
}
});
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","mistvideo-displayCombo"],
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("Dev-reload button clicked.");
}
},{
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;
}