mirror of
https://github.com/eliasstepanik/vdo.ninja.git
synced 2026-01-11 21:58:35 +00:00
Merge pull request #973 from steveseguin/steveseguin-patch-1
v22 updates
This commit is contained in:
commit
fc2d77671d
@ -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;
|
||||
}
|
||||
|
||||
51
iframe.html
51
iframe.html
@ -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);
|
||||
|
||||
83
index.html
83
index.html
@ -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>
|
||||
|
||||
20
main.css
20
main.css
@ -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
234
main.js
@ -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();
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
471
stats.html
471
stats.html
@ -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">×</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<b=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){
|
||||
|
||||
248
thirdparty/StreamSaver.js
vendored
248
thirdparty/StreamSaver.js
vendored
@ -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
253
thirdparty/canvasFilters.js
vendored
Normal 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
11
thirdparty/focus_worker.js
vendored
Normal 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
124
thirdparty/measureBlur.js
vendored
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user