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