forked from server/soundboard
Merge branch 'master' of https://git.wiai.de/mmueller/soundboard
Conflicts: soundboard.py static/main.css static/main.js templates/index.html
This commit is contained in:
commit
95bc31dc24
25
README.rst
25
README.rst
@ -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
|
||||||
@ -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
12
devserver.sh
Executable 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
|
||||||
@ -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
|
||||||
@ -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"])
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
106
static/main.js
106
static/main.js
@ -1,3 +1,7 @@
|
|||||||
|
var localModeEnabled = false;
|
||||||
|
|
||||||
|
// References to howler objects
|
||||||
|
/* jshint esversion: 6 */
|
||||||
|
|
||||||
var localModeEnabled = false;
|
var localModeEnabled = false;
|
||||||
|
|
||||||
@ -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) {
|
||||||
@ -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,12 +227,12 @@ 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]";
|
||||||
@ -170,20 +244,20 @@ function toggleLocalMode() {
|
|||||||
* 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");
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user