mistplayers.videojs = { name: "VideoJS player", mimes: ["html5/application/vnd.apple.mpegurl","html5/application/vnd.apple.mpegurl;version=7"], priority: MistUtil.object.keys(mistplayers).length + 1, isMimeSupported: function (mimetype) { return (this.mimes.indexOf(mimetype) == -1 ? false : true); }, isBrowserSupported: function (mimetype,source,MistVideo) { //check for http/https mismatch if (location.protocol != MistUtil.http.url.split(source.url).protocol) { MistVideo.log("HTTP/HTTPS mismatch for this source"); return false; } //don't use videojs if this location is loaded over file:// if ((location.protocol == "file:") && (mimetype == "html5/application/vnd.apple")) { MistVideo.log("This source ("+mimetype+") won't load if the page is run via file://"); return false; } return ("MediaSource" in window); }, player: function(){}, scriptsrc: function(host) { return host+"/videojs.js"; } }; var p = mistplayers.videojs.player; p.prototype = new MistPlayer(); p.prototype.build = function (MistVideo,callback) { var me = this; //to allow nested functions to access the player class itself var ele; function onVideoJSLoad () { if (MistVideo.destroyed) { return;} MistVideo.log("Building VideoJS player.."); ele = document.createElement("video"); if (MistVideo.source.type != "html5/video/ogg") { ele.crossOrigin = "anonymous"; //required for subtitles, but if ogg, the video won"t load } ele.setAttribute("playsinline",""); //for apple var shortmime = MistVideo.source.type.split("/"); if (shortmime[0] == "html5") { shortmime.shift(); } var source = document.createElement("source"); source.setAttribute("src",MistVideo.source.url); me.source = source; ele.appendChild(source); source.type = shortmime.join("/"); MistVideo.log("Adding "+source.type+" source @ "+MistVideo.source.url); //if (source.type.indexOf("application/vnd.apple.mpegurl") >= 0) { source.type = "application/x-mpegURL"; } //source.type = "application/vnd.apple.mpegurl"; MistUtil.class.add(ele,"video-js"); var vjsopts = {}; if (MistVideo.options.autoplay) { vjsopts.autoplay = true; } if ((MistVideo.options.loop) && (MistVideo.info.type != "live")) { //vjsopts.loop = true; ele.setAttribute("loop",""); } if (MistVideo.options.muted) { //vjsopts.muted = true; ele.setAttribute("muted",""); } if (MistVideo.options.poster) { vjsopts.poster = MistVideo.options.poster; } if (MistVideo.options.controls == "stock") { ele.setAttribute("controls",""); if (!document.getElementById("videojs-css")) { var style = document.createElement("link"); style.rel = "stylesheet"; style.href = MistVideo.options.host+"/skins/videojs.css"; style.id = "videojs-css"; document.head.appendChild(style); } } else { vjsopts.controls = false; } //for android < 7, enable override native function androidVersion(){ var match = navigator.userAgent.toLowerCase().match(/android\s([\d\.]*)/i); return match ? match[1] : false; } var android = MistUtil.getAndroid(); if (android && (parseFloat(android) < 7)) { MistVideo.log("Detected android < 7: instructing videojs to override native playback"); vjsopts.html5 = {hls: {overrideNative: true}}; vjsopts.nativeAudioTracks = false; vjsopts.nativeVideoTracks = false; } me.onready(function(){ MistVideo.log("Building videojs"); me.videojs = videojs(ele,vjsopts,function(){ MistVideo.log("Videojs initialized"); if (MistVideo.info.type == "live") { //overwrite the stream info's buffer window to the seekable range as indicated by the m3u8 MistUtil.event.addListener(ele,"progress",function(e){ var i = MistVideo.player.videojs.seekable().length-1; MistVideo.info.meta.buffer_window = (Math.max(MistVideo.player.videojs.seekable().end(i),ele.duration) - MistVideo.player.videojs.seekable().start(i))*1e3; }); } }); MistUtil.event.addListener(ele,"error",function(e){ if (e.target.error.message.indexOf("NS_ERROR_DOM_MEDIA_OVERFLOW_ERR") >= 0) { //there is a problem with a certain segment, try reloading MistVideo.timers.start(function(){ MistVideo.log("Reloading player because of NS_ERROR_DOM_MEDIA_OVERFLOW_ERR"); MistVideo.reload(); },1e3); } }); me.api.unload = function(){ if (me.videojs) { me.videojs.autoplay(false); //don't play again ffs me.videojs.pause(); //pause goddamn me.videojs.dispose(); //and now die, bitch me.videojs = false; MistVideo.log("Videojs instance disposed"); } }; }); MistVideo.log("Built html"); if (("Proxy" in window) && ("Reflect" in window)) { var overrides = { get: {}, set: {} }; MistVideo.player.api = new Proxy(ele,{ get: function(target, key, receiver){ if (key in overrides.get) { return overrides.get[key].apply(target, arguments); } var method = target[key]; if (typeof method === "function"){ return function () { return method.apply(target, arguments); } } return method; }, set: function(target, key, value) { if (key in overrides.set) { return overrides.set[key].call(target,value); } return target[key] = value; } }); MistVideo.player.api.load = function(){}; overrides.set.currentTime = function(value){ MistVideo.player.videojs.currentTime(value); //seeking backwards does not work if we set it on the video directly //MistVideo.video.currentTime = value; }; if (MistVideo.info.type == "live") { function getLastBuffer(video) { var buffer_end = 0; if (video.buffered.length) { buffer_end = video.buffered.end(video.buffered.length-1) } return buffer_end; } var HLSlatency = 0; //best guess.. overrides.get.duration = function(){ if (MistVideo.info) { var duration = ele.duration; return duration; } return 0; }; MistVideo.player.api.lastProgress = new Date(); MistVideo.player.api.liveOffset = 0; MistUtil.event.addListener(ele,"progress",function(){ MistVideo.player.api.lastProgress = new Date(); }); overrides.set.currentTime = function(value){ var diff = MistVideo.player.api.currentTime - value; var offset = value - MistVideo.player.api.duration; MistVideo.log("Seeking to "+MistUtil.format.time(value)+" ("+Math.round(offset*-10)/10+"s from live)"); MistVideo.player.videojs.currentTime(MistVideo.video.currentTime - diff); } var lastms = 0; overrides.get.currentTime = function(){ if (MistVideo.info) { lastms = MistVideo.info.lastms*1e-3; } var time = MistVideo.player.videojs ? MistVideo.player.videojs.currentTime() : ele.currentTime; if (isNaN(time)) { return 0; } return time; } overrides.get.buffered = function(){ var buffered = MistVideo.player.videojs ? MistVideo.player.videojs.buffered() : ele.buffered; return { length: buffered.length, start: function(i) { return buffered.start(i); }, end: function(i) { return buffered.end(i); i} } }; } } else { me.api = ele; } MistVideo.player.setSize = function(size){ if ("videojs" in MistVideo.player) { MistVideo.player.videojs.dimensions(size.width,size.height); //for some reason, the videojs' container won't be resized with the method above. //so let's cheat and do it ourselves ele.parentNode.style.width = size.width+"px"; ele.parentNode.style.height = size.height+"px"; } this.api.style.width = size.width+"px"; this.api.style.height = size.height+"px"; }; MistVideo.player.api.setSource = function(url) { if (!MistVideo.player.videojs) { return; } if (MistVideo.player.videojs.src() != url) { MistVideo.player.videojs.src({ type: MistVideo.player.videojs.currentSource().type, src: url }); } }; MistVideo.player.api.setSubtitle = function(trackmeta) { //remove previous subtitles var tracks = ele.getElementsByTagName("track"); for (var i = tracks.length - 1; i >= 0; i--) { ele.removeChild(tracks[i]); } if (trackmeta) { //if the chosen track exists //add the new one var track = document.createElement("track"); ele.appendChild(track); track.kind = "subtitles"; track.label = trackmeta.label; track.srclang = trackmeta.lang; track.src = trackmeta.src; track.setAttribute("default",""); } }; if (MistVideo.info.type == "live") { //for some reason, videojs doesn't always fire the canplay event ??? //mitigate by sending one when durationchange follows loadstart var loadstart = MistUtil.event.addListener(ele,"loadstart",function(e){ MistUtil.event.removeListener(loadstart); MistUtil.event.send("canplay",false,this); }); var canplay = MistUtil.event.addListener(ele,"canplay",function(e){ //remove the listener if (loadstart) { MistUtil.event.removeListener(loadstart); } MistUtil.event.removeListener(canplay); }); } callback(ele); } if ("videojs" in window) { onVideoJSLoad(); } else { //load the videojs player var timer = false; function reloadVJSrateLimited(){ try { MistVideo.video.pause(); } catch (e) {} MistVideo.showError("Error in videojs player"); //rate limit the reload if (!window.mistplayer_videojs_failures) { window.mistplayer_videojs_failures = 1; MistVideo.reload(); } else { if (!timer) { var delay = 0.05*Math.pow(2,window.mistplayer_videojs_failures) MistVideo.log("Rate limiter activated: MistPlayer reload delayed by "+Math.round(delay*10)/10+" seconds.","error"); timer = MistVideo.timers.start(function(){ timer = false; delete window.videojs; MistVideo.reload(); },delay*1e3); window.mistplayer_videojs_failures++; } } } var scripturl = MistVideo.urlappend(mistplayers.videojs.scriptsrc(MistVideo.options.host)); var scripttag; var f = function (msg, url, lineNo, columnNo, error) { if (!scripttag) { return; } if (url == scripttag.src) { //error in internal videojs code //console.error(me.videojs,MistVideo.video,ele,arguments); window.removeEventListener("error",f); reloadVJSrateLimited(); } return false; }; window.addEventListener("error",f); //disabled for now because it seemed to cause more issues than it solved /*var old_console_error = console.error; console.error = function(){ if (arguments[0] == "VIDEOJS:") { if ((arguments.length > 3) && arguments[4] && (arguments[4].code == 3)) { return; } //it's a decoding error, nothing in videojs itself //videojs reports an error console.error = old_console_error; reloadVJSrateLimited(); } return old_console_error.apply(this,arguments); };*/ scripttag = MistUtil.scripts.insert(scripturl,{ onerror: function(e){ var msg = "Failed to load videojs.js"; if (e.message) { msg += ": "+e.message; } MistVideo.showError(msg); }, onload: onVideoJSLoad },MistVideo); } }