diff --git a/admin/config/application.rb b/admin/config/application.rb index 573e946f0..08b11d58c 100644 --- a/admin/config/application.rb +++ b/admin/config/application.rb @@ -126,6 +126,8 @@ module JamAdmin config.twitter_app_secret = ENV['TWITTER_APP_SECRET'] || 'Azcy3QqfzYzn2fsojFPYXcn72yfwa0vG6wWDrZ3KT8' config.ffmpeg_path = ENV['FFMPEG_PATH'] || (File.exist?('/usr/local/bin/ffmpeg') ? '/usr/local/bin/ffmpeg' : '/usr/bin/ffmpeg') + config.normalize_ogg_path = ENV['NORMALIZE_OGG_PATH'] || (File.exist?('/usr/local/bin/normalize-ogg') ? '/usr/local/bin/normalize-ogg' : '/usr/bin/normalize-ogg') + config.normalize_mp3_path = ENV['NORMALIZE_MP3_PATH'] || (File.exist?('/usr/local/bin/normalize-mp3') ? '/usr/local/bin/normalize-mp3' : '/usr/bin/normalize-mp3') config.max_audio_downloads = 100 diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index b9a7aeba0..5515757d9 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -481,6 +481,7 @@ message RecordingMasterMixComplete { optional string msg = 3; optional string notification_id = 4; optional string created_at = 5; + optional string claimed_recording_id = 6; } message DownloadAvailable { diff --git a/ruby/lib/jam_ruby/lib/nav.rb b/ruby/lib/jam_ruby/lib/nav.rb index 7f58dad61..bfc788471 100644 --- a/ruby/lib/jam_ruby/lib/nav.rb +++ b/ruby/lib/jam_ruby/lib/nav.rb @@ -27,6 +27,10 @@ module JamRuby "#{base_url}/findSession" end + def self.session(session) + "#{base_url}/session/#{session.id}" + end + private def self.base_url diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 7ece76e28..f5666089a 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -712,9 +712,10 @@ module JamRuby ) end - def recording_master_mix_complete(receiver_id, recording_id, band_id, msg, notification_id, created_at) + def recording_master_mix_complete(receiver_id, recording_id, claimed_recording_id, band_id, msg, notification_id, created_at) recording_master_mix_complete = Jampb::RecordingMasterMixComplete.new( :recording_id => recording_id, + :claimed_recording_id => claimed_recording_id, :band_id => band_id, :msg => msg, :notification_id => notification_id, diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 77b2c2394..adb481c81 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -1147,6 +1147,7 @@ module JamRuby msg = @@message_factory.recording_master_mix_complete( claimed_recording.user_id, recording.id, + claimed_recording.id, notification.band_id, notification_msg, notification.id, diff --git a/ruby/lib/jam_ruby/resque/audiomixer.rb b/ruby/lib/jam_ruby/resque/audiomixer.rb index 66973b12a..da3038622 100644 --- a/ruby/lib/jam_ruby/resque/audiomixer.rb +++ b/ruby/lib/jam_ruby/resque/audiomixer.rb @@ -322,7 +322,7 @@ module JamRuby raise "no output ogg file after mix" unless File.exist? @output_ogg_filename - ffmpeg_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{@output_ogg_filename}\" -ab 128k -metadata JamRecordingId=#{@manifest[:recording_id]} -metadata JamMixId=#{@mix_id} -metadata JamType=Mix \"#{@output_mp3_filename}\"" + ffmpeg_cmd = "#{APP_CONFIG.ffmpeg_path} -i \"#{@output_ogg_filename}\" -ab 192k -metadata JamRecordingId=#{@manifest[:recording_id]} -metadata JamMixId=#{@mix_id} -metadata JamType=Mix \"#{@output_mp3_filename}\"" system(ffmpeg_cmd) @@ -335,6 +335,30 @@ module JamRuby end raise "no output mp3 file after conversion" unless File.exist? @output_mp3_filename + + # time to normalize both mp3 and ogg files + + normalize_ogg_cmd = "#{APP_CONFIG.normalize_ogg_path} --bitrate 128 -i \"#{@output_ogg_filename}\"" + system(normalize_ogg_cmd) + unless $? == 0 + @error_reason = 'normalize-ogg-failed' + @error_detail = $?.to_s + error_msg = "normalize-ogg failed status=#{$?} error_reason=#{@error_reason} error_detail=#{@error_detail}" + @@log.info(error_msg) + raise error_msg + end + raise "no output ogg file after normalization" unless File.exist? @output_ogg_filename + + normalize_mp3_cmd = "#{APP_CONFIG.normalize_mp3_path} --bitrate 128 -i \"#{@output_mp3_filename}\"" + system(normalize_mp3_cmd) + unless $? == 0 + @error_reason = 'normalize-mp3-failed' + @error_detail = $?.to_s + error_msg = "normalize-mp3 failed status=#{$?} error_reason=#{@error_reason} error_detail=#{@error_detail}" + @@log.info(error_msg) + raise error_msg + end + raise "no output mp3 file after conversion" unless File.exist? @output_mp3_filename end def symbolize_keys(obj) diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 7c625ed78..0ed42f4d9 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -38,6 +38,14 @@ def app_config ENV['FFMPEG_PATH'] || '/usr/local/bin/ffmpeg' end + def normalize_ogg_path + ENV['NORMALIZE_OGG_PATH'] || '/usr/local/bin/normalize-ogg' + end + + def normalize_mp3_path + ENV['NORMALIZE_MP3_PATH'] || '/usr/local/bin/normalize-mp3' + end + def icecast_reload_cmd 'true' # as in, /bin/true end diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index 1e56483f3..e91e049b8 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -25,6 +25,7 @@ var notificationBatchSize = 20; var currentNotificationPage = 0; var didLoadAllNotifications = false, isLoading = false; + var ui = new context.JK.UIHelper(JK.app); function isNotificationsPanelVisible() { return $contents.is(':visible') @@ -1101,7 +1102,7 @@ "class": "button-orange", callback: shareRecording, callback_args: { - "recording_id": payload.recording_id + "claimed_recording_id": payload.claimed_recording_id } }] ); @@ -1109,7 +1110,9 @@ } function shareRecording(args) { - var recordingId = args.recording_id; + var claimedRecordingId = args.claimed_recording_id; + + ui.launchShareDialog(claimedRecordingId, 'recording'); } function registerBandInvitation() { diff --git a/web/app/assets/javascripts/recording_utils.js.coffee b/web/app/assets/javascripts/recording_utils.js.coffee index 3a6dbd002..925029c7d 100644 --- a/web/app/assets/javascripts/recording_utils.js.coffee +++ b/web/app/assets/javascripts/recording_utils.js.coffee @@ -84,17 +84,25 @@ class RecordingUtils mixStateClass = 'discarded' mixState = 'discarded' else - mixStateMsg = 'STILL UPLOADING' - mixStateClass = 'still-uploading' - mixState = 'still-uploading' - return { + if mix.fake and mix.discarded + mixStateMsg = 'DISCARDED' + mixStateClass = 'discarded' + mixState = 'discarded' + else + mixStateMsg = 'STILL UPLOADING' + mixStateClass = 'still-uploading' + mixState = 'still-uploading' + + result = { mixStateMsg: mixStateMsg, mixStateClass: mixStateClass, mixState: mixState, isError: mixState == 'error' } + result + onMixHover: () -> $mix = $(this).closest('.mix') mixStateInfo = $mix.data('mix-state') @@ -114,7 +122,10 @@ class RecordingUtils serverInfo = $mix.data('server-info') # lie if this is a virtualized mix (i.e., mix is created after recording is made) - mixState = 'still-uploading' if !serverInfo? or serverInfo.fake + if !serverInfo? + mixState = 'still-uploading' + else if serverInfo.fake + mixState = if serverInfo.discarded then 'discarded' else 'still-uploading' summary = '' if mixState == 'still-uploading' diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index a49148209..c3dbad3b9 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -371,6 +371,14 @@ if(response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track") { app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')); } + else if(response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"]) { + promptLeave = false; + context.window.location = "/client#/findSession"; + app.notify( { title: "Unable to Join Session", text: "The session is currently recording." }, null, true); + } + else { + app.notifyServerError(xhr, 'Unable to Join Session'); + } } else { app.notifyServerError(xhr, 'Unable to Join Session'); @@ -396,7 +404,12 @@ if(screenActive) { // this path is possible if FTUE is invoked on session page, and they cancel sessionModel.leaveCurrentSession() - .fail(app.ajaxError); + .fail(function(jqXHR) { + if(jqXHR.status != 404) { + logger.debug("leave session failed"); + app.ajaxError(arguments) + } + }); } screenActive = false; diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 9bfb3409d..60d8862b8 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -184,10 +184,10 @@ leaveSessionRest(currentSessionId) .done(function() { sessionChanged(); - deferred.resolve(arguments); + deferred.resolve(arguments[0], arguments[1], arguments[2]); }) .fail(function() { - deferred.reject(arguments); + deferred.reject(arguments[0], arguments[1], arguments[2]); }); // 'unregister' for callbacks diff --git a/web/app/assets/javascripts/session_utils.js b/web/app/assets/javascripts/session_utils.js index 482aaeab1..0c22b0108 100644 --- a/web/app/assets/javascripts/session_utils.js +++ b/web/app/assets/javascripts/session_utils.js @@ -133,6 +133,11 @@ rest.getSession(sessionId) .done(function(response) { session = response; + if(session && session.recording) { + context.JK.app.notify( { title: "Unable to Join Session", text: "The session is currently recording." }, null, true); + return; + } + if ("invitations" in session) { var invitation; // user has invitations for this session diff --git a/web/app/assets/javascripts/sync_viewer.js.coffee b/web/app/assets/javascripts/sync_viewer.js.coffee index 51dd2637f..4be93ba13 100644 --- a/web/app/assets/javascripts/sync_viewer.js.coffee +++ b/web/app/assets/javascripts/sync_viewer.js.coffee @@ -220,6 +220,7 @@ context.JK.SyncViewer = class SyncViewer updateTrackState: ($track) => clientInfo = $track.data('client-info') serverInfo = $track.data('server-info') + myTrack = serverInfo.user.id == context.JK.currentUserId # determine client state clientStateMsg = 'UNKNOWN' @@ -261,7 +262,7 @@ context.JK.SyncViewer = class SyncViewer uploadStateClass = 'error' uploadState = @uploadStates.too_many_upload_failures else - if serverInfo.user.id == context.JK.currentUserId + if myTrack if clientInfo? if clientInfo.local_state == 'HQ' uploadStateMsg = 'PENDING UPLOAD' @@ -282,7 +283,7 @@ context.JK.SyncViewer = class SyncViewer else uploadStateMsg = 'UPLOADED' uploadStateClass = 'uploaded' - if serverInfo.user.id == context.JK.currentUserId + if myTrack uploadState = @uploadStates.me_uploaded else uploadState = @uploadStates.them_uploaded @@ -301,6 +302,30 @@ context.JK.SyncViewer = class SyncViewer $uploadStateMsg.text(uploadStateMsg) $uploadStateProgress.css('width', '0') + # this allows us to make styling decisions based on the combination of both client and upload state. + $track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}") + + $clientRetry = $clientState.find('.retry') + $uploadRetry = $uploadState.find('.retry') + + if gon.isNativeClient + # handle client state + + # only show RETRY button if you have a SQ or if it's missing, and it's been uploaded already + if (clientState == @clientStates.sq or clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded) + $clientRetry.show() + else + $clientRetry.hide() + + # only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it + if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon) + $uploadRetry.show() + else + $uploadRetry.hide() + else + $clientRetry.hide() + $uploadRetry.hide() + associateClientInfo: (recording) => for clientInfo in recording.local_tracks $track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']") @@ -485,7 +510,14 @@ context.JK.SyncViewer = class SyncViewer recordingInfo = null if userSync == 'fake' recordingInfo = arguments[1] - userSync = { recording_id: recordingInfo.id, duration: recordingInfo.duration, fake:true } + # sift through the recorded_tracks in here; if they are marked discarded, then we can also mark this one discarded too + discarded = true + for claim in recordingInfo.claimed_recordings + if claim.user_id == context.JK.currentUserId + discarded = false + break + + userSync = { recording_id: recordingInfo.id, duration: recordingInfo.duration, fake:true, discarded: discarded } $mix = $(context._.template(@templateMix.html(), userSync, {variable: 'data'})) else $mix = $(context._.template(@templateMix.html(), userSync, {variable: 'data'})) @@ -504,14 +536,16 @@ context.JK.SyncViewer = class SyncViewer $track.data('sync-viewer', this) $clientState = $track.find('.client-state') $uploadState = $track.find('.upload-state') - $clientState.find('.retry').click(this.retryDownloadRecordedTrack) - $uploadState.find('.retry').click(this.retryUploadRecordedTrack) + $clientStateRetry = $clientState.find('.retry') + $clientStateRetry.click(this.retryDownloadRecordedTrack) + $uploadStateRetry = $uploadState.find('.retry') + $uploadStateRetry.click(this.retryUploadRecordedTrack) context.JK.bindHoverEvents($track) context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true}); context.JK.hoverBubble($clientState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) context.JK.hoverBubble($uploadState, this.onHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) - $clientState.addClass('is-native-client') if context.jamClient.IsNativeClient() - $uploadState.addClass('is-native-client') if context.jamClient.IsNativeClient() + $clientState.addClass('is-native-client') if gon.isNativeClient + $uploadState.addClass('is-native-client') if gon.isNativeClient $track createStreamMix: (userSync) => @@ -523,8 +557,8 @@ context.JK.SyncViewer = class SyncViewer $uploadState.find('.retry').click(this.retryUploadRecordedTrack) context.JK.hoverBubble($clientState, this.onStreamMixHover, {width:'450px', closeWhenOthersOpen: true, positions:['left']}) context.JK.hoverBubble($uploadState, this.onStreamMixHover, {width:'450px', closeWhenOthersOpen: true, positions:['right']}) - $clientState.addClass('is-native-client') if context.jamClient.IsNativeClient() - $uploadState.addClass('is-native-client') if context.jamClient.IsNativeClient() + $clientState.addClass('is-native-client') if gon.isNativeClient + $uploadState.addClass('is-native-client') if gon.isNativeClient $track exportRecording: (e) => diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss index 856ad5f21..e2f174fe0 100644 --- a/web/app/assets/stylesheets/client/common.css.scss +++ b/web/app/assets/stylesheets/client/common.css.scss @@ -180,6 +180,7 @@ $fair: #cc9900; @mixin client-state-box { + .client-state { position:relative; text-align:center; @@ -193,7 +194,6 @@ $fair: #cc9900; &.error { background-color: $error; - &.is-native-client { .retry { display:inline-block; } } } &.hq { @@ -202,12 +202,10 @@ $fair: #cc9900; &.sq { background-color: $good; - &.is-native-client { .retry { display:inline-block; } } } &.missing { background-color: $error; - &.is-native-client { .retry { display:inline-block; } } } &.discarded { @@ -240,7 +238,6 @@ $fair: #cc9900; &.error { background-color: $error; - &.is-native-client { .retry { display:inline-block; } } } &.missing { @@ -249,7 +246,6 @@ $fair: #cc9900; &.upload-soon { background-color: $fair; - &.is-native-client { .retry { display:inline-block; } } } &.uploaded { @@ -278,7 +274,7 @@ $fair: #cc9900; color:white; &.still-uploading { background-color: $fair; } - &.discard {background-color: $unknown; } + &.discarded {background-color: $unknown; } &.unknown { background-color: $unknown; } &.error { background-color: $error; } &.mixed { background-color: $good; } diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb index abd4dc5c3..1fcd99b1b 100644 --- a/web/app/controllers/api_recordings_controller.rb +++ b/web/app/controllers/api_recordings_controller.rb @@ -1,6 +1,7 @@ class ApiRecordingsController < ApiController before_filter :api_signed_in_user, :except => [ :add_like ] + before_filter :lookup_recording, :only => [ :show, :stop, :claim, :discard, :keep ] before_filter :lookup_recorded_track, :only => [ :download, :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ] before_filter :lookup_recorded_video, :only => [ :video_upload_sign, :video_upload_start, :video_upload_complete ] @@ -239,6 +240,61 @@ class ApiRecordingsController < ApiController end + def upload_next_part_stream_mix + length = params[:length] + md5 = params[:md5] + + @quick_mix.upload_next_part(length, md5) + + if @quick_mix.errors.any? + + response.status = :unprocessable_entity + # this is not typical, but please don't change this line unless you are sure it won't break anything + # this is needed because after_rollback in the RecordedTrackObserver touches the model and something about it's + # state doesn't cause errors to shoot out like normal. + render :json => { :errors => @quick_mix.errors }, :status => 422 + else + result = { + :part => @quick_mix.next_part_to_upload, + :offset => @quick_mix.file_offset.to_s + } + + render :json => result, :status => 200 + end + + end + + def upload_sign_stream_mix + render :json => @quick_mix.upload_sign(params[:md5]), :status => 200 + end + + def upload_part_complete_stream_mix + part = params[:part] + offset = params[:offset] + + @quick_mix.upload_part_complete(part, offset) + + if @quick_mix.errors.any? + response.status = :unprocessable_entity + respond_with @quick_mix + else + render :json => {}, :status => 200 + end + end + + def upload_complete_stream_mix + @quick_mix.upload_complete + + if @quick_mix.errors.any? + response.status = :unprocessable_entity + respond_with @quick_mix + return + else + render :json => {}, :status => 200 + end + end + + def upload_next_part_stream_mix length = params[:length] md5 = params[:md5] @@ -305,14 +361,14 @@ class ApiRecordingsController < ApiController raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_track.recording.has_access?(current_user) end - def lookup_recorded_video - @recorded_video = RecordedVideo.find_by_recording_id_and_client_video_source_id!(params[:id], params[:video_id]) - raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_video.recording.has_access?(current_user) - end - def lookup_stream_mix @quick_mix = QuickMix.find_by_recording_id_and_user_id!(params[:id], current_user.id) raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @quick_mix.recording.has_access?(current_user) end + def lookup_recorded_video + @recorded_video = RecordedVideo.find_by_recording_id_and_client_video_source_id!(params[:id], params[:video_id]) + raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR unless @recorded_video.recording.has_access?(current_user) + end + end # class diff --git a/web/app/views/recordings/show.html.erb b/web/app/views/recordings/show.html.erb index 747f6c383..b8e2bf74a 100644 --- a/web/app/views/recordings/show.html.erb +++ b/web/app/views/recordings/show.html.erb @@ -18,7 +18,7 @@ <% end %>