mistserver/embed/wrappers/mews.js

1064 lines
40 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;
//this function is called both when the websocket is ready and the media source is ready - both should be open to proceed
function checkReady() {
if ((player.ws.readyState == player.ws.OPEN) && (player.ms.readyState == "open") && (player.sb)) {
callback(video);
if (MistVideo.options.autoplay) {
player.api.play();
}
return true;
}
}
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(){
resolve();
};
player.ms.onsourceclose = function(e){
console.error("ms close",e);
send({type:"stop"}); //stop sending data please something went wrong
};
player.ms.onsourceended = function(e){
console.error("ms ended",e);
//for debugging
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();
};
if (player.debugging) {
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.");
return;
}
checkReady();
});
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._duration = 1;
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){
if (e.name == "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;
}
}
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()();
}
//console.log("sb inited");
};
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)) {
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(); }
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.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 = [];
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);
});
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 (player.sb._duration != msg.data.end*1e-3) {
player.sb._duration = 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 < 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(",");
}
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)) {
if (player.debugging) console.log("reached switching point");
if (msg.data.current > 0) {
if (player.sb) { //if sb is being cleared at the moment, don't bother
player.sb._do(function(){ //once the source buffer is done updating the current segment, clear the specified interval from the buffer
player.sb.remove(0,msg.data.current*1e-3);
});
}
}
MistVideo.log("Player switched tracks, keeping source buffer as codecs are the same as before.");
}
else {
if (player.debugging) {
console.warn("Different codecs!");
console.warn("video time",video.currentTime,"waiting until",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
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 *should* 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, setting playback position to "+MistUtil.format.time(t,{ms:true}));
var f = function() {
video.currentTime = t;
if (video.currentTime < 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;
}
if (player.debugging) {
console.warn("reached switching point",msg.data.current*1e-3,MistUtil.format.time(msg.data.current*1e-3));
}
clear();
}
}
}
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) {
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
player.sbinit(msg.data.codecs);
checkReady();
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.";
player.wsconnect().then(function(){
send(cmd);
});
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){
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(reject);
player.sb.paused = false;
player.sb.removeEventListener("updateend",g);
}
}
};
player.sb.addEventListener("updateend",g);
}
else {
player.sb.paused = false;
video.play().then(resolve).catch(reject);
}
player.ws.removeListener("on_time",f);
}
else if (e.data.current > video.currentTime) {
player.sb.paused = false;
video.currentTime = e.data.current*1e-3;
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){
obj.type = "tracks";
obj = MistUtil.object.extend({
type: "tracks",
seek_time: 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){
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).toFixed(3);
var f = function() {
video.currentTime = value;
if (video.currentTime != value) {
if (player.debugging) console.log("Failed to set video.currentTime, wanted:",value,"got:",video.currentTime);
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
Object.defineProperty(this.api,"duration",{
get: function(){
return player.sb ? player.sb._duration : 1;
}
});
Object.defineProperty(this.api,"playbackRate",{
get: function(){
return video.playbackRate;
},
set: function(value){
var f = function(msg){
video.playbackRate = msg.data.play_rate;
};
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?!");
}
});
}
//ABR: monitor playback issues and switch to lower bitrate track if available
this.monitor = {
bitCounter: [],
bitsSince: [],
currentBps: null,
nWaiting: 0,
nWaitingThreshold: 3,
listener: MistUtil.event.addListener(video,"waiting",function(){
player.monitor.nWaiting++;
if (player.monitor.nWaiting >= player.monitor.nWaitingThreshold) {
player.monitor.nWaiting = 0;
MistVideo.log("ABR threshold triggered, requesting lower quality");
player.monitor.action();
}
}),
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.bytes(this.currentBps)+"its/s");
}
MistVideo.timers.start(function(){
player.monitor.getBitRate();
},500);
},
action: function(){
player.api.setTracks({video:"max<"+Math.round(this.currentBps)+"bps"});
}
};
this.monitor.getBitRate();
};