* VRFS-1047 - broadcast widget almost fully done

This commit is contained in:
Seth Call 2014-02-28 00:24:55 +00:00
parent dcc5437f3a
commit 51f955fc74
7 changed files with 288 additions and 72 deletions

View File

@ -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'

View File

@ -11,6 +11,7 @@ require "action_mailer"
require "devise"
require "sendgrid"
require "postgres-copy"
require "geokit"
require "geokit-rails"
require "postgres_ext"
require 'builder'

View File

@ -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();
});
}

View File

@ -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() {

View File

@ -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 preload="none"></audio>');
$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 <audio> element found";
}
audioDomElement = $audio.get(0);
function getAllEvents(element) {
var result = [];
for (var key in element) {
if (key.indexOf('on') === 0) {
result.push(key);
}
}
return result.join(' ');
}
console.log("ALL events names: " + getAllEvents(audioDomElement))
$audio.bind(getAllEvents(audioDomElement), function(e) { console.log("ALL EVENT:", arguments) });
$audio.bind('*', function() {alert('works')});
$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);
$audio.bind('load', function() { console.log("load", arguments)})
audioBind();
$parent.bind('play.listenBroadcast', play);
$parent.bind('pause.listenBroadcast', pause);
$parent.bind('destroy.listenBroadcast', destroy);
}
initialize();

View File

@ -1,4 +1,4 @@
.feed-entry.music-session-history-entry
.feed-entry.music-session-history-entry{'data-music-session' => feed_item.id}
/ avatar
.avatar-small.ib
= session_avatar(feed_item)

View File

@ -37,12 +37,14 @@ gem 'devise'
gem 'postgres-copy'
gem 'aws-sdk', '1.29.1'
gem 'bugsnag'
gem 'geokit-rails'
gem 'postgres_ext'
gem 'resque'
gem 'resque-retry'
gem 'resque-failed-job-mailer'
gem 'resque-lonely_job', '~> 1.0.0'
gem 'geokit'
gem 'geokit-rails', '2.0.1'
gem 'mime-types', '1.25.1'
group :development do
gem 'pry'