
Embed: display more consistent timestamps across protocols and correctly show seekable range Embed: fix: don't force nonexistent forcePlayer value Embed: when info.unixoffset is known, display clock time based timestamps
352 lines
12 KiB
JavaScript
352 lines
12 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 (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);
|
|
|
|
}
|
|
}
|