diff --git a/db/manifest b/db/manifest
index 94d24481b..986692c75 100755
--- a/db/manifest
+++ b/db/manifest
@@ -76,3 +76,4 @@ user_progress_tracking.sql
whats_next.sql
add_user_bio.sql
users_geocoding.sql
+recordings_public_launch.sql
diff --git a/db/up/recordings_public_launch.sql b/db/up/recordings_public_launch.sql
new file mode 100644
index 000000000..be7323e75
--- /dev/null
+++ b/db/up/recordings_public_launch.sql
@@ -0,0 +1,27 @@
+-- so that rows can live on after session is over
+ALTER TABLE recordings DROP CONSTRAINT "recordings_music_session_id_fkey";
+-- unambiguous declartion that the recording is over or not
+ALTER TABLE recordings ADD COLUMN is_done BOOLEAN DEFAULT FALSE;
+
+-- add name and description on claimed_recordings, which is the user's individual view of a recording
+ALTER TABLE claimed_recordings ADD COLUMN description VARCHAR(8000);
+ALTER TABLE claimed_recordings ADD COLUMN description_tsv tsvector;
+ALTER TABLE claimed_recordings ADD COLUMN name_tsv tsvector;
+
+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);
+
+-- copies of connection.client_id and track.id
+ALTER TABLE recorded_tracks ADD COLUMN client_id VARCHAR(64) NOT NULL;
+ALTER TABLE recorded_tracks ADD COLUMN track_id VARCHAR(64) NOT NULL;
+
+-- so that server can correlate to client track
+ALTER TABLE tracks ADD COLUMN client_track_id VARCHAR(64) NOT NULL;
diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto
index 6b4f6054b..ebe125766 100644
--- a/pb/src/client_container.proto
+++ b/pb/src/client_container.proto
@@ -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:
diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb
index e79ee8b29..869906da9 100644
--- a/ruby/lib/jam_ruby/connection_manager.rb
+++ b/ruby/lib/jam_ruby/connection_manager.rb
@@ -362,6 +362,7 @@ SQL
t.instrument = instrument
t.connection = connection
t.sound = track["sound"]
+ t.client_track_id = track["client_track_id"]
t.save
connection.tracks << t
end
diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb
index 8f7e8b514..7322fd62a 100644
--- a/ruby/lib/jam_ruby/constants/validation_messages.rb
+++ b/ruby/lib/jam_ruby/constants/validation_messages.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/lib/audiomixer.rb b/ruby/lib/jam_ruby/lib/audiomixer.rb
new file mode 100644
index 000000000..2efc0c955
--- /dev/null
+++ b/ruby/lib/jam_ruby/lib/audiomixer.rb
@@ -0,0 +1,21 @@
+require 'json'
+require 'resque'
+
+module JamRuby
+
+ @queue = :audiomixer
+
+ class AudioMixer
+
+ def self.perform(manifest)
+ tmp = Dir::Tmpname.make_tmpname "/var/tmp/audiomixer/manifest-#{manifest['recordingId']}", nil
+ File.open(tmp,"w") do |f|
+ f.write(manifest.to_json)
+ end
+
+ system("tar zxvf some_big_tarball.tar.gz"))
+ end
+
+ end
+
+end
\ No newline at end of file
diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb
index 8167b71de..c21df795e 100644
--- a/ruby/lib/jam_ruby/message_factory.rb
+++ b/ruby/lib/jam_ruby/message_factory.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb
index 5e8bb963d..aa8d0764d 100644
--- a/ruby/lib/jam_ruby/models/claimed_recording.rb
+++ b/ruby/lib/jam_ruby/models/claimed_recording.rb
@@ -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?
diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb
index 62935f4e3..cbc4a9bc2 100644
--- a/ruby/lib/jam_ruby/models/connection.rb
+++ b/ruby/lib/jam_ruby/models/connection.rb
@@ -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
@@ -72,37 +67,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, ValidationMessages::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
@@ -120,7 +118,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
diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb
index c61246b53..ddb3be628 100644
--- a/ruby/lib/jam_ruby/models/music_session.rb
+++ b/ruby/lib/jam_ruby/models/music_session.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb
index 227fab529..4f7a8a284 100644
--- a/ruby/lib/jam_ruby/models/notification.rb
+++ b/ruby/lib/jam_ruby/models/notification.rb
@@ -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})
diff --git a/ruby/lib/jam_ruby/models/recorded_track.rb b/ruby/lib/jam_ruby/models/recorded_track.rb
index c53d02f62..4a2b0813d 100644
--- a/ruby/lib/jam_ruby/models/recorded_track.rb
+++ b/ruby/lib/jam_ruby/models/recorded_track.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/recording.rb b/ruby/lib/jam_ruby/models/recording.rb
index d9e2a90ed..87740e05d 100644
--- a/ruby/lib/jam_ruby/models/recording.rb
+++ b/ruby/lib/jam_ruby/models/recording.rb
@@ -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
diff --git a/ruby/lib/jam_ruby/models/track.rb b/ruby/lib/jam_ruby/models/track.rb
index 7ae02b24c..76b428b09 100644
--- a/ruby/lib/jam_ruby/models/track.rb
+++ b/ruby/lib/jam_ruby/models/track.rb
@@ -38,7 +38,68 @@ module JamRuby
return query
end
- def self.save(id, connection_id, instrument_id, sound)
+
+ # this is a bit different from a normal track synchronization in that the client just sends up all tracks,
+ # ... some may already exist
+ def self.sync(clientId, tracks)
+ result = []
+
+ Track.transaction do
+ connection = Connection.find_by_client_id!(clientId)
+
+ if tracks.length == 0
+ connection.tracks.delete_all
+ else
+ connection_tracks = connection.tracks
+
+ # we will prune from this as we find matching tracks
+ to_delete = Set.new(connection_tracks)
+ to_add = Array.new(tracks)
+
+ connection_tracks.each do |connection_track|
+ tracks.each do |track|
+ if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id;
+ to_delete.delete(connection_track)
+ to_add.delete(track)
+ # don't update connection_id or client_id; it's unknown what would happen if these changed mid-session
+ connection_track.instrument = Instrument.find(track[:instrument_id])
+ connection_track.sound = track[:sound]
+ connection_track.client_track_id = track[:client_track_id]
+ if connection_track.save
+ result.push(connection_track)
+ next
+ else
+ result = connection_track
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+ end
+
+ to_add.each do |track|
+ connection_track = Track.new
+ connection_track.connection = connection
+ connection_track.instrument = Instrument.find(track[:instrument_id])
+ connection_track.sound = track[:sound]
+ connection_track.client_track_id = track[:client_track_id]
+ if connection_track.save
+ result.push(connection_track)
+ else
+ result = connection_track
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ to_delete.each do| delete_me |
+ delete_me.delete
+ end
+ end
+ end
+
+ result
+ end
+
+ def self.save(id, connection_id, instrument_id, sound, client_track_id)
if id.nil?
track = Track.new()
track.connection_id = connection_id
@@ -54,6 +115,10 @@ module JamRuby
track.sound = sound
end
+ unless client_track_id.nil?
+ track.client_track_id = client_track_id
+ end
+
track.updated_at = Time.now.getutc
track.save
return track
diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb
index 5ba60dc30..d6364b741 100644
--- a/ruby/lib/jam_ruby/models/user.rb
+++ b/ruby/lib/jam_ruby/models/user.rb
@@ -118,7 +118,7 @@ module JamRuby
validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
- validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
+ validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email
validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password?
diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb
index 5de772b2b..dbb845cc5 100644
--- a/ruby/spec/factories.rb
+++ b/ruby/spec/factories.rb
@@ -90,7 +90,7 @@ FactoryGirl.define do
factory :track, :class => JamRuby::Track do
sound "mono"
-
+ sequence(:client_track_id) { |n| "client_track_id#{n}"}
end
factory :recorded_track, :class => JamRuby::RecordedTrack do
diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb
index aacd9f6c9..392807675 100644
--- a/ruby/spec/jam_ruby/connection_manager_spec.rb
+++ b/ruby/spec/jam_ruby/connection_manager_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests
describe ConnectionManager do
- TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono"}]
+ TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "some_client_track_id"}]
before do
@conn = PG::Connection.new(:dbname => SpecDb::TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost")
@@ -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
diff --git a/ruby/spec/jam_ruby/models/mix_spec.rb b/ruby/spec/jam_ruby/models/mix_spec.rb
index 91f3d0463..3ded0054a 100755
--- a/ruby/spec/jam_ruby/models/mix_spec.rb
+++ b/ruby/spec/jam_ruby/models/mix_spec.rb
@@ -9,7 +9,7 @@ describe Mix do
@music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
@music_session.connections << @connection
@music_session.save
- @recording = Recording.start(@music_session.id, @user)
+ @recording = Recording.start(@music_session, @user)
@recording.stop
@mix = Mix.schedule(@recording, "{}")
end
diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb
index 358cdbb09..a8c671208 100644
--- a/ruby/spec/jam_ruby/models/music_session_spec.rb
+++ b/ruby/spec/jam_ruby/models/music_session_spec.rb
@@ -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
diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb
index a93fb1ea8..5ce018da5 100644
--- a/ruby/spec/jam_ruby/models/musician_search_spec.rb
+++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb
@@ -79,11 +79,11 @@ describe User do
music_session = FactoryGirl.create(:music_session, :creator => uu, :musician_access => true)
music_session.connections << connection
music_session.save
- recording = Recording.start(music_session.id, uu)
+ recording = Recording.start(music_session, uu)
recording.stop
recording.reload
genre = FactoryGirl.create(:genre)
- recording.claim(uu, "name", genre, true, true)
+ recording.claim(uu, "name", "description", genre, true, true)
recording.reload
recording
end
diff --git a/ruby/spec/jam_ruby/models/recorded_track_spec.rb b/ruby/spec/jam_ruby/models/recorded_track_spec.rb
index dcd374aaf..7f32a519e 100644
--- a/ruby/spec/jam_ruby/models/recorded_track_spec.rb
+++ b/ruby/spec/jam_ruby/models/recorded_track_spec.rb
@@ -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
diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb
index ed1ce9a4e..e3565f712 100644
--- a/ruby/spec/jam_ruby/models/recording_spec.rb
+++ b/ruby/spec/jam_ruby/models/recording_spec.rb
@@ -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
diff --git a/ruby/spec/jam_ruby/models/track_spec.rb b/ruby/spec/jam_ruby/models/track_spec.rb
new file mode 100644
index 000000000..55bf5fd0a
--- /dev/null
+++ b/ruby/spec/jam_ruby/models/track_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Track do
+
+ let (:connection) { FactoryGirl.create(:connection) }
+ let (:track) { FactoryGirl.create(:track, :connection => connection)}
+ let (:track2) { FactoryGirl.create(:track, :connection => connection)}
+
+ let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} }
+
+ before(:each) do
+
+ end
+
+ describe "sync" do
+ it "create one track" do
+ tracks = Track.sync(connection.client_id, [track_hash])
+ tracks.length.should == 1
+ track = tracks[0]
+ track.client_track_id.should == track_hash[:client_track_id]
+ track.sound = track_hash[:sound]
+ track.instrument.should == Instrument.find('drums')
+ end
+
+ it "create two tracks" do
+ tracks = Track.sync(connection.client_id, [track_hash, track_hash])
+ tracks.length.should == 2
+ track = tracks[0]
+ track.client_track_id.should == track_hash[:client_track_id]
+ track.sound = track_hash[:sound]
+ track.instrument.should == Instrument.find('drums')
+ track = tracks[1]
+ track.client_track_id.should == track_hash[:client_track_id]
+ track.sound = track_hash[:sound]
+ track.instrument.should == Instrument.find('drums')
+ end
+
+ it "delete only track" do
+ track.id.should_not be_nil
+ connection.tracks.length.should == 1
+ tracks = Track.sync(connection.client_id, [])
+ tracks.length.should == 0
+ end
+
+ it "delete one of two tracks using .id to correlate" do
+
+ track.id.should_not be_nil
+ track2.id.should_not be_nil
+ connection.tracks.length.should == 2
+ tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
+ tracks.length.should == 1
+ found = tracks[0]
+ found.id.should == track.id
+ found.sound.should == 'mono'
+ found.client_track_id.should == 'client_guid_new'
+ end
+
+ it "delete one of two tracks using .client_track_id to correlate" do
+
+ track.id.should_not be_nil
+ track2.id.should_not be_nil
+ connection.tracks.length.should == 2
+ tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
+ tracks.length.should == 1
+ found = tracks[0]
+ found.id.should == track.id
+ found.sound.should == 'mono'
+ found.client_track_id.should == track.client_track_id
+ end
+
+
+ it "updates a single track using .id to correlate" do
+ track.id.should_not be_nil
+ connection.tracks.length.should == 1
+ tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
+ tracks.length.should == 1
+ found = tracks[0]
+ found.id.should == track.id
+ found.sound.should == 'mono'
+ found.client_track_id.should == 'client_guid_new'
+ end
+
+ it "updates a single track using .client_track_id to correlate" do
+ track.id.should_not be_nil
+ connection.tracks.length.should == 1
+ tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
+ tracks.length.should == 1
+ found = tracks[0]
+ found.id.should == track.id
+ found.sound.should == 'mono'
+ found.client_track_id.should == track.client_track_id
+ end
+ end
+end
\ No newline at end of file
diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js
index 651ad616d..56d992936 100644
--- a/web/app/assets/javascripts/JamServer.js
+++ b/web/app/assets/javascripts/JamServer.js
@@ -104,7 +104,9 @@
payload = message[messageType],
callbacks = server.dispatchTable[message.type];
- logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload));
+ if(message.type != context.JK.MessageType.HEARTBEAT_ACK) {
+ logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload));
+ }
if (callbacks !== undefined) {
var len = callbacks.length;
@@ -113,6 +115,7 @@
callbacks[i](message, payload);
} catch (ex) {
logger.warn('exception in callback for websocket message:' + ex);
+ throw ex;
}
}
}
@@ -131,7 +134,9 @@
var jsMessage = JSON.stringify(message);
- logger.log("server.send(" + jsMessage + ")");
+ if(message.type != context.JK.MessageType.HEARTBEAT) {
+ logger.log("server.send(" + jsMessage + ")");
+ }
if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) {
server.socket.send(jsMessage);
} else {
@@ -152,6 +157,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 +202,5 @@
}
+
})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/addTrack.js b/web/app/assets/javascripts/addTrack.js
index fdd3cb56f..e40732a31 100644
--- a/web/app/assets/javascripts/addTrack.js
+++ b/web/app/assets/javascripts/addTrack.js
@@ -71,7 +71,6 @@
// set arrays
inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false);
- console.log("inputUnassignedList: " + JSON.stringify(inputUnassignedList));
track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false);
}
@@ -125,18 +124,25 @@
}
function saveSettings() {
+ if (!context.JK.verifyNotRecordingForTrackChange(app)) {
+ return;
+ }
+
if (!validateSettings()) {
return;
}
saveTrack();
+
app.layout.closeDialog('add-track');
}
function saveTrack() {
// TRACK 2 INPUTS
+ var trackId = null;
$("#add-track2-input > option").each(function() {
logger.debug("Saving track 2 input = " + this.value);
+ trackId = this.value;
context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.TRACK2);
});
@@ -150,12 +156,52 @@
// UPDATE SERVER
logger.debug("Adding track with instrument " + instrumentText);
var data = {};
- // use the first track's connection_id (not sure why we need this on the track data model)
- logger.debug("myTracks[0].connection_id=" + myTracks[0].connection_id);
- data.connection_id = myTracks[0].connection_id;
- data.instrument_id = instrumentText;
- data.sound = "stereo";
- sessionModel.addTrack(sessionId, data);
+
+ context.jamClient.TrackSaveAssignments();
+
+ /**
+ setTimeout(function() {
+ var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 2);
+
+ // this is some ugly logic coming up, here's why:
+ // we need the id (guid) that the backend generated for the new track we just added
+ // to get it, we need to make sure 2 tracks come back, and then grab the track that
+ // is not the one we just added.
+ if(inputTracks.length != 2) {
+ var msg = "because we just added a track, there should be 2 available, but we found: " + inputTracks.length;
+ logger.error(msg);
+ alert(msg);
+ throw new Error(msg);
+ }
+
+ var client_track_id = null;
+ $.each(inputTracks, function(index, track) {
+
+
+ console.log("track: %o, myTrack: %o", track, myTracks[0]);
+ if(track.id != myTracks[0].id) {
+ client_track_id = track.id;
+ return false;
+ }
+ });
+
+ if(client_track_id == null)
+ {
+ var msg = "unable to find matching backend track for id: " + this.value;
+ logger.error(msg);
+ alert(msg);
+ throw new Error(msg);
+ }
+
+ // use the first track's connection_id (not sure why we need this on the track data model)
+ data.connection_id = myTracks[0].connection_id;
+ data.instrument_id = instrumentText;
+ data.sound = "stereo";
+ data.client_track_id = client_track_id;
+ sessionModel.addTrack(sessionId, data);
+ }, 1000);
+
+ */
}
function validateSettings() {
diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js
index 91cc6c0f6..581b7306a 100644
--- a/web/app/assets/javascripts/application.js
+++ b/web/app/assets/javascripts/application.js
@@ -18,4 +18,5 @@
//= require jquery.Jcrop
//= require jquery.naturalsize
//= require jquery.queryparams
+//= require globals
//= require_directory .
diff --git a/web/app/assets/javascripts/configureTrack.js b/web/app/assets/javascripts/configureTrack.js
index c2d9b8fb0..94feb2fc8 100644
--- a/web/app/assets/javascripts/configureTrack.js
+++ b/web/app/assets/javascripts/configureTrack.js
@@ -213,7 +213,6 @@
// remove option 1 from voice chat type dropdown if no music (based on what's unused on the Music Audio tab) or chat inputs are available
if ($('#audio-inputs-unused > option').size() === 0 && chatOtherUnassignedList.length === 0 && chatOtherAssignedList.length === 0) {
- logger.debug("Removing Option 1 from Voice Chat dropdown.");
$option1.remove();
}
else {
@@ -353,6 +352,7 @@
// load Audio Driver dropdown
devices = context.jamClient.TrackGetDevices();
+ logger.debug("Called TrackGetDevices with response " + JSON.stringify(devices));
var keys = Object.keys(devices);
for (var i=0; i < keys.length; i++) {
@@ -471,13 +471,13 @@
function _initMusicTabData() {
inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false);
- logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList));
+ //logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList));
track1AudioInputChannels = _loadList(ASSIGNMENT.TRACK1, true, false);
- logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels));
+ //logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels));
track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false);
- logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels));
+ //logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels));
outputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, false, false);
outputAssignedList = _loadList(ASSIGNMENT.OUTPUT, false, false);
@@ -485,16 +485,16 @@
function _initVoiceChatTabData() {
chatUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false);
- logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList));
+ //logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList));
chatAssignedList = _loadList(ASSIGNMENT.CHAT, true, false);
- logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList));
+ //logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList));
chatOtherUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, true);
- logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList));
+ //logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList));
chatOtherAssignedList = _loadList(ASSIGNMENT.CHAT, true, true);
- logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList));
+ //logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList));
}
// TODO: copied in addTrack.js - refactor to common place
@@ -548,6 +548,10 @@
}
function saveSettings() {
+ if (!context.JK.verifyNotRecordingForTrackChange(app)) {
+ return;
+ }
+
if (!validateAudioSettings(false)) {
return;
}
@@ -563,9 +567,6 @@
originalDeviceId = $('#audio-drivers').val();
app.layout.closeDialog('configure-audio');
-
- // refresh Session screen
- sessionModel.refreshCurrentSession();
}
function saveAudioSettings() {
@@ -591,11 +592,6 @@
// logger.debug("Saving track 1 instrument = " + instrumentVal);
context.jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, instrumentVal);
- // UPDATE SERVER
- logger.debug("Updating track " + myTracks[0].trackId + " with instrument " + instrumentText);
- var data = {};
- data.instrument_id = instrumentText;
- sessionModel.updateTrack(sessionId, myTracks[0].trackId, data);
// TRACK 2 INPUTS
var track2Selected = false;
@@ -609,25 +605,6 @@
// TRACK 2 INSTRUMENT
instrumentVal = $('#track2-instrument').val();
instrumentText = $('#track2-instrument > option:selected').text().toLowerCase();
-
- // track 2 new - add
- if (myTrackCount === 1) {
- data = {};
- // use the first track's connection_id (not sure why we need this on the track data model)
- logger.debug("myTracks[0].connection_id=" + myTracks[0].connection_id);
- data.connection_id = myTracks[0].connection_id;
- data.instrument_id = instrumentText;
- data.sound = "stereo";
- sessionModel.addTrack(sessionId, data);
- }
- // track 2 exists - update
- else if (myTrackCount === 2) {
- // UPDATE SERVER
- logger.debug("Updating track " + myTracks[1].trackId + " with instrument " + instrumentText);
- data = {};
- data.instrument_id = instrumentText;
- sessionModel.updateTrack(sessionId, myTracks[1].trackId, data);
- }
logger.debug("Saving track 2 instrument = " + instrumentVal);
context.jamClient.TrackSetInstrument(ASSIGNMENT.TRACK2, instrumentVal);
@@ -636,7 +613,8 @@
// track 2 was removed
if (myTrackCount === 2) {
logger.debug("Deleting track " + myTracks[1].trackId);
- sessionModel.deleteTrack(sessionId, myTracks[1].trackId);
+ client.TrackSetCount(1);
+ //sessionModel.deleteTrack(sessionId, myTracks[1].trackId);
}
}
@@ -651,6 +629,8 @@
logger.debug("Saving session audio output = " + this.value);
context.jamClient.TrackSetAssignment(this.value, false, ASSIGNMENT.OUTPUT);
});
+
+ context.jamClient.TrackSaveAssignments();
}
function saveVoiceChatSettings() {
@@ -799,6 +779,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) {
@@ -807,7 +795,6 @@
});
originalVoiceChat = context.jamClient.TrackGetChatEnable() ? VOICE_CHAT.CHAT : VOICE_CHAT.NO_CHAT;
- logger.debug("originalVoiceChat=" + originalVoiceChat);
$('#voice-chat-type').val(originalVoiceChat);
@@ -820,7 +807,6 @@
// remove option 1 from voice chat if none are available and not already assigned
if (inputUnassignedList.length === 0 && chatAssignedList.length === 0 && chatOtherAssignedList.length === 0 && chatOtherUnassignedList.length === 0) {
- logger.debug("Removing Option 1 from Voice Chat dropdown.");
$option1.remove();
}
// add it if it doesn't exist
@@ -836,7 +822,6 @@
events();
_init();
myTrackCount = myTracks.length;
- logger.debug("initialize:myTrackCount=" + myTrackCount);
};
this.showMusicAudioPanel = showMusicAudioPanel;
diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js
index 7c73e4f72..ab06fdd5b 100644
--- a/web/app/assets/javascripts/fakeJamClient.js
+++ b/web/app/assets/javascripts/fakeJamClient.js
@@ -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 RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, abortedRecordingCallbackName) {
+ fakeJamClientRecordings.RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, abortedRecordingCallbackName);
+ }
+
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.RegisterRecordingCallbacks = RegisterRecordingCallbacks;
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);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/fakeJamClientMessages.js b/web/app/assets/javascripts/fakeJamClientMessages.js
new file mode 100644
index 000000000..90792bf09
--- /dev/null
+++ b/web/app/assets/javascripts/fakeJamClientMessages.js
@@ -0,0 +1,78 @@
+(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, success, reason, detail) {
+ var msg = {};
+ msg.type = self.Types.STOP_RECORDING;
+ msg.msgId = context.JK.generateUUID();
+ msg.recordingId = recordingId;
+ msg.success = success === undefined ? true : success;
+ msg.reason = reason;
+ msg.detail = detail;
+ 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, reason, detail) {
+ var msg = {};
+ msg.type = self.Types.ABORT_RECORDING;
+ msg.msgId = context.JK.generateUUID();
+ msg.recordingId = recordingId;
+ msg.success = false;
+ msg.reason = reason;
+ msg.detail = detail;
+ 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);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/fakeJamClientRecordings.js b/web/app/assets/javascripts/fakeJamClientRecordings.js
new file mode 100644
index 000000000..b48ad335b
--- /dev/null
+++ b/web/app/assets/javascripts/fakeJamClientRecordings.js
@@ -0,0 +1,255 @@
+// 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 abortedRecordingEventCallbackName = null;
+
+ var startingSessionState = null;
+ var stoppingSessionState = null;
+
+ var currentRecordingId = null;
+ var currentRecordingCreatorClientId = null;
+ var currentRecordingClientIds = null;
+
+ function timeoutStartRecordingTimer() {
+ eval(startRecordingResultCallbackName).call(this, startingSessionState.recordingId, {success:false, reason:'client-no-response', detail:startingSessionState.groupedClientTracks[0]});
+ startingSessionState = null;
+ }
+
+ function timeoutStopRecordingTimer() {
+ eval(stopRecordingResultCallbackName).call(this, stoppingSessionState.recordingId, {success:false, reason:'client-no-response', detail:stoppingSessionState.groupedClientTracks[0]});
+ }
+
+ function StartRecording(recordingId, clients) {
+ startingSessionState = {};
+
+ // we expect all clients to respond within 3 seconds to mimic the reliable UDP layer
+ startingSessionState.aggegratingStartResultsTimer = setTimeout(timeoutStartRecordingTimer, 3000);
+ startingSessionState.recordingId = recordingId;
+ startingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId); // we will manipulate this new one
+
+ // store the current recording's data
+ currentRecordingId = recordingId;
+ currentRecordingCreatorClientId = app.clientId;
+ currentRecordingClientIds = copyClientIds(clients, app.clientId);
+
+ if(startingSessionState.groupedClientTracks.length == 0) {
+ // if there are no clients but 'self', then you can declare a successful recording immediately
+ finishSuccessfulStart(recordingId);
+ }
+ else {
+ // signal all other connected clients that the recording has started
+ for(var i = 0; i < startingSessionState.groupedClientTracks.length; i++) {
+ var clientId = startingSessionState.groupedClientTracks[i];
+ context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.startRecording(recordingId)));
+ }
+ }
+ }
+
+ function StopRecording(recordingId, clients, result) {
+
+ if(startingSessionState) {
+ // we are currently starting a session.
+ // TODO
+ }
+
+ if(!result) {
+ result = {success:true}
+ }
+
+ stoppingSessionState = {};
+
+ // we expect all clients to respond within 3 seconds to mimic the reliable UDP layer
+ stoppingSessionState.aggegratingStopResultsTimer = setTimeout(timeoutStopRecordingTimer, 3000);
+ stoppingSessionState.recordingId = recordingId;
+ stoppingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId);
+
+ if(stoppingSessionState.groupedClientTracks.length == 0) {
+ finishSuccessfulStop(recordingId);
+ }
+ else {
+ // signal all other connected clients that the recording has stopped
+ for(var i = 0; i < stoppingSessionState.groupedClientTracks.length; i++) {
+ var clientId = stoppingSessionState.groupedClientTracks[i];
+ context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.stopRecording(recordingId, result.success, result.reason, result.detail)));
+ }
+ }
+ }
+
+ 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, payload.recordingId, {success:true}, from);
+ }
+ }
+
+ 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) {
+ var index = startingSessionState.groupedClientTracks.indexOf(from);
+ startingSessionState.groupedClientTracks.splice(index, 1);
+
+ if(startingSessionState.groupedClientTracks.length == 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, {success:payload.success, reason:payload.reason, detail:from});
+ }
+
+ 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) {
+ var index = stoppingSessionState.groupedClientTracks.indexOf(from);
+ stoppingSessionState.groupedClientTracks.splice(index, 1);
+
+ if(stoppingSessionState.groupedClientTracks.length == 0) {
+ finishSuccessfulStop(payload.recordingId);
+ }
+ }
+ else {
+ // TOOD: a client responded with error; what now?
+ logger.error("client responded with error: ", payload);
+ }
+ }
+ else {
+ // 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
+ for(var i = 0; i < currentRecordingClientIds.length; i++) {
+ var clientId = currentRecordingClientIds[i];
+ context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.abortRecording(currentRecordingId, payload.reason, from)));
+ }
+
+ }
+ else {
+ logger.debug("only the creator currently deals with the abort request. abort request sent from:" + from + " with a reason of: " + payload.errorReason);
+ }
+
+ eval(abortedRecordingEventCallbackName).call(this, payload.recordingId, {success:payload.success, reason:payload.reason, detail:from});
+ }
+
+ function RegisterRecordingCallbacks(startRecordingCallbackName,
+ stopRecordingCallbackName,
+ startedRecordingCallbackName,
+ stoppedRecordingCallbackName,
+ abortedRecordingCallbackName) {
+ startRecordingResultCallbackName = startRecordingCallbackName;
+ stopRecordingResultCallbackName = stopRecordingCallbackName;
+ startedRecordingResultCallbackName = startedRecordingCallbackName;
+ stoppedRecordingEventCallbackName = stoppedRecordingCallbackName;
+ abortedRecordingEventCallbackName = abortedRecordingCallbackName;
+ }
+
+ // copies all clientIds, but removes current client ID because we don't want to message that user
+ function copyClientIds(clientIds, myClientId) {
+ var newClientIds = [];
+ for(var i = 0; i < clientIds.length; i++) {
+ var clientId = clientIds[i]
+ if(clientId != myClientId) {
+ newClientIds.push(clientId);
+ }
+ }
+ return newClientIds;
+ }
+
+ function finishSuccessfulStart(recordingId) {
+ // all clients have responded.
+ clearTimeout(startingSessionState.aggegratingStartResultsTimer);
+ startingSessionState = null;
+ eval(startRecordingResultCallbackName).call(this, recordingId, {success:true});
+ }
+
+ function finishSuccessfulStop(recordingId, errorReason) {
+ // all clients have responded.
+ clearTimeout(stoppingSessionState.aggegratingStopResultsTimer);
+ stoppingSessionState = null;
+ var result = { success: true }
+ if(errorReason)
+ {
+ result.success = false;
+ result.reason = errorReason
+ result.detail = ""
+ }
+ eval(stopRecordingResultCallbackName).call(this, recordingId, result);
+ }
+
+
+ // 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.RegisterRecordingCallbacks = RegisterRecordingCallbacks;
+ }
+
+ })(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/ftue.js b/web/app/assets/javascripts/ftue.js
index 21414246b..0c6216695 100644
--- a/web/app/assets/javascripts/ftue.js
+++ b/web/app/assets/javascripts/ftue.js
@@ -253,6 +253,16 @@
jamClient.TrackSetChatEnable(false);
}
+ var defaultInstrumentId;
+ if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) {
+ defaultInstrumentId = context.JK.instrument_id_to_instrument[context.JK.userMe.instruments[0].instrument_id].client_id;
+ }
+ else {
+ defaultInstrumentId = context.JK.server_to_client_instrument_map['Other'].client_id;
+ }
+
+ jamClient.TrackSetInstrument(1, defaultInstrumentId);
+
logger.debug("Calling FTUESave(" + persist + ")");
var response = jamClient.FTUESave(persist);
setLevels(0);
@@ -260,7 +270,6 @@
logger.warn(response);
// TODO - we may need to do something about errors on save.
// per VRFS-368, I'm hiding the alert, and logging a warning.
- // context.alert(response);
}
} else {
logger.debug("Aborting FTUESave as we need input + output selected.");
@@ -684,13 +693,10 @@
}
function setAsioSettingsVisibility() {
- logger.debug("jamClient.FTUEHasControlPanel()=" + jamClient.FTUEHasControlPanel());
if (jamClient.FTUEHasControlPanel()) {
- logger.debug("Showing ASIO button");
$('#btn-asio-control-panel').show();
}
else {
- logger.debug("Hiding ASIO button");
$('#btn-asio-control-panel').hide();
}
}
diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js
index 117e99960..1c47e8927 100644
--- a/web/app/assets/javascripts/globals.js
+++ b/web/app/assets/javascripts/globals.js
@@ -72,4 +72,17 @@
240: { "server_id": "mandolin" },
250: { "server_id": "other" }
};
+
+ context.JK.instrument_id_to_instrument = {};
+
+ (function() {
+ $.each(context.JK.server_to_client_instrument_map, function(key, value) {
+ context.JK.instrument_id_to_instrument[value.server_id] = { client_id: value.client_id, display: key }
+ });
+ })();
+
+
+ context.JK.entityToPrintable = {
+ music_session: "music session"
+ }
})(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js
index 341e0d284..d0dafc98d 100644
--- a/web/app/assets/javascripts/jam_rest.js
+++ b/web/app/assets/javascripts/jam_rest.js
@@ -13,7 +13,6 @@
var logger = context.JK.logger;
function createJoinRequest(joinRequest) {
- logger.debug("joinRequest=" + JSON.stringify(joinRequest));
return $.ajax({
type: "POST",
dataType: "json",
@@ -316,6 +315,53 @@
});
}
+ 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 putTrackSyncChange(options) {
+ var musicSessionId = options["id"]
+ delete options["id"];
+
+ return $.ajax({
+ type: "PUT",
+ dataType: "json",
+ url: '/api/sessions/' + musicSessionId + '/tracks',
+ contentType: 'application/json',
+ processData: false,
+ data: JSON.stringify(options)
+ });
+ }
+
function initialize() {
return self;
}
@@ -346,6 +392,10 @@
this.createJoinRequest = createJoinRequest;
this.updateJoinRequest = updateJoinRequest;
this.updateUser = updateUser;
+ this.startRecording = startRecording;
+ this.stopRecording = stopRecording;
+ this.getRecording = getRecording;
+ this.putTrackSyncChange = putTrackSyncChange;
return this;
};
diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js
index 61da06700..cc204795e 100644
--- a/web/app/assets/javascripts/jamkazam.js
+++ b/web/app/assets/javascripts/jamkazam.js
@@ -175,7 +175,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 = "
";
+ 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 += "- " + prettyKey + " " + errorsForKey[i] + "
";
+ }
+ }
+
+ text += "";
+
+ 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});
+ }
}
/**
@@ -221,6 +252,11 @@
this.layout.notify(message, descriptor);
};
+ /** Shows an alert notification. Expects text, title */
+ this.notifyAlert = function(title ,text) {
+ this.notify({title:title, text:text, icon_url: "/assets/content/icon_alert_big.png"});
+ }
+
/**
* Initialize any common events.
*/
@@ -264,6 +300,7 @@
if (context.jamClient) {
// Unregister for callbacks.
+ context.jamClient.RegisterRecordingCallbacks("", "", "", "", "");
context.jamClient.SessionRegisterCallback("");
context.jamClient.SessionSetAlertCallback("");
context.jamClient.FTUERegisterVUCallbacks("", "", "");
diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js
index 6958bf426..3335f55ac 100644
--- a/web/app/assets/javascripts/layout.js
+++ b/web/app/assets/javascripts/layout.js
@@ -439,9 +439,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) {
@@ -483,12 +487,20 @@
* also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one
*/
function stackDialogs($dialog, $overlay) {
- console.log("pushing dialog: " + $dialog.attr('layout-id'))
+ // don't push a dialog on the stack that is already on there; remove it from where ever it is currently
+ // and the rest of the code will make it end up at the top
+ var layoutId = $dialog.attr('layout-id');
+ for(var i = openDialogs.length - 1; i >= 0; i--) {
+ if(openDialogs[i].attr('layout-id') === layoutId) {
+ openDialogs.splice(i, 1);
+ }
+ }
+
openDialogs.push($dialog);
var zIndex = 1000;
for(var i in openDialogs) {
- var $dialog = openDialogs[i];
- $dialog.css('zIndex', zIndex);
+ var $openDialog = openDialogs[i];
+ $openDialog.css('zIndex', zIndex);
zIndex++;
}
$overlay.css('zIndex', zIndex - 1);
@@ -496,11 +508,7 @@
function unstackDialogs($overlay) {
if(openDialogs.length > 0) {
- var removed = openDialogs.pop();
- console.log("removed dialog : " + removed.attr('layout-id'));
- }
- else {
- console.log("no dialog removed because nothing was on the stack");
+ openDialogs.pop();
}
var zIndex = 1000 + openDialogs.length;
@@ -512,7 +520,7 @@
}
function showDialog(dialog) {
- dialogEvent(dialog, 'beforeShow');
+ if(!dialogEvent(dialog, 'beforeShow')) {return;}
var $overlay = $('.dialog-overlay')
$overlay.show();
centerDialog(dialog);
diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js
new file mode 100644
index 000000000..19bf02fe2
--- /dev/null
+++ b/web/app/assets/javascripts/recordingModel.js
@@ -0,0 +1,333 @@
+// 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() {
+ 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 context.JK.dkeys(groupedTracks);
+ }
+
+ function startRecording() {
+
+ $self.triggerHandler('startingRecording', {});
+
+ currentlyRecording = true;
+
+ 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 });
+ currentlyRecording = false;
+ })
+
+
+ return true;
+ }
+
+ /** Nulls can be passed for all 3 currently; that's a user request. */
+ function stopRecording(recordingId, reason, detail) {
+
+ waitingOnServerStop = waitingOnClientStop = true;
+ waitingOnStopTimer = setTimeout(timeoutTransitionToStop, 5000);
+
+ $self.triggerHandler('stoppingRecording', {reason: reason, detail: detail});
+
+ // this path assumes that the currentRecording info has, or can be, retrieved
+ // failure for currentRecording is handled elsewhere
+ 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, reason, detail);
+ })
+ .fail(function(jqXHR) {
+ if(jqXHR.status == 422) {
+ waitingOnServerStop = false;
+ attemptTransitionToStop(recording.id, reason, detail);
+ }
+ 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, {reason: errorReason, detail: errorDetail, success:false});
+ }
+
+ 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() {
+ currentlyRecording = false;
+ currentRecording = null;
+ currentRecordingId = null;
+ if(waitingOnStopTimer) {
+ clearTimeout(waitingOnStopTimer);
+ waitingOnStopTimer = null;
+ }
+ }
+
+ function onServerStartRecording() {
+
+ }
+
+ function onServerStopRecording(recordingId) {
+ stopRecording(recordingId, null, null);
+ }
+
+ function handleRecordingStartResult(recordingId, result) {
+
+ var success = result.success;
+ var reason = result.reason;
+ var detail = result.detail;
+
+
+ if(success) {
+ $self.triggerHandler('startedRecording', {clientId: app.clientId})
+ }
+ else {
+ currentlyRecording = false;
+ logger.error("unable to start the recording %o, %o", reason, detail);
+ $self.triggerHandler('startedRecording', { clientId: app.clientId, reason: reason, detail: detail});
+ }
+ }
+
+ function handleRecordingStopResult(recordingId, result) {
+
+ var success = result.success;
+ var reason = result.reason;
+ var detail = result.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(recordingId, result, clientId) {
+ var success = result.success;
+ var reason = result.reason;
+ var detail = result.detail;
+
+ // in this scenario, we don't know all the tracks of the user.
+ // we need to ask sessionModel to populate us with the recording data ASAP
+
+ currentRecording = rest.getRecording({id: recordingId})
+ .fail(function() {
+ abortRecording(recordingId, 'populate-recording-info', app.clientId);
+ })
+ .done(function(recording) {
+ currentRecordingId = recording.id;
+ });
+
+ $self.triggerHandler('startingRecording', {recordingId: recordingId});
+ currentlyRecording = true;
+ $self.triggerHandler('startedRecording', {clientId: clientId, recordingId: recordingId});
+ }
+
+ function handleRecordingStopped(recordingId, result) {
+ var success = result.success;
+ var reason = result.reason;
+ var detail = result.detail;
+
+
+ $self.triggerHandler('stoppingRecording', {recordingId: recordingId, reason: reason, detail: detail });
+ // the backend says the recording must be stopped.
+ // tell the server to stop it too
+ rest.stopRecording({
+ id: recordingId
+ })
+ .always(function() {
+ transitionToStopped();
+ })
+ .fail(function(jqXHR, textStatus, errorMessage) {
+ if(jqXHR.status == 422) {
+ logger.debug("recording already stopped %o", arguments);
+ $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
+ }
+ else if(jqXHR.status == 404) {
+ logger.debug("recording is already deleted %o", arguments);
+ $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
+ }
+ else {
+ $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: textStatus, detail: errorMessage});
+ }
+ })
+ .done(function() {
+ $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
+ })
+ }
+
+ function handleRecordingAborted(recordingId, result) {
+ var success = result.success;
+ var reason = result.reason;
+ var detail = result.detail;
+
+ stoppingRecording = false;
+
+ $self.triggerHandler('abortedRecording', {recordingId: recordingId, reason: reason, detail: detail });
+ // the backend says the recording must be stopped.
+ // tell the server to stop it too
+ rest.stopRecording({
+ id: recordingId
+ })
+ .always(function() {
+ currentlyRecording = false;
+ })
+ }
+
+ /**
+ * If a stop is needed, it will be issued, and the deferred object will fire done()
+ * If a stop is not needed (i.e., there is no recording), then the deferred object will fire immediately
+ * @returns {$.Deferred} in all cases, only .done() is fired.
+ */
+ function stopRecordingIfNeeded() {
+ var deferred = new $.Deferred();
+
+ function resolved() {
+ $self.off('stoppedRecording.stopRecordingIfNeeded', resolved);
+ deferred.resolve(arguments);
+ }
+
+ if(!currentlyRecording) {
+ deferred = new $.Deferred();
+ deferred.resolve();
+ }
+ else {
+ // wait for the next stoppedRecording event message
+ $self.on('stoppedRecording.stopRecordingIfNeeded', resolved);
+
+ if(!stopRecording()) {
+ // no event is coming, so satisfy the deferred immediately
+ $self.off('stoppedRecording.stopRecordingIfNeeded', resolved);
+ deferred = new $.Deferred();
+ deferred.resolve();
+ }
+ }
+
+ return deferred;
+
+ }
+
+ this.initialize = function() {
+ };
+
+ this.startRecording = startRecording;
+ this.stopRecording = stopRecording;
+ this.onServerStopRecording = onServerStopRecording;
+ this.isRecording = isRecording;
+ this.reset = reset;
+ this.stopRecordingIfNeeded = stopRecordingIfNeeded;
+
+ context.JK.HandleRecordingStartResult = handleRecordingStartResult;
+ context.JK.HandleRecordingStopResult = handleRecordingStopResult;
+ context.JK.HandleRecordingStopped = handleRecordingStopped;
+ context.JK.HandleRecordingStarted = handleRecordingStarted;
+ context.JK.HandleRecordingAborted = handleRecordingAborted;
+
+
+ };
+
+ })(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js
index 278f733b8..db0ea888c 100644
--- a/web/app/assets/javascripts/session.js
+++ b/web/app/assets/javascripts/session.js
@@ -10,18 +10,21 @@
var tracks = {};
var myTracks = [];
var mixers = [];
-
var configureTrackDialog;
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.
@@ -92,18 +95,59 @@
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) {
if (type === 2) { // BACKEND_MIXER_CHANGE
- sessionModel.refreshCurrentSession();
+ logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);
+ if(sessionModel.id() && text == "RebuildAudioIoControl") {
+ // this is a local change to our tracks. we need to tell the server about our updated track information
+ var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
+
+ // create a trackSync request based on backend data
+ var syncTrackRequest = {};
+ syncTrackRequest.client_id = app.clientId;
+ syncTrackRequest.tracks = inputTracks;
+ syncTrackRequest.id = sessionModel.id();
+
+ rest.putTrackSyncChange(syncTrackRequest)
+ .done(function() {
+ sessionModel.refreshCurrentSession();
+ })
+ .fail(function() {
+ app.notify({
+ "title": "Can't Sync Local Tracks",
+ "text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
+ "icon_url": "/assets/content/icon_alert_big.png"
+ });
+ })
+ }
+ else {
+ // this is wrong; we shouldn't be using the server to decide what tracks are shown
+ // however, we have to without a refactor, and if we wait a second, then it should be enough time
+ // for the client that initialed this to have made the change
+ // https://jamkazam.atlassian.net/browse/VRFS-854
+ setTimeout(function() {
+ sessionModel.refreshCurrentSession(); // XXX: race condition possible here; other client may not have updated server yet
+ }, 1000);
+ }
} else {
context.setTimeout(function() {
- app.notify({
- "title": alert_type[type].title,
- "text": text,
- "icon_url": "/assets/content/icon_alert_big.png"
- }); }, 1);
+ var alert = alert_type[type];
+
+ if(alert) {
+ app.notify({
+ "title": alert_type[type].title,
+ "text": text,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ });
+ }
+ else {
+ logger.debug("Unknown Backend Event type %o, data %o", type, text)
+ }
+ }, 1);
+
}
}
@@ -116,6 +160,7 @@
// Subscribe for callbacks on audio events
context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback");
context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback");
+ context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
context.jamClient.SessionSetAlertCallback("JK.AlertCallback");
// If you load this page directly, the loading of the current user
@@ -132,8 +177,26 @@
checkForCurrentUser();
}
+ function notifyWithUserInfo(title , text, clientId) {
+ sessionModel.findUserBy({clientId: clientId})
+ .done(function(user) {
+ app.notify({
+ "title": title,
+ "text": user.name + " " + text,
+ "icon_url": context.JK.resolveAvatarUrl(user.photo_url)
+ });
+ })
+ .fail(function() {
+ app.notify({
+ "title": title,
+ "text": 'Someone ' + text,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ });
+ });
+ }
+
+
function afterCurrentUserLoaded() {
- logger.debug("afterCurrentUserLoaded");
// It seems the SessionModel should be a singleton.
// a client can only be in one session at a time,
// and other parts of the code want to know at any certain times
@@ -144,6 +207,130 @@
context.jamClient
);
+ $(sessionModel.recordingModel)
+ .on('startingRecording', function(e, data) {
+ displayStartingRecording();
+ })
+ .on('startedRecording', function(e, data) {
+ if(data.reason) {
+ var reason = data.reason;
+ var detail = data.detail;
+
+ var title = "Could Not Start Recording";
+
+ if(data.reason == 'client-no-response') {
+ notifyWithUserInfo(title, 'did not respond to the start signal.', detail);
+ }
+ else if(data.reason == 'empty-recording-id') {
+ app.notifyAlert(title, "No recording ID specified.");
+ }
+ else if(data.reason == 'missing-client') {
+ notifyWithUserInfo(title, 'could not be signalled to start recording.', detail);
+ }
+ else if(data.reason == 'already-recording') {
+ app.notifyAlert(title, 'Already recording.');
+ }
+ else if(data.reason == 'recording-engine-unspecified') {
+ notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail);
+ }
+ else if(data.reason == 'recording-engine-create-directory') {
+ notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail);
+ }
+ else if(data.reason == 'recording-engine-create-file') {
+ notifyWithUserInfo(title, 'had a problem creating a recording file.', detail);
+ }
+ else if(data.reason == 'recording-engine-sample-rate') {
+ notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail);
+ }
+ else {
+ notifyWithUserInfo(title, 'Error Reason: ' + reason);
+ }
+ displayDoneRecording();
+ }
+ else
+ {
+ displayStartedRecording();
+ displayWhoCreated(data.clientId);
+ }
+ })
+ .on('stoppingRecording', function(e, data) {
+ displayStoppingRecording(data);
+ })
+ .on('stoppedRecording', function(e, data) {
+ if(data.reason) {
+ var reason = data.reason;
+ var detail = data.detail;
+
+ var title = "Recording Discarded";
+
+ if(data.reason == 'client-no-response') {
+ notifyWithUserInfo(title, 'did not respond to the stop signal.', detail);
+ }
+ else if(data.reason == 'missing-client') {
+ notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail);
+ }
+ else if(data.reason == 'empty-recording-id') {
+ app.notifyAlert(title, "No recording ID specified.");
+ }
+ else if(data.reason == 'wrong-recording-id') {
+ app.notifyAlert(title, "Wrong recording ID specified.");
+ }
+ else if(data.reason == 'not-recording') {
+ app.notifyAlert(title, "Not currently recording.");
+ }
+ else if(data.reason == 'already-stopping') {
+ app.notifyAlert(title, "Already stopping the current recording.");
+ }
+ else if(data.reason == 'start-before-stop') {
+ notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail);
+ }
+ else {
+ app.notifyAlert(title, "Error reason: " + reason);
+ }
+
+ displayDoneRecording();
+ }
+ else {
+ displayDoneRecording();
+ promptUserToSave(data.recordingId);
+ }
+
+ })
+ .on('abortedRecording', function(e, data) {
+ var reason = data.reason;
+ var detail = data.detail;
+
+ var title = "Recording Cancelled";
+
+ if(data.reason == 'client-no-response') {
+ notifyWithUserInfo(title, 'did not respond to the start signal.', detail);
+ }
+ else if(data.reason == 'missing-client') {
+ notifyWithUserInfo(title, 'could not be signalled to start recording.', detail);
+ }
+ else if(data.reason == 'populate-recording-info') {
+ notifyWithUserInfo(title, 'could not synchronize with the server.', detail);
+ }
+ else if(data.reason == 'recording-engine-unspecified') {
+ notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail);
+ }
+ else if(data.reason == 'recording-engine-create-directory') {
+ notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail);
+ }
+ else if(data.reason == 'recording-engine-create-file') {
+ notifyWithUserInfo(title, 'had a problem creating a recording file.', detail);
+ }
+ else if(data.reason == 'recording-engine-sample-rate') {
+ notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail);
+ }
+ else {
+ app.notifyAlert(title, "Error reason: " + reason);
+ }
+
+ displayDoneRecording();
+
+ })
+
sessionModel.subscribe('sessionScreen', sessionChanged);
sessionModel.joinSession(sessionId)
.fail(function(xhr, textStatus, errorMessage) {
@@ -775,10 +962,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 = $("(0:00)");
+ var $recordingStatus = $('').append("Stop Recording").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)
$('#track-settings').click(function() {
configureTrackDialog.showVoiceChatPanel(true);
@@ -811,7 +1109,6 @@
context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback;
context.JK.HandleBridgeCallback = handleBridgeCallback;
context.JK.AlertCallback = alertCallback;
-
};
})(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js
index 712efbaee..c4b5148c6 100644
--- a/web/app/assets/javascripts/sessionModel.js
+++ b/web/app/assets/javascripts/sessionModel.js
@@ -14,9 +14,11 @@
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;
+ return currentSession ? currentSession.id : null;
}
function participants() {
@@ -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);
@@ -77,40 +80,41 @@
return deferred;
}
+ function performLeaveSession(deferred) {
+
+ logger.debug("SessionModel.leaveCurrentSession()");
+ // TODO - sessionChanged will be called with currentSession = null
+ server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession);
+ server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession);
+ // leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long
+ // time, for that entire duration you'll still be sending voice data to the other users.
+ // this may be bad if someone decides to badmouth others in the left-session during this time
+ logger.debug("calling jamClient.LeaveSession for clientId=" + clientId);
+ client.LeaveSession({ sessionID: currentSessionId });
+ deferred = leaveSessionRest(currentSessionId);
+ deferred.done(function() {
+ sessionChanged();
+ });
+
+ // 'unregister' for callbacks
+ context.jamClient.SessionRegisterCallback("");
+ context.jamClient.SessionSetAlertCallback("");
+ updateCurrentSession(null);
+ currentSessionId = null;
+ }
/**
* Leave the current session, if there is one.
* callback: called in all conditions; either after an attempt is made to tell the server that we are leaving,
* or immediately if there is no session
*/
function leaveCurrentSession() {
- var deferred;
+ var deferred = new $.Deferred();
- if(currentSessionId) {
- logger.debug("SessionModel.leaveCurrentSession()");
- // TODO - sessionChanged will be called with currentSession = null
- server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession);
- server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession);
- // leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long
- // time, for that entire duration you'll still be sending voice data to the other users.
- // this may be bad if someone decides to badmouth others in the left-session during this time
- logger.debug("calling jamClient.LeaveSession for clientId=" + clientId);
- client.LeaveSession({ sessionID: currentSessionId });
- deferred = leaveSessionRest(currentSessionId);
- deferred.done(function() {
- sessionChanged();
+ recordingModel.stopRecordingIfNeeded()
+ .always(function(){
+ performLeaveSession(deferred);
});
- // 'unregister' for callbacks
- context.jamClient.SessionRegisterCallback("");
- context.jamClient.SessionSetAlertCallback("");
- currentSession = null;
- currentSessionId = null;
- }
- else {
- deferred = new $.Deferred();
- deferred.resolve();
- }
-
return deferred;
}
@@ -118,10 +122,9 @@
* Refresh the current session, and participants.
*/
function refreshCurrentSession() {
+ // XXX use backend instead: https://jamkazam.atlassian.net/browse/VRFS-854
logger.debug("SessionModel.refreshCurrentSession()");
- refreshCurrentSessionRest(function() {
- refreshCurrentSessionParticipantsRest(sessionChanged);
- });
+ refreshCurrentSessionRest(sessionChanged);
}
/**
@@ -139,29 +142,48 @@
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);
+ 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 +248,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;
@@ -269,7 +261,7 @@
}
function addTrack(sessionId, data) {
- logger.debug("track data = " + JSON.stringify(data));
+ logger.debug("updating tracks on the server %o", data);
var url = "/api/sessions/" + sessionId + "/tracks";
$.ajax({
type: "POST",
@@ -281,9 +273,8 @@
processData:false,
success: function(response) {
// save to the backend
- context.jamClient.TrackSaveAssignments();
- logger.debug("Successfully added track (" + JSON.stringify(data) + ")");
- refreshCurrentSession();
+ logger.debug("successfully updated tracks on the server");
+ //refreshCurrentSession();
},
error: ajaxError
});
@@ -307,7 +298,13 @@
}
function deleteTrack(sessionId, trackId) {
+
if (trackId) {
+
+ client.TrackSetCount(1);
+ client.TrackSaveAssignments();
+
+ /**
$.ajax({
type: "DELETE",
url: "/api/sessions/" + sessionId + "/tracks/" + trackId,
@@ -326,6 +323,7 @@
logger.error("Error deleting track " + trackId);
}
});
+ */
}
}
@@ -428,6 +426,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 +459,8 @@
this.updateTrack = updateTrack;
this.deleteTrack = deleteTrack;
this.onWebsocketDisconnected = onWebsocketDisconnected;
+ this.recordingModel = recordingModel;
+ this.findUserBy = findUserBy;
this.getCurrentSession = function() {
return currentSession;
};
diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js
index 1d85a6ca7..e5fb86dbe 100644
--- a/web/app/assets/javascripts/sidebar.js
+++ b/web/app/assets/javascripts/sidebar.js
@@ -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)
+ });
+ }
});
}
diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js
index 1cec3b246..713f78a42 100644
--- a/web/app/assets/javascripts/trackHelpers.js
+++ b/web/app/assets/javascripts/trackHelpers.js
@@ -14,6 +14,21 @@
// take all necessary arguments to complete its work.
context.JK.TrackHelpers = {
+ getTracks: function(jamClient, groupId) {
+ var tracks = [];
+ var trackIds = jamClient.SessionGetIDs();
+ var allTracks = jamClient.SessionGetControlState(trackIds);
+
+ // get client's tracks
+ for (var i=0; i < allTracks.length; i++) {
+ if (allTracks[i].group_id === groupId) {
+ tracks.push(allTracks[i]);
+ }
+ }
+
+ return tracks;
+ },
+
/**
* This function resolves which tracks to configure for a user
* when creating or joining a session. By default, tracks are pulled
@@ -22,80 +37,25 @@
*/
getUserTracks: function(jamClient) {
var trackIds = jamClient.SessionGetIDs();
- var allTracks = jamClient.SessionGetControlState(trackIds);
var localMusicTracks = [];
var i;
var instruments = [];
- var localTrackExists = false;
- // get client's tracks
- for (i=0; i < allTracks.length; i++) {
- if (allTracks[i].group_id === 2) {
- localMusicTracks.push(allTracks[i]);
-
- console.log("allTracks[" + i + "].instrument_id=" + allTracks[i].instrument_id);
- // check if local track config exists
- if (allTracks[i].instrument_id !== 0) {
- localTrackExists = true;
- }
- }
- }
+ localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, 2);
var trackObjects = [];
- console.log("localTrackExists=" + localTrackExists);
-
- // get most proficient instrument from API if no local track config exists
- if (!localTrackExists) {
- if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) {
- var track = {
- instrument_id: context.JK.userMe.instruments[0].instrument_id,
- sound: "stereo"
- };
- trackObjects.push(track);
-
- var desc = context.JK.userMe.instruments[0].description;
- jamClient.TrackSetInstrument(1, context.JK.server_to_client_instrument_map[desc]);
- jamClient.TrackSaveAssignments();
- }
- }
- // use all tracks previously configured
- else {
- console.log("localMusicTracks.length=" + localMusicTracks.length);
- for (i=0; i < localMusicTracks.length; i++) {
- var track = {};
- var instrument_description = '';
- console.log("localMusicTracks[" + i + "].instrument_id=" + localMusicTracks[i].instrument_id);
-
- // no instruments configured
- if (localMusicTracks[i].instrument_id === 0) {
- if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) {
- track.instrument_id = context.JK.userMe.instruments[0].instrument_id;
- }
- else {
- track.instrument_id = context.JK.client_to_server_instrument_map[250].server_id;
- }
- }
- // instruments are configured
- else {
- if (context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id]) {
- track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id;
- }
- // fall back to Other
- else {
- track.instrument_id = context.JK.client_to_server_instrument_map[250].server_id;
- jamClient.TrackSetInstrument(i+1, 250);
- jamClient.TrackSaveAssignments();
- }
- }
- if (localMusicTracks[i].stereo) {
- track.sound = "stereo";
- }
- else {
- track.sound = "mono";
- }
- trackObjects.push(track);
+ for (i=0; i < localMusicTracks.length; i++) {
+ var track = {};
+ track.client_track_id = localMusicTracks[i].id;
+ track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id;
+ if (localMusicTracks[i].stereo) {
+ track.sound = "stereo";
}
+ else {
+ track.sound = "mono";
+ }
+ trackObjects.push(track);
}
return trackObjects;
}
diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js
index a0c347dea..d6ff1c289 100644
--- a/web/app/assets/javascripts/utils.js
+++ b/web/app/assets/javascripts/utils.js
@@ -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.
@@ -209,6 +220,19 @@
return count;
};
+ /*
+ * Get the keys of a dictionary as an array (same as Object.keys, but works in all browsers)
+ */
+ context.JK.dkeys = function(d) {
+ var keys = []
+ for (var i in d) {
+ if (d.hasOwnProperty(i)) {
+ keys.push(i);
+ }
+ }
+ return keys;
+ };
+
/**
* Finds the first error associated with the field.
* @param fieldName the name of the field
@@ -494,4 +518,18 @@
return rhex(a) + rhex(b) + rhex(c) + rhex(d);
};
- })(window,jQuery);
\ No newline at end of file
+ /** 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);
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss
index 67127c152..c75f35e17 100644
--- a/web/app/assets/stylesheets/client/common.css.scss
+++ b/web/app/assets/stylesheets/client/common.css.scss
@@ -12,6 +12,7 @@ $ColorLinkHover: #82AEAF;
$ColorSidebarText: #a0b9bd;
$ColorScreenBackground: lighten($ColorUIBackground, 10%);
$ColorTextBoxBackground: #c5c5c5;
+$ColorRecordingBackground: #471f18;
$color1: #006AB6; /* mid blue */
$color2: #9A9084; /* warm gray */
diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss
index 9a1e4491e..bf23a1479 100644
--- a/web/app/assets/stylesheets/client/session.css.scss
+++ b/web/app/assets/stylesheets/client/session.css.scss
@@ -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;
diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb
index 81be02657..1d1028a8b 100644
--- a/web/app/controllers/api_music_sessions_controller.rb
+++ b/web/app/controllers/api_music_sessions_controller.rb
@@ -141,11 +141,24 @@ class ApiMusicSessionsController < ApiController
end
end
+ def track_sync
+ @tracks = Track.sync(params[:client_id], params[:tracks])
+
+ unless @tracks.kind_of? Array
+ # we have to do this because api_session_detail_url will fail with a bad @music_session
+ response.status = :unprocessable_entity
+ respond_with @tracks
+ else
+ respond_with @tracks, responder: ApiResponder
+ end
+ end
+
def track_create
@track = Track.save(nil,
params[:connection_id],
params[:instrument_id],
- params[:sound])
+ params[:sound],
+ params[:client_track_id])
respond_with @track, responder: ApiResponder, :status => 201, :location => api_session_track_detail_url(@track.connection.music_session, @track)
end
@@ -155,7 +168,8 @@ class ApiMusicSessionsController < ApiController
@track = Track.save(params[:track_id],
nil,
params[:instrument_id],
- params[:sound])
+ params[:sound],
+ params[:client_track_id])
respond_with @track, responder: ApiResponder, :status => 200
diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb
index d0500a0dc..6d9552720 100644
--- a/web/app/controllers/api_recordings_controller.rb
+++ b/web/app/controllers/api_recordings_controller.rb
@@ -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
diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl
index b9dd7af6f..9668f029a 100644
--- a/web/app/views/api_claimed_recordings/show.rabl
+++ b/web/app/views/api_claimed_recordings/show.rabl
@@ -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
}
}
diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl
index b9670496e..adc061e44 100644
--- a/web/app/views/api_music_sessions/show.rabl
+++ b/web/app/views/api_music_sessions/show.rabl
@@ -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
@@ -15,7 +21,7 @@ child(:connections => :participants) {
end
child(:tracks => :tracks) {
- attributes :id, :connection_id, :instrument_id, :sound
+ attributes :id, :connection_id, :instrument_id, :sound, :client_track_id
}
}
diff --git a/web/app/views/api_music_sessions/track_show.rabl b/web/app/views/api_music_sessions/track_show.rabl
index c460b0085..9bfb6d9bd 100644
--- a/web/app/views/api_music_sessions/track_show.rabl
+++ b/web/app/views/api_music_sessions/track_show.rabl
@@ -1,3 +1,3 @@
object @track
-attributes :id, :connection_id, :instrument_id, :sound
\ No newline at end of file
+attributes :id, :connection_id, :instrument_id, :sound, :client_track_id
\ No newline at end of file
diff --git a/web/app/views/api_recordings/show.rabl b/web/app/views/api_recordings/show.rabl
new file mode 100644
index 000000000..f0f15de2b
--- /dev/null
+++ b/web/app/views/api_recordings/show.rabl
@@ -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
+ }
+}
+
diff --git a/web/app/views/api_recordings/start.rabl b/web/app/views/api_recordings/start.rabl
new file mode 100644
index 000000000..7d9e3b9f5
--- /dev/null
+++ b/web/app/views/api_recordings/start.rabl
@@ -0,0 +1,3 @@
+object @recording
+
+extends "api_recordings/show"
\ No newline at end of file
diff --git a/web/app/views/api_recordings/stop.rabl b/web/app/views/api_recordings/stop.rabl
new file mode 100644
index 000000000..7d9e3b9f5
--- /dev/null
+++ b/web/app/views/api_recordings/stop.rabl
@@ -0,0 +1,3 @@
+object @recording
+
+extends "api_recordings/show"
\ No newline at end of file
diff --git a/web/app/views/clients/_recordingFinishedDialog.html.erb b/web/app/views/clients/_recordingFinishedDialog.html.erb
new file mode 100644
index 000000000..b59125b1c
--- /dev/null
+++ b/web/app/views/clients/_recordingFinishedDialog.html.erb
@@ -0,0 +1,14 @@
+
+
+
+
+ <%= image_tag "content/recordbutton-off.png", {:height => 20, :width => 20, :class => 'content-icon'} %>
+
Recording Finished
+
+
+
+ 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.
+
+
+
DISCARD
+
diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb
index 8750d6e5c..eda192a6b 100644
--- a/web/app/views/clients/_session.html.erb
+++ b/web/app/views/clients/_session.html.erb
@@ -83,9 +83,9 @@
-
diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb
index 556447f6e..eb316d30c 100644
--- a/web/app/views/clients/index.html.erb
+++ b/web/app/views/clients/index.html.erb
@@ -32,6 +32,7 @@
<%= render "account_audio_profile" %>
<%= render "invitationDialog" %>
<%= render "whatsNextDialog" %>
+<%= render "recordingFinishedDialog" %>
<%= render "notify" %>
<%= render "client_update" %>
<%= render "banner" %>
@@ -53,11 +54,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)) {
@@ -170,11 +167,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()
diff --git a/web/config/application.rb b/web/config/application.rb
index 09bf7bf38..03afc6ec8 100644
--- a/web/config/application.rb
+++ b/web/config/application.rb
@@ -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
diff --git a/web/config/routes.rb b/web/config/routes.rb
index 4fd9b3284..f008a2ef2 100644
--- a/web/config/routes.rb
+++ b/web/config/routes.rb
@@ -91,6 +91,7 @@ SampleApp::Application.routes.draw do
# music session tracks
match '/sessions/:id/tracks' => 'api_music_sessions#track_create', :via => :post
+ match '/sessions/:id/tracks' => 'api_music_sessions#track_sync', :via => :put
match '/sessions/:id/tracks' => 'api_music_sessions#track_index', :via => :get
match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_update', :via => :post
match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_show', :via => :get, :as => 'api_session_track_detail'
@@ -258,10 +259,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
diff --git a/web/lib/music_session_manager.rb b/web/lib/music_session_manager.rb
index 0433bf3f1..84c61f9a7 100644
--- a/web/lib/music_session_manager.rb
+++ b/web/lib/music_session_manager.rb
@@ -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
diff --git a/web/spec/controllers/claimed_recordings_spec.rb b/web/spec/controllers/claimed_recordings_spec.rb
index ae154c1d7..9e9e58ed9 100644
--- a/web/spec/controllers/claimed_recordings_spec.rb
+++ b/web/spec/controllers/claimed_recordings_spec.rb
@@ -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,7 @@ 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
- response.should be_success
+ response.should be_success
json = JSON.parse(response.body)
json.should_not be_nil
json["id"].should == @claimed_recording.id
diff --git a/web/spec/controllers/api_corporate_controller_spec.rb b/web/spec/controllers/corporate_controller_spec.rb
similarity index 100%
rename from web/spec/controllers/api_corporate_controller_spec.rb
rename to web/spec/controllers/corporate_controller_spec.rb
diff --git a/web/spec/controllers/recordings_controller_spec.rb b/web/spec/controllers/recordings_controller_spec.rb
new file mode 100644
index 000000000..4e8601d57
--- /dev/null
+++ b/web/spec/controllers/recordings_controller_spec.rb
@@ -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
diff --git a/web/spec/factories.rb b/web/spec/factories.rb
index 6bfb5cc27..382d540fe 100644
--- a/web/spec/factories.rb
+++ b/web/spec/factories.rb
@@ -120,6 +120,7 @@ FactoryGirl.define do
factory :track, :class => JamRuby::Track do
sound "mono"
+ sequence(:client_track_id) { |n| "client_track_id_seq_#{n}"}
end
diff --git a/web/spec/features/recordings_spec.rb b/web/spec/features/recordings_spec.rb
new file mode 100644
index 000000000..3795eb84f
--- /dev/null
+++ b/web/spec/features/recordings_spec.rb
@@ -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
+
diff --git a/web/spec/javascripts/callbackReceiver.spec.js b/web/spec/javascripts/callbackReceiver.spec.js
index 404c97730..00dd7fa4e 100644
--- a/web/spec/javascripts/callbackReceiver.spec.js
+++ b/web/spec/javascripts/callbackReceiver.spec.js
@@ -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() {
diff --git a/web/spec/javascripts/faderHelpers.spec.js b/web/spec/javascripts/faderHelpers.spec.js
index de8eabacb..a74ace882 100644
--- a/web/spec/javascripts/faderHelpers.spec.js
+++ b/web/spec/javascripts/faderHelpers.spec.js
@@ -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() {
diff --git a/web/spec/javascripts/findSession.spec.js b/web/spec/javascripts/findSession.spec.js
index 9e8d27f87..61f79c4a1 100644
--- a/web/spec/javascripts/findSession.spec.js
+++ b/web/spec/javascripts/findSession.spec.js
@@ -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');
});
diff --git a/web/spec/javascripts/formToObject.spec.js b/web/spec/javascripts/formToObject.spec.js
index 8a6d72fd3..f3bafb2a0 100644
--- a/web/spec/javascripts/formToObject.spec.js
+++ b/web/spec/javascripts/formToObject.spec.js
@@ -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() {
diff --git a/web/spec/javascripts/helpers/jasmine-jquery.js b/web/spec/javascripts/helpers/jasmine-jquery.js
index ca8f6b0ee..597512e7c 100644
--- a/web/spec/javascripts/helpers/jasmine-jquery.js
+++ b/web/spec/javascripts/helpers/jasmine-jquery.js
@@ -1,546 +1,700 @@
-var readFixtures = function() {
- return jasmine.getFixtures().proxyCallTo_('read', arguments)
-}
+/*!
+ Jasmine-jQuery: a set of jQuery helpers for Jasmine tests.
-var preloadFixtures = function() {
- jasmine.getFixtures().proxyCallTo_('preload', arguments)
-}
+ Version 1.5.92
-var loadFixtures = function() {
- jasmine.getFixtures().proxyCallTo_('load', arguments)
-}
+ https://github.com/velesin/jasmine-jquery
-var appendLoadFixtures = function() {
- jasmine.getFixtures().proxyCallTo_('appendLoad', arguments)
-}
+ Copyright (c) 2010-2013 Wojciech Zawistowski, Travis Jeffery
-var setFixtures = function(html) {
- jasmine.getFixtures().proxyCallTo_('set', arguments)
-}
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
-var appendSetFixtures = function() {
- jasmine.getFixtures().proxyCallTo_('appendSet', arguments)
-}
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
-var sandbox = function(attributes) {
- return jasmine.getFixtures().sandbox(attributes)
-}
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
-var spyOnEvent = function(selector, eventName) {
- return jasmine.JQuery.events.spyOn(selector, eventName)
-}
++function (jasmine, $) { "use strict";
-var preloadStyleFixtures = function() {
- jasmine.getStyleFixtures().proxyCallTo_('preload', arguments)
-}
-
-var loadStyleFixtures = function() {
- jasmine.getStyleFixtures().proxyCallTo_('load', arguments)
-}
-
-var appendLoadStyleFixtures = function() {
- jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments)
-}
-
-var setStyleFixtures = function(html) {
- jasmine.getStyleFixtures().proxyCallTo_('set', arguments)
-}
-
-var appendSetStyleFixtures = function(html) {
- jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments)
-}
-
-var loadJSONFixtures = function() {
- return jasmine.getJSONFixtures().proxyCallTo_('load', arguments)
-}
-
-var getJSONFixture = function(url) {
- return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url]
-}
-
-jasmine.spiedEventsKey = function (selector, eventName) {
- return [$(selector).selector, eventName].toString()
-}
-
-jasmine.getFixtures = function() {
- return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures()
-}
-
-jasmine.getStyleFixtures = function() {
- return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures()
-}
-
-jasmine.Fixtures = function() {
- this.containerId = 'jasmine-fixtures'
- this.fixturesCache_ = {}
- this.fixturesPath = 'spec/javascripts/fixtures'
-}
-
-jasmine.Fixtures.prototype.set = function(html) {
- this.cleanUp()
- this.createContainer_(html)
-}
-
-jasmine.Fixtures.prototype.appendSet= function(html) {
- this.addToContainer_(html)
-}
-
-jasmine.Fixtures.prototype.preload = function() {
- this.read.apply(this, arguments)
-}
-
-jasmine.Fixtures.prototype.load = function() {
- this.cleanUp()
- this.createContainer_(this.read.apply(this, arguments))
-}
-
-jasmine.Fixtures.prototype.appendLoad = function() {
- this.addToContainer_(this.read.apply(this, arguments))
-}
-
-jasmine.Fixtures.prototype.read = function() {
- var htmlChunks = []
-
- var fixtureUrls = arguments
- for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
- htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]))
- }
-
- return htmlChunks.join('')
-}
-
-jasmine.Fixtures.prototype.clearCache = function() {
- this.fixturesCache_ = {}
-}
-
-jasmine.Fixtures.prototype.cleanUp = function() {
- $('#' + this.containerId).remove()
-}
-
-jasmine.Fixtures.prototype.sandbox = function(attributes) {
- var attributesToSet = attributes || {}
- return $('').attr(attributesToSet)
-}
-
-jasmine.Fixtures.prototype.createContainer_ = function(html) {
- var container
- if(html instanceof $) {
- container = $('')
- container.html(html)
- } else {
- container = '' + html + '
'
- }
- $('body').append(container)
-}
-
-jasmine.Fixtures.prototype.addToContainer_ = function(html){
- var container = $('body').find('#'+this.containerId).append(html)
- if(!container.length){
- this.createContainer_(html)
- }
-}
-
-jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) {
- if (typeof this.fixturesCache_[url] === 'undefined') {
- this.loadFixtureIntoCache_(url)
- }
- return this.fixturesCache_[url]
-}
-
-jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
- var url = this.makeFixtureUrl_(relativeUrl)
- var request = $.ajax({
- type: "GET",
- url: url + "?" + new Date().getTime(),
- async: false
- })
- this.fixturesCache_[relativeUrl] = request.responseText
-}
-
-jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){
- return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
-}
-
-jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
- return this[methodName].apply(this, passedArguments)
-}
-
-
-jasmine.StyleFixtures = function() {
- this.fixturesCache_ = {}
- this.fixturesNodes_ = []
- this.fixturesPath = 'spec/javascripts/fixtures'
-}
-
-jasmine.StyleFixtures.prototype.set = function(css) {
- this.cleanUp()
- this.createStyle_(css)
-}
-
-jasmine.StyleFixtures.prototype.appendSet = function(css) {
- this.createStyle_(css)
-}
-
-jasmine.StyleFixtures.prototype.preload = function() {
- this.read_.apply(this, arguments)
-}
-
-jasmine.StyleFixtures.prototype.load = function() {
- this.cleanUp()
- this.createStyle_(this.read_.apply(this, arguments))
-}
-
-jasmine.StyleFixtures.prototype.appendLoad = function() {
- this.createStyle_(this.read_.apply(this, arguments))
-}
-
-jasmine.StyleFixtures.prototype.cleanUp = function() {
- while(this.fixturesNodes_.length) {
- this.fixturesNodes_.pop().remove()
- }
-}
-
-jasmine.StyleFixtures.prototype.createStyle_ = function(html) {
- var styleText = $('').html(html).text(),
- style = $('')
-
- this.fixturesNodes_.push(style)
-
- $('head').append(style)
-}
-
-jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache
-
-jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read
-
-jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_
-
-jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_
-
-jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_
-
-jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_
-
-jasmine.getJSONFixtures = function() {
- return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures()
-}
-
-jasmine.JSONFixtures = function() {
- this.fixturesCache_ = {}
- this.fixturesPath = 'spec/javascripts/fixtures/json'
-}
-
-jasmine.JSONFixtures.prototype.load = function() {
- this.read.apply(this, arguments)
- return this.fixturesCache_
-}
-
-jasmine.JSONFixtures.prototype.read = function() {
- var fixtureUrls = arguments
- for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
- this.getFixtureData_(fixtureUrls[urlIndex])
- }
- return this.fixturesCache_
-}
-
-jasmine.JSONFixtures.prototype.clearCache = function() {
- this.fixturesCache_ = {}
-}
-
-jasmine.JSONFixtures.prototype.getFixtureData_ = function(url) {
- this.loadFixtureIntoCache_(url)
- return this.fixturesCache_[url]
-}
-
-jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
- var self = this
- var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
- $.ajax({
- async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
- cache: false,
- dataType: 'json',
- url: url,
- success: function(data) {
- self.fixturesCache_[relativeUrl] = data
- },
- error: function(jqXHR, status, errorThrown) {
- throw Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
+ jasmine.spiedEventsKey = function (selector, eventName) {
+ return [$(selector).selector, eventName].toString()
}
- })
-}
-jasmine.JSONFixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
- return this[methodName].apply(this, passedArguments)
-}
+ jasmine.getFixtures = function () {
+ return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures()
+ }
-jasmine.JQuery = function() {}
+ jasmine.getStyleFixtures = function () {
+ return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures()
+ }
-jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
- return $('').append(html).html()
-}
+ jasmine.Fixtures = function () {
+ this.containerId = 'jasmine-fixtures'
+ this.fixturesCache_ = {}
+ this.fixturesPath = 'spec/javascripts/fixtures'
+ }
-jasmine.JQuery.elementToString = function(element) {
- var domEl = $(element).get(0)
- if (domEl == undefined || domEl.cloneNode)
- return $('').append($(element).clone()).html()
- else
- return element.toString()
-}
+ jasmine.Fixtures.prototype.set = function (html) {
+ this.cleanUp()
+ return this.createContainer_(html)
+ }
-jasmine.JQuery.matchersClass = {}
+ jasmine.Fixtures.prototype.appendSet= function (html) {
+ this.addToContainer_(html)
+ }
-!function(namespace) {
- var data = {
- spiedEvents: {},
- handlers: []
- }
+ jasmine.Fixtures.prototype.preload = function () {
+ this.read.apply(this, arguments)
+ }
- namespace.events = {
- spyOn: function(selector, eventName) {
- var handler = function(e) {
- data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = e
- }
- $(selector).bind(eventName, handler)
- data.handlers.push(handler)
- return {
- selector: selector,
- eventName: eventName,
- handler: handler,
- reset: function(){
- delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
+ jasmine.Fixtures.prototype.load = function () {
+ this.cleanUp()
+ this.createContainer_(this.read.apply(this, arguments))
+ }
+
+ jasmine.Fixtures.prototype.appendLoad = function () {
+ this.addToContainer_(this.read.apply(this, arguments))
+ }
+
+ jasmine.Fixtures.prototype.read = function () {
+ var htmlChunks = []
+ , fixtureUrls = arguments
+
+ for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
+ htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]))
}
- }
- },
- wasTriggered: function(selector, eventName) {
- return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)])
- },
-
- wasPrevented: function(selector, eventName) {
- return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].isDefaultPrevented()
- },
-
- cleanUp: function() {
- data.spiedEvents = {}
- data.handlers = []
+ return htmlChunks.join('')
}
- }
-}(jasmine.JQuery)
-!function(){
- var jQueryMatchers = {
- toHaveClass: function(className) {
- return this.actual.hasClass(className)
- },
+ jasmine.Fixtures.prototype.clearCache = function () {
+ this.fixturesCache_ = {}
+ }
- toHaveCss: function(css){
- for (var prop in css){
- if (this.actual.css(prop) !== css[prop]) return false
- }
- return true
- },
+ jasmine.Fixtures.prototype.cleanUp = function () {
+ $('#' + this.containerId).remove()
+ }
- toBeVisible: function() {
- return this.actual.is(':visible')
- },
+ jasmine.Fixtures.prototype.sandbox = function (attributes) {
+ var attributesToSet = attributes || {}
+ return $('').attr(attributesToSet)
+ }
- toBeHidden: function() {
- return this.actual.is(':hidden')
- },
+ jasmine.Fixtures.prototype.createContainer_ = function (html) {
+ var container = $('')
+ .attr('id', this.containerId)
+ .html(html)
- toBeSelected: function() {
- return this.actual.is(':selected')
- },
+ $(document.body).append(container)
+ return container
+ }
- toBeChecked: function() {
- return this.actual.is(':checked')
- },
-
- toBeEmpty: function() {
- return this.actual.is(':empty')
- },
-
- toExist: function() {
- return $(document).find(this.actual).length
- },
-
- toHaveAttr: function(attributeName, expectedAttributeValue) {
- return hasProperty(this.actual.attr(attributeName), expectedAttributeValue)
- },
-
- toHaveProp: function(propertyName, expectedPropertyValue) {
- return hasProperty(this.actual.prop(propertyName), expectedPropertyValue)
- },
-
- toHaveId: function(id) {
- return this.actual.attr('id') == id
- },
-
- toHaveHtml: function(html) {
- return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html)
- },
-
- toContainHtml: function(html){
- var actualHtml = this.actual.html()
- var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html)
- return (actualHtml.indexOf(expectedHtml) >= 0)
- },
-
- toHaveText: function(text) {
- var trimmedText = $.trim(this.actual.text())
- if (text && $.isFunction(text.test)) {
- return text.test(trimmedText)
- } else {
- return trimmedText == text
- }
- },
-
- toHaveValue: function(value) {
- return this.actual.val() == value
- },
-
- toHaveData: function(key, expectedValue) {
- return hasProperty(this.actual.data(key), expectedValue)
- },
-
- toBe: function(selector) {
- return this.actual.is(selector)
- },
-
- toContain: function(selector) {
- return this.actual.find(selector).length
- },
-
- toBeDisabled: function(selector){
- return this.actual.is(':disabled')
- },
-
- toBeFocused: function(selector) {
- return this.actual.is(':focus')
- },
-
- toHandle: function(event) {
-
- var events = $._data(this.actual.get(0), "events")
-
- if(!events || !event || typeof event !== "string") {
- return false
- }
-
- var namespaces = event.split(".")
- var eventType = namespaces.shift()
- var sortedNamespaces = namespaces.slice(0).sort()
- var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)")
-
- if(events[eventType] && namespaces.length) {
- for(var i = 0; i < events[eventType].length; i++) {
- var namespace = events[eventType][i].namespace
- if(namespaceRegExp.test(namespace)) {
- return true
- }
+ jasmine.Fixtures.prototype.addToContainer_ = function (html){
+ var container = $(document.body).find('#'+this.containerId).append(html)
+ if(!container.length){
+ this.createContainer_(html)
}
- } else {
- return events[eventType] && events[eventType].length > 0
- }
- },
-
- // tests the existence of a specific event binding + handler
- toHandleWith: function(eventName, eventHandler) {
- var stack = $._data(this.actual.get(0), "events")[eventName]
- for (var i = 0; i < stack.length; i++) {
- if (stack[i].handler == eventHandler) return true
- }
- return false
}
- }
- var hasProperty = function(actualValue, expectedValue) {
- if (expectedValue === undefined) return actualValue !== undefined
- return actualValue == expectedValue
- }
-
- var bindMatcher = function(methodName) {
- var builtInMatcher = jasmine.Matchers.prototype[methodName]
-
- jasmine.JQuery.matchersClass[methodName] = function() {
- if (this.actual
- && (this.actual instanceof $
- || jasmine.isDomNode(this.actual))) {
- this.actual = $(this.actual)
- var result = jQueryMatchers[methodName].apply(this, arguments)
- var element
- if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML")
- this.actual = jasmine.JQuery.elementToString(this.actual)
- return result
- }
-
- if (builtInMatcher) {
- return builtInMatcher.apply(this, arguments)
- }
-
- return false
+ jasmine.Fixtures.prototype.getFixtureHtml_ = function (url) {
+ if (typeof this.fixturesCache_[url] === 'undefined') {
+ this.loadFixtureIntoCache_(url)
+ }
+ return this.fixturesCache_[url]
}
- }
- for(var methodName in jQueryMatchers) {
- bindMatcher(methodName)
- }
-}()
+ jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) {
+ var self = this
+ , url = this.makeFixtureUrl_(relativeUrl)
+ , request = $.ajax({
+ async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
+ cache: false,
+ url: url,
+ success: function (data, status, $xhr) {
+ self.fixturesCache_[relativeUrl] = $xhr.responseText
+ },
+ error: function (jqXHR, status, errorThrown) {
+ throw new Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
+ }
+ })
+ }
-beforeEach(function() {
- this.addMatchers(jasmine.JQuery.matchersClass)
- this.addMatchers({
- toHaveBeenTriggeredOn: function(selector) {
- this.message = function() {
- return [
- "Expected event " + this.actual + " to have been triggered on " + selector,
- "Expected event " + this.actual + " not to have been triggered on " + selector
- ]
- }
- return jasmine.JQuery.events.wasTriggered(selector, this.actual)
+ jasmine.Fixtures.prototype.makeFixtureUrl_ = function (relativeUrl){
+ return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
}
- })
- this.addMatchers({
- toHaveBeenTriggered: function(){
- var eventName = this.actual.eventName,
- selector = this.actual.selector
- this.message = function() {
- return [
- "Expected event " + eventName + " to have been triggered on " + selector,
- "Expected event " + eventName + " not to have been triggered on " + selector
- ]
- }
- return jasmine.JQuery.events.wasTriggered(selector, eventName)
- }
- })
- this.addMatchers({
- toHaveBeenPreventedOn: function(selector) {
- this.message = function() {
- return [
- "Expected event " + this.actual + " to have been prevented on " + selector,
- "Expected event " + this.actual + " not to have been prevented on " + selector
- ]
- }
- return jasmine.JQuery.events.wasPrevented(selector, this.actual)
- }
- })
- this.addMatchers({
- toHaveBeenPrevented: function() {
- var eventName = this.actual.eventName,
- selector = this.actual.selector
- this.message = function() {
- return [
- "Expected event " + eventName + " to have been prevented on " + selector,
- "Expected event " + eventName + " not to have been prevented on " + selector
- ]
- }
- return jasmine.JQuery.events.wasPrevented(selector, eventName)
- }
- })
-})
-afterEach(function() {
- jasmine.getFixtures().cleanUp()
- jasmine.getStyleFixtures().cleanUp()
- jasmine.JQuery.events.cleanUp()
-})
+ jasmine.Fixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) {
+ return this[methodName].apply(this, passedArguments)
+ }
+
+
+ jasmine.StyleFixtures = function () {
+ this.fixturesCache_ = {}
+ this.fixturesNodes_ = []
+ this.fixturesPath = 'spec/javascripts/fixtures'
+ }
+
+ jasmine.StyleFixtures.prototype.set = function (css) {
+ this.cleanUp()
+ this.createStyle_(css)
+ }
+
+ jasmine.StyleFixtures.prototype.appendSet = function (css) {
+ this.createStyle_(css)
+ }
+
+ jasmine.StyleFixtures.prototype.preload = function () {
+ this.read_.apply(this, arguments)
+ }
+
+ jasmine.StyleFixtures.prototype.load = function () {
+ this.cleanUp()
+ this.createStyle_(this.read_.apply(this, arguments))
+ }
+
+ jasmine.StyleFixtures.prototype.appendLoad = function () {
+ this.createStyle_(this.read_.apply(this, arguments))
+ }
+
+ jasmine.StyleFixtures.prototype.cleanUp = function () {
+ while(this.fixturesNodes_.length) {
+ this.fixturesNodes_.pop().remove()
+ }
+ }
+
+ jasmine.StyleFixtures.prototype.createStyle_ = function (html) {
+ var styleText = $('
').html(html).text()
+ , style = $('')
+
+ this.fixturesNodes_.push(style)
+ $('head').append(style)
+ }
+
+ jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache
+ jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read
+ jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_
+ jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_
+ jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_
+ jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_
+
+ jasmine.getJSONFixtures = function () {
+ return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures()
+ }
+
+ jasmine.JSONFixtures = function () {
+ this.fixturesCache_ = {}
+ this.fixturesPath = 'spec/javascripts/fixtures/json'
+ }
+
+ jasmine.JSONFixtures.prototype.load = function () {
+ this.read.apply(this, arguments)
+ return this.fixturesCache_
+ }
+
+ jasmine.JSONFixtures.prototype.read = function () {
+ var fixtureUrls = arguments
+
+ for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
+ this.getFixtureData_(fixtureUrls[urlIndex])
+ }
+
+ return this.fixturesCache_
+ }
+
+ jasmine.JSONFixtures.prototype.clearCache = function () {
+ this.fixturesCache_ = {}
+ }
+
+ jasmine.JSONFixtures.prototype.getFixtureData_ = function (url) {
+ if (!this.fixturesCache_[url]) this.loadFixtureIntoCache_(url)
+ return this.fixturesCache_[url]
+ }
+
+ jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function (relativeUrl) {
+ var self = this
+ , url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl
+
+ $.ajax({
+ async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
+ cache: false,
+ dataType: 'json',
+ url: url,
+ success: function (data) {
+ self.fixturesCache_[relativeUrl] = data
+ },
+ error: function (jqXHR, status, errorThrown) {
+ throw new Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')')
+ }
+ })
+ }
+
+ jasmine.JSONFixtures.prototype.proxyCallTo_ = function (methodName, passedArguments) {
+ return this[methodName].apply(this, passedArguments)
+ }
+
+ jasmine.JQuery = function () {}
+
+ jasmine.JQuery.browserTagCaseIndependentHtml = function (html) {
+ return $('
').append(html).html()
+ }
+
+ jasmine.JQuery.elementToString = function (element) {
+ return $(element).map(function () { return this.outerHTML; }).toArray().join(', ')
+ }
+
+ jasmine.JQuery.matchersClass = {}
+
+ !function (namespace) {
+ var data = {
+ spiedEvents: {}
+ , handlers: []
+ }
+
+ namespace.events = {
+ spyOn: function (selector, eventName) {
+ var handler = function (e) {
+ data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = jasmine.util.argsToArray(arguments)
+ }
+
+ $(selector).on(eventName, handler)
+ data.handlers.push(handler)
+
+ return {
+ selector: selector,
+ eventName: eventName,
+ handler: handler,
+ reset: function (){
+ delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
+ }
+ }
+ },
+
+ args: function (selector, eventName) {
+ var actualArgs = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
+
+ if (!actualArgs) {
+ throw "There is no spy for " + eventName + " on " + selector.toString() + ". Make sure to create a spy using spyOnEvent."
+ }
+
+ return actualArgs
+ },
+
+ wasTriggered: function (selector, eventName) {
+ return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)])
+ },
+
+ wasTriggeredWith: function (selector, eventName, expectedArgs, env) {
+ var actualArgs = jasmine.JQuery.events.args(selector, eventName).slice(1)
+ if (Object.prototype.toString.call(expectedArgs) !== '[object Array]') {
+ actualArgs = actualArgs[0]
+ }
+ return env.equals_(expectedArgs, actualArgs)
+ },
+
+ wasPrevented: function (selector, eventName) {
+ var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
+ , e = args ? args[0] : undefined
+
+ return e && e.isDefaultPrevented()
+ },
+
+ wasStopped: function (selector, eventName) {
+ var args = data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]
+ , e = args ? args[0] : undefined
+ return e && e.isPropagationStopped()
+ },
+
+ cleanUp: function () {
+ data.spiedEvents = {}
+ data.handlers = []
+ }
+ }
+ }(jasmine.JQuery)
+
+ !function (){
+ var jQueryMatchers = {
+ toHaveClass: function (className) {
+ return this.actual.hasClass(className)
+ },
+
+ toHaveCss: function (css){
+ for (var prop in css){
+ var value = css[prop]
+ // see issue #147 on gh
+ ;if (value === 'auto' && this.actual.get(0).style[prop] === 'auto') continue
+ if (this.actual.css(prop) !== value) return false
+ }
+ return true
+ },
+
+ toBeVisible: function () {
+ return this.actual.is(':visible')
+ },
+
+ toBeHidden: function () {
+ return this.actual.is(':hidden')
+ },
+
+ toBeSelected: function () {
+ return this.actual.is(':selected')
+ },
+
+ toBeChecked: function () {
+ return this.actual.is(':checked')
+ },
+
+ toBeEmpty: function () {
+ return this.actual.is(':empty')
+ },
+
+ toExist: function () {
+ return this.actual.length
+ },
+
+ toHaveLength: function (length) {
+ return this.actual.length === length
+ },
+
+ toHaveAttr: function (attributeName, expectedAttributeValue) {
+ return hasProperty(this.actual.attr(attributeName), expectedAttributeValue)
+ },
+
+ toHaveProp: function (propertyName, expectedPropertyValue) {
+ return hasProperty(this.actual.prop(propertyName), expectedPropertyValue)
+ },
+
+ toHaveId: function (id) {
+ return this.actual.attr('id') == id
+ },
+
+ toHaveHtml: function (html) {
+ return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html)
+ },
+
+ toContainHtml: function (html){
+ var actualHtml = this.actual.html()
+ , expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html)
+
+ return (actualHtml.indexOf(expectedHtml) >= 0)
+ },
+
+ toHaveText: function (text) {
+ var trimmedText = $.trim(this.actual.text())
+
+ if (text && $.isFunction(text.test)) {
+ return text.test(trimmedText)
+ } else {
+ return trimmedText == text
+ }
+ },
+
+ toContainText: function (text) {
+ var trimmedText = $.trim(this.actual.text())
+
+ if (text && $.isFunction(text.test)) {
+ return text.test(trimmedText)
+ } else {
+ return trimmedText.indexOf(text) != -1
+ }
+ },
+
+ toHaveValue: function (value) {
+ return this.actual.val() === value
+ },
+
+ toHaveData: function (key, expectedValue) {
+ return hasProperty(this.actual.data(key), expectedValue)
+ },
+
+ toBe: function (selector) {
+ return this.actual.is(selector)
+ },
+
+ toContain: function (selector) {
+ return this.actual.find(selector).length
+ },
+
+ toBeMatchedBy: function (selector) {
+ return this.actual.filter(selector).length
+ },
+
+ toBeDisabled: function (selector){
+ return this.actual.is(':disabled')
+ },
+
+ toBeFocused: function (selector) {
+ return this.actual[0] === this.actual[0].ownerDocument.activeElement
+ },
+
+ toHandle: function (event) {
+ var events = $._data(this.actual.get(0), "events")
+
+ if(!events || !event || typeof event !== "string") {
+ return false
+ }
+
+ var namespaces = event.split(".")
+ , eventType = namespaces.shift()
+ , sortedNamespaces = namespaces.slice(0).sort()
+ , namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)")
+
+ if(events[eventType] && namespaces.length) {
+ for(var i = 0; i < events[eventType].length; i++) {
+ var namespace = events[eventType][i].namespace
+
+ if(namespaceRegExp.test(namespace)) {
+ return true
+ }
+ }
+ } else {
+ return events[eventType] && events[eventType].length > 0
+ }
+ },
+
+ toHandleWith: function (eventName, eventHandler) {
+ var normalizedEventName = eventName.split('.')[0]
+ , stack = $._data(this.actual.get(0), "events")[normalizedEventName]
+
+ for (var i = 0; i < stack.length; i++) {
+ if (stack[i].handler == eventHandler) return true
+ }
+
+ return false
+ }
+ }
+
+ var hasProperty = function (actualValue, expectedValue) {
+ if (expectedValue === undefined) return actualValue !== undefined
+
+ return actualValue == expectedValue
+ }
+
+ var bindMatcher = function (methodName) {
+ var builtInMatcher = jasmine.Matchers.prototype[methodName]
+
+ jasmine.JQuery.matchersClass[methodName] = function () {
+ if (this.actual
+ && (this.actual instanceof $
+ || jasmine.isDomNode(this.actual))) {
+ this.actual = $(this.actual)
+ var result = jQueryMatchers[methodName].apply(this, arguments)
+ , element
+
+ if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML")
+ this.actual = jasmine.JQuery.elementToString(this.actual)
+
+ return result
+ }
+
+ if (builtInMatcher) {
+ return builtInMatcher.apply(this, arguments)
+ }
+
+ return false
+ }
+ }
+
+ for(var methodName in jQueryMatchers) {
+ bindMatcher(methodName)
+ }
+ }()
+
+ beforeEach(function () {
+ this.addMatchers(jasmine.JQuery.matchersClass)
+ this.addMatchers({
+ toHaveBeenTriggeredOn: function (selector) {
+ this.message = function () {
+ return [
+ "Expected event " + this.actual + " to have been triggered on " + selector,
+ "Expected event " + this.actual + " not to have been triggered on " + selector
+ ]
+ }
+ return jasmine.JQuery.events.wasTriggered(selector, this.actual)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenTriggered: function (){
+ var eventName = this.actual.eventName
+ , selector = this.actual.selector
+
+ this.message = function () {
+ return [
+ "Expected event " + eventName + " to have been triggered on " + selector,
+ "Expected event " + eventName + " not to have been triggered on " + selector
+ ]
+ }
+
+ return jasmine.JQuery.events.wasTriggered(selector, eventName)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenTriggeredOnAndWith: function () {
+ var selector = arguments[0]
+ , expectedArgs = arguments[1]
+ , wasTriggered = jasmine.JQuery.events.wasTriggered(selector, this.actual)
+
+ this.message = function () {
+ if (wasTriggered) {
+ var actualArgs = jasmine.JQuery.events.args(selector, this.actual, expectedArgs)[1]
+ return [
+ "Expected event " + this.actual + " to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs),
+ "Expected event " + this.actual + " not to have been triggered with " + jasmine.pp(expectedArgs) + " but it was triggered with " + jasmine.pp(actualArgs)
+ ]
+ } else {
+ return [
+ "Expected event " + this.actual + " to have been triggered on " + selector,
+ "Expected event " + this.actual + " not to have been triggered on " + selector
+ ]
+ }
+ }
+
+ return wasTriggered && jasmine.JQuery.events.wasTriggeredWith(selector, this.actual, expectedArgs, this.env)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenPreventedOn: function (selector) {
+ this.message = function () {
+ return [
+ "Expected event " + this.actual + " to have been prevented on " + selector,
+ "Expected event " + this.actual + " not to have been prevented on " + selector
+ ]
+ }
+
+ return jasmine.JQuery.events.wasPrevented(selector, this.actual)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenPrevented: function () {
+ var eventName = this.actual.eventName
+ , selector = this.actual.selector
+ this.message = function () {
+ return [
+ "Expected event " + eventName + " to have been prevented on " + selector,
+ "Expected event " + eventName + " not to have been prevented on " + selector
+ ]
+ }
+
+ return jasmine.JQuery.events.wasPrevented(selector, eventName)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenStoppedOn: function (selector) {
+ this.message = function () {
+ return [
+ "Expected event " + this.actual + " to have been stopped on " + selector,
+ "Expected event " + this.actual + " not to have been stopped on " + selector
+ ]
+ }
+
+ return jasmine.JQuery.events.wasStopped(selector, this.actual)
+ }
+ })
+ this.addMatchers({
+ toHaveBeenStopped: function () {
+ var eventName = this.actual.eventName
+ , selector = this.actual.selector
+ this.message = function () {
+ return [
+ "Expected event " + eventName + " to have been stopped on " + selector,
+ "Expected event " + eventName + " not to have been stopped on " + selector
+ ]
+ }
+ return jasmine.JQuery.events.wasStopped(selector, eventName)
+ }
+ })
+ jasmine.getEnv().addEqualityTester(function (a, b) {
+ if(a instanceof jQuery && b instanceof jQuery) {
+ if(a.size() != b.size()) {
+ return jasmine.undefined
+ }
+ else if(a.is(b)) {
+ return true
+ }
+ }
+
+ return jasmine.undefined
+ })
+ })
+
+ afterEach(function () {
+ jasmine.getFixtures().cleanUp()
+ jasmine.getStyleFixtures().cleanUp()
+ jasmine.JQuery.events.cleanUp()
+ })
+}(window.jasmine, window.jQuery)
+
++function (jasmine, global) { "use strict";
+
+ global.readFixtures = function () {
+ return jasmine.getFixtures().proxyCallTo_('read', arguments)
+ }
+
+ global.preloadFixtures = function () {
+ jasmine.getFixtures().proxyCallTo_('preload', arguments)
+ }
+
+ global.loadFixtures = function () {
+ jasmine.getFixtures().proxyCallTo_('load', arguments)
+ }
+
+ global.appendLoadFixtures = function () {
+ jasmine.getFixtures().proxyCallTo_('appendLoad', arguments)
+ }
+
+ global.setFixtures = function (html) {
+ return jasmine.getFixtures().proxyCallTo_('set', arguments)
+ }
+
+ global.appendSetFixtures = function () {
+ jasmine.getFixtures().proxyCallTo_('appendSet', arguments)
+ }
+
+ global.sandbox = function (attributes) {
+ return jasmine.getFixtures().sandbox(attributes)
+ }
+
+ global.spyOnEvent = function (selector, eventName) {
+ return jasmine.JQuery.events.spyOn(selector, eventName)
+ }
+
+ global.preloadStyleFixtures = function () {
+ jasmine.getStyleFixtures().proxyCallTo_('preload', arguments)
+ }
+
+ global.loadStyleFixtures = function () {
+ jasmine.getStyleFixtures().proxyCallTo_('load', arguments)
+ }
+
+ global.appendLoadStyleFixtures = function () {
+ jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments)
+ }
+
+ global.setStyleFixtures = function (html) {
+ jasmine.getStyleFixtures().proxyCallTo_('set', arguments)
+ }
+
+ global.appendSetStyleFixtures = function (html) {
+ jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments)
+ }
+
+ global.loadJSONFixtures = function () {
+ return jasmine.getJSONFixtures().proxyCallTo_('load', arguments)
+ }
+
+ global.getJSONFixture = function (url) {
+ return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url]
+ }
+}(jasmine, window);
\ No newline at end of file
diff --git a/web/spec/javascripts/recordingModel.spec.js b/web/spec/javascripts/recordingModel.spec.js
new file mode 100644
index 000000000..6b7111697
--- /dev/null
+++ b/web/spec/javascripts/recordingModel.spec.js
@@ -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));
\ No newline at end of file
diff --git a/web/spec/javascripts/searcher.spec.js b/web/spec/javascripts/searcher.spec.js
index 5cdc368fc..514fd689e 100644
--- a/web/spec/javascripts/searcher.spec.js
+++ b/web/spec/javascripts/searcher.spec.js
@@ -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() {
diff --git a/web/spec/javascripts/vuHelpers.spec.js b/web/spec/javascripts/vuHelpers.spec.js
index a58e28c20..1bc5df8ed 100644
--- a/web/spec/javascripts/vuHelpers.spec.js
+++ b/web/spec/javascripts/vuHelpers.spec.js
@@ -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() {
diff --git a/web/spec/managers/music_session_manager_spec.rb b/web/spec/managers/music_session_manager_spec.rb
index f01155bce..7ddb3a2a1 100644
--- a/web/spec/managers/music_session_manager_spec.rb
+++ b/web/spec/managers/music_session_manager_spec.rb
@@ -13,7 +13,7 @@ describe MusicSessionManager do
@band = FactoryGirl.create(:band)
@genre = FactoryGirl.create(:genre)
@instrument = FactoryGirl.create(:instrument)
- @tracks = [{"instrument_id" => @instrument.id, "sound" => "mono"}]
+ @tracks = [{"instrument_id" => @instrument.id, "sound" => "mono", "client_track_id" => "abcd"}]
@connection = FactoryGirl.create(:connection, :user => @user)
end
diff --git a/web/spec/requests/music_sessions_api_spec.rb b/web/spec/requests/music_sessions_api_spec.rb
index 36bb33114..e4703e064 100755
--- a/web/spec/requests/music_sessions_api_spec.rb
+++ b/web/spec/requests/music_sessions_api_spec.rb
@@ -23,7 +23,7 @@ describe "Music Session API ", :type => :api do
let(:user) { FactoryGirl.create(:user) }
# defopts are used to setup default options for the session
- let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}], :legal_terms => true, :intellectual_property => true} }
+ let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}], :legal_terms => true, :intellectual_property => true} }
before do
#sign_in user
MusicSession.delete_all
@@ -115,7 +115,7 @@ describe "Music Session API ", :type => :api do
# create a 2nd track for this session
conn_id = updated_track["connection_id"]
- post "/api/sessions/#{music_session["id"]}/tracks.json", { :connection_id => "#{conn_id}", :instrument_id => "electric guitar", :sound => "mono" }.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/tracks.json", { :connection_id => "#{conn_id}", :instrument_id => "electric guitar", :sound => "mono", :client_track_id => "client_track_guid" }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should == 201
get "/api/sessions/#{music_session["id"]}/tracks.json", "CONTENT_TYPE" => 'application/json'
@@ -239,7 +239,7 @@ describe "Music Session API ", :type => :api do
musician["client_id"].should == client.client_id
login(user2)
- post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
@@ -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
@@ -311,7 +311,7 @@ describe "Music Session API ", :type => :api do
original_count = MusicSession.all().length
client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1")
- post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "mom", "sound" => "mono"}]}).to_json, "CONTENT_TYPE" => 'application/json'
+ post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "mom", "sound" => "mono", "client_track_id" => "client_track_guid"}]}).to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(404)
# check that the transaction was rolled back
@@ -322,7 +322,7 @@ describe "Music Session API ", :type => :api do
original_count = MusicSession.all().length
client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1")
- post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mom"}]}).to_json, "CONTENT_TYPE" => 'application/json'
+ post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mom", "client_track_id" => "client_track_guid"}]}).to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
JSON.parse(last_response.body)["errors"]["tracks"][0].should == "is invalid"
@@ -411,10 +411,10 @@ describe "Music Session API ", :type => :api do
# users are friends, but no invitation... so we shouldn't be able to join as user 2
login(user2)
- post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}] }.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
join_response = JSON.parse(last_response.body)
- join_response["errors"]["musician_access"].should == [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)
@@ -422,7 +422,7 @@ describe "Music Session API ", :type => :api do
last_response.status.should eql(201)
login(user2)
- post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}] }.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
end
@@ -492,10 +492,10 @@ describe "Music Session API ", :type => :api do
client2 = FactoryGirl.create(:connection, :user => user2, :ip_address => "2.2.2.2")
login(user2)
- post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
rejected_join_attempt = JSON.parse(last_response.body)
- rejected_join_attempt["errors"]["approval_required"] = [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)
@@ -514,7 +514,7 @@ describe "Music Session API ", :type => :api do
# finally, go back to user2 and attempt to join again
login(user2)
- post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
end
@@ -544,7 +544,7 @@ describe "Music Session API ", :type => :api do
track["sound"].should == "mono"
- post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
@@ -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", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
+ last_response.status.should eql(422)
+ JSON.parse(last_response.body)["errors"]["music_session"][0].should == ValidationMessages::CANT_JOIN_RECORDING_SESSION
+ end
end
it "Finds a single open session" do
@@ -607,9 +634,36 @@ describe "Music Session API ", :type => :api do
last_response.status.should == 200
msuh.reload
msuh.rating.should == 0
-
end
+ it "track sync" do
+ user = FactoryGirl.create(:single_user_session)
+ instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
+ music_session = FactoryGirl.create(:music_session, :creator => user)
+ client = FactoryGirl.create(:connection, :user => user, :music_session => music_session)
+ track = FactoryGirl.create(:track, :connection => client, :instrument => instrument)
+
+ existing_track = {:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id }
+ new_track = {:client_track_id => "client_track_id1", :instrument_id => instrument.id, :sound => 'stereo'}
+ # let's add a new track, and leave the existing one alone
+ tracks = [existing_track, new_track]
+ login(user)
+
+ put "/api/sessions/#{music_session.id}/tracks.json", { :client_id => client.client_id, :tracks => tracks }.to_json, "CONTENT_TYPE" => "application/json"
+ last_response.status.should == 204
+
+ get "/api/sessions/#{music_session.id}/tracks.json", "CONTENT_TYPE" => 'application/json'
+ last_response.status.should == 200
+ tracks = JSON.parse(last_response.body)
+ tracks.size.should == 2
+ tracks[0]["id"].should == track.id
+ tracks[0]["instrument_id"].should == instrument.id
+ tracks[0]["sound"].should == "mono"
+ tracks[0]["client_track_id"].should == track.client_track_id
+ tracks[1]["instrument_id"].should == instrument.id
+ tracks[1]["sound"].should == "stereo"
+ tracks[1]["client_track_id"].should == "client_track_id1"
+ end
end
diff --git a/web/spec/requests/user_progression_spec.rb b/web/spec/requests/user_progression_spec.rb
index cff28581d..ec1fd399a 100644
--- a/web/spec/requests/user_progression_spec.rb
+++ b/web/spec/requests/user_progression_spec.rb
@@ -17,7 +17,7 @@ describe "User Progression", :type => :api do
describe "user progression" do
let(:user) { FactoryGirl.create(:user) }
- let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}], :legal_terms => true, :intellectual_property => true} }
+ let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}], :legal_terms => true, :intellectual_property => true} }
before do
login(user)
@@ -105,11 +105,11 @@ describe "User Progression", :type => :api do
music_session = JSON.parse(last_response.body)
login(user2)
- post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
login(user3)
- post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client3.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
+ post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client3.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
# instrument the created_at of the music_history field to be at the beginning of time, so that we cross the 15 minute threshold of a 'real session
diff --git a/web/spec/spec_helper.rb b/web/spec/spec_helper.rb
index 512f00f07..f3ce732c4 100644
--- a/web/spec/spec_helper.rb
+++ b/web/spec/spec_helper.rb
@@ -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
diff --git a/web/spec/support/utilities.rb b/web/spec/support/utilities.rb
index 2db6636c5..7c8134e78 100644
--- a/web/spec/support/utilities.rb
+++ b/web/spec/support/utilities.rb
@@ -1,7 +1,7 @@
include ApplicationHelper
def cookie_jar
- Capybara.current_session.driver.browser.current_session.instance_variable_get(:@rack_mock_session).cookie_jar
+ Capybara.current_session.driver.browser.current_session.instance_variable_get(:@rack_mock_session).cookie_jar
end
@@ -69,15 +69,15 @@ def wait_until_user(wait=Capybara.default_wait_time)
wait = wait * 10 #(because we sleep .1)
counter = 0
- # while page.execute_script("$('.curtain').is(:visible)") == "true"
- # counter += 1
- # sleep(0.1)
- # raise "Waiting for user to populate took longer than #{wait} seconds." if counter >= wait
- # end
+ # while page.execute_script("$('.curtain').is(:visible)") == "true"
+ # counter += 1
+ # sleep(0.1)
+ # raise "Waiting for user to populate took longer than #{wait} seconds." if counter >= wait
+ # end
end
def wait_until_curtain_gone
- should have_no_selector('.curtain')
+ should have_no_selector('.curtain')
end
def wait_to_see_my_track
diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb
index 03e2208b1..863d70842 100644
--- a/websocket-gateway/lib/jam_websockets/router.rb
+++ b/websocket-gateway/lib/jam_websockets/router.rb
@@ -61,6 +61,7 @@ module JamWebsockets
end
def add_client(client_id, client_context)
+ @log.debug "adding client #{client_id} to @client_lookup"
@client_lookup[client_id] = client_context
end
@@ -172,20 +173,26 @@ module JamWebsockets
client_id = routing_key["client.".length..-1]
@semaphore.synchronize do
client_context = @client_lookup[client_id]
- client = client_context.client
- msg = Jampb::ClientMessage.parse(msg)
+ if !client_context.nil?
- @log.debug "client-directed message received from #{msg.from} to client #{client_id}"
+ client = client_context.client
- unless client.nil?
+ msg = Jampb::ClientMessage.parse(msg)
- EM.schedule do
- @log.debug "sending client-directed down websocket to #{client_id}"
- send_to_client(client, msg)
+ @log.debug "client-directed message received from #{msg.from} to client #{client_id}"
+
+ unless client.nil?
+
+ EM.schedule do
+ @log.debug "sending client-directed down websocket to #{client_id}"
+ send_to_client(client, msg)
+ end
+ else
+ @log.debug "client-directed message unroutable to disconnected client #{client_id}"
end
else
- @log.debug "client-directed message unroutable to disconnected client #{client_id}"
+ @log.debug "Can't route message: no client connected with id #{client_id}"
end
end
rescue => e
@@ -342,7 +349,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
@@ -475,7 +484,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?
@@ -520,25 +533,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"
@@ -653,6 +647,10 @@ module JamWebsockets
# belong to
access_p2p(to_client_id, context.user, client_msg)
+ if to_client_id.nil? || to_client_id == 'undefined' # javascript translates to 'undefined' in many cases
+ raise SessionError, "empty client_id specified in peer-to-peer message"
+ end
+
# populate routing data
client_msg.from = client.client_id
diff --git a/websocket-gateway/lib/jam_websockets/server.rb b/websocket-gateway/lib/jam_websockets/server.rb
index d12cf5f37..4b2b299f2 100644
--- a/websocket-gateway/lib/jam_websockets/server.rb
+++ b/websocket-gateway/lib/jam_websockets/server.rb
@@ -20,6 +20,7 @@ module JamWebsockets
@log.info "starting server #{host}:#{port} with staleness_time=#{connect_time_stale}; reconnect time = #{connect_time_expire}"
EventMachine.error_handler{|e|
+ @log.error "unhandled error #{e}"
Bugsnag.notify(e)
}