diff --git a/README.rst b/README.rst index 1375c80..4ce90ab 100644 --- a/README.rst +++ b/README.rst @@ -59,3 +59,28 @@ Production 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. + +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 \ No newline at end of file diff --git a/apache/010-soundboard.conf b/apache/010-soundboard.conf index 84372a9..03cb961 100644 --- a/apache/010-soundboard.conf +++ b/apache/010-soundboard.conf @@ -1,10 +1,10 @@ Listen 5000 - #ServerName example.com - WSGIDaemonProcess soundboard user=www-data group=www-data threads=5 WSGIScriptAlias / /var/www/soundboard/soundboard.wsgi + Alias /sounds /home/pi/sounds + WSGIProcessGroup soundboard WSGIApplicationGroup %{GLOBAL} @@ -12,4 +12,14 @@ Listen 5000 Order deny,allow Allow from all + + + Options FollowSymLinks + AllowOverride All + Order deny,allow + Allow from all + Require all granted + + + diff --git a/devserver.sh b/devserver.sh new file mode 100755 index 0000000..460b9de --- /dev/null +++ b/devserver.sh @@ -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 diff --git a/requirements.txt b/requirements.txt index 6aee249..639537d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ itsdangerous==0.24 Jinja2==2.9.6 MarkupSafe==1.0 Werkzeug==0.12.2 +requests \ No newline at end of file diff --git a/soundboard.py b/soundboard.py index a79494f..01e8e73 100644 --- a/soundboard.py +++ b/soundboard.py @@ -1,7 +1,10 @@ import os +import shutil 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 @@ -33,7 +36,11 @@ def index(sound=None, text=None, video=None): pitch = request.form.get("pitch", default="") 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) +>>>>>>> f570309d9cc64b761aa2354da885be9cdf8d1b42 return redirect("/") video = request.args.get("video") @@ -49,3 +56,23 @@ def index(sound=None, text=None, video=None): subprocess.Popen(["pkill", "-f", "omxplayer"]) return render_template("index.html", sounds=sounds) + +@app.route("/sounds/") +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"]) diff --git a/static/main.css b/static/main.css index 61220b3..ca89f8b 100644 --- a/static/main.css +++ b/static/main.css @@ -139,3 +139,28 @@ div #reset:hover { #local-mode-button .local { 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; +} + diff --git a/static/main.js b/static/main.js index e893bcc..3c30829 100644 --- a/static/main.js +++ b/static/main.js @@ -1,7 +1,11 @@ - var localModeEnabled = false; // References to howler objects +/* jshint esversion: 6 */ + +var localModeEnabled = false; + +// References to howler objects var howlerSounds = []; function ready(fn) { @@ -39,7 +43,6 @@ ready(function() { searchfield.focus(); searchfield.addEventListener("keyup", function() { - var buttons = document.querySelectorAll(".sound"); buttons.forEach(function(item) { @@ -48,7 +51,7 @@ ready(function() { buttons.forEach(function(item) { var name = item.firstChild.innerHTML; - + if (name.toLowerCase().indexOf(searchfield.value.toLowerCase()) === -1) { item.style.display = "none"; } @@ -64,6 +67,68 @@ ready(function() { 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 */ @@ -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."); 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() { toggleButton = document.getElementById("local-mode-button").children[0]; - if(localModeEnabled){ + if (localModeEnabled) { localModeEnabled = false; toggleButton.classList.remove("local"); toggleButton.innerHTML = "[S]"; toggleButton.title = "play sounds on server"; - }else{ + } else { localModeEnabled = true; toggleButton.classList.add("local"); - toggleButton.innerHTML = "[L]"; - toggleButton.title = "play sounds locally"; - } + toggleButton.innerHTML = "[L]"; + 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. */ function playSound(filename) { - if(localModeEnabled){ + if (localModeEnabled){ // play local audio using howler.js var sound = new Howl({ - src: ['/sounds/'+filename] + src: ['/sounds/' + filename] }); sound.play(); howlerSounds.push(sound); - }else{ - ajaxRequest("/play/"+filename); + } else { + ajaxRequest("/play/" + filename); } } function killAllHowlerAudio() { - for(i=0; iSounds Streams Voices + Images @@ -66,8 +67,19 @@ + + + + Image URL + + + Play! + + clear images +