mirror of
https://github.com/eliasstepanik/vdo.ninja.git
synced 2026-01-11 13:48:38 +00:00
Add files via upload
part 1
This commit is contained in:
parent
23ccdea1b4
commit
9ed7ab3c44
55
codecs.html
Normal file
55
codecs.html
Normal file
@ -0,0 +1,55 @@
|
||||
<html>
|
||||
<body>
|
||||
<div id="output"></div>
|
||||
<script>
|
||||
|
||||
function getSupportedMimeTypes(media, types, codecs) {
|
||||
const isSupported = MediaRecorder.isTypeSupported;
|
||||
const supported = [];
|
||||
types.forEach((type) => {
|
||||
const mimeType = `${media}/${type}`;
|
||||
acodecs.forEach((codec2) => {
|
||||
codecs.forEach((codec) => [
|
||||
mimeType+';codecs="'+codec+', '+codec2+'"',
|
||||
mimeType+';codecs:"'+codec+', '+codec2+'"',
|
||||
mimeType+';codecs="'+codec.toUpperCase()+', '+codec2.toUpperCase()+'"',
|
||||
mimeType+';codecs:"'+codec.toUpperCase()+', '+codec2.toUpperCase()+'"'
|
||||
].forEach(variation => {
|
||||
if (isSupported(variation)){
|
||||
supported.push(variation);
|
||||
}
|
||||
}));
|
||||
});
|
||||
if (isSupported(mimeType))
|
||||
supported.push(mimeType);
|
||||
});
|
||||
return supported;
|
||||
};
|
||||
|
||||
// Usage ------------------
|
||||
|
||||
const videoTypes = ["webm", "ogg", "mp4", "x-matroska"];
|
||||
const audioTypes = ["webm", "ogg", "mp3", "x-matroska"];
|
||||
const codecs = ["vp9", "vp9.0", "vp8", "vp8.0", "avc1", "av1", "h265", "h.265", "h264", "h.264"]
|
||||
const acodecs = ["opus"];
|
||||
|
||||
const supportedVideos = getSupportedMimeTypes("video", videoTypes, codecs);
|
||||
const supportedAudios = getSupportedMimeTypes("audio", audioTypes, codecs);
|
||||
|
||||
console.log('-- Top supported Video : ', supportedVideos[0])
|
||||
console.log('-- Top supported Audio : ', supportedAudios[0])
|
||||
console.log('-- All supported Videos : ', supportedVideos)
|
||||
console.log('-- All supported Audios : ', supportedAudios)
|
||||
|
||||
document.getElementById("output").innerHTML= "";
|
||||
|
||||
for (var i=0;i<supportedVideos.length;i++){
|
||||
document.getElementById("output").innerHTML += supportedVideos[i]+"<br/>";
|
||||
}
|
||||
//for (var i=0;i<supportedAudios.length;i++){
|
||||
//document.getElementById("output").innerHTML += supportedAudios[i]+"<br/>";
|
||||
//}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -548,6 +548,7 @@ function addUrlToHistory(url){
|
||||
|
||||
function modURL(){
|
||||
var url = document.getElementById('changeText').value;
|
||||
url = url.trim();
|
||||
if (url.startsWith("obs.ninja")){
|
||||
url = "https://"+url;
|
||||
} else if (url.startsWith("youtube.com")){
|
||||
|
||||
14
index.html
14
index.html
@ -66,8 +66,8 @@
|
||||
<span itemprop="thumbnail" itemscope itemtype="http://schema.org/ImageObject">
|
||||
<link itemprop="url" href="./media/vdoNinja_logo_full.png" />
|
||||
</span>
|
||||
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=34"></script>
|
||||
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=334"></script>
|
||||
<script type="text/javascript" crossorigin="anonymous" src="./thirdparty/CodecsHandler.js?ver=35"></script>
|
||||
<script type="text/javascript" crossorigin="anonymous" src="./webrtc.js?ver=336"></script>
|
||||
<input id="zoomSlider" type="range" style="display: none;" />
|
||||
<div id="header">
|
||||
|
||||
@ -745,7 +745,9 @@
|
||||
<li>
|
||||
Chrome v95 with an AMD GPUs have an issue hardware encoding H264 video; this will be fixed in Chrome v96. <a href='https://bugs.chromium.org/p/chromium/issues/detail?id=1252710' target="_blank">Details here</a>.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
Samsung smartphones (A-series) may fail to publish video with some mobile browsers; try using Firefox or the native <a href='https://docs.vdo.ninja/getting-started/native-mobile-app-versions#android-download-link'>VDO.Ninja Android app</a> in these cases.
|
||||
</li>
|
||||
<br />
|
||||
<h4>
|
||||
<font style="color:#daad09;">Welcome to VDO Ninja! We've rebranded! Nothing else is changing and we're staying 100% free.</font>
|
||||
@ -1804,7 +1806,7 @@
|
||||
|
||||
|
||||
var session = WebRTC.Media; // session is a required global variable if configuring manually. Run before loading main.js but after webrtc.js.
|
||||
session.version = "19.4";
|
||||
session.version = "20.0-beta";
|
||||
session.streamID = session.generateStreamID(); // randomly generates a streamID for this session. You can set your own programmatically if needed
|
||||
|
||||
session.defaultPassword = "someEncryptionKey123"; // Change this password if self-deploying for added security/privacy
|
||||
@ -1875,11 +1877,11 @@
|
||||
// session.introOnClean = true; // this will load the page with the webcam selection screen if &push or &room is in the URL; no need to use &webcam.
|
||||
</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=241"></script>
|
||||
<script type="text/javascript" crossorigin="anonymous" id="lib-js" src="./lib.js?ver=242"></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=283"></script>
|
||||
<script type="text/javascript" crossorigin="anonymous" id="main-js" src="./main.js?ver=284"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
314
lib.js
314
lib.js
@ -74,7 +74,8 @@ var miscTranslations = {
|
||||
"enter-new-codirector-password": "Enter a co-director password to use",
|
||||
"control-room-co-director": "Control Room: Co-Director",
|
||||
"signal-meter": "Video packet loss indicator of video preview; green is good, red is bad. Flame implies CPU is overloaded. May not reflect the packet loss seen by scenes or other guests.",
|
||||
"waiting-for-the-stream": "Waiting for the stream. Tip: Adding &cleanoutput to the URL will hide this spinner, or click to retry, which will also hide it."
|
||||
"waiting-for-the-stream": "Waiting for the stream. Tip: Adding &cleanoutput to the URL will hide this spinner, or click to retry, which will also hide it.",
|
||||
"main-director": "Main Director"
|
||||
};
|
||||
|
||||
// function log(msg){ // uncomment to enable logging.
|
||||
@ -1592,7 +1593,6 @@ function setupIncomingVideoTracking(v, UUID){ // video element.
|
||||
v.usermuted = false;
|
||||
|
||||
v.addEventListener('volumechange',function(e){
|
||||
console.warn("volume changed");
|
||||
var muteState = checkMuteState(UUID);
|
||||
if (this.muted && (this.muted !== muteState)){
|
||||
this.usermuted = true;
|
||||
@ -1696,6 +1696,9 @@ function updateMixerRun(e=false){ // this is the main auto-mixing code. It's a
|
||||
} else if (session.aspectratio==2){
|
||||
arW = 12.0; // square root; cause why not.
|
||||
arH = 12.0;
|
||||
} else if (session.aspectratio==3){
|
||||
arW = 12.0; // square root; cause why not.
|
||||
arH = 9.0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3051,7 +3054,7 @@ function updateMixerRun(e=false){ // this is the main auto-mixing code. It's a
|
||||
if (session.signalMeter){
|
||||
if (vid.dataset.UUID && !session.rpcs[vid.dataset.UUID].signalMeter){
|
||||
session.rpcs[vid.dataset.UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true);
|
||||
session.rpcs[vid.dataset.UUID].signalMeter.style.display = "block";
|
||||
//session.rpcs[vid.dataset.UUID].signalMeter.style.display = "block";
|
||||
session.rpcs[vid.dataset.UUID].signalMeter.id = "signalMeter_" + vid.dataset.UUID;
|
||||
session.rpcs[vid.dataset.UUID].signalMeter.dataset.level = 0;
|
||||
session.rpcs[vid.dataset.UUID].signalMeter.title = miscTranslations["signal-meter"];
|
||||
@ -4734,6 +4737,11 @@ function printMyStats(menu) { // see: setupStatsMenu
|
||||
}
|
||||
printViewValues(session.stats);
|
||||
menu.innerHTML += "<button onclick='session.forcePLI(null,event);' data-translate='send-keyframe-to-viewer'>Send Keyframe to Viewers</button>";
|
||||
|
||||
if (session.mc){
|
||||
printViewValues(session.mc.stats);
|
||||
menu.innerHTML += "<hr>";
|
||||
}
|
||||
for (var uuid in session.pcs) {
|
||||
printViewValues(session.pcs[uuid].stats);
|
||||
menu.innerHTML += "<hr>";
|
||||
@ -4748,6 +4756,10 @@ function printMyStats(menu) { // see: setupStatsMenu
|
||||
}
|
||||
|
||||
|
||||
function publisherMeshcastStats(){
|
||||
|
||||
}
|
||||
|
||||
function updateLocalStats(){
|
||||
|
||||
var totalBitrate = 0;
|
||||
@ -4757,6 +4769,223 @@ function updateLocalStats(){
|
||||
var totalVideo = 0;
|
||||
var totalAudio = 0;
|
||||
var totalScenes = 0;
|
||||
|
||||
|
||||
if (session.mc){
|
||||
try {
|
||||
var atot = 0;
|
||||
var senders = session.mc.getSenders(); // for any connected peer, update the video they have if connected with a video already.
|
||||
senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
|
||||
if (sender.track && sender.track.kind == "video" && sender.track.enabled) {
|
||||
totalVideo+=1
|
||||
} else if (sender.track && sender.track.kind == "audio" && sender.track.enabled && !session.muted) {
|
||||
atot=1;
|
||||
}
|
||||
});
|
||||
totalAudio += atot;
|
||||
|
||||
if ("video_bitrate_kbps" in session.mc.stats){
|
||||
totalBitrate+=session.mc.stats.video_bitrate_kbps || 0;
|
||||
}
|
||||
if ("audio_bitrate_kbps" in session.mc.stats){
|
||||
totalBitrate+=session.mc.stats.audio_bitrate_kbps || 0;
|
||||
}
|
||||
if ("total_sending_bitrate_kbps" in session.mc.stats){
|
||||
totalBitrate2+=session.mc.stats.total_sending_bitrate_kbps || 0;
|
||||
}
|
||||
|
||||
if ("quality_limitation_reason" in session.mc.stats){
|
||||
if (session.mc.stats.quality_limitation_reason == "cpu"){
|
||||
cpuLimited=true;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(function(){
|
||||
|
||||
if (!session.mc){return;}
|
||||
session.mc.getStats().then(function(stats) {
|
||||
if ("audio_bitrate_kbps" in session.mc.stats){
|
||||
session.mc.stats.audio_bitrate_kbps=0;
|
||||
}
|
||||
stats.forEach(stat => {
|
||||
if (stat.type == "transport"){
|
||||
if ("bytesSent" in stat) {
|
||||
if ("_bytesSent" in session.mc.stats){
|
||||
if (session.mc.stats._timestamp){
|
||||
if (stat.timestamp){
|
||||
session.mc.stats.total_sending_bitrate_kbps = parseInt(8*(stat.bytesSent - session.mc.stats._bytesSent)/(stat.timestamp - session.mc.stats._timestamp));
|
||||
}
|
||||
}
|
||||
}
|
||||
session.mc.stats._bytesSent = stat.bytesSent;
|
||||
}
|
||||
if ("timestamp" in stat) {
|
||||
session.mc.stats._timestamp = stat.timestamp;
|
||||
}
|
||||
} else if (stat.type == "outbound-rtp") {
|
||||
if (stat.kind == "video") {
|
||||
|
||||
if ("framesPerSecond" in stat) {
|
||||
session.mc.stats.resolution = stat.frameWidth + " x " + stat.frameHeight + " @ " + stat.framesPerSecond;
|
||||
}
|
||||
if ("encoderImplementation" in stat) {
|
||||
session.mc.stats.video_encoder = stat.encoderImplementation;
|
||||
if (stat.encoderImplementation=="ExternalEncoder"){
|
||||
session.mc.stats._hardwareEncoder = true; // I won't set this to false again, just because once I know it has one, I just need to assume it could always be used unexpectednly
|
||||
session.mc.encoder = true;
|
||||
|
||||
} else {
|
||||
session.mc.encoder = false; // this may not be actually accurate, but lets assume so.
|
||||
}
|
||||
}
|
||||
if ("qualityLimitationReason" in stat) {
|
||||
if (session.mc.stats.quality_limitation_reason){
|
||||
if (session.mc.stats.quality_limitation_reason !== stat.qualityLimitationReason){
|
||||
try{
|
||||
var miniInfo = {};
|
||||
miniInfo.qlr = stat.qualityLimitationReason;
|
||||
if ("_hardwareEncoder" in session.mc.stats){
|
||||
miniInfo.hw_enc = session.mc.stats._hardwareEncoder;
|
||||
} else {
|
||||
miniInfo.hw_enc = null;
|
||||
}
|
||||
session.sendMessage({"miniInfo":miniInfo});
|
||||
} catch(e){warnlog(e);}
|
||||
}
|
||||
}
|
||||
session.mc.stats.quality_limitation_reason = stat.qualityLimitationReason;
|
||||
}
|
||||
|
||||
if ("bytesSent" in stat) {
|
||||
if ("_bytesSentVideo" in session.mc.stats){
|
||||
if (session.mc.stats._timestamp1){
|
||||
session.mc.stats.video_bitrate_kbps = parseInt(8*(stat.bytesSent - session.mc.stats._bytesSentVideo)/(stat.timestamp - session.mc.stats._timestamp1));
|
||||
if (stat.timestamp){
|
||||
}
|
||||
}
|
||||
}
|
||||
session.mc.stats._bytesSentVideo = stat.bytesSent;
|
||||
}
|
||||
|
||||
if ("nackCount" in stat) {
|
||||
if ("_nackCount" in session.mc.stats){
|
||||
if (session.mc.stats._timestamp1){
|
||||
if (stat.timestamp){
|
||||
session.mc.stats.nacks_per_second = parseInt(10000*(stat.nackCount - session.mc.stats._nackCount)/(stat.timestamp - session.mc.stats._timestamp1))/10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("retransmittedBytesSent" in stat) {
|
||||
if ("_retransmittedBytesSent" in session.mc.stats){
|
||||
if (session.mc.stats._timestamp1){
|
||||
if (stat.timestamp){
|
||||
session.mc.stats.retransmitted_kbps = parseInt(8*(stat.retransmittedBytesSent - session.mc.stats._retransmittedBytesSent)/(stat.timestamp - session.mc.stats._timestamp1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("nackCount" in stat) {
|
||||
session.mc.stats._nackCount = stat.nackCount;
|
||||
}
|
||||
|
||||
if ("retransmittedBytesSent" in stat) {
|
||||
session.mc.stats._retransmittedBytesSent = stat.retransmittedBytesSent;
|
||||
|
||||
}
|
||||
|
||||
if ("timestamp" in stat) {
|
||||
session.mc.stats._timestamp1 = stat.timestamp;
|
||||
}
|
||||
|
||||
if ("pliCount" in stat) {
|
||||
session.mc.stats.total_pli_count = stat.pliCount;
|
||||
}
|
||||
if ("keyFramesEncoded" in stat) {
|
||||
session.mc.stats.total_key_frames_encoded = stat.keyFramesEncoded;
|
||||
}
|
||||
|
||||
|
||||
} else if (stat.kind == "audio") {
|
||||
if ("bytesSent" in stat) {
|
||||
if (session.mc.stats._bytesSentAudio){
|
||||
if (session.mc.stats._timestamp2){
|
||||
if (stat.timestamp){
|
||||
if ("audio_bitrate_kbps" in session.mc.stats){
|
||||
session.mc.stats.audio_bitrate_kbps += parseInt(8*(stat.bytesSent - session.mc.stats._bytesSentAudio)/(stat.timestamp - session.mc.stats._timestamp2));
|
||||
} else {
|
||||
session.mc.stats.audio_bitrate_kbps=0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("timestamp" in stat) {
|
||||
session.mc.stats._timestamp2 = stat.timestamp;
|
||||
}
|
||||
|
||||
if ("bytesSent" in stat) {
|
||||
session.mc.stats._bytesSentAudio = stat.bytesSent;
|
||||
|
||||
}
|
||||
}
|
||||
} else if (stat.type == "remote-candidate") {
|
||||
|
||||
if ("candidateType" in stat) {
|
||||
session.mc.stats.remote_candidateType = stat.candidateType;
|
||||
if (stat.candidateType === "relay"){
|
||||
if ("ip" in stat) {
|
||||
session.mc.stats.remote_relay_IP = stat.ip;
|
||||
}
|
||||
if ("relayProtocol" in stat) {
|
||||
session.mc.stats.remote_relayProtocol = stat.relayProtocol;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
delete session.mc.stats.remote_relay_IP;
|
||||
delete session.mc.stats.remote_relayProtocol;
|
||||
} catch(e){}
|
||||
}
|
||||
}
|
||||
} else if (stat.type == "local-candidate") {
|
||||
if ("candidateType" in stat) {
|
||||
session.mc.stats.local_candidateType = stat.candidateType;
|
||||
|
||||
if (stat.candidateType === "relay"){
|
||||
if ("ip" in stat) {
|
||||
session.mc.stats.local_relayIP = stat.ip;
|
||||
}
|
||||
if ("relayProtocol" in stat) {
|
||||
session.mc.stats.local_relayProtocol = stat.relayProtocol;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
delete session.mc.stats.local_relayIP;
|
||||
delete session.mc.stats.local_relayProtocol;
|
||||
} catch(e){}
|
||||
}
|
||||
|
||||
}
|
||||
} else if ((stat.type == "candidate-pair" ) && (stat.nominated)) {
|
||||
|
||||
if ("availableOutgoingBitrate" in stat){
|
||||
session.mc.stats.available_outgoing_bitrate_kbps = parseInt(stat.availableOutgoingBitrate/1024);
|
||||
}
|
||||
if ("totalRoundTripTime" in stat){
|
||||
if ("responsesReceived" in stat){
|
||||
session.mc.stats.average_roundTripTime_ms = parseInt((stat.totalRoundTripTime/stat.responsesReceived)*1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
return;
|
||||
});
|
||||
}, 0);
|
||||
} catch(e){errorlog(e);}
|
||||
}
|
||||
|
||||
for (var uuid in session.pcs) {
|
||||
var atot = 0;
|
||||
var senders = session.pcs[uuid].getSenders(); // for any connected peer, update the video they have if connected with a video already.
|
||||
@ -4933,23 +5162,42 @@ function updateLocalStats(){
|
||||
}
|
||||
} else if (stat.type == "remote-candidate") {
|
||||
if ("relayProtocol" in stat) {
|
||||
if ("ip" in stat) {
|
||||
session.pcs[UUID].stats.remote_relay_IP = stat.ip;
|
||||
}
|
||||
session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol;
|
||||
|
||||
}
|
||||
if ("candidateType" in stat) {
|
||||
session.pcs[UUID].stats.remote_candidateType = stat.candidateType;
|
||||
if (stat.candidateType === "relay"){
|
||||
if ("ip" in stat) {
|
||||
session.pcs[UUID].stats.remote_relay_IP = stat.ip;
|
||||
}
|
||||
if ("relayProtocol" in stat) {
|
||||
session.pcs[UUID].stats.remote_relayProtocol = stat.relayProtocol;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
delete session.pcs[UUID].stats.remote_relay_IP;
|
||||
delete session.pcs[UUID].stats.remote_relayProtocol;
|
||||
} catch(e){}
|
||||
}
|
||||
}
|
||||
} else if (stat.type == "local-candidate") {
|
||||
if ("relayProtocol" in stat) {
|
||||
if ("candidateType" in stat) {
|
||||
session.pcs[UUID].stats.local_candidateType = stat.candidateType;
|
||||
|
||||
if (stat.candidateType === "relay"){
|
||||
if ("ip" in stat) {
|
||||
session.pcs[UUID].stats.local_relayIP = stat.ip;
|
||||
}
|
||||
if ("relayProtocol" in stat) {
|
||||
session.pcs[UUID].stats.local_relayProtocol = stat.relayProtocol;
|
||||
}
|
||||
if ("candidateType" in stat) {
|
||||
session.pcs[UUID].stats.local_candidateType = stat.candidateType;
|
||||
} else {
|
||||
try {
|
||||
delete session.pcs[UUID].stats.local_relayIP;
|
||||
delete session.pcs[UUID].stats.local_relayProtocol;
|
||||
} catch(e){}
|
||||
}
|
||||
|
||||
}
|
||||
} else if ((stat.type == "candidate-pair" ) && (stat.nominated)) {
|
||||
|
||||
@ -5018,7 +5266,7 @@ function updateStats(obsvc = false) {
|
||||
} else {
|
||||
var framerateFPS = track.getSettings().frameRate;
|
||||
if (framerateFPS){
|
||||
getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + "@" + (framerateFPS * 10 / 10) + "fps";
|
||||
getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0) + "@" + (parseInt(framerateFPS * 100) / 100.0) + "fps";
|
||||
} else {
|
||||
getById(wcs).innerHTML = "Current Video Settings: " + (track.getSettings().width || 0) + "x" + (track.getSettings().height || 0);
|
||||
}
|
||||
@ -8326,7 +8574,7 @@ function createControlBox(UUID, soloLink, streamID) {
|
||||
session.rpcs[UUID].signalMeter = getById("signalMeterTemplate").cloneNode(true);
|
||||
session.rpcs[UUID].signalMeter.id = "signalMeter_" + UUID;
|
||||
session.rpcs[UUID].signalMeter.dataset.level = 0;
|
||||
session.rpcs[UUID].signalMeter.style.display = "block";
|
||||
//session.rpcs[UUID].signalMeter.style.display = "block";
|
||||
session.rpcs[UUID].signalMeter.dataset.UUID = UUID;
|
||||
session.rpcs[UUID].signalMeter.title = miscTranslations["signal-meter"];
|
||||
session.rpcs[UUID].signalMeter.addEventListener('click', function(e) { // show stats of video if double clicked
|
||||
@ -11330,7 +11578,7 @@ async function grabVideo(quality = 0, eleName = 'previewWebcam', selector = "sel
|
||||
}
|
||||
|
||||
|
||||
if ((iOS) || (iPad)) { // iOS will not work correctly at 1080p; likely a h264 codec issue.
|
||||
if ((iOS || iPad) && safariVersion()<15) { // iOS will not work correctly at 1080p; likely a h264 codec issue.
|
||||
if (quality == 0) {
|
||||
quality = 1;
|
||||
}
|
||||
@ -11338,7 +11586,7 @@ async function grabVideo(quality = 0, eleName = 'previewWebcam', selector = "sel
|
||||
|
||||
var constraints = {
|
||||
audio: false,
|
||||
video: getUserMediaVideoParams(quality, iOS)
|
||||
video: getUserMediaVideoParams(quality, (iOS || iPad))
|
||||
};
|
||||
|
||||
log("Quality selected:" + quality);
|
||||
@ -11635,8 +11883,8 @@ function updateRenderOutpipe(){ // video only.
|
||||
|
||||
toggleVideoMute(true);
|
||||
}
|
||||
if (session.meshcast){
|
||||
if (session.mc.getSenders){ // should only be 0 or 1 video sender, ever.
|
||||
|
||||
if (session.mc && session.mc.getSenders){ // should only be 0 or 1 video sender, ever.
|
||||
var added = false;
|
||||
session.mc.getSenders().forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
|
||||
if (sender.track && sender.track.kind == "video") {
|
||||
@ -11649,7 +11897,7 @@ function updateRenderOutpipe(){ // video only.
|
||||
session.mc.addTrack(track, session.videoElement.srcObject); // can't replace, so adding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (UUID in session.pcs) {
|
||||
try {
|
||||
@ -11809,8 +12057,7 @@ function senderAudioUpdate(callback=false){
|
||||
if (session.videoElement.srcObject.getAudioTracks()) {
|
||||
var tracks = session.videoElement.srcObject.getAudioTracks();
|
||||
|
||||
if (session.meshcast){
|
||||
if (session.mc.getSenders){
|
||||
if (session.mc && session.mc.getSenders){
|
||||
session.mc.getSenders().forEach((sender) => { // disable senders that aren't part of the active tracks
|
||||
var good = false;
|
||||
if (sender.track && sender.track.id && (sender.track.kind == "audio")) {
|
||||
@ -11872,7 +12119,7 @@ function senderAudioUpdate(callback=false){
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (UUID in session.pcs) {
|
||||
if (session.pcs[UUID].allowAudio == true) {
|
||||
session.pcs[UUID].getSenders().forEach((sender) => {
|
||||
@ -16250,7 +16497,19 @@ function createIframePopup() {
|
||||
}
|
||||
|
||||
if (session.screensharefps!==false){
|
||||
extras += "&maxframerate="+session.screensharefps;
|
||||
extras += "&maxframerate="+parseInt(session.screensharefps*100)/100.0;
|
||||
}
|
||||
if (session.screenshareAEC!==false){
|
||||
extras += "&aec=1";
|
||||
}
|
||||
if (session.screenshareDenoise!==false){
|
||||
extras += "&denoise=1";
|
||||
}
|
||||
if (session.screenshareAutogain!==false){
|
||||
extras += "&autogain=1";
|
||||
}
|
||||
if (session.screenshareStereo!==false){
|
||||
extras += "&stereo="+session.screenshareStereo;
|
||||
}
|
||||
|
||||
if (session.muted){
|
||||
@ -19436,6 +19695,11 @@ function midiHotkeysCommand(command, value){
|
||||
if (elements[guestslot]) {
|
||||
remoteDisplayMute(elements[guestslot]);
|
||||
}
|
||||
} else if (value == 8) {
|
||||
var elements = document.querySelectorAll('[data-action-type="force-keyframe"][data--u-u-i-d]');
|
||||
if (elements[guestslot]) {
|
||||
requestKeyframeScene(elements[guestslot]);
|
||||
}
|
||||
} else if (value == 12) {
|
||||
var elements = document.querySelectorAll('[data-action-type="addToScene"][data-scene="2"][data--u-u-i-d]');
|
||||
if (elements[guestslot]) {
|
||||
@ -19492,7 +19756,11 @@ function playbackMIDI(msg){
|
||||
if ("d" in msg){
|
||||
for (var i in WebMidi.outputs){
|
||||
try {
|
||||
if ("c" in msg){
|
||||
WebMidi.outputs[i].channels[msg.c].send(msg.d[0], [msg.d[1], msg.d[2]]);
|
||||
} else {
|
||||
WebMidi.outputs[i].send(msg.d[0], [msg.d[1], msg.d[2]]);
|
||||
}
|
||||
} catch(e){errorlog(e);}
|
||||
}
|
||||
}
|
||||
@ -19500,8 +19768,12 @@ function playbackMIDI(msg){
|
||||
try {
|
||||
var i = parseInt(session.midiIn)-1;
|
||||
if ("d" in msg){
|
||||
if ("c" in msg){
|
||||
WebMidi.outputs[i].channels[msg.c].send(msg.d[0], [msg.d[1], msg.d[2]]);
|
||||
} else {
|
||||
WebMidi.outputs[i].send(msg.d[0], [msg.d[1], msg.d[2]]);
|
||||
}
|
||||
}
|
||||
} catch(e){errorlog(e);};
|
||||
}
|
||||
if (session.midiRemote==4){
|
||||
|
||||
21
main.css
21
main.css
@ -343,6 +343,19 @@ button.white:active {
|
||||
color: #101020;
|
||||
}
|
||||
|
||||
|
||||
body.darktheme .credits {
|
||||
color: #707a93;
|
||||
}
|
||||
|
||||
body.darktheme .credits>a {
|
||||
color: #707a93;
|
||||
}
|
||||
|
||||
body.darktheme .credits>a:visited {
|
||||
color: #707a93;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
padding: 0px 6px;
|
||||
}
|
||||
@ -538,7 +551,7 @@ hr {
|
||||
top: 1px;
|
||||
background-color: #FFF2;
|
||||
font-size: 1.5em;
|
||||
display:none;
|
||||
display:block;
|
||||
z-index: 2;
|
||||
cursor: help;
|
||||
}
|
||||
@ -551,11 +564,15 @@ hr {
|
||||
.signal-meter[data-cpu="1"]>.la-signal {
|
||||
display:none;
|
||||
}
|
||||
.signal-meter[data-cpu="1"]>.la-fire {
|
||||
.signal-meter[data-cpu="1"]>.la-fire-alt {
|
||||
display:block;
|
||||
}
|
||||
.signal-meter[data-cpu="1"] {
|
||||
display:block!important;
|
||||
}
|
||||
.signal-meter[data-level="0"] {
|
||||
color:#000F;
|
||||
display:none;
|
||||
}
|
||||
.signal-meter[data-level="1"] {
|
||||
color:#FF1B01;
|
||||
|
||||
209
main.js
209
main.js
@ -357,7 +357,6 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
if (urlParams.has('rotate') ) {
|
||||
session.rotate = urlParams.get('rotate') || 90;
|
||||
session.rotate = parseInt(session.rotate);
|
||||
|
||||
}
|
||||
|
||||
if (urlParams.has('midi') || urlParams.has('hotkeys')) {
|
||||
@ -367,18 +366,33 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
|
||||
if (urlParams.has('midiremote') || urlParams.has('remotemidi')){
|
||||
if (session.director!==false){
|
||||
session.midiRemote = urlParams.get('midiremote') || urlParams.get ('remotemidi') || 4;
|
||||
session.midiRemote = parseInt(urlParams.get('midiremote')) || parseInt(urlParams.get ('remotemidi')) || 4;
|
||||
} else {
|
||||
session.midiRemote = urlParams.get('midiremote') || urlParams.get ('remotemidi') || 1;
|
||||
session.midiRemote = parseInt(urlParams.get('midiremote')) || parseInt(urlParams.get ('remotemidi')) || 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.has('midipush') || urlParams.has('midiout') || urlParams.has('mo')){
|
||||
session.midiOut = urlParams.get('midipush') || urlParams.get('midiout') || urlParams.get('mo') || true;
|
||||
session.midiOut = parseInt(urlParams.get('midipush')) || parseInt(urlParams.get('midiout')) || parseInt(urlParams.get('mo')) || true;
|
||||
}
|
||||
|
||||
if (urlParams.has('midipull') || urlParams.has('midiin') || urlParams.has('mi')){
|
||||
session.midiIn = urlParams.get('midipull') || urlParams.get('midiin') || urlParams.get('mi') || true;
|
||||
session.midiIn = parseInt(urlParams.get('midipull')) || parseInt(urlParams.get('midiin')) || parseInt(urlParams.get('mi')) || true;
|
||||
}
|
||||
|
||||
if (urlParams.has('midichannel')){
|
||||
session.midiChannel = parseInt(urlParams.get('midichannel')) || false;
|
||||
}
|
||||
if (session.midiChannel){
|
||||
session.midiChannel = parseInt(session.midiChannel);
|
||||
if (session.midiChannel>16){session.midiChannel=false;}
|
||||
if (session.midiChannel<1){session.midiChannel=false;}
|
||||
}
|
||||
if (urlParams.has('mididevice')){
|
||||
session.midiDevice = parseInt(urlParams.get('mididevice')) || false;
|
||||
}
|
||||
if (session.midiDevice){
|
||||
session.midiDevice = parseInt(session.midiDevice);
|
||||
}
|
||||
|
||||
|
||||
@ -536,6 +550,8 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
session.aspectratio = 1; // 9:16 (default of 0 is 16:9)
|
||||
} else if (urlParams.has('square') || urlParams.has('11')) {
|
||||
session.aspectratio = 2; //1:1 ?
|
||||
} else if (urlParams.has('43')) {
|
||||
session.aspectratio = 3; //1:1 ?
|
||||
}
|
||||
|
||||
if (urlParams.has('forceaspectratio') || urlParams.has('far')) {
|
||||
@ -827,6 +843,46 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.has('screensharestereo') || urlParams.has('sss') || urlParams.has('ssproaudio')) { // both peers need this enabled for HD stereo to be on. If just pub, you get no echo/noise cancellation. if just viewer, you get high bitrate mono
|
||||
log("screenshare stereo ENABLED");
|
||||
session.screenshareStereo = urlParams.get('screensharestereo') || urlParams.get('sss') || urlParams.get('ssproaudio');
|
||||
|
||||
if (session.screenshareStereo) {
|
||||
session.screenshareStereo = session.screenshareStereo.toLowerCase();
|
||||
}
|
||||
|
||||
if (session.screenshareStereo === "false") {
|
||||
session.screenshareStereo = 0;
|
||||
} else if (session.screenshareStereo === "0") {
|
||||
session.screenshareStereo = 0;
|
||||
} else if (session.screenshareStereo === "no") {
|
||||
session.screenshareStereo = 0;
|
||||
} else if (session.screenshareStereo === "off") {
|
||||
session.screenshareStereo = 0;
|
||||
} else if (session.screenshareStereo === "1") {
|
||||
session.screenshareStereo = 1;
|
||||
} else if (session.screenshareStereo === "both") {
|
||||
session.screenshareStereo = 1;
|
||||
} else if (session.screenshareStereo === "3") {
|
||||
session.screenshareStereo = 3;
|
||||
} else if (session.screenshareStereo === "out") {
|
||||
session.screenshareStereo = 3;
|
||||
} else if (session.screenshareStereo === "mono") {
|
||||
session.screenshareStereo = 3;
|
||||
} else if (session.screenshareStereo === "4") {
|
||||
session.screenshareStereo = 4;
|
||||
} else if (session.screenshareStereo === "multi") {
|
||||
session.screenshareStereo = 4;
|
||||
} else if (session.screenshareStereo === "2") {
|
||||
session.screenshareStereo = 2;
|
||||
} else if (session.screenshareStereo === "in") {
|
||||
session.screenshareStereo = 2;
|
||||
} else {
|
||||
session.screenshareStereo = 5; // guests; no stereo in, no high bitrate in, but otherwise like stereo=1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (urlParams.has('pie')){
|
||||
session.customWSS = urlParams.get('pie') || false; // If session.customWSS == true, then there is no need to set parameters via URL
|
||||
@ -924,6 +980,63 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.has("screenshareaec") || urlParams.has("ssec") || urlParams.has("ssaec")) {
|
||||
|
||||
session.screenshareAEC = urlParams.get('screenshareaec') || urlParams.get('ssec') || urlParams.get("ssaec");
|
||||
|
||||
if (session.screenshareAEC) {
|
||||
session.screenshareAEC = session.screenshareAEC.toLowerCase();
|
||||
}
|
||||
if (session.screenshareAEC == "false") {
|
||||
session.screenshareAEC = false;
|
||||
} else if (session.screenshareAEC == "0") {
|
||||
session.screenshareAEC = false;
|
||||
} else if (session.screenshareAEC == "no") {
|
||||
session.screenshareAEC = false;
|
||||
} else if (session.screenshareAEC == "off") {
|
||||
session.screenshareAEC = false;
|
||||
} else {
|
||||
session.screenshareAEC = true;
|
||||
}
|
||||
}
|
||||
if (urlParams.has("screenshareautogain") || urlParams.has("ssag") || urlParams.has("ssagc")) {
|
||||
|
||||
session.screenshareAutogain = urlParams.get('screenshareautogain') || urlParams.get('ssag') || urlParams.get('ssagc');
|
||||
if (session.screenshareAutogain) {
|
||||
session.screenshareAutogain = session.screenshareAutogain.toLowerCase();
|
||||
}
|
||||
if (session.screenshareAutogain == "false") {
|
||||
session.screenshareAutogain = false;
|
||||
} else if (session.screenshareAutogain == "0") {
|
||||
session.screenshareAutogain = false;
|
||||
} else if (session.screenshareAutogain == "no") {
|
||||
session.screenshareAutogain = false;
|
||||
} else if (session.screenshareAutogain == "off") {
|
||||
session.screenshareAutogain = false;
|
||||
} else {
|
||||
session.screenshareAutogain = true;
|
||||
}
|
||||
}
|
||||
if (urlParams.has("screensharedenoise") || urlParams.has("ssdn")) {
|
||||
|
||||
session.screenshareDenoise = urlParams.get('screensharedenoise') || urlParams.get('ssdn');
|
||||
|
||||
if (session.screenshareDenoise) {
|
||||
session.screenshareDenoise = session.screenshareDenoise.toLowerCase();
|
||||
}
|
||||
if (session.screenshareDenoise == "false") {
|
||||
session.screenshareDenoise = false;
|
||||
} else if (session.screenshareDenoise == "0") {
|
||||
session.screenshareDenoise = false;
|
||||
} else if (session.screenshareDenoise == "no") {
|
||||
session.screenshareDenoise = false;
|
||||
} else if (session.screenshareDenoise == "off") {
|
||||
session.screenshareDenoise = false;
|
||||
} else {
|
||||
session.screenshareDenoise = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (urlParams.has('roombitrate') || urlParams.has('roomvideobitrate') || urlParams.has('rbr')) {
|
||||
log("Room BITRATE SET");
|
||||
@ -1626,6 +1739,18 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
session.limitTotalBitrate = parseInt(session.limitTotalBitrate);
|
||||
}
|
||||
|
||||
if (urlParams.has('mcvb') || urlParams.has('meshcastbitrate')){
|
||||
session.meshcastBitrate = urlParams.get('mcvb') || urlParams.get('meshcastbitrate') || 2500;
|
||||
session.meshcastBitrate = parseInt(session.meshcastBitrate);
|
||||
}
|
||||
|
||||
if (urlParams.has('mccodec') || urlParams.has('meshcastcodec')){
|
||||
session.meshcastCodec = urlParams.get('mccodec') || urlParams.get('meshcastcodec') || false;
|
||||
}
|
||||
if (session.meshcastCodec){
|
||||
session.meshcastCodec = session.meshcastCodec.toLowerCase();
|
||||
}
|
||||
|
||||
if (urlParams.has('height') || urlParams.has('h')) {
|
||||
session.height = urlParams.get('height') || urlParams.get('h');
|
||||
session.height = parseInt(session.height);
|
||||
@ -3176,11 +3301,7 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.onload = function() {
|
||||
WebMidi.enable(function(err) { // hotkeys
|
||||
|
||||
if (err) {
|
||||
errorlog(err);
|
||||
}
|
||||
WebMidi.enable().then(() =>{
|
||||
|
||||
WebMidi.addListener("connected", function(e) {
|
||||
log(e);
|
||||
@ -3194,16 +3315,18 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
|
||||
if (session.midiOut===true){
|
||||
for (var i = 0; i < WebMidi.inputs.length; i++) {
|
||||
|
||||
var input = WebMidi.inputs[i];
|
||||
|
||||
input.addListener("midimessage", "all", function(e) {
|
||||
input.addListener("midimessage", function(e) {
|
||||
log(e);
|
||||
var msg = {};
|
||||
msg.midi = {};
|
||||
msg.midi.d = e.data;
|
||||
msg.midi.s = e.timestamp;
|
||||
msg.midi.t = e.type;
|
||||
|
||||
if (e.message && e.message.channel){
|
||||
msg.midi.c = e.message.channel;
|
||||
}
|
||||
for (var UUID in session.pcs){
|
||||
if (session.pcs[UUID].allowMIDI){
|
||||
session.sendMessage(msg, UUID);
|
||||
@ -3214,14 +3337,15 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
} else if (session.midiOut==parseInt(session.midiOut)){
|
||||
try{
|
||||
var input = WebMidi.inputs[parseInt(session.midiOut)-1];
|
||||
input.addListener("midimessage", "all", function(e) {
|
||||
log(e);
|
||||
input.addListener("midimessage", function(e) {
|
||||
console.log(e);
|
||||
var msg = {};
|
||||
msg.midi = {};
|
||||
msg.midi.d = e.data;
|
||||
msg.midi.s = e.timestamp;
|
||||
msg.midi.t = e.type;
|
||||
|
||||
msg.midi.s = parseInt(10000*e.timestamp)/10000.0;
|
||||
if (e.message && e.message.channel){
|
||||
msg.midi.c = e.message.channel;
|
||||
}
|
||||
for (var UUID in session.pcs){
|
||||
if (session.pcs[UUID].allowMIDI){
|
||||
session.sendMessage(msg, UUID);
|
||||
@ -3232,51 +3356,36 @@ async function main(){ // main asyncronous thread; mostly initializes the user s
|
||||
}
|
||||
|
||||
for (var i = 0; i < WebMidi.inputs.length; i++) {
|
||||
|
||||
if (session.midiDevice && (session.midiDevice!==(i+1))){continue;}
|
||||
|
||||
var input = WebMidi.inputs[i];
|
||||
input.addListener('noteon', "all", function(e) {
|
||||
if (session.midiChannel){
|
||||
input = input.channels[session.midiChannel];
|
||||
}
|
||||
if (session.midiHotkeys==4){
|
||||
input.addListener('controlchange', function(e) {
|
||||
log(e);
|
||||
midiHotkeysCommand(e.controller.number, e.rawValue);
|
||||
});
|
||||
} else {
|
||||
input.addListener('noteon', function(e) {
|
||||
log(e);
|
||||
var note = e.note.name + e.note.octave;
|
||||
var velocity = e.velocity || false;
|
||||
midiHotkeysNote(note,velocity);
|
||||
});
|
||||
input.addListener('controlchange', "all", function(e) {
|
||||
|
||||
if (session.midiHotkeys==4){
|
||||
/* channel: 1
|
||||
controller: {number: 110, name: undefined}
|
||||
data: Uint8Array(3) [176, 110, 3]
|
||||
target: Input {_userHandlers: {…}, _midiInput: MIDIInput, …}
|
||||
timestamp: 98235.34000001382
|
||||
type: "controlchange"
|
||||
value: 3 */
|
||||
log(e);
|
||||
if (e.channel!==1){
|
||||
errorlog("VDO.Ninja is currently configured for use on channel 1 for MIDI hotkeys");
|
||||
return;
|
||||
} // channel 1?
|
||||
|
||||
var command = e.controller.number;
|
||||
var value = e.value;
|
||||
|
||||
midiHotkeysCommand(command, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(errorlog);
|
||||
};
|
||||
script.src = "./thirdparty/webmidi.js"; // dynamically load this only if its needed. Keeps loading time down.
|
||||
script.src = "./thirdparty/webmidi3.js"; // dynamically load this only if its needed. Keeps loading time down.
|
||||
document.head.appendChild(script);
|
||||
} else if (session.midiIn){
|
||||
var script = document.createElement('script');
|
||||
script.src = "./thirdparty/webmidi.js"; // dynamically load this only if its needed. Keeps loading time down.
|
||||
script.src = "./thirdparty/webmidi3.js"; // dynamically load this only if its needed. Keeps loading time down.
|
||||
script.onload = function() {
|
||||
WebMidi.enable(function(err) { // hotkeys
|
||||
if (err) {
|
||||
errorlog(err);
|
||||
}
|
||||
console.log(WebMidi.outputs);
|
||||
|
||||
});
|
||||
WebMidi.enable().then(() => console.log(WebMidi.outputs)).catch(errorlog);
|
||||
}
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
571
midi.html
Normal file
571
midi.html
Normal file
@ -0,0 +1,571 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://vdo.ninja/thirdparty/webmidi.js"></script>
|
||||
<link rel="stylesheet" href="https://vdo.ninja/main.css" />
|
||||
<style>
|
||||
.container {
|
||||
max-width: 80%;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 10px;
|
||||
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
|
||||
background-color: #ddd;
|
||||
color: black;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.card>div {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.5em;
|
||||
padding: 10px;
|
||||
background-color: #457b9d;
|
||||
color: white;
|
||||
border-bottom: 2px solid #3b6a87;
|
||||
}
|
||||
|
||||
small {
|
||||
font-style: italic;
|
||||
display: block;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
span.warning {
|
||||
color: rgb(212, 191, 0);
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 640px;
|
||||
max-height: 360px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
audio {
|
||||
max-width: 640px;
|
||||
max-height: 360px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
div#processing {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
place-items: center;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
background: #141926;
|
||||
flex-direction: column;
|
||||
}
|
||||
button {
|
||||
margin:5px;
|
||||
border:solid black 2px;
|
||||
}
|
||||
|
||||
body {
|
||||
color:white;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #225273!important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
<title>VDO.Ninja MIDI Controller</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<a id="logoname" href="./" style="text-decoration: none; color: white; margin: 2px">
|
||||
<span data-translate="logo-header">
|
||||
<font id="qos">V</font>DO.Ninja
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="info">
|
||||
<h1>VDO.Ninja MIDI test app</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2>About</h2>
|
||||
<div>
|
||||
You can check the console debug logs for added of input/output events and device details.
|
||||
<br /><br />You can download a virtual MIDI I/O controller for windwos here:<br />
|
||||
http://www.tobias-erichsen.de/software/loopmidi.html
|
||||
<br /><br />This code uses the WebMIDI.js library, referenced here:<br />
|
||||
https://github.com/djipco/webmidi
|
||||
<br /><br />
|
||||
Below you can test the <a href="https://docs.vdo.ninja/general-settings/midi">MIDI hotkey commands</a> for VDO.Ninja below:<br />
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Select the MIDI Output device:</h2>
|
||||
<div>
|
||||
<label for="outputdevice">MIDI Output device:</label>
|
||||
<select name="outputdevice" id="outputdevice">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>&midi=1</h2>
|
||||
<div id="container1">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>&midi=3</h2>
|
||||
<div id="container2">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>&midi=4 ; director</h2>
|
||||
<div id="container3">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>&midi=4 ; guest 1</h2>
|
||||
<div id="container4">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>&midi=4 ; guest 2</h2>
|
||||
<div id="container5">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Sample Remote Director Control links</h2>
|
||||
<div id="container6">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id='commands'>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
// Enable WebMidi.js
|
||||
WebMidi.enable(function (err) {
|
||||
|
||||
if (err) {
|
||||
console.log("WebMidi could not be enabled.", err);
|
||||
}
|
||||
|
||||
// Viewing available inputs and outputs
|
||||
console.log(WebMidi.inputs);
|
||||
console.log(WebMidi.outputs);
|
||||
|
||||
var output = WebMidi.outputs[0];
|
||||
|
||||
|
||||
var midiout = 0;
|
||||
var outputdevice = document.getElementById("outputdevice");
|
||||
for (var i=0;i<WebMidi.outputs.length;i++){
|
||||
var opt = document.createElement('option');
|
||||
opt.value = WebMidi.outputs[i].id;
|
||||
opt.innerHTML = WebMidi.outputs[i].name + " (id:"+(1+i)+")";
|
||||
if (i==0){
|
||||
midiout = opt.value;
|
||||
opt.selected = true;
|
||||
}
|
||||
outputdevice.appendChild(opt);
|
||||
}
|
||||
|
||||
var path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
|
||||
|
||||
outputdevice.onchange = function(e){
|
||||
midiout = outputdevice.value;
|
||||
output = WebMidi.getOutputById(midiout);
|
||||
//input = WebMidi.getInputById(midiout);
|
||||
console.log("MIDI DEVICE CHANGED: "+midiout);
|
||||
|
||||
var container = document.getElementById("container6");
|
||||
container.innerHTML = "<br />https://"+path+"/?midiremote=4&director=ROOMNAMEHERE";
|
||||
container.innerHTML += "<br /><br />";
|
||||
container.innerHTML += "https://"+path+"/?room=ROOMNAMEHERE&midiout="+(outputdevice.selectedIndex+1)+"&vd=0&ad=0&push&autostart&label=MIDI_CONTROLLER";
|
||||
}
|
||||
|
||||
var container = document.getElementById("container6");
|
||||
container.innerHTML = "<br />https://"+path+"/?midiremote=4&director=ROOMNAMEHERE";
|
||||
container.innerHTML += "<br /><br />";
|
||||
container.innerHTML += "https://"+path+"/?room=ROOMNAMEHERE&midiout="+(outputdevice.selectedIndex+1)+"&vd=0&ad=0&push&autostart&label=MIDI_CONTROLLER";
|
||||
|
||||
// Reacting when a new device becomes available
|
||||
WebMidi.addListener("connected", function(e) {
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
// Reacting when a device becomes unavailable
|
||||
WebMidi.addListener("disconnected", function(e) {
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
// Display the current time
|
||||
console.log(WebMidi.time);
|
||||
|
||||
|
||||
// Retrieve an input by name, id or index
|
||||
// var input = WebMidi.getInputByName("StreamDeck2Daw");
|
||||
// input = WebMidi.getInputById("1809568182");
|
||||
try{
|
||||
for (var i =0;i<WebMidi.inputs.length;i++){
|
||||
var input = WebMidi.inputs[i];
|
||||
|
||||
// Listen for a 'note on' message on all channels
|
||||
input.addListener('noteon', "all",
|
||||
function (e) {
|
||||
console.log("Received 'noteon' message (" + e.note.name + e.note.octave + ").");
|
||||
console.log(e);
|
||||
}
|
||||
);
|
||||
|
||||
// Listen to pitch bend message on channel 3
|
||||
input.addListener('pitchbend', 3,
|
||||
function (e) {
|
||||
console.log("Received 'pitchbend' message.", e);
|
||||
}
|
||||
);
|
||||
|
||||
// Listen to control change message on all channels
|
||||
input.addListener('controlchange', "all",
|
||||
function (e) {
|
||||
console.log("Received 'controlchange' message.", e);
|
||||
}
|
||||
);
|
||||
|
||||
// Listen to NRPN message on all channels
|
||||
input.addListener('nrpn', "all",
|
||||
function (e) {
|
||||
if(e.controller.type === 'entry') {
|
||||
console.log("Received 'nrpn' 'entry' message.", e);
|
||||
}
|
||||
if(e.controller.type === 'decrement') {
|
||||
console.log("Received 'nrpn' 'decrement' message.", e);
|
||||
}
|
||||
if(e.controller.type === 'increment') {
|
||||
console.log("Received 'nrpn' 'increment' message.", e);
|
||||
}
|
||||
console.log("message value: " + e.controller.value + ".", e);
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch(e){errorlog("no input midi found");}
|
||||
|
||||
|
||||
|
||||
var container = document.getElementById("container1");
|
||||
|
||||
|
||||
///
|
||||
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note G3; Chat";
|
||||
button.onclick = function(){output.playNote("G3");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note A3; Mute";
|
||||
button.onclick = function(){output.playNote("A3");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note B3; Mute Video";
|
||||
button.onclick = function(){output.playNote("B3");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C4; ScreenShare";
|
||||
button.onclick = function(){output.playNote("C4");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note D4; Hangup";
|
||||
button.onclick = function(){output.playNote("D4");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note E4; Hands";
|
||||
button.onclick = function(){output.playNote("E4");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note F4; Record";
|
||||
button.onclick = function(){output.playNote("F4");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note G4; Turn on Dir's Audio";
|
||||
button.onclick = function(){output.playNote("G4");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note A4; Stop Dir's Audio";
|
||||
button.onclick = function(){output.playNote("A4");}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
///
|
||||
|
||||
var container = document.getElementById("container2");
|
||||
|
||||
///
|
||||
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 0";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 0});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 1";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 1});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 2";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 2});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 3";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 3});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 4";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 4});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 5";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 5});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 6";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 6});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 7";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 7});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Note C1; velocity 8";
|
||||
button.onclick = function(){output.playNote("C1", 1, {velocity: 8});}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
///
|
||||
|
||||
var container = document.getElementById("container3");
|
||||
|
||||
|
||||
///
|
||||
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 0";
|
||||
button.onclick = function(){output.sendControlChange(110, 0, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 1";
|
||||
button.onclick = function(){output.sendControlChange(110, 1, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 2";
|
||||
button.onclick = function(){output.sendControlChange(110, 2, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 3";
|
||||
button.onclick = function(){output.sendControlChange(110, 3, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 4";
|
||||
button.onclick = function(){output.sendControlChange(110, 4, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 5";
|
||||
button.onclick = function(){output.sendControlChange(110, 5, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 6";
|
||||
button.onclick = function(){output.sendControlChange(110, 6, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 7";
|
||||
button.onclick = function(){output.sendControlChange(110, 7, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 110; value 8";
|
||||
button.onclick = function(){output.sendControlChange(110, 8, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
|
||||
|
||||
///
|
||||
|
||||
var container = document.getElementById("container4");
|
||||
|
||||
|
||||
///
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 111; value 0";
|
||||
button.onclick = function(){output.sendControlChange(111, 0, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 111; value 1";
|
||||
button.onclick = function(){output.sendControlChange(111, 1, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 111; value 2";
|
||||
button.onclick = function(){output.sendControlChange(111, 2, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 111; value 3";
|
||||
button.onclick = function(){output.sendControlChange(111, 3, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 111; value 4";
|
||||
button.onclick = function(){output.sendControlChange(111, 4, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 111; value 5";
|
||||
button.onclick = function(){output.sendControlChange(111, 5, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Rainbow Puke Fix; value 8";
|
||||
button.onclick = function(){output.sendControlChange(111, 8, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
///
|
||||
|
||||
var container = document.getElementById("container5");
|
||||
|
||||
|
||||
///
|
||||
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 112; transfer popup";
|
||||
button.onclick = function(){output.sendControlChange(112, 0, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 112; scene 1";
|
||||
button.onclick = function(){output.sendControlChange(112, 1, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 112; mute in scene";
|
||||
button.onclick = function(){output.sendControlChange(112, 2, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 112; mute everywhere";
|
||||
button.onclick = function(){output.sendControlChange(112, 3, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 112; hang up";
|
||||
button.onclick = function(){output.sendControlChange(112, 4, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "channel 112; solo chat";
|
||||
button.onclick = function(){output.sendControlChange(112, 5, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "remote speaker";
|
||||
button.onclick = function(){output.sendControlChange(112, 6, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "remote display";
|
||||
button.onclick = function(){output.sendControlChange(112, 7, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Rainbow Puke Fix";
|
||||
button.onclick = function(){output.sendControlChange(112, 8, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "scene 2";
|
||||
button.onclick = function(){output.sendControlChange(112, 12, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "scene 3";
|
||||
button.onclick = function(){output.sendControlChange(112, 13, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "scene 4";
|
||||
button.onclick = function(){output.sendControlChange(112, 14, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "scene 5";
|
||||
button.onclick = function(){output.sendControlChange(112, 15, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "scene 6";
|
||||
button.onclick = function(){output.sendControlChange(112, 16, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = " scene 7";
|
||||
button.onclick = function(){output.sendControlChange(112, 17, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "scene 8";
|
||||
button.onclick = function(){output.sendControlChange(112, 18, 1);}; // "speaker" also works in the same way.
|
||||
container.appendChild(button);
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
10
monitor.html
10
monitor.html
@ -2,7 +2,7 @@
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OBSN Monitoring</title>
|
||||
<title>VDO.Ninja Monitoring</title>
|
||||
<style>
|
||||
|
||||
body {
|
||||
@ -99,7 +99,9 @@
|
||||
|
||||
canvas {
|
||||
background-color: black;
|
||||
margin: 20px;
|
||||
margin: 1vw;
|
||||
width: 22vw;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
#log {
|
||||
@ -430,14 +432,12 @@
|
||||
<body onload="loadIframe();">
|
||||
<div id="container">
|
||||
<h1 >
|
||||
<span id="streamID_header">OBS.Ninja Remote Monitor</span>
|
||||
<span id="streamID_header">VDO.Ninja Remote Monitor</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div id="graphs" style="display:none">
|
||||
|
||||
|
||||
|
||||
<div class="graph">
|
||||
<h2>Bitrate (kbps)</h2>
|
||||
<span>0</span>
|
||||
|
||||
@ -116,6 +116,7 @@
|
||||
var quality_reason = "";
|
||||
var encoder = "";
|
||||
var Round_Trip_Time_ms = "";
|
||||
var recordResults = false;
|
||||
|
||||
function copyFunction(copyText) {
|
||||
alert("Log copied to the clipboard.");
|
||||
@ -241,6 +242,10 @@
|
||||
srcString = srcString + "&buffer=" + urlParams.get("buffer");
|
||||
}
|
||||
|
||||
if (urlParams.has("record")) {
|
||||
recordResults = true;
|
||||
}
|
||||
|
||||
iframe.src = srcString;
|
||||
|
||||
iframeContainer.appendChild(iframe);
|
||||
@ -363,14 +368,12 @@
|
||||
if (out.split("Bitrate_in_kbps").length > 1) {
|
||||
for (var key in e.data.stats.inbound_stats[streamID]) {
|
||||
if (key.startsWith("RTCMediaStreamTrack_receiver")) {
|
||||
var bitrate =
|
||||
e.data.stats.inbound_stats[streamID][key][
|
||||
var bitrate = e.data.stats.inbound_stats[streamID][key][
|
||||
"Bitrate_in_kbps"
|
||||
];
|
||||
updateData("bitrate", bitrate);
|
||||
|
||||
var buffer =
|
||||
e.data.stats.inbound_stats[streamID][key][
|
||||
var buffer = e.data.stats.inbound_stats[streamID][key][
|
||||
"Buffer_Delay_in_ms"
|
||||
];
|
||||
updateData("buffer", buffer);
|
||||
@ -381,8 +384,7 @@
|
||||
updateData("packetloss", packetloss);
|
||||
}
|
||||
|
||||
var resolution =
|
||||
e.data.stats.inbound_stats[streamID][key]["Resolution"];
|
||||
var resolution = e.data.stats.inbound_stats[streamID][key]["Resolution"];
|
||||
|
||||
if (previousResolution != resolution) {
|
||||
previousResolution = resolution;
|
||||
@ -392,6 +394,12 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (recordResults){
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('POST', "https://reports.vdo.ninja/record");
|
||||
request.send(JSON.stringify(e.data.stats));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user