merge develop

This commit is contained in:
Brian Smith 2014-06-11 08:45:34 -04:00
commit 5fc9696d01
108 changed files with 5816 additions and 2754 deletions

View File

@ -99,7 +99,7 @@ gem 'debugger'
group :development, :test do
gem 'capybara'
gem 'rspec-rails'
gem 'rspec-rails', '2.14.2'
gem 'guard-rspec', '0.5.5'
gem 'jasmine', '1.3.1'
gem 'execjs', '1.4.0'

View File

@ -169,4 +169,6 @@ periodic_emails.sql
remember_extra_scoring_data.sql
indexing_for_regions.sql
latency_tester.sql
scheduled_sessions_next_session_scheduled.sql
scheduled_sessions_next_session_scheduled.sql
fix_users_location_fields.sql
audio_latency.sql

2
db/up/audio_latency.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE connections ADD COLUMN last_jam_audio_latency double precision;
ALTER TABLE users RENAME COLUMN audio_latency TO last_jam_audio_latency;

View File

@ -0,0 +1,5 @@
ALTER TABLE users DROP COLUMN addr;
ALTER TABLE users DROP COLUMN locidispid;
ALTER TABLE users DROP COLUMN internet_service_provider;
ALTER TABLE users ADD COLUMN statecode varchar(2);
ALTER TABLE users ADD COLUMN countrycode varchar(2);

View File

@ -339,7 +339,7 @@ SQL
end
end
def join_music_session(user, client_id, music_session, as_musician, tracks)
def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency)
connection = nil
ConnectionManager.active_record_transaction do |connection_manager|
@ -347,7 +347,7 @@ SQL
connection = Connection.find_by_client_id_and_user_id!(client_id, user.id)
connection.join_the_session(music_session, as_musician, tracks, user)
connection.join_the_session(music_session, as_musician, tracks, user, audio_latency)
# connection.music_session_id = music_session.id
# connection.as_musician = as_musician
# connection.joining_session = true

View File

@ -19,6 +19,7 @@ module JamRuby
validates :as_musician, :inclusion => {:in => [true, false]}
validates :client_type, :inclusion => {:in => [TYPE_CLIENT, TYPE_BROWSER, TYPE_LATENCY_TESTER]}
validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true
validate :can_join_music_session, :if => :joining_session?
validate :user_or_latency_tester_present
@ -152,7 +153,7 @@ module JamRuby
true
end
def join_the_session(music_session, as_musician, tracks, user)
def join_the_session(music_session, as_musician, tracks, user, audio_latency)
self.music_session_id = music_session.id
self.as_musician = as_musician
self.joining_session = true
@ -163,6 +164,7 @@ module JamRuby
# if user joins the session as a musician, update their addr and location
if as_musician
user.update_addr_loc(self, 'j')
user.update_audio_latency(self, audio_latency)
end
end
@ -185,7 +187,6 @@ module JamRuby
end
end
private
def require_at_least_one_track_when_in_session

View File

@ -4,6 +4,7 @@ module JamRuby
BATCH_SIZE = 500
SINCE_WEEKS = 2
NUM_IDX = 3
SUBTYPES = [:client_notdl, # Registered Musician Has Not Downloaded Client
:client_dl_notrun, # Registered Musician Has Downloaded Client But Not Yet Run It

View File

@ -179,7 +179,7 @@ module JamRuby
unless locidispid.nil?
# score_join of left allows for null scores, whereas score_join of inner requires a score however good or bad
# this is ANY_SCORE:
score_join = 'left' # or 'inner'
score_join = 'left outer' # or 'inner'
score_min = nil
score_max = nil
case score_limit
@ -213,14 +213,14 @@ module JamRuby
# the default of ANY setup above applies
end
rel = rel.joins("#{score_join} join scores on scores.alocidispid = users.last_jam_locidispid")
.where(['scores.blocidispid = ?', locidispid])
rel = rel.joins("#{score_join} join current_scores on current_scores.alocidispid = users.last_jam_locidispid")
.where(['(current_scores.blocidispid = ? or current_scores.blocidispid is null)', locidispid])
rel = rel.where(['scores.score > ?', score_min]) unless score_min.nil?
rel = rel.where(['scores.score <= ?', score_max]) unless score_max.nil?
rel = rel.where(['current_scores.score > ?', score_min]) unless score_min.nil?
rel = rel.where(['current_scores.score <= ?', score_max]) unless score_max.nil?
rel = rel.select('scores.score')
rel = rel.group('scores.score')
rel = rel.select('current_scores.score')
rel = rel.group('current_scores.score')
end
ordering = self.order_param(params)
@ -242,7 +242,7 @@ module JamRuby
end
unless locidispid.nil?
rel = rel.order('scores.score ASC NULLS LAST')
rel = rel.order('current_scores.score ASC NULLS LAST')
end
rel = rel.order('users.created_at DESC')
@ -391,10 +391,10 @@ module JamRuby
rel = User.musicians_geocoded
.where(['created_at >= ? AND users.id != ?', since_date, usr.id])
.joins('inner join scores on users.last_jam_locidispid = scores.alocidispid')
.where(['scores.blocidispid = ?', locidispid])
.where(['scores.score <= ?', score_limit])
.order('scores.score') # best scores first
.joins('inner join current_scores on users.last_jam_locidispid = current_scores.alocidispid')
.where(['current_scores.blocidispid = ?', locidispid])
.where(['current_scores.score <= ?', score_limit])
.order('current_scores.score') # best scores first
.order('users.created_at DESC') # then most recent
.limit(limit)

View File

@ -129,6 +129,7 @@ module JamRuby
validates :musician, :inclusion => {:in => [true, false]}
validates :show_whats_next, :inclusion => {:in => [nil, true, false]}
validates :mods, json: true
validates_numericality_of :last_jam_audio_latency, greater_than:0, :allow_nil => true
# custom validators
validate :validate_musician_instruments
@ -292,7 +293,8 @@ module JamRuby
def joined_score
nil unless has_attribute?(:score)
read_attribute(:score).to_i
a = read_attribute(:score)
a.nil? ? nil : a.to_i
end
# mods comes back as text; so give ourselves a parsed version
@ -1168,6 +1170,18 @@ module JamRuby
self.save
end
def update_audio_latency(connection, audio_latency)
# updating the connection is best effort
if connection
connection.last_jam_audio_latency = audio_latency
connection.save
end
self.last_jam_audio_latency = audio_latency
self.save
end
def top_followings
@topf ||= User.joins("INNER JOIN follows ON follows.followable_id = users.id AND follows.followable_type = '#{self.class.to_s}'")
.where(['follows.user_id = ?', self.id])

View File

@ -283,7 +283,7 @@ describe ConnectionManager do
user = User.find(user_id)
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS)
connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10)
connection.errors.any?.should be_false
@ -308,7 +308,7 @@ describe ConnectionManager do
user = User.find(user_id)
expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS) }.to raise_error(ActiveRecord::RecordNotFound)
expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) }.to raise_error(ActiveRecord::RecordNotFound)
end
@ -325,11 +325,11 @@ describe ConnectionManager do
music_session_id = music_session.id
user = User.find(user_id)
@connman.join_music_session(user, client_id, music_session, true, TRACKS)
@connman.join_music_session(user, client_id, music_session, true, TRACKS, 10)
user = User.find(user_id2)
connection = @connman.join_music_session(user, client_id2, music_session, true, TRACKS)
connection = @connman.join_music_session(user, client_id2, music_session, true, TRACKS, 10)
connection.errors.size.should == 1
connection.errors.get(:as_musician).should == [ValidationMessages::FAN_CAN_NOT_JOIN_AS_MUSICIAN]
end
@ -343,7 +343,7 @@ describe ConnectionManager do
music_session = FactoryGirl.create(:active_music_session, user_id: user_id)
user = User.find(user_id)
connection = @connman.join_music_session(user, client_id, music_session, 'blarg', TRACKS)
connection = @connman.join_music_session(user, client_id, music_session, 'blarg', TRACKS, 10)
connection.errors.size.should == 0
connection.as_musician.should be_false
end
@ -362,11 +362,11 @@ describe ConnectionManager do
user = User.find(musician_id)
@connman.join_music_session(user, musician_client_id, music_session, true, TRACKS)
@connman.join_music_session(user, musician_client_id, music_session, true, TRACKS, 10)
# now join the session as a fan, bt fan_access = false
user = User.find(fan_id)
connection = @connman.join_music_session(user, fan_client_id, music_session, false, TRACKS)
connection = @connman.join_music_session(user, fan_client_id, music_session, false, TRACKS, 10)
connection.errors.size.should == 1
end
@ -381,7 +381,7 @@ describe ConnectionManager do
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
# specify real user id, but not associated with this session
expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS) } .to raise_error(ActiveRecord::RecordNotFound)
expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound)
end
it "join_music_session fails if no music_session" do
@ -392,7 +392,7 @@ describe ConnectionManager do
music_session = ActiveMusicSession.new
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS)
connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10)
connection.errors.size.should == 1
connection.errors.get(:music_session).should == [ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED]
end
@ -407,7 +407,7 @@ describe ConnectionManager do
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
# specify real user id, but not associated with this session
expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS) } .to raise_error(ActiveRecord::RecordNotFound)
expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound)
end
@ -435,7 +435,7 @@ describe ConnectionManager do
dummy_music_session = ActiveMusicSession.new
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
@connman.join_music_session(user, client_id, music_session, true, TRACKS)
@connman.join_music_session(user, client_id, music_session, true, TRACKS, 10)
expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError)
end
@ -448,7 +448,7 @@ describe ConnectionManager do
user = User.find(user_id)
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
@connman.join_music_session(user, client_id, music_session, true, TRACKS)
@connman.join_music_session(user, client_id, music_session, true, TRACKS, 10)
assert_session_exists(music_session_id, true)
@ -492,11 +492,11 @@ describe ConnectionManager do
client_id1 = Faker::Number.number(20)
@connman.create_connection(user_id, client_id1, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
music_session1 = FactoryGirl.create(:active_music_session, :user_id => user_id)
connection1 = @connman.join_music_session(user, client_id1, music_session1, true, TRACKS)
connection1 = @connman.join_music_session(user, client_id1, music_session1, true, TRACKS, 10)
connection1.errors.size.should == 0
music_session2 = FactoryGirl.create(:active_music_session, :user_id => user_id)
connection2 = @connman.join_music_session(user, client_id1, music_session2, true, TRACKS)
connection2 = @connman.join_music_session(user, client_id1, music_session2, true, TRACKS, 10)
connection2.errors.size.should == 1
connection2.errors.get(:music_session).should == [ValidationMessages::CANT_JOIN_MULTIPLE_SESSIONS]

View File

@ -361,7 +361,7 @@ describe ActiveMusicSession do
@music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
# @music_session.connections << @connection
@music_session.save!
@connection.join_the_session(@music_session, true, nil, @user1)
@connection.join_the_session(@music_session, true, nil, @user1, 10)
end
describe "not recording" do

View File

@ -21,7 +21,7 @@ describe ClaimedRecording do
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
# @music_session.connections << @connection
@music_session.save
@connection.join_the_session(@music_session, true, nil, @user)
@connection.join_the_session(@music_session, true, nil, @user, 10)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload

View File

@ -3,6 +3,11 @@ require 'spec_helper'
describe JamRuby::Connection do
let(:user) { FactoryGirl.create(:user) }
let (:music_session) { FactoryGirl.create(:active_music_session, :creator => user) }
let (:conn) { FactoryGirl.create(:connection,
:user => user,
:music_session => music_session,
:ip_address => "1.1.1.1",
:client_id => "1") }
it 'starts in the correct state' do
connection = FactoryGirl.create(:connection,
@ -48,4 +53,22 @@ describe JamRuby::Connection do
user.lng.should == geocode.lng
end
describe "audio latency" do
it "allow update" do
conn.last_jam_audio_latency = 1
conn.save!
end
it "prevent negative or 0" do
conn.last_jam_audio_latency = 0
conn.save.should be_false
conn.errors[:last_jam_audio_latency].should == ['must be greater than 0']
end
it "prevent non numerical" do
conn.last_jam_audio_latency = 'a'
conn.save.should be_false
conn.errors[:last_jam_audio_latency].should == ['is not a number']
end
end
end

View File

@ -61,28 +61,22 @@ describe EmailBatch do
# before { pending }
def handles_new_users(ebatch, user)
3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) }
EmailBatchProgression::NUM_IDX.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) }
dd = user.created_at + ebatch.days_past_for_trigger_index(0).days
Timecop.travel(dd)
vals = [1,0,0]
3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) }
ebatch.make_set(user, 0)
3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) }
dd = dd + ebatch.days_past_for_trigger_index(1).days
Timecop.travel(dd)
vals = [0,1,0]
3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) }
ebatch.make_set(user, 1)
3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) }
dd = dd + ebatch.days_past_for_trigger_index(2).days
Timecop.travel(dd)
vals = [0,0,1]
3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) }
ebatch.make_set(user, 2)
3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) }
dd = user.created_at
EmailBatchProgression::NUM_IDX.times do |idx|
dd = dd + ebatch.days_past_for_trigger_index(idx).days
Timecop.travel(dd)
vals = Array.new(3,0)
vals[idx] = 1
EmailBatchProgression::NUM_IDX.times do |nn|
expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn])
end
ebatch.make_set(user, idx)
EmailBatchProgression::NUM_IDX.times do |nn|
expect(ebatch.fetch_recipients(nn).count).to eq(0)
end
end
end
def handles_existing_users(ebatch, user)

View File

@ -10,7 +10,7 @@ describe Mix do
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
# @music_session.connections << @connection
@music_session.save
@connection.join_the_session(@music_session, true, nil, @user)
@connection.join_the_session(@music_session, true, nil, @user, 10)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.claim(@user, "name", "description", Genre.first, true)

View File

@ -46,6 +46,167 @@ describe MusicSession do
music_session.valid?.should be_false
end
end
describe "nindex" do
it "nindex orders two sessions by created_at starting with most recent" do
creator = FactoryGirl.create(:user)
creator2 = FactoryGirl.create(:user)
earlier_session = FactoryGirl.create(:active_music_session, :creator => creator, :description => "Earlier Session")
c1 = FactoryGirl.create(:connection, user: creator, music_session: earlier_session, addr: 0x01020304, locidispid: 1)
later_session = FactoryGirl.create(:active_music_session, :creator => creator2, :description => "Later Session")
c2 = FactoryGirl.create(:connection, user: creator2, music_session: later_session, addr: 0x21020304, locidispid: 2)
user = FactoryGirl.create(:user)
c3 = FactoryGirl.create(:connection, user: user, locidispid: 3)
Score.createx(c1.locidispid, c1.client_id, c1.addr, c3.locidispid, c3.client_id, c3.addr, 20, nil);
Score.createx(c2.locidispid, c2.client_id, c2.addr, c3.locidispid, c3.client_id, c3.addr, 30, nil);
# scores!
#ActiveRecord::Base.logger = Logger.new(STDOUT)
music_sessions = ActiveMusicSession.nindex(user, client_id: c3.client_id).take(100)
#music_sessions = MusicSession.index(user).take(100)
#ActiveRecord::Base.logger = nil
music_sessions.length.should == 2
music_sessions[0].id.should == later_session.id
music_sessions[1].id.should == earlier_session.id
end
end
it 'uninvited users cant join approval-required sessions without invitation' do
user1 = FactoryGirl.create(:user) # in the jam session
user2 = FactoryGirl.create(:user) # in the jam session
music_session = FactoryGirl.create(:active_music_session, :creator => user1, :musician_access => true, :approval_required => true)
connection1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session)
expect { FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :joining_session => true) }.to raise_error(ActiveRecord::RecordInvalid)
end
it "is_recording? returns false if not recording" do
user1 = FactoryGirl.create(:user)
music_session = FactoryGirl.build(:active_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(:active_music_session, :creator => @user1, :musician_access => true)
# @music_session.connections << @connection
@music_session.save!
@connection.join_the_session(@music_session, true, nil, @user1, 10)
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
describe "claim a recording" do
before(:each) do
@recording = Recording.start(@music_session, @user1)
@recording.errors.any?.should be_false
@recording.stop
@recording.reload
@claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true)
@claimed_recording.errors.any?.should be_false
end
it "allow a claimed recording to be associated" do
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.claimed_recording.should == @claimed_recording
@music_session.claimed_recording_initiator.should == @user1
end
it "allow a claimed recording to be removed" do
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
@music_session.claimed_recording_stop
@music_session.errors.any?.should be_false
@music_session.reload
@music_session.claimed_recording.should be_nil
@music_session.claimed_recording_initiator.should be_nil
end
it "disallow a claimed recording to be started when already started by someone else" do
@user2 = FactoryGirl.create(:user)
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
@music_session.claimed_recording_start(@user2, @claimed_recording)
@music_session.errors.any?.should be_true
@music_session.errors[:claimed_recording] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
end
it "allow a claimed recording to be started when already started by self" do
@user2 = FactoryGirl.create(:user)
@claimed_recording2 = @recording.claim(@user1, "name", "description", Genre.first, true)
@music_session.claimed_recording_start(@user1, @claimed_recording)
@music_session.errors.any?.should be_false
@music_session.claimed_recording_start(@user1, @claimed_recording2)
@music_session.errors.any?.should be_false
end
end
end
describe "get_connection_ids" do
before(:each) do
@user1 = FactoryGirl.create(:user)
@user2 = FactoryGirl.create(:user)
@music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true)
@connection1 = FactoryGirl.create(:connection, :user => @user1, :music_session => @music_session, :as_musician => true)
@connection2 = FactoryGirl.create(:connection, :user => @user2, :music_session => @music_session, :as_musician => false)
end
it "get all connections" do
@music_session.get_connection_ids().should == [@connection1.client_id, @connection2.client_id]
end
it "exclude non-musicians" do
@music_session.get_connection_ids(as_musician: true).should == [@connection1.client_id]
end
it "exclude musicians" do
@music_session.get_connection_ids(as_musician: false).should == [@connection2.client_id]
end
it "exclude particular client" do
@music_session.get_connection_ids(exclude_client_id: @connection1.client_id).should == [@connection2.client_id]
end
it "exclude particular client and exclude non-musicians" do
@music_session.get_connection_ids(exclude_client_id: @connection2.client_id, as_musician: true).should == [@connection1.client_id]
end
end
end

View File

@ -171,7 +171,7 @@ describe 'Musician search' do
music_session = FactoryGirl.create(:active_music_session, :creator => usr, :musician_access => true)
# music_session.connections << connection
# music_session.save
connection.join_the_session(music_session, true, nil, usr)
connection.join_the_session(music_session, true, nil, usr, 10)
recording = Recording.start(music_session, usr)
recording.stop
recording.reload
@ -186,7 +186,7 @@ describe 'Musician search' do
music_session = FactoryGirl.create(:active_music_session, :creator => usr, :musician_access => true)
# music_session.connections << connection
# music_session.save
connection.join_the_session(music_session, true, nil, usr)
connection.join_the_session(music_session, true, nil, usr, 10)
end
context 'musician stat counters' do

View File

@ -80,7 +80,7 @@ describe Recording do
@track2 = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument2)
# @music_session.connections << @connection2
@connection2.join_the_session(@music_session, true, nil, @user2)
@connection2.join_the_session(@music_session, true, nil, @user2, 10)
@recording = Recording.start(@music_session, @user)
@user.recordings.length.should == 0
@ -179,7 +179,7 @@ describe Recording do
@track = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument)
# @music_session.connections << @connection2
@music_session.save
@connection2.join_the_session(@music_session, true, nil, @user2)
@connection2.join_the_session(@music_session, true, nil, @user2, 10)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload

View File

@ -12,19 +12,21 @@ describe User do
subject { @user }
it { should respond_to(:first_name) }
it { should respond_to(:last_name) }
it { should respond_to(:email) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should respond_to(:remember_token) }
it { should respond_to(:admin) }
it { should respond_to(:valid_password?) }
it { should respond_to(:can_invite) }
it { should respond_to(:mods) }
it { should be_valid }
it { should_not be_admin }
it {
should respond_to(:first_name)
should respond_to(:last_name)
should respond_to(:email)
should respond_to(:password)
should respond_to(:password_confirmation)
should respond_to(:remember_token)
should respond_to(:admin)
should respond_to(:valid_password?)
should respond_to(:can_invite)
should respond_to(:mods)
should respond_to(:last_jam_audio_latency)
should be_valid
should_not be_admin
}
describe "accessible attributes" do
it "should not allow access to admin" do
@ -471,6 +473,25 @@ describe User do
@user.connection_expire_time_client.should == 5
end
end
describe "audio latency" do
it "allow update" do
@user.last_jam_audio_latency = 1
@user.save!
end
it "prevent negative or 0" do
@user.last_jam_audio_latency = 0
@user.save.should be_false
@user.errors[:last_jam_audio_latency].should == ['must be greater than 0']
end
it "prevent non numerical" do
@user.last_jam_audio_latency = 'a'
@user.save.should be_false
@user.errors[:last_jam_audio_latency].should == ['is not a number']
end
end
=begin
describe "update avatar" do

View File

@ -10,14 +10,23 @@ if ENV['COVERAGE'] == "1"
SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
SimpleCov.start do
add_filter "/test/"
add_filter "/bin/"
add_filter "/scripts/"
add_filter "/tmp/"
add_filter "/vendor/"
add_filter "/spec/"
add_filter "/features/"
SimpleCov.start :rails do
# remove the :root_filter so that we can see coverage of external dependencies (i.e., jam_ruby)
filters.clear
# ignore Ruby itself (...not to be confused with jam_ruby)
add_filter "/lib/ruby/"
# ignore Rails subfolders which don't contain app code
%w{config coverage db doc features log script spec test tmp}.each do |dir|
add_filter "#{dir}/"
end
# ignore all gem code except our jam gems
add_filter {|src| src.filename =~ /ruby.*\/gems\// unless src.filename =~ /\/gems\/jam/ }
# categorize JamRuby in the coverage report:
add_group 'Jam Ruby', 'jam_ruby'
end
all_files = Dir['**/*.rb']

View File

@ -80,7 +80,7 @@ gem 'iso-639'
gem 'language_list'
group :development, :test do
gem 'rspec-rails', "2.14.2"
gem 'rspec-rails', '2.14.2'
gem "activerecord-import", "~> 0.4.1"
gem 'guard-rspec', '0.5.5'
gem 'jasmine', '1.3.1'

View File

@ -453,7 +453,7 @@
clientType = context.JK.clientType();
}
if(!mode) {
mode = context.jamClient.getOperatingMode();
mode = context.jamClient.getOperatingMode ? context.jamClient.getOperatingMode() : 'client';
}
connectDeferred = new $.Deferred();

View File

@ -4,9 +4,12 @@
context.JK = context.JK || {};
context.JK.AccountAudioProfile = function (app) {
var self = this;
var EVENTS = context.JK.EVENTS;
var gearUtils = context.JK.GearUtils;
var logger = context.JK.logger;
var rest = context.JK.Rest();
var self = this;
var userId;
function beforeShow(data) {
@ -35,28 +38,26 @@
}
function populateAccountAudio() {
var all = context.jamClient.FTUEGetAllAudioConfigurations();
var good = context.jamClient.FTUEGetGoodAudioConfigurations();
var current = context.jamClient.FTUEGetMusicProfileName();
var profiles = gearUtils.getProfiles();
var profiles = [];
context._.each(all, function(item) {
profiles.push({id: item, good: false, class:'bad', current: current == item, active_text: current == item ? '(active)' : ''})
context._.each(profiles, function(profile) {
profile.active_text = profile.current ? '(active)' : '';
});
if(good) {
for(var i = 0; i < good.length; i++) {
for(var j = 0; j < profiles.length; j++) {
if(good[i] == profiles[j].id) {
profiles[j].good = true;
profiles[j].class = 'good';
break;
}
}
}
}
// If you are in the FTUE, and you close the client and/or it crashes
// then you will have 'FTUE' (incomplete) profiles. This is the only time
var template = context._.template($('#template-account-audio').html(), {profiles: profiles}, {variable: 'data'});
var cleansedProfiles = [];
context._.each(profiles, function(profile) {
if(profile.id.indexOf('FTUE') == 0) {
context.jamClient.TrackDeleteProfile(profile.id);
}
else {
cleansedProfiles.push(profile)
}
});
var template = context._.template($('#template-account-audio').html(), {is_admin: context.JK.currentUserAdmin, profiles: cleansedProfiles}, {variable: 'data'});
appendAudio(template);
}
@ -77,6 +78,43 @@
populateAccountAudio();
}
function handleLoopbackAudioProfile(audioProfileId) {
if(audioProfileId != context.jamClient.FTUEGetMusicProfileName()) {
var result = context.jamClient.FTUELoadAudioConfiguration(audioProfileId);
if(!result) {
logger.error("unable to activate audio configuration: " + audioProfileId);
context.JK.alertSupportedNeeded("Unable to activate audio configuration for profile named: " + audioProfileId);
}
else {
// redraw after activation of profile
populateAccountAudio();
}
}
app.layout.showDialog('loopback-wizard');
}
function handleConfigureAudioProfile(audioProfileId) {
if(audioProfileId != context.jamClient.FTUEGetMusicProfileName()) {
var result = context.jamClient.FTUELoadAudioConfiguration(audioProfileId);
if(!result) {
logger.error("unable to activate audio configuration: " + audioProfileId);
context.JK.alertSupportedNeeded("Unable to activate audio configuration for profile named: " + audioProfileId);
}
else {
// redraw after activation of profile
populateAccountAudio();
}
}
app.layout.showDialog('configure-tracks')
.one(EVENTS.DIALOG_CLOSED, populateAccountAudio)
}
function handleActivateAudioProfile(audioProfileId) {
logger.debug("activating audio profile: " + audioProfileId);
@ -143,6 +181,34 @@
return false;
});
$root.on('click', 'a[data-purpose=loopback-audio-profile]', function (evt) {
evt.stopPropagation();
var $btn = $(this);
var status = $btn.closest('tr').attr('data-status');
if(status == "good") {
handleLoopbackAudioProfile($btn.attr('data-id'));
}
else {
context.JK.Banner.showAlert("Unable to loopback test this profile. Please verify that the devices associated are connected.");
}
return false;
});
$root.on('click', 'a[data-purpose=configure-audio-profile]', function (evt) {
evt.stopPropagation();
var $btn = $(this);
var status = $btn.closest('tr').attr('data-status');
if(status == "good") {
handleConfigureAudioProfile($btn.attr('data-id'));
}
else {
context.JK.Banner.showAlert("Unable to configure this profile. Please verify that the devices associated are connected.");
}
return false;
});
$root.on('click', 'a[data-purpose=add-profile]', function (evt) {
evt.stopPropagation();
handleStartAudioQualification();

View File

@ -11,7 +11,7 @@
sessionScreen.setPromptLeave(false);
app.layout.closeDialog('configure-audio');
app.layout.closeDialog('configure-tracks');
context.location = "/client#/home";

View File

@ -39,7 +39,7 @@
message['message'] = $textBox.val();
message['music_session'] = $sessionId;
message['client_id'] = context.JK.clientId;
message['client_id'] = app.clientId;
return message;
}

View File

@ -0,0 +1,229 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.ConfigureTracksDialog = function (app) {
var logger = context.JK.logger;
var ASSIGNMENT = context.JK.ASSIGNMENT;
var VOICE_CHAT = context.JK.VOICE_CHAT;
var gearUtils = context.JK.GearUtils;
var $dialog = null;
var $instructions = null;
var $musicAudioTab = null;
var $musicAudioTabSelector = null;
var $voiceChatTab = null;
var $voiceChatTabSelector = null;
var $certifiedAudioProfile = null;
var $btnCancel = null;
var $btnAddNewGear = null;
var $btnUpdateTrackSettings = null;
var configureTracksHelper = null;
var voiceChatHelper = null;
var profiles = null;
var currentProfile = null;
var configure_audio_instructions = {
"Win32": "Choose the audio device you would like to use for this session. If needed, use arrow buttons to assign audio inputs " +
"to your tracks, to indicate what instrument you are playing on each track, and to assign audio outputs for listening. " +
"If you want to use a new audio device you have not tested/certified for latency using JamKazam, click the Add New Audio " +
"Gear button to test that device.",
"MacOSX": "Choose the audio device you would like to use for this session. If needed, use arrow buttons to assign audio inputs " +
"to your tracks, to indicate what instrument you are playing on each track, and to assign audio outputs for listening. " +
"If you want to use a new audio device you have not tested/certified for latency using JamKazam, click the Add New Audio " +
"Gear button to test that device.",
"Unix": "Choose the audio device you would like to use for this session. If needed, use arrow buttons to assign audio inputs " +
"to your tracks, to indicate what instrument you are playing on each track, and to assign audio outputs for listening. " +
"If you want to use a new audio device you have not tested/certified for latency using JamKazam, click the Add New Audio " +
"Gear button to test that device."
};
var configure_voice_instructions = "If you are using a microphone to capture your instrumental or vocal audio, you can simply use that mic " +
"for both music and chat. Otherwise, choose a device to use for voice chat, and use arrow buttons to " +
"select an input on that device.";
function setInstructions(type) {
if (type === 'audio') {
$instructions.html('Choose your audio device. Drag and drop to assign input ports to tracks, and specify the instrument for each track. Drag and drop to assign a pair of output ports for session stereo audio monitoring.')
return;
var os = context.jamClient.GetOSAsString();
$instructions.html(configure_audio_instructions[os]);
}
else if (type === 'voice') {
$instructions.html(configure_voice_instructions);
}
else {
throw "unknown type in setInstructions(" + type + ')';
}
}
function activateTab(type) {
if (type === 'voice') {
$musicAudioTab.hide();
$voiceChatTab.show();
$musicAudioTabSelector.removeClass('selected');
$voiceChatTabSelector.addClass('selected');
}
else {
$musicAudioTab.show();
$voiceChatTab.hide();
$musicAudioTabSelector.addClass('selected');
$voiceChatTabSelector.removeClass('selected');
}
}
function validateVoiceChatSettings() {
return voiceChatHelper.trySave();
}
function showMusicAudioPanel() {
setInstructions('audio');
activateTab('audio');
}
function validateAudioSettings() {
return configureTracksHelper.trySave();
}
function showVoiceChatPanel() {
setInstructions('voice');
activateTab('voice');
}
function events() {
$musicAudioTabSelector.click(function () {
// validate voice chat settings
if (validateVoiceChatSettings()) {
configureTracksHelper.reset();
voiceChatHelper.reset();
showMusicAudioPanel();
}
});
$voiceChatTabSelector.click(function () {
// validate audio settings
if (validateAudioSettings()) {
configureTracksHelper.reset();
voiceChatHelper.reset();
showVoiceChatPanel();
}
});
$btnCancel.click(function() {
app.layout.closeDialog('configure-tracks')
return false;
});
//$btnAddNewGear.click(function() {
// return false;
//});
$btnUpdateTrackSettings.click(function() {
if(configureTracksHelper.trySave() && voiceChatHelper.trySave()) {
app.layout.closeDialog('configure-tracks');
}
return false;
});
$certifiedAudioProfile.change(deviceChanged);
}
function renderCertifiedGearDropdown() {
var optionsHtml = '';
context._.each(profiles, function (profile) {
if(profile.good) {
optionsHtml += '<option title="' + profile.id + '" value="' + profile.id + '"' + (profile.current ? 'selected="selected"' : '') + '>' + profile.id + '</option>';
}
});
$certifiedAudioProfile.html(optionsHtml);
context.JK.dropdown($certifiedAudioProfile);
}
function deviceChanged() {
var profile = $certifiedAudioProfile.val();
if(currentProfile == profile) {
return; // just bail early, because the easydropdown fires change events even when you select the same value
}
logger.debug("activating audio profile: " + profile);
var result = context.jamClient.FTUELoadAudioConfiguration(profile);
if(!result) {
logger.error("unable to activate audio configuration: " + profile);
context.JK.alertSupportedNeeded("Unable to activate audio configuration for profile named: " + profile);
renderCertifiedGearDropdown(); // force the dropdown to be reflective of the actually active profile
}
else {
configureTracksHelper.reset();
}
}
function beforeShow() {
profiles = gearUtils.getProfiles();
renderCertifiedGearDropdown();
showMusicAudioPanel();
currentProfile = context.jamClient.FTUEGetMusicProfileName();
if(currentProfile != $certifiedAudioProfile.val()) {
logger.error("the currently active profile (" + currentProfile + ") is not the same as the Certified Audio Gear dropdown (" + $certifiedAudioProfile.val() + ")");
context.JK.alertSupportedNeeded("Unable to determine the current profile.");
}
configureTracksHelper.reset();
voiceChatHelper.reset();
}
function afterHide() {
}
function initialize() {
var dialogBindings = {
'beforeShow' : beforeShow,
'afterHide': afterHide
};
app.bindDialog('configure-tracks', dialogBindings);
$dialog = $('#configure-tracks-dialog');
$instructions = $dialog.find('.instructions span');
$musicAudioTab = $dialog.find('div[tab-id="music-audio"]');
$musicAudioTabSelector = $dialog.find('.tab-configure-audio');
$voiceChatTab = $dialog.find('div[tab-id="voice-chat"]');
$voiceChatTabSelector = $dialog.find('.tab-configure-voice');
$certifiedAudioProfile = $dialog.find('.certified-audio-profile');
$btnCancel = $dialog.find('.btn-cancel');
$btnAddNewGear = $dialog.find('.btn-add-new-audio-gear');
$btnUpdateTrackSettings = $dialog.find('.btn-update-settings');
configureTracksHelper = new JK.ConfigureTracksHelper(app);
configureTracksHelper.initialize($dialog);
voiceChatHelper = new JK.VoiceChatHelper(app);
voiceChatHelper.initialize($dialog, false);
events();
}
this.initialize = initialize;
return this;
};
})(window, jQuery);

View File

@ -0,0 +1,530 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.ConfigureTracksHelper = function (app) {
var logger = context.JK.logger;
var ASSIGNMENT = context.JK.ASSIGNMENT;
var VOICE_CHAT = context.JK.VOICE_CHAT;
var MAX_TRACKS = context.JK.MAX_TRACKS;
var MAX_OUTPUTS = context.JK.MAX_OUTPUTS;
var gearUtils = context.JK.GearUtils;
var $parent = null;
var $templateAssignablePort = null;
var $templateTrackTarget = null;
var $templateOutputTarget = null;
var $unassignedInputsHolder = null;
var $unassignedOutputsHolder = null;
var $tracksHolder = null;
var $outputChannelHolder = null;
var $instrumentsHolder = null;
var isDragging = false;
function hoverIn($channel) {
if(isDragging) return;
$channel.css('color', 'white')
var $container = $channel.closest('.target');
var inTarget = $container.length > 0;
if(!inTarget) {
$container = $channel.closest('.channels-holder')
}
$channel.data('container', $container)
$channel.addClass('hovering');
var $inputs = $container.find('.ftue-input');
var index = $inputs.index($channel);
$channel.css('background-color', '#333');
// $channel.css('padding', '0 5px');
if(inTarget) {
$channel.css('border', '#333');
$channel.css('border-radius', '2px');
$channel.css('min-width', '49%');
$channel.css('width', 'auto');
$channel.css('position', 'absolute');
$container.css('overflow', 'visible');
}
else {
// TODO: make the unassigned work
// $channel.css('min-width', $channel.css('width'));
// $channel.css('position', 'absolute');
// $container.addClass('compensate');
}
$channel.css('z-index', 10000)
if(inTarget && $inputs.length == 2) {
if(index == 0) {
$channel.css('right', '50%')
}
else {
$channel.css('left', '51%')
}
}
}
function hoverOut($channel) {
$channel
.removeClass('hovering')
.css('color', '')
.css('font-weight', '')
.css('position', '')
.css('width', '')
.css('background-color', '')
.css('padding', '')
.css('border', '')
.css('border-radius', '')
.css('right', '')
.css('left', '')
.css('min-width', '')
.css('z-index', '')
.css('margin-left', '')
.css('max-width', 'auto');
//var $container = $channel.closest('.target');
var $container = $channel.data('container');
$container.css('overflow', '')
$container.removeClass('compensate');
}
function fixClone($clone) {
$clone
.css('color', '')
.css('font-weight', '')
.css('width', 'auto')
.css('background-color', '')
.css('padding', '')
.css('border', '')
.css('border-radius', '')
.css('right', '')
.css('min-width', '')
}
function loadChannels(forceInputsToUnassign) {
var musicPorts = jamClient.FTUEGetChannels();
$unassignedInputsHolder.empty();
$unassignedOutputsHolder.empty();
// reset all counts
$tracksHolder.find('.track-target').attr('track-count', '0');
$outputChannelHolder.find('.output-target').attr('output-count', '0');
$tracksHolder.find('.ftue-input').remove();
$outputChannelHolder.find('.ftue-input').remove();
var inputChannels = musicPorts.inputs;
var outputChannels = musicPorts.outputs;
// uncomment to add a bunch of bogus channels
//inputChannels = inputChannels.concat(inputChannels.concat(inputChannels.concat(inputChannels)))
context._.each(inputChannels, function (inputChannel) {
var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' }));
$channel.hover(
function() { hoverIn ($(this)) },
function() { hoverOut($(this)) }
);
if(forceInputsToUnassign || inputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
unassignInputChannel($channel);
}
else if(inputChannel.assignment == ASSIGNMENT.CHAT) {
// well, we can't show it as unused... if there were a place to show chat inputs, we would put it there.
// but we don't have it, so just skip
logger.debug("skipping channel ", inputChannel)
return;
}
else {
// find the track this belongs in
var trackNumber = inputChannel.assignment - 1;
var $track = $tracksHolder.find('.track[data-num="' + trackNumber + '"]')
if($track.length == 0) {
context.JK.alertSupportedNeeded('Unable to find a track for channel with assignment ' + inputChannel.assignment);
return false;
}
addChannelToTrack($channel, $track.find('.track-target'));
}
$channel.draggable({
helper: 'clone',
start: function(e, ui) {
isDragging = true;
var $channel = $(this);
fixClone(ui.helper);
var $track = $channel.closest('.track-target');
var isUnassigned = $track.length == 0;
if(isUnassigned) {
$tracksHolder.find('.track-target').addClass('possible-target');
}
else {
$tracksHolder.find('.track-target').addClass('possible-target');
$unassignedInputsHolder.addClass('possible-target');
}
},
stop: function() {
isDragging = false;
$tracksHolder.find('.track-target').removeClass('possible-target');
$unassignedInputsHolder.removeClass('possible-target')
}
});
})
var outputAssignment = 0;
context._.each(outputChannels, function (outputChannel, index) {
var $channel = $(context._.template($templateAssignablePort.html(), outputChannel, { variable: 'data' }));
$channel.hover(
function() { hoverIn ($(this)) },
function() { hoverOut($(this)) }
);
if(outputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
unassignOutputChannel($channel);
}
else {
var $output = $outputChannelHolder.find('.output[data-num="' + index + '"]')
if($output.length == 0) {
context.JK.alertSupportedNeeded('Unable to find an output for channel with assignment ' + outputChannel.assignment);
return false;
}
addChannelToOutput($channel, $output.find('.output-target'));
}
$channel.draggable({
helper: 'clone',
start: function(e,ui) {
isDragging = true;
var $channel = $(this);
fixClone(ui.helper);
var $output = $channel.closest('.output-target');
var isUnassigned = $output.length == 0;
if(isUnassigned) {
$outputChannelHolder.find('.output-target').addClass('possible-target');
}
else {
$outputChannelHolder.find('.output-target').addClass('possible-target');
$unassignedOutputsHolder.addClass('possible-target');
}
},
stop: function() {
isDragging = false;
$outputChannelHolder.find('.output-target').removeClass('possible-target');
$unassignedOutputsHolder.removeClass('possible-target')
}
});
});
}
// iterates through the dom and returns a pure data structure for track associations and output channels
function getCurrentState() {
var state = {};
state.tracks = [];
state.unassignedChannels = [];
state.outputs = [];
var $unassignedInputChannels = $unassignedInputsHolder.find('.ftue-input');
var $unassignedOutputChannels = $unassignedOutputsHolder.find('.ftue-input');
var $tracks = $tracksHolder.find('.track-target');
var $outputs = $outputChannelHolder.find('.output-target');
context._.each($unassignedInputChannels, function($unassignedInput) {
$unassignedInput = $($unassignedInput);
var channelId = $unassignedInput.attr('data-input-id');
state.unassignedChannels.push(channelId);
})
context._.each($unassignedOutputChannels, function($unassignedOutput) {
$unassignedOutput = $($unassignedOutput);
var channelId = $unassignedOutput.attr('data-input-id');
state.unassignedChannels.push(channelId);
})
context._.each($tracks, function($track, index) {
$track = $($track);
var $assignedChannels = $track.find('.ftue-input');
var track = {index: index, channels:[]};
context._.each($assignedChannels, function($assignedChannel) {
$assignedChannel = $($assignedChannel);
track.channels.push($assignedChannel.attr('data-input-id'))
});
// sparse array
if(track.channels.length > 0) {
state.tracks.push(track);
}
var $instrument = $instrumentsHolder.find('[data-num="' + index + '"]').find('.icon-instrument-select');
track.instrument_id = $instrument.data('instrument_id');
})
context._.each($outputs, function($output, index) {
$output = $($output);
var $assignedChannel = $output.find('.ftue-input');
// this is overkill since there should only be 1 or 0 .ftue-inputs in a given .output
var outputSlot = {index: index, channels:[]};
context._.each($assignedChannel, function($assignedChannel) {
$assignedChannel = $($assignedChannel);
outputSlot.channels.push($assignedChannel.attr('data-input-id'))
});
// sparse array
if(outputSlot.channels.length > 0) {
state.outputs.push(outputSlot);
}
})
return state;
}
function validate(tracks) {
// there must be at least one assigned channel
if(tracks.tracks.length == 0) {
logger.debug("ConfigureTracks validation error: must have assigned at least one input port to a track.");
context.JK.Banner.showAlert('Must have assigned at least one input port to a track.');
return false;
}
// there must be some instruments
context._.each(tracks.tracks, function(track) {
if(!track.instrument_id) {
logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument.");
context.JK.Banner.showAlert('All tracks with ports assigned must specify an instrument.');
return false;
}
});
// there must be exactly 2 output channels assigned
if(tracks.outputs.length != 2 || (tracks.outputs[0].channels.length != 1 && track.outputs[1].channels.length != 1)) {
logger.debug("ConfigureTracks validation error: must have assigned exactly two output ports");
context.JK.Banner.showAlert('Must have assigned exactly 2 output ports.');
return false;
}
return true;
}
function save(state) {
context._.each(state.unassignedChannels, function(unassignedChannelId) {
context.jamClient.TrackSetAssignment(unassignedChannelId, true, ASSIGNMENT.UNASSIGNED);
});
// save input/tracks
context._.each(state.tracks, function(track, index) {
var trackNumber = index + 1;
context._.each(track.channels, function(channelId) {
context.jamClient.TrackSetAssignment(channelId, true, trackNumber);
});
logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", trackNumber, track.instrument_id);
context.jamClient.TrackSetInstrument(trackNumber, context.JK.instrument_id_to_instrument[track.instrument_id].client_id);
});
// save outputs
context._.each(state.outputs, function(output, index) {
context._.each(output.channels, function(channelId) {
context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT);
});
});
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
return true;
}
else {
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
return false;
}
}
function loadTrackInstruments(forceInputsToUnassign) {
var $trackInstruments = $instrumentsHolder.find('.track-instrument');
context._.each($trackInstruments, function(trackInstrument) {
var $trackInstrument = $(trackInstrument);
var trackIndex = parseInt($trackInstrument.attr('data-num')) + 1;
var clientInstrument = context.jamClient.TrackGetInstrument(trackIndex);
var instrument = context.JK.client_to_server_instrument_map[clientInstrument];
$trackInstrument.instrumentSelectorSet(instrument ? instrument.server_id : instrument);
});
}
function trySave() {
var state = getCurrentState();
if(!validate(state)) {
return false;
}
var saved = save(state);
if(saved) {
context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS());
}
return saved;
}
function reset(forceInputsToUnassign) {
loadChannels(forceInputsToUnassign);
loadTrackInstruments(forceInputsToUnassign);
}
function unassignOutputChannel($channel) {
var $originallyAssignedTrack = $channel.closest('.output-target');
$unassignedOutputsHolder.append($channel);
$originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length);
}
function unassignInputChannel($channel) {
var $originallyAssignedTrack = $channel.closest('.track-target');
$unassignedInputsHolder.append($channel);
$originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length);
}
function addChannelToTrack($channel, $track) {
var $originallyAssignedTrack = $channel.closest('.track-target');
$track.append($channel);
$track.attr('track-count', $track.find('.ftue-input:not(.ui-draggable-dragging)').length);
$originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length)
}
function addChannelToOutput($channel, $slot) {
var $originallyAssignedTrack = $channel.closest('.output-target');
$slot.append($channel);
$slot.attr('output-count', $slot.find('.ftue-input:not(.ui-draggable-dragging)').length);
$originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length)
}
function initializeUnassignedOutputDroppable() {
$unassignedOutputsHolder.droppable(
{
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
unassignOutputChannel($channel);
}
});
}
function initializeUnassignedInputDroppable() {
$unassignedInputsHolder.droppable(
{
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
unassignInputChannel($channel);
}
});
}
function initializeOutputDroppables() {
var i;
for(i = 0; i < MAX_OUTPUTS; i++) {
var $target = $(context._.template($templateOutputTarget.html(), {num: i }, { variable: 'data' }));
$outputChannelHolder.append($target);
$target.find('.output-target').droppable(
{
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $slot = $(this);
if($slot.attr('output-count') == 1) {
return false; // max of 1 output per slot
}
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
addChannelToOutput($channel, $slot);
}
});
}
}
function initializeTrackDroppables() {
var i;
for(i = 0; i < MAX_TRACKS; i++) {
var $target = $(context._.template($templateTrackTarget.html(), {num: i }, { variable: 'data' }));
$tracksHolder.append($target);
$target.find('.track-target').droppable(
{
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $track = $(this);
if($track.attr('track-count') == 2) {
return false; // max of 2 inputs per track
}
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
addChannelToTrack($channel, $track);
}
});
}
}
function initializeInstrumentDropdown() {
var i;
for(i = 0; i < MAX_TRACKS; i++) {
var $root = $('<div class="track-instrument"></div>');
$root.instrumentSelector().attr('data-num', i);
$instrumentsHolder.append($root);
}
}
function initialize(_$parent) {
$parent = _$parent;
$templateAssignablePort = $('#template-assignable-port');
$templateTrackTarget = $('#template-track-target');
$templateOutputTarget = $('#template-output-target');
$unassignedInputsHolder = $parent.find('.unassigned-input-channels')
$unassignedOutputsHolder = $parent.find('.unassigned-output-channels');
$tracksHolder = $parent.find('.tracks');
$instrumentsHolder = $parent.find('.instruments');
$outputChannelHolder = $parent.find('.output-channels');
initializeUnassignedInputDroppable();
initializeTrackDroppables();
initializeInstrumentDropdown();
initializeUnassignedOutputDroppable();
initializeOutputDroppables();
}
this.initialize = initialize;
this.trySave = trySave;
this.reset = reset;
return this;
};
})(window, jQuery);

View File

@ -172,6 +172,7 @@
if ($('#band-list option:selected').val() !== '') {
data.band = $('#band-list option:selected').val();
}
data.audio_latency = context.jamClient.FTUEGetExpectedLatency().latency;
// 1. If no previous session data, a single stereo track with the
// top instrument in the user's profile.

View File

@ -168,6 +168,9 @@
]
};
}
function FTUEClearChannelAssignments(){}
function FTUEClearChatInput(){}
function FTUEGetAudioDevices() {
return {
"devices": [
@ -235,21 +238,25 @@
}
function FTUEGetGoodConfigurationList() {
return ['a'];
return ['default'];
}
function FTUEGetAllAudioConfigurations() {
return ['a'];
return ['default'];
}
function FTUEGetGoodAudioConfigurations() {
return ['a'];
return ['default'];
}
function FTUEGetConfigurationDevice() {
return 'Good Device';
}
function FTUELoadAudioConfiguration() {
return true;
}
function FTUEIsMusicDeviceWDM() {
return false;
}
@ -850,6 +857,9 @@
this.FTUEGetGoodAudioConfigurations = FTUEGetGoodAudioConfigurations;
this.FTUEGetConfigurationDevice = FTUEGetConfigurationDevice;
this.FTUEIsMusicDeviceWDM = FTUEIsMusicDeviceWDM;
this.FTUELoadAudioConfiguration = FTUELoadAudioConfiguration;
this.FTUEClearChannelAssignments = FTUEClearChannelAssignments;
this.FTUEClearChatInput = FTUEClearChatInput;
// Session
this.SessionAddTrack = SessionAddTrack;

View File

@ -78,6 +78,30 @@
}
}
function score_to_text(score) {
// these are raw scores as reported by client (round trip times)
if (score == null) return "n/a";
return Math.round(score / 2) + " ms";
}
function score_to_color(score) {
// these are raw scores as reported by client (round trip times)
if (score == null) return "purple";
if (0 < score && score <= 40) return "green";
if (40 < score && score <= 80) return "yellow";
if (80 < score && score <= 120) return "red";
return "blue";
}
function score_to_color_alt(score) {
// these are raw scores as reported by client (round trip times)
if (score == null) return "missing";
if (0 < score && score <= 40) return "good";
if (40 < score && score <= 80) return "moderate";
if (80 < score && score <= 120) return "poor";
return "unacceptable";
}
function renderMusicians() {
var ii, len;
var mTemplate = $('#template-find-musician-row').html();
@ -108,7 +132,7 @@
user_id: aFollow.user_id,
musician_name: aFollow.name,
profile_url: '/client#/profile/' + aFollow.user_id,
avatar_url: context.JK.resolveAvatarUrl(aFollow.photo_url),
avatar_url: context.JK.resolveAvatarUrl(aFollow.photo_url)
};
follows += context.JK.fillTemplate(fTemplate, followVals);
if (2 == jj) break;
@ -125,6 +149,7 @@
};
var musician_actions = context.JK.fillTemplate(aTemplate, actionVals);
var joined_score = musician['joined_score']
mVals = {
avatar_url: context.JK.resolveAvatarUrl(musician.photo_url),
profile_url: "/client#/profile/" + musician.id,
@ -138,7 +163,10 @@
session_count: musician['session_count'],
musician_id: musician['id'],
musician_follow_template: follows,
musician_action_template: musician_actions
musician_action_template: musician_actions,
musician_one_way_score: score_to_text(joined_score),
musician_score_color: score_to_color(joined_score),
musician_score_color_alt: score_to_color_alt(joined_score)
};
var musician_row = context.JK.fillTemplate(mTemplate, mVals);
renderings += musician_row;

View File

@ -27,7 +27,12 @@
CHAT: "1"
};
context.JK.EVENTS = {
DIALOG_CLOSED : 'dialog_closed'
}
context.JK.MAX_TRACKS = 6;
context.JK.MAX_OUTPUTS = 2;
// TODO: store these client_id values in instruments table, or store
// server_id as the client_id to prevent maintenance nightmares. As it's

View File

@ -1119,6 +1119,17 @@
});
}
function updateAudioLatency(options) {
var id = getId(options);
return $.ajax({
type: "POST",
url: '/api/users/' + id + '/audio_latency',
dataType: "json",
contentType: 'application/json',
data: options,
});
}
function initialize() {
return self;
}
@ -1216,6 +1227,7 @@
this.getChatMessages = getChatMessages;
this.createDiagnostic = createDiagnostic;
this.getLatencyTester = getLatencyTester;
this.updateAudioLatency = updateAudioLatency;
return this;
};

View File

@ -27,6 +27,7 @@
// privates
var logger = context.JK.logger;
var EVENTS = context.JK.EVENTS;
var NOT_HANDLED = "not handled";
var me = null; // Reference to this instance for context sanity.
@ -433,8 +434,9 @@
var $overlay = $('.dialog-overlay');
unstackDialogs($overlay);
$dialog.hide();
$dialog.triggerHandler(EVENTS.DIALOG_CLOSED, {name: dialog, dialogCount: openDialogs.length});
$(context).triggerHandler(EVENTS.DIALOG_CLOSED, {name: dialog, dialogCount: openDialogs.length})
dialogEvent(dialog, 'afterHide');
$(me).triggerHandler('dialog_closed', {dialogCount: openDialogs.length})
}
function screenEvent(screen, evtName, data) {
@ -538,6 +540,11 @@
*/
layout();
// add an attribute to any dialogs, which let's it know it's current screen (useful for contextual styling)
context._.each(openDialogs, function(dialog) {
addScreenContextToDialog($(dialog));
})
screenEvent(previousScreen, 'afterHide', data);
screenEvent(currentScreen, 'afterShow', data);
@ -631,6 +638,10 @@
}
}
function addScreenContextToDialog($dialog) {
$dialog.attr('current-screen', currentScreen); // useful for contextual styling of dialogs
}
function showDialog(dialog, options) {
if (dialogEvent(dialog, 'beforeShow', options) === false) {
return;
@ -649,8 +660,10 @@
centerDialog(dialog);
var $dialog = $('[layout-id="' + dialog + '"]');
stackDialogs($dialog, $overlay);
addScreenContextToDialog($dialog)
$dialog.show();
dialogEvent(dialog, 'afterShow', options);
return $dialog;
}
function centerDialog(dialog) {
@ -910,7 +923,7 @@
}
this.showDialog = function (dialog, options) {
showDialog(dialog, options);
return showDialog(dialog, options);
};
this.dialogObscuredNotification = function(payload) {
@ -933,6 +946,9 @@
return activeElementEvent(evtName, data);
}
this.getCurrentScreen = function() {
return currentScreen; // will be a string of the layout-id of the active screen
}
this.close = function (evt) {
close(evt);
};

View File

@ -0,0 +1,515 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.NetworkTest = function (app) {
var NETWORK_TEST_TYPES = {
RestPhase : 0,
PktTest100NormalLatency : 1,
PktTest200MediumLatency : 2,
PktTest400LowLatency : 3,
PktTestRateSweep : 4,
RcvOnly : 5
}
var STARTING_NUM_CLIENTS = 4;
var PAYLOAD_SIZE = 100;
var MINIMUM_ACCEPTABLE_SESSION_SIZE = 2;
var rest = context.JK.Rest();
var logger = context.JK.logger;
var $step = null;
var TEST_SUCCESS_CALLBACK = 'JK.HandleNetworkTestSuccess';
var TEST_TIMEOUT_CALLBACK = 'JK.HandleNetworkTestTimeout';
var $startNetworkTestBtn = null;
var $testResults = null;
var $testScore = null;
var $testText = null;
var testedSuccessfully = false;
var $scoringBar = null;
var $goodMarker = null;
var $goodLine = null;
var $currentScore = null;
var $scoredClients = null;
var $subscore = null;
var backendGuardTimeout = null;
var serverClientId = '';
var scoring = false;
var numClientsToTest = STARTING_NUM_CLIENTS;
var testSummary = {attempts : [], final:null}
var $self = $(this);
var scoringZoneWidth = 100;//px
var inGearWizard = false;
var NETWORK_TEST_START = 'network_test.start';
var NETWORK_TEST_DONE = 'network_test.done';
var NETWORK_TEST_FAIL = 'network_test.fail';
function createSuccessCallbackName() {
if(inGearWizard) {
return TEST_SUCCESS_CALLBACK + 'ForGearWizard';
}
else {
return TEST_SUCCESS_CALLBACK + 'ForDialog';
}
}
function createTimeoutCallbackName() {
if(inGearWizard) {
return TEST_TIMEOUT_CALLBACK + 'ForGearWizard';
}
else {
return TEST_TIMEOUT_CALLBACK + 'ForDialog';
}
}
function reset() {
serverClientId = '';
scoring = false;
numClientsToTest = STARTING_NUM_CLIENTS;
testSummary = {attempts : []};
configureStartButton();
$scoredClients.empty();
$testResults.removeClass('good acceptable bad testing');
$testText.empty();
$subscore.empty();
$currentScore.width(0);
}
function renderStartTest() {
configureStartButton();
$testResults.addClass('testing');
$goodLine.css('left', (gon.ftue_packet_rate_treshold * 100) + '%');
$goodMarker.css('left', (gon.ftue_packet_rate_treshold * 100) + '%');
}
function renderStopTest(score, text) {
$scoredClients.html(score);
$testText.html(text);
$testResults.removeClass('testing');
}
function postDiagnostic() {
rest.createDiagnostic({
type: 'NETWORK_TEST_RESULT',
data: {client_type: context.JK.clientType(), client_id: context.JK.JamServer.clientID, summary:testSummary}
});
}
function appendContextualStatement() {
if(inGearWizard) {
return "You can skip this step for now."
}
else {
return '';
}
}
function testFinished() {
var attempt = getCurrentAttempt();
if(!testSummary.final) {
testSummary.final = {reason : attempt.reason};
}
var reason = testSummary.final.reason;
var success = false;
if(reason == "success") {
renderStopTest(attempt.num_clients, "Your router and Internet service will support sessions of up to " + attempt.num_clients + " JamKazam musicians.")
testedSuccessfully = true;
if(!testSummary.final.num_clients) {
testSummary.final.num_clients = attempt.num_clients;
}
context.JK.GA.trackNetworkTest(context.JK.detectOS(), testSummary.final.num_clients);
context.jamClient.SetNetworkTestScore(attempt.num_clients);
if(testSummary.final.num_clients == 2) {
$testResults.addClass('acceptable');
}
else {
$testResults.addClass('good');
}
success = true;
}
else if(reason == "minimum_client_threshold") {
context.jamClient.SetNetworkTestScore(0);
renderStopTest('', "We're sorry, but your router and Internet service will not effectively support JamKazam sessions. Please click the HELP button for more information.")
}
else if(reason == "unreachable") {
context.jamClient.SetNetworkTestScore(0);
renderStopTest('', "We're sorry, but your router will not support JamKazam in its current configuration. Please click the HELP button for more information.");
}
else if(reason == "internal_error") {
context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection.");
renderStopTest('', '');
}
else if(reason == "remote_peer_cant_test") {
context.JK.alertSupportedNeeded("The JamKazam service is experiencing technical difficulties.");
renderStopTest('', '');
}
else if(reason == 'backend_gone') {
context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties.");
renderStopTest('', '');
}
else if(reason == "invalid_response") {
context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection. Reason=" + attempt.backend_data.reason + '.');
renderStopTest('', '');
}
else if(reason == 'no_servers') {
context.JK.alertSupportedNeeded("No network test servers are available." + appendContextualStatement());
renderStopTest('', '');
testedSuccessfully = true;
}
else if(reason == 'no_network') {
context.JK.Banner.showAlert("Please try again later. Your network appears down.");
renderStopTest('', '');
}
else if(reason == "rest_api_error") {
context.JK.alertSupportedNeeded("Unable to acquire a network test server." + appendContextualStatement());
testedSuccessfully = true;
renderStopTest('', '');
}
else if(reason == "timeout") {
context.JK.alertSupportedNeeded("Communication with a network test servers timed out." + appendContextualStatement());
testedSuccessfully = true;
renderStopTest('', '');
}
else {
context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection.");
renderStopTest('', '');
}
numClientsToTest = STARTING_NUM_CLIENTS;
scoring = false;
configureStartButton();
postDiagnostic();
if(success) {
$self.triggerHandler(NETWORK_TEST_DONE)
}
else {
$self.triggerHandler(NETWORK_TEST_FAIL)
}
}
function getCurrentAttempt() {
return testSummary.attempts[testSummary.attempts.length - 1];
}
function backendTimedOut() {
testSummary.final = {reason: 'backend_gone'}
testFinished();
}
function cancel() {
clearBackendGuard();
}
function clearBackendGuard() {
if(backendGuardTimeout) {
clearTimeout(backendGuardTimeout);
backendGuardTimeout = null;
}
}
function attemptTestPass() {
var attempt = {};
attempt.payload_size = PAYLOAD_SIZE;
attempt.duration = gon.ftue_network_test_duration;
attempt.test_type = 'PktTest400LowLatency';
attempt.num_clients = numClientsToTest;
attempt.server_client_id = serverClientId;
attempt.received_progress = false;
testSummary.attempts.push(attempt);
//context.jamClient.StopNetworkTest('');
$testText.text("Simulating the network traffic of a " + numClientsToTest + "-person session.");
updateProgress(0, false);
backendGuardTimeout = setTimeout(function(){backendTimedOut()}, (gon.ftue_network_test_duration + 1) * 1000);
context.jamClient.TestNetworkPktBwRate(serverClientId, createSuccessCallbackName(), createTimeoutCallbackName(),
NETWORK_TEST_TYPES.PktTest400LowLatency,
gon.ftue_network_test_duration,
numClientsToTest,
PAYLOAD_SIZE);
}
function startNetworkTest(checkWireless) {
if(scoring) return false;
if(checkWireless) {
// check if on Wifi 1st
var isWireless = context.jamClient.IsMyNetworkWireless();
if(isWireless == -1) {
logger.warn("unable to determine if the user is on wireless or not for network test. skipping prompt.")
}
else if(isWireless == 1) {
context.JK.Banner.showAlert({buttons: [
{name: 'RUN NETWORK TEST ANYWAY', click: function() {startNetworkTest(false)}},
{name: 'CANCEL', click: function() {}}],
html: "<p>It appears that your computer is connected to your network using WiFi.</p>" +
"<p>We strongly advise against running the JamKazam application on a WiFi connection. " +
"We recommend using a wired Ethernet connection from your computer to your router. " +
"A WiFi connection is likely to cause significant issues in both latency and audio quality.</p>"})
return false;
}
}
reset();
scoring = true;
$self.triggerHandler(NETWORK_TEST_START);
renderStartTest();
rest.getLatencyTester()
.done(function(response) {
// ensure there are no tests ongoing
serverClientId = response.client_id;
testSummary.serverClientId = serverClientId;
logger.info("beginning network test against client_id: " + serverClientId);
attemptTestPass();
})
.fail(function(jqXHR) {
if(jqXHR.status == 404) {
// means there are no network testers available.
// we have to skip this part of the UI
testSummary.final = {reason: 'no_servers'}
}
else {
if(context.JK.isNetworkError(arguments)) {
testSummary.final = {reason: 'no_network'}
}
else {
testSummary.final = {reason: 'rest_api_error'}
}
}
testFinished();
})
logger.info("starting network test");
return false;
}
function updateProgress(throughput, showSubscore) {
var width = throughput * 100;
$currentScore.stop().data('showSubscore', showSubscore);
if(!showSubscore) {
$subscore.text('');
}
$currentScore.animate({
duration: 1000,
width: width + '%'
}, {
step: function (now, fx) {
if(showSubscore) {
var newWidth = ( 100 * parseFloat($currentScore.css('width')) / parseFloat($currentScore.parent().css('width')) );
$subscore.text((Math.round(newWidth * 10) / 10) + '%');
}
}
}).css('overflow', 'visible');
;
}
function networkTestSuccess(data) {
clearBackendGuard();
var attempt = getCurrentAttempt();
function refineTest(up) {
if(up) {
if(numClientsToTest == gon.ftue_network_test_max_clients) {
attempt.reason = "success";
testFinished();
}
else {
numClientsToTest++;
logger.debug("increasing number of clients to " + numClientsToTest);
setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm
}
}
else {
// reduce numclients if we can
if(numClientsToTest == MINIMUM_ACCEPTABLE_SESSION_SIZE) {
// we are too low already. fail the user
attempt.reason = "minimum_client_threshold";
testFinished();
}
else if(numClientsToTest > STARTING_NUM_CLIENTS) {
// this means we've gone up before... so don't go back down (i.e., creating a loop)
attempt.reason = "success";
testSummary.final = { reason:'success', num_clients: numClientsToTest - 1 }
testFinished();
}
else {
numClientsToTest--;
logger.debug("reducing number of clients to " + numClientsToTest);
setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm
}
}
}
attempt.backend_data = data;
if(data.progress === true) {
var animate = true;
if(data.downthroughput && data.upthroughput) {
if(data.downthroughput > 0 || data.upthroughput > 0) {
attempt.received_progress = true;
animate = true;
}
if(attempt.received_progress) {
// take the lower
var throughput= data.downthroughput < data.upthroughput ? data.downthroughput : data.upthroughput;
updateProgress(throughput, true);
}
}
}
else {
logger.debug("network test pass success. data: ", data);
if(data.reason == "unreachable") {
// STUN
logger.debug("network test: unreachable (STUN issue or similar)");
attempt.reason = data.reason;
testFinished();
}
else if(data.reason == "internal_error") {
// oops
logger.debug("network test: internal_error (client had a unexpected problem)");
attempt.reason = data.reason;
testFinished();
}
else if(data.reason == "remote_peer_cant_test") {
// old client
logger.debug("network test: remote_peer_cant_test (old client)")
attempt.reason = data.reason;
testFinished();
}
else {
if(!data.downthroughput || !data.upthroughput) {
// we have to assume this is bad. just not a reason we know about in code
logger.debug("network test: no test data (unknown issue? " + data.reason + ")")
attempt.reason = "invalid_response";
testFinished();
}
else {
// success... but we still have to verify if this data is within threshold
if(data.downthroughput < gon.ftue_packet_rate_treshold) {
logger.debug("network test: downthroughput too low. downthroughput: " + data.downthroughput + ", threshold: " + gon.ftue_packet_rate_treshold);
refineTest(false);
}
else if(data.upthroughput < gon.ftue_packet_rate_treshold) {
logger.debug("network test: upthroughput too low. upthroughput: " + data.upthroughput + ", threshold: " + gon.ftue_packet_rate_treshold);
refineTest(false);
}
else {
// true success. we can accept this score
logger.debug("network test: success")
refineTest(true);
}
}
}
// VRFS-1742
// context.jamClient.StopNetworkTest(serverClientId);
}
}
function networkTestTimeout(data) {
clearBackendGuard();
logger.warn("network timeout when testing latency test: " + data);
var attempt = getCurrentAttempt();
attempt.reason = 'timeout';
attempt.backend_data = data;
testFinished();
}
function hasScoredNetworkSuccessfully() {
return testedSuccessfully;
}
function configureStartButton() {
if(scoring) {
$startNetworkTestBtn.text('NETWORK TEST RUNNING...').removeClass('button-orange').addClass('button-grey')
}
else {
$startNetworkTestBtn.text('START NETWORK TEST').removeClass('button-grey').addClass('button-orange');
}
}
function initializeNextButtonState() {
$dialog.setNextState(hasScoredNetworkSuccessfully() && !scoring);
}
function initializeBackButtonState() {
$dialog.setBackState(!scoring);
}
function beforeHide() {
clearBackendGuard();
}
function initialize(_$step, _inGearWizard) {
$step = _$step;
inGearWizard = _inGearWizard;
$startNetworkTestBtn = $step.find('.start-network-test');
if($startNetworkTestBtn.length == 0) throw 'no start network test button found in network-test'
$testResults = $step.find('.network-test-results');
$testScore = $step.find('.network-test-score');
$testText = $step.find('.network-test-text');
$scoringBar = $step.find('.scoring-bar');
$goodMarker = $step.find('.good-marker');
$goodLine =$step.find('.good-line');
$currentScore = $step.find('.current-score');
$scoredClients= $step.find('.scored-clients');
$subscore = $step.find('.subscore');
$startNetworkTestBtn.on('click', startNetworkTest);
// if this network test is instantiated anywhere else than the gearWizard, or a dialog, then this will have to be expanded
if(inGearWizard) {
context.JK.HandleNetworkTestSuccessForGearWizard = function(data) { networkTestSuccess(data)}; // pin to global for bridge callback
context.JK.HandleNetworkTestTimeoutForGearWizard = function(data) { networkTestTimeout(data)}; // pin to global for bridge callback
}
else {
context.JK.HandleNetworkTestSuccessForDialog = function(data) { networkTestSuccess(data)}; // pin to global for bridge callback
context.JK.HandleNetworkTestTimeoutForDialog = function(data) { networkTestTimeout(data)}; // pin to global for bridge callback
}
}
this.isScoring = function () { return scoring; };
this.hasScoredNetworkSuccessfully = hasScoredNetworkSuccessfully;
this.initialize = initialize;
this.reset = reset;
this.cancel = cancel;
this.NETWORK_TEST_START = NETWORK_TEST_START;
this.NETWORK_TEST_DONE = NETWORK_TEST_DONE;
this.NETWORK_TEST_FAIL = NETWORK_TEST_FAIL;
return this;
}
})(window, jQuery);

View File

@ -0,0 +1,94 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.NetworkTestDialog = function (app) {
var logger = context.JK.logger;
var $dialog = null;
var $btnCancel = null;
var $btnClose = null;
var $btnHelp = null;
var networkTest = new context.JK.NetworkTest(app);
function networkTestDone() {
unfreezeInteractions();
}
function networkTestFail() {
unfreezeInteractions();
}
function networkTestStart() {
freezeInteractions();
}
function freezeInteractions() {
$btnClose.removeClass('button-orange').addClass('button-grey');
}
function unfreezeInteractions() {
$btnClose.removeClass('button-grey').addClass('button-orange');
}
function events() {
$btnCancel.on('click', function() {
// should we stop this if the test is going?
app.layout.closeDialog('network-test')
return false;
})
$btnClose.on('click', function(e) {
if(!networkTest.isScoring()) {
app.layout.closeDialog('network-test');
}
return false;
})
$btnHelp.on('click', function(e) {
context.JK.Banner.showAlert('No help is yet available for the network test.');
return false;
})
}
function beforeShow() {
if(!networkTest.isScoring()) {
networkTest.reset();
}
}
function afterHide() {
}
function initialize() {
var dialogBindings = {
'beforeShow' : beforeShow,
'afterHide': afterHide
};
app.bindDialog('network-test', dialogBindings);
$dialog = $('#network-test-dialog');
$btnCancel = $dialog.find('.btn-cancel');
$btnHelp = $dialog.find('.btn-help');
$btnClose = $dialog.find('.btn-close');
networkTest.initialize($dialog, false);
$(networkTest)
.on(networkTest.NETWORK_TEST_DONE, networkTestDone)
.on(networkTest.NETWORK_TEST_FAIL, networkTestFail)
.on(networkTest.NETWORK_TEST_START, networkTestStart)
events();
}
this.initialize = initialize;
return this;
};
})(window, jQuery);

View File

@ -4,6 +4,7 @@
context.JK = context.JK || {};
context.JK.NotificationPanel = function(app) {
var EVENTS = context.JK.EVENTS;
var logger = context.JK.logger;
var friends = [];
var rest = context.JK.Rest();
@ -138,7 +139,7 @@
}
function events() {
$(app.layout).on('dialog_closed', function(e, data) {if(data.dialogCount == 0) userCameBack(); });
$(context).on(EVENTS.DIALOG_CLOSED, function(e, data) {if(data.dialogCount == 0) userCameBack(); });
$(window).focus(userCameBack);
$(window).blur(windowBlurred);
app.user()

View File

@ -368,7 +368,8 @@
ip_address: server.publicIP,
as_musician: true,
tracks: tracks,
session_id: sessionId
session_id: sessionId,
audio_latency: context.jamClient.FTUEGetExpectedLatency().latency
};
return rest.joinSession(data);

View File

@ -50,6 +50,11 @@
invitationDialog.showFacebookDialog(e);
});
$('.shortcuts .test-network').on('click', function(e) {
app.layout.showDialog('network-test');
return false;
})
$('#header-avatar').on('avatar_changed', function (event, newAvatarUrl) {
updateAvatar(newAvatarUrl);
event.preventDefault();
@ -84,7 +89,6 @@
}
}
// initially show avatar
function showAvatar() {
var photoUrl = context.JK.resolveAvatarUrl(userMe.photo_url);

View File

@ -142,6 +142,15 @@
$element.data("prodTimer", null);
$element.btOff();
}, options['duration']));
$element.on('remove', function() {
var timer = $element.data('prodTimer')
if(timer) {
clearTimeout(timer);
$element.data("prodTimer", null);
$element.btOff();
}
})
}
/**
* Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips)
@ -574,111 +583,6 @@
return keys;
};
context.JK.createProfileName = function(deviceInfo, chatName) {
var isSameInOut = deviceInfo.input.id == deviceInfo.output.id;
var name = null;
if(isSameInOut) {
name = "In/Out: " + deviceInfo.input.info.displayName;
}
else {
name = "In: " + deviceInfo.input.info.displayName + ", Out: " + deviceInfo.output.info.displayName
}
logger.debug("creating profile name: " + name);
return name;
}
context.JK.selectedDeviceInfo = function(audioInputDeviceId, audioOutputDeviceId, deviceInformation) {
if(!deviceInformation) {
deviceInformation = context.JK.loadDeviceInfo();
}
var input = deviceInformation[audioInputDeviceId];
var output = deviceInformation[audioOutputDeviceId];
var inputBehavior = AUDIO_DEVICE_BEHAVIOR[input.type];
var outputBehavior = AUDIO_DEVICE_BEHAVIOR[output.type];
return {
input: {
id: audioInputDeviceId,
info: input,
behavior: inputBehavior
},
output: {
id: audioOutputDeviceId,
info: output,
behavior: outputBehavior
}
}
}
context.JK.loadDeviceInfo = function() {
var operatingSystem = context.JK.GetOSAsString();
// should return one of:
// * MacOSX_builtin
// * MACOSX_interface
// * Win32_wdm
// * Win32_asio
// * Win32_asio4all
// * Linux
function determineDeviceType(deviceId, displayName) {
if (operatingSystem == "MacOSX") {
if (displayName.toLowerCase().trim() == "built-in") {
return "MacOSX_builtin";
}
else {
return "MacOSX_interface";
}
}
else if (operatingSystem == "Win32") {
if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) {
return "Win32_wdm";
}
else if (displayName.toLowerCase().indexOf("asio4all") > -1) {
return "Win32_asio4all"
}
else {
return "Win32_asio";
}
}
else {
return "Linux";
}
}
var devices = context.jamClient.FTUEGetAudioDevices();
logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices));
var loadedDevices = {};
// augment these devices by determining their type
context._.each(devices.devices, function (device) {
if (device.name == "JamKazam Virtual Monitor") {
return;
}
var deviceInfo = {};
deviceInfo.id = device.guid;
deviceInfo.type = determineDeviceType(device.guid, device.display_name);
deviceInfo.displayType = AUDIO_DEVICE_BEHAVIOR[deviceInfo.type].display;
deviceInfo.displayName = device.display_name;
deviceInfo.inputCount = device.input_count;
deviceInfo.outputCount = device.output_count;
loadedDevices[device.guid] = deviceInfo;
})
logger.debug(context.JK.dlen(loadedDevices) + " devices loaded.", loadedDevices);
return loadedDevices;
}
/**
* Finds the first error associated with the field.
* @param fieldName the name of the field

View File

@ -0,0 +1,261 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.VoiceChatHelper = function (app) {
var logger = context.JK.logger;
var ASSIGNMENT = context.JK.ASSIGNMENT;
var VOICE_CHAT = context.JK.VOICE_CHAT;
var MAX_TRACKS = context.JK.MAX_TRACKS;
var MAX_OUTPUTS = context.JK.MAX_OUTPUTS;
var gearUtils = context.JK.GearUtils;
var $parent = null;
var $reuseAudioInputRadio = null;
var $useChatInputRadio = null;
var $chatInputs = null;
var $templateChatInput = null;
var $selectedChatInput = null;// should only be used if isChatEnabled = true
var saveImmediate = null; // if true, then every action by the user results in a save to the backend immediately, false means you have to call trySave to persist
// needed because iCheck fires iChecked event even when you programmatically change it, unlike when using .val(x)
var ignoreICheckEvent = false;
function defaultReuse() {
suppressChange(function(){
$reuseAudioInputRadio.iCheck('check').attr('checked', 'checked');
$useChatInputRadio.removeAttr('checked');
})
}
function isChatEnabled() {
return $useChatInputRadio.is(':checked');
}
function reset(forceDisabledChat) {
$selectedChatInput = null;
if(!forceDisabledChat && context.jamClient.TrackGetChatEnable()) {
enableChat(false);
}
else {
disableChat(false);
}
$chatInputs.empty();
var chatInputs = gearUtils.getChatInputs();
context._.each(chatInputs, function(chatInput) {
if(chatInput.assignment > 0) {
return;
}
var chatChannelName = chatInput.name;
var chatChannelId = chatInput.id;
var isCurrentlyChat = chatInput.assignment == ASSIGNMENT.CHAT;
var $chat = $(context._.template($templateChatInput.html(), {id: chatChannelId, name: chatChannelName}, { variable: 'data' }));
var $chatInput = $chat.find('input');
if(isCurrentlyChat) {
$selectedChatInput = $chatInput;
$selectedChatInput.attr('checked', 'checked');
}
$chat.hide(); // we'll show it once it's styled with iCheck
$chatInputs.append($chat);
});
var $radioButtons = $chatInputs.find('input[name="chat-device"]');
context.JK.checkbox($radioButtons).on('ifChecked', function(e) {
var $input = $(e.currentTarget);
$selectedChatInput = $input; // for use in handleNext
if(saveImmediate) {
var channelId = $input.attr('data-channel-id');
context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT);
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
}
else {
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
return false;
}
}
});
if(!isChatEnabled()) {
disableChatButtonsUI();
}
$chatInputs.find('.chat-input').show().on('click', function() {
// obnoxious; remove soon XXX
// if(!isChatEnabled()) {
// context.JK.prodBubble($parent.find('.use-chat-input h3'), 'chat-not-enabled', {}, { positions:['left']});
// }
})
}
function disableChatButtonsUI() {
var $radioButtons = $chatInputs.find('input[name="chat-device"]');
$radioButtons.iCheck('disable')
$chatInputs.addClass('disabled');
}
function enableChatButtonsUI() {
var $radioButtons = $chatInputs.find('input[name="chat-device"]');
$radioButtons.iCheck('enable')
$chatInputs.removeClass('disabled');
}
function suppressChange(proc) {
ignoreICheckEvent = true;
try {
proc();
}
finally {
ignoreICheckEvent = false;
}
}
function disableChat(applyToBackend) {
if(saveImmediate && applyToBackend) {
logger.debug("voiceChatHelper: disabling chat to backend");
var state = getCurrentState();
//context.jamClient.TrackSetChatEnable(false);
//if(state.chat_channel) {
// context.jamClient.TrackSetAssignment(state.chat_channel, true, ASSIGNMENT.UNASSIGNED);
//}
context.jamClient.FTUEClearChatInput();
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
suppressChange(function() {
$reuseAudioInputRadio.iCheck('check').attr('checked', 'checked');
$useChatInputRadio.removeAttr('checked');
})
}
else {
context.JK.Banner.showAlert('Unable to disable chat. ' + result);
return false;
}
}
else {
logger.debug("voiceChatHelper: disabling chat UI only");
suppressChange(function() {
$reuseAudioInputRadio.iCheck('check').attr('checked', 'checked');
$useChatInputRadio.removeAttr('checked');
})
}
disableChatButtonsUI()
}
function enableChat(applyToBackend) {
if(saveImmediate && applyToBackend) {
logger.debug("voiceChatHelper: enabling chat to backend");
context.jamClient.TrackSetChatEnable(true);
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
suppressChange(function() {
$useChatInputRadio.iCheck('check').attr('checked', 'checked');
$reuseAudioInputRadio.removeAttr('checked');
})
}
else {
context.JK.Banner.showAlert('Unable to enable chat. ' + result);
return false;
}
}
else {
logger.debug("voiceChatHelper: enabling chat UI only");
suppressChange(function() {
$useChatInputRadio.iCheck('check').attr('checked', 'checked');
$reuseAudioInputRadio.removeAttr('checked');
})
}
enableChatButtonsUI();
}
function handleChatEnabledToggle() {
context.JK.checkbox($reuseAudioInputRadio);
context.JK.checkbox($useChatInputRadio);
// plugin sets to relative on the element; have to do this as an override
$reuseAudioInputRadio.closest('.iradio_minimal').css('position', 'absolute');
$useChatInputRadio.closest('.iradio_minimal').css('position', 'absolute');
$reuseAudioInputRadio.on('ifChecked', function() { if(!ignoreICheckEvent) disableChat(true) });
$useChatInputRadio.on('ifChecked', function() { if(!ignoreICheckEvent) enableChat(true) });
}
// gets the state of the UI
function getCurrentState() {
var state = {
enabled:null,
chat_channel:null
};
state.enabled = $useChatInputRadio.is(':checked');
state.chat_channel = $selectedChatInput && $selectedChatInput.attr('data-channel-id');
logger.debug("desired chat state: enabled=" + state.enabled + ", chat_channel=" + state.chat_channel)
return state;
}
function trySave() {
var state = getCurrentState();
if(state.enabled && state.chat_channel) {
logger.debug("enabling chat. chat_channel=" + state.chat_channel);
context.jamClient.TrackSetChatEnable(true);
context.jamClient.FTUESetChatInput(state.chat_channel);
//context.jamClient.TrackSetAssignment(state.chat_channel, true, ASSIGNMENT.CHAT);
}
else {
logger.debug("disabling chat.");
context.jamClient.FTUEClearChatInput();
//context.jamClient.TrackSetChatEnable(false);
//if(state.chat_channel) {
//context.jamClient.TrackSetAssignment(state.chat_channel, true, ASSIGNMENT.UNASSIGNED);
//}
}
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
return true;
}
else {
context.JK.Banner.showAlert('Unable to save chat assignments. ' + result);
return false;
}
}
function initialize(_$step, _saveImmediate) {
$parent = _$step;
saveImmediate = _saveImmediate;
$reuseAudioInputRadio = $parent.find('.reuse-audio-input input');
$useChatInputRadio = $parent.find('.use-chat-input input');
$chatInputs = $parent.find('.chat-inputs');
$templateChatInput = $('#template-chat-input');
handleChatEnabledToggle();
}
this.reset = reset;
this.trySave = trySave;
this.initialize = initialize;
return this;
};
})(window, jQuery);

View File

@ -0,0 +1,117 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.FrameBuffers = function (app) {
var $knobs = null;
var $bufferIn = null;
var $bufferOut = null;
var $frameSize = null;
var $self = $(this);
var FRAMESIZE_CHANGED = 'frame_buffers.framesize_changed';
var BUFFER_IN_CHANGED = 'frame_buffers.buffer_in_changed';
var BUFFER_OUT_CHANGED = 'frame_buffers.buffer_out_changed';
function selectedFramesize() {
return parseFloat($frameSize.val());
}
function selectedBufferIn() {
return parseFloat($bufferIn.val());
}
function selectedBufferOut() {
return parseFloat($bufferOut.val());
}
function setFramesize(value) {
context.JK.dropdown($frameSize.val(value).easyDropDown('select', value.toString(), true))
}
function setBufferIn(value) {
context.JK.dropdown($bufferIn.val(value).easyDropDown('select', value.toString(), true));
}
function setBufferOut(value) {
context.JK.dropdown($bufferOut.val(value).easyDropDown('select', value.toString(), true))
}
function render() {
context.JK.dropdown($frameSize);
context.JK.dropdown($bufferIn);
context.JK.dropdown($bufferOut);
}
function disable() {
$frameSize.attr("disabled", "disabled").easyDropDown('disable');
$bufferIn.attr("disabled", "disabled").easyDropDown('disable');
$bufferOut.attr("disabled", "disabled").easyDropDown('disable');
}
function enable() {
$frameSize.removeAttr("disabled").easyDropDown('enable');
$bufferIn.removeAttr("disabled").easyDropDown('enable');
$bufferOut.removeAttr("disabled").easyDropDown('enable');
}
function resetValues() {
$frameSize.val('2.5');
$bufferIn.val('0');
$bufferOut.val('0');
}
function events() {
$frameSize.unbind('change').change(function () {
$self.triggerHandler(FRAMESIZE_CHANGED, {value: selectedFramesize()});
});
$bufferIn.unbind('change').change(function () {
logger.debug("buffer-in changed: " + selectedBufferIn());
$self.triggerHandler(BUFFER_IN_CHANGED, {value: selectedBufferIn()});
});
$bufferOut.unbind('change').change(function () {
logger.debug("buffer-out changed: " + selectedBufferOut());
$self.triggerHandler(BUFFER_OUT_CHANGED, {value: selectedBufferOut()});
});
}
function initialize(_$knobs) {
$knobs = _$knobs;
if(!$knobs.is('.frame-and-buffers')) {
throw "$knobs != .frame-and-buffers"
}
$bufferIn = $knobs.find('.select-buffer-in');
$bufferOut = $knobs.find('.select-buffer-out');
$frameSize = $knobs.find('.select-frame-size');
events();
render();
}
this.FRAMESIZE_CHANGED = FRAMESIZE_CHANGED;
this.BUFFER_IN_CHANGED = BUFFER_IN_CHANGED;
this.BUFFER_OUT_CHANGED = BUFFER_OUT_CHANGED;
this.initialize = initialize;
this.selectedFramesize = selectedFramesize;
this.selectedBufferIn = selectedBufferIn;
this.selectedBufferOut = selectedBufferOut;
this.setFramesize = setFramesize;
this.setBufferIn = setBufferIn;
this.setBufferOut = setBufferOut;
this.render = render;
this.enable = enable;
this.disable = disable;
this.resetValues = resetValues;
return this;
}
})(window, jQuery);

View File

@ -11,6 +11,7 @@
var wizard = null;
var $wizardSteps = null;
var $templateSteps = null;
var loopbackWizard = null;
var self = this;
@ -74,6 +75,7 @@
var newProfileName = 'FTUEAttempt-' + new Date().getTime().toString();
logger.debug("setting FTUE-prefixed profile name to: " + newProfileName);
context.jamClient.FTUESetMusicProfileName(newProfileName);
context.jamClient.FTUEClearChannelAssignments();
newSession();
var profileName = context.jamClient.FTUEGetMusicProfileName();
@ -145,7 +147,9 @@
}
function initialize() {
function initialize(_loopbackWizard) {
loopbackWizard = _loopbackWizard;
// on initial page load, we are not in the FTUE. so cancel the FTUE and call FTUESetStatus(true) if needed
if(context.jamClient.FTUEGetStatus() == false) {
@ -179,6 +183,8 @@
this.setBackState = setBackState;
this.initialize = initialize;
this.createFTUEProfile = createFTUEProfile;
this.getWizard = function() {return wizard; }
this.getLoopbackWizard = function() { return loopbackWizard; };
self = this;
return this;

View File

@ -10,257 +10,39 @@
var MAX_TRACKS = context.JK.MAX_TRACKS;
var logger = context.JK.logger;
var configureTracksHelper = new context.JK.ConfigureTracksHelper(app);
var $step = null;
var $templateAssignablePort = null;
var $templateTrackTarget = null;
var $unassignedChannelsHolder = null;
var $tracksHolder = null;
var $instrumentsHolder = null;
function loadChannels() {
var musicPorts = jamClient.FTUEGetChannels();
$unassignedChannelsHolder.empty();
$tracksHolder.find('.ftue-input').remove();
var inputChannels = musicPorts.inputs;
context._.each(inputChannels, function (inputChannel) {
if(inputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' }));
unassignChannel($channel);
}
else {
var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' }));
// find the track this belongs in
var trackNumber = inputChannel.assignment - 1;
var $track = $tracksHolder.find('.track[data-num="' + trackNumber + '"]')
if($track.length == 0) {
context.JK.alertSupportedNeeded('Unable to find a track for channel with assignment ' + inputChannel.assignment);
return false;
}
addChannelToTrack($channel, $track.find('.track-target'));
}
$channel.draggable({
helper: 'clone',
start: function() {
var $channel = $(this);
var $track = $channel.closest('.track-target');
var isUnassigned = $track.length == 0;
if(isUnassigned) {
$tracksHolder.find('.track-target').addClass('possible-target');
}
else {
$tracksHolder.find('.track-target').addClass('possible-target');
$unassignedChannelsHolder.addClass('possible-target');
}
},
stop: function() {
$tracksHolder.find('.track-target').removeClass('possible-target');
$unassignedChannelsHolder.removeClass('possible-target')
}
});
})
}
// iterates through the dom and returns a pure data structure for track associations
function trackAssociations() {
var tracks = {};
tracks.tracks = [];
tracks.unassignedChannels = [];
var $unassignedChannels = $unassignedChannelsHolder.find('.ftue-input');
var $tracks = $tracksHolder.find('.track-target');
context._.each($unassignedChannels, function($unassignedTrack) {
$unassignedTrack = $($unassignedTrack);
var channelId = $unassignedTrack.attr('data-input-id');
tracks.unassignedChannels.push(channelId);
})
context._.each($tracks, function($track, index) {
$track = $($track);
var $assignedChannels = $track.find('.ftue-input');
var track = {index: index, channels:[]};
context._.each($assignedChannels, function($assignedChannel) {
$assignedChannel = $($assignedChannel);
track.channels.push($assignedChannel.attr('data-input-id'))
});
// sparse array
if(track.channels.length > 0) {
tracks.tracks.push(track);
}
var $instrument = $instrumentsHolder.find('[data-num="' + index + '"]').find('.icon-instrument-select');
track.instrument_id = $instrument.data('instrument_id');
})
return tracks;
}
function validate(tracks) {
// there must be at least one assigned channel
if(tracks.tracks.length == 0) {
logger.debug("ConfigureTracks validation error: must have assigned at least one input port to a track.");
context.JK.Banner.showAlert('Must have assigned at least one input port to a track.');
return false;
}
context._.each(tracks.tracks, function(track) {
if(!track.instrument_id) {
logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument.");
context.JK.Banner.showAlert('All tracks with ports assigned must specify an instrument.');
return false;
}
});
return true;
}
function save(tracks) {
context._.each(tracks.unassignedChannels, function(unassignedChannelId) {
context.jamClient.TrackSetAssignment(unassignedChannelId, true, ASSIGNMENT.UNASSIGNED);
});
context._.each(tracks.tracks, function(track, index) {
var trackNumber = index + 1;
context._.each(track.channels, function(channelId) {
context.jamClient.TrackSetAssignment(channelId, true, trackNumber);
});
logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", trackNumber, track.instrument_id);
context.jamClient.TrackSetInstrument(trackNumber, context.JK.instrument_id_to_instrument[track.instrument_id].client_id);
});
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
return true;
}
else {
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
return false;
}
}
function loadTrackInstruments() {
var $trackInstruments = $instrumentsHolder.find('.track-instrument');
context._.each($trackInstruments, function(trackInstrument) {
var $trackInstrument = $(trackInstrument);
var trackIndex = parseInt($trackInstrument.attr('data-num')) + 1;
var clientInstrument = context.jamClient.TrackGetInstrument(trackIndex);
var instrument = context.JK.client_to_server_instrument_map[clientInstrument];
$trackInstrument.instrumentSelectorSet(instrument ? instrument.server_id : instrument);
});
}
var successfullyAssignedOnce = false;
function handleNext() {
var tracks = trackAssociations();
if(!validate(tracks)) {
return false;
}
var saved = save(tracks);
var saved = configureTracksHelper.trySave();
if(saved) {
context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS());
successfullyAssignedOnce = true;
}
return saved;
}
function newSession() {
successfullyAssignedOnce = false;
}
function beforeShow() {
loadChannels();
loadTrackInstruments();
}
var forceInputsToUnassigned = !successfullyAssignedOnce;
function unassignChannel($channel) {
var $originallyAssignedTrack = $channel.closest('.track-target');
$unassignedChannelsHolder.append($channel);
$originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length);
}
function addChannelToTrack($channel, $track) {
var $originallyAssignedTrack = $channel.closest('.track-target');
$track.append($channel);
$track.attr('track-count', $track.find('.ftue-input:not(.ui-draggable-dragging)').length);
$originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length)
}
function initializeUnassignedDroppable() {
$unassignedChannelsHolder.droppable(
{
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
unassignChannel($channel);
}
});
}
function initializeTrackDroppables() {
var i;
for(i = 0; i < MAX_TRACKS; i++) {
var $target = $(context._.template($templateTrackTarget.html(), {num: i }, { variable: 'data' }));
$tracksHolder.append($target);
$target.find('.track-target').droppable(
{
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $track = $(this);
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
addChannelToTrack($channel, $track);
}
});
}
}
function initializeInstrumentDropdown() {
var i;
for(i = 0; i < MAX_TRACKS; i++) {
var $root = $('<div class="track-instrument"></div>');
$root.instrumentSelector().attr('data-num', i);
$instrumentsHolder.append($root);
}
configureTracksHelper.reset(forceInputsToUnassigned)
}
function initialize(_$step) {
$step = _$step;
$templateAssignablePort = $('#template-assignable-port');
$templateTrackTarget = $('#template-track-target');
$unassignedChannelsHolder = $step.find('.unassigned-channels');
$tracksHolder = $step.find('.tracks');
$instrumentsHolder = $step.find('.instruments');
initializeUnassignedDroppable();
initializeTrackDroppables();
initializeInstrumentDropdown();
configureTracksHelper.initialize($step);
}
this.newSession = newSession;
this.handleNext = handleNext;
this.beforeShow = beforeShow;
this.initialize = initialize;

View File

@ -7,6 +7,7 @@
var ASSIGNMENT = context.JK.ASSIGNMENT;
var VOICE_CHAT = context.JK.VOICE_CHAT;
var gearUtils = context.JK.GearUtils;
var logger = context.JK.logger;
var $step = null;
@ -16,144 +17,30 @@
var $templateChatInput = null;
var $selectedChatInput = null;// should only be used if isChatEnabled = true
var voiceChatHelper = new context.JK.VoiceChatHelper(app);
var firstTimeShown = false;
function newSession() {
$reuseAudioInputRadio.attr('checked', 'checked').iCheck('check');
}
function isChannelAvailableForChat(chatChannelId, musicPorts) {
var result = true;
context._.each(musicPorts.input, function(inputChannel) {
// if the channel is currently assigned to a track, it not unassigned
if(inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) {
result = false;
return false; // break
}
});
return result;
}
function isChatEnabled() {
return $useChatInputRadio.is(':checked');
firstTimeShown = true;
}
function beforeShow() {
var forceDisabledChat = firstTimeShown;
if(isChatEnabled()) {
enableChat();
}
else {
disableChat();
}
voiceChatHelper.reset(forceDisabledChat);
var musicPorts = jamClient.FTUEGetChannels();
var chatInputs = context.jamClient.FTUEGetChatInputs();
$chatInputs.empty();
context._.each(chatInputs, function(chatChannelName, chatChannelId) {
if(isChannelAvailableForChat(chatChannelId, musicPorts)) {
var $chat = $(context._.template($templateChatInput.html(), {id: chatChannelId, name: chatChannelName}, { variable: 'data' }));
$chat.hide(); // we'll show it once it's styled with iCheck
$chatInputs.append($chat);
}
});
var $radioButtons = $chatInputs.find('input[name="chat-device"]');
context.JK.checkbox($radioButtons).on('ifChecked', function(e) {
var $input = $(e.currentTarget);
$selectedChatInput = $input; // for use in handleNext
var channelId = $input.attr('data-channel-id');
context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT);
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
}
else {
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
return false;
}
});
if(!isChatEnabled()) {
$radioButtons.iCheck('disable');
}
$chatInputs.find('.chat-input').show().on('click', function() {
if(!isChatEnabled()) {
context.JK.prodBubble($step.find('.use-chat-input h3'), 'chat-not-enabled', {}, { positions:['left']});
}
})
}
function disableChat() {
logger.debug("FTUE: disabling chat");
context.jamClient.TrackSetChatEnable(false);
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
}
else {
context.JK.Banner.showAlert('Unable to disable chat. ' + result);
return false;
}
var $radioButtons = $chatInputs.find('input[name="chat-device"]');
$radioButtons.iCheck('disable');
}
function enableChat() {
logger.debug("FTUE: enabling chat");
context.jamClient.TrackSetChatEnable(true);
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
// success
}
else {
context.JK.Banner.showAlert('Unable to enable chat. ' + result);
return false;
}
var $radioButtons = $chatInputs.find('input[name="chat-device"]');
$radioButtons.iCheck('enable');
}
function handleChatEnabledToggle() {
context.JK.checkbox($reuseAudioInputRadio);
context.JK.checkbox($useChatInputRadio);
// plugin sets to relative on the element; have to do this as an override
$reuseAudioInputRadio.closest('.iradio_minimal').css('position', 'absolute');
$useChatInputRadio.closest('.iradio_minimal').css('position', 'absolute');
$reuseAudioInputRadio.on('ifChecked', disableChat);
$useChatInputRadio.on('ifChecked', enableChat)
firstTimeShown = false;
}
function handleNext() {
var selectedDeviceInfo = context.JK.selectedDeviceInfo(context.jamClient.FTUEGetInputMusicDevice(), context.jamClient.FTUEGetOutputMusicDevice());
var chatName = null;
if(isChatEnabled()) {
chatName = $selectedChatInput.attr('data-channel-name');
}
context.jamClient.FTUESetMusicProfileName(context.JK.createProfileName(selectedDeviceInfo, chatName));
return true;
}
function initialize(_$step) {
$step = _$step;
$reuseAudioInputRadio = $step.find('.reuse-audio-input input');
$useChatInputRadio = $step.find('.use-chat-input input');
$chatInputs = $step.find('.chat-inputs');
$templateChatInput = $('#template-chat-input');
voiceChatHelper.initialize($step, true);
handleChatEnabledToggle();
}
this.handleNext = handleNext;

View File

@ -3,454 +3,59 @@
"use strict";
context.JK = context.JK || {};
context.JK.StepNetworkTest = function (app, $dialog) {
context.JK.StepNetworkTest = function (app, dialog) {
var NETWORK_TEST_TYPES = {
RestPhase : 0,
PktTest100NormalLatency : 1,
PktTest200MediumLatency : 2,
PktTest400LowLatency : 3,
PktTestRateSweep : 4,
RcvOnly : 5
}
var STARTING_NUM_CLIENTS = 4;
var PAYLOAD_SIZE = 100;
var MINIMUM_ACCEPTABLE_SESSION_SIZE = 2;
var rest = context.JK.Rest();
var logger = context.JK.logger;
var networkTest = new context.JK.NetworkTest(app);
var $step = null;
var TEST_SUCCESS_CALLBACK = 'JK.HandleNetworkTestSuccess';
var TEST_TIMEOUT_CALLBACK = 'JK.HandleNetworkTestTimeout';
var $startNetworkTestBtn = null;
var $testResults = null;
var $testScore = null;
var $testText = null;
var testedSuccessfully = false;
var $scoringBar = null;
var $goodMarker = null;
var $goodLine = null;
var $currentScore = null;
var $scoredClients = null;
var $subscore = null;
var backendGuardTimeout = null;
var serverClientId = '';
var isScoring = false;
var numClientsToTest = STARTING_NUM_CLIENTS;
var testSummary = {attempts : [], final:null}
var scoringZoneWidth = 100;//px
function reset() {
serverClientId = '';
isScoring = false;
numClientsToTest = STARTING_NUM_CLIENTS;
testSummary = {attempts : []};
updateControlsState();
function networkTestDone() {
updateButtons();
}
function renderStartTest() {
$scoredClients.empty();
$testResults.removeClass('good acceptable bad').addClass('testing');
$testText.empty();
$subscore.empty();
updateControlsState();
$currentScore.width(0);
$goodLine.css('left', (gon.ftue_packet_rate_treshold * 100) + '%');
$goodMarker.css('left', (gon.ftue_packet_rate_treshold * 100) + '%');
function networkTestFail() {
updateButtons();
}
function renderStopTest(score, text) {
$scoredClients.html(score);
$testText.html(text);
$testResults.removeClass('testing');
function networkTestStart() {
updateButtons();
}
function postDiagnostic() {
rest.createDiagnostic({
type: 'NETWORK_TEST_RESULT',
data: {client_type: context.JK.clientType(), client_id: context.JK.JamServer.clientID, summary:testSummary}
});
}
function testFinished() {
var attempt = getCurrentAttempt();
if(!testSummary.final) {
testSummary.final = {reason : attempt.reason};
}
var reason = testSummary.final.reason;
if(reason == "success") {
renderStopTest(attempt.num_clients, "Your router and Internet service will support sessions of up to " + attempt.num_clients + " JamKazam musicians.")
testedSuccessfully = true;
if(!testSummary.final.num_clients) {
testSummary.final.num_clients = attempt.num_clients;
}
context.JK.GA.trackNetworkTest(context.JK.detectOS(), testSummary.final.num_clients);
context.jamClient.SetNetworkTestScore(attempt.num_clients);
if(testSummary.final.num_clients == 2) {
$testResults.addClass('acceptable');
}
else {
$testResults.addClass('good');
}
}
else if(reason == "minimum_client_threshold") {
context.jamClient.SetNetworkTestScore(0);
renderStopTest('', "We're sorry, but your router and Internet service will not effectively support JamKazam sessions. Please click the HELP button for more information.")
}
else if(reason == "unreachable") {
context.jamClient.SetNetworkTestScore(0);
renderStopTest('', "We're sorry, but your router will not support JamKazam in its current configuration. Please click the HELP button for more information.");
}
else if(reason == "internal_error") {
context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection.");
renderStopTest('', '');
}
else if(reason == "remote_peer_cant_test") {
context.JK.alertSupportedNeeded("The JamKazam service is experiencing technical difficulties.");
renderStopTest('', '');
}
else if(reason == 'backend_gone') {
context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties.");
renderStopTest('', '');
}
else if(reason == "invalid_response") {
context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection. Reason=" + attempt.backend_data.reason + '.');
renderStopTest('', '');
}
else if(reason == 'no_servers') {
context.JK.alertSupportedNeeded("No network test servers are available. You can skip this step for now.");
renderStopTest('', '');
testedSuccessfully = true;
}
else if(reason == 'no_network') {
context.JK.Banner.showAlert("Please try again later. Your network appears down.");
renderStopTest('', '');
}
else if(reason == "rest_api_error") {
context.JK.alertSupportedNeeded("Unable to acquire a network test server. You can skip this step for now.");
testedSuccessfully = true;
renderStopTest('', '');
}
else if(reason == "timeout") {
context.JK.alertSupportedNeeded("Communication with a network test servers timed out. You can skip this step for now.");
testedSuccessfully = true;
renderStopTest('', '');
}
else {
context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection.");
renderStopTest('', '');
}
numClientsToTest = STARTING_NUM_CLIENTS;
isScoring = false;
updateControlsState();
postDiagnostic();
}
function getCurrentAttempt() {
return testSummary.attempts[testSummary.attempts.length - 1];
}
function backendTimedOut() {
testSummary.final = {reason: 'backend_gone'}
testFinished();
}
function clearBackendGuard() {
if(backendGuardTimeout) {
clearTimeout(backendGuardTimeout);
backendGuardTimeout = null;
}
}
function attemptTestPass() {
var attempt = {};
attempt.payload_size = PAYLOAD_SIZE;
attempt.duration = gon.ftue_network_test_duration;
attempt.test_type = 'PktTest400LowLatency';
attempt.num_clients = numClientsToTest;
attempt.server_client_id = serverClientId;
attempt.received_progress = false;
testSummary.attempts.push(attempt);
//context.jamClient.StopNetworkTest('');
$testText.text("Simulating the network traffic of a " + numClientsToTest + "-person session.");
updateProgress(0, false);
backendGuardTimeout = setTimeout(function(){backendTimedOut()}, (gon.ftue_network_test_duration + 1) * 1000);
context.jamClient.TestNetworkPktBwRate(serverClientId, TEST_SUCCESS_CALLBACK, TEST_TIMEOUT_CALLBACK,
NETWORK_TEST_TYPES.PktTest400LowLatency,
gon.ftue_network_test_duration,
numClientsToTest,
PAYLOAD_SIZE);
}
function startNetworkTest(checkWireless) {
if(isScoring) return false;
if(checkWireless) {
// check if on Wifi 1st
var isWireless = context.jamClient.IsMyNetworkWireless();
if(isWireless == -1) {
logger.warn("unable to determine if the user is on wireless or not for network test. skipping prompt.")
}
else if(isWireless == 1) {
context.JK.Banner.showAlert({buttons: [
{name: 'RUN NETWORK TEST ANYWAY', click: function() {startNetworkTest(false)}},
{name: 'CANCEL', click: function() {}}],
html: "<p>It appears that your computer is connected to your network using WiFi.</p>" +
"<p>We strongly advise against running the JamKazam application on a WiFi connection. " +
"We recommend using a wired Ethernet connection from your computer to your router. " +
"A WiFi connection is likely to cause significant issues in both latency and audio quality.</p>"})
return false;
}
}
reset();
isScoring = true;
renderStartTest();
rest.getLatencyTester()
.done(function(response) {
// ensure there are no tests ongoing
serverClientId = response.client_id;
logger.info("beginning network test against client_id: " + serverClientId);
attemptTestPass();
})
.fail(function(jqXHR) {
if(jqXHR.status == 404) {
// means there are no network testers available.
// we have to skip this part of the UI
testSummary.final = {reason: 'no_servers'}
}
else {
if(context.JK.isNetworkError(arguments)) {
testSummary.final = {reason: 'no_network'}
}
else {
testSummary.final = {reason: 'rest_api_error'}
}
}
testFinished();
})
logger.info("starting network test");
return false;
}
function updateProgress(throughput, showSubscore) {
var width = throughput * 100;
$currentScore.stop().data('showSubscore', showSubscore);
if(!showSubscore) {
$subscore.text('');
}
$currentScore.animate({
duration: 1000,
width: width + '%'
}, {
step: function (now, fx) {
if(showSubscore) {
var newWidth = ( 100 * parseFloat($currentScore.css('width')) / parseFloat($currentScore.parent().css('width')) );
$subscore.text((Math.round(newWidth * 10) / 10) + '%');
}
}
}).css('overflow', 'visible');
;
}
function networkTestSuccess(data) {
clearBackendGuard();
var attempt = getCurrentAttempt();
function refineTest(up) {
if(up) {
if(numClientsToTest == gon.ftue_network_test_max_clients) {
attempt.reason = "success";
testFinished();
}
else {
numClientsToTest++;
logger.debug("increasing number of clients to " + numClientsToTest);
setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm
}
}
else {
// reduce numclients if we can
if(numClientsToTest == MINIMUM_ACCEPTABLE_SESSION_SIZE) {
// we are too low already. fail the user
attempt.reason = "minimum_client_threshold";
testFinished();
}
else if(numClientsToTest > STARTING_NUM_CLIENTS) {
// this means we've gone up before... so don't go back down (i.e., creating a loop)
attempt.reason = "success";
testSummary.final = { reason:'success', num_clients: numClientsToTest - 1 }
testFinished();
}
else {
numClientsToTest--;
logger.debug("reducing number of clients to " + numClientsToTest);
setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm
}
}
}
attempt.backend_data = data;
if(data.progress === true) {
var animate = true;
if(data.downthroughput && data.upthroughput) {
if(data.downthroughput > 0 || data.upthroughput > 0) {
attempt.received_progress = true;
animate = true;
}
if(attempt.received_progress) {
// take the lower
var throughput= data.downthroughput < data.upthroughput ? data.downthroughput : data.upthroughput;
updateProgress(throughput, true);
}
}
}
else {
logger.debug("network test pass success. data: ", data);
if(data.reason == "unreachable") {
// STUN
logger.debug("network test: unreachable (STUN issue or similar)");
attempt.reason = data.reason;
testFinished();
}
else if(data.reason == "internal_error") {
// oops
logger.debug("network test: internal_error (client had a unexpected problem)");
attempt.reason = data.reason;
testFinished();
}
else if(data.reason == "remote_peer_cant_test") {
// old client
logger.debug("network test: remote_peer_cant_test (old client)")
attempt.reason = data.reason;
testFinished();
}
else {
if(!data.downthroughput || !data.upthroughput) {
// we have to assume this is bad. just not a reason we know about in code
logger.debug("network test: no test data (unknown issue? " + data.reason + ")")
attempt.reason = "invalid_response";
testFinished();
}
else {
// success... but we still have to verify if this data is within threshold
if(data.downthroughput < gon.ftue_packet_rate_treshold) {
logger.debug("network test: downthroughput too low. downthroughput: " + data.downthroughput + ", threshold: " + gon.ftue_packet_rate_treshold);
refineTest(false);
}
else if(data.upthroughput < gon.ftue_packet_rate_treshold) {
logger.debug("network test: upthroughput too low. upthroughput: " + data.upthroughput + ", threshold: " + gon.ftue_packet_rate_treshold);
refineTest(false);
}
else {
// true success. we can accept this score
logger.debug("network test: success")
refineTest(true);
}
}
}
// VRFS-1742
// context.jamClient.StopNetworkTest(serverClientId);
}
}
function networkTestTimeout(data) {
clearBackendGuard();
logger.warn("network timeout when testing latency test: " + data);
var attempt = getCurrentAttempt();
attempt.reason = 'timeout';
attempt.backend_data = data;
testFinished();
}
function hasScoredNetworkSuccessfully() {
return testedSuccessfully;
}
function configureStartButton() {
if(isScoring) {
$startNetworkTestBtn.text('NETWORK TEST RUNNING...').removeClass('button-orange').addClass('button-grey')
}
else {
$startNetworkTestBtn.text('START NETWORK TEST').removeClass('button-grey').addClass('button-orange');
}
}
function updateControlsState() {
function updateButtons() {
initializeNextButtonState();
initializeBackButtonState();
configureStartButton();
}
function initializeNextButtonState() {
$dialog.setNextState(hasScoredNetworkSuccessfully() && !isScoring);
dialog.setNextState(networkTest.hasScoredNetworkSuccessfully() && !networkTest.isScoring());
}
function initializeBackButtonState() {
$dialog.setBackState(!isScoring);
dialog.setBackState(!networkTest.isScoring());
}
function newSession() {
reset();
//context.jamClient.StopNetworkTest('');
networkTest.reset();
updateButtons();
}
function beforeShow() {
reset();
networkTest.cancel();
updateButtons();
}
function beforeHide() {
clearBackendGuard();
networkTest.cancel();
}
function initialize(_$step) {
$step = _$step;
$startNetworkTestBtn = $step.find('.start-network-test');
$testResults = $step.find('.network-test-results');
$testScore = $step.find('.network-test-score');
$testText = $step.find('.network-test-text');
$scoringBar = $step.find('.scoring-bar');
$goodMarker = $step.find('.good-marker');
$goodLine =$step.find('.good-line');
$currentScore = $step.find('.current-score');
$scoredClients= $step.find('.scored-clients');
$subscore = $step.find('.subscore');
$startNetworkTestBtn.on('click', startNetworkTest);
networkTest.initialize($step, true);
$(networkTest)
.on(networkTest.NETWORK_TEST_DONE, networkTestDone)
.on(networkTest.NETWORK_TEST_FAIL, networkTestFail)
.on(networkTest.NETWORK_TEST_START, networkTestStart)
}
this.newSession = newSession;
@ -458,9 +63,6 @@
this.beforeShow = beforeShow;
this.initialize = initialize;
context.JK.HandleNetworkTestSuccess = networkTestSuccess; // pin to global for bridge callback
context.JK.HandleNetworkTestTimeout = networkTestTimeout; // pin to global for bridge callback
return this;
}
})(window, jQuery);

View File

@ -3,40 +3,30 @@
"use strict";
context.JK = context.JK || {};
context.JK.StepSelectGear = function (app, $dialog) {
context.JK.StepSelectGear = function (app, dialog) {
var EVENTS = context.JK.EVENTS;
var ASSIGNMENT = context.JK.ASSIGNMENT;
var VOICE_CHAT = context.JK.VOICE_CHAT;
var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;
var gearUtils = context.JK.GearUtils;
var self = null;
var $step = null;
var logger = context.JK.logger;
var rest = context.JK.Rest();
var frameBuffers = new context.JK.FrameBuffers(app);
var gearTest = new context.JK.GearTest(app);
var loopbackShowing = false;
var $watchVideoInput = null;
var $watchVideoOutput = null;
var $audioInput = null;
var $audioOutput = null;
var $bufferIn = null;
var $bufferOut = null;
var $frameSize = null;
var $inputChannels = null;
var $outputChannels = null;
var $knobs = null;
var $scoreReport = null;
var $latencyScoreSection = null;
var $latencyScore = null;
var $latencyHeader = null;
var $ioHeader = null;
var $ioScoreSection = null;
var $ioRate = null;
var $ioRateScore = null;
var $ioVar = null;
var $ioVarScore = null;
var $ioCountdown = null;
var $ioCountdownSecs = null;
var $resultsText = null;
var $unknownText = null;
var $asioInputControlBtn = null;
var $asioOutputControlBtn = null;
var $resyncBtn = null;
@ -46,7 +36,6 @@
var operatingSystem = null;
var iCheckIgnore = false;
var scoring = false; // are we currently scoring
var validDevice = false; // do we currently have a device selected that we can score against?
// cached values between
@ -57,20 +46,8 @@
var selectedDeviceInfo = null;
var musicPorts = null;
var validLatencyScore = false;
var validIOScore = false;
var lastLatencyScore = null;
var ioScore = null;
var latencyScore = null;
var savedProfile = false;
function isGoodFtue() {
return validLatencyScore && validIOScore;
}
// returns a deviceInfo hash for the device matching the deviceId, or undefined.
function findDevice(deviceId) {
return deviceInformation[deviceId];
@ -84,56 +61,35 @@
return $audioOutput.val();
}
function selectedFramesize() {
return parseFloat($frameSize.val());
}
function selectedBufferIn() {
return parseFloat($bufferIn.val());
}
function selectedBufferOut() {
return parseFloat($bufferOut.val());
}
function setFramesize(value) {
context.JK.dropdown($frameSize.val(value).easyDropDown('select', value.toString(), true))
}
function setBufferIn(value) {
context.JK.dropdown($bufferIn.val(value).easyDropDown('select', value.toString(), true));
}
function setBufferOut(value) {
context.JK.dropdown($bufferOut.val(value).easyDropDown('select', value.toString(), true))
}
function setInputAudioDevice(value) {
context.JK.dropdown($audioInput.val(value).easyDropDown('select', value.toString(), true))
}
function setOutputAudioDevice(value) {
if(value != "" && value == selectedAudioInput()) {
value = ''; // to force Same as Input
}
context.JK.dropdown($audioOutput.val(value).easyDropDown('select', value.toString(), true))
}
function initializeNextButtonState() {
$dialog.setNextState(isGoodFtue());
dialog.setNextState(gearTest.isGoodFtue() || dialog.getLoopbackWizard().getGearTest().isGoodFtue());
}
function initializeBackButtonState() {
$dialog.setBackState(!scoring);
dialog.setBackState(!gearTest.isScoring());
}
function initializeAudioInput() {
var optionsHtml = '';
optionsHtml = '<option selected="selected" value="">Choose...</option>';
context._.each(deviceInformation, function (deviceInfo, deviceId) {
if(deviceInfo.inputCount > 0) {
optionsHtml += '<option title="' + deviceInfo.displayName + '" value="' + deviceId + '">' + deviceInfo.displayName + '</option>';
}
});
console.log("INITIALIZE AUDIO INPUT: " + optionsHtml)
$audioInput.html(optionsHtml);
context.JK.dropdown($audioInput);
$audioInput.easyDropDown('enable')
@ -156,16 +112,6 @@
initializeAudioOutputChanged();
}
function initializeFramesize() {
context.JK.dropdown($frameSize);
}
function initializeBuffers() {
context.JK.dropdown($bufferIn);
context.JK.dropdown($bufferOut);
}
// reloads the backend's channel state for the currently selected audio devices,
// and update's the UI accordingly
function initializeChannels() {
@ -246,7 +192,7 @@
function newInputAssignment() {
var assigned = 0;
context._.each(musicPorts.inputs, function (inputChannel) {
if (isChannelAssigned(inputChannel)) {
if (gearUtils.isChannelAssigned(inputChannel)) {
assigned += 1;
}
});
@ -324,18 +270,13 @@
initializeChannels();
}
// checks if it's an assigned OUTPUT or ASSIGNED CHAT
function isChannelAssigned(channel) {
return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0;
}
function initializeInputPorts(musicPorts) {
$inputChannels.empty();
var inputPorts = musicPorts.inputs;
context._.each(inputPorts, function (inputChannel) {
var $inputChannel = $(context._.template($templateAudioPort.html(), inputChannel, { variable: 'data' }));
var $checkbox = $inputChannel.find('input');
if (isChannelAssigned(inputChannel)) {
if (gearUtils.isChannelAssigned(inputChannel)) {
$checkbox.attr('checked', 'checked');
}
context.JK.checkbox($checkbox);
@ -350,7 +291,7 @@
context._.each(outputChannels, function (outputChannel) {
var $outputPort = $(context._.template($templateAudioPort.html(), outputChannel, { variable: 'data' }));
var $checkbox = $outputPort.find('input');
if (isChannelAssigned(outputChannel)) {
if (gearUtils.isChannelAssigned(outputChannel)) {
$checkbox.attr('checked', 'checked');
}
context.JK.checkbox($checkbox);
@ -361,8 +302,23 @@
function initializeLoopback() {
$launchLoopbackBtn.unbind('click').click(function() {
app.setWizardStep(1);
app.layout.showDialog('ftue');
$(dialog.getLoopbackWizard().getDialog()).one(EVENTS.DIALOG_CLOSED, function() {
loopbackShowing = false;
if(dialog.getLoopbackWizard().getGearTest().isGoodFtue()) {
gearTest.resetScoreReport();
gearTest.showLoopbackDone();
context.JK.prodBubble(dialog.getWizard().getNextButton(), 'move-on-loopback-success', {}, {positions:['top']});
}
initializeNextButtonState();
initializeBackButtonState();
})
loopbackShowing = true;
app.layout.showDialog('loopback-wizard')
return false;
})
}
@ -372,17 +328,9 @@
initializeAudioInput();
initializeAudioOutput();
initializeFramesize();
initializeBuffers();
initializeLoopback();
}
function resetFrameBuffers() {
$frameSize.val('2.5');
$bufferIn.val('0');
$bufferOut.val('0');
}
function clearInputPorts() {
$inputChannels.empty();
}
@ -391,92 +339,6 @@
$outputChannels.empty();
}
function resetScoreReport() {
$ioHeader.hide();
$latencyHeader.hide();
$ioRate.hide();
$ioRateScore.empty();
$ioVar.hide();
$ioVarScore.empty();
$latencyScore.empty();
$resultsText.removeAttr('latency-score');
$resultsText.removeAttr('io-var-score');
$resultsText.removeAttr('io-rate-score');
$resultsText.removeAttr('scored');
$unknownText.hide();
$ioScoreSection.removeClass('good acceptable bad unknown starting skip');
$latencyScoreSection.removeClass('good acceptable bad unknown starting')
}
function renderLatencyScore(latencyValue, latencyClass) {
// latencyValue == null implies starting condition
if (latencyValue) {
$latencyScore.text(latencyValue + ' ms');
}
else {
$latencyScore.text('');
}
if(latencyClass == 'unknown') {
$latencyScore.text('Unknown');
$unknownText.show();
}
$latencyHeader.show();
$resultsText.attr('latency-score', latencyClass);
$latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass);
}
// std deviation is the worst value between in/out
// media is the worst value between in/out
// io is the value returned by the backend, which has more info
// ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad'
function renderIOScore(std, median, ioData, ioClass, ioRateClass, ioVarClass) {
$ioRateScore.text(median !== null ? median : '');
$ioVarScore.text(std !== null ? std : '');
if (ioClass && ioClass != "starting" && ioClass != "skip") {
$ioRate.show();
$ioVar.show();
}
if(ioClass == 'starting' || ioClass == 'skip') {
$ioHeader.show();
}
$resultsText.attr('io-rate-score', ioRateClass);
$resultsText.attr('io-var-score', ioVarClass);
$ioScoreSection.removeClass('good acceptable bad unknown starting skip')
if (ioClass) {
$ioScoreSection.addClass(ioClass);
}
// TODO: show help bubble of all data in IO data
}
function updateScoreReport(latencyResult) {
var latencyClass = "neutral";
var latencyValue = null;
var validLatency = false;
if (latencyResult && latencyResult.latencyknown) {
var latencyValue = latencyResult.latency;
latencyValue = Math.round(latencyValue * 100) / 100;
if (latencyValue <= 10) {
latencyClass = "good";
validLatency = true;
} else if (latencyValue <= gon.ftue_maximum_gear_latency) {
latencyClass = "acceptable";
validLatency = true;
} else {
latencyClass = "bad";
}
}
else {
latencyClass = 'unknown';
}
validLatencyScore = validLatency;
renderLatencyScore(latencyValue, latencyClass);
}
function audioInputDeviceUnselected() {
validDevice = false;
setOutputAudioDevice('');
@ -484,28 +346,22 @@
}
function renderScoringStarted() {
resetScoreReport();
initializeBackButtonState();
initializeNextButtonState();
freezeAudioInteraction();
renderLatencyScore(null, 'starting');
renderIOScore(null, null, null, null, null, null);
}
function renderScoringStopped() {
initializeNextButtonState();
unfreezeAudioInteraction();
$resultsText.attr('scored', 'complete');
scoring = false;
initializeBackButtonState();
unfreezeAudioInteraction();
}
function freezeAudioInteraction() {
logger.debug("freezing audio interaction");
$audioInput.attr("disabled", "disabled").easyDropDown('disable');
$audioOutput.attr("disabled", "disabled").easyDropDown('disable');
$frameSize.attr("disabled", "disabled").easyDropDown('disable');
$bufferIn.attr("disabled", "disabled").easyDropDown('disable');
$bufferOut.attr("disabled", "disabled").easyDropDown('disable');
frameBuffers.disable();
$asioInputControlBtn.on("click", false);
$asioOutputControlBtn.on("click", false);
$resyncBtn.on('click', false);
@ -518,9 +374,7 @@
logger.debug("unfreezing audio interaction");
$audioInput.removeAttr("disabled").easyDropDown('enable');
$audioOutput.removeAttr("disabled").easyDropDown('enable');
$frameSize.removeAttr("disabled").easyDropDown('enable');
$bufferIn.removeAttr("disabled").easyDropDown('enable');
$bufferOut.removeAttr("disabled").easyDropDown('enable');
frameBuffers.enable();
$asioInputControlBtn.off("click", false);
$asioOutputControlBtn.off("click", false);
$resyncBtn.off('click', false);
@ -574,9 +428,8 @@
}
function invalidateScore() {
validLatencyScore = false;
validIOScore = false;
resetScoreReport();
gearTest.invalidateScore();
dialog.getLoopbackWizard().getGearTest().invalidateScore();
initializeNextButtonState();
}
@ -589,61 +442,23 @@
});
}
function initializeKnobs() {
$frameSize.unbind('change').change(function () {
logger.debug("frameize changed: " + selectedFramesize());
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
updateDefaultBuffers();
jamClient.FTUESetFrameSize(selectedFramesize());
invalidateScore();
});
$bufferIn.unbind('change').change(function () {
logger.debug("buffer-in changed: " + selectedBufferIn());
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
jamClient.FTUESetInputLatency(selectedBufferIn());
invalidateScore();
});
$bufferOut.unbind('change').change(function () {
logger.debug("buffer-out changed: " + selectedBufferOut());
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
jamClient.FTUESetOutputLatency(selectedBufferOut());
invalidateScore();
});
function onFramesizeChanged() {
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
updateDefaultBuffers();
jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize());
invalidateScore();
}
function ftueSummary() {
return {
os: operatingSystem,
version: context.jamClient.ClientUpdateVersion(),
success: isGoodFtue(),
score: {
validLatencyScore: validLatencyScore,
validIOScore: validIOScore,
latencyScore: latencyScore,
ioScore : ioScore,
},
audioParameters: {
frameSize: selectedFramesize(),
bufferIn: selectedBufferIn(),
bufferOut: selectedBufferOut(),
},
devices: deviceInformation,
selectedDevice: selectedDeviceInfo
}
function onBufferInChanged() {
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
invalidateScore();
}
function postDiagnostic() {
rest.createDiagnostic({
type: 'GEAR_SELECTION',
data: {
logs: logger.logCache,
client_type: context.JK.clientType(),
client_id:
context.JK.JamServer.clientID,
summary:ftueSummary()}
});
function onBufferOutChanged() {
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
invalidateScore();
}
function getSelectedInputs() {
@ -672,19 +487,6 @@
})
}
function renderIOScoringStarted(secondsLeft) {
$ioCountdownSecs.text(secondsLeft);
$ioCountdown.show();
}
function renderIOScoringStopped() {
$ioCountdown.hide();
}
function renderIOCountdown(secondsLeft) {
$ioCountdownSecs.text(secondsLeft);
}
// sets selectedDeviceInfo, which contains id, behavior, and info for input and output device
function cacheCurrentAudioInfo() {
@ -717,7 +519,7 @@
var profileName = context.jamClient.FTUEGetMusicProfileName();
logger.debug("invaliding previously saved profile: " + profileName);
$dialog.createFTUEProfile();
dialog.createFTUEProfile();
// restore user selections because newSession is called by createFTUEProfile(), invalidating dropdowns
setInputAudioDevice(audioInputDeviceId);
setOutputAudioDevice(audioOutputDeviceId);
@ -731,7 +533,7 @@
lastSelectedDeviceInfo = selectedDeviceInfo;
selectedDeviceInfo = context.JK.selectedDeviceInfo(audioInputDeviceId, audioOutputDeviceId, deviceInformation)
selectedDeviceInfo = gearUtils.selectedDeviceInfo(audioInputDeviceId, audioOutputDeviceId, deviceInformation)
return true;
}
@ -761,9 +563,21 @@
return false;
}
jamClient.FTUESetInputLatency(selectedBufferIn());
jamClient.FTUESetOutputLatency(selectedBufferOut());
jamClient.FTUESetFrameSize(selectedFramesize());
jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize());
// prod user to watch video if the previous type and new type changed
if(!shownInputProdOnce && isInputAudioTypeDifferentFromLastTime()) {
context.JK.prodBubble($watchVideoInput, 'ftue-watch-video', {}, {positions:['top', 'right']});
shownInputProdOnce = true;
}
// prod user to watch video if the previous type and new type changed
if(!shownOutputProdOnce && isOutputAudioTypeDifferentFromLastTime()) {
context.JK.prodBubble($watchVideoOutput, 'ftue-watch-video', {}, {positions:['top', 'right']});
shownOutputProdOnce = true;
}
return true;
}
@ -798,7 +612,6 @@
else {
var inputBehavior = null;
var outputBehavior = null;
}
// deal with watch video
@ -820,7 +633,7 @@
$knobs.hide();
}
// handle ASIO
// handle ASIO visibility
if (inputBehavior) {
if (inputBehavior.showASIO) {
$asioInputControlBtn.show();
@ -856,103 +669,45 @@
function updateDefaultFrameSize() {
if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')) {
setFramesize('10');
frameBuffers.setFramesize('10');
}
else {
setFramesize('2.5')
frameBuffers.setFramesize('2.5')
}
jamClient.FTUESetFrameSize(selectedFramesize());
jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize());
}
function updateDefaultBuffers() {
// handle specific framesize settings
if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')) {
var framesize = selectedFramesize();
var framesize = frameBuffers.selectedFramesize();
if(framesize == 2.5) {
logger.debug("setting default buffers to 1/1");
setBufferIn('1');
setBufferOut('1');
frameBuffers.setBufferIn('1');
frameBuffers.setBufferOut('1');
}
else if(framesize == 5) {
logger.debug("setting default buffers to 3/2");
setBufferIn('3');
setBufferOut('2');
frameBuffers.setBufferIn('3');
frameBuffers.setBufferOut('2');
}
else {
logger.debug("setting default buffers to 6/5");
setBufferIn('6');
setBufferOut('5');
frameBuffers.setBufferIn('6');
frameBuffers.setBufferOut('5');
}
}
else {
logger.debug("setting default buffers to 0/0");
setBufferIn(0);
setBufferOut(0);
frameBuffers.setBufferIn(0);
frameBuffers.setBufferOut(0);
}
jamClient.FTUESetInputLatency(selectedBufferIn());
jamClient.FTUESetOutputLatency(selectedBufferOut());
}
function processIOScore(io) {
// take the higher variance, which is apparently actually std dev
var std = io.in_var > io.out_var ? io.in_var : io.out_var;
std = Math.round(std * 100) / 100;
// take the furthest-off-from-target io rate
var median = Math.abs(io.in_median - io.in_target) > Math.abs(io.out_median - io.out_target) ? [io.in_median, io.in_target] : [io.out_median, io.out_target];
var medianTarget = median[1];
median = Math.round(median[0]);
var stdIOClass = 'bad';
if (std <= 0.50) {
stdIOClass = 'good';
}
else if (std <= 1.00) {
stdIOClass = 'acceptable';
}
var medianIOClass = 'bad';
if (Math.abs(median - medianTarget) <= 1) {
medianIOClass = 'good';
}
else if (Math.abs(median - medianTarget) <= 2) {
medianIOClass = 'acceptable';
}
// take worst between median or std
var ioClassToNumber = {bad: 2, acceptable: 1, good: 0}
var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass;
// now base the overall IO score based on both values.
renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass);
if(aggregrateIOClass == "bad") {
validIOScore = false;
}
else {
validIOScore = true;
}
if(isGoodFtue()) {
onSuccessfulScore();
}
else {
onFailedScore();
}
renderScoringStopped();
postDiagnostic();
}
function onFailedScore() {
rest.userCertifiedGear({success: false});
}
function onSuccessfulScore() {
rest.userCertifiedGear({success: true});
jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
}
// refocused affects how IO testing occurs.
@ -960,85 +715,7 @@
// * reuse IO score if it was good/acceptable
// * rescore IO if it was bad or skipped from previous try
function attemptScore(refocused) {
if(scoring) {return;}
scoring = true;
initializeBackButtonState();
validLatencyScore = false;
latencyScore = null;
if(!refocused) {
// don't reset a valid IO score on refocus
validIOScore = false;
ioScore = null;
}
renderScoringStarted();
// this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in
setTimeout(function () {
logger.debug("Calling FTUESave(false)");
jamClient.FTUESave(false);
var latency = jamClient.FTUEGetExpectedLatency();
latencyScore = latency;
// prod user to watch video if the previous type and new type changed
if(!shownInputProdOnce && isInputAudioTypeDifferentFromLastTime()) {
context.JK.prodBubble($watchVideoInput, 'ftue-watch-video', {}, {positions:['top', 'right']});
shownInputProdOnce = true;
}
// prod user to watch video if the previous type and new type changed
if(!shownOutputProdOnce && isOutputAudioTypeDifferentFromLastTime()) {
context.JK.prodBubble($watchVideoOutput, 'ftue-watch-video', {}, {positions:['top', 'right']});
shownOutputProdOnce = true;
}
updateScoreReport(latency);
if(refocused) {
context.JK.prodBubble($scoreReport, 'refocus-rescore', {validIOScore: validIOScore}, {positions:['top', 'left']});
}
// if there was a valid latency score, go on to the next step
if (validLatencyScore) {
// reuse valid IO score if this is on refocus
if(refocused && validIOScore) {
renderIOScore(null, null, null, 'starting', 'starting', 'starting');
processIOScore(ioScore);
}
else {
renderIOScore(null, null, null, 'starting', 'starting', 'starting');
var testTimeSeconds = gon.ftue_io_wait_time; // allow time for IO to establish itself
var startTime = testTimeSeconds / 2; // start measuring half way through the test, to get past IO oddities
renderIOScoringStarted(testTimeSeconds);
renderIOCountdown(testTimeSeconds);
var interval = setInterval(function () {
testTimeSeconds -= 1;
renderIOCountdown(testTimeSeconds);
if(testTimeSeconds == startTime) {
logger.debug("Starting IO Perf Test starting at " + startTime + "s in")
context.jamClient.FTUEStartIoPerfTest();
}
if (testTimeSeconds == 0) {
clearInterval(interval);
renderIOScoringStopped();
logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in")
var io = context.jamClient.FTUEGetIoPerfData();
ioScore = io;
processIOScore(io);
}
}, 1000);
}
}
else {
onFailedScore();
renderIOScore(null, null, null, 'skip', 'skip', 'skip');
renderScoringStopped();
postDiagnostic();
}
}, 250);
gearTest.attemptScore(refocused);
}
function initializeAudioInputChanged() {
@ -1049,6 +726,20 @@
$audioOutput.unbind('change').change(audioDeviceChanged);
}
function onGearTestStarted(e, data) {
renderScoringStarted();
}
function onGearTestDone(e, data) {
renderScoringStopped();
gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);
}
function onGearTestFail(e, data) {
renderScoringStopped();
gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);
}
function handleNext() {
if(!savedProfile) {
@ -1075,37 +766,43 @@
return false;
}
else {
context.jamClient.FTUESetMusicProfileName(context.JK.createProfileName(selectedDeviceInfo));
context.jamClient.FTUESave(true);
savedProfile = true;
context.JK.GA.trackAudioTestCompletion(context.JK.detectOS());
return true;
context.jamClient.FTUESetMusicProfileName(gearUtils.createProfileName(selectedDeviceInfo));
var result = context.jamClient.FTUESave(true);
if(result && result != "") {
// failed
logger.warn("unable to FTUESave(true). reason:" + result);
context.JK.Banner.alertSupportedNeeded("Unable to persist the audio profile. " + result);
return false;
}
else {
savedProfile = true;
context.JK.GA.trackAudioTestCompletion(context.JK.detectOS());
return true;
}
}
}
}
function onFocus() {
if(!scoring && validDevice && getSelectedInputs().length > 0 && getSelectedOutputs().length == 2 ) {
// in the case the user has been unselecting ports, re-enforce minimum viable channels
validDevice = autoSelectMinimumValidChannels();
if(validDevice && !loopbackShowing && getSelectedInputs().length > 0 && getSelectedOutputs().length == 2 ) {
attemptScore(true);
}
}
function newSession() {
savedProfile = false;
deviceInformation = context.JK.loadDeviceInfo();
deviceInformation = gearUtils.loadDeviceInfo();
resetState();
initializeFormElements();
initializeNextButtonState();
initializeWatchVideo();
initializeASIOButtons();
initializeKnobs();
initializeResync();
}
function beforeShow() {
$(window).on('focus', onFocus);
initializeNextButtonState();
}
function beforeHide() {
@ -1118,7 +815,7 @@
invalidateScore();
clearInputPorts();
clearOutputPorts();
resetFrameBuffers();
frameBuffers.resetValues();
updateDialogForCurrentDevices();
}
@ -1129,26 +826,11 @@
$watchVideoOutput = $step.find('.watch-video.audio-output');
$audioInput = $step.find('.select-audio-input-device');
$audioOutput = $step.find('.select-audio-output-device');
$bufferIn = $step.find('.select-buffer-in');
$bufferOut = $step.find('.select-buffer-out');
$frameSize = $step.find('.select-frame-size');
$inputChannels = $step.find('.input-ports');
$outputChannels = $step.find('.output-ports');
$knobs = $step.find('.frame-and-buffers');
$scoreReport = $step.find('.results');
$latencyScoreSection = $scoreReport.find('.latency-score-section');
$latencyScore = $scoreReport.find('.latency-score');
$latencyHeader = $scoreReport.find('.latency');
$ioHeader = $scoreReport.find('.io');
$ioScoreSection = $scoreReport.find('.io-score-section');
$ioRate = $scoreReport.find('.io-rate');
$ioRateScore = $scoreReport.find('.io-rate-score');
$ioVar = $scoreReport.find('.io-var');
$ioVarScore = $scoreReport.find('.io-var-score');
$ioCountdown = $scoreReport.find('.io-countdown');
$ioCountdownSecs = $scoreReport.find('.io-countdown .secs');
$resultsText = $scoreReport.find('.results-text');
$unknownText = $scoreReport.find('.unknown-text');
$asioInputControlBtn = $step.find('.asio-settings-input-btn');
$asioOutputControlBtn = $step.find('.asio-settings-output-btn');
$resyncBtn = $step.find('.resync-btn');
@ -1156,6 +838,17 @@
$launchLoopbackBtn = $('.loopback-test');
$instructions = $('.instructions');
operatingSystem = context.JK.GetOSAsString();
frameBuffers.initialize($knobs);
$(frameBuffers)
.on(frameBuffers.FRAMESIZE_CHANGED, onFramesizeChanged)
.on(frameBuffers.BUFFER_IN_CHANGED, onBufferInChanged)
.on(frameBuffers.BUFFER_OUT_CHANGED, onBufferOutChanged)
gearTest.initialize($scoreReport, true)
$(gearTest)
.on(gearTest.GEAR_TEST_START, onGearTestStarted)
.on(gearTest.GEAR_TEST_DONE, onGearTestDone)
.on(gearTest.GEAR_TEST_FAIL, onGearTestFail)
}
this.handleNext = handleNext;

View File

@ -10,9 +10,9 @@
function beforeShow() {
var $watchVideo = $step.find('.watch-video');
var videoUrl = 'https://www.youtube.com/watch?v=VexH4834o9I';
var videoUrl = 'https://www.youtube.com/watch?v=NiELWY769Tw';
if (operatingSystem == "Win32") {
$watchVideo.attr('href', 'https://www.youtube.com/watch?v=VexH4834o9I');
$watchVideo.attr('href', 'https://www.youtube.com/watch?v=Z1GxCljtdCY');
}
$watchVideo.attr('href', videoUrl);
}

View File

@ -0,0 +1,465 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.GearTest = function (app) {
var logger = context.JK.logger;
var isAutomated = false;
var drawUI = false;
var scoring = false;
var validLatencyScore = false;
var validIOScore = false;
var latencyScore = null;
var ioScore = null;
var $scoreReport = null;
var $ioHeader = null;
var $latencyHeader = null;
var $ioRate = null;
var $ioRateScore = null;
var $ioVar = null;
var $ioVarScore = null;
var $ioCountdown = null;
var $ioCountdownSecs = null;
var $latencyScore = null;
var $resultsText = null;
var $unknownText = null;
var $loopbackCompleted = null;
var $ioScoreSection = null;
var $latencyScoreSection = null;
var $self = $(this);
var GEAR_TEST_START = "gear_test.start";
var GEAR_TEST_IO_START = "gear_test.io_start";
var GEAR_TEST_IO_DONE = "gear_test.io_done";
var GEAR_TEST_LATENCY_START = "gear_test.latency_start";
var GEAR_TEST_LATENCY_DONE = "gear_test.latency_done";
var GEAR_TEST_DONE = "gear_test.done";
var GEAR_TEST_FAIL = "gear_test.fail";
var GEAR_TEST_IO_PROGRESS = "gear_test.io_progress";
function isGoodFtue() {
return validLatencyScore && validIOScore;
}
function processIOScore(io) {
// take the higher variance, which is apparently actually std dev
var std = io.in_var > io.out_var ? io.in_var : io.out_var;
std = Math.round(std * 100) / 100;
// take the furthest-off-from-target io rate
var median = Math.abs(io.in_median - io.in_target) > Math.abs(io.out_median - io.out_target) ? [io.in_median, io.in_target] : [io.out_median, io.out_target];
var medianTarget = median[1];
median = Math.round(median[0]);
var stdIOClass = 'bad';
if (std <= 0.50) {
stdIOClass = 'good';
}
else if (std <= 1.00) {
stdIOClass = 'acceptable';
}
var medianIOClass = 'bad';
if (Math.abs(median - medianTarget) <= 1) {
medianIOClass = 'good';
}
else if (Math.abs(median - medianTarget) <= 2) {
medianIOClass = 'acceptable';
}
// take worst between median or std
var ioClassToNumber = {bad: 2, acceptable: 1, good: 0}
var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass;
// now base the overall IO score based on both values.
$self.triggerHandler(GEAR_TEST_IO_DONE, {std:std, median:median, io:io, aggregrateIOClass: aggregrateIOClass, medianIOClass : medianIOClass, stdIOClass: stdIOClass})
//renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass);
if(aggregrateIOClass == "bad") {
validIOScore = false;
}
else {
validIOScore = true;
}
scoring = false;
if(isGoodFtue()) {
$self.triggerHandler(GEAR_TEST_DONE)
}
else {
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'io'});
}
}
function automaticScore() {
logger.debug("automaticScore: calling FTUESave(false)");
var result = jamClient.FTUESave(false);
if(result && result != "") {
logger.debug("unable to FTUESave(false). reason=" + result);
return false;
}
else {
var latency = jamClient.FTUEGetExpectedLatency();
context.JK.GearTest.testDeferred.resolve(latency);
}
return true;
}
function loopbackScore() {
var cbFunc = 'JK.loopbackLatencyCallback';
logger.debug("Registering loopback latency callback: " + cbFunc);
context.jamClient.FTUERegisterLatencyCallback('JK.loopbackLatencyCallback');
var now = new Date();
logger.debug("Starting Latency Test..." + now);
context.jamClient.FTUEStartLatency();
}
// refocused affects how IO testing occurs.
// on refocus=true:
// * reuse IO score if it was good/acceptable
// * rescore IO if it was bad or skipped from previous try
function attemptScore(refocused) {
if(scoring) {
logger.debug("gear-test: already scoring");
return;
}
scoring = true;
$self.triggerHandler(GEAR_TEST_START);
$self.triggerHandler(GEAR_TEST_LATENCY_START);
validLatencyScore = false;
latencyScore = null;
if(!refocused) {
// don't reset a valid IO score on refocus
validIOScore = false;
ioScore = null;
}
// this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in
setTimeout(function () {
context.JK.GearTest.testDeferred = new $.Deferred();
if(isAutomated) {
// use automated latency score mechanism
automaticScore()
}
else {
// use loopback score mechanism
loopbackScore();
}
context.JK.GearTest.testDeferred.done(function(latency) {
latencyScore = latency;
if(isAutomated) {
// uncomment to do a manual loopback test
//latency.latencyknown = false;
}
updateScoreReport(latency, refocused);
// if there was a valid latency score, go on to the next step
if (validLatencyScore) {
$self.triggerHandler(GEAR_TEST_IO_START);
// reuse valid IO score if this is on refocus
if(refocused && validIOScore) {
processIOScore(ioScore);
}
else {
var testTimeSeconds = gon.ftue_io_wait_time; // allow time for IO to establish itself
var startTime = testTimeSeconds / 2; // start measuring half way through the test, to get past IO oddities
$self.trigger(GEAR_TEST_IO_PROGRESS, {countdown:testTimeSeconds, first:true})
var interval = setInterval(function () {
testTimeSeconds -= 1;
$self.trigger(GEAR_TEST_IO_PROGRESS, {countdown:testTimeSeconds, first:false})
if(testTimeSeconds == startTime) {
logger.debug("Starting IO Perf Test starting at " + startTime + "s in")
context.jamClient.FTUEStartIoPerfTest();
}
if (testTimeSeconds == 0) {
clearInterval(interval);
logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in")
var io = context.jamClient.FTUEGetIoPerfData();
ioScore = io;
processIOScore(io);
}
}, 1000);
}
}
else {
scoring = false;
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency'})
}
})
}, 250);
}
function updateScoreReport(latencyResult, refocused) {
var latencyClass = "neutral";
var latencyValue = null;
var validLatency = false;
if (latencyResult && latencyResult.latencyknown) {
var latencyValue = latencyResult.latency;
latencyValue = Math.round(latencyValue * 100) / 100;
if (latencyValue <= 10) {
latencyClass = "good";
validLatency = true;
} else if (latencyValue <= gon.ftue_maximum_gear_latency) {
latencyClass = "acceptable";
validLatency = true;
} else {
latencyClass = "bad";
}
}
else {
latencyClass = 'unknown';
}
validLatencyScore = validLatency;
if(refocused) {
context.JK.prodBubble($scoreReport, 'refocus-rescore', {validIOScore: validIOScore}, {positions:['top', 'left']});
}
$self.triggerHandler(GEAR_TEST_LATENCY_DONE, {latencyValue: latencyValue, latencyClass: latencyClass, refocused: refocused});
}
// std deviation is the worst value between in/out
// media is the worst value between in/out
// io is the value returned by the backend, which has more info
// ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad'
function renderIOScore(std, median, ioData, ioClass, ioRateClass, ioVarClass) {
$ioRateScore.text(median !== null ? median : '');
$ioVarScore.text(std !== null ? std : '');
if (ioClass && ioClass != "starting" && ioClass != "skip") {
$ioRate.show();
$ioVar.show();
}
if(ioClass == 'starting' || ioClass == 'skip') {
$ioHeader.show();
}
$resultsText.attr('io-rate-score', ioRateClass);
$resultsText.attr('io-var-score', ioVarClass);
$ioScoreSection.removeClass('good acceptable bad unknown starting skip')
if (ioClass) {
$ioScoreSection.addClass(ioClass);
}
// TODO: show help bubble of all data in IO data
}
function isScoring() {
return scoring;
}
function isValidLatencyScore() {
return validLatencyScore;
}
function isValidIOScore() {
return validIOScore;
}
function getLatencyScore() {
return latencyScore;
}
function getIOScore() {
return ioScore;
}
function showLoopbackDone() {
$loopbackCompleted.show();
}
function resetScoreReport() {
$ioHeader.hide();
$latencyHeader.hide();
$ioRate.hide();
$ioRateScore.empty();
$ioVar.hide();
$ioVarScore.empty();
$latencyScore.empty();
$resultsText.removeAttr('latency-score');
$resultsText.removeAttr('io-var-score');
$resultsText.removeAttr('io-rate-score');
$resultsText.removeAttr('scored');
$unknownText.hide();
$loopbackCompleted.hide();
$ioScoreSection.removeClass('good acceptable bad unknown starting skip');
$latencyScoreSection.removeClass('good acceptable bad unknown starting')
}
function invalidateScore() {
validLatencyScore = false;
validIOScore = false;
resetScoreReport();
}
function renderLatencyScore(latencyValue, latencyClass) {
// latencyValue == null implies starting condition
if (latencyValue) {
$latencyScore.text(latencyValue + ' ms');
}
else {
$latencyScore.text('');
}
if(latencyClass == 'unknown') {
$latencyScore.text('Unknown');
$unknownText.show();
}
$latencyHeader.show();
$resultsText.attr('latency-score', latencyClass);
$latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass);
}
function renderIOScoringStarted(secondsLeft) {
$ioCountdownSecs.text(secondsLeft);
$ioCountdown.show();
}
function renderIOScoringStopped() {
$ioCountdown.hide();
}
function renderIOCountdown(secondsLeft) {
$ioCountdownSecs.text(secondsLeft);
}
function handleUI($testResults) {
if(!$testResults.is('.ftue-box.results')) {
throw "GearTest != .ftue-box.results"
}
$scoreReport = $testResults;
$ioHeader = $scoreReport.find('.io');;
$latencyHeader = $scoreReport.find('.latency');
$ioRate = $scoreReport.find('.io-rate');
$ioRateScore = $scoreReport.find('.io-rate-score');
$ioVar = $scoreReport.find('.io-var');
$ioVarScore = $scoreReport.find('.io-var-score');
$ioScoreSection = $scoreReport.find('.io-score-section');
$latencyScore = $scoreReport.find('.latency-score');
$ioCountdown = $scoreReport.find('.io-countdown');
$ioCountdownSecs = $scoreReport.find('.io-countdown .secs');
$resultsText = $scoreReport.find('.results-text');
$unknownText = $scoreReport.find('.unknown-text');
$loopbackCompleted = $scoreReport.find('.loopback-completed')
$latencyScoreSection = $scoreReport.find('.latency-score-section');
function onGearTestStart(e, data) {
renderIOScore(null, null, null, null, null, null);
}
function onGearTestIOStart(e, data) {
renderIOScore(null, null, null, 'starting', 'starting', 'starting');
}
function onGearTestLatencyStart(e, data) {
resetScoreReport();
renderLatencyScore(null, 'starting');
}
function onGearTestLatencyDone(e, data) {
renderLatencyScore(data.latencyValue, data.latencyClass);
}
function onGearTestIOProgress(e, data) {
if(data.first) {
renderIOScoringStarted(data.countdown);
}
renderIOCountdown(data.countdown);
if(data.countdown == 0) {
renderIOScoringStopped();
}
}
function onGearTestIODone(e, data) {
renderIOScore(data.std, data.median, data.io, data.aggregrateIOClass, data.medianIOClass, data.stdIOClass);
}
function onGearTestDone(e, data) {
$resultsText.attr('scored', 'complete');
rest.userCertifiedGear({success: true, client_id: app.clientId, audio_latency: getLatencyScore()});
}
function onGearTestFail(e, data) {
$resultsText.attr('scored', 'complete');
if(data.reason == "latency") {
renderIOScore(null, null, null, 'skip', 'skip', 'skip');
}
rest.userCertifiedGear({success: false});
}
$self
.on(GEAR_TEST_START, onGearTestStart)
.on(GEAR_TEST_IO_START, onGearTestIOStart)
.on(GEAR_TEST_LATENCY_START, onGearTestLatencyStart)
.on(GEAR_TEST_LATENCY_DONE, onGearTestLatencyDone)
.on(GEAR_TEST_IO_PROGRESS, onGearTestIOProgress)
.on(GEAR_TEST_IO_DONE, onGearTestIODone)
.on(GEAR_TEST_DONE, onGearTestDone)
.on(GEAR_TEST_FAIL, onGearTestFail);
}
function initialize($testResults, automated, noUI) {
isAutomated = automated;
drawUI = !noUI;
if(drawUI) {
handleUI($testResults);
}
}
// Latency Test Callback
context.JK.loopbackLatencyCallback = function (latencyMS) {
// Unregister callback:
context.jamClient.FTUERegisterLatencyCallback('');
logger.debug("loopback test done: " + latencyMS);
context.JK.GearTest.testDeferred.resolve({latency: latencyMS, latencyknown:true})
};
this.GEAR_TEST_START = GEAR_TEST_START;
this.GEAR_TEST_IO_START = GEAR_TEST_IO_START;
this.GEAR_TEST_IO_DONE = GEAR_TEST_IO_DONE;
this.GEAR_TEST_LATENCY_START = GEAR_TEST_LATENCY_START;
this.GEAR_TEST_LATENCY_DONE = GEAR_TEST_LATENCY_DONE;
this.GEAR_TEST_DONE = GEAR_TEST_DONE;
this.GEAR_TEST_FAIL = GEAR_TEST_FAIL;
this.GEAR_TEST_IO_PROGRESS = GEAR_TEST_IO_PROGRESS;
this.initialize = initialize;
this.isScoring = isScoring;
this.attemptScore = attemptScore;
this.resetScoreReport = resetScoreReport;
this.showLoopbackDone = showLoopbackDone;
this.invalidateScore = invalidateScore;
this.isValidLatencyScore = isValidLatencyScore;
this.isValidIOScore = isValidIOScore;
this.isGoodFtue = isGoodFtue;
this.getLatencyScore = getLatencyScore;
this.getIOScore = getIOScore;
return this;
}
})(window, jQuery);

View File

@ -0,0 +1,276 @@
/**
* Common utility functions.
*/
(function (context, $) {
"use strict";
context.JK = context.JK || {};
var gearUtils = {};
var rest = new context.JK.Rest();
context.JK.GearUtils = gearUtils;
var logger = context.JK.logger;
var ASSIGNMENT = context.JK.ASSIGNMENT;
var VOICE_CHAT = context.JK.VOICE_CHAT;
var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;
// checks if it's an assigned OUTPUT or ASSIGNED CHAT
gearUtils.isChannelAssigned = function (channel) {
return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0;
}
gearUtils.createProfileName = function(deviceInfo, chatName) {
var isSameInOut = deviceInfo.input.id == deviceInfo.output.id;
var name = null;
if(isSameInOut) {
name = "In/Out: " + deviceInfo.input.info.displayName;
}
else {
name = "In: " + deviceInfo.input.info.displayName + ", Out: " + deviceInfo.output.info.displayName
}
logger.debug("creating profile name: " + name);
return name;
}
gearUtils.selectedDeviceInfo = function(audioInputDeviceId, audioOutputDeviceId, deviceInformation) {
if(!audioInputDeviceId) {
logger.debug("gearUtils.selectedDeviceInfo: no active input device");
return null;
}
if(!audioOutputDeviceId) {
logger.debug("gearUtils.selectedDeviceInfo: no active output device");
return null;
}
if(!deviceInformation) {
deviceInformation = gearUtils.loadDeviceInfo();
}
var input = deviceInformation[audioInputDeviceId];
var output = deviceInformation[audioOutputDeviceId];
var inputBehavior = AUDIO_DEVICE_BEHAVIOR[input.type];
var outputBehavior = AUDIO_DEVICE_BEHAVIOR[output.type];
return {
input: {
id: audioInputDeviceId,
info: input,
behavior: inputBehavior
},
output: {
id: audioOutputDeviceId,
info: output,
behavior: outputBehavior
}
}
}
gearUtils.loadDeviceInfo = function() {
var operatingSystem = context.JK.GetOSAsString();
// should return one of:
// * MacOSX_builtin
// * MACOSX_interface
// * Win32_wdm
// * Win32_asio
// * Win32_asio4all
// * Linux
function determineDeviceType(deviceId, displayName) {
if (operatingSystem == "MacOSX") {
if (displayName.toLowerCase().trim().indexOf("built-in") == 0) {
return "MacOSX_builtin";
}
else {
return "MacOSX_interface";
}
}
else if (operatingSystem == "Win32") {
if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) {
return "Win32_wdm";
}
else if (displayName.toLowerCase().indexOf("asio4all") > -1) {
return "Win32_asio4all"
}
else {
return "Win32_asio";
}
}
else {
return "Linux";
}
}
var devices = context.jamClient.FTUEGetAudioDevices();
logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices));
var loadedDevices = {};
// augment these devices by determining their type
context._.each(devices.devices, function (device) {
if (device.name == "JamKazam Virtual Monitor") {
return;
}
var deviceInfo = {};
deviceInfo.id = device.guid;
deviceInfo.type = determineDeviceType(device.guid, device.display_name);
deviceInfo.displayType = AUDIO_DEVICE_BEHAVIOR[deviceInfo.type].display;
deviceInfo.displayName = device.display_name;
deviceInfo.inputCount = device.input_count;
deviceInfo.outputCount = device.output_count;
loadedDevices[device.guid] = deviceInfo;
})
logger.debug(context.JK.dlen(loadedDevices) + " devices loaded.", loadedDevices);
return loadedDevices;
}
gearUtils.ftueSummary = function(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) {
return {
os: operatingSystem,
version: context.jamClient.ClientUpdateVersion(),
success: gearTest.isGoodFtue(),
automated: isAutomated,
score: {
validLatencyScore: gearTest.isValidLatencyScore(),
validIOScore: gearTest.isValidIOScore(),
latencyScore: gearTest.getLatencyScore(),
ioScore : gearTest.getIOScore(),
},
audioParameters: {
frameSize: frameBuffers.selectedFramesize(),
bufferIn: frameBuffers.selectedBufferIn(),
bufferOut: frameBuffers.selectedBufferOut(),
},
devices: deviceInformation,
selectedDevice: selectedDeviceInfo
}
}
/**
* Lists all profiles, but marks profiles good: true/false.
* Also, current:true/false indicates which profile is active. (at most 1 profile will be marked current)
* This is to provide a unified view of FTUEGetAllAudioConfigurations & FTUEGetGoodAudioConfigurations
* @returns an array of profiles, where each profile is: {id: profile-name, good: boolean, class: 'bad' | 'good', current: boolean }
*/
gearUtils.getProfiles = function() {
var all = context.jamClient.FTUEGetAllAudioConfigurations();
var good = context.jamClient.FTUEGetGoodAudioConfigurations();
var current = context.jamClient.FTUEGetMusicProfileName();
var profiles = [];
context._.each(all, function(item) {
profiles.push({id: item, good: false, class:'bad', current: current == item})
});
if(good) {
for(var i = 0; i < good.length; i++) {
for(var j = 0; j < profiles.length; j++) {
if(good[i] == profiles[j].id) {
profiles[j].good = true;
profiles[j].class = 'good';
break;
}
}
}
}
return profiles;
}
gearUtils.postDiagnostic = function(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated) {
rest.createDiagnostic({
type: 'GEAR_SELECTION',
data: {
logs: logger.logCache,
client_type: context.JK.clientType(),
client_id:
context.JK.JamServer.clientID,
summary:gearUtils.ftueSummary(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, isAutomated)}
});
}
// complete list of possibly chatInputs, whether currently assigned as the chat channel or not
// each item should be {id: channelId, name: channelName, assignment: channel assignment}
gearUtils.getChatInputs = function(){
var musicPorts = jamClient.FTUEGetChannels();
//var chatsOnCurrentDevice = context.jamClient.FTUEGetChatInputs(true);
var chatsOnOtherDevices = context.jamClient.FTUEGetChatInputs(false);
var chatInputs = [];
//context._.each(musicPorts.inputs, function(input) {
// chatInputs.push({id: input.id, name: input.name, assignment:input.assignment});
//});
var deDupper = {};
context._.each(musicPorts.inputs, function(input) {
var chatInput = {id: input.id, name: input.name, assignment:input.assignment};
if(!deDupper[input.id]) {
if(input.assignment <= 0) {
chatInputs.push(chatInput);
deDupper[input.id] = chatInput;
}
}
});
/**context._.each(chatsOnCurrentDevice, function(chatChannelName, chatChannelId) {
var chatInput = {id: chatChannelId, name: chatChannelName, assignment: ASSIGNMENT.UNASSIGNED};
if(!deDupper[chatInput.id]) {
var assignment = context.jamClient.TrackGetAssignment(chatChannelId, true);
if(assignment <= 0) {
chatInputs.push(chatInput);
deDupper[chatInput.id] = chatInput;
}
}
})*/
context._.each(chatsOnOtherDevices, function(chatChannelName, chatChannelId) {
var chatInput = {id: chatChannelId, name: chatChannelName, assignment: null};
if(!deDupper[chatInput.id]) {
var assignment = context.jamClient.TrackGetAssignment(chatChannelId, true);
chatInput.assignment = assignment;
chatInputs.push(chatInput);
deDupper[chatInput.id] = chatInput;
}
})
logger.debug("chatInputs:", chatInputs)
return chatInputs;
}
gearUtils.isChannelAvailableForChat = function(chatChannelId, musicPorts) {
var result = true;
context._.each(musicPorts.inputs, function(inputChannel) {
// if the channel is currently assigned to a track, it not unassigned
if(inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) {
result = false;
return false; // break
}
});
return result;
}
gearUtils.updateAudioLatency = function(app) {
var latency = jamClient.FTUEGetExpectedLatency().latency;
return rest.updateAudioLatency({client_id: app.clientId, audio_latency: latency})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, "Unable to sync audio latency")
});
}
})(window, jQuery);

View File

@ -2,19 +2,20 @@
"use strict";
context.JK = context.JK || {};
context.JK.LoopbackWizard = function (app) {
var EVENTS = context.JK.EVENTS;
var logger = context.JK.logger;
var $dialog = null;
var wizard = null;
var $wizardSteps = null;
var $self = $(this);
var step1 = new context.JK.Step1(app, this);
var step2 = new context.JK.Step2(app, this);
var step3 = new context.JK.Step3(app, this);
var step1 = new context.JK.StepLoopbackIntro(app, this);
var step2 = new context.JK.StepLoopbackTest(app, this);
var step3 = new context.JK.StepLoopbackResult(app, this);
var STEPS = {
0: step1,
@ -32,7 +33,7 @@
function closeDialog() {
wizard.onCloseDialog();
app.layout.closeDialog('your-wizard');
app.layout.closeDialog('loopback-wizard');
}
function setNextState(enabled) {
@ -60,6 +61,9 @@
return false;
}
function getGearTest() {
return step2.getGearTest();
}
function events() {
$(wizard).on('step_changed', onStepChanged);
@ -67,19 +71,20 @@
$(wizard).on('wizard_close', onClosed);
}
function getDialog() {
return $dialog;
}
function initialize() {
var dialogBindings = { beforeShow: beforeShow, afterHide: afterHide };
app.bindDialog('your-wizard', dialogBindings);
$dialog = $('#your-wizard');
app.bindDialog('loopback-wizard', dialogBindings);
$dialog = $('#loopback-wizard-dialog');
$wizardSteps = $dialog.find('.wizard-step');
step1.initialize($wizardSteps.filter($('[layout-wizard-step=0]')));
step2.initialize($wizardSteps.filter($('[layout-wizard-step=1]')));
step3.initialize($wizardSteps.filter($('[layout-wizard-step=2]')));
wizard = new context.JK.Wizard(app);
wizard.initialize($dialog, $wizardSteps, STEPS);
@ -90,6 +95,8 @@
this.setNextState = setNextState;
this.setBackState = setBackState;
this.initialize = initialize;
this.getGearTest = getGearTest;
this.getDialog = getDialog;
return this;

View File

@ -0,0 +1,37 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.StepLoopbackIntro = function (app, $dialog) {
var $step = null;
function videoLinkClicked(evt) {
var myOS = jamClient.GetOSAsString();
var link;
if (myOS === 'MacOSX') {
link = $(evt.currentTarget).attr('external-link-mac');
} else if (myOS === 'Win32') {
link = $(evt.currentTarget).attr('external-link-win');
}
if (link) {
context.jamClient.OpenSystemBrowser(link);
}
}
function beforeShow() {
}
function initialize(_$step) {
$step = _$step;
$step.find('.ftue-video-link').on('click', videoLinkClicked);
}
this.beforeShow = beforeShow;
this.initialize = initialize;
return this;
}
})(window, jQuery);

View File

@ -0,0 +1,22 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.StepLoopbackResult = function (app, $dialog) {
var logger = context.JK.logger;
var $step = null;
function beforeShow() {
}
function initialize(_$step) {
$step = _$step;
}
this.beforeShow = beforeShow;
this.initialize = initialize;
return this;
}
})(window, jQuery);

View File

@ -0,0 +1,373 @@
(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.StepLoopbackTest = function (app, dialog) {
var ASSIGNMENT = context.JK.ASSIGNMENT;
var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;
var gearUtils = context.JK.GearUtils;
var logger = context.JK.logger;
var $step = null;
var frameBuffers = new context.JK.FrameBuffers(app);
var gearTest = new context.JK.GearTest(app);
var deviceInformation = null;
var operatingSystem = null;
var selectedDeviceInfo = null;
var musicPorts = null;
var $asioInputControlBtn = null;
var $asioOutputControlBtn = null;
var $resyncBtn = null;
var $runTestBtn = null;
var $audioInputDevice = null;
var $audioOutputDevice = null;
var $inputChannels = null;
var $outputChannels = null;
var $templateAudioPort = null;
var $scoreReport = null;
var faderMap = {
'loopback-audio-input-fader': jamClient.FTUESetInputVolume,
'loopback-audio-output-fader': jamClient.FTUESetOutputVolume
};
var faderReadMap = {
'loopback-audio-input-fader': jamClient.FTUEGetInputVolume,
'loopback-audio-output-fader': jamClient.FTUEGetOutputVolume
};
function attemptScore() {
gearTest.attemptScore();
}
function invalidateScore() {
gearTest.invalidateScore();
initializeNextButtonState();
}
function updateDefaultBuffers() {
// handle specific framesize settings
if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')) {
var framesize = frameBuffers.selectedFramesize();
if(framesize == 2.5) {
logger.debug("setting default buffers to 1/1");
frameBuffers.setBufferIn('1');
frameBuffers.setBufferOut('1');
}
else if(framesize == 5) {
logger.debug("setting default buffers to 3/2");
frameBuffers.setBufferIn('3');
frameBuffers.setBufferOut('2');
}
else {
logger.debug("setting default buffers to 6/5");
frameBuffers.setBufferIn('6');
frameBuffers.setBufferOut('5');
}
}
else {
logger.debug("setting default buffers to 0/0");
frameBuffers.setBufferIn(0);
frameBuffers.setBufferOut(0);
}
jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
}
function onFramesizeChanged() {
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
updateDefaultBuffers();
jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize());
invalidateScore();
}
function onBufferInChanged() {
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn());
invalidateScore();
}
function onBufferOutChanged() {
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut());
invalidateScore();
}
function freezeAudioInteraction() {
logger.debug("loopback: freezing audio interaction");
frameBuffers.disable();
$asioInputControlBtn.on("click", false);
$asioOutputControlBtn.on("click", false);
$resyncBtn.on('click', false);
$runTestBtn.on('click', false);
}
function unfreezeAudioInteraction() {
logger.debug("unfreezing audio interaction");
frameBuffers.enable();
$asioInputControlBtn.off("click", false);
$asioOutputControlBtn.off("click", false);
$resyncBtn.off('click', false);
$runTestBtn.off('click', false);
}
function getGearTest() {
return gearTest;
}
function handleNext() {
return true;
}
function handleBack() {
return true;
}
function render(){
if(selectedDeviceInfo) {
var inputBehavior = selectedDeviceInfo.input.behavior;
var outputBehavior = selectedDeviceInfo.output.behavior;
}
else {
var inputBehavior = null;
var outputBehavior = null;
}
// handle ASIO visibility
if (inputBehavior) {
if (inputBehavior.showASIO) {
$asioInputControlBtn.show();
}
else {
$asioInputControlBtn.hide();
}
if(outputBehavior.showASIO && (selectedDeviceInfo.input.id != selectedDeviceInfo.output.id)) {
$asioOutputControlBtn.show();
}
else {
$asioOutputControlBtn.hide();
}
}
else {
// show no ASIO buttons
$asioInputControlBtn.hide();
$asioOutputControlBtn.hide();
}
if(selectedDeviceInfo) {
$audioInputDevice.text(selectedDeviceInfo.input.info.displayName);
$audioOutputDevice.text(selectedDeviceInfo.output.info.displayName);
}
else {
$audioInputDevice.text('Unassigned');
$audioOutputDevice.text('Unassigned');
}
$inputChannels.empty();
context._.each(musicPorts.inputs, function (inputChannel) {
var $inputChannel = $(context._.template($templateAudioPort.html(), inputChannel, { variable: 'data' }));
var $checkbox = $inputChannel.find('input');
if (gearUtils.isChannelAssigned(inputChannel)) {
$checkbox.attr('checked', 'checked');
}
context.JK.checkbox($checkbox).iCheck('disable');
$inputChannels.append($inputChannel);
});
$outputChannels.empty();
context._.each(musicPorts.outputs, function (outputChannel) {
var $outputPort = $(context._.template($templateAudioPort.html(), outputChannel, { variable: 'data' }));
var $checkbox = $outputPort.find('input');
if (gearUtils.isChannelAssigned(outputChannel)) {
$checkbox.attr('checked', 'checked');
}
context.JK.checkbox($checkbox).iCheck('disable');
$outputChannels.append($outputPort);
});
initializeVUMeters();
renderVolumes();
registerVuCallbacks();
}
function initializeASIOButtons() {
$asioInputControlBtn.unbind('click').click(function () {
context.jamClient.FTUEOpenControlPanel(selectedDeviceInfo.input.id);
});
$asioOutputControlBtn.unbind('click').click(function () {
context.jamClient.FTUEOpenControlPanel(selectedDeviceInfo.output.id);
});
}
function initializeResync() {
$resyncBtn.unbind('click').click(function () {
attemptScore();
return false;
})
}
function initializeVUMeters() {
var vuMeters = [
'#loopback-audio-input-vu-left',
'#loopback-audio-input-vu-right',
'#loopback-audio-output-vu-left',
'#loopback-audio-output-vu-right'
];
$.each(vuMeters, function () {
context.JK.VuHelpers.renderVU(this,
{vuType: "horizontal", lightCount: 12, lightWidth: 15, lightHeight: 3});
});
var faders = context._.keys(faderMap);
$.each(faders, function () {
var fid = this;
context.JK.FaderHelpers.renderFader('#' + fid,
{faderId: fid, faderType: "horizontal", width: 163});
context.JK.FaderHelpers.subscribe(fid, faderChange);
});
}
// renders volumes based on what the backend says
function renderVolumes() {
$.each(context._.keys(faderReadMap), function (index, faderId) {
// faderChange takes a value from 0-100
var $fader = $('[fader-id="' + faderId + '"]');
var db = faderReadMap[faderId]();
var faderPct = db + 80;
context.JK.FaderHelpers.setHandlePosition($fader, faderPct);
});
}
function faderChange(faderId, newValue, dragging) {
var setFunction = faderMap[faderId];
// TODO - using hardcoded range of -80 to 20 for output levels.
var mixerLevel = newValue - 80; // Convert our [0-100] to [-80 - +20] range
setFunction(mixerLevel);
}
function registerVuCallbacks() {
logger.debug("loopback-wizard: registering vu callbacks");
jamClient.FTUERegisterVUCallbacks(
"JK.loopbackAudioOutputVUCallback",
"JK.loopbackAudioInputVUCallback",
"JK.loopbackChatInputVUCallback"
);
jamClient.SetVURefreshRate(200);
}
function initializeNextButtonState() {
dialog.setNextState(gearTest.isGoodFtue());
}
function initializeBackButtonState() {
dialog.setBackState(!gearTest.isScoring());
}
function renderScoringStarted() {
initializeBackButtonState();
initializeNextButtonState();
freezeAudioInteraction();
}
function renderScoringStopped() {
initializeNextButtonState();
initializeBackButtonState();
unfreezeAudioInteraction();
}
function onGearTestStarted(e, data) {
renderScoringStarted();
}
function onGearTestDone(e, data) {
renderScoringStopped();
gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);
}
function onGearTestFail(e, data) {
renderScoringStopped();
gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);
}
function beforeShow() {
selectedDeviceInfo = gearUtils.selectedDeviceInfo(context.jamClient.FTUEGetInputMusicDevice(), context.jamClient.FTUEGetOutputMusicDevice());
deviceInformation = gearUtils.loadDeviceInfo();
musicPorts = jamClient.FTUEGetChannels();
render();
}
function beforeHide() {
logger.debug("loopback-wizard: unregistering vu callbacks");
jamClient.FTUERegisterVUCallbacks('', '', '');
}
function initialize(_$step) {
$step = _$step;
$asioInputControlBtn = $step.find('.asio-settings-input-btn');
$asioOutputControlBtn = $step.find('.asio-settings-output-btn');
$resyncBtn = $step.find('.resync-btn');
$runTestBtn = $step.find('.run-test-btn');
$audioInputDevice = $step.find('.audio-device.input')
$audioOutputDevice = $step.find('.audio-device.output')
$inputChannels = $step.find('.input-ports')
$outputChannels = $step.find('.output-ports')
$templateAudioPort = $('#template-audio-port');
$scoreReport = $step.find('.results');
operatingSystem = context.JK.GetOSAsString();
frameBuffers.initialize($step.find('.frame-and-buffers'));
$(frameBuffers)
.on(frameBuffers.FRAMESIZE_CHANGED, onFramesizeChanged)
.on(frameBuffers.BUFFER_IN_CHANGED, onBufferInChanged)
.on(frameBuffers.BUFFER_OUT_CHANGED, onBufferOutChanged)
gearTest.initialize($scoreReport, false)
$(gearTest)
.on(gearTest.GEAR_TEST_START, onGearTestStarted)
.on(gearTest.GEAR_TEST_DONE, onGearTestDone)
.on(gearTest.GEAR_TEST_FAIL, onGearTestFail)
$runTestBtn.click(attemptScore);
initializeASIOButtons();
initializeResync();
}
context.JK.loopbackAudioInputVUCallback = function (dbValue) {
context.JK.ftueVUCallback(dbValue, '#loopback-audio-input-vu-left');
context.JK.ftueVUCallback(dbValue, '#loopback-audio-input-vu-right');
};
context.JK.loopbackAudioOutputVUCallback = function (dbValue) {
context.JK.ftueVUCallback(dbValue, '#loopback-audio-output-vu-left');
context.JK.ftueVUCallback(dbValue, '#loopback-audio-output-vu-right');
};
context.JK.loopbackChatInputVUCallback = function (dbValue) {
};
this.getGearTest = getGearTest;
this.handleNext = handleNext;
this.handleBack = handleBack;
this.beforeShow = beforeShow;
this.beforeHide = beforeHide;
this.initialize = initialize;
return this;
}
})(window, jQuery);

View File

@ -13,11 +13,13 @@
var $wizardSteps = null;
var $currentWizardStep = null;
var $wizardButtons = null;
var options = {};
var $btnHelp = null;
var $btnNext = null;
var $btnBack = null;
var $btnClose = null;
var $btnCancel = null;
var dialogName = null;
var self = this;
var $self = $(this);
@ -51,7 +53,7 @@
}
function back() {
if ($(this).is('.button-grey')) return false;
if ($(this).is('.disabled')) return false;
previousStep = step;
step = step - 1;
moveToStep();
@ -59,7 +61,7 @@
}
function next() {
if ($(this).is('.button-grey')) return false;
if ($(this).is('.disabled')) return false;
var stepInfo = STEPS[step];
if(stepInfo.handleNext) {
@ -83,7 +85,7 @@
$currentWizardStep = $nextWizardStep;
context.JK.GA.virtualPageView(location.pathname + location.search + location.hash + '/d1=' + step, $currentWizardStep.attr('dialog-title'));
context.JK.GA.virtualPageView('/client/' + dialogName, dialogName + ": " + $currentWizardStep.attr('dialog-title'));
$self.triggerHandler('step_changed', {step:step});
@ -150,31 +152,32 @@
step = null;
}
function getNextButton() {
return $btnNext;
}
function setNextState(enabled) {
if(!$btnNext) return;
$btnNext.removeClass('button-orange button-grey');
if (enabled) {
$btnNext.addClass('button-orange');
if(enabled) {
$btnNext.removeClass('disabled');
}
else {
$btnNext.addClass('button-grey');
$btnNext.addClass('disabled');
}
}
function setBackState(enabled) {
if(!$btnBack) return;
$btnBack.removeClass('button-orange button-grey');
if (enabled) {
$btnBack.addClass('button-orange');
if(enabled) {
$btnBack.removeClass('disabled');
}
else {
$btnBack.addClass('button-grey');
$btnBack.addClass('disabled');
}
}
@ -192,15 +195,21 @@
return $currentWizardStep;
}
function initialize(_$dialog, _$wizardSteps, _STEPS) {
function initialize(_$dialog, _$wizardSteps, _STEPS, _options) {
$dialog = _$dialog;
dialogName = $dialog.attr('layout-id');
if (!dialogName) throw "no dialog name (layout-id) in wizard";
$wizardSteps = _$wizardSteps;
STEPS = _STEPS;
$wizardButtons = $dialog.find('.wizard-buttons');
$templateButtons = $('#template-wizard-buttons');
if(_options) {_options = {}};
options = _options;
}
this.getNextButton = getNextButton;
this.setNextState = setNextState;
this.setBackState = setBackState;
this.getCurrentStep = getCurrentStep;

View File

@ -162,11 +162,11 @@
}
}
.lcol {
width: 148px;
width: 200px;
}
.whitespace {
// equal to lcol width.
padding-left: 148px;
padding-left: 200px;
}
.instruments {
width:128px;

View File

@ -34,12 +34,19 @@
*= require ./search
*= require ./ftue
*= require ./jamServer
*= require ./dragDropTracks
*= require ./voiceChatHelper
*= require ./wizard/gearResults
*= require ./wizard/framebuffers
*= require ./wizard/wizard
*= require ./wizard/gearWizard
*= require ./wizard/loopbackWizard
*= require ./networkTestDialog
*= require ./whatsNextDialog
*= require ./invitationDialog
*= require ./shareDialog
*= require ./hoverBubble
*= require ./configureTracksDialog
*= require ./recordingFinishedDialog
*= require ./localRecordingsDialog
*= require ./serverErrorDialog

View File

@ -0,0 +1,121 @@
@import "client/common.css.scss";
@charset "UTF-8";
#configure-tracks-dialog {
min-height: 700px;
max-height: 700px;
width:800px;
&[current-screen="account/audio"] {
.btn-add-new-audio-gear {
display:none;
}
}
.sub-header {
color:white;
margin-bottom:10px;
}
.certified-audio-profile-section {
height:53px;
.easydropdown {
width:120px;
}
.easydropdown-wrapper {
width:120px;
}
.dropdown-container {
min-width:138px;
}
}
.column {
position:relative;
float:left;
vertical-align:top;
@include border_box_sizing;
padding: 20px 20px 0 0;
}
.sub-column {
position:relative;
float:left;
vertical-align:top;
@include border_box_sizing;
}
.tab[tab-id="music-audio"] {
.column {
&:nth-of-type(1) {
width: 30%;
}
&:nth-of-type(2) {
width: 70%;
}
}
.sub-column {
&:nth-of-type(1) {
width: 80%;
}
&:nth-of-type(2) {
width: 20%;
}
}
}
.tab[tab-id="voice-chat"] {
.column {
&:nth-of-type(1) {
width: 50%;
}
&:nth-of-type(2) {
width: 50%;
}
}
}
.unused-audio-inputs-section {
margin-top:20px;
height:270px;
}
.unused-audio-outputs-section {
margin-top:20px;
height:80px;
}
.input-tracks-section {
height:363px;
}
.output-channels-section {
height:80px;
}
.buttons {
bottom: 25px;
position: absolute;
right: 25px;
left:25px;
}
.btn-add-new-audio-gear {
float:left;
}
.btn-cancel {
float:right;
}
.btn-update-settings {
float:right;
}
}

View File

@ -305,7 +305,7 @@ ul.shortcuts {
padding:2px;
}
.account-home, .band-setup, .audio, .get-help, .download-app, .invite-friends {
.account-home, .band-setup, .audio, .get-help, .invite-friends, .test-network{
border-bottom:1px;
border-style:solid;
border-color:#ED3618;

View File

@ -433,12 +433,17 @@ ul.shortcuts {
padding:2px;
}
.account-home, .band-setup, .account-menu-group, .get-help, .download-app, .community-forum, .invite-friends {
.account-home, .band-setup, .account-menu-group, .get-help, .community-forum, .invite-friends {
border-bottom:1px;
border-style:solid;
border-color:#ED3618;
}
.community-forum {
border-top:1px;
border-style:solid;
border-color:#ED3618;
}
span.arrow-right {

View File

@ -0,0 +1,172 @@
@import "client/common.css.scss";
@charset "UTF-8";
.dialog.gear-wizard, #configure-tracks-dialog {
.icon-instrument-select {
padding: 3px 0; // to combine 24 of .current-instrument + 3x on either side
margin: 0 0 15px 25px; // 15 margin-bottom to match tracks on the left
width: 30px;
}
.unassigned-output-channels {
min-height: 90px;
overflow-y: auto;
max-height: 90px;
//padding-right:18px; // to keep draggables off of scrollbar. maybe necessary
&.drag-in-progress {
}
&.possible-target {
border: solid 1px white;
&.drag-hovering {
border: solid 1px #ED3618;
}
}
}
.unassigned-input-channels {
min-height: 22px;
overflow-y: auto;
max-height:23%;
//padding-right:18px; // to keep draggables off of scrollbar. maybe necessary
&.compensate {
.ftue-input:not('.hovering') {
}
}
.ftue-input {
}
&.drag-in-progress {
}
&.possible-target {
border: solid 1px white;
&.drag-hovering {
border: solid 1px #ED3618;
}
}
}
.num {
position: absolute;
height: 29px;
line-height: 29px;
}
.track, .output {
margin-bottom: 15px;
.track-target, .output-target {
&.possible-target {
border-color: white;
}
&.drag-hovering {
border-color: #ED3618;
}
}
}
// do not show tracks with 2 channels as a possible target
.track .track-target.possible-target[track-count="2"] {
border-color:#999;
}
// do now show output slots with 1 channel as a possible target
.output .output-target.possible-target[output-count="1"] {
border-color:#999;
}
.ftue-input {
font-size: 12px;
cursor: move;
padding: 4px;
border: solid 1px #999;
margin-bottom: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
line-height:20px;
//direction: rtl;
&.ui-draggable-dragging {
margin-bottom: 0;
z-index:10000;
background-color:#333;
}
/**
&:hover {
color:white;
overflow:visible;
background-color:#333;
width: auto !important;
direction:ltr;
position:absolute;
line-height:19.5px;
}*/
}
.track-target, .output-target {
position:relative;
cursor: move;
padding: 4px;
border: solid 1px #999;
margin-left: 15px;
height: 20px;
overflow: hidden;
&.drag-in-progress {
overflow: visible;
}
.ftue-input {
padding: 0;
border: 0;
margin-bottom: 0;
&.ui-draggable-dragging {
padding: 4px;
border: solid 1px #999;
overflow: visible;
}
}
.placeholder {
display: none;
font-size: 12px;
}
&[track-count="0"], &[output-count="0"] {
.placeholder {
display: inline;
}
}
&[track-count="2"] {
.ftue-input {
width: 49%;
display: inline-block;
@include border_box_sizing;
&:nth-of-type(1) {
float: left;
padding-right:2px;
/**&:after {
float:right;
content: ',';
padding-right:3px;
}*/
}
&:nth-of-type(2) {
padding-left:2px;
float: right;
}
}
}
}
}

View File

@ -546,6 +546,8 @@ div[layout-id="ftue3"] {
float:left;
}
/**
.ftue-inner a {
color:#ccc;
}
@ -553,6 +555,7 @@ div[layout-id="ftue3"] {
.ftue-inner a:hover {
color:#fff;
}
*/
.ftue-instrumentlist {
width:340px;

View File

@ -0,0 +1,14 @@
#network-test-dialog {
.buttons {
bottom: 25px;
position: absolute;
right: 25px;
}
.network-test {
.network-test-results {
height:268px ! important;
}
}
}

View File

@ -231,11 +231,11 @@
}
}
.lcol {
width: 148px;
width: 200px;
}
.whitespace {
// equal to lcol width.
padding-left: 148px;
padding-left: 200px;
}
.instruments {
width:128px;

View File

@ -201,9 +201,9 @@ small, .small {font-size:11px;}
.button-orange {
margin:0px 8px 0px 8px;
background-color: #ED3618;
background-color: $ColorScreenPrimary;
border: solid 1px #F27861;
outline: solid 2px #ED3618;
outline: solid 2px $ColorScreenPrimary;
padding:3px 10px;
font-size:12px;
font-weight:300;
@ -213,11 +213,17 @@ small, .small {font-size:11px;}
line-height:12px;
&.disabled {
@extend .button-grey;
background-color: transparent;
border: solid 1px #868686;
outline: solid 2px transparent;
color:#ccc;
}
&.disabled:hover {
@extend .button-grey:hover;
//background-color:darken(#f16750, 20%);
//color:#FFF;
background-color: #515151;
color:#ccc;
}
}

View File

@ -0,0 +1,64 @@
.dialog.configure-tracks, .dialog.gear-wizard {
.voicechat-option {
position: relative;
div {
}
h3 {
padding-left: 30px;
margin-top: 14px;
font-weight: bold;
display: inline-block;
}
p {
padding-left: 30px;
margin-top: 5px;
display: inline-block;
}
input {
position: absolute;
margin: auto;
width: 30px;
}
.iradio_minimal {
margin-top: 15px;
display: inline-block;
}
}
.ftue-box {
background-color: #222222;
font-size: 13px;
padding: 8px;
&.chat-inputs {
height: 230px !important;
overflow: auto;
color:white;
&.disabled {
color:gray;
}
p {
white-space: nowrap;
display: inline-block;
height: 32px;
vertical-align: middle;
}
.chat-input {
white-space: nowrap;
.iradio_minimal {
display: inline-block;
}
}
}
}
}

View File

@ -0,0 +1,40 @@
.dialog {
.framesize {
float:left;
margin-right:10px;
width:30%;
}
.select-buffer-in {
width:45%;
}
.select-buffer-out {
width:45%;
}
.buffers {
float:left;
width:60%;
h2 {
margin-left:5px;
}
.dropdown-container {
width:48px;
}
.easydropdown-wrapper:nth-of-type(1) {
left:5px;
}
.easydropdown-wrapper:nth-of-type(2) {
left:30px;
}
.easydropdown, .easydropdown-wrapper {
width:30px !important;
}
}
}

View File

@ -0,0 +1,160 @@
@import "client/common.css.scss";
@charset "UTF-8";
.dialog {
.ftue-box.results {
height: 268px !important;
padding: 0;
@include border_box_sizing;
span.conditional {
display:none;
}
&[data-type=automated] {
span.conditional[data-type=automated] {
display:inline;
}
}
&[data-type=loopback] {
span.conditional[data-type=loopback] {
display:inline;
}
}
.io, .latency {
display: none;
}
.spinner-small {
display:none;
margin:auto;
}
&[data-type="loopback"] .latency-score-section.starting {
.spinner-small {
display:block;
}
}
.loopback-button-holder {
width: 100%;
text-align: center;
}
a.loopback-test {
margin-top: 10px;
}
.unknown-text {
display: none;
padding: 8px;
}
.loopback-completed {
display:none;
}
.scoring-section {
font-size: 15px;
@include border_box_sizing;
height: 64px;
&.good {
background-color: #72a43b;
}
&.acceptable {
background-color: #cc9900;
}
&.bad {
background-color: #660000;
}
&.unknown, &.skip {
background-color: #999;
}
&.skip {
.io-skip-msg {
display: inline;
}
}
}
.io-countdown {
display: none;
padding-left: 19px;
position: relative;
.secs {
position: absolute;
width: 19px;
left: 0;
}
}
.io-skip-msg {
display: none;
}
.io-rate {
display: none;
}
.io-var {
display: none;
}
ul.results-text {
padding: 10px 8px;
li {
display: none
}
&[latency-score="unknown"] {
display: none;
}
&[latency-score="good"] li.latency-good {
display: list-item;
}
&[latency-score="acceptable"] li.latency-acceptable {
display: list-item;
}
&[latency-score="bad"] li.latency-bad {
display: list-item;
}
&[io-var-score="good"] li.io-var-good {
display: list-item;
}
&[io-var-score="acceptable"] li.io-var-acceptable {
display: list-item;
}
&[io-var-score="bad"] li.io-var-bad {
display: list-item;
}
&[io-rate-score="good"] li.io-rate-good {
display: list-item;
}
&[io-rate-score="acceptable"] li.io-rate-acceptable {
display: list-item;
}
&[io-rate-score="bad"] li.io-rate-bad {
display: list-item;
}
&[scored="complete"] {
li.success {
display: list-item;
}
&[io-rate-score="bad"], &[io-var-score="bad"], &[latency-score="bad"] {
li.success {
display: none;
}
li.failure {
display: list-item;
}
}
&[latency-score="unknown"] {
li.success {
display: none;
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,148 @@
@import "client/common.css.scss";
@charset "UTF-8";
.dialog.loopback-wizard {
min-height: 500px;
max-height: 500px;
width:800px;
h2 {
color: #FFFFFF;
font-size: 15px;
font-weight: normal;
margin-bottom: 6px;
white-space:nowrap;
}
.ftue-box {
background-color: #222222;
font-size: 13px;
padding: 8px;
}
.ftue-inner {
line-height: 1.3em;
width:800px;
padding:25px;
font-size:15px;
color:#aaa;
@include border_box_sizing;
}
.wizard-step[layout-wizard-step="0"] {
p.intro {
margin-top:0px;
}
ul {
margin:0;
padding:0px;
margin-top:70px;
}
li {
text-align:center;
width: 31%;
height: 170px;
margin:0px;
/*padding:0px;*/
list-style: none;
float:left;
}
li.first {
width: 34%;
}
}
.wizard-step[layout-wizard-step="1"] {
.wizard-step-content .wizard-step-column {
&:nth-of-type(1), &:nth-of-type(2) {
width:31%;
}
&:nth-of-type(3) {
width:25%;
float:right;
}
}
a.button-orange {
margin-left:3px;
}
.ftue-controls {
margin-top: 16px;
position:relative;
height: 48px;
width: 220px;
background-color: #222;
}
.ftue-vu-left {
position:relative;
top: 0px;
}
.ftue-vu-right {
position:relative;
top: 22px;
}
.ftue-fader {
position:relative;
top: 14px;
left: 8px;
}
.gain-label {
color: $ColorScreenPrimary;
position:absolute;
top: 14px;
right: 6px;
}
.ports-header {
margin-top:20px;
}
.ports {
height:100px;
}
.asio-settings-input-btn, .asio-settings-output-btn {
position:absolute;
right:5px;
}
.resync-btn {
margin-top:35px;
}
.run-test-btn {
text-align:center;
display:inline-block;
margin-top:10px;
}
.frame-and-buffers {
margin-top:10px;
}
.test-results-header {
}
.select-frame-size {
margin-left:-2px;
}
.ftue-box.results {
margin-top:20px;
height: 200px !important;
padding:0;
@include border_box_sizing;
}
h2.results-text-header {
margin-top:5px;
}
}
}

View File

@ -13,7 +13,20 @@
}
.wizard-buttons-holder {
float:right;
position:absolute;
width:100%;
}
.left-buttons {
position:absolute;
left:-6px;
bottom:0;
}
.right-buttons {
position:absolute;
right:56px;
bottom:0;
}
.wizard-step {

View File

@ -125,7 +125,8 @@ class ApiMusicSessionsController < ApiController
band,
params[:genres],
params[:tracks],
params[:legal_terms])
params[:legal_terms],
params[:audio_latency])
if @music_session.errors.any?
response.status = :unprocessable_entity
@ -227,7 +228,8 @@ class ApiMusicSessionsController < ApiController
params[:id],
params[:client_id],
params[:as_musician],
params[:tracks])
params[:tracks],
params[:audio_latency])
if @connection.errors.any?
response.status = :unprocessable_entity

View File

@ -15,6 +15,7 @@ class ApiSearchController < ApiController
conn = (clientid ? Connection.where(client_id: clientid, user_id: current_user.id).first : nil)
# puts "================== query #{query.inspect}"
@search = Search.musician_filter(query, current_user, conn)
# puts "================== search #{@search.inspect}"
else
@search = Search.band_filter(query, current_user)
end

View File

@ -12,7 +12,7 @@ class ApiUsersController < ApiController
:band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations
:set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy,
:share_session, :share_recording,
:affiliate_report]
:affiliate_report, :audio_latency]
respond_to :json
@ -527,6 +527,15 @@ class ApiUsersController < ApiController
@user = current_user
if params[:success]
@user.update_progression_field(:first_certified_gear_at)
connection = Connection.find_by_client_id(params[:client_id])
# update last_jam location information
@user.update_addr_loc(connection, 'g') if connection
if !@user.errors.any?
# update audio gear latency information
@user.update_audio_latency(connection, params[:audio_latency])
end
else
@user.failed_qualification(params[:reason])
end
@ -643,10 +652,16 @@ class ApiUsersController < ApiController
if play.errors.any?
render :json => { :message => "Unexpected error occurred" }, :status => 500
return
else
render :json => {}, :status => 201
return
end
end
# updates audio latency on the user, and associated connection
def audio_latency
Connection.transaction do
@user.update_audio_latency(Connection.find_by_client_id(params[:client_id]), params[:audio_latency])
respond_with_model(@user)
end
end

View File

@ -33,7 +33,7 @@ else
child(:connections => :participants) {
collection @music_sessions, :object_root => false
attributes :ip_address, :client_id, :joined_session_at
attributes :ip_address, :client_id, :joined_session_at, :audio_latency
node :user do |connection|
{ :id => connection.user.id, :photo_url => connection.user.photo_url, :name => connection.user.name, :is_friend => connection.user.friends?(current_user), :connection_state => connection.aasm_state }

View File

@ -0,0 +1,3 @@
object @user
attributes :id

View File

@ -1,6 +1,6 @@
object @user
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency
if @user.musician?
node :location do @user.location end

View File

@ -50,6 +50,10 @@
<td>{{profile.id}} <span data-current="{{profile.current}}">{{profile.active_text}}</span></td>
<td class="actions">
<a href="#" data-id="{{profile.id}}" data-purpose="activate-audio-profile" class="button-orange">ACTIVATE</a>
{% if(data.is_admin) { %}
<a href="#" data-id="{{profile.id}}" data-purpose="loopback-audio-profile" class="button-orange">LOOPBACK (admin only)</a>
{% } %}
<a href="#" data-id="{{profile.id}}" data-purpose="configure-audio-profile" class="button-orange">CONFIGURE</a>
<a href="#" data-id="{{profile.id}}" data-purpose="delete-audio-profile" class="button-orange">DELETE</a></td>
</tr>
{% } %}

View File

@ -0,0 +1,71 @@
.dialog.configure-tracks{ layout: 'dialog', 'layout-id' => 'configure-tracks', id: 'configure-tracks-dialog'}
.content-head
= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' }
%h1 configure tracks
.dialog-inner
.dialog-tabs
%a.selected.tab-configure-audio Music Audio
%a.tab-configure-voice Voice Chat
.instructions
%span
Choose your audio device. Drag and drop to assign input ports to tracks, and specify the instrument
for each track. Drag and drop to assign a pair of output ports for session stereo audio monitoring.
.clearall
.tab.no-selection-range{'tab-id' => 'music-audio'}
.column
.certified-audio-profile-section
.sub-header Certified Audio Profile
%select.certified-audio-profile
.clearall
.unused-audio-inputs-section
.sub-header Unused Input Ports
.unassigned-input-channels.channels-holder
.unused-audio-outputs-section
.sub-header Unused Output Ports
.unassigned-output-channels.channels-holder
.column
.input-tracks-section
.sub-column
.sub-header Track Input Port(s)
.input-tracks.tracks
.sub-column
.sub-header Instrument
.instruments
.output-channels-section
.sub-header Audio Output Port
.output-channels
.clearall
.tab{'tab-id' => 'voice-chat'}
.column
%form.select-voice-chat-option.section
.sub-header Select Voice Chat Option
.voicechat-option.reuse-audio-input
%input{type:"radio", name: "voicechat", checked:"checked"}
%h3 Use Music Microphone
%p I am already using a microphone to capture my vocal or instrumental music, so I can talk with other musicians using that microphone
.voicechat-option.use-chat-input
%input{type:"radio", name: "voicechat"}
%h3 Use Chat Microphone
%p I am not using a microphone for acoustic instruments or vocals, so use the input selected to the right for voice chat during my sessions
.column
.select-voice-chat
.sub-header Voice Chat Input
.ftue-box.chat-inputs
.clearall
.buttons
%a.btn-add-new-audio-gear.button-grey{'layout-link' => 'add-new-audio-gear'} ADD NEW AUDIO GEAR
%a.button-orange.btn-update-settings{href:'#'} UPDATE SETTINGS
%a.button-grey.btn-cancel{href:'#'} CANCEL

View File

@ -28,4 +28,8 @@
<script type="text/template" id="template-help-push-resync-when-done">
Push 'Resync' when done modifying Framesize, Buffer In, or Buffer Out.
</script>
<script type="text/template" id="template-help-move-on-loopback-success">
You can move to the next step now.
</script>

View File

@ -32,12 +32,12 @@
</div>
<!-- todo scott vfrs-1455 i need a template tutorial to define the three variables that substitute here:
score:
one_way_score is joined_score / 2;
score_color is green, yellow, red, blue, or purple (depending upon value of joined_score)
score_color_alt is good, moderate, poor, unacceptable,or missing
musician_one_way_score is joined_score / 2;
musician_score_color is green, yellow, red, blue, or purple (depending upon value of joined_score)
musician_score_color_alt is good, moderate, poor, unacceptable,or missing
-->
<!-- <div class="left" style="***help***">
{one_way_score} ms <img src="../assets/content/icon_{score_color}_score.png" alt="{score_color_alt}" width="12" height="12" align="absmiddle"/>
{musician_one_way_score} <img src="../assets/content/icon_{musician_score_color}_score.png" alt="{musician_score_color_alt}" width="12" height="12" align="absmiddle"/>
</div> -->
<div class="right musician-following" style="width: 120px;">
<div class="bold">FOLLOWING:</div>
@ -58,10 +58,11 @@
</div>
<div class="button-row" data-hint="button-row">
<div class="lcol stats left">
{friend_count} <img src="../assets/content/icon_friend.png" width="14" height="12" align="absmiddle" style="margin-right:4px;"/>
{follow_count} <img src="../assets/content/icon_followers.png" width="22" height="12" align="absmiddle" style="margin-right:4px;"/>
{recording_count} <img src="../assets/content/icon_recordings.png" width="12" height="13" align="absmiddle" style="margin-right:4px;"/>
{session_count} <img src="../assets/content/icon_session_tiny.png" width="12" height="12" align="absmiddle" />
{friend_count} <img src="../assets/content/icon_friend.png" alt="friends" width="14" height="12" align="absmiddle" style="margin-right:4px;"/>
{follow_count} <img src="../assets/content/icon_followers.png" alt="follows" width="22" height="12" align="absmiddle" style="margin-right:4px;"/>
{recording_count} <img src="../assets/content/icon_recordings.png" alt="recordings" width="12" height="13" align="absmiddle" style="margin-right:4px;"/>
{session_count} <img src="../assets/content/icon_session_tiny.png" alt="sessions" width="12" height="12" align="absmiddle" style="margin-right:4px;"/>
{musician_one_way_score} <img src="../assets/content/icon_{musician_score_color}_score.png" alt="{musician_score_color_alt} score" width="12" height="12" align="absmiddle" style="margin-right:4px;"/>
</div>
<div class="result-list-button-wrapper" data-musician-id={musician_id}>
{musician_action_template}

View File

@ -0,0 +1,10 @@
.dialog.network-test{ layout: 'dialog', 'layout-id' => 'network-test', id: 'network-test-dialog'}
.content-head
%h1 Test Router & Network
.dialog-inner
= render :partial => '/clients/network_test'
.clearall
.buttons
%a.button-grey.btn-cancel{href:'#'} CANCEL
%a.button-orange.btn-help{href: '#'} HELP
%a.button-orange.btn-close{href:'#'} CLOSE

View File

@ -0,0 +1,30 @@
.network-test
.help-text In this step, you will test your router and Internet connection to ensure that you can play in online sessions, and to see how many musicians can be in a session with you based on your internet connection.
.wizard-step-content
.wizard-step-column
%h2 Instructions
.ftue-box.instructions
%ul
%li Check that computer is connected to router using Ethernet cable.
%li Click Start Network Test button.
%li View test results.
.center
%a.button-orange.watch-video{href:'#'} WATCH VIDEO
.wizard-step-column
.summary
%p Ensure that your computer is connected to your home router using an Ethernet cable rather than using Wi-Fi wireless access. If necessary, find or purchase a long Ethernet cable, up to 100 ft.
%p Then click on the Start Network Test button below.
.center
%a.button-orange.start-network-test{href:'#'} START NETWORK TEST
.wizard-step-column
%h2 Test Results
.network-test-results.ftue-box
.scoring-bar
.current-score
testing...
.subscore
.good-marker
.good-line
.network-test-score
.scored-clients
.network-test-text

View File

@ -52,7 +52,7 @@
<!-- my tracks -->
<div class="session-mytracks">
<h2>my tracks</h2>
<div id="track-settings" class="session-add" style="display:block;" layout-link="configure-audio">
<div id="track-settings" class="session-add" style="display:block;" layout-link="configure-tracks">
<%= image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} %>&nbsp;&nbsp;Settings
</div>

View File

@ -1,20 +0,0 @@
- show_back ||= local_assigns[:show_back] = local_assigns.fetch(:show_back, true)
- show_next ||= local_assigns[:show_next] = local_assigns.fetch(:show_next, true)
- total_steps = 7
.wizard-buttons
.wizard-buttons-holder
%a.button-grey.btn-help{href: '#'} HELP
- if step > 0 && step != total_steps
%a.button-orange.btn-back{href:'#'} BACK
- if step != total_steps
%a.button-orange.btn-next{href:'#'} NEXT
- if step == total_steps
%a.button-orange.btn-close{href:'#', 'layout-action' => 'close'} CLOSE
- if step == total_steps
%a.button-orange.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL

View File

@ -19,7 +19,9 @@
<%= render "ftue" %>
<%= render "jamServer" %>
<%= render "iconInstrumentSelect" %>
<%= render "clients/gear/gear_wizard" %>
<%= render "clients/wizard/buttons" %>
<%= render "clients/wizard/gear/gear_wizard" %>
<%= render "clients/wizard/loopback/loopback_wizard" %>
<%= render "terms" %>
<%= render "leaveSessionWarning" %>
<%= render "rateSession" %>
@ -46,6 +48,7 @@
<%= render "friendSelector" %>
<%= render "account_profile_avatar" %>
<%= render "account_audio_profile" %>
<%= render "configure_tracks_dialog" %>
<%= render "invitationDialog" %>
<%= render "inviteMusicians" %>
<%= render "hoverBand" %>
@ -55,6 +58,7 @@
<%= render "hoverSession" %>
<%= render "whatsNextDialog" %>
<%= render "shareDialog" %>
<%= render "networkTestDialog" %>
<%= render "recordingFinishedDialog" %>
<%= render "localRecordingsDialog" %>
<%= render "showServerErrorDialog" %>
@ -247,11 +251,21 @@
var whatsNextDialog = new JK.WhatsNextDialog(JK.app);
whatsNextDialog.initialize(invitationDialog);
var ftueWizard = new JK.FtueWizard(JK.app);
ftueWizard.initialize();
//var ftueWizard = new JK.FtueWizard(JK.app);
//ftueWizard.initialize();
var loopbackWizard = new JK.LoopbackWizard(JK.app);
loopbackWizard.initialize();
var gearWizard = new JK.GearWizard(JK.app);
gearWizard.initialize();
gearWizard.initialize(loopbackWizard);
var configureTracksDialog = new JK.ConfigureTracksDialog(JK.app);
configureTracksDialog.initialize();
var networkTestDialog = new JK.NetworkTestDialog(JK.app);
networkTestDialog.initialize();
var testBridgeScreen = new JK.TestBridgeScreen(JK.app);
testBridgeScreen.initialize();
@ -270,8 +284,8 @@
JK.initJamClient();
// latency_tester does not want to be here
if(window.jamClient.getOperatingMode() == "server") {
// latency_tester does not want to be here - redirect it
if(window.jamClient.getOperatingMode && window.jamClient.getOperatingMode() == "server") {
window.location = "/latency_tester";
return;
}

View File

@ -0,0 +1,12 @@
%script{type: 'text/template', id: 'template-wizard-buttons'}
.wizard-buttons-holder
.left-buttons
%a.button-grey.btn-help{href: '#'} HELP
%a.button-grey.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL
.right-buttons
%a.button-grey.btn-back{href:'#'} BACK
%a.button-orange.btn-next{href:'#'} NEXT
%a.button-orange.btn-close{href:'#', 'layout-action' => 'close'} CLOSE

View File

@ -0,0 +1,33 @@
.frame-and-buffers
.framesize
%h2 Frame
%select.select-frame-size
%option{val:'2.5'} 2.5
%option{val:'5'} 5
%option{val:'10'} 10
.buffers
%h2 Buffer In/Out
%select.select-buffer-in
%option{val:'0'} 0
%option{val:'1'} 1
%option{val:'2'} 2
%option{val:'3'} 3
%option{val:'4'} 4
%option{val:'5'} 5
%option{val:'6'} 6
%option{val:'7'} 7
%option{val:'8'} 8
%option{val:'9'} 9
%option{val:'10'} 10
%select.select-buffer-out
%option{val:'0'} 0
%option{val:'1'} 1
%option{val:'2'} 2
%option{val:'3'} 3
%option{val:'4'} 4
%option{val:'5'} 5
%option{val:'6'} 6
%option{val:'7'} 7
%option{val:'8'} 8
%option{val:'9'} 9
%option{val:'10'} 10

View File

@ -0,0 +1,41 @@
.ftue-box.results{'data-type' => test_type}
.left.w50.center.white.scoring-section.latency-score-section
.p5
.latency LATENCY
%span.latency-score
.spinner-small
.left.w50.center.white.scoring-section.io-score-section
.p5
.io I/O
%span.io-skip-msg
Not Tested
%span.io-countdown
%span.secs
seconds left
%span.io-rate<
Rate=
%span.io-rate-score>
%span.io-var<
Var=
%span.io-var-score>
.clearall
%ul.results-text
%li.latency-good Your latency is good.
%li.latency-acceptable Your latency is acceptable.
%li.latency-bad Your latency is poor.
%li.io-rate-good Your I/O rate is good.
%li.io-rate-acceptable Your I/O rate is acceptable.
%li.io-rate-bad Your I/O rate is poor.
%li.io-var-good Your I/O variance is good.
%li.io-var-acceptable Your I/O variance is acceptable.
%li.io-var-bad Your I/O variance is poor.
%li.success You may proceed to the next step.
%li.failure
%span.conditional{'data-type' => 'automated'} We're sorry, but your audio gear has failed. Please watch video or click HELP button below.
%span.conditional{'data-type' => 'loopback'} We're sorry, but your audio gear has failed. Please watch videos on the previous screen.
.unknown-text
%div We cannot accurately predict the latency of your audio gear. To proceed, you must run an audio loopback test. Click button below to do this.
%div.loopback-button-holder
%a.button-orange.loopback-test{href:'#'} RUN LOOPBACK TEST
.loopback-completed
You have completed the loopback test successfully. Click Next to continue.

View File

@ -54,84 +54,17 @@
%h2.audio-channels Audio Output Ports
.ftue-box.list.ports.output-ports
%a.button-orange.asio-settings-output-btn ASIO SETTINGS...
.frame-and-buffers
.framesize
%h2 Frame
%select.select-frame-size
%option{val:'2.5'} 2.5
%option{val:'5'} 5
%option{val:'10'} 10
.buffers
%h2 Buffer In/Out
%select.select-buffer-in
%option{val:'0'} 0
%option{val:'1'} 1
%option{val:'2'} 2
%option{val:'3'} 3
%option{val:'4'} 4
%option{val:'5'} 5
%option{val:'6'} 6
%option{val:'7'} 7
%option{val:'8'} 8
%option{val:'9'} 9
%option{val:'10'} 10
%select.select-buffer-out
%option{val:'0'} 0
%option{val:'1'} 1
%option{val:'2'} 2
%option{val:'3'} 3
%option{val:'4'} 4
%option{val:'5'} 5
%option{val:'6'} 6
%option{val:'7'} 7
%option{val:'8'} 8
%option{val:'9'} 9
%option{val:'10'} 10
= render :partial => "/clients/wizard/framebuffers"
.wizard-step-column
%h2 Test Results
.ftue-box.results
.left.w50.center.white.scoring-section.latency-score-section
.p5
.latency LATENCY
%span.latency-score
.left.w50.center.white.scoring-section.io-score-section
.p5
.io I/O
%span.io-skip-msg
Not Tested
%span.io-countdown
%span.secs
seconds left
%span.io-rate<
Rate=
%span.io-rate-score>
%span.io-var<
Var=
%span.io-var-score>
.clearall
%ul.results-text
%li.latency-good Your latency is good.
%li.latency-acceptable Your latency is acceptable.
%li.latency-bad Your latency is poor.
%li.io-rate-good Your I/O rate is good.
%li.io-rate-acceptable Your I/O rate is acceptable.
%li.io-rate-bad Your I/O rate is poor.
%li.io-var-good Your I/O variance is good.
%li.io-var-acceptable Your I/O variance is acceptable.
%li.io-var-bad Your I/O variance is poor.
%li.success You may proceed to the next step.
%li.failure We're sorry, but your audio gear has failed. Please watch video or click HELP button below.
.unknown-text
%div We cannot accurately predict the latency of your audio gear. To proceed, you must run an audio loopback test. Click button below to do this.
%div.loopback-button-holder
%a.button-orange.loopback-test{href:'#'} RUN LOOPBACK TEST
= render :partial => "/clients/wizard/gear_test", locals: {test_type: 'automated'}
.clearall
.wizard-step{ 'layout-wizard-step' => "2", 'dialog-title' => "Configure Tracks", 'dialog-purpose' => "ConfigureTracks" }
.ftuesteps
.clearall
.help-text In this step, you will select, configure, and test your audio gear. Please watch the video for best instructions.
.wizard-step-content
.wizard-step-content.no-selection-range
.wizard-step-column
%h2 Instructions
.ftue-box.instructions
@ -141,15 +74,17 @@
%li Select the instrument for each track.
.center
%a.button-orange.watch-video{href:'#'} WATCH VIDEO
.wizard-step-column.no-selection-range
.wizard-step-column
%h2 Unassigned Ports
.unassigned-channels
.wizard-step-column.no-selection-range
.unassigned-input-channels.channels-holder
.wizard-step-column
%h2 Track Input Port(s)
.tracks
.wizard-step-column.no-selection-range
.wizard-step-column
%h2 Instrument
.instruments
.output-channels
.unassigned-output-channels.channels-holder
.wizard-step{ 'layout-wizard-step' => "3", 'dialog-title' => "Configure Voice Chat", 'dialog-purpose' => "ConfigureVoiceChat" }
.ftuesteps
@ -165,14 +100,15 @@
%a.button-orange.watch-video{href:'#'} WATCH VIDEO
.wizard-step-column
%h2 Select Voice Chat Option
.voicechat-option.reuse-audio-input
%input{type:"radio", name: "voicechat", checked:"checked"}
%h3 Use Music Microphone
%p I am already using a microphone to capture my vocal or instrumental music, so I can talk with other musicians using that microphone
.voicechat-option.use-chat-input
%input{type:"radio", name: "voicechat"}
%h3 Use Chat Microphone
%p I am not using a microphone for acoustic instruments or vocals, so use the input selected to the right for voice chat during my sessions
%form.voice
.voicechat-option.reuse-audio-input
%input{type:"radio", name: "voicechat", checked:"checked"}
%h3 Use Music Microphone
%p I am already using a microphone to capture my vocal or instrumental music, so I can talk with other musicians using that microphone
.voicechat-option.use-chat-input
%input{type:"radio", name: "voicechat"}
%h3 Use Chat Microphone
%p I am not using a microphone for acoustic instruments or vocals, so use the input selected to the right for voice chat during my sessions
.wizard-step-column
%h2 Voice Chat Input
.ftue-box.chat-inputs
@ -209,39 +145,10 @@
%span.direct-monitoring-btn Play
.wizard-step{ 'layout-wizard-step' => "5", 'dialog-title' => "Test Router & Network", 'dialog-purpose' => "TestRouterNetwork" }
.wizard-step.network-test{ 'layout-wizard-step' => "5", 'dialog-title' => "Test Router & Network", 'dialog-purpose' => "TestRouterNetwork" }
.ftuesteps
.clearall
.help-text In this step, you will test your router and Internet connection to ensure that you can play in online sessions, and to see how many musicians can be in a session with you based on your internet connection.
.wizard-step-content
.wizard-step-column
%h2 Instructions
.ftue-box.instructions
%ul
%li Check that computer is connected to router using Ethernet cable.
%li Click Start Network Test button.
%li View test results.
.center
%a.button-orange.watch-video{href:'#'} WATCH VIDEO
.wizard-step-column
.summary
%p Ensure that your computer is connected to your home router using an Ethernet cable rather than using Wi-Fi wireless access. If necessary, find or purchase a long Ethernet cable, up to 100 ft.
%p Then click on the Start Network Test button below.
.center
%a.button-orange.start-network-test{href:'#'} START NETWORK TEST
.wizard-step-column
%h2 Test Results
.network-test-results.ftue-box
.scoring-bar
.current-score
testing...
.subscore
.good-marker
.good-line
.network-test-score
.scored-clients
.network-test-text
= render :partial => '/clients/network_test'
.wizard-step{ 'layout-wizard-step' => "6", 'dialog-title' => "Success!", 'dialog-purpose' => "Success" }
.ftuesteps
@ -256,23 +163,23 @@
%h2 Tutorial Videos
%ul
%li
%a Creating a Session
%a{href: '#'} Creating a Session
%li
%a Finding a Session
%a{href: '#'} Finding a Session
%li
%a Playing in a Session
%a{href: '#'} Playing in a Session
%li
%a Connecting with Other Musicians
%a{href: '#'} Connecting with Other Musicians
%li
%a Making and Sharing Recordings
%a{href: '#'} Making and Sharing Recordings
%li
%a Broadcasting Your Sessions
%a{href: '#'} Broadcasting Your Sessions
%h2 Other Valuable Resource Links
%ul
%li
%a JamKazam Support Center
%a{href: '#'} JamKazam Support Center
%li
%a JamKazam Community Forum
%a{href: '#'} JamKazam Community Forum
.wizard-buttons
%script{type: 'text/template', id: 'template-ftuesteps'}
@ -313,11 +220,19 @@
%script{type: 'text/template', id: 'template-track-target'}
.track{'data-num' => '{{data.num}}'}
.num {{data.num + 1}}:
.track-target{'data-num' => '{{data.num}}', 'track-count' => 0}
.track-target.target{'data-num' => '{{data.num}}', 'track-count' => 0}
%span.placeholder None
%script{type: 'text/template', id: 'template-output-target'}
.output{'data-num' => '{{data.num}}'}
.num {{data.num + 1}}:
.output-target.target{'data-num' => '{{data.num}}', 'output-count' => 0}
%span.placeholder None
%script{type: 'text/template', id: 'template-chat-input'}
.chat-input
%input{type:"radio", name: "chat-device", 'data-channel-id' => '{{data.id}}', 'data-channel-name' => '{{data.name}}'}
%p
= '{{data.name}}'
%span {{data.name}}

View File

@ -0,0 +1,81 @@
.dialog.loopback-wizard{ layout: 'dialog', 'layout-id' => 'loopback-wizard', id: 'loopback-wizard-dialog'}
.content-head
%h1 loopback latency test
.ftue-inner{ 'layout-wizard' => 'loopback-wizard' }
.wizard-step{ 'layout-wizard-step' => "0", 'dialog-title' => "Welcome", 'dialog-purpose' => "Intro"}
.help-text
Please identify which of the three types of audio gear below you
are going to use with the JamKazam service, and click one to
watch a video on how to navigate this initial setup and testing
process. After watching the video, click the 'NEXT' button to
get started. If you don't have your audio gear handy now, click
Cancel.
%ul.device_type
%li.ftue-video-link.first{'external-link-win'=>"http://www.youtube.com/watch?v=b1JrwGeUcOo", 'external-link-mac'=>"http://www.youtube.com/watch?v=TRzb7OTlO-Q"}
AUDIO DEVICE WITH PORTS FOR INSTRUMENT OR MIC INPUT JACKS
%br
%p
= image_tag "content/audio_capture_ftue.png", {:width => 243, :height => 70}
%li.ftue-video-link{'external-link-win'=>"http://www.youtube.com/watch?v=IDrLa8TOXwQ",'external-link-mac'=>"http://www.youtube.com/watch?v=vIs7ArrjMpE"}
USB MICROPHONE
%br
%p
= image_tag "content/microphone_ftue.png", {:width => 70, :height => 113}
%li.ftue-video-link{'external-link-win'=>"http://www.youtube.com/watch?v=PCri4Xed4CA",'external-link-mac'=>"http://www.youtube.com/watch?v=Gatmd_ja47U"}
COMPUTER'S BUILT-IN MIC &amp; SPEAKERS/HEADPHONES
%br
%p
= image_tag "content/computer_ftue.png", {:width => 118, :height => 105}
.wizard-step{ 'layout-wizard-step' => "1", 'dialog-title' => "Verify Audio", 'dialog-purpose' => "VerifyAudio" }
.help-text
Your currently configured audio gear is shown below. Before proceeding to the next step of the loopback test, play and speak and use the gain sliders
so that you can clearly hear both your instrument and/or voice through your headphones. When done click Next.
.wizard-step-content
.wizard-step-column
%h2
Audio Input
%a.button-orange.asio-settings-input-btn ASIO SETTINGS
.audio-device.input Unknown
%h2.ports-header Audio Input Ports
.ftue-box.input-ports.ports
.ftue-controls
.ftue-vu-left#loopback-audio-input-vu-left
.ftue-fader#loopback-audio-input-fader
.gain-label GAIN
.ftue-vu-right#loopback-audio-input-vu-right
= render :partial => "/clients/wizard/framebuffers"
.wizard-step-column
%h2
Audio Output
%a.button-orange.asio-settings-output-btn ASIO SETTINGS
.audio-device.output Unknown
%h2.ports-header Audio Output Ports
.ftue-box.output-ports.ports
.ftue-controls
.ftue-vu-left#loopback-audio-output-vu-left
.ftue-fader#loopback-audio-output-fader
.gain-label GAIN
.ftue-vu-right#loopback-audio-output-vu-right
%a.button-orange.resync-btn RESYNC
.wizard-step-column
%h2.test-results-header
Test & Results
%a.button-orange.run-test-btn RUN TEST
= render :partial => "/clients/wizard/gear_test", locals: {test_type: 'loopback'}
.clearall
.wizard-step{ 'layout-wizard-step' => "2", 'dialog-title' => "Congratulations", 'dialog-purpose' => "Congratulations" }
.help-text
Congratulations!
%br
%br
You have successfully scored your audio gear using the loopback test. Close the dialog to proceed.
.wizard-buttons

View File

@ -37,6 +37,9 @@
</ul>
</li>
<li class="download-app"><%= link_to "Download App", downloads_path, :rel => "external" %></li>
<% if @nativeClient %>
<li class="test-network"><%= link_to "Test Network", '#' %></li>
<% end %>
<li class="community-forum"><%= link_to "Community Forum", Rails.application.config.vanilla_url, :rel => "external" %></li>
<li class="get-help"><%= link_to "Get Help", 'https://jamkazam.desk.com/', :rel => "external" %></li>
<li class="sign-out"><%= link_to "Sign Out", signout_path, method: "delete" %></li>

View File

@ -283,6 +283,9 @@ SampleApp::Application.routes.draw do
match '/users/progression/certified_gear' => 'api_users#qualified_gear', :via => :post
match '/users/progression/social_promoted' => 'api_users#social_promoted', :via => :post
# audio latency
match '/users/:id/audio_latency' => 'api_users#audio_latency', :via => :post
# social
match '/users/:id/share/session/:provider' => 'api_users#share_session', :via => :get
match '/users/:id/share/recording/:provider' => 'api_users#share_recording', :via => :get

View File

@ -9,7 +9,7 @@ MusicSessionManager < BaseManager
@log = Logging.logger[self]
end
def create(music_session, user, client_id, description, musician_access, approval_required, fan_chat, fan_access, band, genres, tracks, legal_terms)
def create(music_session, user, client_id, description, musician_access, approval_required, fan_chat, fan_access, band, genres, tracks, legal_terms, audio_latency)
return_value = nil
time = Benchmark.realtime do
@ -38,7 +38,7 @@ MusicSessionManager < BaseManager
# auto-join this user into the newly created session
as_musician = true
connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks)
connection = ConnectionManager.new.join_music_session(user, client_id, active_music_session, as_musician, tracks, audio_latency)
unless connection.errors.any?
user.update_progression_field(:first_music_session_at)
@ -83,7 +83,7 @@ MusicSessionManager < BaseManager
music_session
end
def participant_create(user, music_session_id, client_id, as_musician, tracks)
def participant_create(user, music_session_id, client_id, as_musician, tracks, audio_latency)
connection = nil
music_session = nil
ActiveRecord::Base.transaction do
@ -92,7 +92,7 @@ MusicSessionManager < BaseManager
music_session.with_lock do # VRFS-1297
music_session.tick_track_changes
connection = ConnectionManager.new.join_music_session(user, client_id, music_session, as_musician, tracks)
connection = ConnectionManager.new.join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency)
if connection.errors.any?
# rollback the transaction to make sure nothing is disturbed in the database

View File

@ -11,7 +11,7 @@ describe ApiClaimedRecordingsController do
@music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true)
# @music_session.connections << @connection
@music_session.save
@connection.join_the_session(@music_session, true, nil, @user)
@connection.join_the_session(@music_session, true, nil, @user, 10)
@recording = Recording.start(@music_session, @user)
@recording.stop
@recording.reload

View File

@ -0,0 +1,36 @@
require 'spec_helper'
describe ApiUsersController do
render_views
let(:user) { FactoryGirl.create(:user) }
let (:conn) { FactoryGirl.create(:connection, :user => user) }
before(:each) do
controller.current_user = user
end
describe "audio_latency" do
it "updates both connection and user" do
post :audio_latency, id: user.id, client_id: conn.client_id, audio_latency: 1.5, :format => 'json'
response.should be_success
conn.reload
conn.last_jam_audio_latency.should == 1.5
user.reload
conn.user.last_jam_audio_latency.should == 1.5
end
it "if connection does not exist, user is still updated" do
post :audio_latency, id: user.id, client_id: 'nothingness', audio_latency: 1.5, :format => 'json'
response.should be_success
user.reload
conn.user.last_jam_audio_latency.should == 1.5
end
end
end

View File

@ -483,4 +483,25 @@ FactoryGirl.define do
cancel_all false
end
end
factory :diagnostic, :class => JamRuby::Diagnostic do
type JamRuby::Diagnostic::NO_HEARTBEAT_ACK
creator JamRuby::Diagnostic::CLIENT
data Faker::Lorem.sentence
end
factory :latency_tester, :class => JamRuby::LatencyTester do
ignore do
connection nil
make_connection true
end
sequence(:client_id) { |n| "LatencyTesterClientId-#{n}" }
after(:create) do |latency_tester, evaluator|
latency_tester.connection = evaluator.connection if evaluator.connection
latency_tester.connection = FactoryGirl.create(:connection, client_type: Connection::TYPE_LATENCY_TESTER, client_id: latency_tester.client_id) if evaluator.make_connection
latency_tester.save
end
end
end

View File

@ -0,0 +1,18 @@
require 'spec_helper'
describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => true, :slow => true do
subject { page }
let(:user) { FactoryGirl.create(:user) }
before(:each) do
emulate_client
sign_in_poltergeist user
end
it "launches from audio profile screen" do
end
end

View File

@ -7,10 +7,12 @@ describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => tru
let(:user) { FactoryGirl.create(:user) }
before(:each) do
LatencyTester.delete_all
sign_in_poltergeist user
end
it "success path" do
FactoryGirl.create(:latency_tester)
visit '/client#/account/audio'
# step 1 - intro
find("div.account-audio a[data-purpose='add-profile']").trigger(:click)
@ -23,6 +25,12 @@ describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => tru
# step 3 - configure tracks
find('.ftue-step-title', text: 'Configure Tracks')
# drag one input over to tracks area http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Element#drag_to-instance_method
input = first('.ftue-input')
track_slot = first('.track-target')
input.drag_to(track_slot)
find('.btn-next.button-orange').trigger(:click)
# step 4 - configure voice chat
@ -37,7 +45,6 @@ describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => tru
find('.ftue-step-title', text: 'Test Router & Network')
find('.button-orange.start-network-test').trigger(:click)
find('.user-btn', text: 'RUN NETWORK TEST ANYWAY').trigger(:click)
find('.button-grey.start-network-test').trigger(:click)
find('.button-orange.start-network-test')
find('.btn-next.button-orange').trigger(:click)

View File

@ -49,7 +49,7 @@ describe "In a Session", :js => true, :type => :feature, :capybara_feature => tr
wait_for_ajax
expect(page).to have_selector('h1', text: 'configure tracks')
find('#btn-add-new-audio-gear').trigger(:click)
find('.btn-add-new-audio-gear').trigger(:click)
wait_for_ajax
expect(page).to have_selector('h1', text: 'add new audio gear')

Some files were not shown because too many files have changed in this diff Show More