Merge pull request #973 from steveseguin/steveseguin-patch-1

v22 updates
This commit is contained in:
Steve Seguin 2022-07-26 08:19:53 -04:00 committed by GitHub
commit fc2d77671d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 3012 additions and 1031 deletions

View File

@ -563,7 +563,7 @@ function modURL(){
} else if (url.startsWith("https://")){
// pass
} else if (url.startsWith("file:")){
// pass
alert("Warning:\n\nFor security purposes, local files need to be loaded via the command-line or via the right-click context menu -> Edit URL.\n\nThis is supported in Electron Capture 2.15.2 and newer.");
} else {
url = "https://"+url;
}

View File

@ -33,6 +33,10 @@
padding:5px;
margin:5px;
}
video{
max-width:300px;
max-height:100px;
}
</style>
<script>
@ -41,7 +45,7 @@
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
var iframesrc = document.getElementById("viewlink").value;
iframe.allow = "midi;geolocation;autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
iframe.allow = "document-domain;encrypted-media;sync-xhr;usb;web-share;cross-origin-isolated;accelerometer;midi;geolocation;autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
if (iframesrc==""){
iframesrc="./";
@ -304,12 +308,53 @@
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
/// If you have a routing system setup, you could have just one global listener for all iframes instead.
var media = {};
media.tracks = {};
media.streams = {};
window.addEventListener('messageerror', e => {
console.error(e);
});
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if (e.data.frame){ // add `&sendframes` to the view link to trigger this event; it lets you capture video/audio from the parent window
if (!media.tracks[e.data.trackID]){
media.tracks[e.data.trackID] = {};
media.tracks[e.data.trackID].generator = new MediaStreamTrackGenerator({kind:e.data.kind});
media.tracks[e.data.trackID].stream = new MediaStream([media.tracks[e.data.trackID].generator]);
media.tracks[e.data.trackID].frameWriter = media.tracks[e.data.trackID].generator.writable.getWriter();
media.tracks[e.data.trackID].frameWriter.write(e.data.frame);
if (!media.streams[e.data.streamID]){
media.streams[e.data.streamID] = document.createElement("video");
media.streams[e.data.streamID].id = "video_"+e.data.streamID;
media.streams[e.data.streamID].autoplay = true;
// media.streams[e.data.streamID].controls = true;
media.streams[e.data.streamID].srcObject = media.tracks[e.data.trackID].stream;
iframeContainer.appendChild(media.streams[e.data.streamID]);
} else {
if (e.data.kind=="video"){
media.streams[e.data.streamID].srcObject.getVideoTracks().forEach(trk=>{
media.streams[e.data.streamID].srcObject.removeTrack(trk);
});
} else if (e.data.kind=="audio"){
media.streams[e.data.streamID].srcObject.getAudioTracks().forEach(trk=>{
media.streams[e.data.streamID].srcObject.removeTrack(trk);
});
}
media.tracks[e.data.trackID].stream.getTracks().forEach(trk=>{
media.streams[e.data.streamID].srcObject.addTrack(trk);
});
}
} else {
media.tracks[e.data.trackID].frameWriter.write(e.data.frame);
}
return;
} // end of video/audio capture
if ("stats" in e.data){
var outputWindow = document.createElement("div");
console.log(e.data.stats);

View File

@ -57,7 +57,7 @@
<meta property="twitter:image" content="./media/vdoNinja_logo_full.png" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
<link rel="stylesheet" href="./main.css?ver=165" />
<link rel="stylesheet" href="./main.css?ver=169" />
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/adapter.min.js"></script>
<style id="lightbox-animations" type="text/css"></style>
<!-- <link rel="manifest" href="manifest.json" /> -->
@ -82,7 +82,7 @@
<link itemprop="url" href="./media/vdoNinja_logo_full.png" />
</span>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=37"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=473"></script>
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=486"></script>
<input id="zoomSlider" type="range" style="display: none;" />
<span id="electronDragZone" style="pointer-events: none; z-index:-10; position:absolute;top:0;left:0;width:100%;height:2%;-webkit-app-region: drag;min-height:20px;"></span>
<div id="header">
@ -182,6 +182,10 @@
<i id="settingstoggle" class="toggleSize las la-sync-alt my-float"></i>
</div>
<div id="obscontrolbutton" onmousedown="event.preventDefault(); event.stopPropagation();" title="OBS Remote Controller; start/stop and change scenes." onclick="toggleOBSControls();" class="hidden float" tabindex="22" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" style="cursor: pointer;" alt="Toggle the Remote OBS Controls Menu">
<i id="obscontroltoggle" class="toggleSize las la-gamepad my-float"></i>
</div>
<div id="roomsettingsbutton" onmousedown="event.preventDefault(); event.stopPropagation();" title="Room Settings" onclick="toggleRoomSettings();" class="hidden float" tabindex="22" role="button" aria-pressed="false" onkeyup="enterPressedClick(event,this);" style="cursor: pointer;" alt="Toggle the Room Settings Menu">
<i id="roomsettingstoggle" class="toggleSize las la-users-cog my-float"></i>
</div>
@ -1050,6 +1054,16 @@
</label>
<span data-translate="disable-animated-mixing">Disable animations</span>
<Br />
<label class="switch" title="This mode encodes the video and audio into chunks, which are shared with multiple viewers. Limited browser support. Can potentially reduce CPU and improve video quality, but will rely on a buffer.">
<input type="checkbox" data-param="&chunked=500" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
<font class="tooltip" style='cursor: help;position:relative;bottom:2px;font-family:"Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", Times, Symbola, Aegyptus, Code2000, Code2001, Code2002, Musica, serif, LastResort;'><span class="tooltiptext">Pretty experimental and limited browser support, though relatively low CPU usage.</span></font>
<span data-translate="chunked-mode">P2P Chunked-mode</span>
</div>
<div style="display:inline-block;margin-top: 12px; position: relative; margin-right:10px;">
<label class="switch" title="Increase video quality that guests in room see.">
@ -1093,16 +1107,15 @@
<span class="slider"></span>
</label>
<span data-translate="prefix-screenshare">Prefix screenshare IDs</span>
</div>
<div style="display:inline-block;margin-top: 12px; position: relative; ">
<label class="switch" title="This mode encodes the video and audio into chunks, which are shared with multiple viewers. Limited browser support. Can potentially reduce CPU and improve video quality, but will rely on a buffer.">
<input type="checkbox" data-param="&chunked=500" onchange="updateLink(1,this);">
<Br />
<label class="switch" title="Allow the guest to select an avatar image for when they hide their camera">
<input type="checkbox" data-param="&avatar" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
<font class="tooltip" style='cursor: help;position:relative;bottom:2px;font-family:"Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", Times, Symbola, Aegyptus, Code2000, Code2001, Code2002, Musica, serif, LastResort;'><span class="tooltiptext">Pretty experimental and limited browser support, though relatively low CPU usage.</span></font>
<span data-translate="chunked-mode">P2P Chunked-mode</span>
<span data-translate="avatar-selection">Can select an Avatar image</span>
</div>
<div style="display:inline-block;margin-top: 12px; position: relative; ">
<Br />
<label class="switch" title="Use Meshcast servers to restream video data from this guest to its viewers, reducing the CPU and upload load in some cases. Will increase latency a bit.">
<input type="checkbox" data-param="&meshcast" onchange="updateLink(1,this);">
<span class="slider"></span>
@ -1117,6 +1130,13 @@
</label>
<span data-translate="mini-self-preview">Mini self-preview</span>
<Br />
<label class="switch" title="Show an ovelaid grid on the guest's preview video to help with self-centering of the guest.">
<input type="checkbox" data-param="&grid" onchange="updateLink(1,this);">
<span class="slider"></span>
</label>
<span data-translate="rule-of-thirds">Show rule-of-thirds grid</span>
<Br />
<label class="switch" title="The guest can only see the Director's video, if provided">
<input type="checkbox" data-param="&broadcast" id="broadcastSlider" onchange="updateLink(1,this);">
@ -1208,7 +1228,6 @@
</label>
<span data-translate="animate-mixing">Animate mixing</span>
</div>
<div style="display:inline-block;margin-top: 12px; position: relative; margin-right:10px;">
@ -1230,6 +1249,14 @@
<span class="slider"></span>
</label>
<font class="tooltip" style='cursor: help;position:relative;bottom:2px;font-family:"Noto Color Emoji", "Apple Color Emoji", "Segoe UI Emoji", Times, Symbola, Aegyptus, Code2000, Code2001, Code2002, Musica, serif, LastResort;'><span class="tooltiptext">This can cause video playback to lag</span></font> Unlock Video Bitrate
<br />
<label class="switch" title="Disable fit-to-window optmized video scaling for added sharpness; increases CPU / Network load though.">
<input type="checkbox" data-param="&scale=100" onchange="updateLink(3,this);">
<span class="slider"></span>
</label>
<span data-translate="disable-downscaling">Viewer-side downscaling</span>
</div>
<div style="display:inline-block;margin-top: 12px; position: relative; ">
@ -1642,12 +1669,14 @@
<div id="audioTitle2" class="title">
<i class="las la-microphone-alt"></i><span data-translate="select-audio-source"> Audio Source(s) </span>
<i id="chevarrow2" class="chevron bottom" aria-hidden="true"></i>
<div class="meter" id="meter3"></div><div class="meter2" id="meter4"></div>
</div>
</a>
<ul id="audioSource3" class="multiselect-contents">
<li>
</li>
</ul>
</div>
<br />
<span id="headphonesDiv3" style="display: block;">
@ -1872,12 +1901,12 @@
</div>
<div id="roomSettings" style="display:none; user-select: none;">
<div id="roomSettings" class="customModelPopup" style="display:none; user-select: none;">
<div class="promptModalInner">
<span class='modalClose' onclick="toggleRoomSettings();">×</span>
<span></span>
<h3 data-translate="change-room-settings">Change room settings</h3><br />
<label title="Increase this at your peril. Changes the total inbound video bitrate per guest; mobile devices excluded. Webp-mode also excluded." for="trbSettingInput" data-translate="change-room-video-quality">Change room video quality:</label>
<label title="Increase this at your peril. Changes the total inbound video bitrate per guest; mobile devices excluded." for="trbSettingInput" data-translate="change-room-video-quality">Change room video quality:</label>
<span style="margin-left: 6px;" id="trbSettingInputFeedback"></span>-kbps
<input id="trbSettingInput" type="range" min="0" max="4000" value="500" onchange="changeTRB(this);" oninput="getById('trbSettingInputFeedback').innerHTML = this.value;" style="width:100%;display:block;" />
<span style="margin: 20px 0 0 0;display:block" id='highlightDirectorSpan' title="Only the director's video will be visible to guests and within group scenes">
@ -1907,6 +1936,30 @@
</div>
</div>
<div id="remoteOBSControl" class="customModelPopup" style="display:none; user-select: none;">
<div class="promptModalInner">
<span class='modalClose' onclick="toggleOBSControls();">×</span>
<span></span>
<h3 data-translate="remote-control-obs-menu">Remote Controller for OBS Studio</h3><br />
<div id="obsControlHelp" class="hidden" style="margin: 10px 0;display:block" >
No remote controllable instances of OBS Studio were found
</div>
<div id="obsControlButtons" style="margin: 10px 0;display:block" >
</div>
<div id="obsSceneNames" style="margin: 10px 0;display:block" >
</div>
<div id="obsRemotePassword" class="hidden" style="margin: 10px 0;display:block;" >
<font style="font-size:117%"><i class="las la-key" style="margin: 10px;"></i>Remote OBS passcode:</font>
<input style="margin:0 10px;display:inline-block;padding: 8px 10px 6px 10px;" placeholder="Enter the remote OBS password here" />
</div>
<small style="margin: 20px 0 0 0;display:block;" >
See the <a href="https://docs.vdo.ninja/advanced-settings/upcoming-parameters/and-obs" style="color:#314350; cursor:pointer;" target="_blank">documentation</a> for help on using the remote OBS controller
</small>
<div id="debugRemoteOBSControl" class="hidden">
</div>
</div>
</div>
<div id="transferSettingsTemplate" style="display:none">
<h3>Change guest settings</h3><br />
<label class="switch" title="Cannot see videos">
@ -2142,7 +2195,7 @@
// session.offsetChannel // int
// session.audioChannels // int
// session.security // true to disable the wss connection after the first peer connection is made
// session.framerate // int ; publishing frame rate. will fail if camera does not support it.
// session.frameRate // int ; publishing frame rate. will fail if camera does not support it.
// session.sync // see the docs
// session.buffer // int in milliseconds ; see the docs
// session.roomid // "yyyy" -- the room name to use. alphanumeric.
@ -2155,11 +2208,11 @@
// session.defaultBackgroundImages = ["./media/bg_sample1.webp", "./media/bg_sample2.webp"]; // for &effects=5 (virtual backgrounds)
</script>
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/aes.js"></script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=362"></script>
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=377"></script>
<!--
// If you wish to change branding, blank offers a good clean start.
<script type="text/javascript" id="main-js" src="./main.js" data-translation="blank"></script>
-->
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=376"></script>
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=389"></script>
</body>
</html>

2535
lib.js

File diff suppressed because it is too large Load Diff

View File

@ -1054,9 +1054,6 @@ button.btnArmTransferRoom.selected{
#logoname{
display:none;
}
#head1{
display:none;
}
#head4{
display:none;
}
@ -2524,6 +2521,12 @@ button.toggleSettings{
vertical-align: middle;
font-size: 100%;
}
#minipreview > #videosource {
height:auto!important;
width:auto!important;;
}
#videoSourceSelect {
display: inline-block;
vertical-align: middle;
@ -3529,8 +3532,11 @@ input:checked + .slider:before {
transform: translateX(16px);
}
#promptModal, #roomSettings, .promptModal {
#remoteOBSControl button {
margin:5px;
padding:10px;
}
#promptModal, .customModelPopup, .promptModal {
position: absolute;
background-color: rgb(221 221 221);
box-shadow: 0 0 30px 10px #0000005c;
@ -3875,7 +3881,9 @@ input:checked + .slider:before {
.la-cog:before {
content: "\f013"; }
.la-phone:before {
content: "\f095"; }
content: "\f095"; }
.la-gamepad:before {
content: "\f11b"; }
.la-skull-crossbones:before {
content: "\f714"; }
.la-hand-paper:before {

234
main.js
View File

@ -1454,10 +1454,9 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
if (urlParams.has('outboundvideobitrate') || urlParams.has('ovb')) {
session.outboundVideoBitrate = parseInt(urlParams.get('outboundvideobitrate')) || parseInt(urlParams.get('ovb')) || false;
}
if (urlParams.has('webp')){
session.webp = true;
if (urlParams.has('webp') || urlParams.has('images')){ // deprecicating this. chunked mode will replace it.
session.webp = urlParams.get('webp') || urlParams.get('images') || "webp";
}
if (urlParams.has('webpquality') || urlParams.has('webpq') || urlParams.has('wq')){
@ -1553,6 +1552,14 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
if (urlParams.has('ruler') || urlParams.has('grid') || urlParams.has('thirds')) {
session.ruleOfThirds=true;
session.fullscreen = true;
if (!session.manual){
session.manual = false;
}
}
if (urlParams.has('smallshare')){
session.notifyScreenShare = false;
}
if (urlParams.has('proxy')) { // routes the wss traffic via an alternative network path. Not
@ -1610,6 +1617,18 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.remote = urlParams.get('remote') || urlParams.get('rem') || true;
}
if (urlParams.has("slideshow")){ // stream labs mobile fix ?
var ssinterval = parseInt(urlParams.get("slideshow")) || 25;
ssinterval = 1000/ssinterval;
session.manual = true;
session.dynamicScale = false;
setInterval(function(){
try {
slideshowHack();
} catch(e){errorlog(e);}
},ssinterval);
}
if (urlParams.has('latency') || urlParams.has('al') || urlParams.has('audiolatency')) {
log("latency ENABLED");
session.audioLatency = urlParams.get('latency') || urlParams.get('al') || urlParams.get('audiolatency');
@ -1659,15 +1678,38 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
//innerHTML =
}
if (urlParams.has('keyframeinterval') || urlParams.has('keyframerate') || urlParams.has('keyframe') || urlParams.has('fki')) {
log("keyframerate ENABLED");
session.keyframerate = parseInt(urlParams.get('keyframeinterval') || urlParams.get('keyframerate') || urlParams.get('keyframe') || urlParams.get('fki')) || 0;
if (urlParams.has('keyframeinterval') || urlParams.has('keyframeRate') || urlParams.has('keyframe') || urlParams.has('fki')) {
log("keyframeRate ENABLED");
session.keyframeRate = parseInt(urlParams.get('keyframeinterval') || urlParams.get('keyframeRate') || urlParams.get('keyframe') || urlParams.get('fki')) || 0;
}
if (urlParams.has('obsoff') || urlParams.has('oo') || urlParams.has('disableobs')) {
log("OBS feedback disabled");
session.disableOBS = true;
getById("obsState").style.setProperty("display", "none", "important");
}
if (urlParams.has('obscontrols') || urlParams.has('remoteobs') || urlParams.has('obsremote') || urlParams.has('obs') || urlParams.has('controlobs')) {
session.obsControls = urlParams.get('obscontrols') || urlParams.get('remoteobs') || urlParams.get('obsremote') || urlParams.get('obs') || urlParams.get('controlobs');
if (session.obsControls) { // whether to show the button or not; that's it.
session.obsControls = session.obsControls.toLowerCase();
}
if (session.obsControls == "false") {
session.obsControls = false;
} else if (session.obsControls == "0") {
session.obsControls = false;
} else if (session.obsControls == "no") {
session.obsControls = false;
} else if (session.obsControls == "off") {
session.obsControls = false;
} else if (session.obsControls){
session.obsControls = session.obsControls.toLowerCase();
} else {
session.obsControls = true;
}
}
if (session.obsControls){
getById("obscontrolbutton").classList.remove("hidden");
}
if (urlParams.has('tallyoff') || urlParams.has('notally') || urlParams.has('disabletally') || urlParams.has('to')) {
@ -1704,24 +1746,6 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
log("macOS: " + macOS);
log(window.obsstudio);
if (typeof document.visibilityState !== "undefined"){
session.obsState.visibility = document.visibilityState==="visible";
//session.obsState.sourceActive = session.obsState.visibility;
}
window.obsstudio.getStatus(function(obsStatus) {
log("OBS STATUS:");
log(obsStatus);
// TODO: update state here
if ("recording" in obsStatus){
session.obsState.recording = obsStatus.recording;
}
if ("streaming" in obsStatus){
session.obsState.streaming = obsStatus.streaming;
}
});
if (!(urlParams.has('streamlabs'))) {
var ver1 = window.obsstudio.pluginVersion.split(".");
@ -1746,6 +1770,12 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
//}
if (session.disableOBS===false){
if (typeof document.visibilityState !== "undefined"){
session.obsState.visibility = document.visibilityState==="visible";
}
getOBSDetails();
window.addEventListener("obsSourceVisibleChanged", obsSourceVisibleChanged);
window.addEventListener("obsSourceActiveChanged", obsSourceActiveChanged);
window.addEventListener("obsSceneChanged", obsSceneChanged);
@ -1753,6 +1783,8 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
window.addEventListener("obsStreamingStopped", obsStreamingStopped);
window.addEventListener("obsRecordingStarted", obsRecordingStarted);
window.addEventListener("obsRecordingStopped", obsRecordingStopped);
window.addEventListener("obsVirtualcamStarted", obsVirtualcamStarted);
window.addEventListener("obsVirtualcamStopped", obsVirtualcamStopped);
}
} catch (e) {
@ -2096,6 +2128,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
if (urlParams.has('debug')){
session.debug=true;
debugStart();
}
@ -2154,10 +2187,6 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.codec = urlParams.get('codec') || false;
if (session.codec){
session.codec = session.codec.toLowerCase();
if (session.codec=="webp"){
session.webp = true;
session.codec = false;
}
}
} else if (OperaGx){
session.codec = "vp8";
@ -2213,12 +2242,15 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.scale = parseFloat(urlParams.get('scale')) || 100;
}
session.dynamicScale = false; // default true
} else if (urlParams.has('viewwidth') || urlParams.has('vw')) {
session.viewwidth = urlParams.get('viewwidth') || urlParams.get('vw') ||false;
session.dynamicScale = false; // default true
} else if (urlParams.has('viewheight') || urlParams.has('vh')) {
session.viewheight = urlParams.get('viewheight') || urlParams.get('vh') ||false;
session.dynamicScale = false; // default true
} else {
if (urlParams.has('viewwidth') || urlParams.has('vw')) {
session.viewwidth = urlParams.get('viewwidth') || urlParams.get('vw') ||false;
session.dynamicScale = false; // default true
}
if (urlParams.has('viewheight') || urlParams.has('vh')) {
session.viewheight = urlParams.get('viewheight') || urlParams.get('vh') ||false;
session.dynamicScale = false; // default true
}
}
if (urlParams.has('mcscale') || urlParams.has('meshcastscale')) {
@ -2315,14 +2347,18 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
log("totalRoomBitrate ENABLED");
log(session.totalRoomBitrate);
}
if (session.totalRoomBitrate===false){
session.totalRoomBitrate = session.totalRoomBitrate_default;
} else {
session.totalRoomBitrate_default = session.totalRoomBitrate; // trb_default doesn't change dynamically, but trb can (per director I guess)
}
if (session.totalRoomBitrate_default>4000){
getById("trbSettingInput").max = Math.ceil(session.totalRoomBitrate_default);
}
if (urlParams.has('maxtotalscenebitrate') || urlParams.has('totalscenebitrate') || urlParams.has('mtsb') || urlParams.has('tsb') || urlParams.has('totalbitrate') || urlParams.has('tb')) {
session.totalSceneBitrate = urlParams.get('maxtotalscenebitrate') || urlParams.get('totalscenebitrate') || urlParams.get('mtsb') || urlParams.get('tsb') || urlParams.get('totalbitrate') || urlParams.get('tb') || false;
if (session.totalSceneBitrate){
@ -2491,6 +2527,23 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
}
if (urlParams.has('postinterval')){ // interval to post snapimage images
session.postInterval = urlParams.get('postinterval') || session.postInterval;
session.postInterval = parseInt(session.postInterval) || 60;
if (session.postInterval<5){
session.postInterval = 5;
}
}
if (urlParams.has('postimage')){
var postURL = decodeURIComponent(urlParams.get('postimage')) || session.postURL; // default will post to https://temp.vdo.ninja/images/STREAMIDHERE.jpg
setInterval(function(postURL){
try {
uploadImageSnapshot(postURL);
} catch(e){}
}, session.postInterval*1000 , postURL);
}
if (urlParams.has('cleanish')) {
session.cleanish = true;
}
@ -2513,6 +2566,11 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
//}
}
if (urlParams.has('degrade')) {
session.degrade = urlParams.get('degrade') || true; // Firefox, and maybe Safari, supported I think.
// the possible values are maintain-framerate, maintain-resolution, or balanced. The default value is balanced
}
if (urlParams.has('maxviewers') || urlParams.has('mv')) {
session.maxviewers = urlParams.get('maxviewers') || urlParams.get('mv');
@ -2559,11 +2617,11 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
session.randomize = true;
}
if (urlParams.has('framerate') || urlParams.has('fr') || urlParams.has('fps')) {
session.framerate = urlParams.get('framerate') || urlParams.get('fr') || urlParams.get('fps');
session.framerate = parseInt(session.framerate);
log("framerate Changed");
log(session.framerate);
if (urlParams.has('frameRate') || urlParams.has('fr') || urlParams.has('fps')) {
session.frameRate = urlParams.get('frameRate') || urlParams.get('fr') || urlParams.get('fps');
session.frameRate = parseInt(session.frameRate);
log("frameRate Changed");
log(session.frameRate);
}
if (urlParams.has('tz')){
@ -2575,11 +2633,11 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
}
if (urlParams.has('maxframerate') || urlParams.has('mfr') || urlParams.has('mfps')) {
session.maxframerate = urlParams.get('maxframerate') || urlParams.get('mfr') || urlParams.get('mfps');
session.maxframerate = parseInt(session.maxframerate);
log("max framerate assigned");
log(session.maxframerate);
if (urlParams.has('maxframeRate') || urlParams.has('mfr') || urlParams.has('mfps')) {
session.maxframeRate = urlParams.get('maxframeRate') || urlParams.get('mfr') || urlParams.get('mfps');
session.maxframeRate = parseInt(session.maxframeRate);
log("max frameRate assigned");
log(session.maxframeRate);
}
if (urlParams.has('buffer')) { // needs to be before sync
@ -2708,12 +2766,29 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
document.querySelector(':root').style.setProperty('--fadein-speed', 0.5);
setInterval(function(){activeSpeaker(false);},100);
} else if (urlParams.has('noisegate')){
session.quietOthers = urlParams.get('noisegate') || 1;
} else if (urlParams.has('noisegate') || urlParams.has('gating') || urlParams.has('gate') ||urlParams.has('ng')){
session.quietOthers = urlParams.get('noisegate') || urlParams.get('gating') || urlParams.get('gate') || urlParams.get('ng') || 1;
session.quietOthers = parseInt(session.quietOthers);
session.audioEffects = true;
session.audioMeterGuest = true;
setInterval(function(){activeSpeaker(false);},100);
if (session.quietOthers == 1){
session.quietOthers = false;
session.noisegate = true;
session.audioEffects = true;
session.audioMeterGuest = true;
} else if (session.quietOthers == 4){
session.quietOthers = 1;
session.audioEffects = true;
session.audioMeterGuest = true;
setInterval(function(){activeSpeaker(false);},100);
} else if (!session.quietOthers){
session.noisegate = false;
session.quietOthers = false;
} else {
session.audioEffects = true;
session.audioMeterGuest = true;
setInterval(function(){activeSpeaker(false);},100);
}
}
if (urlParams.has('fadein')) {
@ -2809,6 +2884,9 @@ 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.audioEffects = false; // disable audio inbound effects also.
session.audioMeterGuest = false;
if (session.noisegate===null){
session.noisegate = false;
}
}
// For info, see this: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidatePairStats/availableOutgoingBitrate
@ -2822,6 +2900,26 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
}
if (urlParams.has('iframetarget')) {
session.iframetarget = urlParams.get('iframetarget'); // speciifies the IFRAME Hostname target
if (session.iframetarget){
session.iframetarget = decodeURIComponent(session.iframetarget);
} else {
session.iframetarget = window.location.hostname;
}
}
if (urlParams.has('sendframes')) {
session.sendframes = urlParams.get('sendframes');
if(session.sendframes){
session.sendframes = decodeURIComponent(session.sendframes);
} else {
session.sendframes = session.iframetarget || "*";
}
}
if (urlParams.has('tcp')){ // forces the TURN servers to use TCP mode; still need to add &private to force TURN also tho
session.forceTcpMode = true;
}
@ -3856,7 +3954,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
enumerateDevices().then(function(deviceInfos) {
parent.postMessage({
"deviceList": JSON.parse(JSON.stringify(deviceInfos))
}, "*");
}, session.iframetarget);
});
}
@ -3972,7 +4070,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
parent.postMessage({
"stats": stats
}, "*");
}, session.iframetarget);
}, 1000);
}
@ -4004,7 +4102,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
parent.postMessage({
"loudness": loudness
}, "*");
}, session.iframetarget);
} else {
session.pushLoudness = false;
@ -4019,7 +4117,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
//parent.postMessage({
// "effectsData": effectsData,
// "effectsID": session.pushEffectsData
//}, "*");
//}, session.iframetarget);
} else {
session.pushEffectsData = false;
@ -4034,20 +4132,24 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
}
parent.postMessage({
"streamIDs": streamIDs
}, "*");
}, session.iframetarget);
}
}
if ("close" in e.data) { // disconnect and hangup all inbound streams.
for (var i in session.rpcs) {
try {
session.rpcs[i].close();
} catch (e) {
errorlog(e);
}
if (("close" in e.data) || ("hangup" in e.data)) { // disconnect and hangup all inbound streams.
var tmp = e.data.close || e.data.hangup;
if (tmp == "estop"){ // try to stop the video recording even if not complete; if you can't wait even ms before a reload/exit.
console.log("ESTOP");
session.hangup(false,true);
} else if (tmp == "reload"){ // stop and reload the page safely.
session.hangup(true);
} else { // just hangup, but can take up to 1-second to do so fully.
session.hangup();
}
hangup();
}
if ("hangup" in e.data) { // disconnect and hangup all inbound streams.
session.hangup();
}
if ("style" in e.data) { // insert a custom style sheet
@ -4065,7 +4167,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
var detailedState = getDetailedState();
parent.postMessage({
"detailedState": detailedState
}, "*");
}, session.iframetarget);
}
if ("automixer" in e.data) { // stop the auto mixer if you want to control the layout and bitrate yourself
@ -4163,7 +4265,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
var resp = processMessage(e.data); // reuse the companion API
if (resp!==null){
log(resp);
parent.postMessage(resp, "*");
parent.postMessage(resp, session.iframetarget);
}
} else if ("target" in e.data) {
log(e.data);
@ -4369,7 +4471,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
return;
}
warnlog("Connection type changed from " + session.stats.network_type + " to " + Connection.effectiveType);
log("Connection type changed from " + session.stats.network_type + " to " + Connection.effectiveType);
session.stats.network_type = Connection.effectiveType + " / " + Connection.type;
session.ping();

View File

@ -33,9 +33,12 @@ span{
iframe {
border: 0;
padding: 0;
display: block;
height: 100%;
width: 100%;
display: none;
height: 0%;
width: 0%;
position: absolute;
left: -100px;
top: -100px;
}
.popup-message {

View File

@ -1,12 +1,12 @@
<html>
<head>
<title>Mixer app</title>
<title>Versus.cam</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<script src="./thirdparty/jquery/jquery-3.6.0.js?asdf"></script>
<script src="./thirdparty/jquery/jquery-ui.js"></script>
<link rel="stylesheet" href="./thirdparty/jquery/jquery-ui.css">
<link rel="stylesheet" href="./stats.css" >
<link rel="stylesheet" href="./versus.css" >
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<link rel="icon" type="image/png" sizes="32x32" href="./media/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="./media/favicon-16x16.png" />
@ -24,10 +24,12 @@
<div><div id="streamsConnected">0</div> connections</div>
</span>
<span>
<span><button class="menuButtons" onclick="copyFunction(this.value, event);" id="inviteLink" value="test">Copy Invite Link</button></span>
<span><button class="menuButtons" onclick="copyFunction(this.value, event);" id="inviteLink" value="test"><img src="data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z'/%3E%3C/svg%3E" class="icon"> Copy Invite Link</button></span>
<span><button class="menuButtons" onclick="getStreamModal();">Add a Stream Manually</button></span>
</span>
</header>
<div id="mainContainer">
</div>
<div id="iframeContainer">
<div id='canvas' class="hidden">
</div>
@ -119,20 +121,6 @@
</div>
</div>
</div>
<div id="graphTemplate" display="none">
<div class="graph">
<div class='graphTitle'>Bitrate (kbps)</div>
<canvas data-bitrate-graph="true"></canvas>
</div>
<div class="graph">
<div class='graphTitle'>Reported lost packets (per second)</div>
<span>0</span>
<canvas data-nackrate-graph="true"></canvas>
</div>
<div data-log="true" onclick="copyFunction(this.innerText)" style="max-height:400px;display:inline-block;">
<ul></ul>
</div>
</div>
<div id="modal" class="modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
@ -142,6 +130,28 @@
</div>
</div>
<div id="messagePopup" class="popup-message"></div>
<div id="graphTemplate" class="container hidden fadein">
<span class="video" data-action-type="video-container">
</span>
<div class='right-side'>
<span class="streamTitle">
<button data-sololink="false" title="Copy the view link for this video to your clipboard" onclick="copyFunction(this.dataset.sololink, event);"><img src="data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath d='M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z'/%3E%3C/svg%3E" class='icon'> View Link</button>
</span>
<span class="graphSection" data-action-type="stats-graphs-bitrate" data-value="0">
<span class="hidden" data-message="true" data-no-scenes="true"></span>
</span>
<span class="graphSection" data-action-type="stats-graphs-details" data-value="0">
<span class="hidden" data-no-scenes="true"></span>
<span class="stats-container" data-action-type="stats-graphs-details-container">
<span class="hidden stats-sub-container" data-scene-name="true">scene</span>
<span class="hidden stats-sub-container" data-bitrate="true">bitrate (kbps)</span>
<span class="hidden stats-sub-container" data-resolution="true">resolution</span>
<span class="hidden stats-sub-container" style="display:none; word-break: break-all;" data-video-codec="true">video codec</span>
</span>
</span>
</div>
</div>
<script>
function allowDrop(ev) {
@ -321,13 +331,7 @@
}
var urlParams = new URLSearchParams(urlEdited);
var password = false;
if (urlParams.has("password") || urlParams.has("pw") || urlParams.has("p")){
password = urlParams.get("password") || urlParams.get("pw") || urlParams.get("p") || false;
if (password===false){
password = prompt("Please enter a password") || false;
}
}
var viewList = false;
if (urlParams.has("view") || urlParams.has("v")){
@ -337,6 +341,21 @@
viewList = viewList.split(",");
}
var password = false;
if (urlParams.has("password") || urlParams.has("pw") || urlParams.has("p")){
password = urlParams.get("password") || urlParams.get("pw") || urlParams.get("p") || false;
if (password===false){
password = prompt("Please enter a password") || false;
}
}
var additional = "";
if (password){
additional = "&password="+password;
}
var iframe = null;
var aspectRatio = 16/9.0;
document.documentElement.style.setProperty('--aspect-ratio', aspectRatio);
@ -424,6 +443,7 @@
var streamIDs = [];
var slotsNeeded = 1;
var lastLayout = false;
var session = false;
var colors = [
"#00AAAA",
@ -445,6 +465,11 @@
savedSession = {};
}
var pathname = window.location.pathname.split('/');
pathname.pop();
pathname = pathname.join("/");
function exportSession() {
var content = JSON.stringify(savedSession);
var fileName = roomname + ".json";
@ -557,15 +582,8 @@
}
}
var iframe = null;
function loadIframe(){
var additional = "";
if (password){
additional = "&password="+password;
}
roomname = sanitizeRoomName(roomname);
iframe = document.createElement("iframe");
@ -575,11 +593,12 @@
if (!roomname){
roomname = generateString(10);
}
// &viewonly&showall&bitrate=35
var iframesrc = "./index.html?graphs&lightmode&bitrate=300&viewheight=180&viewwidth=320&transparent&cleanoutput&label=Stats_Monitor&scenelinkcodec=h264&manual&nopush&showall&scenelinkbitrate=12000&room="+roomname+additional+"&b64css="+injectCSS;
// note: it's possible to set the width/height dyanmically via the IFRAME API also now. just call on video track load.
var pathname = window.location.pathname.split('/');
pathname.pop();
pathname = pathname.join("/");
getById("inviteLink").value = "https://"+window.location.host+pathname+"/?room="+roomname+additional+"&label&quality&view";
getById("inviteLink").value = "https://"+window.location.host+pathname+"/?room="+roomname+additional+"&label&quality&maxbandwidth&view";
if (roomname!==false){
setStorage("savedRoom", {roomname:roomname,password:password,viewlist:viewList}, 9999);
@ -591,7 +610,13 @@
ele.innerHTML = roomname;
});
iframe.src = "./index.html?graphs&lightmode&ltb=350&transparent&cleanoutput&directorview&label=Stats_Monitor&scenelinkcodec=h264&scenelinkbitrate=12000&director="+roomname+additional+"&b64css="+injectCSS;
iframe.src = iframesrc;
iframe.onload = function(){
session = iframe.contentWindow['session'];
console.log(session);
};
iframeContainer.appendChild(iframe);
//document.getElementById("container").appendChild(iframeContainer);
@ -608,14 +633,24 @@
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
console.log(e.data);
if (!session){
session = iframe.contentWindow['session'];
}
if ("action" in e.data){
if (e.data.action === "view-connection"){
//if (e.data.value){
// iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');
if (e.data.streamID){
updateStreams();
if (streamIDs.includes(e.data.streamID)){return;}
streamIDs.push(e.data.streamID);
if (e.data.value){ // connected
if (e.data.streamID){
updateStreams();
if (streamIDs.includes(e.data.streamID)){return;}
streamIDs.push(e.data.streamID);
}
} else { // disconnected
try {
getById("container_"+e.data.UUID).remove();
} catch(e){console.error(e);}
}
} else if (e.data.action == "requested-stream"){
if (streamIDs.includes(e.data.value)){return;}
@ -629,6 +664,32 @@
}
}
},500);
} else if (e.data.action == "control-box-video-updated"){
var container = getContainer(e.data.UUID, e.data.streamID);
getById("container_"+e.data.UUID).classList.remove("greyout");
var video = container.querySelector("#videoContainer_"+e.data.UUID);
if (!video && iframe){
try {
video = iframe.contentDocument.body.querySelector("#videoContainer_"+e.data.UUID);
container.querySelector('[data-action-type="video-container"]').appendChild(video);
} catch(e){errorlog(e);}
}
} else if (e.data.action == "video-element-created"){
var container = getContainer(e.data.UUID, e.data.streamID);
getById("container_"+e.data.UUID).classList.remove("greyout");
var video = container.querySelector("#videoContainer_"+e.data.UUID);
if (!video && iframe){
try {
video = session.rpcs[e.data.UUID].videoElement;
if (video){
container.querySelector('[data-action-type="video-container"]').appendChild(video);
}
} catch(e){errorlog(e);}
}
}
}
@ -640,38 +701,28 @@
messageList = e.data.messageList;
updateMessages();
}
//
if ("remoteStats" in e.data) {
var UUID = e.data.UUID;
for (var uuid in e.data.remoteStats) {
if (e.data.remoteStats[uuid].video_bitrate_kbps){
var video_bitrate_kbps = e.data.remoteStats[uuid].video_bitrate_kbps;
updateData("bitrate", video_bitrate_kbps, UUID, uuid);
} else if (document.getElementById(uuid)){
updateData("bitrate", 0, UUID, uuid);
}
if (e.data.remoteStats[uuid].nacks_per_second){
var nacks_per_second = e.data.remoteStats[uuid].nacks_per_second;
updateData("nackrate", nacks_per_second, UUID, uuid);
} else if (document.getElementById(uuid)){
updateData("nackrate", 0, UUID, uuid);
}
}
remoteStats(e.data.remoteStats, e.data.UUID, e.data.streamID)
}
//if ("streamIDs" in e.data){
// streamIDs = [];
// for (var key in e.data.streamIDs){
// streamIDs.push(key);
// }
// updateStreams();
// console.log(streamIDs);
//}
});
}
function getContainer(UUID, streamID){
var container = document.getElementById("container_" + UUID);
if (!container){
container = document.getElementById('graphTemplate').cloneNode(true);
container.id = "container_"+UUID;
container.sid = streamID;
container.UUID = UUID;
container.classList.remove("hidden");
document.getElementById("mainContainer").append(container);
container.sololink = "https://"+window.location.host+pathname+"/?scene&room="+roomname+additional+"&bitrate=20000&codec=h264&view="+container.sid+"&label=solo_link"
container.querySelector("[data-sololink]").dataset.sololink = container.sololink;
}
return container;
}
function updateStreams(){
document.getElementById("streamsConnected").innerHTML = streamIDs.length;
if (iframe){
@ -679,39 +730,261 @@
}
}
var bitrate = {
element: "bitrate-graph",
data: 0,
max: 6000,
target: 3000,
};
var frames;
var nackrate = {
element: "nackrate-graph",
data: 0,
max: 15,
target: 15,
};
function updateData(type, data, UUID, uuid) {
if (type == "bitrate") {
bitrate.data = data;
plotData("bitrate", bitrate, UUID, uuid);
}
if (type == "nackrate") {
nackrate.data = data;
plotData("nackrate", nackrate, UUID, uuid);
}
function getColor(value) {
var hue = ((value) * 120).toString(10);
return ["hsl(", hue, ",100%,50%)"].join("");
}
function plotData(type, stat, UUID, uuid){
iframe.contentWindow.document.body.querySelector("#container_"+UUID).querySelectorAll(".graphSection>[data-uid='"+uuid+"']").forEach(ele=>{
iframe.contentWindow.document.body.querySelector("#container_"+UUID).appendChild(ele);
ele.classList.remove("hidden");
function plotData(info, UUID, uuid) { // type = "bitrate" or "nacks"
log("plot data");
var container = getById("container_" + UUID).querySelector('[data-action-type="stats-graphs-bitrate"]');
if (!container){
log("container not found");
return;
}
var canvas = getById("container_" + UUID).querySelector('canvas[data-uid="'+uuid+'"]');
var canvasNew = false
if (!canvas){
canvasNew = true;
canvas = document.createElement("canvas");
canvas.height = 100;
canvas.width = 200;
canvas.className = "canvasStats";
canvas.history_nacks = [];
canvas.history_bitrate = [];
canvas.target = 4000;
if (info.scene){
canvas.title = "Scene: "+info.scene+". Red/orange implies packet loss. Y-axis is 0 to 4000-kbps.";
} else if (info.label){
canvas.title = "Label: "+info.label+". Red/orange implies packet loss. Y-axis is 0 to 4000-kbps.";
} else {
canvas.title = "Red/orange implies packet loss. Y-axis is 0 to 4000-kbps.";
}
canvas.dataset.uid = uuid;
container.appendChild(canvas);
}
selfDestructElement(UUID, uuid);
var context = canvas.getContext("2d");
var bitrate = 0;
if ("video_bitrate_kbps" in info){
bitrate = info.video_bitrate_kbps;
}
if (isNaN(bitrate)) {
bitrate = 0;
}
if (bitrate<0){bitrate = 0;}
var nacks = 0;
if ("nacks_per_second" in info){
nacks = info.nacks_per_second;
}
if (isNaN(nacks)) {
nacks = 0;
}
if (nacks<0){nacks = 0;}
var height = context.canvas.height;
var width = context.canvas.width;
canvas.history_nacks.push(nacks);
canvas.history_bitrate.push(bitrate);
canvas.history_nacks = canvas.history_nacks.slice(-1 * canvas.width);
canvas.history_bitrate = canvas.history_bitrate.slice(-1 * canvas.width);
var maxBitrate = Math.max(...canvas.history_bitrate, (info.available_outgoing_bitrate_kbps || 0));
var target = canvas.target || 4000;
if (target && (maxBitrate > target)){
canvas.target = maxBitrate*1.5; // set it higher than it needs to be, so it doens't jump around a lot
var yScale = height / canvas.target;
context.clearRect(0, 0, width, height);
var w = 1;
var x = width - w;
for (var i = 0; i<canvas.history_bitrate.length;i++){
var nacks = canvas.history_nacks[i];
var bitrate = canvas.history_bitrate[i];
var val = (10-nacks)/10;
if (val>1){val=1;}
else if (val<0){val=0;}
var color = getColor(val);
var y = height - bitrate * yScale;
context.fillStyle = color;
context.fillRect(x, y, w, height);
context.fillStyle = "#DDD5";
context.fillRect(x, y-2, w, 4);
if (y-5>0){
context.fillStyle = "#FFF3";
context.fillRect(x, y+2, w, 1);
}
var imageData = context.getImageData(w, 0, x, height);
context.putImageData(imageData, 0, 0);
context.clearRect(x, 0, w, height);
}
for (var tt = 2500; tt<canvas.target;tt+=2500){
var y = parseInt(height - tt * yScale);
context.fillStyle = "#0555";
context.fillRect(0, y, width, 1);
}
log("finished plotting a new y-axis");
return;
}
//if (info.available_outgoing_bitrate_kbps){
// limit target, but requires a history
//}
var val = (10-nacks)/10;
if (val>1){val=1;}
else if (val<0){val=0;}
var color = getColor(val);
var yScale = height / target;
var w = 1;
var x = width - w;
var y = height - bitrate * yScale;
context.fillStyle = color;
context.fillRect(x, y, w, height);
context.fillStyle = "#DDD5";
context.fillRect(x, y-2, w, 4);
if (y-5>0){
context.fillStyle = "#FFF3";
context.fillRect(x, y+2, w, 1);
}
context.fillStyle = "#0555";
if (canvasNew){
for (var tt = 2500; tt<target;tt+=2500){
var y = parseInt(height - tt * yScale);
context.fillRect(0, y, width, 1);
}
} else {
for (var tt = 2500; tt<target;tt+=2500){
var y = parseInt(height - tt * yScale);
context.fillRect(x, y, w, 1);
}
}
var imageData = context.getImageData(w, 0, x, height);
context.putImageData(imageData, 0, 0);
context.clearRect(x, 0, w, height);
log("finished plotting");
}
function selfDestructElement(UUID, uid){
getById("container_" + UUID).querySelectorAll('[data-uid="'+uid+'"]').forEach(ele=>{
ele.classList.remove("greyout");
clearTimeout(ele.selfFadeout);
ele.selfFadeout = setTimeout(function(ele){
ele.classList.add("greyout");
}, 4000, ele);
clearTimeout(ele.selfDestruct);
ele.selfDestruct = setTimeout(function(ele){
try {
ele.remove();
} catch(e){console.error(e);}
}, 10000, ele);
});
return;
}
function remoteStats(data, UUID, streamID){
var container = getContainer(UUID, streamID);
var size = 0;
for (var key in data) {
if (data.hasOwnProperty(key)){
size++;
}
}
if (!size){
container.querySelectorAll('[data-no-scenes]').forEach(ele=>{
ele.classList.remove("hidden");
if (ele.dataset.message){
ele.innerHTML = "<h3>No viewers active yet</h3>Statistics for this stream will be available once its view or scene link becomes active";
}
});
log("zero size");
return;
}
container.querySelectorAll('[data-no-scenes]').forEach(ele=>{
ele.classList.add("hidden");
});
for (var uuid in data){
var container2 = container.querySelector('[data-action-type="stats-graphs-details-container"][data-uid="'+uuid+'"]');
if (!container2){
container2 = document.querySelector('[data-action-type="stats-graphs-details-container"]').cloneNode(true);
container2.dataset.uid = uuid;
container2.classList.remove("hidden");
}
plotData(data[uuid], UUID, uuid);
if (("video_bitrate_kbps" in data[uuid]) && (data[uuid].video_bitrate_kbps!=="video_bitrate_kbps")){
var span = container.querySelector('[data-bitrate]');
if (span){
span.classList.remove("hidden");
span.innerHTML = "video bitrate:<br />"+numberWithCommas(parseInt(data[uuid].video_bitrate_kbps)) + " (kbps)";
}
}
var span = container.querySelector('[data-scene-name]');
if (span && ("label" in data[uuid]) && data[uuid].label){
span.classList.remove("hidden");
span.innerHTML = "stats for:<br />" + data[uuid].label;
} else if (span && ("scene" in data[uuid]) && (data[uuid].scene !==false)){
span.classList.remove("hidden");
span.innerHTML = "stats for:<br />scene" + data[uuid].scene;
} else if (uuid==="meshcast"){
span.classList.remove("hidden");
span.innerHTML = "stats for:<br />meshcast";
span.title = "You can use &label=xxxx to give your view links a unique label";
} else {
span.classList.remove("hidden");
span.innerHTML = "stats for:<br />a viewer";
span.title = "You can use &label=xxxx to give your view links a unique label";
}
if ("resolution" in data[uuid]){
var span = container.querySelector('[data-resolution]');
if (span){
span.classList.remove("hidden");
span.innerHTML = "res: "+data[uuid].resolution.replace(" @ ","<br />fps: ");
}
}
if ("video_encoder" in data[uuid]){
var span = container.querySelector('[data-video-codec]');
if (span){
span.classList.remove("hidden");
span.innerHTML = "video codec:<br />"+data[uuid].video_encoder;
}
}
}
}
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
function removeStorage(cname){

View File

@ -2,135 +2,34 @@
/* global chrome location ReadableStream define MessageChannel TransformStream */
;((name, definition) => {
typeof module !== 'undefined'
? module.exports = definition()
: typeof define === 'function' && typeof define.amd === 'object'
? define(definition)
: this[name] = definition()
})('streamSaver', () => {
function streamSaverFunction(){
'use strict'
const global = typeof window === 'object' ? window : this
const global = typeof window === 'object' ? window : this;
if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread')
let mitmTransporter = null
let supportsTransferable = false
const test = fn => { try { fn() } catch (e) {} }
const ponyfill = global.WebStreamsPolyfill || {}
const isSecureContext = global.isSecureContext
let mitmTransporter = null;
let supportsTransferable = false;
const test = fn => { try { fn() } catch (e) {} };
const ponyfill = global.WebStreamsPolyfill || {};
const isSecureContext = global.isSecureContext;
//console.log(ponyfill);
//console.log(isSecureContext);
// TODO: Must come up with a real detection test (#69)
let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint
let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint;
//console.log(useBlobFallback);
const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style
? 'iframe'
: 'navigate'
const streamSaver = {
createWriteStream,
WritableStream: global.WritableStream || ponyfill.WritableStream,
supported: true,
version: { full: '2.0.7', major: 2, minor: 0, dot: 7 },
mitm: './thirdparty/mitm.html?v=2'
}
/**
* create a hidden iframe and append it to the DOM (body)
*
* @param {string} src page to load
* @return {HTMLIFrameElement} page to load
*/
function makeIframe (src) {
if (!src) throw new Error('meh')
const iframe = document.createElement('iframe')
iframe.hidden = true
iframe.src = src
iframe.loaded = false
iframe.name = 'iframe'
iframe.isIframe = true
iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
iframe.addEventListener('load', () => {
iframe.loaded = true
}, { once: true })
document.body.appendChild(iframe)
return iframe
}
/**
* create a popup that simulates the basic things
* of what a iframe can do
*
* @param {string} src page to load
* @return {object} iframe like object
*/
function makePopup (src) {
const options = 'width=200,height=100'
const delegate = document.createDocumentFragment()
const popup = {
frame: global.open(src, 'popup', options),
loaded: false,
isIframe: false,
isPopup: true,
remove () { popup.frame.close() },
addEventListener (...args) { delegate.addEventListener(...args) },
dispatchEvent (...args) { delegate.dispatchEvent(...args) },
removeEventListener (...args) { delegate.removeEventListener(...args) },
postMessage (...args) { popup.frame.postMessage(...args) }
}
const onReady = evt => {
if (evt.source === popup.frame) {
popup.loaded = true
global.removeEventListener('message', onReady)
popup.dispatchEvent(new Event('load'))
}
}
global.addEventListener('message', onReady)
return popup
}
try {
// We can't look for service worker since it may still work on http
new Response(new ReadableStream())
if (isSecureContext && !('serviceWorker' in navigator)) {
useBlobFallback = true
}
} catch (err) {
useBlobFallback = true
}
test(() => {
// Transferable stream was first enabled in chrome v73 behind a flag
const { readable } = new TransformStream()
const mc = new MessageChannel()
mc.port1.postMessage(readable, [readable])
mc.port1.close()
mc.port2.close()
supportsTransferable = true
// Freeze TransformStream object (can only work with native)
Object.defineProperty(streamSaver, 'TransformStream', {
configurable: false,
writable: false,
value: TransformStream
})
})
function loadTransporter () {
if (!mitmTransporter) {
mitmTransporter = isSecureContext
? makeIframe(streamSaver.mitm)
: makePopup(streamSaver.mitm)
}
}
/**
* @param {string} filename filename that should be used
* @param {object} options [description]
* @param {number} size deprecated
* @return {WritableStream<Uint8Array>}
*/
: 'navigate';
//console.log(downloadStrategy);
function createWriteStream (filename, stopStream){
//console.log("createWriteStream");
let opts = {
size: null,
pathname: null,
@ -200,6 +99,7 @@
}
channel.port1.onmessage = evt => {
console.log(evt);
// Service worker sent us a link that we should open.
if (evt.data.download) {
// Special treatment for popup...
@ -309,5 +209,109 @@
}, opts.writableStrategy)
}
const streamSaver = {
createWriteStream,
WritableStream: global.WritableStream || ponyfill.WritableStream,
supported: true,
version: { full: '2.0.7', major: 2, minor: 0, dot: 7 },
mitm: './thirdparty/mitm.html?v=2'
}
//console.log(streamSaver);
/**
* create a hidden iframe and append it to the DOM (body)
*
* @param {string} src page to load
* @return {HTMLIFrameElement} page to load
*/
function makeIframe (src) {
if (!src) throw new Error('meh')
const iframe = document.createElement('iframe')
iframe.hidden = true
iframe.src = src
iframe.loaded = false
iframe.name = 'iframe'
iframe.isIframe = true
iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
iframe.addEventListener('load', () => {
iframe.loaded = true
}, { once: true })
document.body.appendChild(iframe)
return iframe
}
/**
* create a popup that simulates the basic things
* of what a iframe can do
*
* @param {string} src page to load
* @return {object} iframe like object
*/
function makePopup (src) {
const options = 'width=200,height=100'
const delegate = document.createDocumentFragment()
const popup = {
frame: global.open(src, 'popup', options),
loaded: false,
isIframe: false,
isPopup: true,
remove () { popup.frame.close() },
addEventListener (...args) { delegate.addEventListener(...args) },
dispatchEvent (...args) { delegate.dispatchEvent(...args) },
removeEventListener (...args) { delegate.removeEventListener(...args) },
postMessage (...args) { popup.frame.postMessage(...args) }
}
const onReady = evt => {
if (evt.source === popup.frame) {
popup.loaded = true
global.removeEventListener('message', onReady)
popup.dispatchEvent(new Event('load'))
}
}
global.addEventListener('message', onReady)
return popup
}
try {
// We can't look for service worker since it may still work on http
new Response(new ReadableStream())
if (isSecureContext && !('serviceWorker' in navigator)) {
useBlobFallback = true
}
} catch (err) {
useBlobFallback = true
}
//console.log("useBlobFallback: "+useBlobFallback);
test(() => {
// Transferable stream was first enabled in chrome v73 behind a flag
const { readable } = new TransformStream()
const mc = new MessageChannel()
mc.port1.postMessage(readable, [readable])
mc.port1.close()
mc.port2.close()
supportsTransferable = true
// Freeze TransformStream object (can only work with native)
Object.defineProperty(streamSaver, 'TransformStream', {
configurable: false,
writable: false,
value: TransformStream
})
})
function loadTransporter () {
if (!mitmTransporter) {
mitmTransporter = isSecureContext
? makeIframe(streamSaver.mitm)
: makePopup(streamSaver.mitm)
}
}
return streamSaver
})
};
var streamSaver = streamSaverFunction();

253
thirdparty/canvasFilters.js vendored Normal file
View File

@ -0,0 +1,253 @@
// Modified copy obtained from https://github.com/timotgl/inspector-bokeh/tree/main/demo - MIT Lic
// Original file based on https://github.com/kig/canvasfilters/blob/master/filters.js
// I reduced the modified code to a few core functions; standard convolve/blur matrix functions.
const Filters = {};
if (typeof Float32Array == 'undefined') { // good
Filters.getFloat32Array = Filters.getUint8Array = function (len) {
if (len.length) {
return len.slice(0);
}
return new Array(len);
};
} else {
Filters.getFloat32Array = function (len) {
return new Float32Array(len);
};
Filters.getUint8Array = function (len) {
return new Uint8Array(len);
};
}
if (typeof document != 'undefined') {
Filters.tmpCanvas = document.createElement('canvas');
Filters.tmpCtx = Filters.tmpCanvas.getContext('2d');
Filters.createImageData = function (w, h) {
return this.tmpCtx.createImageData(w, h);
};
} else {
onmessage = function (e) {
var ds = e.data;
if (!ds.length) {
ds = [ds];
}
postMessage(Filters.runPipeline(ds));
};
Filters.createImageData = function (w, h) {
return { width: w, height: h, data: this.getFloat32Array(w * h * 4) };
};
}
Filters.convolve = function (pixels, weights, opaque) { // good
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side / 2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
var alphaFac = opaque ? 1 : 0;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var sy = y;
var sx = x;
var dstOff = (y * w + x) * 4;
var r = 0,
g = 0,
b = 0,
a = 0;
for (var cy = 0; cy < side; cy++) {
for (var cx = 0; cx < side; cx++) {
var scy = Math.min(sh - 1, Math.max(0, sy + cy - halfSide));
var scx = Math.min(sw - 1, Math.max(0, sx + cx - halfSide));
var srcOff = (scy * sw + scx) * 4;
var wt = weights[cy * side + cx];
r += src[srcOff] * wt;
g += src[srcOff + 1] * wt;
b += src[srcOff + 2] * wt;
a += src[srcOff + 3] * wt;
}
}
dst[dstOff] = r;
dst[dstOff + 1] = g;
dst[dstOff + 2] = b;
dst[dstOff + 3] = a + alphaFac * (255 - a);
}
}
return output;
};
Filters.luminance = function (pixels, args) { // good
var output = Filters.createImageData(pixels.width, pixels.height);
var dst = output.data;
var d = pixels.data;
for (var i = 0; i < d.length; i += 4) {
var r = d[i];
var g = d[i + 1];
var b = d[i + 2];
// CIE luminance for the RGB
var v = 0.2126 * r + 0.7152 * g + 0.0722 * b;
dst[i] = dst[i + 1] = dst[i + 2] = v;
dst[i + 3] = d[i + 3];
}
return output;
};
Filters.runPipeline = function (ds) {
var res = null;
res = this[ds[0].name].apply(this, ds[0].args);
for (var i = 1; i < ds.length; i++) {
var d = ds[i];
var args = d.args.slice(0);
args.unshift(res);
res = this[d.name].apply(this, args);
}
return res;
};
Filters.identity = function (pixels, args) {
var output = Filters.createImageData(pixels.width, pixels.height);
var dst = output.data;
var d = pixels.data;
for (var i = 0; i < d.length; i++) {
dst[i] = d[i];
}
return output;
};
Filters.horizontalConvolve = function (pixels, weightsVector, opaque) {
var side = weightsVector.length;
var halfSide = Math.floor(side / 2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
var w = sw;
var h = sh;
var output = Filters.createImageData(w, h);
var dst = output.data;
var alphaFac = opaque ? 1 : 0;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var sy = y;
var sx = x;
var dstOff = (y * w + x) * 4;
var r = 0,
g = 0,
b = 0,
a = 0;
for (var cx = 0; cx < side; cx++) {
var scy = sy;
var scx = Math.min(sw - 1, Math.max(0, sx + cx - halfSide));
var srcOff = (scy * sw + scx) * 4;
var wt = weightsVector[cx];
r += src[srcOff] * wt;
g += src[srcOff + 1] * wt;
b += src[srcOff + 2] * wt;
a += src[srcOff + 3] * wt;
}
dst[dstOff] = r;
dst[dstOff + 1] = g;
dst[dstOff + 2] = b;
dst[dstOff + 3] = a + alphaFac * (255 - a);
}
}
return output;
};
Filters.separableConvolve = function (
pixels,
horizWeights,
vertWeights,
opaque
) {
return this.horizontalConvolve(
this.verticalConvolveFloat32(pixels, vertWeights, opaque),
horizWeights,
opaque
);
};
Filters.gaussianBlur = function (pixels, diameter) { // good
diameter = Math.abs(diameter);
if (diameter <= 1) return Filters.identity(pixels);
var radius = diameter / 2;
var len = Math.ceil(diameter) + (1 - (Math.ceil(diameter) % 2));
var weights = this.getFloat32Array(len);
var rho = (radius + 0.5) / 3;
var rhoSq = rho * rho;
var gaussianFactor = 1 / Math.sqrt(2 * Math.PI * rhoSq);
var rhoFactor = -1 / (2 * rho * rho);
var wsum = 0;
var middle = Math.floor(len / 2);
for (var i = 0; i < len; i++) {
var x = i - middle;
var gx = gaussianFactor * Math.exp(x * x * rhoFactor);
weights[i] = gx;
wsum += gx;
}
for (var i = 0; i < weights.length; i++) {
weights[i] /= wsum;
}
return Filters.separableConvolve(pixels, weights, weights, false);
};
Filters.verticalConvolveFloat32 = function (pixels, weightsVector, opaque) {
var side = weightsVector.length;
var halfSide = Math.floor(side / 2);
var src = pixels.data;
var sw = pixels.width;
var sh = pixels.height;
var w = sw;
var h = sh;
var output = { width: w, height: h, data: this.getFloat32Array(w * h * 4) };
var dst = output.data;
var alphaFac = opaque ? 1 : 0;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var sy = y;
var sx = x;
var dstOff = (y * w + x) * 4;
var r = 0,
g = 0,
b = 0,
a = 0;
for (var cy = 0; cy < side; cy++) {
var scy = Math.min(sh - 1, Math.max(0, sy + cy - halfSide));
var scx = sx;
var srcOff = (scy * sw + scx) * 4;
var wt = weightsVector[cy];
r += src[srcOff] * wt;
g += src[srcOff + 1] * wt;
b += src[srcOff + 2] * wt;
a += src[srcOff + 3] * wt;
}
dst[dstOff] = r;
dst[dstOff + 1] = g;
dst[dstOff + 2] = b;
dst[dstOff + 3] = a + alphaFac * (255 - a);
}
}
return output;
};
export default Filters;

11
thirdparty/focus_worker.js vendored Normal file
View File

@ -0,0 +1,11 @@
// Part of Inspector Bokeh by @timotgl
// MIT License - Copyright (c) 2016 Timo Taglieber <github@timotaglieber.de>
// https://github.com/timotgl/inspector-bokeh
import measureBlur from './measureBlur.js';
onmessage = (messageEvent) => {
postMessage({
score: measureBlur(messageEvent.data.imageData),
});
};

124
thirdparty/measureBlur.js vendored Normal file
View File

@ -0,0 +1,124 @@
// Inspector Bokeh by @timotgl
// MIT License - Copyright (c) 2016 Timo Taglieber <github@timotaglieber.de>
// https://github.com/timotgl/inspector-bokeh
// This is just a copy of ../src/measureBlur.js that has been edited
// to assume that canvasFilters is already an ES module
// TODO: solve with bundling somehow
import Filters from './canvasFilters.js';
/**
* I forgot why exactly I was doing this.
* It somehow improves edge detection to blur the image a bit beforehand.
* But we don't want to do this for very small images.
*/
const BLUR_BEFORE_EDGE_DETECTION_MIN_WIDTH = 360; // pixels
const BLUR_BEFORE_EDGE_DETECTION_DIAMETER = 5.0; // pixels
/**
* Only count edges that reach a certain intensity.
* I forgot which unit this was. But it's not pixels.
*/
const MIN_EDGE_INTENSITY = 20;
const detectEdges = (imageData) => {
const preBlurredImageData =
imageData.width >= BLUR_BEFORE_EDGE_DETECTION_MIN_WIDTH
? Filters.gaussianBlur(imageData, BLUR_BEFORE_EDGE_DETECTION_DIAMETER)
: imageData;
const greyscaled = Filters.luminance(preBlurredImageData);
const sobelKernel = Filters.getFloat32Array([1, 0, -1, 2, 0, -2, 1, 0, -1]);
return Filters.convolve(greyscaled, sobelKernel, true);
};
/**
* Reduce imageData from RGBA to only one channel (Y/luminance after conversion
* to greyscale) since RGB all have the same values and Alpha was ignored.
*/
const reducedPixels = (imageData) => {
const { data: pixels, width } = imageData;
const rowLen = width * 4;
let i,
x,
y,
row,
rows = [];
for (y = 0; y < pixels.length; y += rowLen) {
row = new Uint8ClampedArray(imageData.width);
x = 0;
for (i = y; i < y + rowLen; i += 4) {
row[x] = pixels[i];
x += 1;
}
rows.push(row);
}
return rows;
};
/**
* @param pixels Array of Uint8ClampedArrays (row in original image)
*/
const detectBlur = (pixels) => {
const width = pixels[0].length;
const height = pixels.length;
let x,
y,
value,
oldValue,
edgeStart,
edgeWidth,
bm,
percWidth,
numEdges = 0,
sumEdgeWidths = 0;
for (y = 0; y < height; y += 1) {
// Reset edge marker, none found yet
edgeStart = -1;
for (x = 0; x < width; x += 1) {
value = pixels[y][x];
// Edge is still open
if (edgeStart >= 0 && x > edgeStart) {
oldValue = pixels[y][x - 1];
// Value stopped increasing => edge ended
if (value < oldValue) {
// Only count edges that reach a certain intensity
if (oldValue >= MIN_EDGE_INTENSITY) {
edgeWidth = x - edgeStart - 1;
numEdges += 1;
sumEdgeWidths += edgeWidth;
}
edgeStart = -1; // Reset edge marker
}
}
// Edge starts
if (value == 0) {
edgeStart = x;
}
}
}
if (numEdges === 0) {
bm = 0;
percWidth = 0;
} else {
bm = sumEdgeWidths / numEdges;
percWidth = (bm / width) * 100;
}
return {
width: width,
height: height,
num_edges: numEdges,
avg_edge_width: bm,
avg_edge_width_perc: percWidth,
};
};
const measureBlur = (imageData) => detectBlur(reducedPixels(detectEdges(imageData)));
export default measureBlur;

File diff suppressed because one or more lines are too long