mirror of
https://github.com/eliasstepanik/vdo.ninja.git
synced 2026-01-25 12:28:27 +00:00
improved &chunked mode; other features
for experimentation
This commit is contained in:
parent
518ac2a4ad
commit
c24dac0253
16
iframe.html
16
iframe.html
@ -233,6 +233,11 @@
|
|||||||
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
|
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
|
||||||
iframeContainer.appendChild(button);
|
iframeContainer.appendChild(button);
|
||||||
|
|
||||||
|
var button = document.createElement("button");
|
||||||
|
button.innerHTML = "get media device list";
|
||||||
|
button.onclick = function(){iframe.contentWindow.postMessage({"getDeviceList":true}, '*');};
|
||||||
|
iframeContainer.appendChild(button);
|
||||||
|
|
||||||
var button = document.createElement("button");
|
var button = document.createElement("button");
|
||||||
button.innerHTML = "Start AutoMixer";
|
button.innerHTML = "Start AutoMixer";
|
||||||
button.onclick = function(){iframe.contentWindow.postMessage({"automixer":true}, '*');};
|
button.onclick = function(){iframe.contentWindow.postMessage({"automixer":true}, '*');};
|
||||||
@ -347,6 +352,17 @@
|
|||||||
iframeContainer.appendChild(outputWindow);
|
iframeContainer.appendChild(outputWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("deviceList" in e.data){
|
||||||
|
var outputWindow = document.createElement("div");
|
||||||
|
outputWindow.innerHTML = "child-page-action: deviceList<br />";
|
||||||
|
for (var i = 0;i<e.data.deviceList.length;i++){
|
||||||
|
outputWindow.innerHTML += e.data.deviceList[i].label + "<br />";
|
||||||
|
}
|
||||||
|
outputWindow.style.border="1px dotted black";
|
||||||
|
iframeContainer.appendChild(outputWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if ("loudness" in e.data){
|
if ("loudness" in e.data){
|
||||||
console.log(e.data);
|
console.log(e.data);
|
||||||
if (document.getElementById("loudness")){
|
if (document.getElementById("loudness")){
|
||||||
|
|||||||
@ -115,7 +115,7 @@
|
|||||||
<div id="controlButtons" >
|
<div id="controlButtons" >
|
||||||
<div id="obsState" class="advanced" >ACTIVE</div>
|
<div id="obsState" class="advanced" >ACTIVE</div>
|
||||||
<div id="subControlButtons">
|
<div id="subControlButtons">
|
||||||
<div id="queuebutton" title="Load the next guest in queue" alt="Load the next guest in queue" onmousedown="event.preventDefault(); event.stopPropagation();" onclick="session.nextQueue()" tabindex="16" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" class="advanced float" style="cursor: pointer;" >
|
<div id="queuebutton" title="Load the next guest in queue" alt="Load the next guest in queue" onmousedown="event.preventDefault(); event.stopPropagation();" onclick="nextQueue()" tabindex="16" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" class="advanced float" style="cursor: pointer;" >
|
||||||
<i id="queuetoggle" class="toggleSize las la-stream my-float"></i>
|
<i id="queuetoggle" class="toggleSize las la-stream my-float"></i>
|
||||||
<div id="queueNotification"></div>
|
<div id="queueNotification"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
872
lib.js
872
lib.js
@ -316,6 +316,156 @@ function submitDebugLog(msg){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); // used by main.js also
|
||||||
|
var iPad = (navigator.maxTouchPoints && navigator.maxTouchPoints > 2 && /MacIntel/.test(navigator.platform));
|
||||||
|
|
||||||
|
|
||||||
|
function isAlphaNumeric(str) {
|
||||||
|
var code, i, len;
|
||||||
|
for (i = 0, len = str.length; i < len; i++) {
|
||||||
|
code = str.charCodeAt(i);
|
||||||
|
if (!(code > 47 && code < 58) && // numeric (0-9)
|
||||||
|
!(code > 64 && code < 91) && // upper alpha (A-Z)
|
||||||
|
!(code > 96 && code < 123)) { // lower alpha (a-z)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertStringToArrayBufferView(str){
|
||||||
|
var bytes = new Uint8Array(str.length);
|
||||||
|
for (var iii = 0; iii < str.length; iii++){
|
||||||
|
bytes[iii] = str.charCodeAt(iii);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toHexString(byteArray){
|
||||||
|
return Array.prototype.map.call(byteArray, function(byte){
|
||||||
|
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
function toByteArray(hexString){
|
||||||
|
var result = [];
|
||||||
|
for (var i = 0; i < hexString.length; i += 2){
|
||||||
|
result.push(parseInt(hexString.substr(i, 2), 16));
|
||||||
|
}
|
||||||
|
return new Uint8Array(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playAllVideos(){
|
||||||
|
for (var i in session.rpcs){
|
||||||
|
try{
|
||||||
|
if (session.rpcs[i].videoElement){
|
||||||
|
if (session.rpcs[i].videoElement.paused){
|
||||||
|
session.rpcs[i].videoElement.play().then(_ => {
|
||||||
|
log("playing");
|
||||||
|
}).catch(warnlog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var videoElements = Array.from(document.querySelectorAll("video"));
|
||||||
|
var audioElements = Array.from(document.querySelectorAll("audio"));
|
||||||
|
var mediaStreamCounter = 0;
|
||||||
|
|
||||||
|
|
||||||
|
function createMediaStream(){
|
||||||
|
mediaStreamCounter+=1;
|
||||||
|
return new MediaStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteOldMedia(){
|
||||||
|
warnlog("CHECKING FOR OLD MEDIA");
|
||||||
|
var i = videoElements.length;
|
||||||
|
while (i--) {
|
||||||
|
//if ((videoElements[i].id == "videosource") || (videoElements[i].id == "previewWebcam")){continue;} // exclude this one, for safety reasons. (Also, iOS safari blanks the video if streams are detached and moved between video elements)
|
||||||
|
if (videoElements[i].isConnected === false){
|
||||||
|
if ((videoElements[i].srcObject==null) || (videoElements[i].srcObject && videoElements[i].srcObject.active === false)){
|
||||||
|
if (videoElements[i].dataset && videoElements[i].dataset.UUID){
|
||||||
|
if (videoElements[i].dataset.UUID in session.rpcs){continue;} // still active, so lets not delete it.
|
||||||
|
}
|
||||||
|
videoElements[i].pause();
|
||||||
|
videoElements[i].removeAttribute("id");
|
||||||
|
videoElements[i].removeAttribute('src'); // empty source
|
||||||
|
videoElements[i].load();
|
||||||
|
videoElements[i].remove();
|
||||||
|
videoElements[i] = null;
|
||||||
|
videoElements.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = audioElements.length;
|
||||||
|
while (i--) {
|
||||||
|
if (audioElements[i].isConnected === false){
|
||||||
|
if ((audioElements[i].srcObject==null) || (audioElements[i].srcObject && audioElements[i].srcObject.active === false)){
|
||||||
|
if (audioElements[i].dataset && audioElements[i].dataset.UUID){
|
||||||
|
if (audioElements[i].dataset.UUID in session.rpcs){continue;} // still active, so lets not delete it.
|
||||||
|
}
|
||||||
|
audioElements[i].pause();
|
||||||
|
audioElements[i].id = null;
|
||||||
|
audioElements[i].removeAttribute('src'); // empty source
|
||||||
|
audioElements[i].load();
|
||||||
|
audioElements[i].remove();
|
||||||
|
audioElements[i] = null;
|
||||||
|
audioElements.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAudioElement(){
|
||||||
|
try{
|
||||||
|
deleteOldMedia();
|
||||||
|
} catch(e){errorlog(e);}
|
||||||
|
var a = document.createElement("audio");
|
||||||
|
audioElements.push(a);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compare_deltas( a, b ) {
|
||||||
|
var aa = a.delta || 0;
|
||||||
|
var bb = b.delta || 0;
|
||||||
|
if ( aa > bb ){
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ( aa < bb ){
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVideoElement(){
|
||||||
|
try{
|
||||||
|
deleteOldMedia();
|
||||||
|
} catch(e){errorlog(e);}
|
||||||
|
var v = document.createElement("video");
|
||||||
|
videoElements.push(v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimezone(){
|
||||||
|
if (session.tz!==false){
|
||||||
|
return session.tz;
|
||||||
|
}
|
||||||
|
const stdTimezoneOffset = () => {
|
||||||
|
var jan = new Date(0, 1);
|
||||||
|
var jul = new Date(6, 1);
|
||||||
|
return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
||||||
|
}
|
||||||
|
var today = new Date();
|
||||||
|
const isDstObserved = (today) => {
|
||||||
|
return today.getTimezoneOffset() < stdTimezoneOffset();
|
||||||
|
}
|
||||||
|
if (isDstObserved(today)) {
|
||||||
|
return today.getTimezoneOffset()+60;
|
||||||
|
} else {
|
||||||
|
return today.getTimezoneOffset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function promptUser(eleId, UUID=null){
|
function promptUser(eleId, UUID=null){
|
||||||
if (session.beepToNotify){
|
if (session.beepToNotify){
|
||||||
@ -1337,6 +1487,100 @@ function getStorage(cname) {
|
|||||||
return item.value;
|
return item.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function play(streamid=null, UUID=false){ // play whatever is in the URL params; or filter by a streamID option
|
||||||
|
log("play stream: "+session.view);
|
||||||
|
if (session.view===""){
|
||||||
|
log("Setting view to null disables all playback");
|
||||||
|
} else if (session.view !== false){
|
||||||
|
var viewlist = session.view.split(",");
|
||||||
|
var played = false;
|
||||||
|
for (var j in viewlist){
|
||||||
|
if (viewlist[j]==""){
|
||||||
|
played=true;
|
||||||
|
// view={blank} implies do not play anything. Useful for setting a default bitrate I guess
|
||||||
|
} else if (streamid===null){ // play what is in the view list ; not a group room probably
|
||||||
|
session.watchStream(viewlist[j]);
|
||||||
|
played=true;
|
||||||
|
} else if (streamid === viewlist[j]){ // plays if the group room list matches the explicit list
|
||||||
|
session.watchStream(viewlist[j]);
|
||||||
|
played=true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!played){
|
||||||
|
if (session.scene!==false){
|
||||||
|
if (!session.permaid){
|
||||||
|
if (!session.queue){ // I don't want to deal with queues.
|
||||||
|
if (session.exclude===false || (!session.exclude.includes(streamid))){
|
||||||
|
if (UUID){
|
||||||
|
if (session.directorList.indexOf(UUID)>=0){
|
||||||
|
warnlog("stream ID added to badStreamList: "+streamid);
|
||||||
|
session.badStreamList.push(streamid);
|
||||||
|
session.watchStream(streamid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (session.exclude !== false){
|
||||||
|
if (session.exclude.includes(streamid)){
|
||||||
|
// we don't play it at all. (if explicity listed as VIDEO, then OKay.)
|
||||||
|
} else {
|
||||||
|
session.watchStream(streamid); // I suppose we do play it.
|
||||||
|
}
|
||||||
|
} else if (streamid){
|
||||||
|
session.watchStream(streamid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextQueue(){
|
||||||
|
if (!session.queue){return;}
|
||||||
|
if (!session.director){return;}
|
||||||
|
if (session.queueList.length==0){
|
||||||
|
getById("queuebutton").classList.add("float2");
|
||||||
|
getById("queuebutton").classList.add("red");
|
||||||
|
getById("queuebutton").classList.remove("float");
|
||||||
|
setTimeout(function(){
|
||||||
|
getById("queuebutton").classList.add("float");
|
||||||
|
getById("queuebutton").classList.remove("float2");
|
||||||
|
getById("queuebutton").classList.remove("red");
|
||||||
|
},50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var nextStream = session.queueList.shift();
|
||||||
|
|
||||||
|
|
||||||
|
getById("queuebutton").classList.add("float2");
|
||||||
|
getById("queuebutton").classList.remove("float");
|
||||||
|
setTimeout(function(){
|
||||||
|
getById("queuebutton").classList.add("float");
|
||||||
|
getById("queuebutton").classList.remove("float2");
|
||||||
|
},200);
|
||||||
|
|
||||||
|
updateQueue();
|
||||||
|
|
||||||
|
session.watchStream(nextStream);
|
||||||
|
log("next stream loading: "+nextStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQueue(){
|
||||||
|
if (!session.queue){return;}
|
||||||
|
if (!session.director){return;}
|
||||||
|
if (session.queueList.length) {
|
||||||
|
if (session.queueList.length>10){
|
||||||
|
getById("queueNotification").innerHTML = "‼";
|
||||||
|
} else {
|
||||||
|
getById("queueNotification").innerHTML = session.queueList.length;
|
||||||
|
}
|
||||||
|
getById("queueNotification").classList.add("queueNotification");
|
||||||
|
} else {
|
||||||
|
getById("queueNotification").innerHTML = "";
|
||||||
|
getById("queueNotification").classList.remove("queueNotification");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setupIncomingScreenTracking(v, UUID){ // SCREEN element.
|
function setupIncomingScreenTracking(v, UUID){ // SCREEN element.
|
||||||
|
|
||||||
if (session.directorList.indexOf(UUID)>=0){
|
if (session.directorList.indexOf(UUID)>=0){
|
||||||
@ -1635,7 +1879,7 @@ function setupIncomingScreenTracking(v, UUID){ // SCREEN element.
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(session.processStats, 1000, UUID);
|
setTimeout(processStats, 1000, UUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupIncomingVideoTracking(v, UUID){ // video element.
|
function setupIncomingVideoTracking(v, UUID){ // video element.
|
||||||
@ -1914,7 +2158,7 @@ function setupIncomingVideoTracking(v, UUID){ // video element.
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(session.processStats, 1000, UUID);
|
setTimeout(processStats, 1000, UUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mediaSourceUpdated(UUID, streamID, videoTrack){
|
function mediaSourceUpdated(UUID, streamID, videoTrack){
|
||||||
@ -1968,6 +2212,34 @@ function updateVolume(update=false){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var updateMixerTimer = null;
|
||||||
|
var updateMixerActive = false;
|
||||||
|
//var cleanupTimeout = null;
|
||||||
|
function updateMixer(e=false){
|
||||||
|
clearInterval(updateMixerTimer);
|
||||||
|
if (updateMixerActive){
|
||||||
|
if (session.mobile){
|
||||||
|
updateMixerTimer = setTimeout(function(){updateMixer();},200);
|
||||||
|
} else {
|
||||||
|
updateMixerTimer = setTimeout(function(){updateMixer();},50);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateMixerActive=true;
|
||||||
|
log("updating mixer");
|
||||||
|
try{
|
||||||
|
updateMixerRun(e);
|
||||||
|
// clearInterval(cleanupTimeout);
|
||||||
|
// cleanupTimeout = setTimeout(function(){deleteOldMedia();},60000);
|
||||||
|
|
||||||
|
} catch(e){}
|
||||||
|
|
||||||
|
if (session.mobile){
|
||||||
|
setTimeout(function(){updateMixerActive=false;},500);
|
||||||
|
} else {
|
||||||
|
setTimeout(function(){updateMixerActive=false;},100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateMixerRun(e=false){ // this is the main auto-mixing code. It's a giant function that runs when there are changes to screensize, video track statuses, etc.
|
function updateMixerRun(e=false){ // this is the main auto-mixing code. It's a giant function that runs when there are changes to screensize, video track statuses, etc.
|
||||||
try {
|
try {
|
||||||
@ -4990,6 +5262,16 @@ function drawFace() {
|
|||||||
}
|
}
|
||||||
//////// END CANVAS EFFECTS ///////////////////
|
//////// END CANVAS EFFECTS ///////////////////
|
||||||
|
|
||||||
|
|
||||||
|
function getNativeOutputResolution(){
|
||||||
|
var tracks = session.videoElement.srcObject.getVideoTracks();
|
||||||
|
if (tracks.length && tracks[0].getSettings){
|
||||||
|
return tracks[0].getSettings();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function remoteStats(msg, UUID){
|
function remoteStats(msg, UUID){
|
||||||
if (session.director){
|
if (session.director){
|
||||||
var output = "";
|
var output = "";
|
||||||
@ -5034,6 +5316,338 @@ function remoteStats(msg, UUID){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processStats(UUID){
|
||||||
|
|
||||||
|
// for (pc in session.pcs){session.pcs[pc].getStats().then(function(stats) {stats.forEach(stat=>{if (stat.id.includes("RTCIce")){console.log(stat)}})})};
|
||||||
|
|
||||||
|
if (!session.rpcs || !(UUID in session.rpcs)){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (session.rpcs[UUID].videoElement.paused){
|
||||||
|
log("trying to play");
|
||||||
|
session.rpcs[UUID].videoElement.play().then(_ => {
|
||||||
|
log("playing");
|
||||||
|
session.firstPlayTriggered=true;
|
||||||
|
}).catch(warnlog);
|
||||||
|
}
|
||||||
|
} catch (e){};
|
||||||
|
|
||||||
|
if (session.rpcs[UUID].mc){
|
||||||
|
processMeshcastStats(UUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
session.rpcs[UUID].getStats().then(function(stats){
|
||||||
|
if (!(UUID in session.rpcs)){return;}
|
||||||
|
|
||||||
|
setTimeout(processStats, 3000, UUID);
|
||||||
|
|
||||||
|
if (!session.rpcs[UUID].stats['Peer-to-Peer Connection']){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.forEach(stat=>{
|
||||||
|
|
||||||
|
if ((stat.type == "candidate-pair") && (stat.nominated==true)){
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection']._local_ice_id = stat.localCandidateId;
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection']._remote_ice_id = stat.remoteCandidateId;
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].Round_Trip_Time_ms = stat.currentRoundTripTime*1000;
|
||||||
|
|
||||||
|
} else if ((stat.type=="track") && (stat.remoteSource==true)){
|
||||||
|
|
||||||
|
|
||||||
|
if (stat.id in session.rpcs[UUID].stats){
|
||||||
|
session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier;
|
||||||
|
session.rpcs[UUID].stats[stat.id].Buffer_Delay_in_ms = parseInt(1000*(parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)/(parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
|
||||||
|
if ("frameWidth" in stat){
|
||||||
|
if ("frameHeight" in stat){
|
||||||
|
session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth+" x "+stat.frameHeight;
|
||||||
|
session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth;
|
||||||
|
session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var media = {};
|
||||||
|
media._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
|
||||||
|
media._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
|
||||||
|
media.Buffer_Delay_in_ms = 0;
|
||||||
|
media._trackID = stat.trackIdentifier;
|
||||||
|
session.rpcs[UUID].stats[stat.id] = media;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (stat.type=="remote-candidate"){
|
||||||
|
|
||||||
|
if (("_remote_ice_id" in session.rpcs[UUID].stats['Peer-to-Peer Connection']) && (session.rpcs[UUID].stats['Peer-to-Peer Connection']._remote_ice_id != stat.id )){return;} // not matched to nominated one
|
||||||
|
|
||||||
|
if ("candidateType" in stat){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].remote_candidateType = stat.candidateType;
|
||||||
|
if (stat.candidateType === "relay"){
|
||||||
|
if ("relayProtocol" in stat){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].remote_relayProtocol = stat.relayProtocol;
|
||||||
|
}
|
||||||
|
if ("ip" in stat){session.rpcs[UUID].stats['Peer-to-Peer Connection'].remote_relay_IP = stat.ip;}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
delete session.rpcs[UUID].stats['Peer-to-Peer Connection'].local_relayIP;
|
||||||
|
delete session.rpcs[UUID].stats['Peer-to-Peer Connection'].local_relayProtocol;
|
||||||
|
} catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("networkType" in stat){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].remote_networkType = stat.networkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (stat.type=="local-candidate"){
|
||||||
|
|
||||||
|
if (("_local_ice_id" in session.rpcs[UUID].stats['Peer-to-Peer Connection']) && (session.rpcs[UUID].stats['Peer-to-Peer Connection']._local_ice_id != stat.id )){return;} // not matched to nominated one
|
||||||
|
|
||||||
|
if ("candidateType" in stat){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].local_candidateType = stat.candidateType;
|
||||||
|
if (stat.candidateType === "relay"){
|
||||||
|
if ("relayProtocol" in stat){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].local_relayProtocol = stat.relayProtocol;
|
||||||
|
}
|
||||||
|
if ("ip" in stat){session.rpcs[UUID].stats['Peer-to-Peer Connection'].local_relayIP = stat.ip;}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
delete session.rpcs[UUID].stats['Peer-to-Peer Connection'].local_relayIP;
|
||||||
|
delete session.rpcs[UUID].stats['Peer-to-Peer Connection'].local_relayProtocol;
|
||||||
|
} catch(e){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("networkType" in stat){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].remote_networkType = stat.networkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (stat.type == "transport"){
|
||||||
|
if ("bytesReceived" in stat) {
|
||||||
|
if ("_bytesReceived" in session.rpcs[UUID].stats['Peer-to-Peer Connection']){
|
||||||
|
if (session.rpcs[UUID].stats['Peer-to-Peer Connection']._timestamp){
|
||||||
|
if (stat.timestamp){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].total_recv_bitrate_kbps = parseInt(8*(stat.bytesReceived - session.rpcs[UUID].stats['Peer-to-Peer Connection']._bytesReceived)/(stat.timestamp - session.rpcs[UUID].stats['Peer-to-Peer Connection']._timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection']._bytesReceived = stat.bytesReceived;
|
||||||
|
}
|
||||||
|
if ("timestamp" in stat) {
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection']._timestamp = stat.timestamp;
|
||||||
|
if (!session.rpcs[UUID].stats['Peer-to-Peer Connection']._timestampStart){
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection']._timestampStart = stat.timestamp;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].stats['Peer-to-Peer Connection'].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats['Peer-to-Peer Connection']._timestampStart)/600)/100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if ((stat.type=="inbound-rtp") && ("trackId" in stat)){
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats[stat.trackId] = session.rpcs[UUID].stats[stat.trackId] || {};
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].Bitrate_in_kbps = parseInt(8*(stat.bytesReceived - session.rpcs[UUID].stats[stat.trackId]._last_bytes)/( stat.timestamp - session.rpcs[UUID].stats[stat.trackId]._last_time));
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[stat.trackId]._last_bytes;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._last_time = stat.timestamp || session.rpcs[UUID].stats[stat.trackId]._last_time;
|
||||||
|
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats._codecId = stat.codecId;
|
||||||
|
session.rpcs[UUID].stats._codecIdTrackId = stat.trackId;
|
||||||
|
|
||||||
|
if (stat.mediaType=="video"){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].type = "Video Track"
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._type = "video";
|
||||||
|
if ((session.obsfix) && ("codec" in session.rpcs[UUID].stats) && (session.rpcs[UUID].stats.codec=="video/VP8")){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].pliDelta = (stat.pliCount - session.rpcs[UUID].stats[stat.trackId].keyFramesRequested_pli) || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = (stat.nackCount - session.rpcs[UUID].stats[stat.trackId].streamErrors_nackCount + session.rpcs[UUID].stats[stat.trackId].nackTrigger) || 0;
|
||||||
|
|
||||||
|
log("OBS PLI FIX MODE ON");
|
||||||
|
if ((session.rpcs[UUID].stats[stat.trackId].pliDelta===0) && (session.rpcs[UUID].stats[stat.trackId].nackTrigger >= session.obsfix)){ // heavy packet loss with no pliCount?
|
||||||
|
session.requestKeyframe(UUID);
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = 0;
|
||||||
|
log("TRYING KEYFRAME");
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].pliDelta>0){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = 0;
|
||||||
|
}
|
||||||
|
} else if ((session.obsfix) && ("codec" in session.rpcs[UUID].stats) && (session.rpcs[UUID].stats.codec=="video/VP9")){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].pliDelta = (stat.pliCount - session.rpcs[UUID].stats[stat.trackId].keyFramesRequested_pli) || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = (stat.nackCount - session.rpcs[UUID].stats[stat.trackId].streamErrors_nackCount + session.rpcs[UUID].stats[stat.trackId].nackTrigger) || 0;
|
||||||
|
|
||||||
|
log("OBS PLI FIX MODE ON");
|
||||||
|
if ((session.rpcs[UUID].stats[stat.trackId].pliDelta===0) && (session.rpcs[UUID].stats[stat.trackId].nackTrigger >= (session.obsfix*4) )){ // heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often.
|
||||||
|
session.requestKeyframe(UUID);
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = 0;
|
||||||
|
log("TRYING KEYFRAME");
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].pliDelta>0){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].keyFramesRequested_pli = stat.pliCount || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].streamErrors_nackCount = stat.nackCount || 0;
|
||||||
|
|
||||||
|
//warnlog(stat);
|
||||||
|
|
||||||
|
if ("framesPerSecond" in stat){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].FPS = parseInt(stat.framesPerSecond);
|
||||||
|
} else if (("framesDecoded" in stat) && (stat.timestamp)){
|
||||||
|
|
||||||
|
var lastFramesDecoded = 0;
|
||||||
|
var lastTimestamp = 0;
|
||||||
|
try{
|
||||||
|
lastFramesDecoded = session.rpcs[UUID].stats[stat.trackId]._framesDecoded;
|
||||||
|
lastTimestamp = session.rpcs[UUID].stats[stat.trackId]._timestamp;
|
||||||
|
} catch(e){}
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].FPS = parseInt(10*(stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp))/10;
|
||||||
|
|
||||||
|
//session.rpcs[UUID].stats[stat.trackId].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp));
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._framesDecoded = stat.framesDecoded;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._timestamp = stat.timestamp/1000;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (stat.mediaType=="audio"){
|
||||||
|
//log("AUDIO LEVEL: "+stat.audioLevel);
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].type = "Audio Track";
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._type = "audio";
|
||||||
|
if ("audioLevel" in stat){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].audio_level = parseInt(parseFloat(stat.audioLevel)*10000)/10000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("packetsLost" in stat && "packetsReceived" in stat){
|
||||||
|
|
||||||
|
if (!("_packetsLost" in session.rpcs[UUID].stats[stat.trackId])){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._packetsLost = stat.packetsLost;
|
||||||
|
}
|
||||||
|
if (!("_packetsReceived" in session.rpcs[UUID].stats[stat.trackId])){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._packetsReceived = stat.packetsReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[stat.trackId])){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage = session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage*0.35 + 0.65*((stat.packetsLost-session.rpcs[UUID].stats[stat.trackId]._packetsLost)*100.0)/((stat.packetsReceived-session.rpcs[UUID].stats[stat.trackId]._packetsReceived)+(stat.packetsLost-session.rpcs[UUID].stats[stat.trackId]._packetsLost)) || 0;
|
||||||
|
|
||||||
|
if (session.rpcs[UUID].signalMeter && (session.rpcs[UUID].stats[stat.trackId]._type==="video")){
|
||||||
|
if (session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage<0.01){
|
||||||
|
if (session.rpcs[UUID].stats[stat.trackId].Bitrate_in_kbps==0){
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 0;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 5;
|
||||||
|
}
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage<0.3){
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 4;
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage<1.0){
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 3;
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage<3.5){
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 2;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._packetsReceived = stat.packetsReceived;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._packetsLost = stat.packetsLost;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (("_codecId" in session.rpcs[UUID].stats) && (stat.id == session.rpcs[UUID].stats._codecId)){
|
||||||
|
|
||||||
|
if ("mimeType" in stat){
|
||||||
|
if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]){
|
||||||
|
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {};
|
||||||
|
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("frameHeight" in stat){
|
||||||
|
if ("frameWidth" in stat){
|
||||||
|
session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth)+" x "+parseInt(stat.frameHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (session.buffer!==false){
|
||||||
|
playoutdelay(UUID);
|
||||||
|
}
|
||||||
|
setTimeout(function(){
|
||||||
|
session.directorSpeakerMute();
|
||||||
|
session.directorDisplayMute();
|
||||||
|
},0);
|
||||||
|
});
|
||||||
|
} catch (e){errorlog(e);}
|
||||||
|
};
|
||||||
|
|
||||||
|
function playoutdelay(UUID){ // applies a delay to all videos
|
||||||
|
try {
|
||||||
|
var target_buffer = session.buffer || 0;
|
||||||
|
target_buffer = parseFloat(target_buffer);
|
||||||
|
|
||||||
|
if (session.buffer!==false){
|
||||||
|
|
||||||
|
// if buffer is set, then session.sync will be set; at least to 0.
|
||||||
|
|
||||||
|
var receivers = getReceivers2(UUID).reverse(); //session.rpcs[UUID].getReceivers().reverse();
|
||||||
|
var video_delay = 0;
|
||||||
|
receivers.forEach(function(receiver){
|
||||||
|
try {
|
||||||
|
for (var tid in session.rpcs[UUID].stats){
|
||||||
|
|
||||||
|
if ((typeof( session.rpcs[UUID].stats[tid])=="object") && ("_trackID" in session.rpcs[UUID].stats[tid]) && (session.rpcs[UUID].stats[tid]._trackID===receiver.track.id) && ("Buffer_Delay_in_ms" in session.rpcs[UUID].stats[tid])){
|
||||||
|
|
||||||
|
var sync_offset = 0.0;
|
||||||
|
|
||||||
|
if (session.rpcs[UUID].stats[tid]._sync_offset){
|
||||||
|
sync_offset = session.rpcs[UUID].stats[tid]._sync_offset;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].stats[tid]._sync_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_offset += target_buffer - session.rpcs[UUID].stats[tid].Buffer_Delay_in_ms; // target_Butt
|
||||||
|
|
||||||
|
if (sync_offset>target_buffer){
|
||||||
|
sync_offset=target_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.rpcs[UUID].stats[tid]._type=="audio"){
|
||||||
|
if (receiver.track.id in session.rpcs[UUID].delayNode){
|
||||||
|
log("updating audio delay");
|
||||||
|
var audio_delay = video_delay - session.rpcs[UUID].stats[tid].Buffer_Delay_in_ms + session.sync; // video is typically showing greater delay than video
|
||||||
|
if (audio_delay<0){audio_delay=0;}
|
||||||
|
log("audio_delay : "+audio_delay);
|
||||||
|
log("audioCtx : "+ session.audioCtx.currentTime);
|
||||||
|
session.rpcs[UUID].delayNode[receiver.track.id].delayTime.setValueAtTime(parseFloat(audio_delay/1000.0), session.audioCtx.currentTime+1);
|
||||||
|
session.rpcs[UUID].stats[tid].Audio_Sync_Delay_ms = audio_delay;
|
||||||
|
|
||||||
|
}
|
||||||
|
} else if (session.rpcs[UUID].stats[tid]._type=="video"){
|
||||||
|
//log("THIS SHOULD BE BEFORE AUDIO - video track"+session.rpcs[UUID].stats[tid].type);
|
||||||
|
video_delay = session.rpcs[UUID].stats[tid].Buffer_Delay_in_ms;
|
||||||
|
if(sync_offset<0){sync_offset=0;}
|
||||||
|
session.rpcs[UUID].stats[tid]._sync_offset = sync_offset;
|
||||||
|
receiver.playoutDelayHint = parseFloat(sync_offset/1000); // only the video we are going to do the playout delay for; doesn't work well with audio.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e){errorlog(e);}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e){
|
||||||
|
errorlog("device does not support playout delay");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function printViewStats(menu, UUID) { // Stats for viewing a remote video
|
function printViewStats(menu, UUID) { // Stats for viewing a remote video
|
||||||
if (!session.rpcs[UUID]){
|
if (!session.rpcs[UUID]){
|
||||||
@ -5189,6 +5803,248 @@ function printValues(obj) { // see: printViewStats
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processMeshcastStats(UUID){
|
||||||
|
try {
|
||||||
|
session.rpcs[UUID].mc.getStats().then(function(stats){
|
||||||
|
if (!(UUID in session.rpcs)){return;}
|
||||||
|
|
||||||
|
if (!session.rpcs[UUID].stats['Meshcast Connection']){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.forEach(stat=>{
|
||||||
|
|
||||||
|
if ((stat.type == "candidate-pair") && (stat.nominated==true)){
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection']._local_ice_id = stat.localCandidateId;
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection']._remote_ice_id = stat.remoteCandidateId;
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].Round_Trip_Time_ms = stat.currentRoundTripTime*1000;
|
||||||
|
|
||||||
|
} else if ((stat.type=="track") && (stat.remoteSource==true)){
|
||||||
|
|
||||||
|
|
||||||
|
if (stat.id in session.rpcs[UUID].stats){
|
||||||
|
session.rpcs[UUID].stats[stat.id]._trackID = stat.trackIdentifier;
|
||||||
|
session.rpcs[UUID].stats[stat.id].Buffer_Delay_in_ms = parseInt(1000*(parseFloat(stat.jitterBufferDelay) - session.rpcs[UUID].stats[stat.id]._jitter_delay)/(parseInt(stat.jitterBufferEmittedCount) - session.rpcs[UUID].stats[stat.id]._jitter_count)) || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.id]._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.id]._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
|
||||||
|
if ("frameWidth" in stat){
|
||||||
|
if ("frameHeight" in stat){
|
||||||
|
session.rpcs[UUID].stats[stat.id].Resolution = stat.frameWidth+" x "+stat.frameHeight;
|
||||||
|
session.rpcs[UUID].stats[stat.id]._frameWidth = stat.frameWidth;
|
||||||
|
session.rpcs[UUID].stats[stat.id]._frameHeight = stat.frameHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var media = {};
|
||||||
|
media._jitter_delay = parseFloat(stat.jitterBufferDelay) || 0;
|
||||||
|
media._jitter_count = parseInt(stat.jitterBufferEmittedCount) || 0;
|
||||||
|
media.Buffer_Delay_in_ms = 0;
|
||||||
|
media._trackID = stat.trackIdentifier;
|
||||||
|
session.rpcs[UUID].stats[stat.id] = media;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (stat.type=="remote-candidate"){
|
||||||
|
|
||||||
|
if (("_remote_ice_id" in session.rpcs[UUID].stats['Meshcast Connection']) && (session.rpcs[UUID].stats['Meshcast Connection']._remote_ice_id != stat.id )){return;} // not matched to nominated one
|
||||||
|
|
||||||
|
if ("candidateType" in stat){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].remote_candidateType = stat.candidateType;
|
||||||
|
if (stat.candidateType === "relay"){
|
||||||
|
if ("relayProtocol" in stat){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].remote_relayProtocol = stat.relayProtocol;
|
||||||
|
}
|
||||||
|
if ("ip" in stat){session.rpcs[UUID].stats['Meshcast Connection'].remote_relay_IP = stat.ip;}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
delete session.rpcs[UUID].stats['Meshcast Connection'].local_relayIP;
|
||||||
|
delete session.rpcs[UUID].stats['Meshcast Connection'].local_relayProtocol;
|
||||||
|
} catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("networkType" in stat){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].remote_networkType = stat.networkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (stat.type=="local-candidate"){
|
||||||
|
|
||||||
|
if (("_local_ice_id" in session.rpcs[UUID].stats['Meshcast Connection']) && (session.rpcs[UUID].stats['Meshcast Connection']._local_ice_id != stat.id )){return;} // not matched to nominated one
|
||||||
|
|
||||||
|
if ("candidateType" in stat){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].local_candidateType = stat.candidateType;
|
||||||
|
if (stat.candidateType === "relay"){
|
||||||
|
if ("relayProtocol" in stat){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].local_relayProtocol = stat.relayProtocol;
|
||||||
|
}
|
||||||
|
if ("ip" in stat){session.rpcs[UUID].stats['Meshcast Connection'].local_relayIP = stat.ip;}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
delete session.rpcs[UUID].stats['Meshcast Connection'].local_relayIP;
|
||||||
|
delete session.rpcs[UUID].stats['Meshcast Connection'].local_relayProtocol;
|
||||||
|
} catch(e){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("networkType" in stat){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].remote_networkType = stat.networkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (stat.type == "transport"){
|
||||||
|
if ("bytesReceived" in stat) {
|
||||||
|
if ("_bytesReceived" in session.rpcs[UUID].stats['Meshcast Connection']){
|
||||||
|
if (session.rpcs[UUID].stats['Meshcast Connection']._timestamp){
|
||||||
|
if (stat.timestamp){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].total_recv_bitrate_kbps = parseInt(8*(stat.bytesReceived - session.rpcs[UUID].stats['Meshcast Connection']._bytesReceived)/(stat.timestamp - session.rpcs[UUID].stats['Meshcast Connection']._timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection']._bytesReceived = stat.bytesReceived;
|
||||||
|
}
|
||||||
|
if ("timestamp" in stat) {
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection']._timestamp = stat.timestamp;
|
||||||
|
if (!session.rpcs[UUID].stats['Meshcast Connection']._timestampStart){
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection']._timestampStart = stat.timestamp;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].stats['Meshcast Connection'].time_active_minutes = parseInt((stat.timestamp - session.rpcs[UUID].stats['Meshcast Connection']._timestampStart)/600)/100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if ((stat.type=="inbound-rtp") && ("trackId" in stat)){
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats[stat.trackId] = session.rpcs[UUID].stats[stat.trackId] || {};
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].Bitrate_in_kbps = parseInt(8*(stat.bytesReceived - session.rpcs[UUID].stats[stat.trackId]._last_bytes)/( stat.timestamp - session.rpcs[UUID].stats[stat.trackId]._last_time));
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._last_bytes = stat.bytesReceived || session.rpcs[UUID].stats[stat.trackId]._last_bytes;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._last_time = stat.timestamp || session.rpcs[UUID].stats[stat.trackId]._last_time;
|
||||||
|
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats._codecId = stat.codecId;
|
||||||
|
session.rpcs[UUID].stats._codecIdTrackId = stat.trackId;
|
||||||
|
|
||||||
|
if (stat.mediaType=="video"){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].type = "Video Track"
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._type = "video";
|
||||||
|
if ((session.obsfix) && ("codec" in session.rpcs[UUID].stats) && (session.rpcs[UUID].stats.codec=="video/VP8")){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].pliDelta = (stat.pliCount - session.rpcs[UUID].stats[stat.trackId].keyFramesRequested_pli) || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = (stat.nackCount - session.rpcs[UUID].stats[stat.trackId].streamErrors_nackCount + session.rpcs[UUID].stats[stat.trackId].nackTrigger) || 0;
|
||||||
|
|
||||||
|
log("OBS PLI FIX MODE ON");
|
||||||
|
if ((session.rpcs[UUID].stats[stat.trackId].pliDelta===0) && (session.rpcs[UUID].stats[stat.trackId].nackTrigger >= session.obsfix)){ // heavy packet loss with no pliCount?
|
||||||
|
session.requestKeyframe(UUID);
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = 0;
|
||||||
|
log("TRYING KEYFRAME");
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].pliDelta>0){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = 0;
|
||||||
|
}
|
||||||
|
} else if ((session.obsfix) && ("codec" in session.rpcs[UUID].stats) && (session.rpcs[UUID].stats.codec=="video/VP9")){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].pliDelta = (stat.pliCount - session.rpcs[UUID].stats[stat.trackId].keyFramesRequested_pli) || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = (stat.nackCount - session.rpcs[UUID].stats[stat.trackId].streamErrors_nackCount + session.rpcs[UUID].stats[stat.trackId].nackTrigger) || 0;
|
||||||
|
|
||||||
|
log("OBS PLI FIX MODE ON");
|
||||||
|
if ((session.rpcs[UUID].stats[stat.trackId].pliDelta===0) && (session.rpcs[UUID].stats[stat.trackId].nackTrigger >= (session.obsfix*4) )){ // heavy packet loss with no pliCount? well, VP9 will trigger hopefully not as often.
|
||||||
|
session.requestKeyframe(UUID);
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = 0;
|
||||||
|
log("TRYING KEYFRAME");
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].pliDelta>0){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].nackTrigger = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].keyFramesRequested_pli = stat.pliCount || 0;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].streamErrors_nackCount = stat.nackCount || 0;
|
||||||
|
|
||||||
|
//warnlog(stat);
|
||||||
|
|
||||||
|
if ("framesPerSecond" in stat){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].FPS = parseInt(stat.framesPerSecond);
|
||||||
|
} else if (("framesDecoded" in stat) && (stat.timestamp)){
|
||||||
|
|
||||||
|
var lastFramesDecoded = 0;
|
||||||
|
var lastTimestamp = 0;
|
||||||
|
try{
|
||||||
|
lastFramesDecoded = session.rpcs[UUID].stats[stat.trackId]._framesDecoded;
|
||||||
|
lastTimestamp = session.rpcs[UUID].stats[stat.trackId]._timestamp;
|
||||||
|
} catch(e){}
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].FPS = parseInt(10*(stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp))/10;
|
||||||
|
|
||||||
|
//session.rpcs[UUID].stats[stat.trackId].FPS = parseInt((stat.framesDecoded - lastFramesDecoded)/(stat.timestamp/1000 - lastTimestamp));
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._framesDecoded = stat.framesDecoded;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._timestamp = stat.timestamp/1000;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else if (stat.mediaType=="audio"){
|
||||||
|
//log("AUDIO LEVEL: "+stat.audioLevel);
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].type = "Audio Track";
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._type = "audio";
|
||||||
|
if ("audioLevel" in stat){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].audio_level = parseInt(parseFloat(stat.audioLevel)*10000)/10000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("packetsLost" in stat && "packetsReceived" in stat){
|
||||||
|
|
||||||
|
if (!("_packetsLost" in session.rpcs[UUID].stats[stat.trackId])){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._packetsLost = stat.packetsLost;
|
||||||
|
}
|
||||||
|
if (!("_packetsReceived" in session.rpcs[UUID].stats[stat.trackId])){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._packetsReceived = stat.packetsReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("packetLoss_in_percentage" in session.rpcs[UUID].stats[stat.trackId])){
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage = session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage*0.35 + 0.65*((stat.packetsLost-session.rpcs[UUID].stats[stat.trackId]._packetsLost)*100.0)/((stat.packetsReceived-session.rpcs[UUID].stats[stat.trackId]._packetsReceived)+(stat.packetsLost-session.rpcs[UUID].stats[stat.trackId]._packetsLost)) || 0;
|
||||||
|
|
||||||
|
if (session.rpcs[UUID].signalMeter && (session.rpcs[UUID].stats[stat.trackId]._type==="video")){
|
||||||
|
if (session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage<0.01){
|
||||||
|
if (session.rpcs[UUID].stats[stat.trackId].Bitrate_in_kbps==0){
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 0;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 5;
|
||||||
|
}
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage<0.3){
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 4;
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage<1.0){
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 3;
|
||||||
|
} else if (session.rpcs[UUID].stats[stat.trackId].packetLoss_in_percentage<3.5){
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 2;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].signalMeter.dataset.level = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._packetsReceived = stat.packetsReceived;
|
||||||
|
session.rpcs[UUID].stats[stat.trackId]._packetsLost = stat.packetsLost;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (("_codecId" in session.rpcs[UUID].stats) && (stat.id == session.rpcs[UUID].stats._codecId)){
|
||||||
|
|
||||||
|
if ("mimeType" in stat){
|
||||||
|
if (session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId]){
|
||||||
|
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
|
||||||
|
} else {
|
||||||
|
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId] = {};
|
||||||
|
session.rpcs[UUID].stats[session.rpcs[UUID].stats._codecIdTrackId].codec = stat.mimeType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("frameHeight" in stat){
|
||||||
|
if ("frameWidth" in stat){
|
||||||
|
session.rpcs[UUID].stats.Resolution = parseInt(stat.frameWidth)+" x "+parseInt(stat.frameHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e){errorlog(e);}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function printMyStats(menu) { // see: setupStatsMenu
|
function printMyStats(menu) { // see: setupStatsMenu
|
||||||
var scrollLeft = getById("menuStatsBox").scrollLeft;
|
var scrollLeft = getById("menuStatsBox").scrollLeft;
|
||||||
@ -18932,6 +19788,18 @@ async function recordVideo(target, event, videoKbps = false) { // event.currentT
|
|||||||
|
|
||||||
var UUID = target.dataset.UUID;
|
var UUID = target.dataset.UUID;
|
||||||
var video = session.rpcs[UUID].videoElement;
|
var video = session.rpcs[UUID].videoElement;
|
||||||
|
|
||||||
|
if (video.stopWriter){
|
||||||
|
video.stopWriter();
|
||||||
|
updateLocalRecordButton(UUID, -1);
|
||||||
|
return;
|
||||||
|
} else if (video.startWriter){
|
||||||
|
await video.startWriter();
|
||||||
|
updateLocalRecordButton(UUID, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var audioKbps = false;
|
var audioKbps = false;
|
||||||
|
|
||||||
if (event === null) {
|
if (event === null) {
|
||||||
|
|||||||
22
main.css
22
main.css
@ -1034,7 +1034,27 @@ input[type='radio'] { cursor:pointer; }
|
|||||||
margin: 44vh auto;
|
margin: 44vh auto;
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
#retryimage{
|
||||||
|
display: block;
|
||||||
|
margin: 25vh auto;
|
||||||
|
max-width: 50%;
|
||||||
|
max-height: 50%;
|
||||||
|
animation: fadeIn 2s;
|
||||||
|
}
|
||||||
|
#retrymessage{
|
||||||
|
display: block;
|
||||||
|
margin: 80vh auto;
|
||||||
|
animation: fadeIn 2s;
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin-animation {
|
@keyframes spin-animation {
|
||||||
0% {
|
0% {
|
||||||
|
|||||||
85
main.js
85
main.js
@ -718,6 +718,10 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
|||||||
session.preloadbitrate = parseInt(urlParams.get('preloadbitrate')) || 0; // 1000
|
session.preloadbitrate = parseInt(urlParams.get('preloadbitrate')) || 0; // 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (urlParams.has('rampuptime')) {
|
||||||
|
session.rampUpTime = parseInt(urlParams.get('rampuptime')) || 10000;
|
||||||
|
}
|
||||||
|
|
||||||
if (urlParams.has('scenetype') || urlParams.has('type')) {
|
if (urlParams.has('scenetype') || urlParams.has('type')) {
|
||||||
session.sceneType = parseInt(urlParams.get('scenetype')) || parseInt(urlParams.get('type')) || false;
|
session.sceneType = parseInt(urlParams.get('scenetype')) || parseInt(urlParams.get('type')) || false;
|
||||||
}
|
}
|
||||||
@ -1368,7 +1372,26 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
|||||||
session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers?
|
session.disableWebAudio = true; // default true; might be useful to disable on slow or old computers?
|
||||||
session.audioMeterGuest = false;
|
session.audioMeterGuest = false;
|
||||||
session.audioEffects = false;
|
session.audioEffects = false;
|
||||||
session.obsfix = 15; // can be manually set via URL. ; VP8=15, VP9=30. (previous was 20.)
|
if (window.obsstudio.pluginVersion){
|
||||||
|
if (navigator.userAgent.indexOf('Mac OS X') !== -1){ // if mac, no fix
|
||||||
|
//session.obsfix = false;
|
||||||
|
} else if (window.obsstudio.pluginVersion=="2.17.4"){ // if obs v27.2 beta, no fix
|
||||||
|
//session.obsfix = false;
|
||||||
|
} else {
|
||||||
|
var ver = window.obsstudio.pluginVersion.split(".");
|
||||||
|
if (ver.length >= 2){
|
||||||
|
if (parseInt(ver[0])<=2){
|
||||||
|
if (parseInt(ver[0])==2){
|
||||||
|
if (parseInt(ver[1])<=16){
|
||||||
|
session.obsfix = 15;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session.obsfix = 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
log("OBS VERSION:" + window.obsstudio.pluginVersion);
|
log("OBS VERSION:" + window.obsstudio.pluginVersion);
|
||||||
log("macOS: " + navigator.userAgent.indexOf('Mac OS X') != -1);
|
log("macOS: " + navigator.userAgent.indexOf('Mac OS X') != -1);
|
||||||
@ -1493,9 +1516,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
|||||||
}
|
}
|
||||||
} catch(e){errorlog(e);}
|
} catch(e){errorlog(e);}
|
||||||
|
|
||||||
if (urlParams.has("videodevice") || urlParams.has("vdevice") || urlParams.has('vd') || urlParams.has('device') || urlParams.has('d')) {
|
if (urlParams.has("videodevice") || urlParams.has("vdevice") || urlParams.has('vd') || urlParams.has('device') || urlParams.has('d') || urlParams.has('vdo')) {
|
||||||
|
|
||||||
session.videoDevice = urlParams.get("videodevice") || urlParams.get("vdevice") || urlParams.get("vd") || urlParams.get("device") || urlParams.get("d");
|
session.videoDevice = urlParams.get("videodevice") || urlParams.get("vdevice") || urlParams.get("vd") || urlParams.get("device") || urlParams.get("d") || urlParams.get("vdo");
|
||||||
|
|
||||||
if (session.videoDevice === null) {
|
if (session.videoDevice === null) {
|
||||||
session.videoDevice = "1";
|
session.videoDevice = "1";
|
||||||
@ -1528,7 +1551,10 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
|||||||
// whatever the user entered I guess, santitized.
|
// whatever the user entered I guess, santitized.
|
||||||
session.videoDevice = session.videoDevice.replace(/[\W]+/g, "_").toLowerCase();
|
session.videoDevice = session.videoDevice.replace(/[\W]+/g, "_").toLowerCase();
|
||||||
}
|
}
|
||||||
getById("videoMenu").style.display = "none";
|
|
||||||
|
if (!urlParams.has('vdo')){
|
||||||
|
getById("videoMenu").style.display = "none";
|
||||||
|
}
|
||||||
log("session.videoDevice:" + session.videoDevice);
|
log("session.videoDevice:" + session.videoDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1741,6 +1767,14 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
|||||||
session.sensorData = parseInt(session.sensorData);
|
session.sensorData = parseInt(session.sensorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (urlParams.has('ptime')) {
|
||||||
|
session.ptime = parseInt(urlParams.get('ptime')) || 20;
|
||||||
|
if (session.ptime<10){
|
||||||
|
session.ptime = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (urlParams.has('minptime')) {
|
if (urlParams.has('minptime')) {
|
||||||
session.minptime = parseInt(urlParams.get('minptime')) || 10;
|
session.minptime = parseInt(urlParams.get('minptime')) || 10;
|
||||||
if (session.minptime < 10) {
|
if (session.minptime < 10) {
|
||||||
@ -1761,16 +1795,6 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (urlParams.has('ptime')) {
|
|
||||||
session.ptime = parseInt(urlParams.get('ptime')) || 20;
|
|
||||||
if (session.minptime < 10) {
|
|
||||||
session.ptime = 10;
|
|
||||||
}
|
|
||||||
if (session.minptime > 300) {
|
|
||||||
session.ptime = 300;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (urlParams.has('codec')) {
|
if (urlParams.has('codec')) {
|
||||||
log("CODEC CHANGED");
|
log("CODEC CHANGED");
|
||||||
@ -2954,23 +2978,38 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
|||||||
getById("mainmenu").innerHTML = '';
|
getById("mainmenu").innerHTML = '';
|
||||||
getById("mainmenu").classList.remove("row");
|
getById("mainmenu").classList.remove("row");
|
||||||
|
|
||||||
|
var timeout = 5000;
|
||||||
|
if (urlParams.has('waittimeout')){
|
||||||
|
timeout = parseInt(urlParams.get('waittimeout')) || 0;
|
||||||
|
}
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
try {
|
try {
|
||||||
if ((session.view) && (!(session.cleanOutput))) {
|
if ((session.view)) {
|
||||||
if (document.getElementById("mainmenu")) {
|
if (document.getElementById("mainmenu")) {
|
||||||
getById("mainmenu").innerHTML += '<div class="retry-spinner" id="retrySpinner"></div>';
|
if (urlParams.has('waitimage')){
|
||||||
retrySpinner.title = miscTranslations["waiting-for-the-stream"]
|
getById("mainmenu").innerHTML += '<img id="retryimage"/>';
|
||||||
retrySpinner.onclick = function(){
|
getById("retryimage").src = decodeURIComponent(urlParams.get('waitimage'));
|
||||||
updateURL("cleanoutput");
|
getById("retryimage").onerror = function(){this.style.display='none';};
|
||||||
location.reload();
|
|
||||||
|
|
||||||
|
} else if (!(session.cleanOutput)){
|
||||||
|
getById("mainmenu").innerHTML += '<div class="retry-spinner" id="retrySpinner"></div>';
|
||||||
|
getById("retrySpinner").onclick = function(){
|
||||||
|
updateURL("cleanoutput");
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
getById("retrySpinner").title = miscTranslations["waiting-for-the-stream"]
|
||||||
|
}
|
||||||
|
if (urlParams.has('waitmessage')){
|
||||||
|
getById("mainmenu").innerHTML += '<div id="retrymessage"></div>';
|
||||||
|
getById("retrymessage").innerText = urlParams.get('waitmessage');
|
||||||
|
getById("retrySpinner").title = urlParams.get('waitmessage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorlog("Error handling QR Code failure");
|
errorlog(e);
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, timeout);
|
||||||
|
|
||||||
log("auto playing");
|
log("auto playing");
|
||||||
var SafariVer = safariVersion();
|
var SafariVer = safariVersion();
|
||||||
@ -3267,7 +3306,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
|||||||
warnlog(e.data.getDeviceList);
|
warnlog(e.data.getDeviceList);
|
||||||
enumerateDevices().then(function(deviceInfos) {
|
enumerateDevices().then(function(deviceInfos) {
|
||||||
parent.postMessage({
|
parent.postMessage({
|
||||||
"deviceList": deviceInfos
|
"deviceList": JSON.parse(JSON.stringify(deviceInfos))
|
||||||
}, "*");
|
}, "*");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
496
mixer.html
496
mixer.html
@ -7,7 +7,7 @@
|
|||||||
body{
|
body{
|
||||||
padding:0;
|
padding:0;
|
||||||
margin:0;
|
margin:0;
|
||||||
background-color: #0000;
|
background-color: #c5c2c2;
|
||||||
}
|
}
|
||||||
iframe {
|
iframe {
|
||||||
border:0;
|
border:0;
|
||||||
@ -89,452 +89,6 @@
|
|||||||
padding: 10px 10px 0 15px;
|
padding: 10px 10px 0 15px;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes move {
|
|
||||||
100% {
|
|
||||||
transform: translate3d(0, 0, 1px) rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.background {
|
|
||||||
position: fixed;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background: #bfbfbf;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index:-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.background span {
|
|
||||||
width: 50vmin;
|
|
||||||
height: 50vmin;
|
|
||||||
border-radius: 50vmin;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
position: absolute;
|
|
||||||
animation: move;
|
|
||||||
animation-duration: 7;
|
|
||||||
animation-timing-function: linear;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.background span:nth-child(0) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 26%;
|
|
||||||
left: 54%;
|
|
||||||
animation-duration: 133s;
|
|
||||||
animation-delay: -143s;
|
|
||||||
transform-origin: 17vw 23vh;
|
|
||||||
box-shadow: 100vmin 0 13.032236536031524vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(1) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 32%;
|
|
||||||
left: 60%;
|
|
||||||
animation-duration: 98s;
|
|
||||||
animation-delay: -19s;
|
|
||||||
transform-origin: -9vw -17vh;
|
|
||||||
box-shadow: -100vmin 0 13.361880379427102vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(2) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 76%;
|
|
||||||
left: 30%;
|
|
||||||
animation-duration: 27s;
|
|
||||||
animation-delay: -382s;
|
|
||||||
transform-origin: -21vw -12vh;
|
|
||||||
box-shadow: -100vmin 0 13.29654068929897vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(3) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 13%;
|
|
||||||
left: 3%;
|
|
||||||
animation-duration: 89s;
|
|
||||||
animation-delay: -71s;
|
|
||||||
transform-origin: -5vw -23vh;
|
|
||||||
box-shadow: -100vmin 0 13.269376230706396vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(4) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 98%;
|
|
||||||
left: 83%;
|
|
||||||
animation-duration: 61s;
|
|
||||||
animation-delay: -57s;
|
|
||||||
transform-origin: 19vw -16vh;
|
|
||||||
box-shadow: 100vmin 0 13.465063933009704vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(5) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 73%;
|
|
||||||
left: 100%;
|
|
||||||
animation-duration: 312s;
|
|
||||||
animation-delay: -390s;
|
|
||||||
transform-origin: 23vw -6vh;
|
|
||||||
box-shadow: -100vmin 0 12.719463296930117vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(6) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 6%;
|
|
||||||
left: 84%;
|
|
||||||
animation-duration: 176s;
|
|
||||||
animation-delay: -255s;
|
|
||||||
transform-origin: 16vw 3vh;
|
|
||||||
box-shadow: -100vmin 0 13.358884039462355vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(7) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 99%;
|
|
||||||
left: 9%;
|
|
||||||
animation-duration: 221s;
|
|
||||||
animation-delay: -385s;
|
|
||||||
transform-origin: -16vw -9vh;
|
|
||||||
box-shadow: 100vmin 0 13.435082385169103vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(8) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 8%;
|
|
||||||
left: 92%;
|
|
||||||
animation-duration: 142s;
|
|
||||||
animation-delay: -99s;
|
|
||||||
transform-origin: -1vw -14vh;
|
|
||||||
box-shadow: -100vmin 0 13.456440775816723vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(9) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 80%;
|
|
||||||
left: 70%;
|
|
||||||
animation-duration: 99s;
|
|
||||||
animation-delay: -124s;
|
|
||||||
transform-origin: 21vw -10vh;
|
|
||||||
box-shadow: 100vmin 0 13.007405893839119vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(10) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 42%;
|
|
||||||
left: 2%;
|
|
||||||
animation-duration: 70s;
|
|
||||||
animation-delay: -341s;
|
|
||||||
transform-origin: -15vw -23vh;
|
|
||||||
box-shadow: 100vmin 0 13.005431373357645vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(11) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 28%;
|
|
||||||
left: 2%;
|
|
||||||
animation-duration: 244s;
|
|
||||||
animation-delay: -369s;
|
|
||||||
transform-origin: -17vw -16vh;
|
|
||||||
box-shadow: 100vmin 0 13.351086475613746vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(12) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 80%;
|
|
||||||
left: 50%;
|
|
||||||
animation-duration: 117s;
|
|
||||||
animation-delay: -399s;
|
|
||||||
transform-origin: 24vw 8vh;
|
|
||||||
box-shadow: 100vmin 0 12.979558964166843vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(13) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 74%;
|
|
||||||
left: 65%;
|
|
||||||
animation-duration: 283s;
|
|
||||||
animation-delay: -50s;
|
|
||||||
transform-origin: 13vw 14vh;
|
|
||||||
box-shadow: 100vmin 0 13.282742227479792vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(14) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 64%;
|
|
||||||
left: 61%;
|
|
||||||
animation-duration: 194s;
|
|
||||||
animation-delay: -278s;
|
|
||||||
transform-origin: -6vw 4vh;
|
|
||||||
box-shadow: -100vmin 0 13.333280580396286vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(15) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 11%;
|
|
||||||
left: 86%;
|
|
||||||
animation-duration: 403s;
|
|
||||||
animation-delay: -329s;
|
|
||||||
transform-origin: 3vw 0vh;
|
|
||||||
box-shadow: -100vmin 0 13.1162690433691vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(16) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 14%;
|
|
||||||
left: 59%;
|
|
||||||
animation-duration: 100s;
|
|
||||||
animation-delay: -15s;
|
|
||||||
transform-origin: -23vw 21vh;
|
|
||||||
box-shadow: -100vmin 0 13.30508866110655vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(17) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 68%;
|
|
||||||
left: 45%;
|
|
||||||
animation-duration: 394s;
|
|
||||||
animation-delay: -137s;
|
|
||||||
transform-origin: 18vw -20vh;
|
|
||||||
box-shadow: -100vmin 0 12.809008306332636vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(18) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 7%;
|
|
||||||
left: 33%;
|
|
||||||
animation-duration: 132s;
|
|
||||||
animation-delay: -346s;
|
|
||||||
transform-origin: -22vw -17vh;
|
|
||||||
box-shadow: -100vmin 0 12.940258664941906vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(19) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 24%;
|
|
||||||
left: 65%;
|
|
||||||
animation-duration: 49s;
|
|
||||||
animation-delay: -44s;
|
|
||||||
transform-origin: 0vw -24vh;
|
|
||||||
box-shadow: -100vmin 0 12.733081490401828vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(20) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 98%;
|
|
||||||
left: 49%;
|
|
||||||
animation-duration: 204s;
|
|
||||||
animation-delay: -160s;
|
|
||||||
transform-origin: -1vw 3vh;
|
|
||||||
box-shadow: -100vmin 0 13.05844646387434vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(21) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 2%;
|
|
||||||
left: 29%;
|
|
||||||
animation-duration: 294s;
|
|
||||||
animation-delay: -272s;
|
|
||||||
transform-origin: 13vw -18vh;
|
|
||||||
box-shadow: -100vmin 0 12.626917095797205vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(22) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 73%;
|
|
||||||
left: 32%;
|
|
||||||
animation-duration: 424s;
|
|
||||||
animation-delay: -59s;
|
|
||||||
transform-origin: 19vw 3vh;
|
|
||||||
box-shadow: 100vmin 0 13.092155905631065vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(23) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 45%;
|
|
||||||
left: 93%;
|
|
||||||
animation-duration: 277s;
|
|
||||||
animation-delay: -215s;
|
|
||||||
transform-origin: 0vw -14vh;
|
|
||||||
box-shadow: 100vmin 0 12.768802405913084vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(24) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 35%;
|
|
||||||
left: 40%;
|
|
||||||
animation-duration: 375s;
|
|
||||||
animation-delay: -205s;
|
|
||||||
transform-origin: -16vw -21vh;
|
|
||||||
box-shadow: -100vmin 0 12.845025825087218vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(25) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 82%;
|
|
||||||
left: 77%;
|
|
||||||
animation-duration: 211s;
|
|
||||||
animation-delay: -150s;
|
|
||||||
transform-origin: 15vw -14vh;
|
|
||||||
box-shadow: 100vmin 0 12.825514275205975vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(26) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 99%;
|
|
||||||
left: 95%;
|
|
||||||
animation-duration: 342s;
|
|
||||||
animation-delay: -118s;
|
|
||||||
transform-origin: -16vw -10vh;
|
|
||||||
box-shadow: 100vmin 0 13.09952938429165vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(27) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 24%;
|
|
||||||
left: 43%;
|
|
||||||
animation-duration: 191s;
|
|
||||||
animation-delay: -249s;
|
|
||||||
transform-origin: 10vw 25vh;
|
|
||||||
box-shadow: -100vmin 0 12.948079018854608vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(28) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 35%;
|
|
||||||
left: 83%;
|
|
||||||
animation-duration: 38s;
|
|
||||||
animation-delay: -5s;
|
|
||||||
transform-origin: -15vw 21vh;
|
|
||||||
box-shadow: -100vmin 0 12.884993041770123vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(29) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 50%;
|
|
||||||
left: 19%;
|
|
||||||
animation-duration: 83s;
|
|
||||||
animation-delay: -367s;
|
|
||||||
transform-origin: -17vw 24vh;
|
|
||||||
box-shadow: 100vmin 0 12.943907699048086vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(30) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 94%;
|
|
||||||
left: 43%;
|
|
||||||
animation-duration: 139s;
|
|
||||||
animation-delay: -151s;
|
|
||||||
transform-origin: 15vw 13vh;
|
|
||||||
box-shadow: -100vmin 0 13.188888046545854vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(31) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 1%;
|
|
||||||
left: 30%;
|
|
||||||
animation-duration: 276s;
|
|
||||||
animation-delay: -118s;
|
|
||||||
transform-origin: 20vw -13vh;
|
|
||||||
box-shadow: 100vmin 0 12.67400347628477vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(32) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 62%;
|
|
||||||
left: 91%;
|
|
||||||
animation-duration: 179s;
|
|
||||||
animation-delay: -95s;
|
|
||||||
transform-origin: 21vw 0vh;
|
|
||||||
box-shadow: 100vmin 0 13.078495151792405vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(33) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 62%;
|
|
||||||
left: 24%;
|
|
||||||
animation-duration: 176s;
|
|
||||||
animation-delay: -417s;
|
|
||||||
transform-origin: 11vw 1vh;
|
|
||||||
box-shadow: -100vmin 0 13.180095388225237vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(34) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 44%;
|
|
||||||
left: 7%;
|
|
||||||
animation-duration: 15s;
|
|
||||||
animation-delay: -23s;
|
|
||||||
transform-origin: -5vw 24vh;
|
|
||||||
box-shadow: -100vmin 0 12.99638150831451vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(35) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 41%;
|
|
||||||
left: 18%;
|
|
||||||
animation-duration: 52s;
|
|
||||||
animation-delay: -66s;
|
|
||||||
transform-origin: 10vw 18vh;
|
|
||||||
box-shadow: 100vmin 0 12.881618623995024vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(36) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 32%;
|
|
||||||
left: 75%;
|
|
||||||
animation-duration: 377s;
|
|
||||||
animation-delay: -252s;
|
|
||||||
transform-origin: 19vw -21vh;
|
|
||||||
box-shadow: -100vmin 0 13.235523992130878vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(37) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 26%;
|
|
||||||
left: 96%;
|
|
||||||
animation-duration: 352s;
|
|
||||||
animation-delay: -359s;
|
|
||||||
transform-origin: -22vw -3vh;
|
|
||||||
box-shadow: 100vmin 0 13.203141789834469vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(38) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 63%;
|
|
||||||
left: 64%;
|
|
||||||
animation-duration: 9s;
|
|
||||||
animation-delay: -70s;
|
|
||||||
transform-origin: 16vw 10vh;
|
|
||||||
box-shadow: 100vmin 0 12.63072769188053vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(39) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 69%;
|
|
||||||
left: 81%;
|
|
||||||
animation-duration: 56s;
|
|
||||||
animation-delay: -361s;
|
|
||||||
transform-origin: -17vw 11vh;
|
|
||||||
box-shadow: 100vmin 0 13.123746520776026vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(40) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 54%;
|
|
||||||
left: 33%;
|
|
||||||
animation-duration: 142s;
|
|
||||||
animation-delay: -2s;
|
|
||||||
transform-origin: 4vw 24vh;
|
|
||||||
box-shadow: 100vmin 0 13.216474278399133vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(41) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 56%;
|
|
||||||
left: 10%;
|
|
||||||
animation-duration: 65s;
|
|
||||||
animation-delay: -105s;
|
|
||||||
transform-origin: 3vw -24vh;
|
|
||||||
box-shadow: 100vmin 0 13.322200697362886vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(42) {
|
|
||||||
color: #4b6477;
|
|
||||||
top: 2%;
|
|
||||||
left: 39%;
|
|
||||||
animation-duration: 213s;
|
|
||||||
animation-delay: -62s;
|
|
||||||
transform-origin: 4vw 10vh;
|
|
||||||
box-shadow: -100vmin 0 12.580279613916804vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(43) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 100%;
|
|
||||||
left: 49%;
|
|
||||||
animation-duration: 12s;
|
|
||||||
animation-delay: -71s;
|
|
||||||
transform-origin: 9vw -21vh;
|
|
||||||
box-shadow: 100vmin 0 13.356960223158397vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(44) {
|
|
||||||
color: #a9a0bb;
|
|
||||||
top: 75%;
|
|
||||||
left: 59%;
|
|
||||||
animation-duration: 286s;
|
|
||||||
animation-delay: -81s;
|
|
||||||
transform-origin: -6vw 18vh;
|
|
||||||
box-shadow: 100vmin 0 12.718648874626872vmin currentColor;
|
|
||||||
}
|
|
||||||
.background span:nth-child(45) {
|
|
||||||
color: #888aa0;
|
|
||||||
top: 22%;
|
|
||||||
left: 12%;
|
|
||||||
animation-duration: 87s;
|
|
||||||
animation-delay: -298s;
|
|
||||||
transform-origin: -17vw 5vh;
|
|
||||||
box-shadow: 100vmin 0 13.066314361220197vmin currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
@ -1073,53 +627,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
</div>
|
</div>
|
||||||
<div class="background">
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user