diff --git a/ruby/Gemfile b/ruby/Gemfile
index 4856141b3..7ba18443c 100644
--- a/ruby/Gemfile
+++ b/ruby/Gemfile
@@ -33,6 +33,7 @@ gem 'carrierwave'
gem 'aasm', '3.0.16'
gem 'devise', '>= 1.1.2'
gem 'postgres-copy'
+gem 'geokit'
gem 'geokit-rails'
gem 'postgres_ext'
gem 'resque'
diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb
index d7c05c345..4e840a9ee 100755
--- a/ruby/lib/jam_ruby.rb
+++ b/ruby/lib/jam_ruby.rb
@@ -11,6 +11,7 @@ require "action_mailer"
require "devise"
require "sendgrid"
require "postgres-copy"
+require "geokit"
require "geokit-rails"
require "postgres_ext"
require 'builder'
diff --git a/web/app/assets/javascripts/feed_item_recording.js b/web/app/assets/javascripts/feed_item_recording.js
index 9b6c7bdd9..55187e8a6 100644
--- a/web/app/assets/javascripts/feed_item_recording.js
+++ b/web/app/assets/javascripts/feed_item_recording.js
@@ -16,13 +16,12 @@
function toggleDetails() {
if(toggledOpen) {
-
$feedItem.css('height', $feedItem.height() + 'px')
$feedItem.animate({'height': $feedItem.data('original-max-height')}).promise().done(function() {
$feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height'));
$musicians.hide();
- $description.css('height', $description.css('height'));
+ $description.css('height', $description.data('original-height'));
$description.dotdotdot();
});
}
diff --git a/web/app/assets/javascripts/feed_item_session.js b/web/app/assets/javascripts/feed_item_session.js
index 63d49750a..e8c14a3c1 100644
--- a/web/app/assets/javascripts/feed_item_session.js
+++ b/web/app/assets/javascripts/feed_item_session.js
@@ -5,47 +5,60 @@
context.JK = context.JK || {};
context.JK.FeedItemSession = function($parentElement, options){
+ var logger = context.JK.logger;
+ var rest = new context.JK.Rest();
+
var $feedItem = $parentElement;
var $description = $('.description', $feedItem)
var $musicians = $('.musician-detail', $feedItem)
var $controls = $('.session-controls', $feedItem);
+ var $status = $('.session-status', $feedItem);
var playing = false;
var toggledOpen = false;
+ var musicSessionId = $feedItem.attr('data-music-session');
if(!$feedItem.is('.feed-entry')) {
throw "$parentElement must be a .feed-entry"
}
+ function startPlay() {
+ var img = $('.play-icon', $feedItem);
+ img.attr('src', '/assets/content/icon_pausebutton.png');
+ $controls.trigger('play.listenBroadcast');
+ playing = true;
+ }
+
+ function stopPlay() {
+ var img = $('.play-icon', $feedItem);
+ img.attr('src', '/assets/content/icon_playbutton.png');
+ $controls.trigger('pause.listenBroadcast');
+ playing = false;
+ }
function togglePlay() {
if(playing) {
- var img = $('.play-icon', $(this));
- img.attr('src', '/assets/content/icon_playbutton.png');
- $controls.trigger('pause.listenBroadcast');
+ $status.text('SESSION IN PROGRESS');
+ stopPlay();
}
else {
- var img = $('.play-icon', $(this));
- img.attr('src', '/assets/content/icon_pausebutton.png');
- $controls.trigger('play.listenBroadcast');
+ startPlay();
}
-
- playing = !playing;
-
return false;
}
function toggleDetails() {
if(toggledOpen) {
-
$feedItem.css('height', $feedItem.height() + 'px')
$feedItem.animate({'height': $feedItem.data('original-max-height')}).promise().done(function() {
$feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height'));
$musicians.hide();
+ $description.css('height', $description.data('original-height'));
$description.dotdotdot();
});
}
else {
$description.trigger('destroy.dot');
+ $description.data('original-height', $description.css('height')).css('height', 'auto');
$musicians.show();
$feedItem.animate({'max-height': '1000px'});
}
@@ -55,10 +68,84 @@
return false;
}
+ function refresh() {
+ return rest.getSession(musicSessionId)
+ .done(function(response) {
+ // we only refresh for sessions that are playing (no reason to bother for ended ones).
+ // so if we got a response. Then that's it. We are still OK
+ })
+ .fail(function(jqXHR) {
+ if(jqXHR.status == 404 || jqXHR.status == 403) {
+ $controls.trigger('destroy.listenBroadcast').removeClass('inprogress').addClass('ended')
+ $status.text("SESSION ENDED");
+ }
+ else if(jqXHR.status >= 500 && jqXHR.status <= 599){
+ $status.text('SERVER ERROR')
+ stopPlay();
+ }
+ else{
+ $status.text("NO NETWORK")
+ stopPlay();
+ }
+ })
+ }
+
+ function stateChange(e, data) {
+ var state = data.state;
+
+ if(state == 'none') {
+ //$status.text('SESSION IN PROGRESS');
+ }
+ else if(state == 'initializing') {
+ $status.text('PREPARING AUDIO');
+ }
+ else if(state == 'buffering') {
+
+ }
+ else if(state == 'playing') {
+ $status.text('SESSION IN PROGRESS');
+ }
+ else if(state == 'stalled') {
+ $status.text('RECONNECTING');
+ }
+ else if(state == 'ended' || state == 'session_over') {
+ $status.text('STREAM DISCONNECTED');
+ stopPlay();
+ refresh();
+ }
+ else if(state == 'retrying_play') {
+ if(data.retryCount == 2) {
+ $status.text('STILL TRYING, HANG ON');
+ }
+ }
+ else if(state == 'failed_start') {
+ $status.text('AUDIO DID NOT START');
+ stopPlay();
+ }
+ else if(state == 'failed_playing') {
+ $status.text('AUDIO FAILED');
+ stopPlay();
+ refresh();
+ }
+ else if(state == 'network_error') {
+ $status.text('NO NETWORK');
+ stopPlay();
+ }
+ else if(state == 'server_error') {
+ $status.text('SERVER ERROR');
+ stopPlay();
+ }
+ else {
+ logger.error("unknown state: " + state)
+ }
+ }
+
function events() {
$('.details', $feedItem).click(toggleDetails);
$('.details-arrow', $feedItem).click(toggleDetails);
$('.play-button', $feedItem).click(togglePlay);
+
+ $controls.bind('statechange.listenBroadcast', stateChange);
}
function initialize() {
diff --git a/web/app/assets/javascripts/jquery.listenbroadcast.js b/web/app/assets/javascripts/jquery.listenbroadcast.js
index 126044286..435cb56ce 100644
--- a/web/app/assets/javascripts/jquery.listenbroadcast.js
+++ b/web/app/assets/javascripts/jquery.listenbroadcast.js
@@ -27,22 +27,34 @@
// this will also interact with any REST APIs needed to
context.JK.ListenBroadcast = function($parentElement, options){
- var logger = context.JK.logger;
- var rest = context.JK.Rest();
- var $parent = $parentElement;
- var $audio = null;
- var audioDomElement = null;
- var musicSessionId = null;
+ var WAIT_FOR_BUFFER_TIMEOUT = 5000;
+ var RETRY_ATTEMPTS = 5; // we try 4 times, so the user will wait up until RETRY_ATTEMPTS * WAIT_FOR_BUFFER_TIMEOUTS
- var isPlaying = false;
- var PlayStateNone = 'none';
- var PlayStateInitializing = 'initializing'; // user clicked play--nothing has happened yet
- var PlayStateBuffering = 'buffering'; // user clicked play--the stream is being read, buffer is being built
- var PlayStatePlaying = 'playing'; // we are playing
- var PlayStateStalled = 'stalled'; // we were playing, but the stream is stalled
- var PlayStateEnded = 'ended'; // we were playing, but the stream ended
- var PlayStateFailed = 'failed'; // we could not start the stream.
+ var logger = context.JK.logger;
+ var rest = context.JK.Rest();
+ var $parent = $parentElement;
+ var $audio = null;
+ var audioDomElement = null;
+ var musicSessionId = null;
+ var waitForBufferingTimeout = null;
+ var retryAttempts = 0;
+
+ var destroyed = false;
+
+ var PlayStateNone = 'none';
+ var PlayStateInitializing = 'initializing'; // user clicked play--nothing has happened yet
+ var PlayStateBuffering = 'buffering'; // user clicked play--the stream is being read, buffer is being built
+ var PlayStatePlaying = 'playing'; // we are playing
+ var PlayStateStalled = 'stalled'; // we were playing, but the stream is stalled
+ var PlayStateEnded = 'ended'; // we were playing, but the stream ended
+ var PlayStateRetrying = 'retrying_play'; // we are retrying to play.
+ var PlayStateFailedStart = 'failed_start'; // we could not start the stream. no more events are coming
+ var PlayStateFailedPlaying = 'failed_playing'; // failed while playing.
+ var PlayStateSessionOver = 'session_over'; // session is done
+ var PlayStateNetworkError = 'network_error'; // network error
+ var PlayStateServerError = 'server_error'; // network error
+
var playState = PlayStateNone; // tracks if the stream is actually playing
@@ -52,67 +64,189 @@
e.stopPropagation();
}
+ if(destroyed) return;
+
if(!audioDomElement) throw "no audio element supplied; the user should not be able to attempt a play"
- audioDomElement.play();
+ rest.getSession(musicSessionId)
+ .done(function(response) {
+ audioDomElement.play();
+
+ retryAttempts = 0;
+
+ transition(PlayStateInitializing);
+
+ // keep this after transition, because any transition clears this timer
+ waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT);
+ })
+ .fail(function(jqXHR) {
+ if(jqXHR.status == 404 || jqXHR.status == 403) {
+ transition(PlayStateSessionOver);
+ }
+ else if(jqXHR.status >= 500 && jqXHR.status <= 599){
+ transition(PlayStateServerError);
+ }
+ else {
+ transition(PlayStateNetworkError);
+ }
+ })
}
+
function pause(e) {
if(e) {
e.preventDefault();
e.stopPropagation();
}
- if (!audioDomElement) throw "no audio element supplied; the user should not be able to attempt a pause"
+ if(destroyed) return;
+ if(!audioDomElement) throw "no audio element supplied; the user should not be able to attempt a pause"
+
+ transition(PlayStateNone);
+
+ recreateAudioElement();
+ }
+
+ function destroy(e) {
+ if(!destroyed) {
+ $audio.remove();
+ $audio = null;
+ audioDomElement = null;
+ destroyed = true;
+ }
+ }
+
+ // this is the only way to make audio stop buffering after the user hits pause
+ function recreateAudioElement() {
+ // jeez: http://stackoverflow.com/questions/4071872/html5-video-force-abort-of-buffering/13302599#13302599
+ var originalSource = $audio.html()
audioDomElement.pause();
+ audioDomElement.src = '';
+ audioDomElement.load();
+ var $parent = $audio.parent();
+ $audio.remove();
+ $parent.append('');
+ $audio = $('audio', $parent);
+ $audio.append(originalSource);
+ audioDomElement = $audio.get(0);
+ audioBind();
+ }
+
+ function clearBufferTimeout() {
+ if(waitForBufferingTimeout) {
+ clearTimeout (waitForBufferingTimeout);
+ waitForBufferingTimeout = null;
+ }
+ }
+ function transition(newState) {
+ logger.debug("transitioning from " + playState + " to " + newState);
+
+ playState = newState;
+
+ clearBufferTimeout();
+
+ $parent.triggerHandler('statechange.listenBroadcast', {state: playState, retryCount: retryAttempts});
+ }
+
+ function noBuffer() {
+
+ if(retryAttempts >= RETRY_ATTEMPTS) {
+ logger.debug("never received indication of buffering or playing");
+ transition(PlayStateFailedStart);
+ }
+ else {
+ retryAttempts++;
+
+ clearBufferTimeout();
+
+ // tell audio to stop/start, in attempt to retry
+ //audioDomElement.stop();
+ audioDomElement.load();
+ audioDomElement.play();
+
+ transition(PlayStateRetrying);
+
+ waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT);
+ }
+
+
+ }
+
+ function isNoisyEvent(eventName) {
+ if(playState == PlayStateNone) {
+ console.log("ignoring: " + eventName)
+ return true;
+ }
+
+ return false;
}
function onPlay() {
- logger.debug("onplay", arguments);
-
- isPlaying = true;
+ // this just confirms that the user tried to play
}
function onPlaying() {
- logger.debug("onplaying", arguments);
+ if(isNoisyEvent('playing')) return;
+ logger.debug("playing", arguments);
-
- isPlaying = true;
+ transition(PlayStatePlaying);
}
function onPause() {
- logger.debug("onpause", arguments);
+ if(isNoisyEvent('pause')) return;
+ logger.debug("pause", arguments);
- isPlaying = false;
+ transition(PlayStateStalled);
}
function onError() {
- logger.debug("onerror", arguments);
+ if(isNoisyEvent('error')) return;
+ logger.debug("error", arguments);
+
+ if(playState == PlayStatePlaying || playState == PlayStateStalled) {
+ transition(PlayStateFailedPlaying);
+ }
+ else {
+ transition(PlayStateFailedStart);
+ }
}
function onEnded() {
- logger.debug("onended", arguments);
+ if(isNoisyEvent('ended')) return;
+ logger.debug("ended", arguments);
+
+ transition(PlayStateEnded);
}
function onEmptied() {
- logger.debug("onemptied", arguments);
+ if(isNoisyEvent('emptied')) return;
+ logger.debug("emptied", arguments);
}
function onAbort() {
- logger.debug("onabort", arguments);
+ if(isNoisyEvent('abort')) return;
+ logger.debug("abort", arguments);
}
function onStalled() {
- logger.debug("onstalled", arguments);
+ if(isNoisyEvent('stalled')) return;
+ logger.debug("stalled", arguments);
// fires in Chrome on page load
+
+ if(playState == PlayStateBuffering || playState == PlayStatePlaying) {
+ transition(PlayStateStalled);
+ }
}
function onSuspend() {
+ if(isNoisyEvent('suspend')) return;
logger.debug("onsuspend", arguments);
// fires in FF on page load
+
+ transition(PlayStateStalled);
}
function onTimeUpdate() {
@@ -120,9 +254,27 @@
}
function onProgress() {
- //logger.debug("onprogress", arguments);
+ if(isNoisyEvent('progress')) return;
+
+ if(playState == PlayStateInitializing) {
+ transition(PlayStateBuffering);
+ }
}
+ function audioBind() {
+ $audio.bind('play', onPlay);
+ $audio.bind('playing', onPlaying);
+ $audio.bind('error', onError);
+ $audio.bind('emptied', onEmptied);
+ $audio.bind('abort', onAbort);
+ $audio.bind('ended', onEnded);
+ $audio.bind('pause', onPause);
+ $audio.bind('suspend', onSuspend);
+ $audio.bind('stalled', onStalled);
+ $audio.bind('timeupdate', onTimeUpdate);
+ $audio.bind('progress', onProgress);
+
+ }
function initialize() {
musicSessionId = $parent.attr('data-music-session');
@@ -138,38 +290,12 @@
throw "more than one