Add Group Video Chat to OBS
+Add Group Chat to OBS
Rooms allow for simplified group-chat and the advanced management of multiple streams at once.
+
Room Name:
-
Room Name:
+ + +
Add your Camera to OBS
+Add your Camera to OBS
-
Select the audio/video source below and when you're ready just click START SHARING WEBCAM
- - +
Select the audio/video source below
+ ++ +
+-
Video source:
+
Video source:
+ +
-
+
- +
Audio source:
Remote Screenshare into OBS
+Remote Screenshare into OBS
note: Do not forget to click "Share audio" in Chrome.
(Firefox does not support audio sharing.)

+ note: Do not forget to click "Share audio" in Chrome.
(Firefox does not support audio sharing.)

- - + +
+
Audio Sources:
+
Generate Invite Link
-Create Reusable Invite
+- Invite Links are reusable, but each invited media source needs a unique invite link.
-
+
+ - -
What is OBS.Ninja
+
+
+ Known issues:
-
What is OBS.Ninja
-
-
- Known issues:
+
+ + Site last updated: May 7th, 2020. The previous version can be found at https://obs.ninja/v3/ if you are having new issues. -
- Site last updated: April 21th, 2020. The previous version can be found at https://obs.ninja/old/ if you are having new issues - -
-
Check out the sub-reddit for help and advanced info. I'm also on Discord and you can email me at steve@seguin.email
++
Check out the sub-reddit for help and advanced info. I'm also on Discord and you can email me at steve@seguin.email
+Please select an option to join.'; + } + document.getElementById("add_camera").innerHTML = "Join Room with Camera"; document.getElementById("add_screen").innerHTML = "Screenshare with Room"; document.getElementById("head3").className = 'advanced'; @@ -203,10 +297,12 @@ if (urlParams.has('roomid')){ function checkConnection(){ - if (session.ws.readyState === WebSocket.OPEN) { - document.getElementById("qos").style.color = "white"; - } else { - document.getElementById("qos").style.color = "red"; + if (document.getElementById("qos")){ + if (session.ws.readyState === WebSocket.OPEN) { + document.getElementById("qos").style.color = "white"; + } else { + document.getElementById("qos").style.color = "red"; + } } } setInterval(function(){checkConnection();},5000); @@ -221,25 +317,34 @@ function updateStats(){ log(track.getSettings()); log(track.getSettings().frameRate); //log(track.getSettings().frameRate); - document.getElementById("webcamstats").innerHTML = "Current Video Settings: "+(track.getSettings().width|0) +"x"+(track.getSettings().height|0)+"@"+(parseInt(track.getSettings().frameRate*10)/10)+"fps"; + document.getElementById("webcamstats").innerHTML = "Current Video Settings: "+(track.getSettings().width||0) +"x"+(track.getSettings().height||0)+"@"+(parseInt(track.getSettings().frameRate*10)/10)+"fps"; } ); } function toggleMute(){ // TODO: I need to have this be MUTE, toggle, with volume not touched. - var msg = {}; - if (micvolume==0){ - micvolume = 100; - document.getElementById("mutetoggle").className="fa fa-microphone my-float"; - document.getElementById("mutebutton").className="float3"; - } else{ - micvolume=0; + + if (session.muted==false){ + session.muted = true; document.getElementById("mutetoggle").className="fa fa-microphone-slash my-float"; document.getElementById("mutebutton").className="float"; + session.streamSrc.getAudioTracks().forEach((track) => { + track.enabled = false; + }); + + } else{ + session.muted=false; + + document.getElementById("mutetoggle").className="fa fa-microphone my-float"; + document.getElementById("mutebutton").className="float3"; + + + session.streamSrc.getAudioTracks().forEach((track) => { + track.enabled = true; + }); } - msg.volume = micvolume; - session.volume = micvolume; - session.sendMessage(msg); + + } //////////////////////////// @@ -249,7 +354,7 @@ function directEnable(ele){ // A directing room only is controlled by the Direct ele.parentNode.parentNode.dataset.enable = 0; ele.className = ""; ele.innerHTML = "Add to Group Scene"; - ele.parentNode.parentNode.style.backgroundColor = "#E3E4EF"; + ele.parentNode.parentNode.style.backgroundColor = "#E3E4FF"; } else { ele.parentNode.parentNode.style.backgroundColor = "#AFA"; ele.parentNode.parentNode.dataset.enable = 1; @@ -259,7 +364,7 @@ function directEnable(ele){ // A directing room only is controlled by the Direct var msg = {}; msg.request = "sendroom"; msg.roomid = session.roomid; - msg.director = "1" // scene + msg.director = "1"; // scene msg.action = "display"; msg.value = ele.parentNode.parentNode.dataset.enable; msg.target = ele.parentNode.parentNode.dataset.UUID; @@ -323,6 +428,7 @@ var activatedStream = false; function publishScreen(){ if( activatedStream == true){return;} activatedStream = true; + setTimeout(function(){activatedStream=false;},1000); var title = "ScreenShare";//document.getElementById("videoname2").value; @@ -342,21 +448,14 @@ function publishScreen(){ audio: {echoCancellation: false, autoGainControl: false, noiseSuppression:false }, // I hope this doesn't break things.. video: {width: width, height: height, cursor: "never", mediaSource: "browser"} }; - + if (session.framerate){ constraints.video.frameRate = {exact: session.framerate}; - } - - if (session.roomid){ - window.addEventListener("resize", updateMixer); - joinRoom(session.roomid,100); - document.getElementById("head3").className = 'advanced'; - //updateURL("permaid="+session.streamID); - } else { - document.getElementById("head3").className = ''; - } - updateURL("permaid="+session.streamID); - session.publishScreen(constraints, title); + } + + var audioSelect = document.querySelector('select#audioSourceScreenshare'); + + session.publishScreen(constraints, title, audioSelect); log("streamID is: "+session.streamID); document.getElementById("mutebutton").className="float3"; @@ -369,26 +468,27 @@ function publishScreen(){ function publishWebcam(){ if( activatedStream == true){return;} activatedStream = true; - + log("PRESSED PUBLISH WEBCAM!!"); var title = "Webcam"; // document.getElementById("videoname3").value; var ele = document.getElementById("previewWebcam"); var stream = ele.srcObject; + ele.parentNode.removeChild(ele); formSubmitting = false; window.scrollTo(0, 0); // iOS has a nasty habit of overriding the CSS when changing camaera selections, so this addresses that. if (session.roomid){ + console.log("ROOM ID ENABLED"); window.addEventListener("resize", updateMixer); - joinRoom(session.roomid,100); + joinRoom(session.roomid); document.getElementById("head3").className = 'advanced'; - //updateURL("permaid="+session.streamID); } else { document.getElementById("head3").className = ''; } - updateURL("permaid="+session.streamID); + updateURL("push="+session.streamID); session.publishStream(stream, title); log("streamID is: "+session.streamID); document.getElementById("head1").className = 'advanced'; @@ -408,27 +508,28 @@ function joinRoom(roomname, maxbitrate=false){ session.joinRoom(roomname,maxbitrate).then(function(response){ // callback from server; we've joined the room log("Members in Room"); log(response); - for (i in response){ + for (var i in response){ if ("UUID" in response[i]){ if ("streamID" in response[i]){ - if (response[i]['UUID'] in session.pcs){ + if (response[i].UUID in session.pcs){ log("RTC already connected"); /// lets just say instead of Stream, we have } else { //var title = ""; // TODO: Assign labels //if ("title" in response[i]){ // title = response[i]["title"]; //} - if (urlParams.has('streamid')){ - play(response[i]['streamID']); + + if ((urlParams.has('streamid')) || (urlParams.has('view'))){ + play(response[i].streamID); } else { - session.watchStream(response[i]['streamID']); // How do I make sure they aren't requesting the same movie twice as a race condition? + session.watchStream(response[i].streamID); // How do I make sure they aren't requesting the same movie twice as a race condition? } } } } } - },function(error){return {}}); + },function(error){return {};}); } else { errorlog("Room name not long enough or contained all bad characaters"); } @@ -438,6 +539,7 @@ function joinRoom(roomname, maxbitrate=false){ function createRoom(){ var roomname = document.getElementById("videoname1").value; + log(roomname); if (roomname.length==0){ alert("Please enter a room name before continuing"); @@ -447,12 +549,6 @@ function createRoom(){ var gridlayout = document.getElementById("gridlayout"); gridlayout.classList.add("directorsgrid"); - // var sheet = document.createElement('style'); - // sheet.innerHTML = ".tile{object-fit:contain }"; - // document.body.appendChild(sheet); - - var roomname = document.getElementById("videoname1").value; - log(roomname); session.roomid = roomname; formSubmitting = false; @@ -464,7 +560,7 @@ function createRoom(){ document.getElementById("head3").className = 'advanced'; document.getElementById("head4").className = ''; - document.getElementById("dirroomid").innerHTML = roomname; + document.getElementById("dirroomid").innerHTML = roomname; document.getElementById("roomid").innerHTML = roomname; @@ -473,32 +569,60 @@ function createRoom(){ session.director = true; document.getElementById("reshare").parentNode.removeChild(document.getElementById("reshare")); - gridlayout.innerHTML = "
Add Local Camera or OBS VirtualCam: https://"+location.hostname+location.pathname+"?roomid="+session.roomid+"&streamid\ -
"; + gridlayout.innerHTML += "
- Link to Invite users to broadcast their feeds to the group. These users will not see or hear any feed from the group.
"; - gridlayout.innerHTML += " Group Scene (OBS link /w auto-mixing):https://"+location.hostname+location.pathname+"?scene=1&roomid="+session.roomid+"
"; + gridlayout.innerHTML += " - This is an OBS Browser Source link that contains the group chat in just a single scene. Videos must be added to Group Scene.
"; - gridlayout.innerHTML += "
\ - As guests join, their videos will appear below. You can bring their video streams into OBS manually as solo-scenes or you can add them to the Group Scene. The Group Scene auto-mixes together videos you have added to the group-scene; just add the Group Scene link to OBS in that case.\ -
"; + gridlayout.innerHTML += ''; - joinRoom(roomname,100); + gridlayout.innerHTML += "
"; + + gridlayout.innerHTML += "
GUEST SLOT #1
(A video will appear here when a guest joins)
A Solo-Link for OBS will appear here.
GUEST SLOT #2
(A video will appear here when a guest joins)
A Solo Link for OBS will appear here
GUEST SLOT #3
(A video will appear here when a guest joins)
A Solo Link for OBS will appear here
GUEST SLOT #4
(A video will appear here when a guest joins)
A Solo Link for OBS will appear here
"; - }); - },function(error){return {}}); + +function recordVideo(event, video, UUID, videoKbps=2500){ + var target = event.currentTarget; + if ("recording" in video){ + log("ALREADY RECORDING!"); + target.style.backgroundColor = null; + target.innerHTML = "Record"; + video.recorder.stop(); + session.requestRateLimit(100,UUID); + delete(video.recorder); + delete(video.recording); + + return; + + } else { + target.style.backgroundColor = "#FCC"; + target.innerHTML = "Download"; + video.recording = true; + } + + videoKbps = prompt("Press OK to start recording. Press again to stop and download.\n\nWarning: Keep this browser tab active to continue recording.\n\nYou can change the default video bitrate if desired below (kbps)",videoKbps); + videoKbps = parseInt(videoKbps); + if (videoKbps<35){ + videoKbps=35; + } + session.requestRateLimit(videoKbps, UUID); + + var filename = Date.now().toString(); + //var canvas = document.createElement('canvas'); + //canvas.width = video.videoWidth; + //canvas.height = video.videoHeight; + //var ctx = canvas.getContext('2d'); + var recordedBlobs = []; + + ///var stream = canvas.captureStream(); + stream = video.srcObject;//.getVideoTracks().forEach( + //stream.getAudioTracks + + var cancell = false; + if (typeof stream == undefined || !stream) {return;} + + this.stop = stopRecording; + + let options = { + mimeType: "video/webm", + videoBitsPerSecond: parseInt(videoKbps*1000) // 2.5Mbps + }; + var mediaRecorder = new MediaRecorder(stream,options); + + var lasttime = 0; + //function drawVideoFrame() { + // if (Date.now() - lasttime <= 16){return;} + // lasttime=Date.now(); + // ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + // }; + + //var drawtimer = setInterval(function(){ + // requestAnimationFrame(drawVideoFrame); + // },25); + + function download() { + const blob = new Blob(recordedBlobs, { type: "video/webm" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.style.display = 'none'; + a.href = url; + a.download = filename+".webm"; + document.body.appendChild(a); + a.click(); + setTimeout(() => { + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }, 100); + } + + function handleDataAvailable(event) { + if (event.data && event.data.size > 0) { + recordedBlobs.push(event.data); + } + } + function stopRecording() { + mediaRecorder.stop(); + //clearInterval(drawtimer); + cancell = true; + console.log('Recorded Blobs: ', recordedBlobs); + download(); + } + mediaRecorder.ondataavailable = handleDataAvailable; + //drawVideoFrame(); + + mediaRecorder.onerror = function(event) { + errorlog(event); + stopRecording(); + session.requestRateLimit(100,UUID); + alert("an error occured with the media recorder; stopping recording"); + }; + + stream.ended = function(event) { + stopRecording(); + session.requestRateLimit(100,UUID); + alert("stream ended! stopping recording"); + }; + + mediaRecorder.start(100); // 100ms chunks + + + console.log('MediaRecorder started', mediaRecorder); + + return this; +} +function copyFunction(copyText) { + copyText.select(); + copyText.setSelectionRange(0, 99999); + document.execCommand("copy"); + } -function generateQRPage(ele){ + +function generateQRPage(){ try{ var title = encodeURI(document.getElementById("videoname4").value); if (title.length){ title = "&label="+title; } - var sid = session.generateStreamID(); - ele.parentNode.innerHTML = '
\ -
and don\'t forget the
OBS Link:
https://' + location.hostname + location.pathname + '?streamid=' + sid + title + '
In OBS v25 you can drag this link directly into OBS, or you can create a Browse element in OBS and insert it the URL source. \ + var sid = session.generateStreamID(); + + var viewstr = ""; + var sendstr = ""; + + if (document.getElementById("invite_bitrate").checked){ + viewstr+="&bitrate=20000"; + } + if (document.getElementById("invite_vp9").checked){ + viewstr+="&codec=vp9"; + } + if (document.getElementById("invite_stereo").checked){ + viewstr+="&stereo"; + sendstr+="&stereo"; + } + if (document.getElementById("invite_secure").checked){ + sendstr+="&secure"; + } + + sendstr = 'https://' + location.host + location.pathname + '?push=' + sid + sendstr; + viewstr = 'https://' + location.host+ location.pathname + '?view=' + sid + viewstr + title; + + document.getElementById("gencontent").innerHTML = '
Guest Invite Link:
and don\'t forget the
OBS Browser Source Link:
\\ Please also note, the invite link and OBS ingestion link created is reusable, but only one person may use a specific invite at a time.'; var qrcode = new QRCode(document.getElementById("qrcode"), { @@ -921,7 +1511,7 @@ function generateQRPage(ele){ colorLight : "#FFFFFF", useSVG: false }); - qrcode.makeCode('https://' + location.hostname + location.pathname + '?permaid=' + sid); + qrcode.makeCode(sendstr); } catch(e){ errorlog(e); @@ -929,13 +1519,13 @@ function generateQRPage(ele){ } -if (urlParams.has('streamid')){ +if ((urlParams.has('streamid')) || (urlParams.has('view'))){ document.getElementById("main").className = ""; document.getElementById("credits").style.display = 'none'; } -if ((urlParams.has('streamid')) && (session.roomid==false)){ +if (((urlParams.has('streamid')) || (urlParams.has('view'))) && (session.roomid==false)){ document.getElementById("container-4").className = 'column columnfade'; document.getElementById("container-3").className = 'column columnfade'; document.getElementById("container-2").className = 'column columnfade'; @@ -957,27 +1547,17 @@ if ((urlParams.has('streamid')) && (session.roomid==false)){ setTimeout(function(){ try{ - if (urlParams.get("streamid")){ + if ((urlParams.has('streamid')) || (urlParams.has('view'))){ if (document.getElementById("mainmenu")){ document.getElementById("mainmenu").innerHTML = '
Attempting to load video stream.
'; - document.getElementById("mainmenu").innerHTML += 'If the stream does not load within a few seconds, the stream may not be available or some other error has occured. If the issue persists, please check out the https://reddit.com/r/obsninja for possible solutions or contact steve@seguin.email.'; - - document.getElementById("mainmenu").innerHTML += '
Stream Invite URL:
https://' + location.hostname + location.pathname + '?permaid=' + urlParams.get("streamid") + '
'; retry = setInterval(function(){ if (document.getElementById("mainmenu")){ play(); } else { clearInterval(retry); } - },10000) + },10000); }} } catch(e){ errorlog("Error handling QR Code failure"); @@ -999,8 +1579,12 @@ if ((urlParams.has('streamid')) && (session.roomid==false)){ } } + + +// + function updateMixer(){ - //log("update mixer"); + log("UPDATE mixer"); var playarea = document.getElementById("gridlayout"); //log(session.mediaPool); @@ -1017,29 +1601,65 @@ function updateMixer(){ var mediaPool = []; - if (session.videoElement){ - if (session.videoElement.style.display!="none"){ + + if (session.videoElement){ // I, myself, exist + if (session.videoElement.style.display!="none"){ // local feed mediaPool.push(session.videoElement); } } - for (i in session.rpcs){ - if (session.rpcs[i].videoElement){ - if (session.rpcs[i].videoElement.style.display!="none"){ - session.requestRateLimit(-1,i); // unlock bitrate - mediaPool.push(session.rpcs[i].videoElement); - } else { - session.requestRateLimit(300,i); + if ((session.infocus) && (session.infocus in session.rpcs)){ + mediaPool = []; + log(session.infocus+" set fullscreen"); + session.requestRateLimit(1200, session.infocus); // 1.2mbps is decent, no? + mediaPool.push(session.rpcs[session.infocus].videoElement); + for (var j in session.rpcs){ + if (j != session.infocus){ + session.requestRateLimit(40, j); } } - }; + } else if ((session.infocus) && (session.infocus === true)){ + log("myself set fullscreen"); + for (var j in session.rpcs){ + session.requestRateLimit(40, j); + } + } else { + for (var i in session.rpcs){ + if (session.rpcs[i].videoElement){ // remote feeds + + session.rpcs[i].targetBandwidth = -1; + + if (session.rpcs[i].videoElement.style.display!="none"){ // Add it if not hidden + mediaPool.push(session.rpcs[i].videoElement); + } + + if (session.director){ // director video should be low-bitrate, although this should never fire. + errorlog("Update should not be called on DIRECTORs view? sorta at least"); + //session.requestRateLimit(100, i); + } else if (session.rpcs[i].videoElement.style.display=="none"){ + if (session.scene){ + session.requestRateLimit(300, i); // hidden. I dont want it to be super low, for video quality reasons. + } else { + session.requestRateLimit(40, i); // w/e This is not in OBS, so we just set it as low as possible. + } + } else if (session.single){ // max + } else if (session.scene){ // max + } else if (session.roomid){ // guests should see video at low bitrate, ie: 100kbps (not 40kbps like if disabled) + session.requestRateLimit(100, i); + } + // only set to 300 if not a guest, or if zoomed in. + + } + } + } + if (session.director){return;} // director view says go no further :) if (mediaPool.length>1){ - var mod = Math.pow((ww*hh)/(mediaPool.length),0.5) // 80 + var mod = Math.pow((ww*hh)/(mediaPool.length),0.5); // 80 var rw = Math.ceil(ww/mod); // 80/80 var rh = Math.ceil(hh/mod); - } else { rw=1; rh=1;} + } else { var rw=1; var rh=1;} //log(mod+","+rw+","+rh); //80,1,1 playarea.innerHTML = ""; @@ -1063,7 +1683,6 @@ function updateMixer(){ offsety = (h- Math.ceil(mediaPool.length/rw)*Math.ceil(h/rh))/2; vid.style.left = offsetx+Math.floor(((i%rw)+0)*w/rw)+"px"; - //vid.style.left = Math.floor(((i%rw)+0)*w/rw)+"px"; vid.style.top = offsety+Math.floor((Math.floor(i/rw)+0)*h/rh + hi)+"px"; @@ -1071,34 +1690,70 @@ function updateMixer(){ vid.style.height = Math.ceil(h/rh)+"px"; playarea.appendChild(vid); vid.play(); + + + var button = document.createElement("DIV"); + button.id = "button_"+vid.id; + + button.innerHTML = ""; + + button.style.width ="50px"; + button.style.height = "50px"; + button.style.position = "absolute"; + button.style.display="none"; + + button.style.left = (Math.ceil(w/rw) - 50 + offsetx+Math.floor(((i%rw)+0)*w/rw))+"px"; + button.style.top = ( offsety+Math.floor((Math.floor(i/rw)+0)*h/rh + hi))+"px"; + button.style.color = "white"; + button.style.cursor = "pointer"; + + playarea.appendChild(button); + if (vid.id == "videosource"){ + button.onclick = function(){ + var target = event.currentTarget; + if (session.infocus === true){ + session.infocus = false; + target.innerHTML = ""; + } else { + session.infocus = true; + log("session: myself"); + target.innerHTML = ""; + } + setTimeout(()=>updateMixer(),10); + }; + } else { + button.dataset.UUID = vid.dataset.UUID; + button.onclick = function(event){ + var target = event.currentTarget; + log("fullscreen"); + log(target); + if (session.infocus === target.dataset.UUID){ + target.innerHTML = ""; + session.infocus = false; + } else { + target.innerHTML = ""; + session.infocus = target.dataset.UUID; + log("session:"+target.dataset.UUID); + } + setTimeout(()=>updateMixer(),10); + + }; + } + + button.onmouseenter = function(){ + button.style.display="block"; + }; + + vid.onmouseenter = function(){ + button.style.display="block"; + }; + vid.onmouseleave = function(){ + button.style.display="none"; + }; i+=1; }); } -document.addEventListener("dragstart", e => { - var url = e.target.href || e.target.data; - if (!url || !url.startsWith('http')) return; - - var streamId = url.split('streamid='); - var label = url.split('label='); - - - url += '&layer-name=OBS.Ninja'; - if (streamId.length>1) url += ': ' + streamId[1].split('&')[0]; - - if (label.length>1) url += ' - ' + decodeURI(label[1].split('&')[0]); - - try{ - var video = document.getElementById('videosource'); - url += '&layer-width=' + video.videoWidth; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += '&layer-height=' + video.videoHeight; - } catch(e){ - url += '&layer-width=1280'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough - url += '&layer-height=720'; - } - e.dataTransfer.setData("text/uri-list", encodeURI(url)); -}); - var vis = (function(){ var stateKey, eventKey, keys = { hidden: "visibilitychange", @@ -1119,5 +1774,300 @@ var vis = (function(){ //document.addEventListener("focus", c); } return !document[stateKey]; - } + }; })(); + +(function() { // right click menu + + "use strict"; + + function clickInsideElement( e, className ) { + var el = e.srcElement || e.target; + + if ( el.classList.contains(className) ) { + return el; + } else { + while ( el = el.parentNode ) { + if ( el.classList && el.classList.contains(className) ) { + return el; + } + } + } + + return false; + } + + /** + * Get's exact position of event. + * + * @param {Object} e The event passed in + * @return {Object} Returns the x and y position + */ + function getPosition(event2) { + var posx = 0; + var posy = 0; + + if (!event2) var event = window.event; + + if (event2.pageX || event2.pageY) { + posx = event2.pageX; + posy = event2.pageY; + } else if (event2.clientX || event2.clientY) { + posx = event2.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + posy = event2.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + return { + x: posx, + y: posy + }; + } + + var contextMenuClassName = "context-menu"; + var contextMenuItemClassName = "context-menu__item"; + var contextMenuLinkClassName = "context-menu__link"; + var contextMenuActive = "context-menu--active"; + + var taskItemClassName = "task"; + var taskItemInContext; + + var clickCoords; + var clickCoordsX; + var clickCoordsY; + + var menu = document.querySelector("#context-menu"); + var menuItems = menu.querySelectorAll(".context-menu__item"); + var menuState = 0; + var menuWidth; + var menuHeight; + var menuPosition; + var menuPositionX; + var menuPositionY; + + var windowWidth; + var windowHeight; + + /** + * Initialise our application's code. + */ + function init() { + contextListener(); + clickListener(); + keyupListener(); + resizeListener(); + } + + /** + * Listens for contextmenu events. + */ + function contextListener() { + document.addEventListener( "contextmenu", function(e) { + taskItemInContext = clickInsideElement( e, taskItemClassName ); + + if ( taskItemInContext ) { + e.preventDefault(); + toggleMenuOn(); + positionMenu(e); + } else { + taskItemInContext = null; + toggleMenuOff(); + } + }); + } + + /** + * Listens for click events. + */ + function clickListener() { + document.addEventListener( "click", function(e) { + var clickeElIsLink = clickInsideElement( e, contextMenuLinkClassName ); + + if ( clickeElIsLink ) { + e.preventDefault(); + menuItemListener( clickeElIsLink ); + } else { + var button = e.which || e.button; + if ( button === 1 ) { + toggleMenuOff(); + } + } + }); + } + + /** + * Listens for keyup events. + */ + function keyupListener() { + window.onkeyup = function(e) { + if ( e.keyCode === 27 ) { + toggleMenuOff(); + } + }; + } + + /** + * Window resize event listener + */ + function resizeListener() { + window.onresize = function(e) { + toggleMenuOff(); + }; + } + + /** + * Turns the custom context menu on. + */ + function toggleMenuOn() { + if ( menuState !== 1 ) { + menuState = 1; + menu.classList.add( contextMenuActive ); + } + } + + /** + * Turns the custom context menu off. + */ + function toggleMenuOff() { + if ( menuState !== 0 ) { + menuState = 0; + menu.classList.remove( contextMenuActive ); + } + } + + /** + * Positions the menu properly. + * + * @param {Object} e The event + */ + function positionMenu(e) { + clickCoords = getPosition(e); + clickCoordsX = clickCoords.x; + clickCoordsY = clickCoords.y; + + menuWidth = menu.offsetWidth + 4; + menuHeight = menu.offsetHeight + 4; + + windowWidth = window.innerWidth; + windowHeight = window.innerHeight; + + if ( (windowWidth - clickCoordsX) < menuWidth ) { + menu.style.left = windowWidth - menuWidth + "px"; + } else { + menu.style.left = clickCoordsX + "px"; + } + + if ( (windowHeight - clickCoordsY) < menuHeight ) { + menu.style.top = windowHeight - menuHeight + "px"; + } else { + menu.style.top = clickCoordsY + "px"; + } + } + + /** + * Dummy action function that logs an action when a menu item link is clicked + * + * @param {HTMLElement} link The link that was clicked + */ + function menuItemListener( link ) { + if (link.getAttribute("data-action")=="Open"){ + window.open(taskItemInContext.value); + } else { + // nothing needed + } + console.log( "Task ID - " + taskItemInContext + ", Task action - " + link.getAttribute("data-action")); + toggleMenuOff(); + } + + /** + * Run the app. + */ + init(); + +})(); + +document.addEventListener("dragstart", event => { + var url = event.target.href || event.target.value; + if (!url || !url.startsWith('https://')) return; + if (event.target.dataset.drag!="1"){ + return; + } + var streamId = url.split('view='); + var label = url.split('label='); + + url += '&layer-name=OBS.Ninja'; + if (streamId.length>1) url += ': ' + streamId[1].split('&')[0]; + if (label.length>1) url += ' - ' + decodeURI(label[1].split('&')[0]); + + try{ + var video = document.getElementById('videosource'); + url += '&layer-width=' + video.videoWidth; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += '&layer-height=' + video.videoHeight; + } catch(error){ + url += '&layer-width=1280'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough + url += '&layer-height=720'; + } + event.dataTransfer.setData("text/uri-list", encodeURI(url)); +}); + +function popupMessage(e, message="Copied to Clipboard"){ // right click menu + + var posx = 0; + var posy = 0; + + if (!e) var e = window.event; + + if (e.pageX || e.pageY) { + posx = e.pageX; + posy = e.pageY; + } else if (e.clientX || e.clientY) { + posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + posx += 10; + + var contextMenuActive = "context-menu--active"; + + var menu = document.querySelector("#messagePopup"); + menu.innerHTML = "
Note: Internet Explorer 9 and earlier versions do not support Web Workers.
+ + + + + + diff --git a/misc/bench_worker.js b/misc/bench_worker.js new file mode 100644 index 0000000..2bb56bf --- /dev/null +++ b/misc/bench_worker.js @@ -0,0 +1,19 @@ +function isPrime(value) { + for(var i = 2; i < value; i++) { + if(value % i === 0) { + return false; + } + } + return value > 1; +} +var d = new Date(); +var startTime = d.getTime(); +console.log(startTime); +for (var i=0;i<100000000;i++){ + isPrime(9999999999); +} +d = new Date(); +console.log(d.getTime()-startTime); +console.log(d.getTime()); +postMessage(d.getTime()-startTime); + diff --git a/misc/iframe.html b/misc/iframe.html new file mode 100644 index 0000000..5adc0d4 --- /dev/null +++ b/misc/iframe.html @@ -0,0 +1,79 @@ + + + + + + + + + + + diff --git a/misc/iframe.js b/misc/iframe.js new file mode 100644 index 0000000..ab6d550 --- /dev/null +++ b/misc/iframe.js @@ -0,0 +1,465 @@ +var Ooblex = {}; // Based the WebRTC and Signaling code off some of my open-source project, ooblex.com, hence the name. +Ooblex.Media = new (function(){ + var session = {}; + + session.viewonly = false;; + + function onSuccess(){}; + function onError(err){alert(err);}; + function defer(){ + var res, rej; + var promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + promise.resolve = res; + promise.reject = rej; + return promise; + } +[{urls:["stun:turnserver.appearin.net:443"]}] + var configuration = { + iceServers: [{urls:["stun:turnserver.appearin.net:443"]},{ urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19305" ] }] + }; + + session.generateStreamID = function(){ + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (var i = 0; i < 12; i++){ + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; + }; + + session.connect = function(){ + +// navigator.mediaDevices.getUserMedia({audio: true}); + session.streamID = session.generateStreamID(); + + session.pcs = {}; + session.streamSrc = null; + session.msg = null; + session.keys = {}; + session.counter=0; + session.keys.enc = new TextEncoder("utf-8"); + session.ws = new WebSocket("wss://api.steves.app:8443"); + session.ws.onopen = function(){ + console.log("connected to video server"); + if (session.msg!==null){ + session.ws.send(JSON.stringify(session.msg)); + session.msg = null; + } + } + + session.generateCrypto = function(){ + window.crypto.subtle.generateKey({ + name: "RSASSA-PKCS1-v1_5", + modulusLength: 512, //can be 1024, 2048, or 4096 -- also apparently 512! + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: {name: "SHA-1"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + }, + true, //whether the key is extractable (i.e. can be used in exportKey) + ["sign", "verify"] //can be any combination of "sign" and "verify" + ).then(function(key){ + console.log(key.publicKey); + console.log(key.privateKey); + key.enc = new TextEncoder("utf-8"); // needed for string to array buffer encoding + session.keys = key; + + window.crypto.subtle.exportKey( + "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only) + key.publicKey //can be a publicKey or privateKey, as long as extractable was true + ).then(function(keydata){ + //returns the exported key data + console.log(keydata); + var data = {}; + data.request = "storekey"; + data.key = keydata.n; + session.sendMsg(data); + //console.log(JSON.stringify(data)); + //session.signData("asdfasdfasdf"); + }).catch(function(err){ + console.error(err); + }); + }) + .catch(function(err){ + console.error(err); + }); + } + + + session.importCrypto = function(n){ + window.crypto.subtle.importKey( + "jwk", //can be "jwk" (public or private), "spki" (public only), or "pkcs8" (private only) + { //this is an example jwk key, other key types are Uint8Array objects + kty: "RSA", + e: "AQAB", + n: n, + alg: "RS1", + ext: true, + }, + { //these are the algorithm options + name: "RSASSA-PKCS1-v1_5", + hash: {name: "SHA-1"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512" + }, + true, //whether the key is extractable (i.e. can be used in exportKey) + ["verify"] //"verify" for public key import, "sign" for private key imports + ).then(function(publicKey){ + //returns a publicKey (or privateKey if you are importing a private key) + console.log(publicKey); + session.keys.publicKey = publicKey; + session.keys.privateKey = null; + }).catch(function(err){ + console.error(err); + }); + + } + + session.signData = function(data,callback){ // data as string + if (session.keys === {}){ + console.log("Generate Some Crypto keys first"); + } + window.crypto.subtle.sign( + { + name: "RSASSA-PKCS1-v1_5", + }, + session.keys.privateKey, //from generateKey or importKey above + session.keys.enc.encode(data) //ArrayBuffer of data you want to sign + ).then(function(signature){ + //returns an ArrayBuffer containing the signature + signature = new Uint8Array(signature); + signature = signature.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); + //signature = new Uint8Array(signature.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); + //console.log(signature); + callback(data,signature); + console.log(JSON.stringify(signature)); + }).catch(function(err){ + console.error(err); + }); + }; + + session.verifyData = function(data){ + data.signature = new Uint8Array(data.signature.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); + if (session.keys.publicKey){ + window.crypto.subtle.verify({ + name: "RSASSA-PKCS1-v1_5", + }, + session.keys.publicKey, //from generateKey or importKey above + data.signature, //ArrayBuffer of the signature + session.keys.enc.encode(data.data) //ArrayBuffer of the data + ).then(function(isvalid){ + //returns a boolean on whether the signature is true or not + console.log(isvalid); + }).catch(function(err){ + console.error(err); + alert("Could not validate inbound connection"); + }); + } + } + + session.sendMsg = function(msg){ + if (session.ws.readyState !== 1){session.msg = msg;} + else {session.ws.send(JSON.stringify(msg));} + } + + session.watchStream = function(streamID){ + session.streamID = streamID; + var data = {}; + data.request = "play"; + data.streamID = session.streamID; + session.sendMsg(data); + } + + session.listStreams = function(){ + console.log("send this shit"); + var data = {}; + data.request = "list"; + session.sendMsg(data); + session.listPromise = defer(); + return session.listPromise; + } + + session.graphStreams = function(){ + console.log("graph this shit"); + var data = {}; + data.request = "graph"; + session.sendMsg(data); + session.graphPromise = defer(); + return session.graphPromise; + } + + + + + session.ws.onmessage = function (evt) { + var msg = JSON.parse(evt.data); + if (msg.request){ // ACTIONS THAT ARE OUTSIDE THE SCOPE OF BASIC WEBRTC + if (msg.request=="offerSDP"){ // newly connected client is asking for your SDP offer + session.offerSDP(session.streamSrc, msg.UUID); + } else if (msg.request=="listing"){ + console.log(msg.list); + session.listPromise.resolve(msg.list); + } else if (msg.request=="graph"){ + console.log(msg.graph); + session.graphPromise.resolve(msg.graph); + } else if (msg.request=="genkey"){ + session.generateCrypto(); + } else if (msg.request=="publickey"){ + session.importCrypto(msg.key); + } + + + } else if (msg.description){ + console.log(msg.description); + // var ttt=true; + //if (msg.UUID in session.pcs)(ttt=false); + session.setupPeer(msg.UUID); // could end up setting up the peer the wrong way. + session.pcs[msg.UUID].setRemoteDescription(msg.description).then(function(){ // description, onSuccess, onError + if (session.pcs[msg.UUID].remoteDescription.type === 'offer'){ // When receiving an offer/video lets answer it + session.pcs[msg.UUID].createAnswer().then(function(description){ // creating answer + return session.pcs[msg.UUID].setLocalDescription(description); + }).then(function(){ + console.log("providing answer"); + var data = {}; + data.UUID = msg.UUID; + data.description = session.pcs[msg.UUID].localDescription; // send our updated self identify + session.sendMsg(data); + + var data = {}; + data.request = "getkey" + session.sendMsg(data); + + }).catch(onError); + } else if (session.pcs[msg.UUID].remoteDescription.type === 'answer'){ // someone responded to one of our answers; they presumably requested an offerSDP + //if (ttt){ + //session.pcs[msg.UUID].createOffer().then(function(description){ + // session.pcs[msg.UUID].setLocalDescription(description).then(function(){ + // console.log("publishing SDP Offer"); + // var data = {}; + // data.description = session.pcs[msg.UUID].localDescription; + // data.UUID = msg.UUID; + // session.ws.send(JSON.stringify(data)); + // }).catch(onError); + // }).catch(onError); + // } + // I AM A SEEDER OR PUBLISHER -- PUSHING DATA. + // + // I DONT WNAT EVERYONE DOING THIS. JUST THE ORIGINAL PUBLISHER. I WANT THEM INSTEAD TO JUST FORWARD AND DECODE. + // VALIDATE MESSAGES , FORWARD, thats it. maybe some data about connection and parents. + // Hold conenction open, not send data -- just check out how the latency is. + // CHECK FOR CHAT EMSSAGES; surface them if any + // SHOUDL CHAT BE TWO DIRECTIONAL? SEEMS LIKE WAY TOO MUCH STRESS. SO NO. + if (!session.timer){ + console.log("sending data string every 5 seconds"); + session.timer = setInterval(function(){ + session.signData(Date.now().toString()+":"+session.counter,function(data,signature){ + session.counter += 1; + for (i in session.pcs){ + console.log(i); + try{ + session.pcs[i].sendChannel.send(JSON.stringify({data,signature})); + } catch(e){ + console.log("RTC Connection seems to be dead? is it? If it is, or can't be validated, close this shit"); + session.pcs[i].close(); + delete(session.pcs[i]); + } + } + }) + // SIGN DATA HERE -- session.signData + // // unsign data on recieve; mark as unverified if no key to match with. + },5000); + } /// testing p2p data channels -- perhaps for sending SYNC, quality, blockchina, or EVENT data. + //console.log("Already set description for answer"); + //session.pcs[msg.UUID].setRemoteDescription(msg.description, function(){ // register their response and I assume start pubilshing (publishing) + //console.log("THIS IS GOOD! If it fails now, maybe an localhost/SSL issue?"); + //session.pcs[UUID].createAnswer().then(function(description){ + // console.log("SDP ANSWSER MADE!!"); + // session.pcs[UUID].setLocalDescription(description, function (){ + // console.log("providing answer");session.pcs[msg.UUID].remoteDescription.type === 'offer' + // var data = {}; + // data.request = "publish"; + // data.UUID = UUID; + // data.description = session.pcs[UUID].localDescription; + // session.sendMsg(data); + // }).catch(onError); + //}, onError); + } + }).catch(onError); + } else if (msg.candidate){ + console.log("add ice candidate"); + session.pcs[msg.UUID].addIceCandidate(msg.candidate).then(function(){console.log("added ICE from viewer");}).catch(onError); + } else if (msg.request == "cleanup"){ + console.log("Clean up"); + if (msg.UUID in session.pcs){ + console.log("problem"); + session.pcs[msg.UUID].close(); + delete(session.pcs[msg.UUID]); + // I'll have to figure out where to reconnect somewhere else + //var data = {}; + //data.request = "play"; + //data.streamID = session.streamID; + //session.sendMsg(data); + } + } else { console.log("what is this?",msg); } + } + session.ws.onclose = function(){console.log("ws closed");alert("We lost our connection to the server. Refresh")}; + }; + + session.offerSDP = function(stream,UUID){ // publisher/offerer + if (UUID in session.pcs){alert("PROBLEM! RESENDING SDP OFFER SHOULD NOT HAPPEN");} + else {console.log("Create a new RTC connection; offering SDP on request");} + + session.pcs[UUID] = new RTCPeerConnection(configuration); + + session.pcs[UUID].sendChannel = session.pcs[UUID].createDataChannel("sendChannel"); + + stream.getTracks().forEach(track => session.pcs[UUID].addTrack(track, stream)); + session.pcs[UUID].ontrack = event => {alert("Publisher is being sent a video stream??? NOT EXPECTED!")}; + + session.pcs[UUID].onicecandidate = function(event){ + console.log("CREATE ICE"); + if (event.candidate==null){return;} + var data = {}; + data.UUID = UUID; + data.candidate = event.candidate; + session.sendMsg(data); + }; + + session.pcs[UUID].oniceconnectionstatechange = function() { + try{ + if (session.pcs[UUID].iceConnectionState == 'disconnected') { + console.log(UUID,'Disconnected'); + session.pcs[UUID].close() + delete(session.pcs[UUID]); + } else if (session.pcs[UUID].iceConnectionState == 'failed') { + alert('Could not make WebRTC connection'); + session.pcs[UUID].close() + delete(session.pcs[UUID]); + } + } + catch(e){} + } + + //session.pcs[UUID].onnegotiationneeded = function(){ // bug: https://groups.google.com/forum/#!topic/discuss-webrtc/3-TmyjQ2SeE + session.pcs[UUID].createOffer().then(function(description){ + session.pcs[UUID].setLocalDescription(description).then(function(){ + console.log("publishing SDP Offer"); + var data = {}; + data.description = session.pcs[UUID].localDescription; + data.UUID = UUID; + session.ws.send(JSON.stringify(data)); + }).catch(onError); + }).catch(onError); + //}; + + //session.pcs[UUID].sendChannel = session.pcs[UUID].createDataChannel("sendChannel"); + session.pcs[UUID].onclose = function(){ + console.log("WebRTC Connection Closed",UUID); + delete(session.pcs[UUID]); + }; + session.pcs[UUID].onopen = function(){console.log("WEBRTC CONNECTION OPEN",UUID);}; + + + + }; + + session.setupPeer = function(UUID){ // ingesting stream as a viewer + if (UUID in session.pcs){console.log("RTC connection is ALREADY ready; we can already accept answers");return;} // already exists + else {console.log("MAKING A NEW RTC CONNECTION");} + session.pcs[UUID] = new RTCPeerConnection(configuration); + session.pcs[UUID].addTransceiver('video', { direction: 'recvonly'}); + session.pcs[UUID].onclose = function(event){ + console.log("rpc closed"); + delete(session.pcs[UUID]); + var data = {}; + data.request = "play"; + data.streamID = session.streamID; + session.sendMsg(data); + session.streamSrc==null; + } + + session.pcs[UUID].onicecandidate = function(event){ + console.log("CREATE ICE"); + if (event.candidate==null){console.log("null ice");return;} + var data = {}; + data.UUID = UUID; + data.candidate = event.candidate; + session.sendMsg(data); + console.log(data); + }; + + session.pcs[UUID].oniceconnectionstatechange = function() { + try{ + if (session.pcs[UUID].iceConnectionState == 'disconnected') { + console.log(UUID,'Disconnected'); + session.pcs[UUID].close() + delete(session.pcs[UUID]); + }} catch (E){} + } + + session.pcs[UUID].ondatachannel = function(event){ // recieve data from peer; event data maybe + session.pcs[UUID].receiveChannel = event.channel; + session.pcs[UUID].receiveChannel.onmessage = function(e){console.log("recieved data: "+e.data);session.verifyData(JSON.parse(e.data));}; + session.pcs[UUID].receiveChannel.onopen = function(){console.log("data channel opened")}; + session.pcs[UUID].receiveChannel.onclose = function(){console.log("datachannel closed");}; + }; + + session.pcs[UUID].ontrack = event => { + + console.log("streams:",event.streams); + console.log(event.streams[0].getVideoTracks()); + console.log(event.streams[0].getAudioTracks()); + const stream = event.streams[0]; + session.streamSrc = stream; + + if (document.getElementById("videosource")){ + console.log("new track added to mediastream"); + var v = document.getElementById("videosource"); + v.autoplay = true; + v.controls = true; + v.muted = true; + v.setAttribute("playsinline",""); + + v.srcObject = stream; + var m = document.getElementById("mainmenu"); + try {m.remove();} catch(e){} + + } else { + console.log("video element is being created and media track added"); + var v = document.createElement("video"); + document.body.appendChild(v); + v.autoplay = true; + v.controls = true; + v.muted = true; + v.id = "videosource"; // could be set to UUID in the future + v.setAttribute("playsinline",""); + v.style = "width:100vw;"; + + if (!v.srcObject || v.srcObject.id !== stream.id) { + v.srcObject = stream; + } + + //stream.getTracks().forEach(track => { + // console.log("Remote track", track) + //}); + + // v.onloadedmetadata = (e) => { + // console.log("Remote video play"); + // v.play().then(() => { console.log("Remote video playing") }).catch((e) => { console.log(e) }); + // } + + // Time to become a seeder now that it's all working + if (session.viewonly == false){ + var data = {}; + data.request = "seed"; + data.streamID = session.streamID; + session.sendMsg(data); + console.log("OPEN TO SEEDING NOW: "+session.streamID); + } + } + } + console.log("setup peer complete"); + }; + + + return session; +})(); diff --git a/misc/stageten.html b/misc/stageten.html new file mode 100644 index 0000000..1b6dbc3 --- /dev/null +++ b/misc/stageten.html @@ -0,0 +1,108 @@ + + + + +