Fully kiosk volume slider

So I want a way to control the volume of the actual tablet from Sharptools. I usually see topics to adjust external speakers, but I want the actual tablet or bluetooth speakers to adjust.

I’m completely unfamiliar with web development, but I know other people have been searching for this and I’m hoping someone can help me or build further on what I’m trying to do.

At the moment I have a few buttons with different volume levels in a custom tile. It’s simple Javascript and it’s just a set of buttons, which open the remote admin link to set a certain volume leven. This opens a tab, which closes by itself within a second, which usually is enough to load up the change in volume.

Now I’m stuck at my next “want”. I want it to be a slider, which would update after release (so it doesn’t start spamming updates to remote admin). I would want it to be in 20% increments, to not have overly many volume options.
But since I’m just learning this as of this week, it’s too complicated just yet. My other idea besides the slider, would be to have some kind of bars graphic, where every bar triggers the link and changes colour.

PS: Maybe this could be a vanilla tile, to work with Fully Kiosk? As a configuration you could select which audio stream needs to be affected. In my case this is Stream=3 for audio stream, I believe bluetooth is 9.

Here’s my code at the moment:

<!DOCTYPE html>
<html>
<head>
  <title>Close popup window</title>
</head>
<body>
  <Button type="button" onclick="volume0()">Mute</button><br>
  <Button type="button" onclick="volume15()">Volume 15</button><br>
  <Button type="button" onclick="volume30()">Volume 30</button><br>
  <Button type="button" onclick="volume45()">Volume 45</button><br>
  <Button type="button" onclick="volume60()">Volume 60</button><br>
  <Button type="button" onclick="volume75()">Volume 75</button><br>
  <Button type="button" onclick="volume90()">Volume 90</button>
  <script type="text/javascript">
function volume0(){
    var popupwin = window.open('IP ADDRESS/?cmd=setAudioVolume&level=0&stream=3&password=XXX','Volume 0','width=50,height=50,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}
function volume15(){
    var popupwin = window.open('IP ADDRESS/?cmd=setAudioVolume&level=15&stream=3&password=XXX','Volume 15','width=50,height=50,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}
function volume30(){
    var popupwin = window.open('IP ADDRESS/?cmd=setAudioVolume&level=30&stream=3&password=XXX','Volume 30','width=50,height=50,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}
function volume45(){
    var popupwin = window.open('IP ADDRESS/?cmd=setAudioVolume&level=45&stream=3&password=XXX','Volume 45','width=50,height=50,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}
function volume60(){
    var popupwin = window.open('IP ADDRESS/?cmd=setAudioVolume&level=60&stream=3&password=XXX','Volume 60','width=50,height=50,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}
function volume75(){
    var popupwin = window.open('IP ADDRESS/?cmd=setAudioVolume&level=75&stream=3&password=XXX','Volume 75','width=50,height=50,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}
function volume90(){
    var popupwin = window.open('IP ADDRESS/?cmd=setAudioVolume&level=90&stream=3&password=XXX','Volume 90','width=50,height=50,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}
</script>
</body>
</html>

@Sgt.Flippy_PJ, you will need to use the <input type="range"> element, instead of <button> in this case. You can either set a listener and send the request when the range input value changes, or add a button next to it, so you adjust the volume then click the button to send the request.

1 Like

Afternoon at work I found out about the range and you can read the value. So if my reasoning is okay, I should be able to declare the variable value, mix it in with the remote admin address and then I should be good.
HTML and Javascript and a little bit of CSS is quite a lot to read about to find 1 specific use :rofl: But it’s already been interesting!

Will have to do some fiddling tonight to see if what I have in my mind works.

2 Likes

Now I’m at the stage the slider works, it loads up, but it doesn’t adjust the volume.
I have variables set for the part of the url before and after what the input needs to be. But I can’t figure out what to put there for the slider result.
Now when I move the slider, it sends the request, but it always sets the volume to 50 (the default value of the slider).

Are you using onchange event listener in the range element to trigger the volume setting request? If so, you should be able to get the event’s value directly. Can you post the code snippet so we can better understand and help?

Yes, using onchange, I have tried a number of things as URL 3, with varying results. Most gave me an error message from remote admin, discarding non-numeral or invalid numbers. The current code always sets volume to 50 instead of the desired number. The text beneath the bar changes correctly though, so guess I’m inputting a wrong part.

As I said, I’m completely new at this, so this might be something really stupid and I’m just trying to understand things and using snippets from various sources to try out. So it’s possible there’s stuff in there that’s completely useless.

Here’s the full code:

<!DOCTYPE html>
<html>

<body>

<h1>Custom Range Slider</h1>
<p>Drag the slider to display the current value.</p>

<div class="slidecontainer">
  <input type="range" min="0" max="100" value="50" step="10" class="slider" id="myRange" list="tickmarks">
  
  <datalist id="tickmarks">
<option value="0"></option>
<option value="10"></option>
<option value="20"></option>
<option value="30"></option>
<option value="40"></option>
<option value="50"></option>
<option value="60"></option>
<option value="70"></option>
<option value="80"></option>
<option value="90"></option>
<option value="100"></option>
</datalist>

  <p>Value: <span id="demo"></span></p>
</div>

<script>
let slider = document.getElementById("myRange");
let output = document.getElementById("demo");
let URL1 = "IP ADDRESS:2323/?cmd=setAudioVolume&level="
let URL2 = "&stream=3&password=XXX"

let URL3 = Number(myRange.value)
let URL = URL1 + URL3 + URL2

output.innerHTML = slider.value;

  slider.onchange = function() {
  output.innerHTML = this.value;
      var popupwin = window.open(URL,'Volume','width=150,height=150,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 6000);
}


</script>

</body>
</html>

I declare myself an idiot…

As in my code, all the variables are before the function, they need to be inside.
I got it working!

PS: for anyone searching for this, here’s the code. I’m still working on styling it a bit (aka learning how to). I already made it vertical, it fits better to my needs. From what I read, this also only works in Chrome.

To use, replace IP ADDRESS with your own and XXX with your remote admin password, inside the URL1 and URL2 parts of the code. To handle different audio streams, like bluetooth, calls or alarms, edit the number after “stream” in URL2.

Quick edit so the volume level is shown while moving the slider, so you can tell what volume you will be setting to.

<!DOCTYPE html>
<html>

<body>
<div class="slidecontainer">
  <input type="range" min="0" max="100" step="10" class="slider" id="myRange" list="tickmarks" orient="vertical">
<style>

.slider{
-webkit-appearance: slider-vertical;
width: 8px;
height: 225px;
padding: 0 5px;
position: relative

}

</style>
  <datalist id="tickmarks">
<option value="0"></option>
<option value="10"></option>
<option value="20"></option>
<option value="30"></option>
<option value="40"></option>
<option value="50"></option>
<option value="60"></option>
<option value="70"></option>
<option value="80"></option>
<option value="90"></option>
<option value="100"></option>
</datalist>

  <p>Volume: <span id="demo"></span></p>
</div>

<script>
let slider = document.getElementById("myRange");
let output = document.getElementById("demo");
let URL1 = "http://IP ADDRESS/?cmd=setAudioVolume&level="
let URL2 = "&stream=3&password=XXX"

output.innerHTML = slider.value;

  slider.oninput = function() {
  output.innerHTML = this.value;
}

  slider.onchange = function() {
  output.innerHTML = this.value;
  let URL3 = this.value
  let URL = URL1 + URL3 + URL2
      var popupwin = window.open(URL,'Volume','width=15,height=15,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}


</script>

</body>
</html>
2 Likes

Awesome work and thanks for sharing! You solved a need and picked up some new skills while at it! :star::muscle:

1 Like

Per your question about how to style content in custom tiles to fit different sized devices… and my response around the complexity of styling standard HTML range inputs especially when vertical and dynamic scaling is desired… and how using an external range/slider library can sometimes simplify this, here’s an updated copy of your custom tile using the popular noUiSlider library:

<!-- Do not edit below -->
<script type="application/json" id="tile-settings">
{"schema": "0.1.0", "settings": [], "dimensions": {"height": 3, "width": 1}}
</script>
<!-- Do not edit above -->

<link href="https://unpkg.com/nouislider@15.5.1/dist/nouislider.css" rel="stylesheet">
<script src="https://unpkg.com/nouislider@15.5.1/dist/nouislider.min.js"></script>

<style>
  html, body {
    height: 100vh;
    margin: 0;
  }
  .slide-container {
    display: flex;
  }
  #range {
    height: 76vh;
    margin: 2vh 0;
    /* center it */
    margin-left: auto;
    margin-right: auto;
    padding: 2vh 2vw;
  }
  .label { 
    text-align: center;
    font-size: min(10vh, 20vw);
    line-height: 1;
    height: 20vh;
    display: flex;
    align-items: center; /* vertical center */
    justify-content: center; /* horizontal center */
  }
  .label > * {
    margin: 2vh 2vw;
  }
</style>

<div class="slidecontainer">
  <div id="range"></div>

  <div class="label">
    <span>
      Volume: <span id="volume"></span>
    </span>
  </div>
</div>

<script>
let slider = document.getElementById("range");
let output = document.getElementById("volume");
let BASE_URL = "http://IP ADDRESS/?cmd=setAudioVolume&level="
let PARAMS = "&stream=3&password=XXX"

//create the slider
noUiSlider.create(slider, {
  start: [50],
  connect: true,
  range: {
    'min': 0,
    'max': 100
  },
  step: 10,
  orientation: 'vertical',
  direction: 'rtl', //0 at bottom
  // Show a scale with the slider
  pips: {
    mode: 'steps',
    stepped: true,
    density: 1
  }
});

//anytime the value updates while dragging, update the label
range.noUiSlider.on('update', function (values, handle) {
  let value = parseInt(values[handle]); //store the value in a simplified variable name
  output.innerHTML = value; //update the label
});

//only make the API call when the value is actually changed (drag handle released)
range.noUiSlider.on('change', function(values, handle) {
  let value = parseInt(values[handle]); 
  let url = BASE_URL + value + PARAMS //craft the URL
  //make the API call by popping up the window
  var popupwin = window.open(url,'Volume','width=15,height=15,left=5,top=3');
  setTimeout(function() { popupwin.close();}, 1000);
})

//get the initial value to display
output.innerHTML = parseInt(slider.noUiSlider.get());

</script>


</div>

9w3e7Zc2xc

2 Likes

Awesome, just read the code and looks like you also fixed the issue with the label not being correct when reloading the page, if I’m not mistaken.

Looks amazing, thank you so much!

It’s still just using your 50% default. :slight_smile:
(It just reads it from the ‘range’ that gets created)

1 Like

I played with this a bit further and added some more customizations. After adding this tile to your dashboard, you can edit the tile and there’s some settings you can tweak.

By default, this version uses the native local JavaScript API that you can enable in Fully Kiosk browser (Advanced Web Settings > Enable JS API). This should result in better stability and the ability to read the current volume level when the tile is first loaded.

You can also disable that in the tile settings and you’ll be presented with options to enter the IP address and passcode for Fully Remote Admin. That approach will use your popup communication by default or you can change the tile setting to 'iframe' which is invisible and works on Fully but requires allowing mixed content in Chrome.

<!-- Do not edit below -->
<script type="application/json" id="tile-settings">
{
  "schema": "0.1.0",
  "settings": [
    {
      "type": "BOOLEAN",
      "default": true,
      "name": "useJsApi",
      "label": "Use JavaScript API?"
    },
    {
      "showIf": ["useJsApi", "==", false],
      "label": "Fully IP Address",
      "type": "STRING",
      "name": "fullyIpAddress"
    },
    {
      "name": "fullyPasscode",
      "type": "STRING",
      "showIf": ["useJsApi", "==", false],
      "label": "Fully Passcode"
    },
    {
      "name": "apiTactic",
      "type": "ENUM",
      "showIf": ["useJsApi", "==", false],
      "values": ["iframe", "popup"],
      "default": "popup",
      "label": "API Communication"
    },
    {
      "name": "audioChannel",
      "label": "Audio Channel",
      "type": "NUMBER",
      "default": 3
    }
  ],
  "name": "Fully Volume Controller",
  "dimensions": {"width": 1, "height": 3}
}
</script>
<!-- Do not edit above -->

<link href="https://unpkg.com/nouislider@15.5.1/dist/nouislider.css" rel="stylesheet">
<script src="https://unpkg.com/nouislider@15.5.1/dist/nouislider.min.js"></script>
<script src="https://cdn.sharptools.io/js/custom-tiles.js"></script>


<style>
  html, body {
    height: 100vh;
    margin: 0;
  }
  .slide-container {
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center; /* vertical align */
  }
  #range {
    height: 75vh;
    /* center it */
    margin-left: auto;
    margin-right: auto;
    margin-top: 2vh; /* move everything down a tick */
  }
  .label { 
    text-align: center;
    font-size: min(10vh, 20vw);
    line-height: 1;
    height: 18vh;
    display: flex;
    align-items: center; /* vertical center */
    justify-content: center; /* horizontal center */
    padding: 0 4vw;
  }
  iframe.api-caller {
    display: none;
  }
  iframe.api-caller {
    position: absolute;
    border: 0;
    width: 100%;
    background: rgba(255,0,0,0.5); /* red transparent for debug */
    top: 0; left: 0; right: 0; bottom: 0;
  }

  #fully-js-warning {
    background: rgba(223, 0, 0, 0.7);
    padding: 1em;
    position: fixed;
    bottom:0;
    left: 0;
    right: 0;
    max-height: calc(90% - 2em);
    overflow-y: auto;
  }
  #range[disabled] .noUi-handle, #range[disabled] .noUi-connects {
    display: none;
  }
</style>

<div class="slide-container">
  <div id="range"></div>

  <div class="label">
    <span>
      Volume: <span id="volume"></span>
    </span>
  </div>
</div>

<div id="fully-js-warning" style="display:none;">
  The Fully JavaScript API option is selected in the tile settings, but the 
  <strong>Enable JavaScript Interface</strong> setting in Fully's <strong>Advanced Web Settings</strong> 
  section is not enabled.
  <br>
  <br>
  Either enable the setting in Fully or disable the JavaScript option in this tile's settings.
</div>

<script>
let slider = document.getElementById("range");
let output = document.getElementById("volume");
let fullyJsWarning = document.getElementById("fully-js-warning");

//defaults, actuals get loaded below
let useJsApi = true; 
let fullyIpAddress = "";
let fullyPasscode = "";
let audioStream = 3;
let apiTactic = "popup";

//when the tile settings are ready, let's grab them
stio.ready(function(data){
  if(data.settings.useJsApi === false){
    useJsApi = data.settings.useJsApi;
  }
  if(data.settings.fullyIpAddress != null){
    let addr = data.settings.fullyIpAddress
    //if there's a colon, assume the user entered a port
    if(addr.indexOf(":") > -1){
    	fullyIpAddress = addr;  
    }
    else{
      fullyIpAddress = `${addr}:2323`; //add the default port
    }
    
  }
  if(data.settings.fullyIpAddress != null){
    fullyPasscode = data.settings.fullyPasscode
  }
  if(data.settings.audioStream != null){
    audioStream = parseInt(data.settings.audioStream)
  }
  if(data.settings.apiTactic != null){
    apiTactic = data.settings.apiTactic;
  }

  initVolume();
});

//create the slider
noUiSlider.create(slider, {
  start: [50],
  connect: 'lower',
  range: {
    'min': 0,
    'max': 100
  },
  step: 10,
  orientation: 'vertical',
  direction: 'rtl', //0 at bottom
  // Show a scale with the slider
  pips: {
    mode: 'steps',
    stepped: true,
    density: 1
  }
});

//anytime the value updates while dragging, update the label
range.noUiSlider.on('update', function (values, handle) {
  let value = parseInt(values[handle]); //store the value in a simplified variable name
  output.innerHTML = value; //update the label
});

//only make the API call when the value is actually changed (drag handle released)
range.noUiSlider.on('change', function(values, handle) {
  let value = parseInt(values[handle]); 
  setVolume(value);
})

function setVolume(level){
  if(useJsApi){
    void fully.setAudioVolume(level, audioStream)
  }
  else{
    let url = getCommandUrl('setAudioVolume', {level: level, stream: audioStream})
    console.debug('URL is', url);
    loadUrl(url);
  }
}

function getCommandUrl(command, params){
  let url = `http://${fullyIpAddress}/?cmd=${command}&password=${fullyPasscode}`;
  //layer in additional parameters
  Object.keys(params).forEach(key =>{
    url += `&${key}=${params[key]}`;
  })
  return url;
}

function loadUrl(url){
  if(apiTactic === "iframe"){
    //create an iframe with our class and url, then add it to the document
    let iframe = document.createElement('iframe');
    iframe.classList.add("api-caller")
    iframe.src = url;
    document.body.appendChild(iframe);
    //then remove it in a second
    setTimeout(()=>{
      document.body.removeChild(iframe);
    }, 1000)
  }
  else{
  	//make the API call by popping up the window
    var popupwin = window.open(url,'Volume','width=15,height=15,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
  }
}


function initVolume(){
  //if the tile is configured to use the JS API
  if(useJsApi){
    //if the API isn't enabled, display the warning
    if(window.fully == null){
      slider.setAttribute("disabled", true); //disable the slider;
      fullyJsWarning.style.display = "block";
    }
    else{
      //make sure the slider is enabled and the warning is not displayed
      slider.removeAttribute('disabled');
      fullyJsWarning.style.display = "none";
      //try to get the volume
      let volume = fully.getAudioVolume(audioStream)
      slider.noUiSlider.set(volume);
    }
  }

  //get the value from the slider
  output.innerHTML = parseInt(slider.noUiSlider.get());
}


</script>