Skip to content

Commit

Permalink
Added info about setting up MPD for streaming in the README.
Browse files Browse the repository at this point in the history
Created start_bragi shellscript to run a websockify instance that can also serve as webserver for Bragi.
Made updatePlaytime less frequent to possibly save battery on mobile devices.
'Static' vars now initialized in init().
Password prompt now shows instance name.
Seekbar position only updated when user isn't touching it.
Seek commands are now rate limited to 1 per second at most to avoid filling the command queue, which would essentially disconnect Bragi.
Further fixes to stream error handling.
Stream error handler now retries with increasing delay.
  • Loading branch information
josedpedroso committed Mar 23, 2018
1 parent 923a932 commit f45a365
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 35 deletions.
22 changes: 20 additions & 2 deletions README.md
Expand Up @@ -74,10 +74,10 @@ If you have more than one output defined for an instance the outputs tab will be
If you have more than one instance configured, here you can switch between them. Password management is also handled in this screen if you have more than one instance (if you just have one instance you will be prompted)

![settings](https://raw.githubusercontent.com/wiki/bobboau/Bragi-MPD/img/screenshots/Bragi-settings.png)
Basic MPD play settings can be controlled by Bgragi-MPD too.
Basic MPD play settings can be controlled by Bragi-MPD too.

Installation and Setup
---------------------
----------------------

You must have a [working installation of MPD](http://www.musicpd.org/doc/user/) for many Linux distributions this is little more than 'apt-get install mpd'.

Expand All @@ -101,3 +101,21 @@ The first section is defined by the key 'clients'. This is the list of MPD insta
There is also a 'theme' section, here you can specify an array of additional CSS files to load. If the theme key is present it is expected to be an array full of strings which are URLs of css files (relative to the page's url). These additional files will be loaded following the normal files and will override them assuming they have equal or greater [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity).

For more config details see the [wiki page](https://github.com/bobboau/Bragi-MPD/wiki/Configuration)

MPD configuration for streaming
-------------------------------

Your mpd.conf needs at least one httpd output section for streaming to work. In most cases, the encoder should be LAME (MP3). If you're running MPD on a low power device, such as a router, shine is a usable alternative, but audio quality will suffer noticeably for any given bitrate.

audio_output {
type "httpd" #mandatory for streaming
name "HTTP MP3 stream" #name it anything you like
encoder "lame" #or "shine" if lame uses too much cpu time
bitrate "160" #160kbps seems like a good balance of quality and low mobile data usage
port "58000" #port where MPD listens for streaming clients, this is what you'll put in stream_port in config.js
bind_to_address "0.0.0.0" #listen on all network interfaces
format "44100:16:2" #be sure to use a fixed stream format, like this one (44KHz, 16 bit, stereo)
always_on "no" #not needed for Bragi, enable only if necessary for other clients
max_clients "4" #0 for no limit
tags "yes" #for more track info, especially in other clients
}
2 changes: 1 addition & 1 deletion index.html
Expand Up @@ -334,7 +334,7 @@
</div>
<div class="hbox">
<div class="MPD_prev MPD_button" onclick="UI.previous();"></div>
<input class="MPD_seek" type="range" min="0" oninput="UI.seek(this);" ></input>
<input class="MPD_seek" type="range" min="0" oninput="UI.seek(this);"></input>
<div class="MPD_next MPD_button" onclick="UI.next();"></div>
</div>
</div>
Expand Down
81 changes: 49 additions & 32 deletions js/UI.js
Expand Up @@ -91,16 +91,21 @@ var UI = (function(){

setInterval(function(){
updatePlaytime(getClient());
},150);
}, 250);

if(!mobileCheck()){
setInterval(function(){
updatePageTitle(getClient());
},250);
}, 250);
}

//setup event handlers for marque elements
setupMarque();

//initialize 'static' variables here so we don't have to check if they're set all the time
updatePageTitle.offset = 0;
seek.last_seek = 0;
onStreamError.last_error = 0;
}

/*******************\
Expand Down Expand Up @@ -422,14 +427,7 @@ var UI = (function(){
var playing = (client.getPlaystate() == 'play');

if(client.stream_port && playing){
var no_cache = '?no_cache=';
var current_url = stream.src.split(no_cache, 2)[0];

if(current_url != client.stream_url){
stream.src = client.stream_url + no_cache + Math.random() * 99999999;
}

//make sure the stream keeps playing. UI.streamError() might give up, so try playing now that state has changed
//make sure the stream keeps playing. UI.streamError() gives up after a few errors, so try playing now that state has changed
playStream(stream);
}
else{
Expand Down Expand Up @@ -674,7 +672,7 @@ var UI = (function(){
var password = localStorage.getItem('password_'+client.name);
// we\the user tried to do something we are not allowed to do
if(UI.clients.length === 1){
var password = prompt('please enter a password');
var password = prompt('Please enter password for '+client.name);
localStorage.setItem('password_'+client.name, password);
client.authorize(password);
}
Expand All @@ -695,7 +693,10 @@ var UI = (function(){
var current_song = client.getCurrentSong();
if(current_song){
//there is a mix of div/span/td type html and input/select typeelements
$('input.MPD_seek').val(Math.round(client.getCurrentSongTime()));
if(getTime() - seek.last_seek > 2) {
//update the seekbar only if the user hasn't touched it in a bit
$('input.MPD_seek').val(Math.round(client.getCurrentSongTime()));
}
var formatted_time = formatTime(client.getCurrentSongTime());
$('.MPD_controller_current_song_time').html(formatted_time);
}
Expand All @@ -714,9 +715,6 @@ var UI = (function(){
* update our UI for ticking of play time
*/
function updatePageTitle(client){
if(typeof updatePageTitle.offset === 'undefined'){
updatePageTitle.offset = 0;
}
var current_song = client.getCurrentSong();
if(current_song){
var title = current_song.getDisplayName()+' - ';
Expand Down Expand Up @@ -1126,8 +1124,11 @@ var UI = (function(){
*/
function seek(element){
var client = getClient();
if(client.getCurrentSong()){
var current_time = getTime();
if(client.getCurrentSong() && current_time - seek.last_seek >= 1){
//seek only once per second at most to avoid filling the MPD.js queue with seek commands
client.seek($(element).val());
seek.last_seek = current_time;
}
}

Expand Down Expand Up @@ -1945,13 +1946,28 @@ var UI = (function(){
}

/**
* play stream, handling promises to avoid spamming the console with errors
* utility function, returns current time in seconds
*/
function getTime(stream){
return new Date().getTime() / 1000;
}

/**
* play stream, changing the url if needed and handling promises to avoid spamming the console with errors
*/
function playStream(stream){
if(!stream.src || !stream.paused){
if(!stream.paused){
return;
}

var client = getClient();
var no_cache = '?no_cache=';
var current_url = stream.src.split(no_cache, 2)[0];

if(current_url != client.stream_url){
stream.src = client.stream_url + no_cache + Math.random() * 99999999;
}

stream.load();

var promise = stream.play();
Expand All @@ -1961,9 +1977,7 @@ var UI = (function(){
}

/**
* stop stream and prevent buffering by setting empty source.
* not ideal as this causes a slight delay when playing again.
* nevertheless, better than pausing and having sound totally out of sync when playing again.
* stop stream and prevent buffering by setting empty source
*/
function stopStream(stream){
stream.pause();
Expand All @@ -1973,31 +1987,34 @@ var UI = (function(){
}

/**
* handle stream errors by retrying 2 seconds later up to 10 times a minute
* handle stream errors by retrying up to 10 times in 2 minutes, with increasing delay
*/
function onStreamError(stream){
stream.pause();
//stop stream completely to make sure that playStream() will do its thing and to give the network some idle time until we actually retry
stopStream(stream);

if(typeof onStreamError.last_error == 'undefined'){
onStreamError.last_error = 0;
}

//reset error counter when last error is more than 1 minute ago (or never)
var current_time = new Date().getTime();
if(current_time - onStreamError.last_error > 60000){
//reset error counter when last error is more than 2 minutes ago
var current_time = getTime();
if(current_time - onStreamError.last_error > 120){
onStreamError.error_counter = 0;
}

//don't retry if error limit was reached or if we're already about to retry
if(onStreamError.error_counter >= 10 || onStreamError.timer){
console.log('Stream error, not retrying');
return;
}

onStreamError.last_error = current_time;
onStreamError.error_counter++;
console.log('Stream error ' + onStreamError.error_counter);

//retry with delay
onStreamError.timer = setTimeout(function(){
onStreamError.timer = null;
console.log('Stream retry ' + onStreamError.error_counter);
playStream(stream);
}, 2000);
onStreamError.timer = null;
}, 2000 * onStreamError.error_counter);
}

return {
Expand Down
2 changes: 2 additions & 0 deletions start_bragi
@@ -0,0 +1,2 @@
#!/bin/sh
websockify --daemon --web=. 0.0.0.0:8800 localhost:6600

0 comments on commit f45a365

Please sign in to comment.