Conflicts:
	soundboard.py
	static/main.css
	static/main.js
	templates/index.html
This commit is contained in:
otrs 2017-10-23 21:15:53 +02:00
commit 95bc31dc24
8 changed files with 220 additions and 18 deletions

View File

@ -59,3 +59,28 @@ Production
For production deployment Apache (or other webservers like nginx, lighttpd etc.) should be used. For production deployment Apache (or other webservers like nginx, lighttpd etc.) should be used.
For Apache a sample configuration is provided in the folder `apache` which can be adapted. For Apache a sample configuration is provided in the folder `apache` which can be adapted.
Please keep in mind that you may have to adjust the path to the sound files in your apache config in order to enable local playback on the clients.
Usage
=====
Users can browse the webinterface, search for sounds and play them on the remote server or their local browser.
Additionally to the normal point and click usage there are also several keyboard shortcuts available.
Keyboard shortcuts
------------------
- Navigation items, input fields and sound buttons can be selected by using tab and shift+tab
- esc clears the search field
- enter plays the selected sound
- ctrl+k and ctrl+c kill all sounds
- ctrl+x switches between local and remote playback
- ctrl+enter kills sounds before the selected gets played
Live version
============
A list of sound requests and already added sounds can be found here: https://pad.wiai.de/p/soundboardTodos

View File

@ -1,10 +1,10 @@
Listen 5000 Listen 5000
<VirtualHost *:5000> <VirtualHost *:5000>
#ServerName example.com
WSGIDaemonProcess soundboard user=www-data group=www-data threads=5 WSGIDaemonProcess soundboard user=www-data group=www-data threads=5
WSGIScriptAlias / /var/www/soundboard/soundboard.wsgi WSGIScriptAlias / /var/www/soundboard/soundboard.wsgi
Alias /sounds /home/pi/sounds
<Directory /var/www/soundboard> <Directory /var/www/soundboard>
WSGIProcessGroup soundboard WSGIProcessGroup soundboard
WSGIApplicationGroup %{GLOBAL} WSGIApplicationGroup %{GLOBAL}
@ -12,4 +12,14 @@ Listen 5000
Order deny,allow Order deny,allow
Allow from all Allow from all
</Directory> </Directory>
<Directory /home/pi/sounds>
Options FollowSymLinks
AllowOverride All
Order deny,allow
Allow from all
Require all granted
</Directory>
</VirtualHost> </VirtualHost>

12
devserver.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
basedir=$(dirname "$0")
if [ ! -d "$basedir/py" ]; then
echo "Directory \"py\" with a Python virtual environment"
echo "does not exist. See README.rst on how to set it up."
else
export FLASK_DEBUG=1
export FLASK_APP="$basedir/soundboard.py"
"$basedir"/py/bin/flask run
fi

View File

@ -4,3 +4,4 @@ itsdangerous==0.24
Jinja2==2.9.6 Jinja2==2.9.6
MarkupSafe==1.0 MarkupSafe==1.0
Werkzeug==0.12.2 Werkzeug==0.12.2
requests

View File

@ -1,7 +1,10 @@
import os import os
import shutil
import subprocess import subprocess
import tempfile
from flask import Flask, render_template, request, redirect, url_for import requests
from flask import Flask, render_template, request, redirect, url_for, send_from_directory
import config import config
@ -33,7 +36,11 @@ def index(sound=None, text=None, video=None):
pitch = request.form.get("pitch", default="") pitch = request.form.get("pitch", default="")
pitch = pitch if pitch.strip() != "" else "50" pitch = pitch if pitch.strip() != "" else "50"
<<<<<<< HEAD
subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode('utf-8')], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode('utf-8')], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
=======
subprocess.Popen(["espeak", "-v", voice, "-s", speed, "-p", pitch, text.encode("utf-8")], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
>>>>>>> f570309d9cc64b761aa2354da885be9cdf8d1b42
return redirect("/") return redirect("/")
video = request.args.get("video") video = request.args.get("video")
@ -49,3 +56,23 @@ def index(sound=None, text=None, video=None):
subprocess.Popen(["pkill", "-f", "omxplayer"]) subprocess.Popen(["pkill", "-f", "omxplayer"])
return render_template("index.html", sounds=sounds) return render_template("index.html", sounds=sounds)
@app.route("/sounds/<path:name>")
def sounds(name):
return send_from_directory(config.path, name)
@app.route("/image/")
def image(imageurl=None):
if not imageurl:
return False
with tempfile.NamedTemporaryFile(mode="wb", buffering=True) as tmp:
image = requests.get(imageurl, stream=True)
if not image.ok:
return False
shutil.copyfileobj(image.raw, tmp)
subprocess.Popen(["killall", "pngview"])
subprocess.Popen(["/opt/raspidmx/pngview/pngview", "-b", "0", "-d", "0", "-n", tmp.name])
@app.route("/kill/images")
def killImages():
subprocess.Popen(["killall", "pngview"])

View File

@ -139,3 +139,28 @@ div #reset:hover {
#local-mode-button .local { #local-mode-button .local {
color: red; color: red;
} }
.sound a:active {
background-color: #ff4136;
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6);
cursor: pointer;
}
.sound-pressed {
background-color: #ff4136 !important;
box-shadow: inset 5px 5px 5px rgba(0, 0, 0, 0.6) !important;
}
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#local-mode-button .local {
color: red;
}

View File

@ -1,7 +1,11 @@
var localModeEnabled = false; var localModeEnabled = false;
// References to howler objects // References to howler objects
/* jshint esversion: 6 */
var localModeEnabled = false;
// References to howler objects
var howlerSounds = []; var howlerSounds = [];
function ready(fn) { function ready(fn) {
@ -39,7 +43,6 @@ ready(function() {
searchfield.focus(); searchfield.focus();
searchfield.addEventListener("keyup", function() { searchfield.addEventListener("keyup", function() {
var buttons = document.querySelectorAll(".sound"); var buttons = document.querySelectorAll(".sound");
buttons.forEach(function(item) { buttons.forEach(function(item) {
@ -48,7 +51,7 @@ ready(function() {
buttons.forEach(function(item) { buttons.forEach(function(item) {
var name = item.firstChild.innerHTML; var name = item.firstChild.innerHTML;
if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) { if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) {
item.style.display = "none"; item.style.display = "none";
} }
@ -64,6 +67,68 @@ ready(function() {
addKeyListeners(); addKeyListeners();
}); });
/*
* Adds all necessary KeyListeners to document
*/
function addKeyListeners() {
// keyboard listener for global key strokes/inputs
document.onkeydown = function(evt) {
evt = evt || window.event;
if (evt.keyCode == 27) {
// esc key
resetSearch();
} else if ((evt.keyCode == 75 || evt.keyCode == 67) && evt.ctrlKey) {
// ctrl+k and ctrl+c key binding
killAllAudio();
} else if (evt.keyCode == 88 && evt.ctrlKey){
// ctrl+x key binding
toggleLocalMode();
}
};
// keyboard listener for playing sounds using enter key
var buttons = document.querySelectorAll(".sound");
buttons.forEach(function(item) {
item.firstChild.addEventListener('keypress', async function (e) {
var key = e.keyCode;
var source = e.target;
// keylistener for enter or ctrl+enter (which is k10 on chrome)
if (key === 13 || (key === 10 && e.ctrlKey)) {
if (e.ctrlKey){
killAllAudio();
await sleep(100);
}
source.classList.add("sound-pressed");
source.onclick();
await sleep(300);
source.classList.remove("sound-pressed");
}
});
});
}
function resetSearch() {
var searchfield = document.querySelector("#search");
var buttons = document.querySelectorAll(".sound");
buttons.forEach(function(item) {
item.style.display = "inline-block";
});
searchfield.value = "";
searchfield.focus();
}
/*
* Reads the stream url from input with id #streaming-url and forwards it to the server using AJAX.
*/
function playStream() {
var streamUrl = document.querySelector("#streaming-url").value;
ajaxRequest("/?video="+encodeURI(streamUrl));
}
/* /*
* Adds all necessary KeyListeners to document * Adds all necessary KeyListeners to document
*/ */
@ -145,6 +210,15 @@ function ajaxRequest(url) {
alert("Unfortunately we were unable to handle your request. Please try again later and contact the server administrator if the problem persists."); alert("Unfortunately we were unable to handle your request. Please try again later and contact the server administrator if the problem persists.");
return false; return false;
} }
var ajaxRequest;
try {
ajaxRequest = new XMLHttpRequest();
ajaxRequest.open("GET", url, true);
ajaxRequest.send(null);
} catch (e) {
alert("Unfortunately we were unable to handle your request. Please try again later and contact the server administrator if the problem persists.");
return false;
}
} }
/* /*
@ -153,39 +227,39 @@ function ajaxRequest(url) {
*/ */
function toggleLocalMode() { function toggleLocalMode() {
toggleButton = document.getElementById("local-mode-button").children[0]; toggleButton = document.getElementById("local-mode-button").children[0];
if(localModeEnabled){ if (localModeEnabled) {
localModeEnabled = false; localModeEnabled = false;
toggleButton.classList.remove("local"); toggleButton.classList.remove("local");
toggleButton.innerHTML = "[S]"; toggleButton.innerHTML = "[S]";
toggleButton.title = "play sounds on server"; toggleButton.title = "play sounds on server";
}else{ } else {
localModeEnabled = true; localModeEnabled = true;
toggleButton.classList.add("local"); toggleButton.classList.add("local");
toggleButton.innerHTML = "[L]"; toggleButton.innerHTML = "[L]";
toggleButton.title = "play sounds locally"; toggleButton.title = "play sounds locally";
} }
} }
/* /*
* Either plays the given sound file locally by using howler.js or forwards the request to the remote server using AJAX. * Either plays the given sound file locally by using howler.js or forwards the request to the remote server using AJAX.
*/ */
function playSound(filename) { function playSound(filename) {
if(localModeEnabled){ if (localModeEnabled){
// play local audio using howler.js // play local audio using howler.js
var sound = new Howl({ var sound = new Howl({
src: ['/sounds/'+filename] src: ['/sounds/' + filename]
}); });
sound.play(); sound.play();
howlerSounds.push(sound); howlerSounds.push(sound);
}else{ } else {
ajaxRequest("/play/"+filename); ajaxRequest("/play/" + filename);
} }
} }
function killAllHowlerAudio() { function killAllHowlerAudio() {
for(i=0; i<howlerSounds.length; i++) { for (var i = 0; i < howlerSounds.length; i++) {
howlerSounds[i].stop(); howlerSounds[i].stop();
} }
} }
/* /*
@ -199,3 +273,19 @@ function killAllAudio() {
} }
} }
/*
* Reads the stream url from input with id #streaming-url and forwards it to the server using AJAX.
*/
function showImage() {
var streamUrl = document.querySelector("#streaming-url").value;
ajaxRequest("/image/?imageurl="+encodeURI(streamUrl));
}
/*
* Kills all images
*/
function killAllImages() {
ajaxRequest("/kill/images");
}

View File

@ -33,6 +33,7 @@
<a href="#sounds" id="sounds-nav">Sounds</a> <a href="#sounds" id="sounds-nav">Sounds</a>
<a href="#streams">Streams</a> <a href="#streams">Streams</a>
<a href="#voice">Voices</a> <a href="#voice">Voices</a>
<a href="#image">Images</a>
</nav> </nav>
<content> <content>
<section id="sounds" style="display:none;"> <section id="sounds" style="display:none;">
@ -66,8 +67,19 @@
<input type="submit" value="Voice" /> <input type="submit" value="Voice" />
</form> </form>
</section> </section>
<section id="images" style="display:none;">
<div>
<form>
<label for="image">Image URL</label>
<input id="streaming-url" type="text" name="image" placeholder="paste image url here..." />
</form>
<button onclick="showImage();">Play!</button>
</div>
<button name="submit" onclick="killAllImages();">clear images</button>
</section>
</content> </content>
<script src="/static/howler.js"></script> <script src="/static/howler.js"></script>
<script src="/static/main.js"></script> <script src="/static/main.js"></script>
</body> </body>
</html> </html>