
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
1270 lines
49 KiB
JavaScript
1270 lines
49 KiB
JavaScript
mistplayers.mews = {
|
|
name: "MSE websocket player",
|
|
mimes: ["ws/video/mp4","ws/video/webm"],
|
|
priority: MistUtil.object.keys(mistplayers).length + 1,
|
|
isMimeSupported: function (mimetype) {
|
|
return (this.mimes.indexOf(mimetype) == -1 ? false : true);
|
|
},
|
|
isBrowserSupported: function (mimetype,source,MistVideo) {
|
|
|
|
if ((!("WebSocket" in window)) || (!("MediaSource" in window))) { return false; }
|
|
|
|
//check for http/https mismatch
|
|
if (location.protocol.replace(/^http/,"ws") != MistUtil.http.url.split(source.url.replace(/^http/,"ws")).protocol) {
|
|
MistVideo.log("HTTP/HTTPS mismatch for this source");
|
|
return false;
|
|
}
|
|
|
|
//it runs on MacOS, but breaks often on seek/track switch etc
|
|
if (navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
|
return false;
|
|
}
|
|
|
|
//check (and save) codec compatibility
|
|
function translateCodec(track) {
|
|
function bin2hex(index) {
|
|
return ("0"+track.init.charCodeAt(index).toString(16)).slice(-2);
|
|
}
|
|
switch (track.codec) {
|
|
case "AAC":
|
|
return "mp4a.40.2";
|
|
case "MP3":
|
|
return "mp4a.40.34";
|
|
case "AC3":
|
|
return "ec-3";
|
|
case "H264":
|
|
return "avc1."+bin2hex(1)+bin2hex(2)+bin2hex(3);
|
|
case "HEVC":
|
|
return "hev1."+bin2hex(1)+bin2hex(6)+bin2hex(7)+bin2hex(8)+bin2hex(9)+bin2hex(10)+bin2hex(11)+bin2hex(12);
|
|
default:
|
|
return track.codec.toLowerCase();
|
|
}
|
|
|
|
}
|
|
var codecs = {};
|
|
for (var i in MistVideo.info.meta.tracks) {
|
|
if (MistVideo.info.meta.tracks[i].type != "meta") {
|
|
codecs[translateCodec(MistVideo.info.meta.tracks[i])] = MistVideo.info.meta.tracks[i].codec;
|
|
}
|
|
}
|
|
var container = mimetype.split("/")[2];
|
|
function test(codecs) {
|
|
//if (container == "webm") { return true; }
|
|
return MediaSource.isTypeSupported("video/"+container+";codecs=\""+codecs+"\"");
|
|
}
|
|
source.supportedCodecs = [];
|
|
for (var i in codecs) {
|
|
//i is the long name (like mp4a.40.2), codecs[i] is the short name (like AAC)
|
|
var s = test(i);
|
|
if (s) {
|
|
source.supportedCodecs.push(codecs[i]);
|
|
}
|
|
}
|
|
if ((!MistVideo.options.forceType) && (!MistVideo.options.forcePlayer)) { //unless we force mews, skip this players if not both video and audio are supported
|
|
if (source.supportedCodecs.length < source.simul_tracks) {
|
|
MistVideo.log("Not enough playable tracks for this source");
|
|
return false;
|
|
}
|
|
}
|
|
return source.supportedCodecs.length > 0;
|
|
},
|
|
player: function(){}
|
|
};
|
|
var p = mistplayers.mews.player;
|
|
p.prototype = new MistPlayer();
|
|
p.prototype.build = function (MistVideo,callback) {
|
|
|
|
var video = document.createElement("video");
|
|
video.setAttribute("playsinline",""); //iphones. effin' iphones.
|
|
|
|
//apply options
|
|
var attrs = ["autoplay","loop","poster"];
|
|
for (var i in attrs) {
|
|
var attr = attrs[i];
|
|
if (MistVideo.options[attr]) {
|
|
video.setAttribute(attr,(MistVideo.options[attr] === true ? "" : MistVideo.options[attr]));
|
|
}
|
|
}
|
|
if (MistVideo.options.muted) {
|
|
video.muted = true; //don't use attribute because of Chrome bug
|
|
}
|
|
if (MistVideo.info.type == "live") {
|
|
video.loop = false;
|
|
}
|
|
if (MistVideo.options.controls == "stock") {
|
|
video.setAttribute("controls","");
|
|
}
|
|
video.setAttribute("crossorigin","anonymous");
|
|
this.setSize = function(size){
|
|
video.style.width = size.width+"px";
|
|
video.style.height = size.height+"px";
|
|
};
|
|
|
|
var player = this;
|
|
//player.debugging = true;
|
|
//player.debugging = "dl"; //download appended data on ms close
|
|
player.built = false;
|
|
|
|
//this function is called both when the websocket is ready and the media source is ready - both should be open to proceed
|
|
function checkReady() {
|
|
//console.log("checkready",player.ws.readyState,player.ms.readyState);
|
|
if ((player.ws.readyState == player.ws.OPEN) && (player.ms.readyState == "open") && (player.sb)) {
|
|
if (!player.built) {
|
|
callback(video);
|
|
player.built = true;
|
|
}
|
|
if (MistVideo.options.autoplay) {
|
|
player.api.play().catch(function(){});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
this.msoninit = []; //array of functions that will be executed once ms is open
|
|
this.msinit = function() {
|
|
return new Promise(function(resolve,reject){
|
|
//prepare mediasource
|
|
player.ms = new MediaSource();
|
|
video.src = URL.createObjectURL(player.ms);
|
|
player.ms.onsourceopen = function(){
|
|
for (var i in player.msoninit) {
|
|
player.msoninit[i]();
|
|
}
|
|
player.msoninit = [];
|
|
resolve();
|
|
};
|
|
player.ms.onsourceclose = function(e){
|
|
if (player.debugging) console.error("ms close",e);
|
|
send({type:"stop"}); //stop sending data please something went wrong
|
|
};
|
|
player.ms.onsourceended = function(e){
|
|
if (player.debugging) console.error("ms ended",e);
|
|
|
|
if (player.debugging == "dl") {
|
|
function downloadBlob (data, fileName, mimeType) {
|
|
var blob, url;
|
|
blob = new Blob([data], {
|
|
type: mimeType
|
|
});
|
|
url = window.URL.createObjectURL(blob);
|
|
downloadURL(url, fileName);
|
|
setTimeout(function() {
|
|
return window.URL.revokeObjectURL(url);
|
|
}, 1000);
|
|
};
|
|
|
|
function downloadURL (data, fileName) {
|
|
var a;
|
|
a = document.createElement('a');
|
|
a.href = data;
|
|
a.download = fileName;
|
|
document.body.appendChild(a);
|
|
a.style = 'display: none';
|
|
a.click();
|
|
a.remove();
|
|
};
|
|
|
|
var l = 0;
|
|
for (var i = 0; i < player.sb.appended.length; i++) {
|
|
l += player.sb.appended[i].length;
|
|
}
|
|
var d = new Uint8Array(l);
|
|
var l = 0;
|
|
for (var i = 0; i < player.sb.appended.length; i++) {
|
|
d.set(player.sb.appended[i],l);
|
|
l += player.sb.appended[i].length;
|
|
}
|
|
|
|
downloadBlob(d, 'appended.mp4.bin', 'application/octet-stream');
|
|
}
|
|
send({type:"stop"}); //stop sending data please something went wrong
|
|
};
|
|
});
|
|
}
|
|
this.msinit().then(function(){
|
|
if (player.sb) {
|
|
MistVideo.log("Not creating source buffer as one already exists.");
|
|
checkReady();
|
|
return;
|
|
}
|
|
});
|
|
this.onsbinit = [];
|
|
this.sbinit = function(codecs){
|
|
if (!codecs) { MistVideo.showError("Did not receive any codec: nothing to initialize."); return; }
|
|
|
|
//console.log("sourcebuffers",player.ms.sourceBuffers.length);
|
|
//console.log("sb init","video/"+MistVideo.source.type.split("/")[2]+";codecs=\""+codecs.join(",")+"\"");
|
|
player.sb = player.ms.addSourceBuffer("video/"+MistVideo.source.type.split("/")[2]+";codecs=\""+codecs.join(",")+"\"");
|
|
player.sb.mode = "segments"; //the fragments will be put in the buffer at the correct time: much better behavior when seeking / not playing from 0s
|
|
|
|
//save the current source buffer codecs
|
|
player.sb._codecs = codecs;
|
|
|
|
player.sb._size = 0;
|
|
player.sb.queue = [];
|
|
var do_on_updateend = [];
|
|
player.sb.do_on_updateend = do_on_updateend; //so we can check it from the ws onmessage handler too
|
|
player.sb.appending = null;
|
|
player.sb.appended = [];
|
|
var n = 0;
|
|
player.sb.addEventListener("updateend",function(){
|
|
if (!player.sb) {
|
|
MistVideo.log("Reached updateend but the source buffer is "+JSON.stringify(player.sb)+". ");
|
|
return;
|
|
}
|
|
//player.sb._busy = true;
|
|
//console.log("start updateend");
|
|
|
|
if (player.debugging) {
|
|
if (player.sb.appending) player.sb.appended.push(player.sb.appending);
|
|
player.sb.appending = null;
|
|
}
|
|
|
|
//every 500 fragments, clean the buffer (about every 15 sec)
|
|
if (n >= 500) {
|
|
//console.log(n,video.currentTime - video.buffered.start(0));
|
|
n = 0;
|
|
player.sb._clean(10); //keep 10 sec
|
|
}
|
|
else {
|
|
n++;
|
|
}
|
|
|
|
var do_funcs = do_on_updateend.slice(); //clone the array
|
|
do_on_updateend = [];
|
|
for (var i in do_funcs) {
|
|
//console.log("do_funcs",Number(i)+1,"/",do_funcs.length);
|
|
if (!player.sb) {
|
|
if (player.debugging) { console.warn("I was doing on_updateend but the sb was reset"); }
|
|
break;
|
|
}
|
|
if (player.sb.updating) {
|
|
//it's updating again >_>
|
|
do_on_updateend.concat(do_funcs.slice(i)); //add the remaining functions to do_on_updateend
|
|
if (player.debugging) { console.warn("I was doing on_updateend but was interrupted"); }
|
|
break;
|
|
}
|
|
do_funcs[i](i < do_funcs.length-1 ? do_funcs.slice(i) : []); //pass remaining do_funcs as argument
|
|
}
|
|
|
|
if (!player.sb) { return; }
|
|
|
|
player.sb._busy = false;
|
|
//console.log("end udpateend");
|
|
//console.log("onupdateend",player.sb.queue.length,player.sb.updating);
|
|
if (player.sb && player.sb.queue.length > 0 && !player.sb.updating && !video.error) {
|
|
//console.log("appending from queue");
|
|
player.sb._append(this.queue.shift());
|
|
}
|
|
});
|
|
player.sb.error = function(e){
|
|
console.error("sb error",e);
|
|
};
|
|
player.sb.abort = function(e){
|
|
console.error("sb abort",e);
|
|
};
|
|
|
|
player.sb._doNext = function(func) {
|
|
do_on_updateend.push(func);
|
|
};
|
|
player.sb._do = function(func) {
|
|
if (this.updating || this._busy) {
|
|
this._doNext(func);
|
|
}
|
|
else {
|
|
func();
|
|
}
|
|
}
|
|
player.sb._append = function(data) {
|
|
if (!data) { return; }
|
|
if (!data.buffer) { return; }
|
|
if (player.debugging) { player.sb.appending = new Uint8Array(data); }
|
|
if (player.sb._busy) {
|
|
if (player.debugging) console.warn("I wanted to append data, but now I won't because the thingy was still busy. Putting it back in the queue.");
|
|
player.sb.queue.unshift(data);
|
|
return;
|
|
}
|
|
player.sb._busy = true;
|
|
//console.log("appendBuffer");
|
|
try {
|
|
player.sb.appendBuffer(data);
|
|
}
|
|
catch(e){
|
|
switch (e.name) {
|
|
case "QuotaExceededError": {
|
|
if (video.buffered.length) {
|
|
if (video.currentTime - video.buffered.start(0) > 1) {
|
|
//clear as much from the buffer as we can
|
|
MistVideo.log("Triggered QuotaExceededError: cleaning up "+(Math.round((video.currentTime - video.buffered.start(0) - 1)*10)/10)+"s");
|
|
player.sb._clean(1);
|
|
}
|
|
else {
|
|
var bufferEnd = video.buffered.end(video.buffered.length-1);
|
|
MistVideo.log("Triggered QuotaExceededError but there is nothing to clean: skipping ahead "+(Math.round((bufferEnd - video.currentTime)*10)/10)+"s");
|
|
video.currentTime = bufferEnd;
|
|
}
|
|
player.sb._busy = false;
|
|
player.sb._append(data); //now try again
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
case "InvalidStateError": {
|
|
player.api.pause(); //playback is borked, so stop downloading more data
|
|
if (MistVideo.video.error) {
|
|
//Failed to execute 'appendBuffer' on 'SourceBuffer': The HTMLMediaElement.error attribute is not null
|
|
|
|
//the video element error is already triggering the showError()
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
MistVideo.showError(e.message);
|
|
}
|
|
}
|
|
|
|
//we're initing the source buffer and there is a msg queue of data built up before the buffer was ready. Start by adding these data fragments to the source buffer
|
|
if (player.msgqueue) {
|
|
//There may be more than one msg queue, i.e. when rapidly switching tracks. Add only one msg queue and always add the oldest msg queue first.
|
|
if (player.msgqueue[0]) {
|
|
var do_do = false; //if there are no messages in the queue, make sure to execute any do_on_updateend functions right away
|
|
if (player.msgqueue[0].length) {
|
|
for (var i in player.msgqueue[0]) {
|
|
if (player.sb.updating || player.sb.queue.length || player.sb._busy) {
|
|
player.sb.queue.push(player.msgqueue[0][i]);
|
|
}
|
|
else {
|
|
//console.log("appending new data");
|
|
player.sb._append(player.msgqueue[0][i]);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
do_do = true;
|
|
}
|
|
player.msgqueue.shift();
|
|
if (player.msgqueue.length == 0) { player.msgqueue = false; }
|
|
MistVideo.log("The newly initialized source buffer was filled with data from a seperate message queue."+(player.msgqueue ? " "+player.msgqueue.length+" more message queue(s) remain." : ""));
|
|
if (do_do) {
|
|
MistVideo.log("The seperate message queue was empty; manually triggering any onupdateend functions");
|
|
player.sb.dispatchEvent(new Event("updateend"));
|
|
}
|
|
}
|
|
}
|
|
|
|
//remove everything keepaway secs before the current playback position to keep sourcebuffer from filling up
|
|
player.sb._clean = function(keepaway){
|
|
if (!keepaway) keepaway = 180;
|
|
if (video.currentTime > keepaway) {
|
|
player.sb._do(function(){
|
|
//make sure end time is never 0
|
|
player.sb.remove(0,Math.max(0.1,video.currentTime - keepaway));
|
|
});
|
|
}
|
|
}
|
|
|
|
if (player.onsbinit.length) {
|
|
player.onsbinit.shift()();
|
|
}
|
|
checkReady();
|
|
};
|
|
|
|
this.wsconnect = function(){
|
|
return new Promise(function(resolve,reject){
|
|
//prepare websocket (both data and messages)
|
|
this.ws = new WebSocket(MistVideo.source.url);
|
|
this.ws.binaryType = "arraybuffer";
|
|
|
|
this.ws.s = this.ws.send;
|
|
this.ws.send = function(){
|
|
if (this.readyState == 1) {
|
|
return this.s.apply(this,arguments);
|
|
}
|
|
return false;
|
|
};
|
|
this.ws.onopen = function(){
|
|
this.wasConnected = true;
|
|
resolve();
|
|
};
|
|
this.ws.onerror = function(e){
|
|
MistVideo.showError("MP4 over WS: websocket error");
|
|
};
|
|
this.ws.onclose = function(e){
|
|
MistVideo.log("MP4 over WS: websocket closed");
|
|
if (this.wasConnected && (!MistVideo.destroyed) && (!player.sb || !player.sb.paused) && (MistVideo.state == "Stream is online") && (!(MistVideo.video && MistVideo.video.error))) {
|
|
MistVideo.log("MP4 over WS: reopening websocket");
|
|
player.wsconnect().then(function(){
|
|
if (!player.sb) {
|
|
//retrieve codec info
|
|
var f = function(msg){
|
|
//got codec data, set up source buffer
|
|
if (!player.sb) { player.sbinit(msg.data.codecs); }
|
|
else { player.api.play().catch(function(){}); }
|
|
|
|
player.ws.removeListener("codec_data",f);
|
|
};
|
|
player.ws.addListener("codec_data",f);
|
|
send({type:"request_codec_data",supported_codecs:MistVideo.source.supportedCodecs});
|
|
}
|
|
else {
|
|
player.api.play();
|
|
}
|
|
},function(){
|
|
Mistvideo.error("Lost connection to the Media Server");
|
|
});
|
|
}
|
|
};
|
|
this.ws.timeOut = MistVideo.timers.start(function(){
|
|
if (player.ws.readyState == 0) {
|
|
MistVideo.log("MP4 over WS: socket timeout - try next combo");
|
|
MistVideo.nextCombo();
|
|
}
|
|
},5e3);
|
|
|
|
this.ws.listeners = {}; //kind of event listener list for websocket messages
|
|
this.ws.addListener = function(type,f){
|
|
if (!(type in this.listeners)) { this.listeners[type] = []; }
|
|
this.listeners[type].push(f);
|
|
};
|
|
this.ws.removeListener = function(type,f) {
|
|
if (!(type in this.listeners)) { return; }
|
|
var i = this.listeners[type].indexOf(f);
|
|
if (i < 0) { return; }
|
|
this.listeners[type].splice(i,1);
|
|
return true;
|
|
}
|
|
player.msgqueue = false;
|
|
var requested_rate = 1;
|
|
var serverdelay = [];
|
|
var currenttracks = [];
|
|
this.ws.onmessage = function(e){
|
|
if (!e.data) { throw "Received invalid data"; }
|
|
if (typeof e.data == "string") {
|
|
var msg = JSON.parse(e.data);
|
|
if (player.debugging && (msg.type != "on_time")) { console.log("ws message",msg); }
|
|
switch (msg.type) {
|
|
case "on_stop": {
|
|
//the last fragment has been added to the buffer
|
|
var eObj;
|
|
eObj = MistUtil.event.addListener(video,"waiting",function(e){
|
|
player.sb.paused = true;
|
|
MistUtil.event.send("ended",null,video);
|
|
MistUtil.event.removeListener(eObj);
|
|
});
|
|
player.ws.onclose = function(){}; //don't reopen websocket, just close, it's okay, rly
|
|
break;
|
|
}
|
|
case "on_time": {
|
|
var buffer = msg.data.current - video.currentTime*1e3;
|
|
var serverDelay = player.ws.serverDelay.get();
|
|
var desiredBuffer = Math.max(100+serverDelay,serverDelay*2);
|
|
var desiredBufferwithJitter = desiredBuffer+(msg.data.jitter ? msg.data.jitter : 0);
|
|
|
|
|
|
if (MistVideo.info.type != "live") { desiredBuffer += 2000; } //if VoD, keep an extra 2 seconds of buffer
|
|
|
|
if (player.debugging) {
|
|
console.log(
|
|
"on_time received", msg.data.current/1e3,
|
|
"currtime", video.currentTime,
|
|
requested_rate+"x",
|
|
"buffer",Math.round(buffer),"/",Math.round(desiredBuffer),
|
|
(MistVideo.info.type == "live" ? "latency:"+Math.round(msg.data.end-video.currentTime*1e3)+"ms" : ""),
|
|
(player.monitor ? "bitrate:"+MistUtil.format.bits(player.monitor.currentBps)+"/s" : ""),
|
|
"listeners",player.ws.listeners && player.ws.listeners.on_time ? player.ws.listeners.on_time : 0,
|
|
"msgqueue",player.msgqueue ? player.msgqueue.length : 0,
|
|
"readyState",MistVideo.video.readyState,msg.data
|
|
);
|
|
}
|
|
|
|
if (!player.sb) {
|
|
MistVideo.log("Received on_time, but the source buffer is being cleared right now. Ignoring.");
|
|
break;
|
|
}
|
|
|
|
if (lastduration != msg.data.end*1e-3) {
|
|
lastduration = msg.data.end*1e-3;
|
|
MistUtil.event.send("durationchange",null,MistVideo.video);
|
|
}
|
|
MistVideo.info.meta.buffer_window = msg.data.end - msg.data.begin;
|
|
player.sb.paused = false;
|
|
|
|
if (MistVideo.info.type == "live") {
|
|
if (requested_rate == 1) {
|
|
if (msg.data.play_rate_curr == "auto") {
|
|
if (video.currentTime > 0) { //give it some time to seek to live first when starting up
|
|
//assume we want to be as live as possible
|
|
if (buffer > desiredBufferwithJitter*2) {
|
|
requested_rate = 1 + Math.min(1,((buffer-desiredBufferwithJitter)/desiredBufferwithJitter))*0.08;
|
|
video.playbackRate *= requested_rate;
|
|
MistVideo.log("Our buffer ("+Math.round(buffer)+"ms) is big (>"+Math.round(desiredBufferwithJitter*2)+"ms), so increase the playback speed to "+(Math.round(requested_rate*100)/100)+" to catch up.");
|
|
}
|
|
else if (buffer < 0) {
|
|
requested_rate = 0.8;
|
|
video.playbackRate *= requested_rate;
|
|
MistVideo.log("Our buffer ("+Math.round(buffer)+"ms) is negative so decrease the playback speed to "+(Math.round(requested_rate*100)/100)+" to let it catch up.");
|
|
}
|
|
else if (buffer < desiredBuffer/2) {
|
|
requested_rate = 1 + Math.min(1,((buffer-desiredBuffer)/desiredBuffer))*0.08;
|
|
video.playbackRate *= requested_rate;
|
|
MistVideo.log("Our buffer ("+Math.round(buffer)+"ms) is small (<"+Math.round(desiredBuffer/2)+"ms), so decrease the playback speed to "+(Math.round(requested_rate*100)/100)+" to catch up.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (requested_rate > 1) {
|
|
if (buffer < desiredBufferwithJitter) {
|
|
video.playbackRate /= requested_rate;
|
|
requested_rate = 1;
|
|
MistVideo.log("Our buffer ("+Math.round(buffer)+"ms) is small enough (<"+Math.round(desiredBufferwithJitter)+"ms), so return to real time playback.");
|
|
}
|
|
}
|
|
else {
|
|
//requested rate < 1
|
|
if (buffer > desiredBufferwithJitter) {
|
|
video.playbackRate /= requested_rate;
|
|
requested_rate = 1;
|
|
MistVideo.log("Our buffer ("+Math.round(buffer)+"ms) is big enough (>"+Math.round(desiredBufferwithJitter)+"ms), so return to real time playback.");
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
//it's VoD, change the rate at which the server sends data to try and keep the buffer small
|
|
if (requested_rate == 1) {
|
|
if (msg.data.play_rate_curr == "auto") {
|
|
if (buffer < desiredBuffer/2) {
|
|
if (buffer < -10e3) {
|
|
//seek to play point
|
|
send({type: "seek", seek_time: Math.round(video.currentTime*1e3)});
|
|
}
|
|
else {
|
|
//negative buffer? ask for faster delivery
|
|
requested_rate = 2;
|
|
MistVideo.log("Our buffer is negative, so request a faster download rate.");
|
|
send({type: "set_speed", play_rate: requested_rate});
|
|
}
|
|
}
|
|
else if (buffer - desiredBuffer > desiredBuffer) {
|
|
MistVideo.log("Our buffer is big, so request a slower download rate.");
|
|
requested_rate = 0.5;
|
|
send({type: "set_speed", play_rate: requested_rate});
|
|
}
|
|
}
|
|
}
|
|
else if (requested_rate > 1) {
|
|
if (buffer > desiredBuffer) {
|
|
//we have enough buffer, ask for real time delivery
|
|
send({type: "set_speed", play_rate: "auto"});
|
|
requested_rate = 1;
|
|
MistVideo.log("The buffer is big enough, so ask for realtime download rate.");
|
|
}
|
|
}
|
|
else { //requested_rate < 1
|
|
if (buffer < desiredBuffer) {
|
|
//we have a small enough bugger, ask for real time delivery
|
|
send({type: "set_speed", play_rate: "auto"});
|
|
requested_rate = 1;
|
|
MistVideo.log("The buffer is small enough, so ask for realtime download rate.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (MistVideo.reporting && msg.data.tracks) {
|
|
MistVideo.reporting.stats.d.tracks = msg.data.tracks.join(",");
|
|
}
|
|
|
|
//check if the tracks are different than before, and if so, signal the skin to display the playing tracks
|
|
if ((msg.data.tracks) && (currenttracks != msg.data.tracks)) {
|
|
var tracks = MistVideo.info ? MistUtil.tracks.parse(MistVideo.info.meta.tracks) : [];
|
|
for (var i in msg.data.tracks) {
|
|
if (currenttracks.indexOf(msg.data.tracks[i]) < 0) {
|
|
//find track type
|
|
var type;
|
|
for (var j in tracks) {
|
|
if (msg.data.tracks[i] in tracks[j]) {
|
|
type = j;
|
|
break;
|
|
}
|
|
}
|
|
if (!type) {
|
|
//track type not found, this should not happen
|
|
continue;
|
|
}
|
|
|
|
//create an event to pass this to the skin
|
|
MistUtil.event.send("playerUpdate_trackChanged",{
|
|
type: type,
|
|
trackid: msg.data.tracks[i]
|
|
},MistVideo.video);
|
|
}
|
|
}
|
|
|
|
currenttracks = msg.data.tracks;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "tracks": {
|
|
//check if all codecs are equal to the ones we were using before
|
|
function checkEqual(arr1,arr2) {
|
|
if (!arr2) { return false; }
|
|
if (arr1.length != arr2.length) { return false; }
|
|
for (var i in arr1) {
|
|
if (arr2.indexOf(arr1[i]) < 0) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
if (checkEqual(player.last_codecs ? player.last_codecs : player.sb._codecs,msg.data.codecs)) {
|
|
MistVideo.log("Player switched tracks, keeping source buffer as codecs are the same as before.");
|
|
if ((video.currentTime == 0) && (msg.data.current != 0)) {
|
|
video.currentTime = msg.data.current;
|
|
}
|
|
}
|
|
else {
|
|
if (player.debugging) {
|
|
console.warn("Different codecs!");
|
|
console.warn("video time",video.currentTime,"switch startpoint",msg.data.current*1e-3);
|
|
}
|
|
player.last_codecs = msg.data.codecs;
|
|
//start gathering messages in a new msg queue. They won't be appended to the current source buffer
|
|
if (player.msgqueue) {
|
|
player.msgqueue.push([]);
|
|
}
|
|
else {
|
|
player.msgqueue = [[]];
|
|
}
|
|
//play out buffer, then when we reach the starting timestamp of the new data, reset the source buffers
|
|
var clear = function(){
|
|
//once the source buffer is done updating the current segment, clear the specified interval from the buffer
|
|
currPos = video.currentTime.toFixed(3);
|
|
if (player && player.sb) {
|
|
player.sb._do(function(remaining_do_on_updateend){
|
|
if (!player.sb.updating) {
|
|
if (!isNaN(player.ms.duration)) player.sb.remove(0,Infinity);
|
|
player.sb.queue = [];
|
|
player.ms.removeSourceBuffer(player.sb);
|
|
player.sb = null;
|
|
var t = (msg.data.current*1e-3).toFixed(3); //rounded because of floating point issues
|
|
video.src = "";
|
|
player.ms.onsourceclose = null;
|
|
player.ms.onsourceended = null;
|
|
//console.log("sb murdered");
|
|
if (player.debugging && remaining_do_on_updateend && remaining_do_on_updateend.length) {
|
|
console.warn("There are do_on_updateend functions queued, which I will re-apply after clearing the sb.");
|
|
}
|
|
|
|
player.msinit().then(function(){
|
|
player.sbinit(msg.data.codecs);
|
|
player.sb.do_on_updateend = remaining_do_on_updateend;
|
|
|
|
var e = MistUtil.event.addListener(video,"loadedmetadata",function(){
|
|
MistVideo.log("Buffer cleared");
|
|
|
|
var f = function() {
|
|
if (currPos > t) {
|
|
t = currPos;
|
|
}
|
|
if (!video.buffered.length || (video.buffered.end(video.buffered.length-1) < t)) {
|
|
if (player.debugging) { console.log("Desired seeking position ("+MistUtil.format.time(t,{ms:true})+") not yet in buffer ("+(video.buffered.length ? MistUtil.format.time(video.buffered.end(video.buffered.length-1),{ms:true}) : "null")+")"); }
|
|
player.sb._doNext(f);
|
|
return;
|
|
}
|
|
video.currentTime = t;
|
|
MistVideo.log("Setting playback position to "+MistUtil.format.time(t,{ms:true}));
|
|
if (video.currentTime.toFixed(3) < t) {
|
|
player.sb._doNext(f);
|
|
if (player.debugging) { console.log("Could not set playback position"); }
|
|
}
|
|
else {
|
|
if (player.debugging) { console.log("Set playback position to "+MistUtil.format.time(t,{ms:true})); }
|
|
var p = function(){
|
|
player.sb._doNext(function(){
|
|
if (video.buffered.length) {
|
|
//if (player.debugging) { console.log(video.buffered.start(0),video.buffered.end(0),video.currentTime); }
|
|
if (video.buffered.start(0) > video.currentTime) {
|
|
var b = video.buffered.start(0);
|
|
video.currentTime = b;
|
|
if (video.currentTime != b) {
|
|
p();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
p();
|
|
}
|
|
});
|
|
};
|
|
p();
|
|
}
|
|
}
|
|
f();
|
|
|
|
MistUtil.event.removeListener(e);
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
clear();
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
if (player.debugging) { console.warn("sb not available to do clear"); }
|
|
player.onsbinit.push(clear);
|
|
}
|
|
};
|
|
|
|
if (!msg.data.codecs || !msg.data.codecs.length) {
|
|
MistVideo.showError("Track switch does not contain any codecs, aborting.");
|
|
//reset setTracks to auto
|
|
MistVideo.options.setTracks = false;
|
|
clear();
|
|
break;
|
|
}
|
|
function reachedSwitchingPoint(msg) {
|
|
if (player.debugging) {
|
|
console.warn("reached switching point",msg.data.current*1e-3,MistUtil.format.time(msg.data.current*1e-3));
|
|
}
|
|
MistVideo.log("Track switch: reached switching point");
|
|
clear();
|
|
}
|
|
if (video.currentTime == 0) {
|
|
reachedSwitchingPoint(msg);
|
|
}
|
|
else {
|
|
if (msg.data.current >= video.currentTime*1e3) {
|
|
MistVideo.log("Track switch: waiting for playback to reach the switching point ("+MistUtil.format.time(msg.data.current*1e-3,{ms:true})+")");
|
|
|
|
//wait untill the video has reached the time of the newly received track or the end of our buffer
|
|
var ontime = MistUtil.event.addListener(video,"timeupdate",function(){
|
|
if (msg.data.current < video.currentTime * 1e3) {
|
|
if (player.debugging) { console.log("Track switch: video.currentTime has reached switching point."); }
|
|
reachedSwitchingPoint(msg);
|
|
MistUtil.event.removeListener(ontime);
|
|
MistUtil.event.removeListener(onwait);
|
|
}
|
|
});
|
|
var onwait = MistUtil.event.addListener(video,"waiting",function(){
|
|
if (player.debugging) { console.log("Track switch: video has reached end of buffer.","Gap:",Math.round(msg.data.current - video.currentTime * 1e3),"ms"); }
|
|
reachedSwitchingPoint(msg);
|
|
MistUtil.event.removeListener(ontime);
|
|
MistUtil.event.removeListener(onwait);
|
|
});
|
|
}
|
|
else {
|
|
//subscribe to on_time, wait until we've received current playback point
|
|
//if we don't wait, the screen will go black until the buffer is full enough
|
|
MistVideo.log("Track switch: waiting for the received data to reach the current playback point");
|
|
var ontime = function(newmsg){
|
|
if (newmsg.data.current >= video.currentTime*1e3) {
|
|
reachedSwitchingPoint(newmsg);
|
|
player.ws.removeListener("on_time",ontime);
|
|
}
|
|
}
|
|
player.ws.addListener("on_time",ontime);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "pause": {
|
|
if (player.sb) { player.sb.paused = true; }
|
|
break;
|
|
}
|
|
}
|
|
if (msg.type in this.listeners) {
|
|
for (var i = this.listeners[msg.type].length-1; i >= 0; i--) { //start at last in case the listeners remove themselves
|
|
this.listeners[msg.type][i](msg);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
var data = new Uint8Array(e.data);
|
|
if (data) {
|
|
if (player.monitor && player.monitor.bitCounter) {
|
|
for (var i in player.monitor.bitCounter) {
|
|
player.monitor.bitCounter[i] += e.data.byteLength*8;
|
|
}
|
|
}
|
|
if ((player.sb) && (!player.msgqueue)) {
|
|
if (player.sb.updating || player.sb.queue.length || player.sb._busy) {
|
|
player.sb.queue.push(data);
|
|
}
|
|
else {
|
|
//console.log("appending new data");
|
|
player.sb._append(data);
|
|
}
|
|
}
|
|
else {
|
|
//There is no active source buffer or we're preparing for a track switch.
|
|
//Any data is kept in a seperate buffer and won't be appended to the source buffer until it is reinitialised.
|
|
if (!player.msgqueue) { player.msgqueue = [[]]; }
|
|
//There may be more than one seperate buffer (in case of rapid track switches), always append to the last of the buffers
|
|
player.msgqueue[player.msgqueue.length-1].push(data);
|
|
}
|
|
}
|
|
else {
|
|
//console.warn("no data, wut?",data,new Uint8Array(e.data));
|
|
MistVideo.log("Expecting data from websocket, but received none?!");
|
|
}
|
|
}
|
|
|
|
|
|
this.ws.serverDelay = {
|
|
delays: [],
|
|
log: function (type) {
|
|
var responseType = false;
|
|
switch (type) {
|
|
case "seek":
|
|
case "set_speed": {
|
|
//wait for cmd.type
|
|
responseType = type;
|
|
break;
|
|
}
|
|
case "request_codec_data": {
|
|
responseType = "codec_data";
|
|
break;
|
|
}
|
|
default: {
|
|
//do nothing
|
|
return;
|
|
}
|
|
}
|
|
if (responseType) {
|
|
var starttime = new Date().getTime();
|
|
function onResponse() {
|
|
player.ws.serverDelay.add(new Date().getTime() - starttime);
|
|
player.ws.removeListener(responseType,onResponse);
|
|
}
|
|
player.ws.addListener(responseType,onResponse);
|
|
}
|
|
},
|
|
add: function(delay){
|
|
this.delays.unshift(delay);
|
|
if (this.delays.length > 5) {
|
|
this.delays.splice(5);
|
|
}
|
|
},
|
|
get: function(){
|
|
if (this.delays.length) {
|
|
//return average of the last 3 recorded delays
|
|
let sum = 0;
|
|
let i = 0;
|
|
for (null; i < this.delays.length; i++){
|
|
if (i >= 3) { break; }
|
|
sum += this.delays[i];
|
|
}
|
|
return sum/i;
|
|
}
|
|
return 500;
|
|
}
|
|
};
|
|
}.bind(this));
|
|
};
|
|
this.wsconnect().then(function(){
|
|
//retrieve codec info
|
|
var f = function(msg){
|
|
//got codec data, set up source buffer
|
|
if (player.ms && player.ms.readyState == "open") {
|
|
player.sbinit(msg.data.codecs);
|
|
}
|
|
else {
|
|
player.msoninit.push(function(){
|
|
player.sbinit(msg.data.codecs);
|
|
});
|
|
}
|
|
|
|
player.ws.removeListener("codec_data",f);
|
|
};
|
|
this.ws.addListener("codec_data",f);
|
|
send({type:"request_codec_data",supported_codecs:MistVideo.source.supportedCodecs});
|
|
}.bind(this));
|
|
|
|
function send(cmd){
|
|
if (!player.ws) { throw "No websocket to send to"; }
|
|
if (player.ws.readyState >= player.ws.CLOSING) {
|
|
//throw "WebSocket has been closed already.";
|
|
MistVideo.log("MP4 over WS: reopening websocket");
|
|
player.wsconnect().then(function(){
|
|
if (!player.sb) {
|
|
//retrieve codec info
|
|
var f = function(msg){
|
|
//got codec data, set up source buffer
|
|
if (!player.sb) { player.sbinit(msg.data.codecs); }
|
|
else { player.api.play().catch(function(){}); }
|
|
|
|
player.ws.removeListener("codec_data",f);
|
|
};
|
|
player.ws.addListener("codec_data",f);
|
|
send({type:"request_codec_data",supported_codecs:MistVideo.source.supportedCodecs});
|
|
}
|
|
else {
|
|
player.api.play();
|
|
}
|
|
send(cmd);
|
|
},function(){
|
|
Mistvideo.error("Lost connection to the Media Server");
|
|
});
|
|
return;
|
|
}
|
|
if (player.debugging) { console.log("ws send",cmd); }
|
|
|
|
player.ws.serverDelay.log(cmd.type);
|
|
player.ws.send(JSON.stringify(cmd));
|
|
}
|
|
|
|
player.findBuffer = function (position) {
|
|
var buffern = false;
|
|
for (var i = 0; i < video.buffered.length; i++) {
|
|
if ((video.buffered.start(i) <= position) && (video.buffered.end(i) >= position)) {
|
|
buffern = i;
|
|
break;
|
|
}
|
|
}
|
|
return buffern;
|
|
};
|
|
|
|
this.api = {
|
|
play: function(skipToLive){
|
|
return new Promise(function(resolve,reject){
|
|
if (!video.paused) {
|
|
//we're already playing, what are you doing?
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
if (("paused" in player.sb) && !player.sb.paused) {
|
|
video.play().then(resolve).catch(reject);
|
|
return;
|
|
}
|
|
|
|
|
|
var f = function(e){
|
|
if (!player.sb) {
|
|
MistVideo.log("Attempting to play, but the source buffer is being cleared. Waiting for next on_time.");
|
|
return;
|
|
}
|
|
if (MistVideo.info.type == "live") {
|
|
if (skipToLive || (video.currentTime == 0)) {
|
|
var g = function(){
|
|
if (video.buffered.length) {
|
|
//is data.current contained within a buffer? is video.currentTime also contained in that buffer? if not, seek the video
|
|
var buffern = player.findBuffer(e.data.current*1e-3);
|
|
if (buffern !== false) {
|
|
if ((video.buffered.start(buffern) > video.currentTime) || (video.buffered.end(buffern) < video.currentTime)) {
|
|
video.currentTime = e.data.current*1e-3;
|
|
MistVideo.log("Setting live playback position to "+MistUtil.format.time(video.currentTime));
|
|
}
|
|
video.play().then(resolve).catch(function(){
|
|
//could not play video, pause the download
|
|
return reject.apply(this,arguments);
|
|
});
|
|
player.sb.paused = false;
|
|
player.sb.removeEventListener("updateend",g);
|
|
}
|
|
}
|
|
};
|
|
player.sb.addEventListener("updateend",g);
|
|
}
|
|
else {
|
|
player.sb.paused = false;
|
|
video.play().then(resolve).catch(function(){
|
|
//could not play video, pause the download
|
|
player.api.pause();
|
|
return reject.apply(this,arguments);
|
|
});
|
|
}
|
|
player.ws.removeListener("on_time",f);
|
|
}
|
|
else if (e.data.current > video.currentTime) {
|
|
player.sb.paused = false;
|
|
if (video.buffered.length && video.buffered.start(0) > video.currentTime) {
|
|
video.currentTime = video.buffered.start(0);
|
|
}
|
|
video.play().then(resolve).catch(reject);
|
|
player.ws.removeListener("on_time",f);
|
|
}
|
|
};
|
|
player.ws.addListener("on_time",f);
|
|
var cmd = {type:"play"};
|
|
if (skipToLive) { cmd.seek_time = "live"; }
|
|
send(cmd);
|
|
});
|
|
},
|
|
pause: function(){
|
|
video.pause();
|
|
send({type: "hold"});
|
|
if (player.sb) { player.sb.paused = true; }
|
|
},
|
|
setTracks: function(obj){
|
|
if (!MistUtil.object.keys(obj).length) { return; }
|
|
obj.type = "tracks";
|
|
obj = MistUtil.object.extend({
|
|
type: "tracks",
|
|
//seek_time: Math.round(Math.max(0,video.currentTime*1e3-(500+player.ws.serverDelay.get())))
|
|
},obj);
|
|
send(obj);
|
|
},
|
|
unload: function(){
|
|
player.api.pause();
|
|
player.sb._do(function(){
|
|
player.sb.remove(0,Infinity);
|
|
try {
|
|
player.ms.endOfStream();
|
|
|
|
//it's okay if it fails
|
|
} catch (e) { }
|
|
});
|
|
player.ws.close();
|
|
}
|
|
};
|
|
|
|
//override seeking
|
|
Object.defineProperty(this.api,"currentTime",{
|
|
get: function(){
|
|
return video.currentTime;
|
|
},
|
|
set: function(value){
|
|
//console.warn("seek to",value);
|
|
if (isNaN(value) || (value < 0)) {
|
|
MistVideo.log("Ignoring seek to "+value+" because ewww.");
|
|
return;
|
|
}
|
|
MistUtil.event.send("seeking",value,video);
|
|
send({type: "seek", seek_time: Math.round(Math.max(0,value*1e3-(250+player.ws.serverDelay.get())))}); //safety margin for server latency
|
|
//set listener "seek"
|
|
var onseek = function(e){
|
|
player.ws.removeListener("seek",onseek);
|
|
var ontime = function(e){
|
|
player.ws.removeListener("on_time",ontime);
|
|
//in the first on_time, assume that the data were getting is where we want to be
|
|
value = e.data.current * 1e-3;
|
|
value = value.toFixed(3);
|
|
//retry a max of 10 times
|
|
var retries = 10;
|
|
var f = function() {
|
|
video.currentTime = value;
|
|
if (video.currentTime.toFixed(3) < value) {
|
|
MistVideo.log("Failed to seek, wanted: "+value+" got: "+video.currentTime.toFixed(3));
|
|
if (retries >= 0) {
|
|
retries--;
|
|
player.sb._doNext(f);
|
|
}
|
|
}
|
|
}
|
|
f();
|
|
};
|
|
player.ws.addListener("on_time",ontime);
|
|
}
|
|
player.ws.addListener("seek",onseek);
|
|
video.currentTime = value;
|
|
MistVideo.log("Seeking to "+MistUtil.format.time(value,{ms:true})+" ("+value+")");
|
|
}
|
|
});
|
|
//override duration
|
|
var lastduration = Infinity;
|
|
Object.defineProperty(this.api,"duration",{
|
|
get: function(){
|
|
return lastduration;
|
|
}
|
|
});
|
|
Object.defineProperty(this.api,"playbackRate",{
|
|
get: function(){
|
|
return video.playbackRate;
|
|
},
|
|
set: function(value){
|
|
var f = function(msg){
|
|
video.playbackRate = msg.data.play_rate_curr;
|
|
};
|
|
player.ws.addListener("set_speed",f);
|
|
send({type: "set_speed", play_rate: (value == 1 ? "auto" : value)});
|
|
}
|
|
});
|
|
|
|
//redirect properties
|
|
//using a function to make sure the "item" is in the correct scope
|
|
function reroute(item) {
|
|
Object.defineProperty(player.api,item,{
|
|
get: function(){ return video[item]; },
|
|
set: function(value){
|
|
return video[item] = value;
|
|
}
|
|
});
|
|
}
|
|
var list = [
|
|
"volume"
|
|
,"buffered"
|
|
,"muted"
|
|
,"loop"
|
|
,"paused",
|
|
,"error"
|
|
,"textTracks"
|
|
,"webkitDroppedFrameCount"
|
|
,"webkitDecodedFrameCount"
|
|
];
|
|
for (var i in list) {
|
|
reroute(list[i]);
|
|
}
|
|
|
|
//loop
|
|
MistUtil.event.addListener(video,"ended",function(){
|
|
if (player.api.loop) {
|
|
player.api.currentTime = 0;
|
|
player.sb._do(function(){
|
|
player.sb.remove(0,Infinity);
|
|
});
|
|
}
|
|
});
|
|
|
|
var seeking = false;
|
|
MistUtil.event.addListener(video,"seeking",function(){
|
|
seeking = true;
|
|
var seeked = MistUtil.event.addListener(video,"seeked",function(){
|
|
seeking = false;
|
|
MistUtil.event.removeListener(seeked);
|
|
});
|
|
});
|
|
MistUtil.event.addListener(video,"waiting",function(){
|
|
//check if there is a gap in the buffers, and if so, jump it
|
|
if (seeking) { return; }
|
|
var buffern = player.findBuffer(video.currentTime);
|
|
if (buffern !== false) {
|
|
if ((buffern+1 < video.buffered.length) && (video.buffered.start(buffern+1) - video.currentTime < 10e3)) {
|
|
MistVideo.log("Skipped over buffer gap (from "+MistUtil.format.time(video.currentTime)+" to "+MistUtil.format.time(video.buffered.start(buffern+1))+")");
|
|
video.currentTime = video.buffered.start(buffern+1);
|
|
}
|
|
}
|
|
});
|
|
MistUtil.event.addListener(video,"pause",function(){
|
|
if (player.sb && !player.sb.paused) {
|
|
MistVideo.log("The browser paused the vid - probably because it has no audio and the tab is no longer visible. Pausing download.");
|
|
send({type:"hold"});
|
|
player.sb.paused = true;
|
|
var p = MistUtil.event.addListener(video,"play",function(){
|
|
if (player.sb && player.sb.paused) {
|
|
send({type:"play"});
|
|
}
|
|
MistUtil.event.removeListener(p);
|
|
});
|
|
}
|
|
});
|
|
|
|
if (player.debugging) {
|
|
MistUtil.event.addListener(video,"waiting",function(){
|
|
//check the buffer available
|
|
var buffers = [];
|
|
var contained = false;
|
|
for (var i = 0; i < video.buffered.length; i++) {
|
|
if ((video.currentTime >= video.buffered.start(i)) && (video.currentTime <= video.buffered.end(i))) {
|
|
contained = true;
|
|
}
|
|
buffers.push([
|
|
video.buffered.start(i),
|
|
video.buffered.end(i),
|
|
]);
|
|
}
|
|
console.log("waiting","currentTime",video.currentTime,"buffers",buffers,contained ? "contained" : "outside of buffer","readystate",video.readyState,"networkstate",video.networkState);
|
|
if ((video.readyState >= 2) && (video.networkState >= 2)) {
|
|
console.error("Why am I waiting?!",video.currentTime);
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
this.ABR = {
|
|
size: null,
|
|
bitrate: null,
|
|
generateString: function(type,raw){
|
|
switch (type) {
|
|
case "size": {
|
|
return "~"+[raw.width,raw.height].join("x");
|
|
}
|
|
case "bitrate": {
|
|
return "<"+Math.round(raw)+"bps,minbps";
|
|
}
|
|
default: {
|
|
throw "Unknown ABR type";
|
|
}
|
|
}
|
|
},
|
|
request: function(type,value){
|
|
this[type] = value;
|
|
|
|
var request = [];
|
|
if (this.bitrate !== null) {
|
|
request.push(this.generateString("bitrate",this.bitrate));
|
|
}
|
|
if (this.size !== null) {
|
|
request.push(this.generateString("size",this.size));
|
|
}
|
|
else {
|
|
request.push("maxbps");
|
|
}
|
|
|
|
return player.api.setTracks({
|
|
video: request.join(",|")
|
|
});
|
|
}
|
|
}
|
|
|
|
this.api.ABR_resize = function(size){
|
|
MistVideo.log("Requesting the video track with the resolution that best matches the player size");
|
|
player.ABR.request("size",size);
|
|
};
|
|
//ABR: monitor playback issues and switch to lower bitrate track if available
|
|
//NB: this ABR requests a lower bitrate if needed, but it can never go back up
|
|
this.monitor = {
|
|
bitCounter: [],
|
|
bitsSince: [],
|
|
currentBps: null,
|
|
nWaiting: 0,
|
|
nWaitingThreshold: 3,
|
|
listener: MistVideo.options.ABR_bitrate ? MistUtil.event.addListener(video,"waiting",function(){
|
|
player.monitor.nWaiting++;
|
|
|
|
if (player.monitor.nWaiting >= player.monitor.nWaitingThreshold) {
|
|
player.monitor.nWaiting = 0;
|
|
player.monitor.action();
|
|
}
|
|
}) : null,
|
|
getBitRate: function(){
|
|
if (player.sb && !player.sb.paused) {
|
|
|
|
this.bitCounter.push(0);
|
|
this.bitsSince.push(new Date().getTime());
|
|
|
|
//calculate current bitrate
|
|
var bits, since;
|
|
if (this.bitCounter.length > 5) {
|
|
bits = player.monitor.bitCounter.shift();
|
|
since = this.bitsSince.shift();
|
|
}
|
|
else {
|
|
bits = player.monitor.bitCounter[0];
|
|
since = this.bitsSince[0];
|
|
}
|
|
var dt = new Date().getTime() - since;
|
|
this.currentBps = bits / (dt*1e-3);
|
|
|
|
//console.log(MistUtil.format.bits(this.currentBps)+"its/s");
|
|
}
|
|
MistVideo.timers.start(function(){
|
|
player.monitor.getBitRate();
|
|
},500);
|
|
},
|
|
action: function(){
|
|
if (MistVideo.options.setTracks && MistVideo.options.setTracks.video) {
|
|
//a video track was selected by the user, do not change it
|
|
return;
|
|
}
|
|
MistVideo.log("ABR threshold triggered, requesting lower quality");
|
|
player.ABR.request("bitrate",this.currentBps);
|
|
}
|
|
};
|
|
this.monitor.getBitRate();
|
|
};
|