* VRFS-813 -most all frontend changes needed for start/stop recordings

This commit is contained in:
Seth Call 2013-11-03 14:55:55 -06:00
parent 9e7e905c87
commit db76c34ba5
63 changed files with 2569 additions and 799 deletions

View File

@ -74,3 +74,4 @@ crash_dumps_idx.sql
music_sessions_user_history_add_session_removed_at.sql
user_progress_tracking.sql
whats_next.sql
recordings_public_launch.sql

View File

@ -0,0 +1,30 @@
-- so that columns can live on
ALTER TABLE recordings DROP CONSTRAINT "recordings_music_session_id_fkey";
ALTER TABLE recordings ADD COLUMN is_done BOOLEAN DEFAULT FALSE;
--ALTER TABLE music_session ADD COLUMN is_recording BOOLEAN DEFAULT FALSE;
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;
CREATE TRIGGER tsvectorupdate_description BEFORE INSERT OR UPDATE
ON claimed_recordings FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(description_tsv, 'public.jamenglish', description);
CREATE TRIGGER tsvectorupdate_name BEFORE INSERT OR UPDATE
ON claimed_recordings FOR EACH ROW EXECUTE PROCEDURE
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;

View File

@ -177,6 +177,7 @@ message MusicianSessionDepart {
optional string user_id = 2; // this is the user_id and can be used for user unicast messages
optional string username = 3; // meant to be a display name
optional string photo_url = 4;
optional string recordingId = 5; // if specified, the recording was stopped automatically
}
// route_to: client:

View File

@ -34,4 +34,18 @@ module ValidationMessages
EMAIL_ALREADY_TAKEN = "has already been taken"
EMAIL_MATCHES_CURRENT = "is same as your current email"
INVALID_FPFILE = "is not valid"
#connection
SELECT_AT_LEAST_ONE = "Please select at least one track"
FAN_CAN_NOT_JOIN_AS_MUSICIAN = "A fan can not join a music session as a musician"
MUSIC_SESSION_MUST_BE_SPECIFIED = "A music session must be specified"
INVITE_REQUIRED = "You must be invited to join this session"
FANS_CAN_NOT_JOIN = "Fans can not join this session"
CANT_JOIN_RECORDING_SESSION = "is currently recording"
# recordings
ALREADY_BEING_RECORDED = "already being recorded"
NO_LONGER_RECORDING = "no longer recording"
NOT_IN_SESSION = "not in session"
end

View File

@ -107,8 +107,8 @@
end
# create a musician left session message
def musician_session_depart(session_id, user_id, username, photo_url)
left = Jampb::MusicianSessionDepart.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
def musician_session_depart(session_id, user_id, username, photo_url, recordingId = nil)
left = Jampb::MusicianSessionDepart.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url, :recordingId => recordingId)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_DEPART, :route_to => CLIENT_TARGET, :musician_session_depart => left)
end

View File

@ -2,6 +2,7 @@ module JamRuby
class ClaimedRecording < ActiveRecord::Base
validates :name, no_profanity: true
validates :description, no_profanity: true
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :claimed_recordings
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :claimed_recordings
@ -16,6 +17,7 @@ module JamRuby
end
self.name = params[:name] unless params[:name].nil?
self.description = params[:description] unless params[:description].nil?
self.genre = Genre.find(params[:genre]) unless params[:genre].nil?
self.is_public = params[:is_public] unless params[:is_public].nil?
self.is_downloadable = params[:is_downloadable] unless params[:is_downloadable].nil?

View File

@ -3,11 +3,6 @@ require 'aasm'
module JamRuby
class Connection < ActiveRecord::Base
SELECT_AT_LEAST_ONE = "Please select at least one track"
FAN_CAN_NOT_JOIN_AS_MUSICIAN = "A fan can not join a music session as a musician"
MUSIC_SESSION_MUST_BE_SPECIFIED = "A music session must be specified"
INVITE_REQUIRED = "You must be invited to join this session"
FANS_CAN_NOT_JOIN = "Fans can not join this session"
attr_accessor :joining_session
@ -71,37 +66,40 @@ module JamRuby
def can_join_music_session
if music_session.nil?
errors.add(:music_session, MUSIC_SESSION_MUST_BE_SPECIFIED)
errors.add(:music_session, ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED)
return false
end
if as_musician
unless self.user.musician
errors.add(:as_musician, FAN_CAN_NOT_JOIN_AS_MUSICIAN)
errors.add(:as_musician, ValidationMesages::FAN_CAN_NOT_JOIN_AS_MUSICIAN)
return false
end
if music_session.musician_access
if music_session.approval_required
unless music_session.creator == user || music_session.invited_musicians.exists?(user)
errors.add(:approval_required, INVITE_REQUIRED)
errors.add(:approval_required, ValidationMessages::INVITE_REQUIRED)
return false
end
end
else
unless music_session.creator == user || music_session.invited_musicians.exists?(user)
errors.add(:musician_access, INVITE_REQUIRED)
errors.add(:musician_access, ValidationMessages::INVITE_REQUIRED)
return false
end
end
else
unless self.music_session.fan_access
# it's someone joining as a fan, and the only way a fan can join is if fan_access is true
errors.add(:fan_access, FANS_CAN_NOT_JOIN)
errors.add(:fan_access, ValidationMessages::FANS_CAN_NOT_JOIN)
return false
end
end
if music_session.is_recording?
errors.add(:music_session, ValidationMessages::CANT_JOIN_RECORDING_SESSION)
end
return true
end
@ -115,7 +113,7 @@ module JamRuby
private
def require_at_least_one_track_when_in_session
if tracks.count == 0
errors.add(:genres, SELECT_AT_LEAST_ONE)
errors.add(:genres, ValidationMessages::SELECT_AT_LEAST_ONE)
end
end

View File

@ -1,6 +1,5 @@
module JamRuby
class MusicSession < ActiveRecord::Base
self.primary_key = 'id'
attr_accessor :legal_terms, :skip_genre_validation
@ -17,8 +16,7 @@ module JamRuby
has_many :fan_invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::FanInvitation"
has_many :invited_fans, :through => :fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
has_one :recording, :class_name => "JamRuby::Recording", :inverse_of => :music_session
has_many :recordings, :class_name => "JamRuby::Recording", :inverse_of => :music_session
belongs_to :band, :inverse_of => :music_sessions, :class_name => "JamRuby::Band", :foreign_key => "band_id"
after_save :require_at_least_one_genre, :limit_max_genres
@ -38,7 +36,7 @@ module JamRuby
def creator_is_musician
unless creator.musician?
errors.add(:creator, "creator must be a musician")
errors.add(:creator, "must be a musician")
end
end
@ -168,7 +166,22 @@ module JamRuby
def access? user
return self.users.exists? user
end
# is this music session currently recording?
def is_recording?
recordings.where(:duration => nil).count > 0
end
def recording
recordings.where(:duration => nil).first
end
# stops any active recording
def stop_recording
current_recording = self.recording
current_recording.stop unless current_recording.nil?
end
def to_s
return description
end

View File

@ -241,10 +241,10 @@ module JamRuby
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => connection.client_id})
end
def send_musician_session_depart(music_session, client_id, user)
def send_musician_session_depart(music_session, client_id, user, recordingId = nil)
# (1) create notification
msg = @@message_factory.musician_session_depart(music_session.id, user.id, user.name, user.photo_url)
msg = @@message_factory.musician_session_depart(music_session.id, user.id, user.name, user.photo_url, recordingId)
# (2) send notification
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})

View File

@ -12,17 +12,19 @@ module JamRuby
belongs_to :instrument, :class_name => "JamRuby::Instrument"
validates :sound, :inclusion => {:in => SOUND}
validates :client_id, :presence => true # not a connection relation on purpose
validates :track_id, :presence => true # not a track relation on purpose
before_destroy :delete_s3_files
# Copy an ephemeral track to create a saved one. Some fields are ok with defaults
def self.create_from_track(track, recording)
recorded_track = self.new
recorded_track.recording = recording
recorded_track.client_id = track.connection.client_id
recorded_track.track_id = track.id
recorded_track.user = track.connection.user
recorded_track.instrument = track.instrument
recorded_track.sound = track.sound
recorded_track.save
recorded_track
end

View File

@ -3,51 +3,48 @@ module JamRuby
self.primary_key = 'id'
attr_accessible :name, :description, :genre, :is_public, :is_downloadable
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording
has_many :users, :through => :claimed_recordings, :class_name => "JamRuby::User"
has_many :users, :through => :recorded_tracks, :class_name => "JamRuby::User"
belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings
belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings
belongs_to :music_session, :class_name => "JamRuby::MusicSession", :inverse_of => :recording
belongs_to :music_session, :class_name => "JamRuby::MusicSession", :inverse_of => :recordings
has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id
validates :music_session, :presence => true
validate :not_already_recording, :on => :create
validate :already_stopped_recording
def not_already_recording
if music_session.is_recording?
errors.add(:music_session, ValidationMessages::ALREADY_BEING_RECORDED)
end
end
def already_stopped_recording
if is_done && is_done_was
errors.add(:music_session, ValidationMessages::NO_LONGER_RECORDING)
end
end
# Start recording a session.
def self.start(music_session_id, owner)
def self.start(music_session, owner)
recording = nil
# Use a transaction and lock to avoid races.
ActiveRecord::Base.transaction do
music_session = MusicSession.find(music_session_id, :lock => true)
if music_session.nil?
raise PermissionError, "the session has ended"
end
unless music_session.recording.nil?
raise PermissionError, "the session is already being recorded"
end
music_session.with_lock do
recording = Recording.new
recording.music_session = music_session
recording.owner = owner
recording.band = music_session.band
music_session.connections.each do |connection|
# Note that we do NOT connect the recording to any users at this point.
# That ONLY happens if a user clicks 'save'
# recording.users << connection.user
connection.tracks.each do |track|
RecordedTrack.create_from_track(track, recording)
recording.recorded_tracks << RecordedTrack.create_from_track(track, recording)
end
end
# Note that I believe this can be nil.
recording.band = music_session.band
recording.save
music_session.recording = recording
music_session.save
end
@ -55,10 +52,10 @@ module JamRuby
# NEED TO SEND NOTIFICATION TO ALL USERS IN THE SESSION THAT RECORDING HAS STARTED HERE.
# I'LL STUB IT A BIT. NOTE THAT I REDO THE FIND HERE BECAUSE I DON'T WANT TO SEND THESE
# NOTIFICATIONS WHILE THE DB ROW IS LOCKED
music_session = MusicSession.find(music_session_id)
music_session.connections.each do |connection|
# connection.notify_recording_has_started
end
#music_session = MusicSession.find(music_session_id)
#music_session.connections.each do |connection|
# # connection.notify_recording_has_started
#end
recording
end
@ -66,34 +63,27 @@ module JamRuby
# Stop recording a session
def stop
# Use a transaction and lock to avoid races.
ActiveRecord::Base.transaction do
music_session = MusicSession.find(self.music_session_id, :lock => true)
if music_session.nil?
raise PermissionError, "the session has ended"
end
unless music_session.recording
raise PermissionError, "the session is not currently being recorded"
end
music_session.recording = nil
music_session.save
music_session = MusicSession.find_by_id(music_session_id)
locker = music_session.nil? ? self : music_session
locker.with_lock do
self.duration = Time.now - created_at
self.is_done = true
self.save
end
self.duration = Time.now - created_at
save
self
end
# Called when a user wants to "claim" a recording. To do this, the user must have been one of the tracks in the recording.
def claim(user, name, genre, is_public, is_downloadable)
if self.users.include?(user)
raise PermissionError, "user already claimed this recording"
end
def claim(user, name, description, genre, is_public, is_downloadable)
# if self.users.include?(user)
# raise PermissionError, "user already claimed this recording"
# end
unless self.recorded_tracks.find { |recorded_track| recorded_track.user == user }
unless self.users.exists?(user)
raise PermissionError, "user was not in this session"
end
unless self.music_session.nil?
if self.music_session.is_recording?
raise PermissionError, "recording cannot be claimed while it is being recorded"
end
@ -105,11 +95,11 @@ module JamRuby
claimed_recording.user = user
claimed_recording.recording = self
claimed_recording.name = name
claimed_recording.description = description
claimed_recording.genre = genre
claimed_recording.is_public = is_public
claimed_recording.is_downloadable = is_downloadable
self.claimed_recordings << claimed_recording
save
claimed_recording
end

View File

@ -287,7 +287,7 @@ describe ConnectionManager do
connection = @connman.join_music_session(user, client_id2, music_session, true, TRACKS)
connection.errors.size.should == 1
connection.errors.get(:as_musician).should == [Connection::FAN_CAN_NOT_JOIN_AS_MUSICIAN]
connection.errors.get(:as_musician).should == [ValidationMessages::FAN_CAN_NOT_JOIN_AS_MUSICIAN]
end
it "as_musician is coerced to boolean" do
@ -352,7 +352,7 @@ describe ConnectionManager do
@connman.create_connection(user_id, client_id, "1.1.1.1")
connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS)
connection.errors.size.should == 1
connection.errors.get(:music_session).should == [Connection::MUSIC_SESSION_MUST_BE_SPECIFIED]
connection.errors.get(:music_session).should == [ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED]
end
it "join_music_session fails if approval_required and no invitation, but generates join_request" do

View File

@ -394,5 +394,44 @@ describe MusicSession do
music_session.valid?.should be_false
end
it "is_recording? returns false if not recording" do
user1 = FactoryGirl.create(:user)
music_session = FactoryGirl.build(:music_session, :creator => user1)
music_session.is_recording?.should be_false
end
describe "recordings" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user1)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:music_session, :creator => @user1, :musician_access => true)
@music_session.connections << @connection
@music_session.save
end
describe "not recording" do
it "stop_recording should return nil if not recording" do
@music_session.stop_recording.should be_nil
end
end
describe "currently recording" do
before(:each) do
@recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user1)
end
it "is_recording? returns true if recording" do
@music_session.is_recording?.should be_true
end
it "stop_recording should return recording object if recording" do
@music_session.stop_recording.should == @recording
end
end
end
end

View File

@ -6,8 +6,9 @@ describe RecordedTrack do
@user = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@recording = FactoryGirl.create(:recording, :owner => @user)
@recording = FactoryGirl.create(:recording, :music_session => @music_session, :owner => @user)
end
it "should copy from a regular track properly" do
@ -17,6 +18,8 @@ describe RecordedTrack do
@recorded_track.instrument.id.should == @track.instrument.id
@recorded_track.next_part_to_upload.should == 0
@recorded_track.fully_uploaded.should == false
@recorded_track.client_id = @connection.client_id
@recorded_track.track_id = @track.id
end
it "should update the next part to upload properly" do
@ -38,11 +41,13 @@ describe RecordedTrack do
it "properly finds a recorded track given its upload filename" do
@recorded_track = RecordedTrack.create_from_track(@track, @recording)
@recorded_track.save.should be_true
RecordedTrack.find_by_upload_filename("recording_#{@recorded_track.id}").should == @recorded_track
end
it "gets a url for the track" do
@recorded_track = RecordedTrack.create_from_track(@track, @recording)
@recorded_track.save.should be_true
@recorded_track.url.should == S3Manager.url(S3Manager.hashed_filename("recorded_track", @recorded_track.id))
end

View File

@ -5,23 +5,17 @@ describe Recording do
before do
S3Manager.set_unit_test
@user = FactoryGirl.create(:user)
@connection = FactoryGirl.create(:connection, :user => @user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
@connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session)
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
@music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
@music_session.connections << @connection
@music_session.save
end
it "should not start a recording if the music session doesnt exist" do
expect { Recording.start("bad_music_session_id", @user) }.to raise_error
end
end
it "should set up the recording properly when recording is started with 1 user in the session" do
@music_session.recording.should == nil
@recording = Recording.start(@music_session.id, @user)
@music_session.is_recording?.should be_false
@recording = Recording.start(@music_session, @user)
@music_session.reload
@music_session.recording.should == @recording
@music_session.recordings[0].should == @recording
@recording.owner_id.should == @user.id
@recorded_tracks = RecordedTrack.where(:recording_id => @recording.id)
@ -31,31 +25,34 @@ describe Recording do
end
it "should not start a recording if the session is already being recorded" do
Recording.start(@music_session.id, @user)
expect { Recording.start(@music_session.id, @user) }.to raise_error
Recording.start(@music_session, @user).errors.any?.should be_false
recording = Recording.start(@music_session, @user)
recording.valid?.should_not be_true
recording.errors[:music_session].should_not be_nil
end
it "should return the state to normal properly when you stop a recording" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@music_session.reload
@music_session.recording.should == nil
@recording.reload
@recording.music_session.should == nil
@music_session.is_recording?.should be_false
end
it "should error when you stop a recording twice" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
expect { @recording.stop }.to raise_error
@recording.errors.any?.should be_false
@recording.stop
@recording.errors.any?.should be_true
end
it "should be able to start, stop then start a recording again for the same music session" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording2 = Recording.start(@music_session.id, @user)
@music_session.recording.should == @recording2
@recording2 = Recording.start(@music_session, @user)
@music_session.recordings.exists?(@recording2).should be_true
end
it "should NOT attach the recording to all users in a the music session when recording started" do
@ -66,7 +63,7 @@ describe Recording do
@music_session.connections << @connection2
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@user.recordings.length.should == 0
#@user.recordings.first.should == @recording
@ -75,7 +72,7 @@ describe Recording do
end
it "should report correctly whether its tracks have been uploaded" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.uploaded?.should == false
@recording.stop
@recording.reload
@ -85,7 +82,7 @@ describe Recording do
end
it "should destroy a recording and all its recorded tracks properly" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@recorded_track = @recording.recorded_tracks.first
@ -95,11 +92,11 @@ describe Recording do
end
it "should allow a user to claim a recording" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "name", @genre, true, true)
@recording.claim(@user, "name", "description", @genre, true, true)
@recording.reload
@recording.users.length.should == 1
@recording.users.first.should == @user
@ -108,56 +105,58 @@ describe Recording do
@recording.claimed_recordings.length.should == 1
@claimed_recording = @recording.claimed_recordings.first
@claimed_recording.name.should == "name"
@claimed_recording.description.should == "description"
@claimed_recording.genre.should == @genre
@claimed_recording.is_public.should == true
@claimed_recording.is_downloadable.should == true
end
it "should fail if a user who was not in the session claims a recording" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
user2 = FactoryGirl.create(:user)
expect { @recording.claim(user2) }.to raise_error
expect { @recording.claim(user2, "name", "description", @genre, true, true) }.to raise_error
end
it "should fail if a user tries to claim a recording twice" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "name", @genre, true, true)
@recording.claim(@user, "name", "description", @genre, true, true)
@recording.reload
expect { @recording.claim(@user, "name", @genre, true, true) }.to raise_error
expect { @recording.claim(@user, "name", "description", @genre, true, true) }.to raise_error
end
it "should allow editing metadata for claimed recordings" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@claimed_recording = @recording.claim(@user, "name", @genre, true, true)
@claimed_recording = @recording.claim(@user, "name", "description", @genre, true, true)
@genre2 = FactoryGirl.create(:genre)
@claimed_recording.update_fields(@user, :name => "name2", :genre => @genre2.id, :is_public => false, :is_downloadable => false)
@claimed_recording.update_fields(@user, :name => "name2", :description => "description2", :genre => @genre2.id, :is_public => false, :is_downloadable => false)
@claimed_recording.reload
@claimed_recording.name.should == "name2"
@claimed_recording.description.should == "description2"
@claimed_recording.genre.should == @genre2
@claimed_recording.is_public.should == false
@claimed_recording.is_downloadable.should == false
end
it "should only allow the owner to edit a claimed recording" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@claimed_recording = @recording.claim(@user, "name", @genre, true, true)
@claimed_recording = @recording.claim(@user, "name", "description", @genre, true, true)
@user2 = FactoryGirl.create(:user)
expect { @claimed_recording.update_fields(@user2, "name2") }.to raise_error
end
it "should record the duration of the recording properly" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.duration.should be_nil
@recording.stop
@recording.reload
@ -173,35 +172,35 @@ describe Recording do
@track = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument)
@music_session.connections << @connection2
@music_session.save
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@claimed_recording = @recording.claim(@user, "name", @genre, true, true)
@claimed_recording = @recording.claim(@user, "name", "description", @genre, true, true)
expect { @claimed_recordign.discard(@user2) }.to raise_error
@claimed_recording = @recording.claim(@user2, "name2", @genre, true, true)
@claimed_recording = @recording.claim(@user2, "name2", "description2", @genre, true, true)
@claimed_recording.discard(@user2)
@recording.reload
@recording.claimed_recordings.length.should == 1
end
it "should destroy the entire recording if there was only one claimed_recording which is discarded" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@claimed_recording = @recording.claim(@user, "name", @genre, true, true)
@claimed_recording = @recording.claim(@user, "name", "description", @genre, true, true)
@claimed_recording.discard(@user)
expect { Recording.find(@recording.id) }.to raise_error
expect { ClaimedRecording.find(@claimed_recording.id) }.to raise_error
end
it "should return a file list for a user properly" do
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "Recording", @genre, true, true)
@recording.claim(@user, "Recording", "Recording Description", @genre, true, true)
Recording.list(@user)["downloads"].length.should == 0
Recording.list(@user)["uploads"].length.should == 1
file = Recording.list(@user)["uploads"].first
@ -241,7 +240,7 @@ describe Recording do
@track2 = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument2)
@music_session.connections << @connection2
@music_session.save
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
#sleep 4
@recording.stop
@recording.recorded_tracks.length.should == 2

View File

@ -113,6 +113,7 @@
callbacks[i](message, payload);
} catch (ex) {
logger.warn('exception in callback for websocket message:' + ex);
throw ex;
}
}
}
@ -152,6 +153,11 @@
server.send(loginMessage);
};
/** with the advent of the reliable UDP channel, this is no longer how messages are sent from client-to-clent
* however, the mechanism still exists and is useful in test contexts; and maybe in the future
* @param receiver_id client ID of message to send
* @param message the actual message
*/
server.sendP2PMessage = function(receiver_id, message) {
logger.log("P2P message from [" + server.clientID + "] to [" + receiver_id + "]: " + message);
var outgoing_msg = msg_factory.client_p2p_message(server.clientID, receiver_id, message);
@ -192,4 +198,5 @@
}
})(window, jQuery);

View File

@ -125,6 +125,10 @@
}
function saveSettings() {
if (!context.JK.verifyNotRecordingForTrackChange(app)) {
return;
}
if (!validateSettings()) {
return;
}

View File

@ -18,4 +18,5 @@
//= require jquery.Jcrop
//= require jquery.naturalsize
//= require jquery.queryparams
//= require globals
//= require_directory .

View File

@ -571,6 +571,10 @@
}
function saveSettings() {
if (!context.JK.verifyNotRecordingForTrackChange(app)) {
return;
}
if (!validateAudioSettings(false)) {
return;
}
@ -797,6 +801,14 @@
}
function _init() {
var dialogBindings = {
'beforeShow' : function() {
return context.JK.verifyNotRecordingForTrackChange(app);
}
};
app.bindDialog('configure-audio', dialogBindings);
// load instrument array for populating listboxes, using client_id in instrument_map as ID
context.JK.listInstruments(app, function(instruments) {
$.each(instruments, function(index, val) {

View File

@ -3,7 +3,7 @@
"use strict";
context.JK = context.JK || {};
context.JK.FakeJamClient = function(app) {
context.JK.FakeJamClient = function(app, p2pMessageFactory) {
var logger = context.JK.logger;
logger.info("*** Fake JamClient instance initialized. ***");
@ -18,6 +18,8 @@
var device_id = -1;
var latencyCallback = null;
var frameSize = 2.5;
var fakeJamClientRecordings = null;
var p2pCallbacks = null;
function dbg(msg) { logger.debug('FakeJamClient: ' + msg); }
@ -142,7 +144,31 @@
function LatencyUpdated(map) { dbg('LatencyUpdated:' + JSON.stringify(map)); }
function LeaveSession(map) { dbg('LeaveSession:' + JSON.stringify(map)); }
function P2PMessageReceived(s1,s2) { dbg('P2PMessageReceived:' + s1 + ',' + s2); }
// this is not a real bridge method; purely used by the fake jam client
function RegisterP2PMessageCallbacks(callbacks) {
p2pCallbacks = callbacks;
}
function P2PMessageReceived(from, payload) {
dbg('P2PMessageReceived');
// this function is different in that the payload is a JSON ready string;
// whereas a real p2p message is base64 encoded binary packaged data
try {
payload = JSON.parse(payload);
}
catch(e) {
logger.warn("unable to parse payload as JSON from client %o, %o, %o", from, e, payload);
}
var callback = p2pCallbacks[payload.type];
if(callback) {
callback(from, payload);
}
}
function JoinSession(sessionId) {dbg('JoinSession:' + sessionId);}
function ParticipantLeft(session, participant) {
dbg('ParticipantLeft:' + JSON.stringify(session) + ',' +
@ -167,9 +193,21 @@
}
function StartPlayTest(s) { dbg('StartPlayTest' + JSON.stringify(arguments)); }
function StartRecordTest(s) { dbg('StartRecordTest' + JSON.stringify(arguments)); }
function StartRecording(map) { dbg('StartRecording' + JSON.stringify(arguments)); }
function StartRecording(recordingId, groupedClientTracks) {
dbg('StartRecording');
fakeJamClientRecordings.StartRecording(recordingId, groupedClientTracks);
}
function StopPlayTest() { dbg('StopPlayTest'); }
function StopRecording(map) { dbg('StopRecording' + JSON.stringify(arguments)); }
function StopRecording(recordingId, groupedTracks, errorReason, detail) {
dbg('StopRecording');
fakeJamClientRecordings.StopRecording(recordingId, groupedTracks, errorReason, detail);
}
function AbortRecording(recordingId, errorReason, errorDetail) {
dbg('AbortRecording');
fakeJamClientRecordings.AbortRecording(recordingId, errorReason, errorDetail);
}
function TestASIOLatency(s) { dbg('TestASIOLatency' + JSON.stringify(arguments)); }
function TestLatency(clientID, callbackFunctionName, timeoutCallbackName) {
@ -244,6 +282,11 @@
"User@208.191.152.98_*"
];
}
function RecordingRegisterCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, requestStopCallbackName) {
fakeJamClientRecordings.RecordingRegisterCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, requestStopCallbackName);
}
function SessionRegisterCallback(callbackName) {
eventCallbackName = callbackName;
if (callbackTimer) { context.clearInterval(callbackTimer); }
@ -483,8 +526,18 @@
}
function ClientUpdateStartUpdate(path, successCallback, failureCallback) {}
// -------------------------------
// fake jam client methods
// not a part of the actual bridge
// -------------------------------
function SetFakeRecordingImpl(fakeRecordingsImpl) {
fakeJamClientRecordings = fakeRecordingsImpl;
}
// Javascript Bridge seems to camel-case
// Set the instance functions:
this.AbortRecording = AbortRecording;
this.GetASIODevices = GetASIODevices;
this.GetOS = GetOS;
this.GetOSAsString = GetOSAsString;
@ -548,6 +601,7 @@
this.SessionAddTrack = SessionAddTrack;
this.SessionGetControlState = SessionGetControlState;
this.SessionGetIDs = SessionGetIDs;
this.RecordingRegisterCallbacks = RecordingRegisterCallbacks;
this.SessionRegisterCallback = SessionRegisterCallback;
this.SessionSetAlertCallback = SessionSetAlertCallback;
this.SessionSetControlState = SessionSetControlState;
@ -596,6 +650,10 @@
this.ClientUpdateStartUpdate = ClientUpdateStartUpdate;
this.OpenSystemBrowser = OpenSystemBrowser;
// fake calls; not a part of the actual jam client
this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks;
this.SetFakeRecordingImpl = SetFakeRecordingImpl;
};
})(window,jQuery);

View File

@ -0,0 +1,76 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.FakeJamClientMessages = function() {
var self = this;
function startRecording(recordingId) {
var msg = {};
msg.type = self.Types.START_RECORDING;
msg.msgId = context.JK.generateUUID();
msg.recordingId = recordingId;
return msg;
}
function startRecordingAck(recordingId, success, reason, detail) {
var msg = {};
msg.type = self.Types.START_RECORDING_ACK;
msg.msgId = context.JK.generateUUID();
msg.recordingId = recordingId;
msg.success = success;
msg.reason = reason;
msg.detail = detail;
return msg;
}
function stopRecording(recordingId, errorReason, errorDetail) {
var msg = {};
msg.type = self.Types.STOP_RECORDING;
msg.msgId = context.JK.generateUUID();
msg.recordingId = recordingId;
msg.errorReason = errorReason;
msg.errorDetail = errorDetail;
return msg;
}
function stopRecordingAck(recordingId, success, reason, detail) {
var msg = {};
msg.type = self.Types.STOP_RECORDING_ACK;
msg.msgId = context.JK.generateUUID();
msg.recordingId = recordingId;
msg.success = success;
msg.reason = reason;
msg.detail = detail;
return msg;
}
function abortRecording(recordingId, errorReason, errorDetail) {
var msg = {};
msg.type = self.Types.ABORT_RECORDING;
msg.msgId = context.JK.generateUUID();
msg.recordingId = recordingId;
msg.errorReason = errorReason;
msg.errorDetail = errorDetail;
return msg;
}
this.Types = {};
this.Types.START_RECORDING = 'start_recording';
this.Types.START_RECORDING_ACK = 'start_recording_ack';
this.Types.STOP_RECORDING = 'stop_recording;'
this.Types.STOP_RECORDING_ACK = 'stop_recording_ack';
this.Types.ABORT_RECORDING = 'abort_recording';
this.startRecording = startRecording;
this.startRecordingAck = startRecordingAck;
this.stopRecording = stopRecording;
this.stopRecordingAck = stopRecordingAck;
this.abortRecording = abortRecording;
}
})(window, jQuery);

View File

@ -0,0 +1,232 @@
// this code simulates what the actual backend recording feature will do
(function(context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.FakeJamClientRecordings = function(app, fakeJamClient, p2pMessageFactory) {
var logger = context.JK.logger;
var startRecordingResultCallbackName = null;
var stopRecordingResultCallbackName = null;
var startedRecordingResultCallbackName = null;
var stoppedRecordingEventCallbackName = null;
var requestStopCallbackName = null;
var startingSessionState = null;
var stoppingSessionState = null;
var currentRecordingId = null;
var currentRecordingCreatorClientId = null;
function timeoutStartRecordingTimer() {
eval(startRecordingResultCallbackName).call(this, startingSessionState.recordingId, false, 'client-no-response', startingSessionState.groupedClientTracks);
startingSessionState = null;
}
function timeoutStopRecordingTimer() {
eval(stopRecordingResultCallbackName).call(this, stoppingSessionState.recordingId, false, 'client-no-response', stoppingSessionState.groupedClientTracks);
}
function StartRecording(recordingId, groupedClientTracks) {
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
// store the current recording's data
currentRecordingId = recordingId;
currentRecordingCreatorClientId = app.clientId;
if(context.JK.dlen(startingSessionState.groupedClientTracks) == 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) {
context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.startRecording(recordingId)));
}
}
}
function StopRecording(recordingId, groupedClientTracks, errorReason, errorDetail) {
if(startingSessionState) {
// we are currently starting a session.
// TODO
}
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);
if(context.JK.dlen(stoppingSessionState.groupedClientTracks) == 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)));
}
}
//eval(stopRecordingResultCallbackName).call(this, recordingId, true, null, null);
}
function AbortRecording(recordingId, errorReason, errorDetail) {
// todo check recordingId
context.JK.JamServer.sendP2PMessage(currentRecordingCreatorClientId, JSON.stringify(p2pMessageFactory.abortRecording(recordingId, errorReason, errorDetail)));
}
function onStartRecording(from, payload) {
logger.debug("received start recording request from " + from);
if(context.JK.CurrentSessionModel.recordingModel.isRecording()) {
// reject the request to start the recording
context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.startRecordingAck(payload.recordingId, false, "already-recording", null)));
}
else {
// accept the request, and then tell the frontend we are now recording
// a better client implementation would verify that the tracks specified match that what we have configured currently
// store the current recording's data
currentRecordingId = payload.recordingId;
currentRecordingCreatorClientId = from;
context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.startRecordingAck(payload.recordingId, true, null, null)));
eval(startedRecordingResultCallbackName).call(this, from, payload.recordingId);
}
}
function onStartRecordingAck(from, payload) {
logger.debug("received start recording ack from " + from);
// we should check transactionId; this could be an ACK for a different recording
if(startingSessionState) {
if(payload.success) {
delete startingSessionState.groupedClientTracks[from];
if(context.JK.dlen(startingSessionState.groupedClientTracks) == 0) {
finishSuccessfulStart(payload.recordingId);
}
}
else {
// TOOD: a client responded with error; we need to tell all other clients to abandon recording
logger.warn("received an unsuccessful start_record_ack from: " + from);
}
}
else {
logger.warn("received a start_record_ack when there is no recording starting from: " + from);
// TODO: this is an error case; we should signal back to the sender that we gave up
}
}
function onStopRecording(from, payload) {
logger.debug("received stop recording request from " + from);
// TODO check recordingId, and if currently recording
// we should return success if we are currently recording, or if we were already asked to stop for this recordingId
// 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);
}
function onStopRecordingAck(from, payload) {
logger.debug("received stop recording ack from " + from);
// we should check transactionId; this could be an ACK for a different recording
if(stoppingSessionState) {
if(payload.success) {
delete stoppingSessionState.groupedClientTracks[from];
if(context.JK.dlen(stoppingSessionState.groupedClientTracks) == 0) {
finishSuccessfulStop(payload.recordingId);
}
}
else {
// TOOD: a client responded with error; what now?
}
}
else {
// TODO: this is an error case; we should tell the caller we have no recording at the moment
}
}
function onAbortRecording(from, payload) {
logger.debug("received abort recording from " + from);
// TODO check if currently recording and if matches payload.recordingId
// 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);
}
else {
logger.warn("only the creator currently deals with the abort request. abort request sent from:" + from + " with a reason of: " + payload.errorReason);
}
}
function RecordingRegisterCallbacks(startRecordingCallbackName,
stopRecordingCallbackName,
startedRecordingCallbackName,
stoppedRecordingCallbackName,
_requestStopCallbackName) {
startRecordingResultCallbackName = startRecordingCallbackName;
stopRecordingResultCallbackName = stopRecordingCallbackName;
startedRecordingResultCallbackName = startedRecordingCallbackName;
stoppedRecordingEventCallbackName = stoppedRecordingCallbackName;
requestStopCallbackName = _requestStopCallbackName;
}
// 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) {
if(clientId != myClientId) {
newTracks[clientId] = tracks[clientId];
}
}
return newTracks;
}
function finishSuccessfulStart(recordingId) {
// all clients have responded.
clearTimeout(startingSessionState.aggegratingStartResultsTimer);
startingSessionState = null;
eval(startRecordingResultCallbackName).call(this, recordingId, true);
}
function finishSuccessfulStop(recordingId, errorReason) {
// all clients have responded.
clearTimeout(stoppingSessionState.aggegratingStopResultsTimer);
stoppingSessionState = null;
eval(stopRecordingResultCallbackName).call(this, recordingId, true, errorReason);
}
// register for p2p callbacks
var callbacks = {};
callbacks[p2pMessageFactory.Types.START_RECORDING] = onStartRecording;
callbacks[p2pMessageFactory.Types.START_RECORDING_ACK] = onStartRecordingAck;
callbacks[p2pMessageFactory.Types.STOP_RECORDING] = onStopRecording;
callbacks[p2pMessageFactory.Types.STOP_RECORDING_ACK] = onStopRecordingAck;
callbacks[p2pMessageFactory.Types.ABORT_RECORDING] = onAbortRecording;
fakeJamClient.RegisterP2PMessageCallbacks(callbacks);
this.StartRecording = StartRecording;
this.StopRecording = StopRecording;
this.AbortRecording = AbortRecording;
this.RecordingRegisterCallbacks = RecordingRegisterCallbacks;
}
})(window, jQuery);

View File

@ -72,4 +72,8 @@
240: { "server_id": "mandolin" },
250: { "server_id": "other" }
};
context.JK.entityToPrintable = {
music_session: "music session"
}
})(window,jQuery);

View File

@ -308,6 +308,39 @@
});
}
function startRecording(options) {
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/recordings/start",
data: JSON.stringify(options)
})
}
function stopRecording(options) {
var recordingId = options["id"]
return $.ajax({
type: "POST",
dataType: "json",
contentType: 'application/json',
url: "/api/recordings/" + recordingId + "/stop",
data: JSON.stringify(options)
})
}
function getRecording(options) {
var recordingId = options["id"];
return $.ajax({
type: "GET",
dataType: "json",
contentType: 'application/json',
url: "/api/recordings/" + recordingId
})
}
function initialize() {
return self;
}
@ -338,6 +371,9 @@
this.createJoinRequest = createJoinRequest;
this.updateJoinRequest = updateJoinRequest;
this.updateUser = updateUser;
this.startRecording = startRecording;
this.stopRecording = stopRecording;
this.getRecording = getRecording;
return this;
};

View File

@ -176,7 +176,38 @@
*/
function ajaxError(jqXHR, textStatus, errorMessage) {
logger.error("Unexpected ajax error: " + textStatus);
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
if(jqXHR.status == 404) {
app.notify({title: "Oops!", text: "What you were looking for is gone now."});
}
else if(jqXHR.status = 422) {
// present a nicer message
try {
var text = "<ul>";
var errorResponse = JSON.parse(jqXHR.responseText)["errors"];
for(var key in errorResponse) {
var errorsForKey = errorResponse[key];
console.log("key: " + key);
var prettyKey = context.JK.entityToPrintable[key];
if(!prettyKey) { prettyKey = key; }
for(var i = 0; i < errorsForKey.length; i++) {
text += "<li>" + prettyKey + " " + errorsForKey[i] + "</li>";
}
}
text += "<ul>";
app.notify({title: "Oops!", text: text, "icon_url": "/assets/content/icon_alert_big.png"});
}
catch(e) {
// give up; not formatted correctly
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
}
}
else {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
}
}
/**
@ -269,6 +300,7 @@
if (context.jamClient) {
// Unregister for callbacks.
context.jamClient.RecordingRegisterCallbacks("", "", "", "", "");
context.jamClient.SessionRegisterCallback("");
context.jamClient.SessionSetAlertCallback("");
context.jamClient.FTUERegisterVUCallbacks("", "", "");

View File

@ -428,9 +428,13 @@
function dialogEvent(dialog, evtName, data) {
if (dialog && dialog in dialogBindings) {
if (evtName in dialogBindings[dialog]) {
dialogBindings[dialog][evtName].call(me, data);
var result = dialogBindings[dialog][evtName].call(me, data);
if(result === false) {
return false;
}
}
}
return true;
}
function changeToScreen(screen, data) {
@ -496,7 +500,7 @@
}
function showDialog(dialog) {
dialogEvent(dialog, 'beforeShow');
if(!dialogEvent(dialog, 'beforeShow')) {return;}
var $overlay = $('.dialog-overlay')
$overlay.show();
centerDialog(dialog);

View File

@ -0,0 +1,299 @@
// The recording must be fed certain events, and as a simplification to consumers, it will emit state engine transition events.
// This class automatically watches for server notifications relating to recordings (start/stop), as well as backend events
// inputs:
// * startRecording: user wants to start a recording
// * stopRecording: user wants to stop recording
//
// events:
// * startingRecording: a recording has been requested, but isn't confirmed started
// * startedRecording: a recording is officially started
// * stoppingRecording: a stop to the current recording has been requested, but it isn't confirmed yet
// * stoppedRecording: a recording is not running
// *
(function(context,$) {
"use strict";
context.JK = context.JK || {};
var logger = context.JK.logger;
context.JK.RecordingModel = function(app, sessionModel, _rest, _jamClient) {
var currentRecording = null; // the JSON response from the server for a recording
var currentRecordingId = null;
var rest = _rest;
var currentlyRecording = false;
var startingRecording = false;
var stoppingRecording = false;
var waitingOnServerStop = false;
var waitingOnClientStop = false;
var waitingOnStopTimer = null;
var jamClient = _jamClient;
var sessionModel = sessionModel;
var $self = $(this);
function isRecording (recordingId) {
// if you specify recordingId, the check is more exact
if(recordingId) {
return recordingId == currentRecordingId;
}
else {
// if you omit recordingId, then we'll just check if we are recording at all
return currentlyRecording;
}
}
/** called every time a session is joined, to ensure clean state */
function reset() {
stoppingRecording = false;
startingRecording = false;
currentlyRecording = false;
waitingOnServerStop = false;
waitingOnClientStop = false;
if(waitingOnStopTimer != null) {
clearTimeout(waitingOnStopTimer);
waitingOnStopTimer = null;
}
currentRecording = null;
currentRecordingId = null;
}
function groupTracksToClient(recording) {
// group N tracks to the same client Id
var groupedTracks = {};
var recordingTracks = recording["recorded_tracks"];
for (var i = 0; i < recordingTracks.length; i++) {
var clientId = recordingTracks[i].client_id;
var tracksForClient = groupedTracks[clientId];
if (!tracksForClient) {
tracksForClient = [];
groupedTracks[clientId] = tracksForClient;
}
tracksForClient.push(recordingTracks[i]);
}
return 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', {});
currentRecording = rest.startRecording({"music_session_id": sessionModel.id()})
.done(function(recording) {
currentRecordingId = recording.id;
// ask the backend to start the session.
var groupedTracks = groupTracksToClient(recording);
jamClient.StartRecording(recording["id"], groupedTracks);
})
.fail(function() {
$self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments });
startingRecording = 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;
waitingOnServerStop = waitingOnClientStop = true;
waitingOnStopTimer = setTimeout(timeoutTransitionToStop, 5000);
$self.triggerHandler('stoppingRecording', {reason: errorReason, detail: errorDetail});
// this path assumes that the currentRecording info has, or can be, retrieved
// failure for currentRecording is handled elsewhere
currentRecording
.done(function(recording) {
var groupedTracks = groupTracksToClient(recording);
jamClient.StopRecording(recording.id, groupedTracks);
rest.stopRecording( { "id": recording.id } )
.done(function() {
waitingOnServerStop = false;
attemptTransitionToStop(recording.id, errorReason, errorDetail);
})
.fail(function(jqXHR) {
if(jqXHR.status == 422) {
waitingOnServerStop = false;
attemptTransitionToStop(recording.id, errorReason, errorDetail);
}
else {
logger.error("unable to stop recording %o", arguments);
transitionToStopped();
$self.triggerHandler('stoppedRecording', {'recordingId': recording.id, 'reason' : 'rest', 'details' : arguments});
}
});
});
return true;
}
function abortRecording(recordingId, errorReason, errorDetail) {
jamClient.AbortRecording(recordingId, errorReason, errorDetail);
}
function timeoutTransitionToStop() {
// doh. couldn't stop
waitingOnStopTimer = null;
transitionToStopped();
$self.triggerHandler('stoppedRecordingFailed', { 'reason' : 'timeout' });
}
// Only tell the user that we've stopped once both server and client agree we've stopped
function attemptTransitionToStop(recordingId, errorReason, errorDetail) {
if(!waitingOnClientStop && !waitingOnServerStop) {
transitionToStopped();
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail});
}
}
function transitionToStopped() {
stoppingRecording = false;
currentlyRecording = false;
currentRecording = null;
currentRecordingId = null;
if(waitingOnStopTimer) {
clearTimeout(waitingOnStopTimer);
waitingOnStopTimer = null;
}
}
function onServerStartRecording() {
}
function onServerStopRecording(recordingId) {
stopRecording(recordingId, null, null);
}
function handleRecordingStartResult(recordingId, success, reason, detail) {
startingRecording = false;
currentlyRecording = true;
if(success) {
$self.triggerHandler('startedRecording', {clientId: app.clientId})
}
else {
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) {
waitingOnClientStop = false;
if(success) {
attemptTransitionToStop(recordingId, reason, detail);
}
else {
transitionToStopped();
logger.error("backend unable to stop the recording %o, %o", reason, detail);
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail : detail});
}
}
function handleRecordingStarted(clientId, recordingId) {
// 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
currentRecording = rest.getRecording({id: recordingId})
.fail(function() {
abortRecording(recordingId, 'populate-recording-info', app.clientId);
})
.done(function(recording) {
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 });
// the backend says the recording must be stopped.
// tell the server to stop it too
rest.stopRecording({
recordingId: recordingId
})
.always(function() {
stoppingRecording = false;
currentlyRecording = false;
})
.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});
}
else if(jqXHR.status == 404) {
logger.debug("recording is already deleted %o", arguments);
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail});
}
else {
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: textStatus, detail: errorMessage});
}
})
.done(function() {
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail});
})
}
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);
}
this.initialize = function() {
};
this.startRecording = startRecording;
this.stopRecording = stopRecording;
this.onServerStopRecording = onServerStopRecording;
this.isRecording = isRecording;
this.reset = reset;
context.JK.HandleRecordingStartResult = handleRecordingStartResult;
context.JK.HandleRecordingStopResult = handleRecordingStopResult;
context.JK.HandleRecordingStopped = handleRecordingStopped;
context.JK.HandleRecordingStarted = handleRecordingStarted;
context.JK.HandleRequestRecordingStop = handleRequestRecordingStop;
};
})(window,jQuery);

View File

@ -10,19 +10,22 @@
var tracks = {};
var myTracks = [];
var mixers = [];
var configureTrackDialog;
var addTrackDialog;
var addNewGearDialog;
var screenActive = false;
var currentMixerRangeMin = null;
var currentMixerRangeMax = null;
var lookingForMixersCount = 0;
var lookingForMixersTimer = null;
var lookingForMixers = {};
var $recordingTimer = null;
var recordingTimerInterval = null;
var startTimeDate = null;
var startingRecording = false; // double-click guard
var rest = JK.Rest();
var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there.
@ -93,6 +96,7 @@
function beforeShow(data) {
sessionId = data.id;
$('#session-mytracks-container').empty();
displayDoneRecording(); // assumption is that you can't join a recording session, so this should be safe
}
function alertCallback(type, text) {
@ -117,6 +121,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.SessionSetAlertCallback("JK.AlertCallback");
// If you load this page directly, the loading of the current user
@ -145,12 +150,60 @@
context.jamClient
);
$(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();
}
})
.on('startedRecording', function(e, data) {
displayStartedRecording();
displayWhoCreated(data.clientId)
})
.on('stoppingRecording', function(e, data) {
displayStoppingRecording(data);
})
.on('stoppedRecording', function(e, data) {
if(data.reason) {
var reason = data.reason;
if(data.reason == 'client-no-response') {
reason = 'someone in the session has disconnected';
}
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"
});
displayDoneRecording();
}
else {
displayDoneRecording();
promptUserToSave(data.recordingId);
}
})
.on('startedRecordingFailed', function(e, data) {
})
.on('stoppedRecordingFailed', function(data) {
});
sessionModel.subscribe('sessionScreen', sessionChanged);
sessionModel.joinSession(sessionId)
.fail(function(xhr, textStatus, errorMessage) {
if(xhr.status == 404) {
// we tried to join the session, but it's already gone. kick user back to join session screen
context.window.location = "#/findSession";
context.window.location = "/client#/findSession";
app.notify(
{ title: "Unable to Join Session",
text: "The session you attempted to join is over."
@ -158,6 +211,10 @@
{ no_cancel: true });
}
else {
if(xhr.status == 422) {
// we tried to join the session, but it's already gone. kick user back to join session screen
context.window.location = "/client#/findSession";
}
app.ajaxError(xhr, textStatus, errorMessage);
}
});
@ -448,8 +505,16 @@
context.JK.showErrorDialog(app, "You can only have a maximum of 2 personal tracks per session.", "max # of tracks");
}
else {
app.layout.showDialog('add-track');
addTrackDialog.showDialog();
if(context.JK.CurrentSessionModel.recordingModel.isRecording()) {
app.notify({
title: "Currently Recording",
text: "Tracks can not be modified while recording.",
icon_url: "/assets/content/icon_alert_big.png"});
}
else {
app.layout.showDialog('add-track');
addTrackDialog.showDialog();
}
}
});
}
@ -804,10 +869,121 @@
return false;
}
// http://stackoverflow.com/questions/2604450/how-to-create-a-jquery-clock-timer
function updateRecordingTimer() {
function pretty_time_string(num) {
return ( num < 10 ? "0" : "" ) + num;
}
var total_seconds = (new Date - startTimeDate) / 1000;
var hours = Math.floor(total_seconds / 3600);
total_seconds = total_seconds % 3600;
var minutes = Math.floor(total_seconds / 60);
total_seconds = total_seconds % 60;
var seconds = Math.floor(total_seconds);
hours = pretty_time_string(hours);
minutes = pretty_time_string(minutes);
seconds = pretty_time_string(seconds);
if(hours > 0) {
var currentTimeString = hours + ":" + minutes + ":" + seconds;
}
else {
var currentTimeString = minutes + ":" + seconds;
}
$recordingTimer.text('(' + currentTimeString + ')');
}
function displayStartingRecording() {
$('#recording-start-stop').addClass('currently-recording');
$('#recording-status').text("Starting...")
}
function displayStartedRecording() {
startTimeDate = new Date;
$recordingTimer = $("<span id='recording-timer'>(0:00)</span>");
var $recordingStatus = $('<span></span>').append("<span>Stop Recording</span>").append($recordingTimer);
$('#recording-status').html( $recordingStatus );
recordingTimerInterval = setInterval(updateRecordingTimer, 1000);
}
function displayStoppingRecording(data) {
if(data) {
if(data.reason) {
app.notify({
"title": "Recording Aborted",
"text": "The recording was aborted due to '" + data.reason + '"',
"icon_url": "/assets/content/icon_alert_big.png"
});
}
}
$('#recording-status').text("Stopping...");
}
function displayDoneRecording() {
if(recordingTimerInterval) {
clearInterval(recordingTimerInterval);
recordingTimerInterval = null;
startTimeDate = null;
}
$recordingTimer = null;
$('#recording-start-stop').removeClass('currently-recording');
$('#recording-status').text("Make a Recording");
}
function displayWhoCreated(clientId) {
if(app.clientId != clientId) { // don't show to creator
sessionModel.findUserBy({clientId: clientId})
.done(function(user) {
app.notify({
"title": "Recording Started",
"text": user.name + " started a recording",
"icon_url": context.JK.resolveAvatarUrl(user.photo_url)
});
})
.fail(function() {
app.notify({
"title": "Recording Started",
"text": "Oops! Can't determine who started this recording",
"icon_url": "/assets/content/icon_alert_big.png"
});
})
}
}
function promptUserToSave(recordingId) {
rest.getRecording( {id: recordingId} )
.done(function(recording) {
app.layout.showDialog('recordingFinished');
})
.fail(app.ajaxError);
}
function startStopRecording() {
if(sessionModel.recordingModel.isRecording()) {
sessionModel.recordingModel.stopRecording();
}
else {
sessionModel.recordingModel.startRecording();
}
}
function events() {
$('#session-resync').on('click', sessionResync);
$('#session-contents').on("click", '[action="delete"]', deleteSession);
$('#tracks').on('click', 'div[control="mute"]', toggleMute);
$('#recording-start-stop').on('click', startStopRecording)
$('.voicechat-settings').click(function() {
// call this to initialize Music Audio tab
@ -841,7 +1017,6 @@
context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback;
context.JK.HandleBridgeCallback = handleBridgeCallback;
context.JK.AlertCallback = alertCallback;
};
})(window,jQuery);

View File

@ -138,12 +138,12 @@
var $parentRow = $('tr[id=' + session.id + ']', tbGroup);
if (session.approval_required) {
$('#join-link', $parentRow).click(function(evt) {
$('.join-link', $parentRow).click(function(evt) {
openAlert(session.id);
});
}
else {
$('#join-link', $parentRow).click(function(evt) {
$('.join-link', $parentRow).click(function(evt) {
openTerms(session.id);
});
}

View File

@ -14,7 +14,9 @@
var subscribers = {};
var users = {}; // User info for session participants
var rest = context.JK.Rest();
var requestingSessionRefresh = false;
var pendingSessionRefresh = false;
var recordingModel = new context.JK.RecordingModel(app, this, rest, context.jamClient);
function id() {
return currentSession.id;
}
@ -63,6 +65,7 @@
context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join);
}
recordingModel.reset();
client.JoinSession({ sessionID: sessionId });
refreshCurrentSession();
server.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession);
@ -101,9 +104,10 @@
});
// 'unregister' for callbacks
//context.jamClient.RecordingRegisterCallbacks("", "", "", "", "");
context.jamClient.SessionRegisterCallback("");
context.jamClient.SessionSetAlertCallback("");
currentSession = null;
updateCurrentSession(null);
currentSessionId = null;
}
else {
@ -119,9 +123,7 @@
*/
function refreshCurrentSession() {
logger.debug("SessionModel.refreshCurrentSession()");
refreshCurrentSessionRest(function() {
refreshCurrentSessionParticipantsRest(sessionChanged);
});
refreshCurrentSessionRest(sessionChanged);
}
/**
@ -139,29 +141,50 @@
function sessionChanged() {
logger.debug("SessionModel.sessionChanged()");
for (var subscriberId in subscribers) {
subscribers[subscriberId]();
subscribers[subscriberId](currentSession);
}
}
function updateCurrentSession(sessionData) {
currentSession = sessionData;
}
/**
* Reload the session data from the REST server, calling
* the provided callback when complete.
*/
function refreshCurrentSessionRest(callback) {
var url = "/api/sessions/" + currentSessionId;
$.ajax({
type: "GET",
url: url,
async: false,
success: function(response) {
sendClientParticipantChanges(currentSession, response);
logger.debug("Current Session Refreshed:");
logger.debug(response);
currentSession = response;
callback();
},
error: ajaxError
});
if(requestingSessionRefresh) {
// if someone asks for a refresh while one is going on, we ask for another to queue up
pendingSessionRefresh = true;
}
else {
requestingSessionRefresh = true;
$.ajax({
type: "GET",
url: url,
async: false,
success: function(response) {
sendClientParticipantChanges(currentSession, response);
logger.debug("Current Session Refreshed:");
logger.debug(response);
updateCurrentSession(response);
if(callback != null) {
callback();
}
},
error: ajaxError,
complete: function() {
requestingSessionRefresh = false;
if(pendingSessionRefresh) {
// and when the request is done, if we have a pending, fire t off again
pendingSessionRefresh = false;
refreshCurrentSessionRest(null);
}
}
});
}
}
/**
@ -226,36 +249,6 @@
});
}
/**
* Ensure that we have user info for all current participants.
*/
function refreshCurrentSessionParticipantsRest(callback) {
var callCount = 0;
$.each(participants(), function(index, value) {
if (!(this.user.id in users)) {
var userInfoUrl = "/api/users/" + this.user.id;
callCount += 1;
$.ajax({
type: "GET",
url: userInfoUrl,
async: false,
success: function(user) {
callCount -= 1;
users[user.id] = user;
},
error: function(jqXHR, textStatus, errorThrown) {
callCount -= 1;
logger.error('Error getting user info from ' + userInfoUrl);
}
});
}
});
if (!(callback)) {
return;
}
context.JK.joinCalls(
function() { return callCount === 0; }, callback, 10);
}
function participantForClientId(clientId) {
var foundParticipant = null;
@ -428,6 +421,27 @@
logger.error("Unexpected ajax error: " + textStatus);
}
// returns a deferred object
function findUserBy(finder) {
if(finder.clientId) {
var foundParticipant = null;
$.each(participants(), function(index, participant) {
if(participant.client_id == finder.clientId) {
foundParticipant = participant;
return false;
}
});
if(foundParticipant) {
return $.Deferred().resolve(foundParticipant.user).promise();
}
}
// TODO: find it via some REST API if not found?
return $.Deferred().reject().promise();
}
// Public interface
this.id = id;
this.participants = participants;
@ -440,6 +454,8 @@
this.updateTrack = updateTrack;
this.deleteTrack = deleteTrack;
this.onWebsocketDisconnected = onWebsocketDisconnected;
this.recordingModel = recordingModel;
this.findUserBy = findUserBy;
this.getCurrentSession = function() {
return currentSession;
};

View File

@ -517,12 +517,21 @@
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, function(header, payload) {
logger.debug("Handling MUSICIAN_SESSION_DEPART msg " + JSON.stringify(payload));
// display notification
app.notify({
"title": "Musician Left Session",
"text": payload.username + " has left the session.",
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
if(payload.recordingId && context.JK.CurrentSessionModel.recordingModel.isRecording(payload.recordingId)) {
context.JK.CurrentSessionModel.recordingModel.onServerStopRecording(payload.recordingId);
/**app.notify({
"title": "Recording Stopped",
"text": payload.username + " has left the session.",
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}); */
}
else {
app.notify({
"title": "Musician Left Session",
"text": payload.username + " has left the session.",
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
}
});
}

View File

@ -16,6 +16,17 @@
}
};
// http://stackoverflow.com/a/8809472/834644
context.JK.generateUUID = function(){
var d = new Date().getTime();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c=='x' ? r : (r&0x7|0x8)).toString(16);
});
return uuid;
};
// Build up two maps of images, for each instrument id.
// This map is a simple base map of instrument id to the basic image name.
// Below, a loop goes through this and builds two size-specific maps.
@ -494,4 +505,18 @@
return rhex(a) + rhex(b) + rhex(c) + rhex(d);
};
})(window,jQuery);
/** validates that no changes are being made to tracks while recording */
context.JK.verifyNotRecordingForTrackChange = function(app) {
if(context.JK.CurrentSessionModel.recordingModel.isRecording()) {
app.notify({
title: "Currently Recording",
text: "Tracks can not be modified while recording.",
icon_url: "/assets/content/icon_alert_big.png"});
return false;
}
return true;
}
})(window,jQuery);

View File

@ -12,6 +12,7 @@ $ColorLinkHover: #82AEAF;
$ColorSidebarText: #a0b9bd;
$ColorScreenBackground: lighten($ColorUIBackground, 10%);
$ColorTextBoxBackground: #c5c5c5;
$ColorRecordingBackground: #471f18;
$color1: #006AB6; /* mid blue */
$color2: #9A9084; /* warm gray */

View File

@ -615,6 +615,14 @@ table.vu td {
font-size:18px;
}
.currently-recording {
background-color: $ColorRecordingBackground;
}
#recording-timer {
margin-left:8px;
}
/* GAIN SLIDER POSITIONS -- TEMPORARY FOR DISPLAY PURPOSES ONLY */
.pos0 {
bottom:0px;

View File

@ -1,7 +1,7 @@
class ApiRecordingsController < ApiController
before_filter :api_signed_in_user
before_filter :look_up_recording, :only => [ :stop, :claim ]
before_filter :look_up_recording, :only => [ :show, :stop, :claim, :keep ]
before_filter :parse_filename, :only => [ :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ]
respond_to :json
@ -15,37 +15,56 @@ class ApiRecordingsController < ApiController
end
end
def show
end
def start
begin
Recording.start(params[:music_session_id], current_user)
respond_with responder: ApiResponder, :status => 204
rescue
render :json => { :message => "recording could not be started" }, :status => 403
music_session = MusicSession.find(params[:music_session_id])
unless music_session.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@recording = Recording.start(music_session, current_user)
if @recording.errors.any?
response.status = :unprocessable_entity
respond_with @recording
else
respond_with @recording, responder: ApiResponder, :location => api_recordings_detail_url(@recording)
end
end
def stop
begin
if @recording.owner_id != current_user.id
render :json => { :message => "recording not found" }, :status => 404
end
@recording.stop
respond_with responder: ApiResponder, :status => 204
rescue
render :json => { :message => "recording could not be stopped" }, :status => 403
end
end
def claim
begin
claimed_recording = @recording.claim(current_user, params[:name], Genre.find(params[:genre_id]), params[:is_public], params[:is_downloadable])
render :json => { :claimed_recording_id => claimed_recording.id }, :status => 200
rescue
render :json => { :message => "recording could not be claimed" }, :status => 403
unless @recording.users.exists?(current_user)
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
@recording.stop
if @recording.errors.any?
response.status = :unprocessable_entity
respond_with @recording
else
respond_with @recording, responder: ApiResponder, :location => api_recordings_detail_url(@recording)
end
end
# keep will kick off a mix, as well as create a claimed recording for the creator
def claim
claim = @recording.claim(current_user, params[:name], Genre.find(params[:genre_id]), params[:is_public], params[:is_downloadable])
claim.save
if claim.errors.any?
response.status = :unprocessable_entity
respond_with claim
else
respond_with claim, responder: ApiResponder, :location => api_session_detail_url(claim)
end
end
def upload_next_part
if @recorded_track.next_part_to_upload == 0
if (!params[:length] || !params[:md5])
@ -85,10 +104,7 @@ class ApiRecordingsController < ApiController
end
def look_up_recording
@recording = Recording.find(params[id])
if @recording.nil?
render :json => { :message => "recording not found" }, :status => 404
end
@recording = Recording.find(params[:id])
end
end

View File

@ -23,7 +23,7 @@ child(:recorded_tracks => :recorded_tracks) {
attributes :id, :description
}
child(:user => :user) {
attributes :id, :email, :first_name, :last_name, :city, :state, :country, :photo_url
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
}
}

View File

@ -6,6 +6,12 @@ node :genres do |item|
item.genres.map(&:description)
end
if :is_recording?
node do |music_session|
{ :recording => partial("api_recordings/show", :object => music_session.recording) }
end
end
child(:connections => :participants) {
collection @music_sessions, :object_root => false
attributes :ip_address, :client_id

View File

@ -0,0 +1,14 @@
object @recording
attributes :id, :band, :created_at, :duration
child(:recorded_tracks => :recorded_tracks) {
attributes :id, :client_id, :track_id, :user_id, :fully_uploaded, :url
child(:instrument => :instrument) {
attributes :id, :description
}
child(:user => :user) {
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
}
}

View File

@ -0,0 +1,3 @@
object @recording
extends "api_recordings/show"

View File

@ -0,0 +1,3 @@
object @recording
extends "api_recordings/show"

View File

@ -72,7 +72,7 @@
</a>
</td>
<td class="noborder" style="text-align:center; vertical-align:middle;">
<a id="join-link" style="display:{join_link_display_style};">
<a class="join-link" style="display:{join_link_display_style};">
<%= image_tag "content/icon_join.png", :size => "19x22" %>
</a>
</td>

View File

@ -0,0 +1,14 @@
<!-- Invitation Dialog -->
<div class="dialog recordingFinished-overlay ftue-overlay tall" layout="dialog" layout-id="recordingFinished" id="recording-finished-dialog">
<div class="content-head">
<%= image_tag "content/recordbutton-off.png", {:height => 20, :width => 20, :class => 'content-icon'} %>
<h1>Recording Finished</h1>
</div>
<div class="dialog-inner">
Fill out the fields below and click the "SAVE" button to save this recording to your library. If you do not want to keep the recording, click the "DISCARD" button.
</div>
<a href="#" class="button-grey" layout-action="close">DISCARD</a><br>
</div>

View File

@ -88,9 +88,9 @@
</p>
</div>
<br clear="all" />
<div class="recording">
<div class="recording" id="recording-start-stop">
<a>
<%= image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"} %>&nbsp;&nbsp;Make a Recording
<%= image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"} %>&nbsp;&nbsp;<span id="recording-status">Make a Recording</span>
</a>
</div>
</div>

View File

@ -31,6 +31,7 @@
<%= render "account_audio_profile" %>
<%= render "invitationDialog" %>
<%= render "whatsNextDialog" %>
<%= render "recordingFinishedDialog" %>
<%= render "notify" %>
<%= render "client_update" %>
<%= render "banner" %>
@ -52,11 +53,7 @@
<% end %>
if (console) { console.debug("websocket_gateway_uri:" + JK.websocket_gateway_uri); }
// If no jamClient (when not running in native client)
// create a fake one.
if (!(window.jamClient)) {
window.jamClient = new JK.FakeJamClient();
}
// If no trackVolumeObject (when not running in native client)
// create a fake one.
if (!(window.trackVolumeObject)) {
@ -163,11 +160,19 @@
JK.hideCurtain(300);
}
JK.app = JK.JamKazam();
// If no jamClient (when not running in native client)
// create a fake one.
if (!(window.jamClient)) {
var p2pMessageFactory = new JK.FakeJamClientMessages();
window.jamClient = new JK.FakeJamClient(JK.app, p2pMessageFactory);
window.jamClient.SetFakeRecordingImpl(new JK.FakeJamClientRecordings(JK.app, jamClient, p2pMessageFactory));
}
// Let's get things rolling...
if (JK.currentUserId) {
JK.app = JK.JamKazam();
// do a client update early check upon initialization
var clientUpdate = new JK.ClientUpdate(JK.app)
clientUpdate.initialize().check()

View File

@ -87,7 +87,7 @@ if defined?(Bundler)
# filepicker app configured to use S3 bucket jamkazam-dev
config.filepicker_rails.api_key = "Asx4wh6GSlmpAAzoM0Cunz"
config.filepicker_upload_dir = 'avatars'
config.fp_secret = 'YSES4ABIMJCWDFSLCFJUGEBKSE'
config.fp_secret = 'FTDL4TYDENBWZKK3UZCFIQWXS4'
config.recaptcha_enable = false

View File

@ -258,10 +258,12 @@ SampleApp::Application.routes.draw do
match '/isps' => 'api_maxmind_requests#isps', :via => :get
# Recordings
match '/recordings/list' => 'api_recordings#list', :via => :get
match '/recordings/start' => 'api_recordings#start', :via => :post
match '/recordings/:id/stop' => 'api_recordings#stop', :via => :put
match '/recordings/:id/claim' => 'api_recordings#claim', :via => :post
match '/recordings/list' => 'api_recordings#list', :via => :get, :as => 'api_recordings_list'
match '/recordings/start' => 'api_recordings#start', :via => :post, :as => 'api_recordings_start'
match '/recordings/:id' => 'api_recordings#show', :via => :get, :as => 'api_recordings_detail'
match '/recordings/:id/stop' => 'api_recordings#stop', :via => :post, :as => 'api_recordings_stop'
match '/recordings/:id/claim' => 'api_recordings#claim', :via => :post, :as => 'api_recordings_claim'
match '/recordings/upload_next_part' => 'api_recordings#upload_next_part', :via => :get
match '/recordings/upload_sign' => 'api_recordings#upload_sign', :via => :get
match '/recordings/upload_part_complete' => 'api_recordings#upload_part_complete', :via => :put

View File

@ -125,13 +125,9 @@ MusicSessionManager < BaseManager
end
ConnectionManager.new.leave_music_session(user, connection, music_session) do
Notification.send_musician_session_depart(music_session, connection.client_id, user)
end
unless music_session.nil?
# send out notification to queue to the rest of the session
# TODO: we should rename the notification to music_session_participants_change or something
# TODO: also this isn't necessarily a user leaving; it's a client leaving
recording = music_session.stop_recording # stop any ongoing recording, if there is one
recordingId = recording.id unless recording.nil?
Notification.send_musician_session_depart(music_session, connection.client_id, user, recordingId)
end
end
end

View File

@ -12,11 +12,11 @@ describe ApiClaimedRecordingsController 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
@recording.reload
@genre = FactoryGirl.create(:genre)
@recording.claim(@user, "name", @genre, true, true)
@recording.claim(@user, "name", "description", @genre, true, true)
@recording.reload
@claimed_recording = @recording.claimed_recordings.first
end
@ -26,7 +26,6 @@ describe ApiClaimedRecordingsController do
it "should show the right thing when one recording just finished" do
controller.current_user = @user
get :show, :id => @claimed_recording.id
# puts response.body
response.should be_success
json = JSON.parse(response.body)
json.should_not be_nil

View File

@ -0,0 +1,89 @@
require 'spec_helper'
describe ApiRecordingsController do
render_views
before(:each) do
@user = FactoryGirl.create(:user)
@instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
@music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
@connection = FactoryGirl.create(:connection, :user => @user, :music_session => @music_session)
@track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument)
controller.current_user = @user
end
describe "start" do
it "should work" do
post :start, { :format => 'json', :music_session_id => @music_session.id }
response.should be_success
response_body = JSON.parse(response.body)
response_body['id'].should_not be_nil
recording = Recording.find(response_body['id'])
end
it "should not allow multiple starts" do
post :start, { :format => 'json', :music_session_id => @music_session.id }
post :start, { :format => 'json', :music_session_id => @music_session.id }
response.status.should == 422
response_body = JSON.parse(response.body)
response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_BEING_RECORDED
end
it "should not allow start by somebody not in the music session" do
user2 = FactoryGirl.create(:user)
controller.current_user = user2
post :start, { :format => 'json', :music_session_id => @music_session.id }
response.status.should == 403
end
end
describe "get" do
it "should work" do
post :start, { :format => 'json', :music_session_id => @music_session.id }
response.should be_success
response_body = JSON.parse(response.body)
response_body['id'].should_not be_nil
recordingId = response_body['id']
get :show, {:format => 'json', :id => recordingId}
response.should be_success
response_body = JSON.parse(response.body)
response_body['id'].should == recordingId
end
end
describe "stop" do
it "should work" do
post :start, { :format => 'json', :music_session_id => @music_session.id }
response_body = JSON.parse(response.body)
recording = Recording.find(response_body['id'])
post :stop, { :format => 'json', :id => recording.id }
response.should be_success
response_body = JSON.parse(response.body)
response_body['id'].should_not be_nil
Recording.find(response_body['id']).id.should == recording.id
end
it "should not allow stop on a session not being recorded" do
post :start, { :format => 'json', :music_session_id => @music_session.id }
response_body = JSON.parse(response.body)
recording = Recording.find(response_body['id'])
post :stop, { :format => 'json', :id => recording.id }
post :stop, { :format => 'json', :id => recording.id }
response.status.should == 422
response_body = JSON.parse(response.body)
end
it "should not allow stop on a session requested by a different member" do
post :start, { :format => 'json', :music_session_id => @music_session.id }
response_body = JSON.parse(response.body)
recording = Recording.find(response_body['id'])
user2 = FactoryGirl.create(:user)
controller.current_user = user2
post :stop, { :format => 'json', :id => recording.id }
response.status.should == 403
end
end
end

View File

@ -0,0 +1,230 @@
require 'spec_helper'
describe "Find Session", :js => true, :type => :feature, :capybara_feature => true, :slow => true do
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 30 # these tests are SLOOOOOW
end
let(:creator) { FactoryGirl.create(:user) }
let(:joiner1) { FactoryGirl.create(:user) }
before(:each) do
MusicSession.delete_all
end
# creates a recording, and stops it, and confirms the 'Finished Recording' dialog shows for both
it "creator start/stop" do
create_join_session(creator, [joiner1])
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner1) do
find('#notification').should have_content 'started a recording'
find('#recording-status').should have_content 'Stop Recording'
end
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
in_client(joiner1) do
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
end
# confirms that anyone can start/stop a recording
it "creator starts and other stops" do
create_join_session(creator, [joiner1])
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner1) do
find('#notification').should have_content 'started a recording'
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner1) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
in_client(creator) do
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
end
# confirms that a formal leave (by hitting the 'Leave' button) will result in a good recording
it "creator starts and then leaves" do
create_join_session(creator, [joiner1])
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner1) do
find('#notification').should have_content 'started a recording'
find('#recording-status').should have_content 'Stop Recording'
end
in_client(creator) do
find('#session-leave').trigger(:click)
expect(page).to have_selector('h2', text: 'feed')
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
in_client(joiner1) do
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
end
# confirms that if someone leaves 'ugly' (without calling 'Leave' REST API), that the recording is junked
it "creator starts and then abruptly leave" do
create_join_session(creator, [joiner1])
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner1) do
find('#notification').should have_content 'started a recording'
find('#recording-status').should have_content 'Stop Recording'
end
in_client(creator) do
visit "http://www.google.com" # kills websocket, looking like an abrupt leave
end
in_client(joiner1) do
find('#notification').should have_content 'Recording Deleted'
find('#notification').should have_content 'someone in the session has disconnected'
find('#recording-status').should have_content 'Make a Recording'
end
end
it "creator starts/stops, with 3 total participants" do
joiner2 = FactoryGirl.create(:user)
create_join_session(creator, [joiner1, joiner2])
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner1) do
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner2) do
find('#recording-status').should have_content 'Stop Recording'
end
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
in_client(joiner1) do
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
in_client(joiner2) do
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
end
it "creator starts with session leave to stop, with 3 total participants" do
joiner2 = FactoryGirl.create(:user)
create_join_session(creator, [joiner1, joiner2])
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner1) do
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner2) do
find('#recording-status').should have_content 'Stop Recording'
end
in_client(creator) do
find('#session-leave').trigger(:click)
expect(page).to have_selector('h2', text: 'feed')
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
in_client(joiner1) do
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
in_client(joiner2) do
find('#recording-status').should have_content 'Make a Recording'
should have_selector('h1', 'Recording Finished')
end
end
# confirms that if someone leaves 'ugly' (without calling 'Leave' REST API), that the recording is junked with 3 participants
it "creator starts and then abruptly leave with 3 participants" do
joiner2 = FactoryGirl.create(:user)
create_join_session(creator, [joiner1, joiner2])
in_client(creator) do
find('#recording-start-stop').trigger(:click)
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner1) do
find('#recording-status').should have_content 'Stop Recording'
end
in_client(joiner2) do
find('#recording-status').should have_content 'Stop Recording'
end
in_client(creator) do
visit "http://www.google.com" # kills websocket, looking like an abrupt leave
end
in_client(joiner1) do
find('#notification').should have_content 'Recording Deleted'
find('#notification').should have_content 'someone in the session has disconnected'
find('#recording-status').should have_content 'Make a Recording'
end
in_client(joiner2) do
find('#notification').should have_content 'Recording Deleted'
find('#notification').should have_content 'someone in the session has disconnected'
find('#recording-status').should have_content 'Make a Recording'
end
end
end

View File

@ -1,4 +1,4 @@
(function(context, $) {
v(function(context, $) {
describe("Callbacks", function() {
describe("makeStatic", function() {
it("should create static function which invokes instance function", function() {

View File

@ -4,8 +4,8 @@
describe("faderHelpers tests", function() {
beforeEach(function() {
JKTestUtils.loadFixtures('/base/app/views/clients/_faders.html.erb');
JKTestUtils.loadFixtures('/base/spec/javascripts/fixtures/faders.htm');
JKTestUtils.loadFixtures('/app/views/clients/_faders.html.erb');
JKTestUtils.loadFixtures('/spec/javascripts/fixtures/faders.htm');
});
describe("renderVU", function() {

View File

@ -21,7 +21,7 @@
beforeEach(function() {
fss = null;
// Use the actual screen markup
JKTestUtils.loadFixtures('/base/app/views/clients/_findSession.html.erb');
JKTestUtils.loadFixtures('/app/views/clients/_findSession.html.erb');
spyOn(appFake, 'notify');
});

View File

@ -3,7 +3,7 @@
describe("jquery.formToObject tests", function() {
beforeEach(function() {
JKTestUtils.loadFixtures('/base/spec/javascripts/fixtures/formToObject.htm');
JKTestUtils.loadFixtures('/spec/javascripts/fixtures/formToObject.htm');
});
describe("Top level", function() {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
(function(context, $) {
describe("RecordingModel", function() {
var recordingModel = null;
var sessionModel = null;
var app = null;
var rest = null;
var jamClient = null;
var validRecordingData = null;
beforeEach(function() {
app = { };
sessionModel = { id: null };
rest = { startRecording: null, stopRecording: null};
jamClient = { StartRecording: null, StopRecording: null};
recordingModel = new context.JK.RecordingModel(app, sessionModel, rest, jamClient);
validRecordingData = {
id: '1',
recorded_tracks: [
{ id: '1', track_id: '1', user_id: '1', 'client_id':'1' }
]
}
});
it("constructs", function() {
});
it("allows start recording", function() {
spyOn(sessionModel, 'id').andReturn('1');
spyOn(rest, 'startRecording').andCallFake(function (req) {
return $.Deferred().resolve(validRecordingData).promise();
});
spyOn(jamClient, 'StartRecording').andCallFake(function(recordingId, tracks) {
eval(context.JK.HandleRecordingStartResult).call(this, recordingId, true);
});
spyOnEvent($(recordingModel), 'startingRecording');
spyOnEvent($(recordingModel), 'startedRecording');
expect(recordingModel.startRecording()).toBe(true);
expect('startingRecording').toHaveBeenTriggeredOn($(recordingModel));
expect('startedRecording').toHaveBeenTriggeredOn($(recordingModel));
});
it("allows stop recording", function() {
spyOn(sessionModel, 'id').andReturn('1');
spyOn(rest, 'startRecording').andCallFake(function (req) {
return $.Deferred().resolve(validRecordingData).promise();
});
spyOn(rest, 'stopRecording').andCallFake(function (req) {
return $.Deferred().resolve(validRecordingData).promise();
});
spyOn(jamClient, 'StartRecording').andCallFake(function(recordingId, tracks) {
eval(context.JK.HandleRecordingStartResult).call(this, recordingId, true);
});
spyOn(jamClient, 'StopRecording').andCallFake(function(recordingId, tracks) {
eval(context.JK.HandleRecordingStopResult).call(this, recordingId, true);
});
spyOnEvent($(recordingModel), 'stoppingRecording');
spyOnEvent($(recordingModel), 'stoppedRecording');
expect(recordingModel.startRecording()).toBe(true);
expect(recordingModel.stopRecording()).toBe(true);
expect('stoppingRecording').toHaveBeenTriggeredOn($(recordingModel));
expect('stoppedRecording').toHaveBeenTriggeredOn($(recordingModel));
});
});
}(window, jQuery));

View File

@ -3,7 +3,7 @@
describe("searcher.js tests", function() {
beforeEach(function() {
JKTestUtils.loadFixtures('/base/spec/javascripts/fixtures/searcher.htm');
JKTestUtils.loadFixtures('/spec/javascripts/fixtures/searcher.htm');
});
describe("Empty Search", function() {

View File

@ -4,8 +4,8 @@
describe("vuHelper tests", function() {
beforeEach(function() {
JKTestUtils.loadFixtures('/base/app/views/clients/_vu_meters.html.erb');
JKTestUtils.loadFixtures('/base/spec/javascripts/fixtures/vuHelpers.htm');
JKTestUtils.loadFixtures('/app/views/clients/_vu_meters.html.erb');
JKTestUtils.loadFixtures('/spec/javascripts/fixtures/vuHelpers.htm');
});
describe("renderVU", function() {

View File

@ -301,7 +301,7 @@ describe "Music Session API ", :type => :api do
client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1")
post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => nil}).to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
JSON.parse(last_response.body)["errors"]["genres"][0].should == Connection::SELECT_AT_LEAST_ONE
JSON.parse(last_response.body)["errors"]["genres"][0].should == ValidationMessages::SELECT_AT_LEAST_ONE
# check that the transaction was rolled back
MusicSession.all().length.should == original_count
@ -414,7 +414,7 @@ describe "Music Session API ", :type => :api do
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'
last_response.status.should eql(422)
join_response = JSON.parse(last_response.body)
join_response["errors"]["musician_access"].should == [Connection::INVITE_REQUIRED]
join_response["errors"]["musician_access"].should == [ValidationMessages::INVITE_REQUIRED]
# but let's make sure if we then invite, that we can then join'
login(user)
@ -495,7 +495,7 @@ describe "Music Session API ", :type => :api do
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'
last_response.status.should eql(422)
rejected_join_attempt = JSON.parse(last_response.body)
rejected_join_attempt["errors"]["approval_required"] = [Connection::INVITE_REQUIRED]
rejected_join_attempt["errors"]["approval_required"] = [ValidationMessages::INVITE_REQUIRED]
# now send up a join_request to try and get in
login(user2)
@ -558,6 +558,33 @@ describe "Music Session API ", :type => :api do
track["instrument_id"].should == "electric guitar"
track["sound"].should == "mono"
end
it "can't join session that's recording" do
user = FactoryGirl.create(:user)
user2 = FactoryGirl.create(:user)
client = FactoryGirl.create(:connection, :user => user)
client2 = FactoryGirl.create(:connection, :user => user2)
instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
track = FactoryGirl.create(:track, :connection => client, :instrument => instrument)
track2 = FactoryGirl.create(:track, :connection => client2, :instrument => instrument)
# 1st user joins
login(user)
post '/api/sessions.json', defopts.merge({:client_id => client.client_id}).to_json, "CONTENT_TYPE" => 'application/json'
location_header = last_response.headers["Location"]
get location_header
music_session = JSON.parse(last_response.body)
# start a recording
post "/api/recordings/start", {:format => :json, :music_session_id => music_session['id'] }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
# 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'
last_response.status.should eql(422)
JSON.parse(last_response.body)["errors"]["music_session"][0].should == ValidationMessages::CANT_JOIN_RECORDING_SESSION
end
end
it "Finds a single open session" do
@ -607,7 +634,6 @@ describe "Music Session API ", :type => :api do
last_response.status.should == 200
msuh.reload
msuh.rating.should == 0
end
end

View File

@ -114,6 +114,12 @@ Spork.prefork do
if example.metadata[:js]
#sleep (ENV['SLEEP_JS'] || 0.2).to_i # necessary though otherwise intermittent failures: http://stackoverflow.com/questions/14265983/upgrading-capybara-from-1-0-1-to-1-1-4-makes-database-cleaner-break-my-specs
end
# dump response.body if an example fails
if example.metadata[:type] == :controller && example.exception
puts "'#{determine_test_name(example.metadata)}' controller test failed."
puts "response.status = #{response.status}, response.body = " + response.body
end
end
end
end

View File

@ -6,7 +6,7 @@ end
def in_client(name) # to assist multiple-client RSpec/Capybara testing
Capybara.session_name = name
Capybara.session_name = name.class == JamRuby::User ? name.id : name
yield
end
@ -70,4 +70,56 @@ end
def wait_until_curtain_gone
should have_no_selector('.curtain')
end
def determine_test_name(metadata, test_name_buffer = '')
description = metadata[:description_args]
if description.kind_of?(Array)
description = description[0]
end
if metadata.has_key? :example_group
return determine_test_name(metadata[:example_group], "#{description} #{test_name_buffer}")
else
return "#{description} #{test_name_buffer}"
end
end
def create_join_session(creator, joiners=[])
unique_session_desc = "create_join_session #{SecureRandom.urlsafe_base64}"
# create session in one client
in_client(creator) do
page.driver.resize(1500, 600) # crude hack
sign_in_poltergeist creator
wait_until_curtain_gone
visit "/client#/createSession"
expect(page).to have_selector('h2', text: 'session info')
within('#create-session-form') do
fill_in('description', :with => unique_session_desc)
select('Rock', :from => 'genres')
find('div.intellectual-property ins').trigger(:click)
find('#btn-create-session').trigger(:click) # fails if page width is low
end
# verify that the in-session page is showing
expect(page).to have_selector('h2', text: 'my tracks')
end
# find session in second client
joiners.each do |joiner|
in_client(joiner) do
sign_in_poltergeist joiner
wait_until_curtain_gone
visit "/client#/findSession"
# verify the session description is seen by second client
expect(page).to have_text(unique_session_desc)
find('.join-link').trigger(:click)
find('#btn-accept-terms').trigger(:click)
expect(page).to have_selector('h2', text: 'my tracks')
end
end
end

View File

@ -342,7 +342,9 @@ module JamWebsockets
Notification.send_friend_update(user_id, false, conn) if count == 0
music_session = MusicSession.find_by_id(music_session_id) unless music_session_id.nil?
user = User.find_by_id(user_id) unless user_id.nil?
Notification.send_musician_session_depart(music_session, cid, user) unless music_session.nil? || user.nil?
recording = music_session.stop_recording unless music_session.nil? # stop any ongoing recording, if there is one
recordingId = recording.id unless recording.nil?
Notification.send_musician_session_depart(music_session, cid, user, recordingId) unless music_session.nil? || user.nil?
}
end
end
@ -459,7 +461,11 @@ module JamWebsockets
if music_session_id.nil?
# if this is a reclaim of a connection, but music_session_id comes back null, then we need to check if this connection was IN a music session before.
# if so, then we need to tell the others in the session that this user is now departed
Notification.send_musician_session_depart(music_session_upon_reentry, client.client_id, context.user) unless context.nil? || music_session_upon_reentry.nil? || music_session_upon_reentry.destroyed?
unless context.nil? || music_session_upon_reentry.nil? || music_session_upon_reentry.destroyed?
recording = music_session_upon_reentry.stop_recording
recordingId = recording.id unless recording.nil?
Notification.send_musician_session_depart(music_session_upon_reentry, client.client_id, context.user, recordingId)
end
else
music_session = MusicSession.find_by_id(music_session_id)
Notification.send_musician_session_fresh(music_session, client.client_id, context.user) unless context.nil?
@ -515,25 +521,6 @@ module JamWebsockets
end
end
# TODO: deprecated; jam_ruby has routine inspired by this
def send_friend_update(user, online, client)
@log.debug "sending friend update for user #{user} online = #{online}"
if !user.nil? && user.friends.exists?
@log.debug "user has friends - sending friend updates"
# create the friend_update message
friend_update_msg = @message_factory.friend_update(user.id, online)
# send the friend_update to each friend that has active connections
user.friends.each do |friend|
@log.debug "sending friend update message to #{friend}"
handle_user_directed(friend.id, friend_update_msg, client)
end
end
end
def handle_heartbeat(heartbeat, heartbeat_message_id, client)
unless context = @clients[client]
@log.warn "*** WARNING: unable to find context due to heartbeat from client: #{client.client_id}; calling cleanup"