diff --git a/db/up/recordings_public_launch.sql b/db/up/recordings_public_launch.sql index fcab08d92..be7323e75 100644 --- a/db/up/recordings_public_launch.sql +++ b/db/up/recordings_public_launch.sql @@ -1,9 +1,9 @@ --- so that columns can live on +-- so that rows can live on after session is over ALTER TABLE recordings DROP CONSTRAINT "recordings_music_session_id_fkey"; +-- unambiguous declartion that the recording is over or not ALTER TABLE recordings ADD COLUMN is_done BOOLEAN DEFAULT FALSE; ---ALTER TABLE music_session ADD COLUMN is_recording BOOLEAN DEFAULT FALSE; - +-- add name and description on claimed_recordings, which is the user's individual view of a recording ALTER TABLE claimed_recordings ADD COLUMN description VARCHAR(8000); ALTER TABLE claimed_recordings ADD COLUMN description_tsv tsvector; ALTER TABLE claimed_recordings ADD COLUMN name_tsv tsvector; @@ -19,12 +19,9 @@ tsvector_update_trigger(name_tsv, 'public.jamenglish', name); CREATE INDEX claimed_recordings_description_tsv_index ON claimed_recordings USING gin(description_tsv); CREATE INDEX claimed_recordings_name_tsv_index ON claimed_recordings USING gin(name_tsv); ---ALTER TABLE recordings ADD COLUMN is_kept BOOLEAN NOT NULL DEFAULT false; - ---ALTER TABLE recordings ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT true; ---ALTER TABLE recordings ADD COLUMN is_downloadable BOOLEAN NOT NULL DEFAULT true; ---ALTER TABLE recordings ADD COLUMN genre_id VARCHAR(64) NOT NULL REFERENCES genres(id); - -- copies of connection.client_id and track.id ALTER TABLE recorded_tracks ADD COLUMN client_id VARCHAR(64) NOT NULL; ALTER TABLE recorded_tracks ADD COLUMN track_id VARCHAR(64) NOT NULL; + +-- so that server can correlate to client track +ALTER TABLE tracks ADD COLUMN client_track_id VARCHAR(64) NOT NULL; diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index e79ee8b29..869906da9 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -362,6 +362,7 @@ SQL t.instrument = instrument t.connection = connection t.sound = track["sound"] + t.client_track_id = track["client_track_id"] t.save connection.tracks << t end diff --git a/ruby/lib/jam_ruby/lib/audiomixer.rb b/ruby/lib/jam_ruby/lib/audiomixer.rb new file mode 100644 index 000000000..2efc0c955 --- /dev/null +++ b/ruby/lib/jam_ruby/lib/audiomixer.rb @@ -0,0 +1,21 @@ +require 'json' +require 'resque' + +module JamRuby + + @queue = :audiomixer + + class AudioMixer + + def self.perform(manifest) + tmp = Dir::Tmpname.make_tmpname "/var/tmp/audiomixer/manifest-#{manifest['recordingId']}", nil + File.open(tmp,"w") do |f| + f.write(manifest.to_json) + end + + system("tar zxvf some_big_tarball.tar.gz")) + end + + end + +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 881cc6690..d08ec1efe 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -72,7 +72,7 @@ module JamRuby if as_musician unless self.user.musician - errors.add(:as_musician, ValidationMesages::FAN_CAN_NOT_JOIN_AS_MUSICIAN) + errors.add(:as_musician, ValidationMessages::FAN_CAN_NOT_JOIN_AS_MUSICIAN) return false end diff --git a/ruby/lib/jam_ruby/models/track.rb b/ruby/lib/jam_ruby/models/track.rb index 7ae02b24c..76b428b09 100644 --- a/ruby/lib/jam_ruby/models/track.rb +++ b/ruby/lib/jam_ruby/models/track.rb @@ -38,7 +38,68 @@ module JamRuby return query end - def self.save(id, connection_id, instrument_id, sound) + + # this is a bit different from a normal track synchronization in that the client just sends up all tracks, + # ... some may already exist + def self.sync(clientId, tracks) + result = [] + + Track.transaction do + connection = Connection.find_by_client_id!(clientId) + + if tracks.length == 0 + connection.tracks.delete_all + else + connection_tracks = connection.tracks + + # we will prune from this as we find matching tracks + to_delete = Set.new(connection_tracks) + to_add = Array.new(tracks) + + connection_tracks.each do |connection_track| + tracks.each do |track| + if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id; + to_delete.delete(connection_track) + to_add.delete(track) + # don't update connection_id or client_id; it's unknown what would happen if these changed mid-session + connection_track.instrument = Instrument.find(track[:instrument_id]) + connection_track.sound = track[:sound] + connection_track.client_track_id = track[:client_track_id] + if connection_track.save + result.push(connection_track) + next + else + result = connection_track + raise ActiveRecord::Rollback + end + end + end + end + + to_add.each do |track| + connection_track = Track.new + connection_track.connection = connection + connection_track.instrument = Instrument.find(track[:instrument_id]) + connection_track.sound = track[:sound] + connection_track.client_track_id = track[:client_track_id] + if connection_track.save + result.push(connection_track) + else + result = connection_track + raise ActiveRecord::Rollback + end + end + + to_delete.each do| delete_me | + delete_me.delete + end + end + end + + result + end + + def self.save(id, connection_id, instrument_id, sound, client_track_id) if id.nil? track = Track.new() track.connection_id = connection_id @@ -54,6 +115,10 @@ module JamRuby track.sound = sound end + unless client_track_id.nil? + track.client_track_id = client_track_id + end + track.updated_at = Time.now.getutc track.save return track diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 7af848254..d24051b6e 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -114,7 +114,7 @@ module JamRuby validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i - validates :email, presence: true, format: {with: VALID_EMAIL_REGEX} + validates :email, presence: true, format: {with: VALID_EMAIL_REGEX} validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password? diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 951cd7232..85a8f162f 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -90,7 +90,7 @@ FactoryGirl.define do factory :track, :class => JamRuby::Track do sound "mono" - + sequence(:client_track_id) { |n| "client_track_id#{n}"} end factory :recorded_track, :class => JamRuby::RecordedTrack do diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb index cc673f0d4..63ac9cd86 100644 --- a/ruby/spec/jam_ruby/connection_manager_spec.rb +++ b/ruby/spec/jam_ruby/connection_manager_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' # these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests describe ConnectionManager do - TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono"}] + TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "some_client_track_id"}] before do @conn = PG::Connection.new(:dbname => SpecDb::TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost") diff --git a/ruby/spec/jam_ruby/models/mix_spec.rb b/ruby/spec/jam_ruby/models/mix_spec.rb index 91f3d0463..3ded0054a 100755 --- a/ruby/spec/jam_ruby/models/mix_spec.rb +++ b/ruby/spec/jam_ruby/models/mix_spec.rb @@ -9,7 +9,7 @@ describe Mix do @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true) @music_session.connections << @connection @music_session.save - @recording = Recording.start(@music_session.id, @user) + @recording = Recording.start(@music_session, @user) @recording.stop @mix = Mix.schedule(@recording, "{}") end diff --git a/ruby/spec/jam_ruby/models/track_spec.rb b/ruby/spec/jam_ruby/models/track_spec.rb new file mode 100644 index 000000000..55bf5fd0a --- /dev/null +++ b/ruby/spec/jam_ruby/models/track_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Track do + + let (:connection) { FactoryGirl.create(:connection) } + let (:track) { FactoryGirl.create(:track, :connection => connection)} + let (:track2) { FactoryGirl.create(:track, :connection => connection)} + + let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} } + + before(:each) do + + end + + describe "sync" do + it "create one track" do + tracks = Track.sync(connection.client_id, [track_hash]) + tracks.length.should == 1 + track = tracks[0] + track.client_track_id.should == track_hash[:client_track_id] + track.sound = track_hash[:sound] + track.instrument.should == Instrument.find('drums') + end + + it "create two tracks" do + tracks = Track.sync(connection.client_id, [track_hash, track_hash]) + tracks.length.should == 2 + track = tracks[0] + track.client_track_id.should == track_hash[:client_track_id] + track.sound = track_hash[:sound] + track.instrument.should == Instrument.find('drums') + track = tracks[1] + track.client_track_id.should == track_hash[:client_track_id] + track.sound = track_hash[:sound] + track.instrument.should == Instrument.find('drums') + end + + it "delete only track" do + track.id.should_not be_nil + connection.tracks.length.should == 1 + tracks = Track.sync(connection.client_id, []) + tracks.length.should == 0 + end + + it "delete one of two tracks using .id to correlate" do + + track.id.should_not be_nil + track2.id.should_not be_nil + connection.tracks.length.should == 2 + tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + tracks.length.should == 1 + found = tracks[0] + found.id.should == track.id + found.sound.should == 'mono' + found.client_track_id.should == 'client_guid_new' + end + + it "delete one of two tracks using .client_track_id to correlate" do + + track.id.should_not be_nil + track2.id.should_not be_nil + connection.tracks.length.should == 2 + tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + tracks.length.should == 1 + found = tracks[0] + found.id.should == track.id + found.sound.should == 'mono' + found.client_track_id.should == track.client_track_id + end + + + it "updates a single track using .id to correlate" do + track.id.should_not be_nil + connection.tracks.length.should == 1 + tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}]) + tracks.length.should == 1 + found = tracks[0] + found.id.should == track.id + found.sound.should == 'mono' + found.client_track_id.should == 'client_guid_new' + end + + it "updates a single track using .client_track_id to correlate" do + track.id.should_not be_nil + connection.tracks.length.should == 1 + tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}]) + tracks.length.should == 1 + found = tracks[0] + found.id.should == track.id + found.sound.should == 'mono' + found.client_track_id.should == track.client_track_id + end + end +end \ No newline at end of file diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 13f1329ef..56d992936 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -104,7 +104,9 @@ payload = message[messageType], callbacks = server.dispatchTable[message.type]; - logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); + if(message.type != context.JK.MessageType.HEARTBEAT_ACK) { + logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); + } if (callbacks !== undefined) { var len = callbacks.length; @@ -132,7 +134,9 @@ var jsMessage = JSON.stringify(message); - logger.log("server.send(" + jsMessage + ")"); + if(message.type != context.JK.MessageType.HEARTBEAT) { + logger.log("server.send(" + jsMessage + ")"); + } if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) { server.socket.send(jsMessage); } else { diff --git a/web/app/assets/javascripts/addTrack.js b/web/app/assets/javascripts/addTrack.js index eae6c5f78..e40732a31 100644 --- a/web/app/assets/javascripts/addTrack.js +++ b/web/app/assets/javascripts/addTrack.js @@ -71,7 +71,6 @@ // set arrays inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); - console.log("inputUnassignedList: " + JSON.stringify(inputUnassignedList)); track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false); } @@ -134,13 +133,16 @@ } saveTrack(); + app.layout.closeDialog('add-track'); } function saveTrack() { // TRACK 2 INPUTS + var trackId = null; $("#add-track2-input > option").each(function() { logger.debug("Saving track 2 input = " + this.value); + trackId = this.value; context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.TRACK2); }); @@ -154,12 +156,52 @@ // UPDATE SERVER logger.debug("Adding track with instrument " + instrumentText); var data = {}; - // use the first track's connection_id (not sure why we need this on the track data model) - logger.debug("myTracks[0].connection_id=" + myTracks[0].connection_id); - data.connection_id = myTracks[0].connection_id; - data.instrument_id = instrumentText; - data.sound = "stereo"; - sessionModel.addTrack(sessionId, data); + + context.jamClient.TrackSaveAssignments(); + + /** + setTimeout(function() { + var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 2); + + // this is some ugly logic coming up, here's why: + // we need the id (guid) that the backend generated for the new track we just added + // to get it, we need to make sure 2 tracks come back, and then grab the track that + // is not the one we just added. + if(inputTracks.length != 2) { + var msg = "because we just added a track, there should be 2 available, but we found: " + inputTracks.length; + logger.error(msg); + alert(msg); + throw new Error(msg); + } + + var client_track_id = null; + $.each(inputTracks, function(index, track) { + + + console.log("track: %o, myTrack: %o", track, myTracks[0]); + if(track.id != myTracks[0].id) { + client_track_id = track.id; + return false; + } + }); + + if(client_track_id == null) + { + var msg = "unable to find matching backend track for id: " + this.value; + logger.error(msg); + alert(msg); + throw new Error(msg); + } + + // use the first track's connection_id (not sure why we need this on the track data model) + data.connection_id = myTracks[0].connection_id; + data.instrument_id = instrumentText; + data.sound = "stereo"; + data.client_track_id = client_track_id; + sessionModel.addTrack(sessionId, data); + }, 1000); + + */ } function validateSettings() { diff --git a/web/app/assets/javascripts/configureTrack.js b/web/app/assets/javascripts/configureTrack.js index 783c162a0..ae9b033a3 100644 --- a/web/app/assets/javascripts/configureTrack.js +++ b/web/app/assets/javascripts/configureTrack.js @@ -232,7 +232,6 @@ // remove option 1 from voice chat type dropdown if no music (based on what's unused on the Music Audio tab) or chat inputs are available if ($('#audio-inputs-unused > option').size() === 0 && chatOtherUnassignedList.length === 0 && chatOtherAssignedList.length === 0) { - logger.debug("Removing Option 1 from Voice Chat dropdown."); $option1.remove(); } else { @@ -494,13 +493,13 @@ function _initMusicTabData() { inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); - logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList)); + //logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList)); track1AudioInputChannels = _loadList(ASSIGNMENT.TRACK1, true, false); - logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels)); + //logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels)); track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false); - logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels)); + //logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels)); outputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, false, false); outputAssignedList = _loadList(ASSIGNMENT.OUTPUT, false, false); @@ -508,16 +507,16 @@ function _initVoiceChatTabData() { chatUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); - logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList)); + //logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList)); chatAssignedList = _loadList(ASSIGNMENT.CHAT, true, false); - logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList)); + //logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList)); chatOtherUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, true); - logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList)); + //logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList)); chatOtherAssignedList = _loadList(ASSIGNMENT.CHAT, true, true); - logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList)); + //logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList)); } // TODO: copied in addTrack.js - refactor to common place @@ -592,7 +591,7 @@ app.layout.closeDialog('configure-audio'); // refresh Session screen - sessionModel.refreshCurrentSession(); + //sessionModel.refreshCurrentSession(); } function saveAudioSettings() { @@ -817,7 +816,6 @@ }); originalVoiceChat = context.jamClient.TrackGetChatEnable() ? VOICE_CHAT.CHAT : VOICE_CHAT.NO_CHAT; - logger.debug("originalVoiceChat=" + originalVoiceChat); $('#voice-chat-type').val(originalVoiceChat); @@ -830,7 +828,6 @@ // remove option 1 from voice chat if none are available and not already assigned if (inputUnassignedList.length === 0 && chatAssignedList.length === 0 && chatOtherAssignedList.length === 0 && chatOtherUnassignedList.length === 0) { - logger.debug("Removing Option 1 from Voice Chat dropdown."); $option1.remove(); } // add it if it doesn't exist @@ -846,7 +843,6 @@ events(); _init(); myTrackCount = myTracks.length; - logger.debug("initialize:myTrackCount=" + myTrackCount); toggleTrack2ConfigDetails(myTrackCount > 1); }; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 2a12e2362..ab06fdd5b 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -283,8 +283,8 @@ ]; } - function RecordingRegisterCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, requestStopCallbackName) { - fakeJamClientRecordings.RecordingRegisterCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, requestStopCallbackName); + function RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, abortedRecordingCallbackName) { + fakeJamClientRecordings.RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, abortedRecordingCallbackName); } function SessionRegisterCallback(callbackName) { @@ -601,7 +601,7 @@ this.SessionAddTrack = SessionAddTrack; this.SessionGetControlState = SessionGetControlState; this.SessionGetIDs = SessionGetIDs; - this.RecordingRegisterCallbacks = RecordingRegisterCallbacks; + this.RegisterRecordingCallbacks = RegisterRecordingCallbacks; this.SessionRegisterCallback = SessionRegisterCallback; this.SessionSetAlertCallback = SessionSetAlertCallback; this.SessionSetControlState = SessionSetControlState; diff --git a/web/app/assets/javascripts/fakeJamClientMessages.js b/web/app/assets/javascripts/fakeJamClientMessages.js index b64c474ed..90792bf09 100644 --- a/web/app/assets/javascripts/fakeJamClientMessages.js +++ b/web/app/assets/javascripts/fakeJamClientMessages.js @@ -26,13 +26,14 @@ return msg; } - function stopRecording(recordingId, errorReason, errorDetail) { + function stopRecording(recordingId, success, reason, detail) { var msg = {}; msg.type = self.Types.STOP_RECORDING; msg.msgId = context.JK.generateUUID(); msg.recordingId = recordingId; - msg.errorReason = errorReason; - msg.errorDetail = errorDetail; + msg.success = success === undefined ? true : success; + msg.reason = reason; + msg.detail = detail; return msg; } @@ -47,13 +48,14 @@ return msg; } - function abortRecording(recordingId, errorReason, errorDetail) { + function abortRecording(recordingId, reason, detail) { var msg = {}; msg.type = self.Types.ABORT_RECORDING; msg.msgId = context.JK.generateUUID(); msg.recordingId = recordingId; - msg.errorReason = errorReason; - msg.errorDetail = errorDetail; + msg.success = false; + msg.reason = reason; + msg.detail = detail; return msg; } diff --git a/web/app/assets/javascripts/fakeJamClientRecordings.js b/web/app/assets/javascripts/fakeJamClientRecordings.js index 0255734c2..b48ad335b 100644 --- a/web/app/assets/javascripts/fakeJamClientRecordings.js +++ b/web/app/assets/javascripts/fakeJamClientRecordings.js @@ -13,72 +13,78 @@ var stopRecordingResultCallbackName = null; var startedRecordingResultCallbackName = null; var stoppedRecordingEventCallbackName = null; - var requestStopCallbackName = null; + var abortedRecordingEventCallbackName = null; var startingSessionState = null; var stoppingSessionState = null; var currentRecordingId = null; var currentRecordingCreatorClientId = null; + var currentRecordingClientIds = null; function timeoutStartRecordingTimer() { - eval(startRecordingResultCallbackName).call(this, startingSessionState.recordingId, false, 'client-no-response', startingSessionState.groupedClientTracks); + eval(startRecordingResultCallbackName).call(this, startingSessionState.recordingId, {success:false, reason:'client-no-response', detail:startingSessionState.groupedClientTracks[0]}); startingSessionState = null; } function timeoutStopRecordingTimer() { - eval(stopRecordingResultCallbackName).call(this, stoppingSessionState.recordingId, false, 'client-no-response', stoppingSessionState.groupedClientTracks); + eval(stopRecordingResultCallbackName).call(this, stoppingSessionState.recordingId, {success:false, reason:'client-no-response', detail:stoppingSessionState.groupedClientTracks[0]}); } - function StartRecording(recordingId, groupedClientTracks) { + function StartRecording(recordingId, clients) { startingSessionState = {}; // we expect all clients to respond within 3 seconds to mimic the reliable UDP layer startingSessionState.aggegratingStartResultsTimer = setTimeout(timeoutStartRecordingTimer, 3000); startingSessionState.recordingId = recordingId; - startingSessionState.groupedClientTracks = copyTracks(groupedClientTracks, app.clientId); // we will manipulate this new one + startingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId); // we will manipulate this new one // store the current recording's data currentRecordingId = recordingId; currentRecordingCreatorClientId = app.clientId; + currentRecordingClientIds = copyClientIds(clients, app.clientId); - if(context.JK.dlen(startingSessionState.groupedClientTracks) == 0) { + if(startingSessionState.groupedClientTracks.length == 0) { // if there are no clients but 'self', then you can declare a successful recording immediately finishSuccessfulStart(recordingId); } else { // signal all other connected clients that the recording has started - for(var clientId in startingSessionState.groupedClientTracks) { + for(var i = 0; i < startingSessionState.groupedClientTracks.length; i++) { + var clientId = startingSessionState.groupedClientTracks[i]; context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.startRecording(recordingId))); } } } - function StopRecording(recordingId, groupedClientTracks, errorReason, errorDetail) { + function StopRecording(recordingId, clients, result) { if(startingSessionState) { // we are currently starting a session. // TODO } + if(!result) { + result = {success:true} + } + stoppingSessionState = {}; // we expect all clients to respond within 3 seconds to mimic the reliable UDP layer stoppingSessionState.aggegratingStopResultsTimer = setTimeout(timeoutStopRecordingTimer, 3000); stoppingSessionState.recordingId = recordingId; - stoppingSessionState.groupedClientTracks = copyTracks(groupedClientTracks, app.clientId); + stoppingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId); - if(context.JK.dlen(stoppingSessionState.groupedClientTracks) == 0) { + if(stoppingSessionState.groupedClientTracks.length == 0) { finishSuccessfulStop(recordingId); } else { - // signal all other connected clients that the recording has started - for(var clientId in stoppingSessionState.groupedClientTracks) { - context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.stopRecording(recordingId, errorReason, errorDetail))); + // signal all other connected clients that the recording has stopped + for(var i = 0; i < stoppingSessionState.groupedClientTracks.length; i++) { + var clientId = stoppingSessionState.groupedClientTracks[i]; + context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.stopRecording(recordingId, result.success, result.reason, result.detail))); } } - - //eval(stopRecordingResultCallbackName).call(this, recordingId, true, null, null); } function AbortRecording(recordingId, errorReason, errorDetail) { @@ -101,7 +107,7 @@ currentRecordingCreatorClientId = from; context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.startRecordingAck(payload.recordingId, true, null, null))); - eval(startedRecordingResultCallbackName).call(this, from, payload.recordingId); + eval(startedRecordingResultCallbackName).call(this, payload.recordingId, {success:true}, from); } } @@ -112,9 +118,10 @@ if(startingSessionState) { if(payload.success) { - delete startingSessionState.groupedClientTracks[from]; + var index = startingSessionState.groupedClientTracks.indexOf(from); + startingSessionState.groupedClientTracks.splice(index, 1); - if(context.JK.dlen(startingSessionState.groupedClientTracks) == 0) { + if(startingSessionState.groupedClientTracks.length == 0) { finishSuccessfulStart(payload.recordingId); } } @@ -137,7 +144,7 @@ // this means we should keep a list of the last N recordings that we've seen, rather than just keeping the current context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.stopRecordingAck(payload.recordingId, true))); - eval(stopRecordingResultCallbackName).call(this, payload.recordingId, !payload.errorReason, payload.errorReason, payload.errorDetail); + eval(stopRecordingResultCallbackName).call(this, payload.recordingId, {success:payload.success, reason:payload.reason, detail:from}); } function onStopRecordingAck(from, payload) { @@ -147,14 +154,16 @@ if(stoppingSessionState) { if(payload.success) { - delete stoppingSessionState.groupedClientTracks[from]; + var index = stoppingSessionState.groupedClientTracks.indexOf(from); + stoppingSessionState.groupedClientTracks.splice(index, 1); - if(context.JK.dlen(stoppingSessionState.groupedClientTracks) == 0) { + if(stoppingSessionState.groupedClientTracks.length == 0) { finishSuccessfulStop(payload.recordingId); } } else { // TOOD: a client responded with error; what now? + logger.error("client responded with error: ", payload); } } else { @@ -170,48 +179,62 @@ // if creator, tell everyone else to stop if(app.clientId == currentRecordingCreatorClientId) { // ask the front end to stop the recording because it has the full track listing - eval(requestStopCallbackName).call(this, payload.errorReason, payload.errorDetail); + for(var i = 0; i < currentRecordingClientIds.length; i++) { + var clientId = currentRecordingClientIds[i]; + context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.abortRecording(currentRecordingId, payload.reason, from))); + } + } else { - logger.warn("only the creator currently deals with the abort request. abort request sent from:" + from + " with a reason of: " + payload.errorReason); + logger.debug("only the creator currently deals with the abort request. abort request sent from:" + from + " with a reason of: " + payload.errorReason); } + + eval(abortedRecordingEventCallbackName).call(this, payload.recordingId, {success:payload.success, reason:payload.reason, detail:from}); } - function RecordingRegisterCallbacks(startRecordingCallbackName, + function RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, - _requestStopCallbackName) { + abortedRecordingCallbackName) { startRecordingResultCallbackName = startRecordingCallbackName; stopRecordingResultCallbackName = stopRecordingCallbackName; startedRecordingResultCallbackName = startedRecordingCallbackName; stoppedRecordingEventCallbackName = stoppedRecordingCallbackName; - requestStopCallbackName = _requestStopCallbackName; + abortedRecordingEventCallbackName = abortedRecordingCallbackName; } - // copies all tracks, but removes current client ID because we don't want to message that user - function copyTracks(tracks, myClientId) { - var newTracks = {}; - for(var clientId in tracks) { + // copies all clientIds, but removes current client ID because we don't want to message that user + function copyClientIds(clientIds, myClientId) { + var newClientIds = []; + for(var i = 0; i < clientIds.length; i++) { + var clientId = clientIds[i] if(clientId != myClientId) { - newTracks[clientId] = tracks[clientId]; + newClientIds.push(clientId); } } - return newTracks; + return newClientIds; } function finishSuccessfulStart(recordingId) { // all clients have responded. clearTimeout(startingSessionState.aggegratingStartResultsTimer); startingSessionState = null; - eval(startRecordingResultCallbackName).call(this, recordingId, true); + eval(startRecordingResultCallbackName).call(this, recordingId, {success:true}); } function finishSuccessfulStop(recordingId, errorReason) { // all clients have responded. clearTimeout(stoppingSessionState.aggegratingStopResultsTimer); stoppingSessionState = null; - eval(stopRecordingResultCallbackName).call(this, recordingId, true, errorReason); + var result = { success: true } + if(errorReason) + { + result.success = false; + result.reason = errorReason + result.detail = "" + } + eval(stopRecordingResultCallbackName).call(this, recordingId, result); } @@ -226,7 +249,7 @@ this.StartRecording = StartRecording; this.StopRecording = StopRecording; this.AbortRecording = AbortRecording; - this.RecordingRegisterCallbacks = RecordingRegisterCallbacks; + this.RegisterRecordingCallbacks = RegisterRecordingCallbacks; } })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/ftue.js b/web/app/assets/javascripts/ftue.js index 9bb2253b2..8465a033d 100644 --- a/web/app/assets/javascripts/ftue.js +++ b/web/app/assets/javascripts/ftue.js @@ -245,6 +245,16 @@ jamClient.TrackSetChatEnable(false); } + var defaultInstrumentId; + if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) { + defaultInstrumentId = context.JK.instrument_id_to_instrument[context.JK.userMe.instruments[0].instrument_id].client_id; + } + else { + defaultInstrumentId = context.JK.server_to_client_instrument_map['Other'].client_id; + } + + jamClient.TrackSetInstrument(1, defaultInstrumentId); + logger.debug("Calling FTUESave(" + persist + ")"); var response = jamClient.FTUESave(persist); setLevels(0); @@ -252,7 +262,6 @@ logger.warn(response); // TODO - we may need to do something about errors on save. // per VRFS-368, I'm hiding the alert, and logging a warning. - // context.alert(response); } } else { logger.debug("Aborting FTUESave as we need input + output selected."); @@ -430,13 +439,10 @@ } function setAsioSettingsVisibility() { - logger.debug("jamClient.FTUEHasControlPanel()=" + jamClient.FTUEHasControlPanel()); if (jamClient.FTUEHasControlPanel()) { - logger.debug("Showing ASIO button"); $('#btn-asio-control-panel').show(); } else { - logger.debug("Hiding ASIO button"); $('#btn-asio-control-panel').hide(); } } diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 8f757a911..1c47e8927 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -73,6 +73,15 @@ 250: { "server_id": "other" } }; + context.JK.instrument_id_to_instrument = {}; + + (function() { + $.each(context.JK.server_to_client_instrument_map, function(key, value) { + context.JK.instrument_id_to_instrument[value.server_id] = { client_id: value.client_id, display: key } + }); + })(); + + context.JK.entityToPrintable = { music_session: "music session" } diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 4a58c5f2d..aa923e89b 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -13,7 +13,6 @@ var logger = context.JK.logger; function createJoinRequest(joinRequest) { - logger.debug("joinRequest=" + JSON.stringify(joinRequest)); return $.ajax({ type: "POST", dataType: "json", @@ -341,6 +340,20 @@ }) } + function putTrackSyncChange(options) { + var musicSessionId = options["id"] + delete options["id"]; + + return $.ajax({ + type: "PUT", + dataType: "json", + url: '/api/sessions/' + musicSessionId + '/tracks', + contentType: 'application/json', + processData: false, + data: JSON.stringify(options) + }); + } + function initialize() { return self; } @@ -374,6 +387,7 @@ this.startRecording = startRecording; this.stopRecording = stopRecording; this.getRecording = getRecording; + this.putTrackSyncChange = putTrackSyncChange; return this; }; diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 99d642b9c..b0c29390c 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -253,6 +253,11 @@ this.layout.notify(message, descriptor); }; + /** Shows an alert notification. Expects text, title */ + this.notifyAlert = function(title ,text) { + this.notify({title:title, text:text, icon_url: "/assets/content/icon_alert_big.png"}); + } + /** * Initialize any common events. */ @@ -300,7 +305,7 @@ if (context.jamClient) { // Unregister for callbacks. - context.jamClient.RecordingRegisterCallbacks("", "", "", "", ""); + context.jamClient.RegisterRecordingCallbacks("", "", "", "", ""); context.jamClient.SessionRegisterCallback(""); context.jamClient.SessionSetAlertCallback(""); context.jamClient.FTUERegisterVUCallbacks("", "", ""); diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 6402edcd0..cb0c56b5f 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -476,17 +476,27 @@ * also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one */ function stackDialogs($dialog, $overlay) { + // don't push a dialog on the stack that is already on there; remove it from where ever it is currently + // and the rest of the code will make it end up at the top + var layoutId = $dialog.attr('layout-id'); + for(var i = openDialogs.length - 1; i >= 0; i--) { + if(openDialogs[i].attr('layout-id') === layoutId) { + openDialogs.splice(i, 1); + } + } + openDialogs.push($dialog); var zIndex = 1000; for(var i in openDialogs) { - var $dialog = openDialogs[i]; - $dialog.css('zIndex', zIndex); + var $openDialog = openDialogs[i]; + $openDialog.css('zIndex', zIndex); zIndex++; } $overlay.css('zIndex', zIndex - 1); } function unstackDialogs($overlay) { + console.log("unstackDialogs. openDialogs: %o", openDialogs); if(openDialogs.length > 0) { openDialogs.pop(); } diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js index c8738bc80..19bf02fe2 100644 --- a/web/app/assets/javascripts/recordingModel.js +++ b/web/app/assets/javascripts/recordingModel.js @@ -46,8 +46,6 @@ /** called every time a session is joined, to ensure clean state */ function reset() { - stoppingRecording = false; - startingRecording = false; currentlyRecording = false; waitingOnServerStop = false; waitingOnClientStop = false; @@ -73,23 +71,15 @@ } tracksForClient.push(recordingTracks[i]); } - return groupedTracks; + return context.JK.dkeys(groupedTracks); } function startRecording() { - if(currentlyRecording) { - logger.warn("ignoring request to start recording because we are currently recording"); - return false; - } - if(startingRecording) { - logger.warn("ignoring request to start recording because recording currently started"); - return false; - } - - startingRecording = true; $self.triggerHandler('startingRecording', {}); + currentlyRecording = true; + currentRecording = rest.startRecording({"music_session_id": sessionModel.id()}) .done(function(recording) { currentRecordingId = recording.id; @@ -100,34 +90,20 @@ }) .fail(function() { $self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments }); - startingRecording = false; + currentlyRecording = false; }) + return true; } /** Nulls can be passed for all 3 currently; that's a user request. */ - function stopRecording(recordingId, errorReason, errorDetail) { - if(recordingId && recordingId != currentRecordingId) { - logger.debug("asked to stop an unknown recording: %o", recordingId); - return false; - } - - if(!currentlyRecording) { - logger.debug("ignoring request to stop recording because there is not currently a recording"); - return false; - } - if(stoppingRecording) { - logger.debug("request to stop recording ignored because recording currently stopping") - return false; - } - - stoppingRecording = true; + function stopRecording(recordingId, reason, detail) { waitingOnServerStop = waitingOnClientStop = true; waitingOnStopTimer = setTimeout(timeoutTransitionToStop, 5000); - $self.triggerHandler('stoppingRecording', {reason: errorReason, detail: errorDetail}); + $self.triggerHandler('stoppingRecording', {reason: reason, detail: detail}); // this path assumes that the currentRecording info has, or can be, retrieved // failure for currentRecording is handled elsewhere @@ -140,12 +116,12 @@ rest.stopRecording( { "id": recording.id } ) .done(function() { waitingOnServerStop = false; - attemptTransitionToStop(recording.id, errorReason, errorDetail); + attemptTransitionToStop(recording.id, reason, detail); }) .fail(function(jqXHR) { if(jqXHR.status == 422) { waitingOnServerStop = false; - attemptTransitionToStop(recording.id, errorReason, errorDetail); + attemptTransitionToStop(recording.id, reason, detail); } else { logger.error("unable to stop recording %o", arguments); @@ -159,7 +135,7 @@ } function abortRecording(recordingId, errorReason, errorDetail) { - jamClient.AbortRecording(recordingId, errorReason, errorDetail); + jamClient.AbortRecording(recordingId, {reason: errorReason, detail: errorDetail, success:false}); } function timeoutTransitionToStop() { @@ -178,7 +154,6 @@ } function transitionToStopped() { - stoppingRecording = false; currentlyRecording = false; currentRecording = null; currentRecordingId = null; @@ -196,21 +171,28 @@ stopRecording(recordingId, null, null); } - function handleRecordingStartResult(recordingId, success, reason, detail) { + function handleRecordingStartResult(recordingId, result) { + + var success = result.success; + var reason = result.reason; + var detail = result.detail; - startingRecording = false; - currentlyRecording = true; if(success) { $self.triggerHandler('startedRecording', {clientId: app.clientId}) } else { + currentlyRecording = false; logger.error("unable to start the recording %o, %o", reason, detail); $self.triggerHandler('startedRecording', { clientId: app.clientId, reason: reason, detail: detail}); } } - function handleRecordingStopResult(recordingId, success, reason, detail) { + function handleRecordingStopResult(recordingId, result) { + + var success = result.success; + var reason = result.reason; + var detail = result.detail; waitingOnClientStop = false; @@ -224,7 +206,11 @@ } } - function handleRecordingStarted(clientId, recordingId) { + function handleRecordingStarted(recordingId, result, clientId) { + var success = result.success; + var reason = result.reason; + var detail = result.detail; + // in this scenario, we don't know all the tracks of the user. // we need to ask sessionModel to populate us with the recording data ASAP @@ -236,47 +222,93 @@ currentRecordingId = recording.id; }); - startingRecording = true; $self.triggerHandler('startingRecording', {recordingId: recordingId}); - startingRecording = false; currentlyRecording = true; $self.triggerHandler('startedRecording', {clientId: clientId, recordingId: recordingId}); } - function handleRecordingStopped(recordingId, success, errorReason, errorDetail) { - stoppingRecording = true; - $self.triggerHandler('stoppingRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail }); + function handleRecordingStopped(recordingId, result) { + var success = result.success; + var reason = result.reason; + var detail = result.detail; + + + $self.triggerHandler('stoppingRecording', {recordingId: recordingId, reason: reason, detail: detail }); // the backend says the recording must be stopped. // tell the server to stop it too rest.stopRecording({ - recordingId: recordingId + id: recordingId }) .always(function() { - stoppingRecording = false; - currentlyRecording = false; + transitionToStopped(); }) .fail(function(jqXHR, textStatus, errorMessage) { if(jqXHR.status == 422) { logger.debug("recording already stopped %o", arguments); - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail}); + $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail}); } else if(jqXHR.status == 404) { logger.debug("recording is already deleted %o", arguments); - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail}); + $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail}); } else { $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: textStatus, detail: errorMessage}); } }) .done(function() { - $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail}); + $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail}); }) } - function handleRequestRecordingStop(recordingId, errorReason, errorDetail) { - // TODO: check recordingId - // this is always an error case, when the backend autonomously asks tho frontend to stop - stopRecording(recordingId, errorReason, errorDetail); + function handleRecordingAborted(recordingId, result) { + var success = result.success; + var reason = result.reason; + var detail = result.detail; + + stoppingRecording = false; + + $self.triggerHandler('abortedRecording', {recordingId: recordingId, reason: reason, detail: detail }); + // the backend says the recording must be stopped. + // tell the server to stop it too + rest.stopRecording({ + id: recordingId + }) + .always(function() { + currentlyRecording = false; + }) + } + + /** + * If a stop is needed, it will be issued, and the deferred object will fire done() + * If a stop is not needed (i.e., there is no recording), then the deferred object will fire immediately + * @returns {$.Deferred} in all cases, only .done() is fired. + */ + function stopRecordingIfNeeded() { + var deferred = new $.Deferred(); + + function resolved() { + $self.off('stoppedRecording.stopRecordingIfNeeded', resolved); + deferred.resolve(arguments); + } + + if(!currentlyRecording) { + deferred = new $.Deferred(); + deferred.resolve(); + } + else { + // wait for the next stoppedRecording event message + $self.on('stoppedRecording.stopRecordingIfNeeded', resolved); + + if(!stopRecording()) { + // no event is coming, so satisfy the deferred immediately + $self.off('stoppedRecording.stopRecordingIfNeeded', resolved); + deferred = new $.Deferred(); + deferred.resolve(); + } + } + + return deferred; + } this.initialize = function() { @@ -287,12 +319,14 @@ this.onServerStopRecording = onServerStopRecording; this.isRecording = isRecording; this.reset = reset; + this.stopRecordingIfNeeded = stopRecordingIfNeeded; context.JK.HandleRecordingStartResult = handleRecordingStartResult; context.JK.HandleRecordingStopResult = handleRecordingStopResult; context.JK.HandleRecordingStopped = handleRecordingStopped; context.JK.HandleRecordingStarted = handleRecordingStarted; - context.JK.HandleRequestRecordingStop = handleRequestRecordingStop; + context.JK.HandleRecordingAborted = handleRecordingAborted; + }; diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 59b355ea3..9fdce64fc 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -101,14 +101,54 @@ function alertCallback(type, text) { if (type === 2) { // BACKEND_MIXER_CHANGE - sessionModel.refreshCurrentSession(); + logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text); + if(sessionModel.id() && text == "RebuildAudioIoControl") { + // this is a local change to our tracks. we need to tell the server about our updated track information + var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); + + // create a trackSync request based on backend data + var syncTrackRequest = {}; + syncTrackRequest.client_id = app.clientId; + syncTrackRequest.tracks = inputTracks; + syncTrackRequest.id = sessionModel.id(); + + rest.putTrackSyncChange(syncTrackRequest) + .done(function() { + sessionModel.refreshCurrentSession(); + }) + .fail(function() { + app.notify({ + "title": "Can't Sync Local Tracks", + "text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + }) + } + else { + // this is wrong; we shouldn't be using the server to decide what tracks are shown + // however, we have to without a refactor, and if we wait a second, then it should be enough time + // for the client that initialed this to have made the change + // https://jamkazam.atlassian.net/browse/VRFS-854 + setTimeout(function() { + sessionModel.refreshCurrentSession(); // XXX: race condition possible here; other client may not have updated server yet + }, 1000); + } } else { context.setTimeout(function() { - app.notify({ - "title": alert_type[type].title, - "text": text, - "icon_url": "/assets/content/icon_alert_big.png" - }); }, 1); + var alert = alert_type[type]; + + if(alert) { + app.notify({ + "title": alert_type[type].title, + "text": text, + "icon_url": "/assets/content/icon_alert_big.png" + }); + } + else { + logger.debug("Unknown Backend Event type %o, data %o", type, text) + } + }, 1); + } } @@ -121,7 +161,7 @@ // Subscribe for callbacks on audio events context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback"); context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback"); - context.jamClient.RecordingRegisterCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRequestRecordingStop"); + context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); context.jamClient.SessionSetAlertCallback("JK.AlertCallback"); // If you load this page directly, the loading of the current user @@ -138,8 +178,26 @@ checkForCurrentUser(); } + function notifyWithUserInfo(title , text, clientId) { + sessionModel.findUserBy({clientId: clientId}) + .done(function(user) { + app.notify({ + "title": title, + "text": user.name + " " + text, + "icon_url": context.JK.resolveAvatarUrl(user.photo_url) + }); + }) + .fail(function() { + app.notify({ + "title": title, + "text": 'Someone ' + text, + "icon_url": "/assets/content/icon_alert_big.png" + }); + }); + } + + function afterCurrentUserLoaded() { - logger.debug("afterCurrentUserLoaded"); // It seems the SessionModel should be a singleton. // a client can only be in one session at a time, // and other parts of the code want to know at any certain times @@ -152,21 +210,49 @@ $(sessionModel.recordingModel) .on('startingRecording', function(e, data) { - if(data.reason) { - // error path - displayDoneRecording(); - app.notify({ - "title": "Unable to Start Recording", - "text": "Unable to start the recording due to '" + data.reason + "'", - "icon_url": "/assets/content/icon_alert_big.png"}); - } - else { - displayStartingRecording(); - } + displayStartingRecording(); }) .on('startedRecording', function(e, data) { - displayStartedRecording(); - displayWhoCreated(data.clientId) + if(data.reason) { + var reason = data.reason; + var detail = data.detail; + + var title = "Could Not Start Recording"; + + if(data.reason == 'client-no-response') { + notifyWithUserInfo(title, 'did not respond to the start signal.', detail); + } + else if(data.reason == 'empty-recording-id') { + app.notifyAlert(title, "No recording ID specified."); + } + else if(data.reason == 'missing-client') { + notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); + } + else if(data.reason == 'already-recording') { + app.notifyAlert(title, 'Already recording.'); + } + else if(data.reason == 'recording-engine-unspecified') { + notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); + } + else if(data.reason == 'recording-engine-create-directory') { + notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); + } + else if(data.reason == 'recording-engine-create-file') { + notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); + } + else if(data.reason == 'recording-engine-sample-rate') { + notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); + } + else { + notifyWithUserInfo(title, 'Error Reason: ' + reason); + } + displayDoneRecording(); + } + else + { + displayStartedRecording(); + displayWhoCreated(data.clientId); + } }) .on('stoppingRecording', function(e, data) { displayStoppingRecording(data); @@ -174,15 +260,35 @@ .on('stoppedRecording', function(e, data) { if(data.reason) { var reason = data.reason; + var detail = data.detail; + + var title = "Recording Discarded"; + if(data.reason == 'client-no-response') { - reason = 'someone in the session has disconnected'; + notifyWithUserInfo(title, 'did not respond to the stop signal.', detail); } - var text = "This recording has been thrown out because " + reason + "." - app.notify({ - "title": "Recording Deleted", - "text": text, - "icon_url": "/assets/content/icon_alert_big.png" - }); + else if(data.reason == 'missing-client') { + notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail); + } + else if(data.reason == 'empty-recording-id') { + app.notifyAlert(title, "No recording ID specified."); + } + else if(data.reason == 'wrong-recording-id') { + app.notifyAlert(title, "Wrong recording ID specified."); + } + else if(data.reason == 'not-recording') { + app.notifyAlert(title, "Not currently recording."); + } + else if(data.reason == 'already-stopping') { + app.notifyAlert(title, "Already stopping the current recording."); + } + else if(data.reason == 'start-before-stop') { + notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail); + } + else { + app.notifyAlert(title, "Error reason: " + reason); + } + displayDoneRecording(); } else { @@ -191,12 +297,40 @@ } }) - .on('startedRecordingFailed', function(e, data) { + .on('abortedRecording', function(e, data) { + var reason = data.reason; + var detail = data.detail; + + var title = "Recording Cancelled"; + + if(data.reason == 'client-no-response') { + notifyWithUserInfo(title, 'did not respond to the start signal.', detail); + } + else if(data.reason == 'missing-client') { + notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); + } + else if(data.reason == 'populate-recording-info') { + notifyWithUserInfo(title, 'could not synchronize with the server.', detail); + } + else if(data.reason == 'recording-engine-unspecified') { + notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); + } + else if(data.reason == 'recording-engine-create-directory') { + notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); + } + else if(data.reason == 'recording-engine-create-file') { + notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); + } + else if(data.reason == 'recording-engine-sample-rate') { + notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); + } + else { + app.notifyAlert(title, "Error reason: " + reason); + } + + displayDoneRecording(); }) - .on('stoppedRecordingFailed', function(data) { - - }); sessionModel.subscribe('sessionScreen', sessionChanged); sessionModel.joinSession(sessionId) @@ -497,7 +631,7 @@ addNewGearDialog = new context.JK.AddNewGearDialog(app, ftueCallback); // # NO LONGER HIDING ADD TRACK even when there are 2 tracks (VRFS-537) - $('#div-add-track').click(function() { + $('#div-add-track').unbind('click').click(function() { if (myTracks.length === 2) { $('#btn-error-ok').click(function() { app.layout.closeDialog('error-dialog'); diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 1d9fa6e05..c4b5148c6 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -18,7 +18,7 @@ var pendingSessionRefresh = false; var recordingModel = new context.JK.RecordingModel(app, this, rest, context.jamClient); function id() { - return currentSession.id; + return currentSession ? currentSession.id : null; } function participants() { @@ -80,41 +80,41 @@ return deferred; } + function performLeaveSession(deferred) { + + logger.debug("SessionModel.leaveCurrentSession()"); + // TODO - sessionChanged will be called with currentSession = null + server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession); + server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession); + // leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long + // time, for that entire duration you'll still be sending voice data to the other users. + // this may be bad if someone decides to badmouth others in the left-session during this time + logger.debug("calling jamClient.LeaveSession for clientId=" + clientId); + client.LeaveSession({ sessionID: currentSessionId }); + deferred = leaveSessionRest(currentSessionId); + deferred.done(function() { + sessionChanged(); + }); + + // 'unregister' for callbacks + context.jamClient.SessionRegisterCallback(""); + context.jamClient.SessionSetAlertCallback(""); + updateCurrentSession(null); + currentSessionId = null; + } /** * Leave the current session, if there is one. * callback: called in all conditions; either after an attempt is made to tell the server that we are leaving, * or immediately if there is no session */ function leaveCurrentSession() { - var deferred; + var deferred = new $.Deferred(); - if(currentSessionId) { - logger.debug("SessionModel.leaveCurrentSession()"); - // TODO - sessionChanged will be called with currentSession = null - server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession); - server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession); - // leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long - // time, for that entire duration you'll still be sending voice data to the other users. - // this may be bad if someone decides to badmouth others in the left-session during this time - logger.debug("calling jamClient.LeaveSession for clientId=" + clientId); - client.LeaveSession({ sessionID: currentSessionId }); - deferred = leaveSessionRest(currentSessionId); - deferred.done(function() { - sessionChanged(); + recordingModel.stopRecordingIfNeeded() + .always(function(){ + performLeaveSession(deferred); }); - // 'unregister' for callbacks - //context.jamClient.RecordingRegisterCallbacks("", "", "", "", ""); - context.jamClient.SessionRegisterCallback(""); - context.jamClient.SessionSetAlertCallback(""); - updateCurrentSession(null); - currentSessionId = null; - } - else { - deferred = new $.Deferred(); - deferred.resolve(); - } - return deferred; } @@ -122,6 +122,7 @@ * Refresh the current session, and participants. */ function refreshCurrentSession() { + // XXX use backend instead: https://jamkazam.atlassian.net/browse/VRFS-854 logger.debug("SessionModel.refreshCurrentSession()"); refreshCurrentSessionRest(sessionChanged); } @@ -167,8 +168,6 @@ async: false, success: function(response) { sendClientParticipantChanges(currentSession, response); - logger.debug("Current Session Refreshed:"); - logger.debug(response); updateCurrentSession(response); if(callback != null) { callback(); @@ -262,7 +261,7 @@ } function addTrack(sessionId, data) { - logger.debug("track data = " + JSON.stringify(data)); + logger.debug("updating tracks on the server %o", data); var url = "/api/sessions/" + sessionId + "/tracks"; $.ajax({ type: "POST", @@ -274,9 +273,8 @@ processData:false, success: function(response) { // save to the backend - context.jamClient.TrackSaveAssignments(); - logger.debug("Successfully added track (" + JSON.stringify(data) + ")"); - refreshCurrentSession(); + logger.debug("successfully updated tracks on the server"); + //refreshCurrentSession(); }, error: ajaxError }); @@ -300,7 +298,13 @@ } function deleteTrack(sessionId, trackId) { + if (trackId) { + + client.TrackSetCount(1); + client.TrackSaveAssignments(); + + /** $.ajax({ type: "DELETE", url: "/api/sessions/" + sessionId + "/tracks/" + trackId, @@ -319,6 +323,7 @@ logger.error("Error deleting track " + trackId); } }); + */ } } diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js index 1cec3b246..713f78a42 100644 --- a/web/app/assets/javascripts/trackHelpers.js +++ b/web/app/assets/javascripts/trackHelpers.js @@ -14,6 +14,21 @@ // take all necessary arguments to complete its work. context.JK.TrackHelpers = { + getTracks: function(jamClient, groupId) { + var tracks = []; + var trackIds = jamClient.SessionGetIDs(); + var allTracks = jamClient.SessionGetControlState(trackIds); + + // get client's tracks + for (var i=0; i < allTracks.length; i++) { + if (allTracks[i].group_id === groupId) { + tracks.push(allTracks[i]); + } + } + + return tracks; + }, + /** * This function resolves which tracks to configure for a user * when creating or joining a session. By default, tracks are pulled @@ -22,80 +37,25 @@ */ getUserTracks: function(jamClient) { var trackIds = jamClient.SessionGetIDs(); - var allTracks = jamClient.SessionGetControlState(trackIds); var localMusicTracks = []; var i; var instruments = []; - var localTrackExists = false; - // get client's tracks - for (i=0; i < allTracks.length; i++) { - if (allTracks[i].group_id === 2) { - localMusicTracks.push(allTracks[i]); - - console.log("allTracks[" + i + "].instrument_id=" + allTracks[i].instrument_id); - // check if local track config exists - if (allTracks[i].instrument_id !== 0) { - localTrackExists = true; - } - } - } + localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, 2); var trackObjects = []; - console.log("localTrackExists=" + localTrackExists); - - // get most proficient instrument from API if no local track config exists - if (!localTrackExists) { - if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) { - var track = { - instrument_id: context.JK.userMe.instruments[0].instrument_id, - sound: "stereo" - }; - trackObjects.push(track); - - var desc = context.JK.userMe.instruments[0].description; - jamClient.TrackSetInstrument(1, context.JK.server_to_client_instrument_map[desc]); - jamClient.TrackSaveAssignments(); - } - } - // use all tracks previously configured - else { - console.log("localMusicTracks.length=" + localMusicTracks.length); - for (i=0; i < localMusicTracks.length; i++) { - var track = {}; - var instrument_description = ''; - console.log("localMusicTracks[" + i + "].instrument_id=" + localMusicTracks[i].instrument_id); - - // no instruments configured - if (localMusicTracks[i].instrument_id === 0) { - if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) { - track.instrument_id = context.JK.userMe.instruments[0].instrument_id; - } - else { - track.instrument_id = context.JK.client_to_server_instrument_map[250].server_id; - } - } - // instruments are configured - else { - if (context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id]) { - track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id; - } - // fall back to Other - else { - track.instrument_id = context.JK.client_to_server_instrument_map[250].server_id; - jamClient.TrackSetInstrument(i+1, 250); - jamClient.TrackSaveAssignments(); - } - } - if (localMusicTracks[i].stereo) { - track.sound = "stereo"; - } - else { - track.sound = "mono"; - } - trackObjects.push(track); + for (i=0; i < localMusicTracks.length; i++) { + var track = {}; + track.client_track_id = localMusicTracks[i].id; + track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id; + if (localMusicTracks[i].stereo) { + track.sound = "stereo"; } + else { + track.sound = "mono"; + } + trackObjects.push(track); } return trackObjects; } diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index c8797b1ec..d6ff1c289 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -220,6 +220,19 @@ return count; }; + /* + * Get the keys of a dictionary as an array (same as Object.keys, but works in all browsers) + */ + context.JK.dkeys = function(d) { + var keys = [] + for (var i in d) { + if (d.hasOwnProperty(i)) { + keys.push(i); + } + } + return keys; + }; + /** * Finds the first error associated with the field. * @param fieldName the name of the field diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index 81be02657..1d1028a8b 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -141,11 +141,24 @@ class ApiMusicSessionsController < ApiController end end + def track_sync + @tracks = Track.sync(params[:client_id], params[:tracks]) + + unless @tracks.kind_of? Array + # we have to do this because api_session_detail_url will fail with a bad @music_session + response.status = :unprocessable_entity + respond_with @tracks + else + respond_with @tracks, responder: ApiResponder + end + end + def track_create @track = Track.save(nil, params[:connection_id], params[:instrument_id], - params[:sound]) + params[:sound], + params[:client_track_id]) respond_with @track, responder: ApiResponder, :status => 201, :location => api_session_track_detail_url(@track.connection.music_session, @track) end @@ -155,7 +168,8 @@ class ApiMusicSessionsController < ApiController @track = Track.save(params[:track_id], nil, params[:instrument_id], - params[:sound]) + params[:sound], + params[:client_track_id]) respond_with @track, responder: ApiResponder, :status => 200 diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 415f4221d..adc061e44 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -21,7 +21,7 @@ child(:connections => :participants) { end child(:tracks => :tracks) { - attributes :id, :connection_id, :instrument_id, :sound + attributes :id, :connection_id, :instrument_id, :sound, :client_track_id } } diff --git a/web/app/views/api_music_sessions/track_show.rabl b/web/app/views/api_music_sessions/track_show.rabl index c460b0085..9bfb6d9bd 100644 --- a/web/app/views/api_music_sessions/track_show.rabl +++ b/web/app/views/api_music_sessions/track_show.rabl @@ -1,3 +1,3 @@ object @track -attributes :id, :connection_id, :instrument_id, :sound \ No newline at end of file +attributes :id, :connection_id, :instrument_id, :sound, :client_track_id \ No newline at end of file diff --git a/web/config/routes.rb b/web/config/routes.rb index 2f30a9b2a..f008a2ef2 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -91,6 +91,7 @@ SampleApp::Application.routes.draw do # music session tracks match '/sessions/:id/tracks' => 'api_music_sessions#track_create', :via => :post + match '/sessions/:id/tracks' => 'api_music_sessions#track_sync', :via => :put match '/sessions/:id/tracks' => 'api_music_sessions#track_index', :via => :get match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_update', :via => :post match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_show', :via => :get, :as => 'api_session_track_detail' diff --git a/web/spec/factories.rb b/web/spec/factories.rb index d64e4eacf..af2e17887 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -120,6 +120,7 @@ FactoryGirl.define do factory :track, :class => JamRuby::Track do sound "mono" + sequence(:client_track_id) { |n| "client_track_id_seq_#{n}"} end diff --git a/web/spec/managers/music_session_manager_spec.rb b/web/spec/managers/music_session_manager_spec.rb index f01155bce..7ddb3a2a1 100644 --- a/web/spec/managers/music_session_manager_spec.rb +++ b/web/spec/managers/music_session_manager_spec.rb @@ -13,7 +13,7 @@ describe MusicSessionManager do @band = FactoryGirl.create(:band) @genre = FactoryGirl.create(:genre) @instrument = FactoryGirl.create(:instrument) - @tracks = [{"instrument_id" => @instrument.id, "sound" => "mono"}] + @tracks = [{"instrument_id" => @instrument.id, "sound" => "mono", "client_track_id" => "abcd"}] @connection = FactoryGirl.create(:connection, :user => @user) end diff --git a/web/spec/requests/music_sessions_api_spec.rb b/web/spec/requests/music_sessions_api_spec.rb index fb092a0b1..e4703e064 100755 --- a/web/spec/requests/music_sessions_api_spec.rb +++ b/web/spec/requests/music_sessions_api_spec.rb @@ -23,7 +23,7 @@ describe "Music Session API ", :type => :api do let(:user) { FactoryGirl.create(:user) } # defopts are used to setup default options for the session - let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}], :legal_terms => true, :intellectual_property => true} } + let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}], :legal_terms => true, :intellectual_property => true} } before do #sign_in user MusicSession.delete_all @@ -115,7 +115,7 @@ describe "Music Session API ", :type => :api do # create a 2nd track for this session conn_id = updated_track["connection_id"] - post "/api/sessions/#{music_session["id"]}/tracks.json", { :connection_id => "#{conn_id}", :instrument_id => "electric guitar", :sound => "mono" }.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{music_session["id"]}/tracks.json", { :connection_id => "#{conn_id}", :instrument_id => "electric guitar", :sound => "mono", :client_track_id => "client_track_guid" }.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should == 201 get "/api/sessions/#{music_session["id"]}/tracks.json", "CONTENT_TYPE" => 'application/json' @@ -239,7 +239,7 @@ describe "Music Session API ", :type => :api do musician["client_id"].should == client.client_id login(user2) - post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(201) @@ -311,7 +311,7 @@ describe "Music Session API ", :type => :api do original_count = MusicSession.all().length client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1") - post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "mom", "sound" => "mono"}]}).to_json, "CONTENT_TYPE" => 'application/json' + post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "mom", "sound" => "mono", "client_track_id" => "client_track_guid"}]}).to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(404) # check that the transaction was rolled back @@ -322,7 +322,7 @@ describe "Music Session API ", :type => :api do original_count = MusicSession.all().length client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1") - post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mom"}]}).to_json, "CONTENT_TYPE" => 'application/json' + post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mom", "client_track_id" => "client_track_guid"}]}).to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(422) JSON.parse(last_response.body)["errors"]["tracks"][0].should == "is invalid" @@ -411,7 +411,7 @@ describe "Music Session API ", :type => :api do # users are friends, but no invitation... so we shouldn't be able to join as user 2 login(user2) - post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}] }.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(422) join_response = JSON.parse(last_response.body) join_response["errors"]["musician_access"].should == [ValidationMessages::INVITE_REQUIRED] @@ -422,7 +422,7 @@ describe "Music Session API ", :type => :api do last_response.status.should eql(201) login(user2) - post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}] }.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(201) end @@ -492,7 +492,7 @@ describe "Music Session API ", :type => :api do client2 = FactoryGirl.create(:connection, :user => user2, :ip_address => "2.2.2.2") login(user2) - post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(422) rejected_join_attempt = JSON.parse(last_response.body) rejected_join_attempt["errors"]["approval_required"] = [ValidationMessages::INVITE_REQUIRED] @@ -514,7 +514,7 @@ describe "Music Session API ", :type => :api do # finally, go back to user2 and attempt to join again login(user2) - post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(201) end @@ -544,7 +544,7 @@ describe "Music Session API ", :type => :api do track["sound"].should == "mono" - post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(201) @@ -581,7 +581,7 @@ describe "Music Session API ", :type => :api do # user 2 should not be able to join login(user2) - post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(422) JSON.parse(last_response.body)["errors"]["music_session"][0].should == ValidationMessages::CANT_JOIN_RECORDING_SESSION end @@ -636,6 +636,34 @@ describe "Music Session API ", :type => :api do msuh.rating.should == 0 end + it "track sync" do + user = FactoryGirl.create(:single_user_session) + instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + music_session = FactoryGirl.create(:music_session, :creator => user) + client = FactoryGirl.create(:connection, :user => user, :music_session => music_session) + track = FactoryGirl.create(:track, :connection => client, :instrument => instrument) + + existing_track = {:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id } + new_track = {:client_track_id => "client_track_id1", :instrument_id => instrument.id, :sound => 'stereo'} + # let's add a new track, and leave the existing one alone + tracks = [existing_track, new_track] + login(user) + + put "/api/sessions/#{music_session.id}/tracks.json", { :client_id => client.client_id, :tracks => tracks }.to_json, "CONTENT_TYPE" => "application/json" + last_response.status.should == 204 + + get "/api/sessions/#{music_session.id}/tracks.json", "CONTENT_TYPE" => 'application/json' + last_response.status.should == 200 + tracks = JSON.parse(last_response.body) + tracks.size.should == 2 + tracks[0]["id"].should == track.id + tracks[0]["instrument_id"].should == instrument.id + tracks[0]["sound"].should == "mono" + tracks[0]["client_track_id"].should == track.client_track_id + tracks[1]["instrument_id"].should == instrument.id + tracks[1]["sound"].should == "stereo" + tracks[1]["client_track_id"].should == "client_track_id1" + end end diff --git a/web/spec/requests/user_progression_spec.rb b/web/spec/requests/user_progression_spec.rb index cff28581d..ec1fd399a 100644 --- a/web/spec/requests/user_progression_spec.rb +++ b/web/spec/requests/user_progression_spec.rb @@ -17,7 +17,7 @@ describe "User Progression", :type => :api do describe "user progression" do let(:user) { FactoryGirl.create(:user) } - let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}], :legal_terms => true, :intellectual_property => true} } + let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}], :legal_terms => true, :intellectual_property => true} } before do login(user) @@ -105,11 +105,11 @@ describe "User Progression", :type => :api do music_session = JSON.parse(last_response.body) login(user2) - post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(201) login(user3) - post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client3.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json' + post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client3.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json' last_response.status.should eql(201) # instrument the created_at of the music_history field to be at the beginning of time, so that we cross the 15 minute threshold of a 'real session diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index a64d6d01b..f6a6f6b7c 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -61,6 +61,7 @@ module JamWebsockets end def add_client(client_id, client_context) + @log.debug "adding client #{client_id} to @client_lookup" @client_lookup[client_id] = client_context end @@ -172,20 +173,26 @@ module JamWebsockets client_id = routing_key["client.".length..-1] @semaphore.synchronize do client_context = @client_lookup[client_id] - client = client_context.client - msg = Jampb::ClientMessage.parse(msg) + if !client_context.nil? - @log.debug "client-directed message received from #{msg.from} to client #{client_id}" + client = client_context.client - unless client.nil? + msg = Jampb::ClientMessage.parse(msg) - EM.schedule do - @log.debug "sending client-directed down websocket to #{client_id}" - send_to_client(client, msg) + @log.debug "client-directed message received from #{msg.from} to client #{client_id}" + + unless client.nil? + + EM.schedule do + @log.debug "sending client-directed down websocket to #{client_id}" + send_to_client(client, msg) + end + else + @log.debug "client-directed message unroutable to disconnected client #{client_id}" end else - @log.debug "client-directed message unroutable to disconnected client #{client_id}" + @log.debug "Can't route message: no client connected with id #{client_id}" end end rescue => e @@ -633,6 +640,10 @@ module JamWebsockets # belong to access_p2p(to_client_id, context.user, client_msg) + if to_client_id.nil? || to_client_id == 'undefined' # javascript translates to 'undefined' in many cases + raise SessionError, "empty client_id specified in peer-to-peer message" + end + # populate routing data client_msg.from = client.client_id