mistserver/embed/wrappers/videojs.js

379 lines
13 KiB
JavaScript

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 (MistUtil.array.indexOf(this.mimes,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;
}
//capture any errors generated before videojs is initialized and ignore them
var captureErrors = MistUtil.event.addListener(ele,"error",function(e){
e.stopImmediatePropagation();
var msg = e.message;
if (!msg && ele.error) {
if (("code" in ele.error) && (ele.error.code)) {
msg = "Code "+ele.error.code;
for (var i in ele.error) {
if (i == "code") { continue; }
if (ele.error[i] == ele.error.code) {
msg = i;
break;
}
}
}
else {
msg = JSON.stringify(ele.error);
}
}
MistVideo.log("Error captured and stopped because videojs has not yet loaded: "+msg);
});
//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(){
//remove error grabbing
MistUtil.event.removeListener(captureErrors);
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 && e.target && e.target.error && e.target.error.message && (MistUtil.array.indexOf(e.target.error.message,"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;
};
//get first and lastms
var lastms = 0;
var firstms = Infinity;
for (var i in MistVideo.info.meta.tracks) {
lastms = Math.max(lastms,MistVideo.info.meta.tracks[i].lastms);
firstms = Math.min(firstms,MistVideo.info.meta.tracks[i].firstms);
}
//correct the currentTime timestamp
var correction = firstms*1e-3;
overrides.get.duration = function(){
if (MistVideo.info) {
var duration = ele.duration;
return duration + correction;
}
return 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);
}
overrides.get.currentTime = function(){
var time = MistVideo.player.videojs ? MistVideo.player.videojs.currentTime() : ele.currentTime;
if (isNaN(time)) { return 0; }
return time + correction;
}
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) + correction; },
end: function(i) { return buffered.end(i) + correction; }
}
};
if (MistVideo.info.type == "live") {
MistVideo.player.api.lastProgress = new Date();
MistVideo.player.api.liveOffset = 0;
}
}
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);
}
}