diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb index 607eb053c..1002068bf 100644 --- a/ruby/lib/jam_ruby/models/claimed_recording.rb +++ b/ruby/lib/jam_ruby/models/claimed_recording.rb @@ -61,7 +61,7 @@ module JamRuby def has_mix? - recording.mixes.length > 0 && recording.mixes.first.completed + recording.has_mix? end def can_download?(some_user) diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb index a2aa8f8b4..fba123938 100644 --- a/ruby/lib/jam_ruby/models/recording.rb +++ b/ruby/lib/jam_ruby/models/recording.rb @@ -43,6 +43,10 @@ module JamRuby self.comments.size end + def has_mix? + self.mixes.length > 0 && self.mixes.first.completed + end + # this can probably be done more efficiently, but David needs this asap for a video def grouped_tracks tracks = [] diff --git a/web/app/assets/javascripts/feed_item_recording.js b/web/app/assets/javascripts/feed_item_recording.js index 55187e8a6..fbc030982 100644 --- a/web/app/assets/javascripts/feed_item_recording.js +++ b/web/app/assets/javascripts/feed_item_recording.js @@ -6,14 +6,46 @@ context.JK.FeedItemRecording = function($parentElement, options){ var $feedItem = $parentElement; - var $description = $('.description', $feedItem) - var $musicians = $('.musician-detail', $feedItem) + var $description = $('.description', $feedItem); + var $musicians = $('.musician-detail', $feedItem); + var $controls = $('.recording-controls', $feedItem); + var playing = false; var toggledOpen = false; 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.listenRecording'); + playing = true; + } + + function stopPlay() { + var img = $('.play-icon', $feedItem); + img.attr('src', '/assets/content/icon_playbutton.png'); + $controls.trigger('pause.listenRecording'); + playing = false; + } + + function togglePlay() { + + if(playing) { + stopPlay(); + } + else { + startPlay(); + } + return false; + } + + + function stateChange(e, data) { + if(data.isEnd) stopPlay(); + } + function toggleDetails() { if(toggledOpen) { $feedItem.css('height', $feedItem.height() + 'px') @@ -40,11 +72,14 @@ function events() { $('.details', $feedItem).click(toggleDetails); $('.details-arrow', $feedItem).click(toggleDetails); + $('.play-button', $feedItem).click(togglePlay); + $controls.bind('statechange.listenRecording', stateChange); } function initialize() { $('.timeago', $feedItem).timeago(); $('.dotdotdot', $feedItem).dotdotdot(); + $controls.listenRecording({sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'}); context.JK.prettyPrintElements($('time.duration', $feedItem)); context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem)); diff --git a/web/app/assets/javascripts/feed_item_session.js b/web/app/assets/javascripts/feed_item_session.js index 329f154f2..95e996b34 100644 --- a/web/app/assets/javascripts/feed_item_session.js +++ b/web/app/assets/javascripts/feed_item_session.js @@ -35,6 +35,7 @@ $controls.trigger('pause.listenBroadcast'); playing = false; } + function togglePlay() { if(playing) { $status.text('SESSION IN PROGRESS'); @@ -83,7 +84,6 @@ $('.details', $feedItem).click(toggleDetails); $('.details-arrow', $feedItem).click(toggleDetails); $('.play-button', $feedItem).click(togglePlay); - $controls.bind('statechange.listenBroadcast', stateChange); } @@ -91,29 +91,12 @@ $('.timeago', $feedItem).timeago(); $('.dotdotdot', $feedItem).dotdotdot(); $controls.listenBroadcast(); - - context.JK.prettyPrintElements($('time.duration', $feedItem)); + context.JK.prettyPrintElements($('time.duration', $feedItem).show()); context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem)); $feedItem.data('original-max-height', $feedItem.css('height')); events(); - - - // this is a bit lame, because this is a singleton. - // the idea is, if there are any session widgets on the page, then we start ticking time - if(!context.JK.FeedItemSessionTimer) { - context.JK.FeedItemSessionTimer = setInterval(function() { - $.each($('.feed-entry.music-session-history-entry .inprogress .session-duration'), function(index, item) { - var $duration = $(item); - - var createdAt = new Date(Number($duration.attr('data-created-at')) * 1000) - var millisElapsed = (new Date()).getTime() - createdAt.getTime(); - console.log("createdAt, millis", createdAt, millisElapsed); - $duration.text(context.JK.prettyPrintSeconds( parseInt(millisElapsed / 1000))); - }); - }, 333); - } } initialize(); diff --git a/web/app/assets/javascripts/jquery.listenRecording.js b/web/app/assets/javascripts/jquery.listenRecording.js new file mode 100644 index 000000000..2dd952c55 --- /dev/null +++ b/web/app/assets/javascripts/jquery.listenRecording.js @@ -0,0 +1,377 @@ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + // the purpose of this code is to simply the interaction for recording widgets + context.JK.ListenRecording = function($parentElement, options){ + + var WAIT_FOR_BUFFER_TIMEOUT = 5000; + + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var $parent = $parentElement; + var $currentTime = null; + var $slider = null; + var $sliderBar = null; + var $audio = null; + var audioDomElement = null; + var self = this; + var waitForBufferingTimeout = null; + var destroyed = false; + var lastTime = 0; + var dragging = 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 PlayStateFailedStart = 'failed_start'; // we could not start the stream. no more events are coming + var PlayStateFailedPlaying = 'failed_playing'; // failed while playing. + var PlayStateNetworkError = 'network_error'; // network error + + var playState = PlayStateNone; // tracks if the stream is actually playing + + var canPlay = false; + + function play(e) { + if(e) { + e.preventDefault(); + e.stopPropagation(); + } + + if(destroyed) return; + + if(!audioDomElement) throw "no audio element supplied; the user should not be able to attempt a play" + + if(canPlay) { + audioDomElement.currentTime = lastTime + } + + audioDomElement.play(); + + transition(PlayStateInitializing); + + // keep this after transition, because any transition clears this timer + waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); + } + + function pause(e) { + if(e) { + e.preventDefault(); + e.stopPropagation(); + } + + if(destroyed) return; + + if(!audioDomElement) throw "no audio element supplied; the user should not be able to attempt a pause" + + transition(PlayStateNone); + + audioDomElement.pause(); + //recreateAudioElement(); + } + + function destroy() { + if(!destroyed) { + $audio.remove(); + $audio = null; + audioDomElement = null; + destroyed = true; + } + } + + + function noBuffer() { + logger.debug("never received indication of buffering or playing"); + transition(PlayStateFailedStart); + } + + // 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(); + + if( playState == PlayStateNone || + playState == PlayStateEnded || + playState == PlayStateFailedStart || + playState == PlayStateFailedPlaying || + playState == PlayStateNetworkError) { + context.JK.ListenBroadcastCurrentlyPlaying = null; + } + + triggerStateChange(); + } + + + function isNoisyEvent(eventName) { + if(playState == PlayStateNone) { + console.log("ignoring: " + eventName) + return true; + } + + return false; + } + + function onPlay() { + // this just confirms that the user tried to play + } + + function onPlaying() { + if(isNoisyEvent('playing')) return; + logger.debug("playing", arguments); + + transition(PlayStatePlaying); + } + + function onPause() { + if(isNoisyEvent('pause')) return; + logger.debug("pause", arguments); + + transition(PlayStateStalled); + } + + function onError() { + if(isNoisyEvent('error')) return; + logger.debug("error", arguments); + + if(playState == PlayStatePlaying || playState == PlayStateStalled) { + transition(PlayStateFailedPlaying); + } + else { + transition(PlayStateFailedStart); + } + } + + function onEnded() { + if(isNoisyEvent('ended')) return; + logger.debug("ended", arguments); + + transition(PlayStateEnded); + } + + function onEmptied() { + if(isNoisyEvent('emptied')) return; + logger.debug("emptied", arguments); + } + + function onAbort() { + if(isNoisyEvent('abort')) return; + logger.debug("abort", arguments); + } + + function onStalled() { + 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() { + + if(dragging) { + return; + } + + var percentComplete = (audioDomElement.currentTime / audioDomElement.duration) * 100; + updateSliderPosition(percentComplete); + $currentTime.html(context.JK.prettyPrintSeconds(parseInt(audioDomElement.currentTime))); + + // reset icon to play and slider to far left when done + if (percentComplete === 100) { + updateSliderPosition(0); + $currentTime.html("0:00"); + lastTime = 0 + } + else { + lastTime = audioDomElement.currentTime; + } + } + + function updateSliderPosition(percent) { + console.log("updateSliderPosition", percent) + $slider.css({'left': percent + '%'}); + } + + function onProgress() { + if(isNoisyEvent('progress')) return; + + if(playState == PlayStateInitializing) { + transition(PlayStateBuffering); + } + } + + function startDrag(e, ui) { + dragging = true; + } + + function stopDrag(e, ui) { + dragging = false; + + var percent = ui.position.left / $sliderBar.width() * 100; + updateSliderPosition(percent); + + audioDomElement.currentTime = percent * audioDomElement.duration; + } + + function onDrag(e, ui) { + updateSliderPosition(ui.position.left / $sliderBar.width() * 100); + } + + function triggerStateChange() { + + var isEnd = false; + var displayText = null; + var refresh = false; + + if(playState == 'none') { + //$status.text('SESSION IN PROGRESS'); + } + else if(playState == 'initializing') { + displayText = 'PREPARING AUDIO'; + } + else if(playState == 'buffering') { + + } + else if(playState == 'playing') { + displayText = 'PLAYING'; + } + else if(playState == 'stalled') { + displayText = 'RECONNECTING'; + } + else if(playState == 'ended') { + displayText = 'DISCONNECTED'; + isEnd = true; + } + else if(playState == 'failed_start') { + displayText = 'AUDIO DID NOT START'; + isEnd = true; + } + else if(playState == 'failed_playing') { + displayText = 'STREAM DISCONNECTED'; + isEnd = true; + } + else if(playState == 'network_error') { + displayText = 'NO NETWORK'; + isEnd = true; + } + else { + logger.error("unknown state: " + playState) + } + + $parent.triggerHandler('statechange.listenRecording', + { + state: playState, + displayText: displayText, + isEnd: isEnd + }) + } + + 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); + $audio.bind('canplay', function() { canPlay = true;}) + } + + function initialize() { + + $slider = $(options.sliderSelector, $parent); + $sliderBar = $(options.sliderBarSelector, $parent); + $currentTime = $(options.currentTimeSelector, $parent); + $audio = $('audio', $parent); + + if($audio.length == 0) { + logger.debug("listen_recording: no audio element. deactivating") + return; + } + if($audio.length > 1) { + throw "more than one