merge develop

This commit is contained in:
Brian Smith 2014-06-23 01:09:03 -04:00
commit e211871168
70 changed files with 1515 additions and 597 deletions

View File

@ -116,8 +116,8 @@ ActiveAdmin.register_page 'Feed' do
mixes.each do |mix| mixes.each do |mix|
li do li do
text_node "Created At: #{mix.created_at.strftime('%b %d %Y, %H:%M')}, " text_node "Created At: #{mix.created_at.strftime('%b %d %Y, %H:%M')}, "
text_node "Started At: #{mix.started_at.strftime('%b %d %Y, %H:%M')}, " text_node "Started At: #{mix.started_at ? mix.started_at.strftime('%b %d %Y, %H:%M') : ''}, "
text_node "Completed At: #{mix.completed_at.strftime('%b %d %Y, %H:%M')}, " text_node "Completed At: #{mix.completed_at ? mix.completed_at.strftime('%b %d %Y, %H:%M') : ''}, "
text_node "Error Count: #{mix.error_count}, " text_node "Error Count: #{mix.error_count}, "
text_node "Error Reason: #{mix.error_reason}, " text_node "Error Reason: #{mix.error_reason}, "
text_node "Error Detail: #{mix.error_detail}, " text_node "Error Detail: #{mix.error_detail}, "

View File

@ -10,7 +10,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
filter :email filter :email
filter :first_name filter :first_name
filter :last_name filter :last_name
filter :internet_service_provider
filter :created_at filter :created_at
filter :updated_at filter :updated_at
@ -28,7 +27,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
ff.input :musician ff.input :musician
ff.input :can_invite ff.input :can_invite
ff.input :photo_url ff.input :photo_url
ff.input :internet_service_provider
ff.input :session_settings ff.input :session_settings
end end
ff.inputs "Signup" do ff.inputs "Signup" do
@ -53,7 +51,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
row :last_name row :last_name
row :birth_date row :birth_date
row :gender row :gender
row :internet_service_provider
row :email_confirmed row :email_confirmed
row :image do user.photo_url ? image_tag(user.photo_url) : '' end row :image do user.photo_url ? image_tag(user.photo_url) : '' end
row :session_settings row :session_settings
@ -81,7 +78,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do
column :last_name column :last_name
column :birth_date column :birth_date
column :gender column :gender
column :internet_service_provider
column :email_confirmed column :email_confirmed
column :photo_url column :photo_url
column :session_settings column :session_settings

View File

@ -6,7 +6,7 @@
CONFIRM_URL = "http://www.jamkazam.com/confirm" # we can't get request.host_with_port, so hard-code confirm url (with user override) CONFIRM_URL = "http://www.jamkazam.com/confirm" # we can't get request.host_with_port, so hard-code confirm url (with user override)
attr_accessible :admin, :raw_password, :musician, :can_invite, :photo_url, :internet_service_provider, :session_settings, :confirm_url, :email_template # :invite_email attr_accessible :admin, :raw_password, :musician, :can_invite, :photo_url, :session_settings, :confirm_url, :email_template # :invite_email
def raw_password def raw_password
'' ''

View File

@ -3,6 +3,11 @@ description "jam-admin"
start on startup start on startup
start on runlevel [2345] start on runlevel [2345]
stop on runlevel [016] stop on runlevel [016]
limit nofile 20000 20000
limit core unlimited unlimited
respawn
respawn limit 10 5
pre-start script pre-start script
set -e set -e

View File

@ -36,6 +36,7 @@ FactoryGirl.define do
addr 0 addr 0
locidispid 0 locidispid 0
client_type 'client' client_type 'client'
sequence(:channel_id) { |n| "Channel#{n}"}
association :user, factory: :user association :user, factory: :user
end end

View File

@ -179,4 +179,5 @@ sms_index.sql
music_sessions_description_search.sql music_sessions_description_search.sql
rsvp_slots_prof_level.sql rsvp_slots_prof_level.sql
add_file_name_music_notation.sql add_file_name_music_notation.sql
change_scheduled_start_music_session.sql change_scheduled_start_music_session.sql
connection_channel_id.sql

View File

@ -0,0 +1 @@
ALTER TABLE connections ADD COLUMN channel_id VARCHAR(256) NOT NULL;

View File

@ -86,6 +86,7 @@ message ClientMessage {
SERVER_REJECTION_ERROR = 1005; SERVER_REJECTION_ERROR = 1005;
SERVER_PERMISSION_ERROR = 1010; SERVER_PERMISSION_ERROR = 1010;
SERVER_BAD_STATE_ERROR = 1015; SERVER_BAD_STATE_ERROR = 1015;
SERVER_DUPLICATE_CLIENT_ERROR = 1016;
} }
// Identifies which inner message is filled in // Identifies which inner message is filled in
@ -179,6 +180,7 @@ message ClientMessage {
optional ServerRejectionError server_rejection_error = 1005; optional ServerRejectionError server_rejection_error = 1005;
optional ServerPermissionError server_permission_error = 1010; optional ServerPermissionError server_permission_error = 1010;
optional ServerBadStateError server_bad_state_error = 1015; optional ServerBadStateError server_bad_state_error = 1015;
optional ServerDuplicateClientError server_duplicate_client_error = 1016;
} }
// route_to: server // route_to: server
@ -636,4 +638,9 @@ message ServerBadStateError {
optional string error_msg = 1; optional string error_msg = 1;
} }
// route_to: client
// this indicates that we detected another client with the same client ID
message ServerDuplicateClientError {
}

View File

@ -44,7 +44,7 @@ module JamRuby
end end
# reclaim the existing connection, if ip_address is not nil then perhaps a new address as well # reclaim the existing connection, if ip_address is not nil then perhaps a new address as well
def reconnect(conn, reconnect_music_session_id, ip_address, connection_stale_time, connection_expire_time) def reconnect(conn, channel_id, reconnect_music_session_id, ip_address, connection_stale_time, connection_expire_time)
music_session_id = nil music_session_id = nil
reconnected = false reconnected = false
@ -86,7 +86,7 @@ module JamRuby
end end
sql =<<SQL sql =<<SQL
UPDATE connections SET (aasm_state, updated_at, music_session_id, joined_session_at, stale_time, expire_time) = ('#{Connection::CONNECT_STATE.to_s}', NOW(), #{music_session_id_expression}, #{joined_session_at_expression}, #{connection_stale_time}, #{connection_expire_time}) UPDATE connections SET (channel_id, aasm_state, updated_at, music_session_id, joined_session_at, stale_time, expire_time) = ('#{channel_id}', '#{Connection::CONNECT_STATE.to_s}', NOW(), #{music_session_id_expression}, #{joined_session_at_expression}, #{connection_stale_time}, #{connection_expire_time})
WHERE WHERE
client_id = '#{conn.client_id}' client_id = '#{conn.client_id}'
RETURNING music_session_id RETURNING music_session_id
@ -184,7 +184,7 @@ SQL
# this number is used by notification logic elsewhere to know # this number is used by notification logic elsewhere to know
# 'oh the user joined for the 1st time, so send a friend update', or # 'oh the user joined for the 1st time, so send a friend update', or
# 'don't bother because the user has connected somewhere else already' # 'don't bother because the user has connected somewhere else already'
def create_connection(user_id, client_id, ip_address, client_type, connection_stale_time, connection_expire_time, &blk) def create_connection(user_id, client_id, channel_id, ip_address, client_type, connection_stale_time, connection_expire_time, &blk)
# validate client_type # validate client_type
raise "invalid client_type: #{client_type}" if client_type != 'client' && client_type != 'browser' raise "invalid client_type: #{client_type}" if client_type != 'client' && client_type != 'browser'
@ -216,8 +216,8 @@ SQL
lock_connections(conn) lock_connections(conn)
conn.exec("INSERT INTO connections (user_id, client_id, ip_address, client_type, addr, locidispid, aasm_state, stale_time, expire_time) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)", conn.exec("INSERT INTO connections (user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, aasm_state, stale_time, expire_time) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
[user_id, client_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time]).clear [user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time]).clear
# we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends # we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends
conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result| conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result|

View File

@ -222,6 +222,17 @@ module JamRuby
) )
end end
# create a server bad state error
def server_duplicate_client_error
error = Jampb::ServerDuplicateClientError.new()
Jampb::ClientMessage.new(
:type => ClientMessage::Type::SERVER_DUPLICATE_CLIENT_ERROR,
:route_to => CLIENT_TARGET,
:server_duplicate_client_error => error
)
end
###################################### NOTIFICATIONS ###################################### ###################################### NOTIFICATIONS ######################################
# create a friend update message # create a friend update message

View File

@ -19,6 +19,7 @@ module JamRuby
# or bootstrap a new latency_tester # or bootstrap a new latency_tester
def self.connect(options) def self.connect(options)
client_id = options[:client_id] client_id = options[:client_id]
channel_id = options[:channel_id]
ip_address = options[:ip_address] ip_address = options[:ip_address]
connection_stale_time = options[:connection_stale_time] connection_stale_time = options[:connection_stale_time]
connection_expire_time = options[:connection_expire_time] connection_expire_time = options[:connection_expire_time]
@ -69,6 +70,7 @@ module JamRuby
connection.stale_time = connection_stale_time connection.stale_time = connection_stale_time
connection.expire_time = connection_expire_time connection.expire_time = connection_expire_time
connection.as_musician = false connection.as_musician = false
connection.channel_id = channel_id
unless connection.save unless connection.save
return connection return connection
end end

View File

@ -125,7 +125,8 @@ FactoryGirl.define do
addr 0 addr 0
locidispid 0 locidispid 0
client_type 'client' client_type 'client'
last_jam_audio_latency { user.last_jam_audio_latency if user } last_jam_audio_latency { user.last_jam_audio_latency if user }
# sequence(:channel_id) { |n| "Channel#{n}"}
association :user, factory: :user association :user, factory: :user
end end

View File

@ -9,6 +9,8 @@ describe ConnectionManager do
STALE_BUT_NOT_EXPIRED = 50 STALE_BUT_NOT_EXPIRED = 50
DEFINITELY_EXPIRED = 70 DEFINITELY_EXPIRED = 70
let(:channel_id) {'1'}
before do before do
@conn = PG::Connection.new(:dbname => SpecDb::TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost") @conn = PG::Connection.new(:dbname => SpecDb::TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost")
@connman = ConnectionManager.new(:conn => @conn) @connman = ConnectionManager.new(:conn => @conn)
@ -46,8 +48,8 @@ describe ConnectionManager do
user.save! user.save!
user = nil user = nil
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
expect { @connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) }.to raise_error(PG::Error) expect { @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) }.to raise_error(PG::Error)
end end
it "create connection then delete it" do it "create connection then delete it" do
@ -56,7 +58,7 @@ describe ConnectionManager do
#user_id = create_user("test", "user2", "user2@jamkazam.com") #user_id = create_user("test", "user2", "user2@jamkazam.com")
user = FactoryGirl.create(:user) user = FactoryGirl.create(:user)
count = @connman.create_connection(user.id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) count = @connman.create_connection(user.id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
count.should == 1 count.should == 1
@ -86,7 +88,7 @@ describe ConnectionManager do
#user_id = create_user("test", "user2", "user2@jamkazam.com") #user_id = create_user("test", "user2", "user2@jamkazam.com")
user = FactoryGirl.create(:user) user = FactoryGirl.create(:user)
count = @connman.create_connection(user.id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) count = @connman.create_connection(user.id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
count.should == 1 count.should == 1
@ -102,7 +104,7 @@ describe ConnectionManager do
cc.addr.should == 0x01010101 cc.addr.should == 0x01010101
cc.locidispid.should == 17192000002 cc.locidispid.should == 17192000002
@connman.reconnect(cc, nil, "33.1.2.3", STALE_TIME, EXPIRE_TIME) @connman.reconnect(cc, channel_id, nil, "33.1.2.3", STALE_TIME, EXPIRE_TIME)
cc = Connection.find_by_client_id!(client_id) cc = Connection.find_by_client_id!(client_id)
cc.connected?.should be_true cc.connected?.should be_true
@ -215,7 +217,7 @@ describe ConnectionManager do
it "flag stale connection" do it "flag stale connection" do
client_id = "client_id8" client_id = "client_id8"
user_id = create_user("test", "user8", "user8@jamkazam.com") user_id = create_user("test", "user8", "user8@jamkazam.com")
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
num = JamRuby::Connection.count(:conditions => ['aasm_state = ?','connected']) num = JamRuby::Connection.count(:conditions => ['aasm_state = ?','connected'])
num.should == 1 num.should == 1
@ -256,7 +258,7 @@ describe ConnectionManager do
it "expires stale connection" do it "expires stale connection" do
client_id = "client_id8" client_id = "client_id8"
user_id = create_user("test", "user8", "user8@jamkazam.com") user_id = create_user("test", "user8", "user8@jamkazam.com")
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
conn = Connection.find_by_client_id(client_id) conn = Connection.find_by_client_id(client_id)
set_updated_at(conn, Time.now - STALE_BUT_NOT_EXPIRED) set_updated_at(conn, Time.now - STALE_BUT_NOT_EXPIRED)
@ -282,7 +284,7 @@ describe ConnectionManager do
music_session_id = music_session.id music_session_id = music_session.id
user = User.find(user_id) user = User.find(user_id)
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10)
connection.errors.any?.should be_false connection.errors.any?.should be_false
@ -318,8 +320,8 @@ describe ConnectionManager do
client_id2 = "client_id10.12" client_id2 = "client_id10.12"
user_id = create_user("test", "user10.11", "user10.11@jamkazam.com", :musician => true) user_id = create_user("test", "user10.11", "user10.11@jamkazam.com", :musician => true)
user_id2 = create_user("test", "user10.12", "user10.12@jamkazam.com", :musician => false) user_id2 = create_user("test", "user10.12", "user10.12@jamkazam.com", :musician => false)
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
@connman.create_connection(user_id2, client_id2, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id2, client_id2, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
music_session = FactoryGirl.create(:active_music_session, user_id: user_id) music_session = FactoryGirl.create(:active_music_session, user_id: user_id)
music_session_id = music_session.id music_session_id = music_session.id
@ -338,7 +340,7 @@ describe ConnectionManager do
client_id = "client_id10.2" client_id = "client_id10.2"
user_id = create_user("test", "user10.2", "user10.2@jamkazam.com") user_id = create_user("test", "user10.2", "user10.2@jamkazam.com")
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
music_session = FactoryGirl.create(:active_music_session, user_id: user_id) music_session = FactoryGirl.create(:active_music_session, user_id: user_id)
user = User.find(user_id) user = User.find(user_id)
@ -354,8 +356,8 @@ describe ConnectionManager do
fan_client_id = "client_id10.4" fan_client_id = "client_id10.4"
musician_id = create_user("test", "user10.3", "user10.3@jamkazam.com") musician_id = create_user("test", "user10.3", "user10.3@jamkazam.com")
fan_id = create_user("test", "user10.4", "user10.4@jamkazam.com", :musician => false) fan_id = create_user("test", "user10.4", "user10.4@jamkazam.com", :musician => false)
@connman.create_connection(musician_id, musician_client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(musician_id, musician_client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
@connman.create_connection(fan_id, fan_client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(fan_id, fan_client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
music_session = FactoryGirl.create(:active_music_session, :fan_access => false, user_id: musician_id) music_session = FactoryGirl.create(:active_music_session, :fan_access => false, user_id: musician_id)
music_session_id = music_session.id music_session_id = music_session.id
@ -379,7 +381,7 @@ describe ConnectionManager do
music_session_id = music_session.id music_session_id = music_session.id
user = User.find(user_id2) user = User.find(user_id2)
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
# specify real user id, but not associated with this session # specify real user id, but not associated with this session
expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound) expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound)
end end
@ -391,7 +393,7 @@ describe ConnectionManager do
user = User.find(user_id) user = User.find(user_id)
music_session = ActiveMusicSession.new music_session = ActiveMusicSession.new
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) connection = @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10)
connection.errors.size.should == 1 connection.errors.size.should == 1
connection.errors.get(:music_session).should == [ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED] connection.errors.get(:music_session).should == [ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED]
@ -405,7 +407,7 @@ describe ConnectionManager do
music_session_id = music_session.id music_session_id = music_session.id
user = User.find(user_id2) user = User.find(user_id2)
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
# specify real user id, but not associated with this session # specify real user id, but not associated with this session
expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound) expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound)
end end
@ -419,7 +421,7 @@ describe ConnectionManager do
user = User.find(user_id) user = User.find(user_id)
dummy_music_session = ActiveMusicSession.new dummy_music_session = ActiveMusicSession.new
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError) expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError)
end end
@ -434,7 +436,7 @@ describe ConnectionManager do
dummy_music_session = ActiveMusicSession.new dummy_music_session = ActiveMusicSession.new
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
@connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) @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) expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError)
end end
@ -447,7 +449,7 @@ describe ConnectionManager do
music_session_id = music_session.id music_session_id = music_session.id
user = User.find(user_id) user = User.find(user_id)
@connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
@connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10)
assert_session_exists(music_session_id, true) assert_session_exists(music_session_id, true)
@ -490,7 +492,7 @@ describe ConnectionManager do
user = User.find(user_id) user = User.find(user_id)
client_id1 = Faker::Number.number(20) client_id1 = Faker::Number.number(20)
@connman.create_connection(user_id, client_id1, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) @connman.create_connection(user_id, client_id1, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME)
music_session1 = FactoryGirl.create(:active_music_session, :user_id => user_id) music_session1 = FactoryGirl.create(:active_music_session, :user_id => user_id)
connection1 = @connman.join_music_session(user, client_id1, music_session1, true, TRACKS, 10) connection1 = @connman.join_music_session(user, client_id1, music_session1, true, TRACKS, 10)
connection1.errors.size.should == 0 connection1.errors.size.should == 0

View File

@ -2,7 +2,7 @@ require 'spec_helper'
describe LatencyTester do describe LatencyTester do
let(:params) {{client_id: 'abc', ip_address: '10.1.1.1', connection_stale_time:40, connection_expire_time:60} } let(:params) {{client_id: 'abc', ip_address: '10.1.1.1', connection_stale_time:40, connection_expire_time:60, channel_id: '1'} }
it "success" do it "success" do
latency_tester = FactoryGirl.create(:latency_tester) latency_tester = FactoryGirl.create(:latency_tester)

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

View File

@ -75,7 +75,8 @@
SERVER_BAD_STATE_RECOVERED: "SERVER_BAD_STATE_RECOVERED", SERVER_BAD_STATE_RECOVERED: "SERVER_BAD_STATE_RECOVERED",
SERVER_GENERIC_ERROR : "SERVER_GENERIC_ERROR", SERVER_GENERIC_ERROR : "SERVER_GENERIC_ERROR",
SERVER_REJECTION_ERROR : "SERVER_REJECTION_ERROR", SERVER_REJECTION_ERROR : "SERVER_REJECTION_ERROR",
SERVER_BAD_STATE_ERROR : "SERVER_BAD_STATE_ERROR" SERVER_BAD_STATE_ERROR : "SERVER_BAD_STATE_ERROR",
SERVER_DUPLICATE_CLIENT_ERROR : "SERVER_DUPLICATE_CLIENT_ERROR"
}; };
var route_to = context.JK.RouteToPrefix = { var route_to = context.JK.RouteToPrefix = {

View File

@ -33,6 +33,7 @@
var notificationLastSeenAt = undefined; var notificationLastSeenAt = undefined;
var notificationLastSeen = undefined; var notificationLastSeen = undefined;
var clientClosedConnection = false; var clientClosedConnection = false;
var initialConnectAttempt = true;
// reconnection logic // reconnection logic
var connectDeferred = null; var connectDeferred = null;
@ -85,6 +86,7 @@
// handles logic if the websocket connection closes, and if it was in error then also prompt for reconnect // handles logic if the websocket connection closes, and if it was in error then also prompt for reconnect
function closedCleanup(in_error) { function closedCleanup(in_error) {
server.connected = false;
// stop future heartbeats // stop future heartbeats
if (heartbeatInterval != null) { if (heartbeatInterval != null) {
@ -98,15 +100,16 @@
heartbeatAckCheckInterval = null; heartbeatAckCheckInterval = null;
} }
if (server.connected) { clearConnectTimeout();
server.connected = false;
if (app.clientUpdating) {
// we don't want to do a 'cover the whole screen' dialog
// because the client update is already showing.
return;
}
server.reconnecting = true; // noReconnect is a global to suppress reconnect behavior, so check it first
// we don't show any reconnect dialog on the initial connect; so we have this one-time flag
// to cause reconnects in the case that the websocket is down on the initially connect
if (!server.noReconnect &&
(initialConnectAttempt || !server.connecting)) {
server.connecting = true;
initialConnectAttempt = false;
var result = activeElementEvent('beforeDisconnect'); var result = activeElementEvent('beforeDisconnect');
@ -174,12 +177,16 @@
return mode == "client"; return mode == "client";
} }
function loggedIn(header, payload) { function clearConnectTimeout() {
if (connectTimeout) {
if (!connectTimeout) {
clearTimeout(connectTimeout); clearTimeout(connectTimeout);
connectTimeout = null; connectTimeout = null;
} }
}
function loggedIn(header, payload) {
clearConnectTimeout();
heartbeatStateReset(); heartbeatStateReset();
@ -191,6 +198,13 @@
$.cookie('client_id', payload.client_id); $.cookie('client_id', payload.client_id);
} }
// this has to be after context.jamclient.OnLoggedIn, because it hangs in scenarios
// where there is no device on startup for the current profile.
// So, in that case, it's possible that a reconnect loop will attempt, but we *do not want*
// it to go through unless we've passed through .OnLoggedIn
server.connecting = false;
initialConnectAttempt = false;
heartbeatMS = payload.heartbeat_interval * 1000; heartbeatMS = payload.heartbeat_interval * 1000;
connection_expire_time = payload.connection_expire_time * 1000; connection_expire_time = payload.connection_expire_time * 1000;
logger.info("jamkazam.js.loggedIn(): clientId=" + app.clientId + ", heartbeat=" + payload.heartbeat_interval + "s, expire_time=" + payload.connection_expire_time + 's'); logger.info("jamkazam.js.loggedIn(): clientId=" + app.clientId + ", heartbeat=" + payload.heartbeat_interval + "s, expire_time=" + payload.connection_expire_time + 's');
@ -240,7 +254,7 @@
var start = new Date().getTime(); var start = new Date().getTime();
server.connect() server.connect()
.done(function () { .done(function () {
guardAgainstRapidTransition(start, performReconnect); guardAgainstRapidTransition(start, finishReconnect);
}) })
.fail(function () { .fail(function () {
guardAgainstRapidTransition(start, closedOnReconnectAttempt); guardAgainstRapidTransition(start, closedOnReconnectAttempt);
@ -252,7 +266,7 @@
failedReconnect(); failedReconnect();
} }
function performReconnect() { function finishReconnect() {
if(!clientClosedConnection) { if(!clientClosedConnection) {
lastDisconnectedReason = 'WEBSOCKET_CLOSED_REMOTELY' lastDisconnectedReason = 'WEBSOCKET_CLOSED_REMOTELY'
@ -277,7 +291,6 @@
else { else {
window.location.reload(); window.location.reload();
} }
server.reconnecting = false;
}); });
@ -459,16 +472,29 @@
connectDeferred = new $.Deferred(); connectDeferred = new $.Deferred();
channelId = context.JK.generateUUID(); // create a new channel ID for every websocket connection channelId = context.JK.generateUUID(); // create a new channel ID for every websocket connection
var uri = context.gon.websocket_gateway_uri + '?channel_id=' + channelId; // Set in index.html.erb. // we will log in one of 3 ways:
// browser: use session cookie, and auth with token
// native: use session cookie, and use the token
// latency_tester: ask for client ID from backend; no token (trusted)
var params = {
channel_id: channelId,
token: $.cookie("remember_token"),
client_type: isClientMode() ? context.JK.clientType : 'latency_tester',
client_id: isClientMode() ? (gon.global.env == "development" ? $.cookie('client_id') : null): context.jamClient.clientID
}
var uri = context.gon.websocket_gateway_uri + '?' + $.param(params); // Set in index.html.erb.
logger.debug("connecting websocket: " + uri); logger.debug("connecting websocket: " + uri);
server.connecting = true;
server.socket = new context.WebSocket(uri); server.socket = new context.WebSocket(uri);
server.socket.onopen = server.onOpen; server.socket.onopen = server.onOpen;
server.socket.onmessage = server.onMessage; server.socket.onmessage = server.onMessage;
server.socket.onclose = server.onClose; server.socket.onclose = server.onClose;
connectTimeout = setTimeout(function () { connectTimeout = setTimeout(function () {
logger.debug("connection timeout fired")
connectTimeout = null; connectTimeout = null;
if(connectDeferred.state() === 'pending') { if(connectDeferred.state() === 'pending') {
@ -505,12 +531,7 @@
server.onOpen = function () { server.onOpen = function () {
logger.debug("server.onOpen"); logger.debug("server.onOpen");
if(isClientMode()) { // we should receive LOGIN_ACK very soon. we already set a timer elsewhere to give 4 seconds to receive it
server.rememberLogin();
}
else {
server.latencyTesterLogin();
}
}; };
server.onMessage = function (e) { server.onMessage = function (e) {
@ -675,7 +696,6 @@
} }
this.initialize = initialize; this.initialize = initialize;
this.initiateReconnect = initiateReconnect;
return this; return this;
} }

View File

@ -33,11 +33,11 @@
//= require jquery.pulse //= require jquery.pulse
//= require jquery.browser //= require jquery.browser
//= require jquery.custom-protocol //= require jquery.custom-protocol
//= require AAC_underscore
//= require AAA_Log //= require AAA_Log
//= require globals //= require globals
//= require AAB_message_factory //= require AAB_message_factory
//= require jam_rest //= require jam_rest
//= require AAC_underscore
//= require utils //= require utils
//= require custom_controls //= require custom_controls
//= require_directory . //= require_directory .

View File

@ -0,0 +1,108 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
// Class to intercept and delegate out all backend alerts as necessary
// better if modules that needed certain events would just register for them.
context.JK.BackendAlerts = function(app) {
var $document = $(document);
var ALERT_TYPES = context.JK.ALERT_TYPES;
function onNoValidAudioConfig(type, text) {
app.notify({
"title": ALERT_TYPES[type].title,
"text": text,
"icon_url": "/assets/content/icon_alert_big.png"
});
context.location = "/client#"; // leaveSession will be called in beforeHide below
}
function onStunEvent() {
var testResults = context.jamClient.NetworkTestResult();
$.each(testResults, function (index, val) {
if (val.bStunFailed) {
// if true we could not reach a stun server
}
else if (val.bRemoteUdpBocked) {
// if true the user cannot communicate with peer via UDP, although they could do LAN based session
}
});
}
function onGenericEvent(type, text) {
context.setTimeout(function() {
var alert = ALERT_TYPES[type];
if(alert && alert.title) {
app.notify({
"title": ALERT_TYPES[type].title,
"text": text,
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unknown Backend Event type %o, data %o", type, text)
}
}, 1);
}
function alertCallback(type, text) {
function timeCallback() {
var start = new Date();
setTimeout(function() {
var timed = new Date().getTime() - start.getTime();
if(timed > 250) {
logger.warn("SLOW AlERT_CALLBACK. type: %o text: %o time: %o", type, text, timed);
}
}, 1);
}
timeCallback();
logger.debug("alert callback", type, text);
var alertData = ALERT_TYPES[type];
if(alertData) {
$document.triggerHandler(alertData.name, alertData);
}
if (type === 2) { // BACKEND_MIXER_CHANGE
context.JK.CurrentSessionModel.onBackendMixerChanged(type, text)
}
else if (type === 19) { // NO_VALID_AUDIO_CONFIG
onNoValidAudioConfig(type, text);
}
else if (type === 24) {
onStunEvent();
}
else if (type === 26) { // DEAD_USER_REMOVE_EVENT
context.JK.CurrentSessionModel.onDeadUserRemove(type, text);
}
else if (type === 27) { // WINDOW_CLOSE_BACKGROUND_MODE
context.JK.CurrentSessionModel.onWindowBackgrounded(type, text);
}
else if(type != 30 && type != 31 && type != 21){ // these are handled elsewhere
onGenericEvent(type, text);
}
}
function initialize() {
context.jamClient.SessionSetAlertCallback("JK.AlertCallback");
}
this.initialize = initialize;
context.JK.AlertCallback = alertCallback;
return this;
}
})(window, jQuery);

View File

@ -10,9 +10,17 @@
var logger = context.JK.logger; var logger = context.JK.logger;
var $banner = null; var $banner = null;
// you can also do
// * showAlert('title', 'text')
// * showAlert('text')
function showAlert(options) { function showAlert(options) {
if (typeof options == 'string' || options instanceof String) { if (typeof options == 'string' || options instanceof String) {
options = {html:options}; if(arguments.length == 2) {
options = {title: options, html:arguments[1]}
}
else {
options = {html:options};
}
} }
options.type = 'alert' options.type = 'alert'
return show(options); return show(options);
@ -24,6 +32,13 @@
var text = options.text; var text = options.text;
var html = options.html; var html = options.html;
if(!options.title) {
options.title = 'alert'
}
var $h1 = $banner.find('h1');
$h1.html(options.title);
var newContent = null; var newContent = null;
if (html) { if (html) {
newContent = $('#banner .dialog-inner').html(html); newContent = $('#banner .dialog-inner').html(html);

View File

@ -274,7 +274,6 @@
if(response.next == null) { if(response.next == null) {
// if we less results than asked for, end searching // if we less results than asked for, end searching
$chatMessagesScroller.infinitescroll('pause'); $chatMessagesScroller.infinitescroll('pause');
logger.debug("end of chatss");
if(currentPage > 0) { if(currentPage > 0) {
// there are bugs with infinitescroll not removing the 'loading'. // there are bugs with infinitescroll not removing the 'loading'.

View File

@ -80,7 +80,8 @@
} }
function validateVoiceChatSettings() { function validateVoiceChatSettings() {
return voiceChatHelper.trySave(); //return voiceChatHelper.trySave(); // not necessary since we use saveImmediate now
return true;
} }
function showMusicAudioPanel() { function showMusicAudioPanel() {
@ -117,7 +118,9 @@
}); });
$btnCancel.click(function() { $btnCancel.click(function() {
app.layout.closeDialog('configure-tracks') if(voiceChatHelper.cancel()) {
app.layout.closeDialog('configure-tracks')
}
return false; return false;
}); });
@ -185,10 +188,12 @@
configureTracksHelper.reset(); configureTracksHelper.reset();
voiceChatHelper.reset(); voiceChatHelper.reset();
voiceChatHelper.beforeShow();
} }
function afterHide() { function afterHide() {
voiceChatHelper.beforeHide();
} }
@ -212,11 +217,11 @@
$btnAddNewGear = $dialog.find('.btn-add-new-audio-gear'); $btnAddNewGear = $dialog.find('.btn-add-new-audio-gear');
$btnUpdateTrackSettings = $dialog.find('.btn-update-settings'); $btnUpdateTrackSettings = $dialog.find('.btn-update-settings');
configureTracksHelper = new JK.ConfigureTracksHelper(app); configureTracksHelper = new context.JK.ConfigureTracksHelper(app);
configureTracksHelper.initialize($dialog); configureTracksHelper.initialize($dialog);
voiceChatHelper = new JK.VoiceChatHelper(app); voiceChatHelper = new context.JK.VoiceChatHelper(app);
voiceChatHelper.initialize($dialog, false); voiceChatHelper.initialize($dialog, 'configure_track_dialog', true, {vuType: "vertical", lightCount: 10, lightWidth: 3, lightHeight: 17}, 191);
events(); events();
} }

View File

@ -22,25 +22,30 @@
var $instrumentsHolder = null; var $instrumentsHolder = null;
var isDragging = false; var isDragging = false;
function removeHoverer($hoverChannel) {
var $channel = $hoverChannel.data('original')
$channel.data('cloned', null);
$hoverChannel.remove();
}
function hoverIn($channel) { function hoverIn($channel) {
if(isDragging) return; if(isDragging) return;
$channel.css('color', 'white')
var $container = $channel.closest('.target'); var $container = $channel.closest('.target');
var inTarget = $container.length > 0; var inTarget = $container.length > 0;
if(!inTarget) { if(!inTarget) {
$container = $channel.closest('.channels-holder') $container = $channel.closest('.channels-holder')
} }
$channel.data('container', $container)
$channel.addClass('hovering');
var $inputs = $container.find('.ftue-input'); var $inputs = $container.find('.ftue-input');
var index = $inputs.index($channel); var index = $inputs.index($channel);
$channel.css('background-color', '#333');
// $channel.css('padding', '0 5px'); // $channel.css('padding', '0 5px');
if(inTarget) { if(inTarget) {
$channel.data('container', $container)
$channel.addClass('hovering');
$channel.css('color', 'white')
$channel.css('background-color', '#333');
$channel.css('border', '#333'); $channel.css('border', '#333');
$channel.css('border-radius', '2px'); $channel.css('border-radius', '2px');
$channel.css('min-width', '49%'); $channel.css('min-width', '49%');
@ -49,10 +54,44 @@
$container.css('overflow', 'visible'); $container.css('overflow', 'visible');
} }
else { else {
// TODO: make the unassigned work var $offsetParent = $channel.offsetParent();
// $channel.css('min-width', $channel.css('width')); var parentOffset = $offsetParent.offset();
// $channel.css('position', 'absolute');
// $container.addClass('compensate'); var hoverChannel = $(context._.template($templateAssignablePort.html(), {id: 'bogus', name: $channel.text()}, { variable: 'data' }));
hoverChannel
.css('position', 'absolute')
.css('color', 'white')
.css('left', $channel.position().left)
.css('top', $channel.position().top)
.css('background-color', '#333')
.css('min-width', $channel.width())
.css('min-height', $channel.height())
.css('z-index', 10000)
.css('background-position', '4px 6px')
.css('padding-left', '14px')
.data('original', $channel);
$channel.data('cloned', hoverChannel);
hoverChannel
.hover(function(e) {
var hoverCheckTimeout = hoverChannel.data('hoverCheckTimeout');
if(hoverCheckTimeout) {
clearTimeout(hoverCheckTimeout);
hoverChannel.data('hoverCheckTimeout', null);
}
}, function() { removeHoverer($(this)); })
.mousedown(function(e) {
// because we have obscured the element the user wants to drag,
// we proxy a mousedown on the hover-element to the covered .ftue-input ($channel).
// this causes jquery.drag to get going even though the user clicked a different element
$channel.trigger(e)
})
hoverChannel.data('hoverCheckTimeout', setTimeout(function() {
// check if element has already been left
hoverChannel.data('hoverCheckTimeout', null);
removeHoverer(hoverChannel);
}, 500));
hoverChannel.prependTo($offsetParent);
} }
$channel.css('z-index', 10000) $channel.css('z-index', 10000)
@ -69,6 +108,12 @@
} }
function hoverOut($channel) { function hoverOut($channel) {
var $cloned = $channel.data('cloned');
if($cloned) {
return; // let the cloned handle the rest of hover out logic when it's hovered-out
}
$channel $channel
.removeClass('hovering') .removeClass('hovering')
.css('color', '') .css('color', '')
@ -77,6 +122,8 @@
.css('width', '') .css('width', '')
.css('background-color', '') .css('background-color', '')
.css('padding', '') .css('padding', '')
.css('padding-left', '')
.css('background-position', '')
.css('border', '') .css('border', '')
.css('border-radius', '') .css('border-radius', '')
.css('right', '') .css('right', '')
@ -88,9 +135,9 @@
//var $container = $channel.closest('.target'); //var $container = $channel.closest('.target');
var $container = $channel.data('container'); var $container = $channel.data('container');
$container.css('overflow', '') if($container) {
$container.removeClass('compensate'); $container.css('overflow', '')
}
} }
function fixClone($clone) { function fixClone($clone) {

View File

@ -13,7 +13,6 @@
var $draggingFader = null; var $draggingFader = null;
var draggingOrientation = null; var draggingOrientation = null;
var subscribers = {};
var logger = g.JK.logger; var logger = g.JK.logger;
function faderClick(e) { function faderClick(e) {
@ -21,7 +20,6 @@
var $fader = $(this); var $fader = $(this);
draggingOrientation = $fader.attr('orientation'); draggingOrientation = $fader.attr('orientation');
var faderId = $fader.attr("fader-id");
var offset = $fader.offset(); var offset = $fader.offset();
var position = { top: e.pageY - offset.top, left: e.pageX - offset.left} var position = { top: e.pageY - offset.top, left: e.pageX - offset.left}
@ -31,12 +29,7 @@
return false; return false;
} }
// Notify subscribers of value change $fader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: false})
g._.each(subscribers, function (changeFunc, index, list) {
if (faderId === index) {
changeFunc(faderId, faderPct, false);
}
});
setHandlePosition($fader, faderPct); setHandlePosition($fader, faderPct);
return false; return false;
@ -110,7 +103,6 @@
} }
function onFaderDrag(e, ui) { function onFaderDrag(e, ui) {
var faderId = $draggingFader.attr("fader-id");
var faderPct = faderValue($draggingFader, e, ui.position); var faderPct = faderValue($draggingFader, e, ui.position);
// protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows // protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows
@ -118,12 +110,7 @@
return false; return false;
} }
// Notify subscribers of value change $draggingFader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: true})
g._.each(subscribers, function (changeFunc, index, list) {
if (faderId === index) {
changeFunc(faderId, faderPct, true);
}
});
} }
function onFaderDragStart(e, ui) { function onFaderDragStart(e, ui) {
@ -133,7 +120,6 @@
} }
function onFaderDragStop(e, ui) { function onFaderDragStop(e, ui) {
var faderId = $draggingFader.attr("fader-id");
var faderPct = faderValue($draggingFader, e, ui.position); var faderPct = faderValue($draggingFader, e, ui.position);
// protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows // protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows
@ -142,12 +128,8 @@
return; return;
} }
// Notify subscribers of value change $draggingFader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: false});
g._.each(subscribers, function (changeFunc, index, list) {
if (faderId === index) {
changeFunc(faderId, faderPct, false);
}
});
$draggingFaderHandle = null; $draggingFaderHandle = null;
$draggingFader = null; $draggingFader = null;
draggingOrientation = null; draggingOrientation = null;
@ -159,26 +141,15 @@
g.JK.FaderHelpers = { g.JK.FaderHelpers = {
/**
* Subscribe to fader change events. Provide a subscriber id
* and a function in the form: change(faderId, newValue) which
* will be called anytime a fader changes value.
*/
subscribe: function (subscriberId, changeFunction) {
subscribers[subscriberId] = changeFunction;
},
/** /**
* Render a fader into the element identifed by the provided * Render a fader into the element identifed by the provided
* selector, with the provided options. * selector, with the provided options.
*/ */
renderFader: function (selector, userOptions) { renderFader: function (selector, userOptions) {
selector = $(selector);
if (userOptions === undefined) { if (userOptions === undefined) {
throw ("renderFader: userOptions is required"); throw ("renderFader: userOptions is required");
} }
if (!(userOptions.hasOwnProperty("faderId"))) {
throw ("renderFader: userOptions.faderId is required");
}
var renderDefaults = { var renderDefaults = {
faderType: "vertical", faderType: "vertical",
height: 83, // only used for vertical height: 83, // only used for vertical
@ -189,9 +160,9 @@
"#template-fader-h" : '#template-fader-v'; "#template-fader-h" : '#template-fader-v';
var templateSource = $(templateSelector).html(); var templateSource = $(templateSelector).html();
$(selector).html(g._.template(templateSource, options)); selector.html(g._.template(templateSource, options));
$('div[control="fader-handle"]', $(selector)).draggable({ selector.find('div[control="fader-handle"]').draggable({
drag: onFaderDrag, drag: onFaderDrag,
start: onFaderDragStart, start: onFaderDragStart,
stop: onFaderDragStop, stop: onFaderDragStop,
@ -202,7 +173,7 @@
// Embed any custom styles, applied to the .fader below selector // Embed any custom styles, applied to the .fader below selector
if ("style" in options) { if ("style" in options) {
for (var key in options.style) { for (var key in options.style) {
$(selector + ' .fader').css(key, options.style[key]); selector.find(' .fader').css(key, options.style[key]);
} }
} }
}, },

View File

@ -110,11 +110,8 @@
}); });
} }
function faderChange(faderId, newValue, dragging) { function faderChange(e, data) {
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 setSaveButtonState($save, enabled) { function setSaveButtonState($save, enabled) {
@ -1043,7 +1040,7 @@
context.JK.ftueVUCallback = function (dbValue, selector) { context.JK.ftueVUCallback = function (dbValue, selector) {
// Convert DB into a value from 0.0 - 1.0 // Convert DB into a value from 0.0 - 1.0
var floatValue = (dbValue + 80) / 100; var floatValue = (dbValue + 80) / 100;
context.JK.VuHelpers.updateVU(selector, floatValue); context.JK.VuHelpers.updateVU($(selector), floatValue);
}; };
context.JK.ftueAudioInputVUCallback = function (dbValue) { context.JK.ftueAudioInputVUCallback = function (dbValue) {

View File

@ -85,10 +85,33 @@
google: 'google', google: 'google',
}; };
var audioTestFailReasons = {
latency : 'Latency',
ioVariance : 'ioVariance',
ioTarget : 'ioTarget'
}
var audioTestDataReasons = {
pass : 'Pass',
latencyFail : 'LatencyFail',
ioVarianceFail : 'ioVarianceFail',
ioTargetFail : 'ioTargetFail'
}
var networkTestFailReasons = {
stun : 'STUN',
bandwidth : 'Bandwidth',
packetRate : 'PacketRate',
jitter : 'Jitter',
jamerror : 'ServiceError',
noNetwork : 'NoNetwork'
}
var categories = { var categories = {
register : "Register", register : "Register",
download : "DownloadClient", download : "DownloadClient",
audioTest : "AudioTest", audioTest : "AudioTest",
audioTestData : 'AudioTestData',
trackConfig : "AudioTrackConfig", trackConfig : "AudioTrackConfig",
networkTest : "NetworkTest", networkTest : "NetworkTest",
sessionCount : "SessionCount", sessionCount : "SessionCount",
@ -105,7 +128,6 @@
jkFollow : 'jkFollow', jkFollow : 'jkFollow',
jkFavorite : 'jkFavorite', jkFavorite : 'jkFavorite',
jkComment : 'jkComment' jkComment : 'jkComment'
}; };
function translatePlatformForGA(platform) { function translatePlatformForGA(platform) {
@ -289,18 +311,48 @@
context.ga('send', 'event', categories.networkTest, 'Passed', normalizedPlatform, numUsers); context.ga('send', 'event', categories.networkTest, 'Passed', normalizedPlatform, numUsers);
} }
function trackNetworkTestFailure(reason, data) {
assertOneOf(reason, networkTestFailReasons);
context.ga('send', 'event', categories.networkTest, 'Failed', reason, data);
}
function trackAudioTestCompletion(platform) { function trackAudioTestCompletion(platform) {
var normalizedPlatform = translatePlatformForGA(platform); var normalizedPlatform = translatePlatformForGA(platform);
context.ga('send', 'event', categories.audioTest, 'Passed', normalizedPlatform); context.ga('send', 'event', categories.audioTest, 'Passed', normalizedPlatform);
} }
function trackAudioTestFailure(platform, reason, data) {
assertOneOf(reason, audioTestFailReasons);
var normalizedPlatform = translatePlatformForGA(platform);
if(normalizedPlatform == "Windows") {
var action = "FailedWin";
}
else if(normalizedPlatform == "Mac") {
var action = "FailedMac";
}
else {
var action = "FailedLinux";
}
context.ga('send', 'event', categories.audioTest, action, reason, data);
}
function trackConfigureTracksCompletion(platform) { function trackConfigureTracksCompletion(platform) {
var normalizedPlatform = translatePlatformForGA(platform); var normalizedPlatform = translatePlatformForGA(platform);
context.ga('send', 'event', categories.trackConfig, 'Passed', normalizedPlatform); context.ga('send', 'event', categories.trackConfig, 'Passed', normalizedPlatform);
} }
function trackAudioTestData(uniqueDeviceName, reason, data) {
assertOneOf(reason, audioTestDataReasons);
context.ga('send', 'event', categories.audioTestData, uniqueDeviceName, reason, data);
}
var GA = {}; var GA = {};
GA.Categories = categories; GA.Categories = categories;
GA.SessionCreationTypes = sessionCreationTypes; GA.SessionCreationTypes = sessionCreationTypes;
@ -310,11 +362,17 @@
GA.RecordingActions = recordingActions; GA.RecordingActions = recordingActions;
GA.BandActions = bandActions; GA.BandActions = bandActions;
GA.JKSocialTargets = jkSocialTargets; GA.JKSocialTargets = jkSocialTargets;
GA.AudioTestFailReasons = audioTestFailReasons;
GA.AudioTestDataReasons = audioTestDataReasons;
GA.NetworkTestFailReasons = networkTestFailReasons;
GA.trackRegister = trackRegister; GA.trackRegister = trackRegister;
GA.trackDownload = trackDownload; GA.trackDownload = trackDownload;
GA.trackFTUECompletion = trackFTUECompletion; GA.trackFTUECompletion = trackFTUECompletion;
GA.trackNetworkTest = trackNetworkTest; GA.trackNetworkTest = trackNetworkTest;
GA.trackNetworkTestFailure = trackNetworkTestFailure;
GA.trackAudioTestCompletion = trackAudioTestCompletion; GA.trackAudioTestCompletion = trackAudioTestCompletion;
GA.trackAudioTestFailure = trackAudioTestFailure;
GA.trackAudioTestData = trackAudioTestData;
GA.trackConfigureTracksCompletion = trackConfigureTracksCompletion; GA.trackConfigureTracksCompletion = trackConfigureTracksCompletion;
GA.trackSessionCount = trackSessionCount; GA.trackSessionCount = trackSessionCount;
GA.trackSessionMusicians = trackSessionMusicians; GA.trackSessionMusicians = trackSessionMusicians;

View File

@ -31,6 +31,118 @@
DIALOG_CLOSED : 'dialog_closed' DIALOG_CLOSED : 'dialog_closed'
} }
context.JK.ALERT_NAMES = {
NO_EVENT : 0,
BACKEND_ERROR : 1, //generic error - eg P2P message error
BACKEND_MIXER_CHANGE : 2, //event that controls have been regenerated
//network related
PACKET_JTR : 3,
PACKET_LOSS : 4,
PACKET_LATE : 5,
JTR_QUEUE_DEPTH : 6,
NETWORK_JTR : 7,
NETWORK_PING : 8,
BITRATE_THROTTLE_WARN : 9,
BANDWIDTH_LOW : 10,
//IO related events
INPUT_IO_RATE : 11,
INPUT_IO_JTR : 12,
OUTPUT_IO_RATE : 13,
OUTPUT_IO_JTR : 14,
// CPU load related
CPU_LOAD : 15,
DECODE_VIOLATIONS : 16,
LAST_THRESHOLD : 17,
WIFI_NETWORK_ALERT : 18, //user or peer is using wifi
NO_VALID_AUDIO_CONFIG : 19, // alert the user to popup a config
AUDIO_DEVICE_NOT_PRESENT : 20, // the audio device is not connected
RECORD_PLAYBACK_STATE : 21, // record/playback events have occurred
RUN_UPDATE_CHECK_BACKGROUND : 22, //this is auto check - do
RUN_UPDATE_CHECK_INTERACTIVE : 23, //this is initiated by user
STUN_EVENT : 24, // system completed stun test... come get the result
DEAD_USER_WARN_EVENT : 25, //the backend is not receiving audio from this peer
DEAD_USER_REMOVE_EVENT : 26, //the backend is removing the user from session as no audio is coming from this peer
WINDOW_CLOSE_BACKGROUND_MODE : 27, //the user has closed the window and the client is now in background mode
WINDOW_OPEN_FOREGROUND_MODE : 28, //the user has opened the window and the client is now in forground mode/
SESSION_LIVEBROADCAST_FAIL : 29, //error of some sort - so can't broadcast
SESSION_LIVEBROADCAST_ACTIVE : 30, //active
SESSION_LIVEBROADCAST_STOPPED : 31, //stopped by server/user
SESSION_LIVEBROADCAST_PINNED : 32, //node pinned by user
SESSION_LIVEBROADCAST_UNPINNED : 33, //node unpinned by user
BACKEND_STATUS_MSG : 34, //status/informational message
LOCAL_NETWORK_VARIANCE_HIGH : 35,//the ping time via a hairpin for the user network is unnaturally high or variable.
//indicates problem with user computer stack or network itself (wifi, antivirus etc)
LOCAL_NETWORK_LATENCY_HIGH : 36,
RECORDING_CLOSE : 37, //update and remove tracks from front-end
PEER_REPORTS_NO_AUDIO_RECV : 38, //letting front-end know audio is not being received from a user in session
LAST_ALERT : 39
}
// recreate eThresholdType enum from MixerDialog.h
context.JK.ALERT_TYPES = {
0: {"title": "", "message": ""}, // NO_EVENT,
1: {"title": "", "message": ""}, // BACKEND_ERROR: generic error - eg P2P message error
2: {"title": "", "message": ""}, // BACKEND_MIXER_CHANGE, - event that controls have been regenerated
3: {"title": "High Packet Jitter", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // PACKET_JTR,
4: {"title": "High Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // PACKET_LOSS
5: {"title": "High Packet Late", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // PACKET_LATE,
6: {"title": "Large Jitter Queue", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // JTR_QUEUE_DEPTH,
7: {"title": "High Network Jitter", "message": "Your network connection is currently experiencing network jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // NETWORK_JTR,
8: {"title": "High Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // NETWORK_PING,
9: {"title": "Bandwidth Throttled", "message": "The available bandwidth on your network has diminished, and this may impact your audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // BITRATE_THROTTLE_WARN,
10:{"title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low, and this may impact your audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // BANDWIDTH_LOW
// IO related events
11:{"title": "Variable Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // INPUT_IO_RATE
12:{"title": "High Input Jitter", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // INPUT_IO_JTR,
13:{"title": "Variable Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // OUTPUT_IO_RATE
14:{"title": "High Output Jitter", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // OUTPUT_IO_JTR,
// CPU load related
15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // CPU_LOAD
16: {"title": "Decode Violations", "message": ""}, // DECODE_VIOLATIONS,
17: {"title": "", "message": ""}, // LAST_THRESHOLD
18: {"title": "Wifi Alert", "message": ""}, // WIFI_NETWORK_ALERT, //user or peer is using wifi
19: {"title": "No Audio Configuration", "message": "You cannot join the session because you do not have a valid audio configuration."}, // NO_VALID_AUDIO_CONFIG,
20: {"title": "Audio Device Not Present", "message": ""}, // AUDIO_DEVICE_NOT_PRESENT, // the audio device is not connected
21: {"title": "", "message": ""}, // RECORD_PLAYBACK_STATE, // record/playback events have occurred
22: {"title": "", "message": ""}, // RUN_UPDATE_CHECK_BACKGROUND, //this is auto check - do
23: {"title": "", "message": ""}, // RUN_UPDATE_CHECK_INTERACTIVE, //this is initiated by user
24: {"title": "", "message": ""}, // STUN_EVENT, // system completed stun test... come get the result
25: {"title": "No Audio", "message": "Your system is no longer transmitting audio. Other session members are unable to hear you."}, // DEAD_USER_WARN_EVENT, //the backend is not receiving audio from this peer
26: {"title": "No Audio", "message": "Your system is no longer transmitting audio. Other session members are unable to hear you."}, // DEAD_USER_REMOVE_EVENT, //the backend is removing the user from session as no audio is coming from this peer
27: {"title": "", "message": ""}, // WINDOW_CLOSE_BACKGROUND_MODE, //the user has closed the window and the client is now in background mode
28: {"title": "", "message": ""}, // WINDOW_OPEN_FOREGROUND_MODE, //the user has opened the window and the client is now in forground mode/
29: {"title": "Failed to Broadcast", "message": ""}, // SESSION_LIVEBROADCAST_FAIL, //error of some sort - so can't broadcast
30: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_ACTIVE, //active
31: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_STOPPED, //stopped by server/user
32: {"title": "Client Pinned", "message": "This client will be the source of a broadcast."}, // SESSION_LIVEBROADCAST_PINNED, //node pinned by user
33: {"title": "Client No Longer Pinned", "message": "This client is no longer designated as the source of the broadcast."}, // SESSION_LIVEBROADCAST_UNPINNED, //node unpinned by user
34: {"title": "", "message": ""}, // BACKEND_STATUS_MSG, //status/informational message
35: {"title": "LAN Unpredictable", "message": "Your local network is adding considerable variance to transmit times. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // LOCAL_NETWORK_VARIANCE_HIGH,//the ping time via a hairpin for the user network is unnaturally high or variable.
//indicates problem with user computer stack or network itself (wifi, antivirus etc)
36: {"title": "LAN High Latency", "message": "Your local network is adding considerable latency. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // LOCAL_NETWORK_LATENCY_HIGH,
37: {"title": "", "message": ""}, // RECORDING_CLOSE, //update and remove tracks from front-end
38: {"title": "No Audio Sent", "message": ""}, // PEER_REPORTS_NO_AUDIO_RECV, //update and remove tracks from front-end
39: {"title": "", "message": ""} // LAST_ALERT
};
// add the alert's name to the ALERT_TYPES structure
context._.each(context.JK.ALERT_NAMES, function(alert_code, alert_name) {
var alert_data = context.JK.ALERT_TYPES[alert_code];
alert_data.name = alert_name;
})
context.JK.MAX_TRACKS = 6; context.JK.MAX_TRACKS = 6;
context.JK.MAX_OUTPUTS = 2; context.JK.MAX_OUTPUTS = 2;
@ -113,36 +225,42 @@
context.JK.AUDIO_DEVICE_BEHAVIOR = { context.JK.AUDIO_DEVICE_BEHAVIOR = {
MacOSX_builtin: { MacOSX_builtin: {
display: 'MacOSX Built-In', display: 'MacOSX Built-In',
videoURL: undefined, shortName: 'Built-In',
videoURL: "https://www.youtube.com/watch?v=7-9PW50ygHk",
showKnobs: false, showKnobs: false,
showASIO: false showASIO: false
}, },
MacOSX_interface: { MacOSX_interface: {
display: 'MacOSX external interface', display: 'MacOSX external interface',
videoURL: undefined, shortName: 'External',
videoURL: "https://www.youtube.com/watch?v=7BLld6ogm14",
showKnobs: false, showKnobs: false,
showASIO: false showASIO: false
}, },
Win32_wdm: { Win32_wdm: {
display: 'Windows WDM', display: 'Windows WDM',
videoURL: undefined, shortName : 'WDM',
videoURL: "https://www.youtube.com/watch?v=L36UBkAV14c",
showKnobs: true, showKnobs: true,
showASIO: false showASIO: false
}, },
Win32_asio: { Win32_asio: {
display: 'Windows ASIO', display: 'Windows ASIO',
videoURL: undefined, shortName : 'ASIO',
videoURL: "https://www.youtube.com/watch?v=PGUmISTVVMY",
showKnobs: false, showKnobs: false,
showASIO: true showASIO: true
}, },
Win32_asio4all: { Win32_asio4all: {
display: 'Windows ASIO4ALL', display: 'Windows ASIO4ALL',
videoURL: undefined, shortName : 'ASIO4ALL',
videoURL: "https://www.youtube.com/watch?v=PGUmISTVVMY",
showKnobs: false, showKnobs: false,
showASIO: true showASIO: true
}, },
Linux: { Linux: {
display: 'Linux', display: 'Linux',
shortName : 'linux',
videoURL: undefined, videoURL: undefined,
showKnobs: true, showKnobs: true,
showASIO: false showASIO: false

View File

@ -718,10 +718,9 @@
/** check if the server is alive */ /** check if the server is alive */
function serverHealthCheck(options) { function serverHealthCheck(options) {
logger.debug("serverHealthCheck")
return $.ajax({ return $.ajax({
type: "GET", type: "GET",
url: "/api/versioncheck" url: "/api/healthcheck"
}); });
} }

View File

@ -94,22 +94,30 @@
context.jamClient.OnDownloadAvailable(); context.jamClient.OnDownloadAvailable();
} }
/**
* This most likely occurs when multiple tabs in the same browser are open, until we make a fix for this...
*/
function duplicateClientError() {
context.JK.Banner.showAlert("Duplicate Window (Development Mode Only)", "You have logged in another window in this browser. This window will continue to work but with degraded functionality. This limitation will soon be fixed.")
context.JK.JamServer.noReconnect = true;
}
function registerBadStateError() { function registerBadStateError() {
logger.debug("register for server_bad_state_error");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_ERROR, serverBadStateError); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_ERROR, serverBadStateError);
} }
function registerBadStateRecovered() { function registerBadStateRecovered() {
logger.debug("register for server_bad_state_recovered");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered);
} }
function registerDownloadAvailable() { function registerDownloadAvailable() {
logger.debug("register for download_available");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.DOWNLOAD_AVAILABLE, downloadAvailable); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.DOWNLOAD_AVAILABLE, downloadAvailable);
} }
function registerDuplicateClientError() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_DUPLICATE_CLIENT_ERROR, duplicateClientError);
}
/** /**
* Generic error handler for Ajax calls. * Generic error handler for Ajax calls.
*/ */
@ -330,6 +338,7 @@
registerBadStateRecovered(); registerBadStateRecovered();
registerBadStateError(); registerBadStateError();
registerDownloadAvailable(); registerDownloadAvailable();
registerDuplicateClientError();
context.JK.FaderHelpers.initialize(); context.JK.FaderHelpers.initialize();
context.window.onunload = this.unloadFunction; context.window.onunload = this.unloadFunction;

View File

@ -34,6 +34,7 @@
var $currentScore = null; var $currentScore = null;
var $scoredClients = null; var $scoredClients = null;
var $subscore = null; var $subscore = null;
var $watchVideo = null;
var backendGuardTimeout = null; var backendGuardTimeout = null;
var serverClientId = ''; var serverClientId = '';
@ -43,6 +44,12 @@
var $self = $(this); var $self = $(this);
var scoringZoneWidth = 100;//px var scoringZoneWidth = 100;//px
var inGearWizard = false; var inGearWizard = false;
var operatingSystem = null;
// these try to make it such that we only pass a NetworkTest Pass/Failed one time in a new user flow
var trackedPass = false;
var lastNetworkFailure = null;
var bandwidthSamples = [];
var NETWORK_TEST_START = 'network_test.start'; var NETWORK_TEST_START = 'network_test.start';
var NETWORK_TEST_DONE = 'network_test.done'; var NETWORK_TEST_DONE = 'network_test.done';
@ -66,7 +73,27 @@
} }
} }
// this averages bandwidthSamples; this method is meant just for GA data
function avgBandwidth(num_others) {
if(bandwidthSamples.length == 0) {
return 0;
}
else {
var total = 0;
context._.each(bandwidthSamples, function(sample) {
total += (sample * num_others * 400 * (100 + 28)); // sample is a percentage of 400. So sample * 400 gives us how many packets/sec. 100 is payload; 28 is UDP+ETHERNET overhead, to give us bandwidth
})
return total / bandwidthSamples.length;
}
}
function reset() { function reset() {
trackedPass = false;
lastNetworkFailure = null;
resetTestState();
}
function resetTestState() {
serverClientId = ''; serverClientId = '';
scoring = false; scoring = false;
numClientsToTest = STARTING_NUM_CLIENTS; numClientsToTest = STARTING_NUM_CLIENTS;
@ -77,6 +104,8 @@
$testText.empty(); $testText.empty();
$subscore.empty(); $subscore.empty();
$currentScore.width(0); $currentScore.width(0);
bandwidthSamples = [];
} }
function renderStartTest() { function renderStartTest() {
@ -107,6 +136,17 @@
return ''; return '';
} }
} }
function getLastNetworkFailure() {
return lastNetworkFailure;
}
function storeLastNetworkFailure(reason, data) {
if(!trackedPass) {
lastNetworkFailure = {reason:reason, data:data};
}
}
function testFinished() { function testFinished() {
var attempt = getCurrentAttempt(); var attempt = getCurrentAttempt();
@ -123,7 +163,14 @@
if(!testSummary.final.num_clients) { if(!testSummary.final.num_clients) {
testSummary.final.num_clients = attempt.num_clients; testSummary.final.num_clients = attempt.num_clients;
} }
context.JK.GA.trackNetworkTest(context.JK.detectOS(), testSummary.final.num_clients);
// context.jamClient.GetNetworkTestScore() == 0 is a rough approximation if the user has passed the FTUE before
if(inGearWizard || context.jamClient.GetNetworkTestScore() == 0) {
trackedPass = true;
lastNetworkFailure = null;
context.JK.GA.trackNetworkTest(context.JK.detectOS(), testSummary.final.num_clients);
}
context.jamClient.SetNetworkTestScore(attempt.num_clients); context.jamClient.SetNetworkTestScore(attempt.num_clients);
if(testSummary.final.num_clients == 2) { if(testSummary.final.num_clients == 2) {
$testResults.addClass('acceptable'); $testResults.addClass('acceptable');
@ -136,49 +183,65 @@
else if(reason == "minimum_client_threshold") { else if(reason == "minimum_client_threshold") {
context.jamClient.SetNetworkTestScore(0); 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.") renderStopTest('', "We're sorry, but your router and Internet service will not effectively support JamKazam sessions. Please click the HELP button for more information.")
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.bandwidth, avgBandwidth(attempt.num_clients - 1));
} }
else if(reason == "unreachable") { else if(reason == "unreachable" || reason == "no-transmit") {
context.jamClient.SetNetworkTestScore(0); 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."); renderStopTest('', "We're sorry, but your router will not support JamKazam in its current configuration. Please click the HELP button for more information.");
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.stun, attempt.num_clients);
} }
else if(reason == "internal_error") { else if(reason == "internal_error") {
context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection."); context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection.");
renderStopTest('', ''); renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
} }
else if(reason == "remote_peer_cant_test") { else if(reason == "remote_peer_cant_test") {
context.JK.alertSupportedNeeded("The JamKazam service is experiencing technical difficulties."); context.JK.alertSupportedNeeded("The JamKazam service is experiencing technical difficulties.");
renderStopTest('', ''); renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
}
else if(reason == "server_comm_timeout") {
context.JK.alertSupportedNeeded("Communication with the JamKazam network service has timed out." + appendContextualStatement());
renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
} }
else if(reason == 'backend_gone') { else if(reason == 'backend_gone') {
context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties."); context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties.");
renderStopTest('', ''); renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
} }
else if(reason == "invalid_response") { 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 + '.'); context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection.<br/><br/>Reason: " + attempt.backend_data.reason + '.');
renderStopTest('', ''); renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
} }
else if(reason == 'no_servers') { else if(reason == 'no_servers') {
context.JK.alertSupportedNeeded("No network test servers are available." + appendContextualStatement()); context.JK.alertSupportedNeeded("No network test servers are available." + appendContextualStatement());
renderStopTest('', ''); renderStopTest('', '');
testedSuccessfully = true; testedSuccessfully = true;
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
} }
else if(reason == 'no_network') { else if(reason == 'no_network') {
context.JK.Banner.showAlert("Please try again later. Your network appears down."); context.JK.Banner.showAlert("Please try again later. Your network appears down.");
renderStopTest('', ''); renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.noNetwork);
} }
else if(reason == "rest_api_error") { else if(reason == "rest_api_error") {
context.JK.alertSupportedNeeded("Unable to acquire a network test server." + appendContextualStatement()); context.JK.alertSupportedNeeded("Unable to acquire a network test server." + appendContextualStatement());
testedSuccessfully = true; testedSuccessfully = true;
renderStopTest('', ''); renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
} }
else if(reason == "timeout") { else if(reason == "timeout") {
context.JK.alertSupportedNeeded("Communication with a network test servers timed out." + appendContextualStatement()); context.JK.alertSupportedNeeded("Communication with the JamKazam network service timed out." + appendContextualStatement());
testedSuccessfully = true; testedSuccessfully = true;
renderStopTest('', ''); renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
} }
else { else {
context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection."); context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection.");
renderStopTest('', ''); renderStopTest('', '');
storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror);
} }
numClientsToTest = STARTING_NUM_CLIENTS; numClientsToTest = STARTING_NUM_CLIENTS;
@ -204,8 +267,9 @@
} }
function cancel() { function cancel() {
clearBackendGuard();
} }
function clearBackendGuard() { function clearBackendGuard() {
if(backendGuardTimeout) { if(backendGuardTimeout) {
clearTimeout(backendGuardTimeout); clearTimeout(backendGuardTimeout);
@ -213,6 +277,11 @@
} }
} }
function setBackendGuard() {
clearBackendGuard();
backendGuardTimeout = setTimeout(function(){backendTimedOut()}, (gon.ftue_network_test_duration + 1) * 1000);
}
function attemptTestPass() { function attemptTestPass() {
var attempt = {}; var attempt = {};
@ -230,7 +299,7 @@
updateProgress(0, false); updateProgress(0, false);
backendGuardTimeout = setTimeout(function(){backendTimedOut()}, (gon.ftue_network_test_duration + 1) * 1000); setBackendGuard();
context.jamClient.TestNetworkPktBwRate(serverClientId, createSuccessCallbackName(), createTimeoutCallbackName(), context.jamClient.TestNetworkPktBwRate(serverClientId, createSuccessCallbackName(), createTimeoutCallbackName(),
NETWORK_TEST_TYPES.PktTest400LowLatency, NETWORK_TEST_TYPES.PktTest400LowLatency,
@ -262,7 +331,7 @@
} }
} }
reset(); resetTestState();
scoring = true; scoring = true;
$self.triggerHandler(NETWORK_TEST_START); $self.triggerHandler(NETWORK_TEST_START);
renderStartTest(); renderStartTest();
@ -300,6 +369,7 @@
} }
function updateProgress(throughput, showSubscore) { function updateProgress(throughput, showSubscore) {
var width = throughput * 100; var width = throughput * 100;
$currentScore.stop().data('showSubscore', showSubscore); $currentScore.stop().data('showSubscore', showSubscore);
@ -322,9 +392,7 @@
; ;
} }
function networkTestSuccess(data) { function networkTestComplete(data) {
clearBackendGuard();
var attempt = getCurrentAttempt(); var attempt = getCurrentAttempt();
function refineTest(up) { function refineTest(up) {
@ -364,6 +432,8 @@
if(data.progress === true) { if(data.progress === true) {
setBackendGuard();
var animate = true; var animate = true;
if(data.downthroughput && data.upthroughput) { if(data.downthroughput && data.upthroughput) {
@ -376,12 +446,14 @@
// take the lower // take the lower
var throughput= data.downthroughput < data.upthroughput ? data.downthroughput : data.upthroughput; var throughput= data.downthroughput < data.upthroughput ? data.downthroughput : data.upthroughput;
bandwidthSamples.push(data.upthroughput);
updateProgress(throughput, true); updateProgress(throughput, true);
} }
} }
} }
else { else {
clearBackendGuard();
logger.debug("network test pass success. data: ", data); logger.debug("network test pass success. data: ", data);
if(data.reason == "unreachable") { if(data.reason == "unreachable") {
@ -390,6 +462,11 @@
attempt.reason = data.reason; attempt.reason = data.reason;
testFinished(); testFinished();
} }
else if(data.reason == "no-transmit") {
logger.debug("network test: no-transmit (STUN issue or similar)");
attempt.reason = data.reason;
testFinished();
}
else if(data.reason == "internal_error") { else if(data.reason == "internal_error") {
// oops // oops
logger.debug("network test: internal_error (client had a unexpected problem)"); logger.debug("network test: internal_error (client had a unexpected problem)");
@ -402,6 +479,11 @@
attempt.reason = data.reason; attempt.reason = data.reason;
testFinished(); testFinished();
} }
else if(data.reason == "server_comm_timeout") {
logger.debug("network test: server_comm_timeout (communication with server problem)")
attempt.reason = data.reason;
testFinished();
}
else { else {
if(!data.downthroughput || !data.upthroughput) { if(!data.downthroughput || !data.upthroughput) {
// we have to assume this is bad. just not a reason we know about in code // we have to assume this is bad. just not a reason we know about in code
@ -467,9 +549,17 @@
} }
function beforeHide() { function beforeHide() {
clearBackendGuard();
} }
function initializeVideoWatchButton() {
if(operatingSystem == "Win32") {
$watchVideo.attr('href', 'https://www.youtube.com/watch?v=rhAdCVuwhBc');
}
else {
$watchVideo.attr('href', 'https://www.youtube.com/watch?v=0r1py0AYJ4Y');
}
}
function initialize(_$step, _inGearWizard) { function initialize(_$step, _inGearWizard) {
$step = _$step; $step = _$step;
inGearWizard = _inGearWizard; inGearWizard = _inGearWizard;
@ -487,15 +577,19 @@
$currentScore = $step.find('.current-score'); $currentScore = $step.find('.current-score');
$scoredClients= $step.find('.scored-clients'); $scoredClients= $step.find('.scored-clients');
$subscore = $step.find('.subscore'); $subscore = $step.find('.subscore');
$watchVideo = $step.find('.watch-video');
$startNetworkTestBtn.on('click', startNetworkTest); $startNetworkTestBtn.on('click', startNetworkTest);
operatingSystem = context.JK.GetOSAsString();
initializeVideoWatchButton();
// if this network test is instantiated anywhere else than the gearWizard, or a dialog, then this will have to be expanded // if this network test is instantiated anywhere else than the gearWizard, or a dialog, then this will have to be expanded
if(inGearWizard) { if(inGearWizard) {
context.JK.HandleNetworkTestSuccessForGearWizard = function(data) { networkTestSuccess(data)}; // pin to global for bridge callback context.JK.HandleNetworkTestSuccessForGearWizard = function(data) { networkTestComplete(data)}; // pin to global for bridge callback
context.JK.HandleNetworkTestTimeoutForGearWizard = function(data) { networkTestTimeout(data)}; // pin to global for bridge callback context.JK.HandleNetworkTestTimeoutForGearWizard = function(data) { networkTestTimeout(data)}; // pin to global for bridge callback
} }
else { else {
context.JK.HandleNetworkTestSuccessForDialog = function(data) { networkTestSuccess(data)}; // pin to global for bridge callback context.JK.HandleNetworkTestSuccessForDialog = function(data) { networkTestComplete(data)}; // pin to global for bridge callback
context.JK.HandleNetworkTestTimeoutForDialog = function(data) { networkTestTimeout(data)}; // pin to global for bridge callback context.JK.HandleNetworkTestTimeoutForDialog = function(data) { networkTestTimeout(data)}; // pin to global for bridge callback
} }
} }
@ -505,6 +599,7 @@
this.initialize = initialize; this.initialize = initialize;
this.reset = reset; this.reset = reset;
this.cancel = cancel; this.cancel = cancel;
this.getLastNetworkFailure = getLastNetworkFailure;
this.NETWORK_TEST_START = NETWORK_TEST_START; this.NETWORK_TEST_START = NETWORK_TEST_START;
this.NETWORK_TEST_DONE = NETWORK_TEST_DONE; this.NETWORK_TEST_DONE = NETWORK_TEST_DONE;

View File

@ -30,7 +30,6 @@
var claimedRecording = null; var claimedRecording = null;
var playbackControls = null; var playbackControls = null;
var promptLeave = false; var promptLeave = false;
var backendMixerAlertThrottleTimer = null;
var rateSessionDialog = null; var rateSessionDialog = null;
var rest = context.JK.Rest(); var rest = context.JK.Rest();
@ -76,60 +75,6 @@
"PeerMediaTrackGroup": 10 "PeerMediaTrackGroup": 10
}; };
// recreate eThresholdType enum from MixerDialog.h
var alert_type = {
0: {"title": "", "message": ""}, // NO_EVENT,
1: {"title": "", "message": ""}, // BACKEND_ERROR: generic error - eg P2P message error
2: {"title": "", "message": ""}, // BACKEND_MIXER_CHANGE, - event that controls have been regenerated
3: {"title": "High Packet Jitter", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // PACKET_JTR,
4: {"title": "High Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // PACKET_LOSS
5: {"title": "High Packet Late", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // PACKET_LATE,
6: {"title": "Large Jitter Queue", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // JTR_QUEUE_DEPTH,
7: {"title": "High Network Jitter", "message": "Your network connection is currently experiencing network jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // NETWORK_JTR,
8: {"title": "High Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // NETWORK_PING,
9: {"title": "Bandwidth Throttled", "message": "The available bandwidth on your network has diminished, and this may impact your audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // BITRATE_THROTTLE_WARN,
10:{"title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low, and this may impact your audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // BANDWIDTH_LOW
// IO related events
11:{"title": "Variable Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // INPUT_IO_RATE
12:{"title": "High Input Jitter", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // INPUT_IO_JTR,
13:{"title": "Variable Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // OUTPUT_IO_RATE
14:{"title": "High Output Jitter", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // OUTPUT_IO_JTR,
// CPU load related
15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>." }, // CPU_LOAD
16: {"title": "Decode Violations", "message": ""}, // DECODE_VIOLATIONS,
17: {"title": "", "message": ""}, // LAST_THRESHOLD
18: {"title": "Wifi Alert", "message": ""}, // WIFI_NETWORK_ALERT, //user or peer is using wifi
19: {"title": "No Audio Configuration", "message": "You cannot join the session because you do not have a valid audio configuration."}, // NO_VALID_AUDIO_CONFIG,
20: {"title": "Audio Device Not Present", "message": ""}, // AUDIO_DEVICE_NOT_PRESENT, // the audio device is not connected
21: {"title": "", "message": ""}, // RECORD_PLAYBACK_STATE, // record/playback events have occurred
22: {"title": "", "message": ""}, // RUN_UPDATE_CHECK_BACKGROUND, //this is auto check - do
23: {"title": "", "message": ""}, // RUN_UPDATE_CHECK_INTERACTIVE, //this is initiated by user
24: {"title": "", "message": ""}, // STUN_EVENT, // system completed stun test... come get the result
25: {"title": "No Audio", "message": "Your system is no longer transmitting audio. Other session members are unable to hear you."}, // DEAD_USER_WARN_EVENT, //the backend is not receiving audio from this peer
26: {"title": "No Audio", "message": "Your system is no longer transmitting audio. Other session members are unable to hear you."}, // DEAD_USER_REMOVE_EVENT, //the backend is removing the user from session as no audio is coming from this peer
27: {"title": "", "message": ""}, // WINDOW_CLOSE_BACKGROUND_MODE, //the user has closed the window and the client is now in background mode
28: {"title": "", "message": ""}, // WINDOW_OPEN_FOREGROUND_MODE, //the user has opened the window and the client is now in forground mode/
29: {"title": "Failed to Broadcast", "message": ""}, // SESSION_LIVEBROADCAST_FAIL, //error of some sort - so can't broadcast
30: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_ACTIVE, //active
31: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_STOPPED, //stopped by server/user
32: {"title": "Client Pinned", "message": "This client will be the source of a broadcast."}, // SESSION_LIVEBROADCAST_PINNED, //node pinned by user
33: {"title": "Client No Longer Pinned", "message": "This client is no longer designated as the source of the broadcast."}, // SESSION_LIVEBROADCAST_UNPINNED, //node unpinned by user
34: {"title": "", "message": ""}, // BACKEND_STATUS_MSG, //status/informational message
35: {"title": "LAN Unpredictable", "message": "Your local network is adding considerable variance to transmit times. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // LOCAL_NETWORK_VARIANCE_HIGH,//the ping time via a hairpin for the user network is unnaturally high or variable.
//indicates problem with user computer stack or network itself (wifi, antivirus etc)
36: {"title": "LAN High Latency", "message": "Your local network is adding considerable latency. For troubleshooting tips, <a href='https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems'>click here</a>."}, // LOCAL_NETWORK_LATENCY_HIGH,
37: {"title": "", "message": ""}, // RECORDING_CLOSE, //update and remove tracks from front-end
38: {"title": "No Audio Sent", "message": ""}, // PEER_REPORTS_NO_AUDIO_RECV, //update and remove tracks from front-end
39: {"title": "", "message": ""} // LAST_ALERT
};
function beforeShow(data) { function beforeShow(data) {
sessionId = data.id; sessionId = data.id;
promptLeave = true; promptLeave = true;
@ -144,114 +89,6 @@
return { freezeInteraction: true }; return { freezeInteraction: true };
} }
function alertCallback(type, text) {
function timeCallback() {
var start = new Date();
setTimeout(function() {
var timed = new Date().getTime() - start.getTime();
if(timed > 250) {
logger.warn("SLOW AlERT_CALLBACK. type: %o text: %o time: %o", type, text, timed);
}
}, 1);
}
timeCallback();
logger.debug("alert callback", type, text);
if (type === 2) { // BACKEND_MIXER_CHANGE
logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);
if(sessionModel.id() && text == "RebuildAudioIoControl") {
// the backend will send these events rapid-fire back to back.
// the server can still perform correctly, but it is nicer to wait 100 ms to let them all fall through
if(backendMixerAlertThrottleTimer) {clearTimeout(backendMixerAlertThrottleTimer);}
backendMixerAlertThrottleTimer = setTimeout(function() {
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.id = sessionModel.id();
rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
})
.fail(function() {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
})
}, 100);
}
else if(sessionModel.id() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {
sessionModel.refreshCurrentSession(true);
}
}
else if (type === 19) { // NO_VALID_AUDIO_CONFIG
app.notify({
"title": alert_type[type].title,
"text": text,
"icon_url": "/assets/content/icon_alert_big.png"
});
context.location = "/client#"; // leaveSession will be called in beforeHide below
}
else if (type === 24) { // STUN_EVENT
var testResults = context.jamClient.NetworkTestResult();
$.each(testResults, function(index, val) {
if (val.bStunFailed) {
// if true we could not reach a stun server
}
else if (val.bRemoteUdpBocked) {
// if true the user cannot communicate with peer via UDP, although they could do LAN based session
}
});
}
else if (type === 26) {
var clientId = text;
var participant = sessionModel.getParticipant(clientId);
if(participant) {
app.notify({
"title": alert_type[type].title,
"text": participant.user.name + " is no longer sending audio.",
"icon_url": context.JK.resolveAvatarUrl(participant.user.photo_url)
});
var $track = $('div.track[client-id="' + clientId + '"]');
$('.disabled-track-overlay', $track).show();
}
}
else if (type === 27) { // WINDOW_CLOSE_BACKGROUND_MODE
// the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen
promptLeave = false;
context.location = '/client#/home'
}
else if(type != 30 && type != 31 && type != 21){ // these are handled elsewhere
context.setTimeout(function() {
var alert = alert_type[type];
if(alert && alert.title) {
app.notify({
"title": alert_type[type].title,
"text": text,
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unknown Backend Event type %o, data %o", type, text)
}
}, 1);
}
}
function initializeSession() { function initializeSession() {
// indicate that the screen is active, so that // indicate that the screen is active, so that
// body-scoped drag handlers can go active // body-scoped drag handlers can go active
@ -327,7 +164,8 @@
context.JK.CurrentSessionModel = sessionModel = new context.JK.SessionModel( context.JK.CurrentSessionModel = sessionModel = new context.JK.SessionModel(
context.JK.app, context.JK.app,
context.JK.JamServer, context.JK.JamServer,
context.jamClient context.jamClient,
self
); );
$(sessionModel.recordingModel) $(sessionModel.recordingModel)
@ -653,7 +491,8 @@
} }
}); });
var faderId = mixerIds.join(','); var faderId = mixerIds.join(',');
$('#volume').attr('mixer-id', faderId); var $volume = $('#volume');
$volume.attr('mixer-id', faderId);
var faderOpts = { var faderOpts = {
faderId: faderId, faderId: faderId,
faderType: "horizontal", faderType: "horizontal",
@ -664,8 +503,9 @@
"height": "24px" "height": "24px"
} }
}; };
context.JK.FaderHelpers.renderFader("#volume", faderOpts); context.JK.FaderHelpers.renderFader($volume, faderOpts);
context.JK.FaderHelpers.subscribe(faderId, faderChanged);
$volume.on('fader_change', faderChanged);
// Visually update fader to underlying mixer start value. // Visually update fader to underlying mixer start value.
// Always do this, even if gainPercent is zero. // Always do this, even if gainPercent is zero.
@ -699,8 +539,8 @@
"height": "24px" "height": "24px"
} }
}; };
context.JK.FaderHelpers.renderFader(faderId, faderOpts); context.JK.FaderHelpers.renderFader($mixSlider, faderOpts);
context.JK.FaderHelpers.subscribe(faderId, l2mChanged); $mixSlider.on('fader_change', l2mChanged);
var value = context.jamClient.SessionGetMasterLocalMix(); var value = context.jamClient.SessionGetMasterLocalMix();
context.JK.FaderHelpers.setFaderValue(faderId, percentFromMixerValue(-80, 20, value)); context.JK.FaderHelpers.setFaderValue(faderId, percentFromMixerValue(-80, 20, value));
@ -709,9 +549,9 @@
/** /**
* This has a specialized jamClient call, so custom handler. * This has a specialized jamClient call, so custom handler.
*/ */
function l2mChanged(faderId, newValue, dragging) { function l2mChanged(e, data) {
//var dbValue = context.JK.FaderHelpers.convertLinearToDb(newValue); //var dbValue = context.JK.FaderHelpers.convertLinearToDb(newValue);
context.jamClient.SessionSetMasterLocalMix(newValue - 80); context.jamClient.SessionSetMasterLocalMix(data.percentage - 80);
} }
function _addVoiceChat() { function _addVoiceChat() {
@ -723,7 +563,8 @@
var $voiceChat = $('#voice-chat'); var $voiceChat = $('#voice-chat');
$voiceChat.show(); $voiceChat.show();
$voiceChat.attr('mixer-id', mixer.id); $voiceChat.attr('mixer-id', mixer.id);
$('#voice-chat .voicechat-mute').attr('mixer-id', mixer.id); var $voiceChatGain = $voiceChat.find('.voicechat-gain');
var $voiceChatMute = $voiceChat.find('.voicechat-mute').attr('mixer-id', mixer.id);
var gainPercent = percentFromMixerValue( var gainPercent = percentFromMixerValue(
mixer.range_low, mixer.range_high, mixer.volume_left); mixer.range_low, mixer.range_high, mixer.volume_left);
var faderOpts = { var faderOpts = {
@ -731,12 +572,11 @@
faderType: "horizontal", faderType: "horizontal",
width: 50 width: 50
}; };
context.JK.FaderHelpers.renderFader("#voice-chat .voicechat-gain", faderOpts); context.JK.FaderHelpers.renderFader($voiceChatGain, faderOpts);
context.JK.FaderHelpers.subscribe(mixer.id, faderChanged); $voiceChatGain.on('fader_change', faderChanged);
context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent); context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent);
if (mixer.mute) { if (mixer.mute) {
var $mute = $voiceChat.find('.voicechat-mute'); _toggleVisualMuteControl($voiceChatMute, true);
_toggleVisualMuteControl($mute, true);
} }
} }
}); });
@ -945,16 +785,17 @@
var vuLeftSelector = trackSelector + " .track-vu-left"; var vuLeftSelector = trackSelector + " .track-vu-left";
var vuRightSelector = trackSelector + " .track-vu-right"; var vuRightSelector = trackSelector + " .track-vu-right";
var faderSelector = trackSelector + " .track-gain"; var faderSelector = trackSelector + " .track-gain";
var $fader = $(faderSelector).attr('mixer-id', mixerId);
var $track = $(trackSelector); var $track = $(trackSelector);
// Set mixer-id attributes and render VU/Fader // Set mixer-id attributes and render VU/Fader
context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts); context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts);
$track.find('.track-vu-left').attr('mixer-id', mixerId + '_vul'); $track.find('.track-vu-left').attr('mixer-id', mixerId + '_vul');
context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts); context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts);
$track.find('.track-vu-right').attr('mixer-id', mixerId + '_vur'); $track.find('.track-vu-right').attr('mixer-id', mixerId + '_vur');
context.JK.FaderHelpers.renderFader(faderSelector, faderOpts); context.JK.FaderHelpers.renderFader($fader, faderOpts);
// Set gain position // Set gain position
context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent); context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent);
context.JK.FaderHelpers.subscribe(mixerId, faderChanged); $fader.on('fader_change', faderChanged);
} }
// Function called on an interval when participants change. Mixers seem to // Function called on an interval when participants change. Mixers seem to
@ -976,6 +817,8 @@
], ],
usedMixers); usedMixers);
if (mixer) { if (mixer) {
var participant = (sessionModel.getParticipant(clientId) || {name:'unknown'}).name;
logger.debug("found mixer=" + mixer.id + ", participant=" + participant)
usedMixers[mixer.id] = true; usedMixers[mixer.id] = true;
keysToDelete.push(key); keysToDelete.push(key);
var gainPercent = percentFromMixerValue( var gainPercent = percentFromMixerValue(
@ -998,6 +841,8 @@
$('.disabled-track-overlay', $track).show(); $('.disabled-track-overlay', $track).show();
$('.track-connection', $track).removeClass('red yellow green').addClass('red'); $('.track-connection', $track).removeClass('red yellow green').addClass('red');
} }
var participant = (sessionModel.getParticipant(clientId) || {name:'unknown'}).name;
logger.debug("still looking for mixer for participant=" + participant + ", clientId=" + clientId)
} }
} }
@ -1029,14 +874,14 @@
if (!(mixer.stereo)) { // mono track if (!(mixer.stereo)) { // mono track
if (mixerId.substr(-4) === "_vul") { if (mixerId.substr(-4) === "_vul") {
// Do the left // Do the left
selector = '#tracks [mixer-id="' + pureMixerId + '_vul"]'; selector = $('#tracks [mixer-id="' + pureMixerId + '_vul"]');
context.JK.VuHelpers.updateVU(selector, value); context.JK.VuHelpers.updateVU(selector, value);
// Do the right // Do the right
selector = '#tracks [mixer-id="' + pureMixerId + '_vur"]'; selector = $('#tracks [mixer-id="' + pureMixerId + '_vur"]');
context.JK.VuHelpers.updateVU(selector, value); context.JK.VuHelpers.updateVU(selector, value);
} // otherwise, it's a mono track, _vur event - ignore. } // otherwise, it's a mono track, _vur event - ignore.
} else { // stereo track } else { // stereo track
selector = '#tracks [mixer-id="' + mixerId + '"]'; selector = $('#tracks [mixer-id="' + mixerId + '"]');
context.JK.VuHelpers.updateVU(selector, value); context.JK.VuHelpers.updateVU(selector, value);
} }
} }
@ -1102,12 +947,14 @@
* Will be called when fader changes. The fader id (provided at subscribe time), * Will be called when fader changes. The fader id (provided at subscribe time),
* the new value (0-100) and whether the fader is still being dragged are passed. * the new value (0-100) and whether the fader is still being dragged are passed.
*/ */
function faderChanged(faderId, newValue, dragging) { function faderChanged(e, data) {
var $target = $(this);
var faderId = $target.attr('mixer-id');
var mixerIds = faderId.split(','); var mixerIds = faderId.split(',');
$.each(mixerIds, function(i,v) { $.each(mixerIds, function(i,v) {
var broadcast = !(dragging); // If fader is still dragging, don't broadcast var broadcast = !(data.dragging); // If fader is still dragging, don't broadcast
fillTrackVolumeObject(v, broadcast); fillTrackVolumeObject(v, broadcast);
setMixerVolume(v, newValue); setMixerVolume(v, data.percentage);
}); });
} }
@ -1568,7 +1415,6 @@
context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback; context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback;
context.JK.HandleBridgeCallback = handleBridgeCallback; context.JK.HandleBridgeCallback = handleBridgeCallback;
context.JK.AlertCallback = alertCallback;
}; };
})(window,jQuery); })(window,jQuery);

View File

@ -7,7 +7,10 @@
context.JK = context.JK || {}; context.JK = context.JK || {};
var logger = context.JK.logger; var logger = context.JK.logger;
context.JK.SessionModel = function(app, server, client) { // screen can be null
context.JK.SessionModel = function(app, server, client, sessionScreen) {
var ALERT_TYPES = context.JK.ALERT_TYPES;
var clientId = client.clientID; var clientId = client.clientID;
var currentSessionId = null; // Set on join, prior to setting currentSession. var currentSessionId = null; // Set on join, prior to setting currentSession.
var currentSession = null; var currentSession = null;
@ -19,6 +22,7 @@
var pendingSessionRefresh = false; var pendingSessionRefresh = false;
var recordingModel = new context.JK.RecordingModel(app, this, rest, context.jamClient); var recordingModel = new context.JK.RecordingModel(app, this, rest, context.jamClient);
var currentTrackChanges = 0; var currentTrackChanges = 0;
var backendMixerAlertThrottleTimer = null;
// we track all the clientIDs of all the participants ever seen by this session, so that we can reliably convert a clientId from the backend into a username/avatar // we track all the clientIDs of all the participants ever seen by this session, so that we can reliably convert a clientId from the backend into a username/avatar
var participantsEverSeen = {}; var participantsEverSeen = {};
var $self = $(this); var $self = $(this);
@ -29,6 +33,10 @@
return currentSession ? currentSession.id : null; return currentSession ? currentSession.id : null;
} }
function inSession() {
return !!currentSessionId;
}
function participants() { function participants() {
if (currentSession) { if (currentSession) {
return currentSession.participants; return currentSession.participants;
@ -131,7 +139,7 @@
// 'unregister' for callbacks // 'unregister' for callbacks
context.jamClient.SessionRegisterCallback(""); context.jamClient.SessionRegisterCallback("");
context.jamClient.SessionSetAlertCallback(""); //context.jamClient.SessionSetAlertCallback("");
context.jamClient.SessionSetConnectionStatusRefreshRate(0); context.jamClient.SessionSetConnectionStatusRefreshRate(0);
updateCurrentSession(null); updateCurrentSession(null);
$(document).trigger('jamkazam.session_stopped', {session: {id: currentSessionId}}); $(document).trigger('jamkazam.session_stopped', {session: {id: currentSessionId}});
@ -214,6 +222,12 @@
* the provided callback when complete. * the provided callback when complete.
*/ */
function refreshCurrentSessionRest(callback, force) { function refreshCurrentSessionRest(callback, force) {
if(!inSession()) {
logger.debug("refreshCurrentSession skipped: ")
return;
}
var url = "/api/sessions/" + currentSessionId; var url = "/api/sessions/" + currentSessionId;
if(requestingSessionRefresh) { if(requestingSessionRefresh) {
// if someone asks for a refresh while one is going on, we ask for another to queue up // if someone asks for a refresh while one is going on, we ask for another to queue up
@ -239,7 +253,14 @@
logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter); logger.info("ignoring refresh because we already have current: " + currentTrackChanges + ", seen: " + response.track_changes_counter);
} }
}, },
error: function(jqXHR) { app.notifyServerError(jqXHR, "Unable to refresh session data") }, error: function(jqXHR) {
if(jqXHR.status != 404) {
app.notifyServerError(jqXHR, "Unable to refresh session data")
}
else {
logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone")
}
},
complete: function() { complete: function() {
requestingSessionRefresh = false; requestingSessionRefresh = false;
if(pendingSessionRefresh) { if(pendingSessionRefresh) {
@ -411,6 +432,73 @@
return $.Deferred().reject().promise(); return $.Deferred().reject().promise();
} }
function onDeadUserRemove(type, text) {
if(!inSession()) return;
var clientId = text;
var participant = participantsEverSeen[clientId];
if(participant) {
app.notify({
"title": ALERT_TYPES[type].title,
"text": participant.user.name + " is no longer sending audio.",
"icon_url": context.JK.resolveAvatarUrl(participant.user.photo_url)
});
var $track = $('div.track[client-id="' + clientId + '"]');
$('.disabled-track-overlay', $track).show();
}
}
function onWindowBackgrounded(type, text) {
if(!inSession()) return;
// the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen
if(sessionScreen) {
sessionScreen.setPromptLeave(false);
context.location = '/client#/home'
}
}
function onBackendMixerChanged(type, text) {
logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);
if(inSession() && text == "RebuildAudioIoControl") {
// the backend will send these events rapid-fire back to back.
// the server can still perform correctly, but it is nicer to wait 100 ms to let them all fall through
if(backendMixerAlertThrottleTimer) {clearTimeout(backendMixerAlertThrottleTimer);}
backendMixerAlertThrottleTimer = setTimeout(function() {
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.id = id();
rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
})
.fail(function(jqXHR) {
if(jqXHR.status != 404) {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unable to sync local tracks because session is gone.")
}
})
}, 100);
}
else if(inSession() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) {
refreshCurrentSession(true);
}
}
// Public interface // Public interface
this.id = id; this.id = id;
this.recordedTracks = recordedTracks; this.recordedTracks = recordedTracks;
@ -425,6 +513,12 @@
this.onWebsocketDisconnected = onWebsocketDisconnected; this.onWebsocketDisconnected = onWebsocketDisconnected;
this.recordingModel = recordingModel; this.recordingModel = recordingModel;
this.findUserBy = findUserBy; this.findUserBy = findUserBy;
// ALERT HANDLERS
this.onBackendMixerChanged = onBackendMixerChanged;
this.onDeadUserRemove = onDeadUserRemove;
this.onWindowBackgrounded = onWindowBackgrounded;
this.getCurrentSession = function() { this.getCurrentSession = function() {
return currentSession; return currentSession;
}; };

View File

@ -9,6 +9,9 @@
var logger = context.JK.logger; var logger = context.JK.logger;
var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR; var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;
var ALERT_NAMES = context.JK.ALERT_NAMES;
var ALERT_TYPES = context.JK.ALERT_TYPES;
var days = new Array("Sun", "Mon", "Tue", var days = new Array("Sun", "Mon", "Tue",
"Wed", "Thu", "Fri", "Sat"); "Wed", "Thu", "Fri", "Sat");
@ -448,6 +451,20 @@
$('.dialog-overlay').hide(); $('.dialog-overlay').hide();
} }
// usage: context.JK.onBackendEvent(ALERT_NAMES.SOME_EVENT)
context.JK.onBackendEvent = function(type, namespace, callback) {
var alertData = ALERT_TYPES[type];
if(!alertData) {throw "onBackendEvent: unknown alert type " + type};
logger.debug("onBackendEvent: " + alertData.name + '.' + namespace)
$(document).on(alertData.name + '.' + namespace, callback);
}
context.JK.offBackendEvent = function(type, namespace, callback) {
var alertData = ALERT_TYPES[type];
if(!alertData) {throw "offBackendEvent: unknown alert type " + type};
logger.debug("offBackendEvent: " + alertData.name + '.' + namespace, alertData)
$(document).off(alertData.name + '.' + namespace);
}
/* /*
* Loads a listbox or dropdown with the values in input_array, setting the option value * Loads a listbox or dropdown with the values in input_array, setting the option value
* to the id_field and the option text to text_field. It will preselect the option with * to the id_field and the option text to text_field. It will preselect the option with
@ -529,8 +546,8 @@
return ret; return ret;
} }
context.JK.alertSupportedNeeded = function(additionalContext) { context.JK.alertSupportedNeeded = function(additionalContext) {
var $item = context.JK.Banner.showAlert(additionalContext + ' Please <a href="http://jamkazam.desk.com" rel="external">contact support</a>'); var $item = context.JK.Banner.showAlert(additionalContext + '<br/><br/>Please <a href="http://jamkazam.desk.com" rel="external">contact support</a>.');
context.JK.popExternalLinks($item); context.JK.popExternalLinks($item);
return $item; return $item;
} }

View File

@ -5,6 +5,7 @@
context.JK = context.JK || {}; context.JK = context.JK || {};
context.JK.VoiceChatHelper = function (app) { context.JK.VoiceChatHelper = function (app) {
var logger = context.JK.logger; var logger = context.JK.logger;
var ALERT_NAMES = context.JK.ALERT_NAMES;
var ASSIGNMENT = context.JK.ASSIGNMENT; var ASSIGNMENT = context.JK.ASSIGNMENT;
var VOICE_CHAT = context.JK.VOICE_CHAT; var VOICE_CHAT = context.JK.VOICE_CHAT;
var MAX_TRACKS = context.JK.MAX_TRACKS; var MAX_TRACKS = context.JK.MAX_TRACKS;
@ -18,10 +19,19 @@
var $chatInputs = null; var $chatInputs = null;
var $templateChatInput = null; var $templateChatInput = null;
var $selectedChatInput = null;// should only be used if isChatEnabled = true 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 var $voiceChatVuLeft = null;
var $voiceChatVuRight = null;
var $voiceChatFader = null;
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
var uniqueCallbackName = null;
// needed because iCheck fires iChecked event even when you programmatically change it, unlike when using .val(x) // needed because iCheck fires iChecked event even when you programmatically change it, unlike when using .val(x)
var ignoreICheckEvent = false; var ignoreICheckEvent = false;
var lastSavedTime = new Date();
var vuOptions = null;
var faderHeight = null;
var startingState = null;
var resettedOnInvalidDevice = false; // this is set to true when we clear chat, and set to false when the user interacts in anyway
function defaultReuse() { function defaultReuse() {
suppressChange(function(){ suppressChange(function(){
@ -34,6 +44,48 @@
return $useChatInputRadio.is(':checked'); return $useChatInputRadio.is(':checked');
} }
function onInvalidAudioDevice(e, data) {
logger.debug("voice_chat_helper: onInvalidAudioDevice")
if(resettedOnInvalidDevice) {
// we've already tried to clear the audio device, and the user hasn't interacted, but still we are getting this event
// we can't keep taking action, so stop
logger.error("voice_chat_helper: onInvalidAudioDevice: ignoring event because we have already tried to handle it");
return;
}
resettedOnInvalidDevice = true;
$selectedChatInput = null;
// you can't do this in the event callback; it hangs the app indefinitely, and somehow 'sticks' the mic input into bad state until reboot
setTimeout(function() {
context.jamClient.FTUEClearChatInput();
context.jamClient.TrackSetChatEnable(true);
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
context.JK.Banner.showAlert('It appears the selected chat input is not functioning. Please try another chat input.');
}
else {
context.JK.alertSupportedNeeded("Unable to unwind invalid chat input selection.")
}
}, 1);
}
function beforeShow() {
userInteracted();
renderNoVolume();
context.JK.onBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'voice_chat_helper', onInvalidAudioDevice);
registerVuCallbacks();
}
function beforeHide() {
context.JK.offBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'voice_chat_helper', onInvalidAudioDevice);
jamClient.FTUERegisterVUCallbacks('', '', '');
}
function userInteracted() {
resettedOnInvalidDevice = false;
}
function reset(forceDisabledChat) { function reset(forceDisabledChat) {
$selectedChatInput = null; $selectedChatInput = null;
@ -62,24 +114,36 @@
$selectedChatInput = $chatInput; $selectedChatInput = $chatInput;
$selectedChatInput.attr('checked', 'checked'); $selectedChatInput.attr('checked', 'checked');
} }
$chat.hide(); // we'll show it once it's styled with iCheck //$chat.hide(); // we'll show it once it's styled with iCheck
$chatInputs.append($chat); $chatInputs.append($chat);
}); });
var $radioButtons = $chatInputs.find('input[name="chat-device"]'); var $radioButtons = $chatInputs.find('input[name="chat-device"]');
context.JK.checkbox($radioButtons).on('ifChecked', function(e) { context.JK.checkbox($radioButtons).on('ifChecked', function(e) {
userInteracted();
var $input = $(e.currentTarget); var $input = $(e.currentTarget);
$selectedChatInput = $input; // for use in handleNext $selectedChatInput = $input; // for use in handleNext
if(saveImmediate) { if(saveImmediate) {
var channelId = $input.attr('data-channel-id'); var channelId = $input.attr('data-channel-id');
context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT); lastSavedTime = new Date();
context.jamClient.TrackSetChatInput(channelId);
lastSavedTime = new Date();
//context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT);
var result = context.jamClient.TrackSaveAssignments(); var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) { if(!result || result.length == 0) {
// success // success
} }
else { else {
context.JK.Banner.showAlert('Unable to save assignments. ' + result); context.jamClient.FTUEClearChatInput();
context.jamClient.TrackSetChatEnable(true);
var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) {
context.JK.Banner.showAlert('Unable to save chat selection. ' + result);
}
else {
context.JK.alertSupportedNeeded("Unable to unwind invalid chat selection.")
}
return false; return false;
} }
} }
@ -89,18 +153,17 @@
disableChatButtonsUI(); disableChatButtonsUI();
} }
$chatInputs.find('.chat-input').show().on('click', function() { renderVolumes();
// obnoxious; remove soon XXX
// if(!isChatEnabled()) { startingState = getCurrentState();
// context.JK.prodBubble($parent.find('.use-chat-input h3'), 'chat-not-enabled', {}, { positions:['left']});
// }
})
} }
function disableChatButtonsUI() { function disableChatButtonsUI() {
var $radioButtons = $chatInputs.find('input[name="chat-device"]'); var $radioButtons = $chatInputs.find('input[name="chat-device"]');
$radioButtons.iCheck('disable') $radioButtons.iCheck('disable')
$chatInputs.addClass('disabled'); $chatInputs.addClass('disabled');
$radioButtons.iCheck('uncheck');
$selectedChatInput = null;
} }
function enableChatButtonsUI() { function enableChatButtonsUI() {
@ -133,6 +196,7 @@
var result = context.jamClient.TrackSaveAssignments(); var result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0) { if(!result || result.length == 0) {
renderNoVolume();
// success // success
suppressChange(function() { suppressChange(function() {
$reuseAudioInputRadio.iCheck('check').attr('checked', 'checked'); $reuseAudioInputRadio.iCheck('check').attr('checked', 'checked');
@ -151,7 +215,7 @@
$useChatInputRadio.removeAttr('checked'); $useChatInputRadio.removeAttr('checked');
}) })
} }
disableChatButtonsUI() disableChatButtonsUI();
} }
function enableChat(applyToBackend) { function enableChat(applyToBackend) {
@ -181,6 +245,7 @@
} }
enableChatButtonsUI(); enableChatButtonsUI();
renderVolumes();
} }
function handleChatEnabledToggle() { function handleChatEnabledToggle() {
@ -191,8 +256,18 @@
$reuseAudioInputRadio.closest('.iradio_minimal').css('position', 'absolute'); $reuseAudioInputRadio.closest('.iradio_minimal').css('position', 'absolute');
$useChatInputRadio.closest('.iradio_minimal').css('position', 'absolute'); $useChatInputRadio.closest('.iradio_minimal').css('position', 'absolute');
$reuseAudioInputRadio.on('ifChecked', function() { if(!ignoreICheckEvent) disableChat(true) }); $reuseAudioInputRadio.on('ifChecked', function() {
$useChatInputRadio.on('ifChecked', function() { if(!ignoreICheckEvent) enableChat(true) }); if(!ignoreICheckEvent) {
userInteracted();
disableChat(true);
}
});
$useChatInputRadio.on('ifChecked', function() {
if(!ignoreICheckEvent) {
userInteracted();
enableChat(true)
}
});
} }
// gets the state of the UI // gets the state of the UI
@ -208,9 +283,17 @@
return state; return state;
} }
function trySave() { function cancel() {
logger.debug("canceling voice chat state");
return trySave(startingState);
}
var state = getCurrentState();
function trySave(state) {
if(!state) {
state = getCurrentState();
}
if(state.enabled && state.chat_channel) { if(state.enabled && state.chat_channel) {
logger.debug("enabling chat. chat_channel=" + state.chat_channel); logger.debug("enabling chat. chat_channel=" + state.chat_channel);
@ -231,6 +314,9 @@
if(!result || result.length == 0) { if(!result || result.length == 0) {
// success // success
if(!state.enabled) {
renderNoVolume();
}
return true; return true;
} }
else { else {
@ -239,21 +325,80 @@
} }
} }
function initialize(_$step, _saveImmediate) { function initializeVUMeters() {
context.JK.VuHelpers.renderVU($voiceChatVuLeft, vuOptions);
context.JK.VuHelpers.renderVU($voiceChatVuRight, vuOptions);
context.JK.FaderHelpers.renderFader($voiceChatFader, {faderId: '', faderType: "vertical", height: faderHeight});
$voiceChatFader.on('fader_change', faderChange);
}
// renders volumes based on what the backend says
function renderVolumes() {
var $fader = $voiceChatFader.find('[control="fader"]');
var db = context.jamClient.FTUEGetChatInputVolume();
var faderPct = db + 80;
context.JK.FaderHelpers.setHandlePosition($fader, faderPct);
}
function renderNoVolume() {
var $fader = $voiceChatFader.find('[control="fader"]');
context.JK.FaderHelpers.setHandlePosition($fader, 50);
context.JK.VuHelpers.updateVU($voiceChatVuLeft, 0);
context.JK.VuHelpers.updateVU($voiceChatVuRight, 0);
}
function faderChange(e, data) {
// TODO - using hardcoded range of -80 to 20 for output levels.
var mixerLevel = data.percentage - 80; // Convert our [0-100] to [-80 - +20] range
context.jamClient.FTUESetChatInputVolume(mixerLevel);
}
function registerVuCallbacks() {
logger.debug("voice-chat-helper: registering vu callbacks");
jamClient.FTUERegisterVUCallbacks(
"JK.voiceChatHelperAudioOutputVUCallback",
"JK.voiceChatHelperAudioInputVUCallback",
"JK." + uniqueCallbackName
);
jamClient.SetVURefreshRate(200);
}
function initialize(_$step, caller, _saveImmediate, _vuOptions, _faderHeight) {
$parent = _$step; $parent = _$step;
saveImmediate = _saveImmediate; saveImmediate = _saveImmediate;
vuOptions = _vuOptions;
faderHeight = _faderHeight;
$reuseAudioInputRadio = $parent.find('.reuse-audio-input input'); $reuseAudioInputRadio = $parent.find('.reuse-audio-input input');
$useChatInputRadio = $parent.find('.use-chat-input input'); $useChatInputRadio = $parent.find('.use-chat-input input');
$chatInputs = $parent.find('.chat-inputs'); $chatInputs = $parent.find('.chat-inputs');
$templateChatInput = $('#template-chat-input'); $templateChatInput = $('#template-chat-input');
$voiceChatVuLeft = $parent.find('.voice-chat-vu-left');
$voiceChatVuRight = $parent.find('.voice-chat-vu-right');
$voiceChatFader = $parent.find('.chat-fader')
handleChatEnabledToggle(); handleChatEnabledToggle();
initializeVUMeters();
renderVolumes();
uniqueCallbackName = 'voiceChatHelperChatInputVUCallback' + caller;
context.JK[uniqueCallbackName] = function(dbValue) {
context.JK.ftueVUCallback(dbValue, $voiceChatVuLeft);
context.JK.ftueVUCallback(dbValue, $voiceChatVuRight);
}
} }
context.JK.voiceChatHelperAudioInputVUCallback = function (dbValue) {};
context.JK.voiceChatHelperAudioOutputVUCallback = function (dbValue) {};
this.reset = reset; this.reset = reset;
this.trySave = trySave; this.trySave = trySave;
this.cancel = cancel;
this.initialize = initialize; this.initialize = initialize;
this.beforeShow = beforeShow;
this.beforeHide = beforeHide;
return this; return this;
}; };

View File

@ -19,6 +19,7 @@
* vuType can be either "horizontal" or "vertical" * vuType can be either "horizontal" or "vertical"
*/ */
renderVU: function(selector, userOptions) { renderVU: function(selector, userOptions) {
selector = $(selector);
/** /**
* The default options for rendering a VU * The default options for rendering a VU
*/ */
@ -35,25 +36,25 @@
templateSelector = "#template-vu-h"; templateSelector = "#template-vu-h";
} }
var templateSource = $(templateSelector).html(); var templateSource = $(templateSelector).html();
$(selector).empty(); selector.empty();
$(selector).html(context._.template(templateSource, options, {variable: 'data'})); selector.html(context._.template(templateSource, options, {variable: 'data'}));
}, },
/** /**
* Given a selector representing a container for a VU meter and * Given a selector representing a container for a VU meter and
* a value between 0.0 and 1.0, light the appropriate lights. * a value between 0.0 and 1.0, light the appropriate lights.
*/ */
updateVU: function (selector, value) { updateVU: function ($selector, value) {
// There are 13 VU lights. Figure out how many to // There are 13 VU lights. Figure out how many to
// light based on the incoming value. // light based on the incoming value.
var countSelector = 'tr'; var countSelector = 'tr';
var horizontal = ($('table.horizontal', selector).length); var horizontal = ($selector.find('table.horizontal').length);
if (horizontal) { if (horizontal) {
countSelector = 'td'; countSelector = 'td';
} }
var lightCount = $(countSelector, selector).length; var lightCount = $selector.find(countSelector).length;
var i = 0; var i = 0;
var state = 'on'; var state = 'on';
var lights = Math.round(value * lightCount); var lights = Math.round(value * lightCount);
@ -61,15 +62,15 @@
var $light = null; var $light = null;
var colorClass = 'vu-green-'; var colorClass = 'vu-green-';
var lightSelectorPrefix = selector + ' td.vu'; var lightSelectorPrefix = $selector.find('td.vu');
var thisLightSelector = null; var thisLightSelector = null;
// Remove all light classes from all lights // Remove all light classes from all lights
var allLightsSelector = selector + ' td.vulight'; var allLightsSelector = $selector.find('td.vulight');
$(allLightsSelector).removeClass('vu-green-off vu-green-on vu-red-off vu-red-on'); $(allLightsSelector).removeClass('vu-green-off vu-green-on vu-red-off vu-red-on');
// Set the lights // Set the lights
for (i=0; i<lightCount; i++) { for (i = 0; i < lightCount; i++) {
colorClass = 'vu-green-'; colorClass = 'vu-green-';
state = 'on'; state = 'on';
if (i >= redSwitch) { if (i >= redSwitch) {
@ -78,9 +79,9 @@
if (i >= lights) { if (i >= lights) {
state = 'off'; state = 'off';
} }
thisLightSelector = lightSelectorPrefix + i;
$light = $(thisLightSelector); var lightIndex = horizontal ? i : lightCount - i - 1;
$light.addClass(colorClass + state); allLightsSelector.eq(lightIndex).addClass(colorClass + state);
} }
} }

View File

@ -105,7 +105,30 @@
} }
function reportFailureAnalytics() {
// on cancel, see if the user is leaving without having passed the audio test; if so, report it
var lastAnalytics = stepSelectGear.getLastAudioTestFailAnalytics();
if(lastAnalytics) {
logger.debug("reporting audio test failed")
context.JK.GA.trackAudioTestFailure(lastAnalytics.platform, lastAnalytics.reason, lastAnalytics.data);
}
else {
logger.debug("audiotest failure: nothing to report");
}
var lastAnalytics = stepNetworkTest.getLastNetworkFailAnalytics();
if(lastAnalytics) {
logger.debug("reporting network test failed");
context.JK.GA.trackNetworkTestFailure(lastAnalytics.reason, lastAnalytics.data);
}
else {
logger.debug("network test failure: nothing to report");
}
}
function onCanceled() { function onCanceled() {
reportFailureAnalytics();
if (app.cancelFtue) { if (app.cancelFtue) {
app.cancelFtue(); app.cancelFtue();
app.afterFtue = null; app.afterFtue = null;
@ -116,6 +139,8 @@
} }
function onClosed() { function onClosed() {
reportFailureAnalytics();
if (app.afterFtue) { if (app.afterFtue) {
// If there's a function to invoke, invoke it. // If there's a function to invoke, invoke it.
app.afterFtue(); app.afterFtue();

View File

@ -28,10 +28,14 @@
var forceDisabledChat = firstTimeShown; var forceDisabledChat = firstTimeShown;
voiceChatHelper.reset(forceDisabledChat); voiceChatHelper.reset(forceDisabledChat);
voiceChatHelper.beforeShow();
firstTimeShown = false; firstTimeShown = false;
} }
function beforeHide() {
voiceChatHelper.beforeHide();
}
function handleNext() { function handleNext() {
return true; return true;
} }
@ -39,13 +43,14 @@
function initialize(_$step) { function initialize(_$step) {
$step = _$step; $step = _$step;
voiceChatHelper.initialize($step, true); voiceChatHelper.initialize($step, 'configure_voice_gear_wizard', true, {vuType: "vertical", lightCount: 8, lightWidth: 3, lightHeight: 10}, 101);
} }
this.handleNext = handleNext; this.handleNext = handleNext;
this.newSession = newSession; this.newSession = newSession;
this.beforeShow = beforeShow; this.beforeShow = beforeShow;
this.beforeHide = beforeHide;
this.initialize = initialize; this.initialize = initialize;
return this; return this;

View File

@ -5,6 +5,7 @@
context.JK = context.JK || {}; context.JK = context.JK || {};
context.JK.StepDirectMonitoring = function (app) { context.JK.StepDirectMonitoring = function (app) {
var logger = context.JK.logger;
var $step = null; var $step = null;
var $directMonitoringBtn = null; var $directMonitoringBtn = null;
var isPlaying = false; var isPlaying = false;
@ -58,6 +59,7 @@
function beforeShow() { function beforeShow() {
context.jamClient.SessionRemoveAllPlayTracks(); context.jamClient.SessionRemoveAllPlayTracks();
logger.debug("adding test sound");
if(!context.jamClient.SessionAddPlayTrack("skin:jktest-audio.wav")) { if(!context.jamClient.SessionAddPlayTrack("skin:jktest-audio.wav")) {
context.JK.alertSupportedNeeded('Unable to open test sound'); context.JK.alertSupportedNeeded('Unable to open test sound');
} }
@ -68,6 +70,7 @@
stopPlay(); stopPlay();
} }
logger.debug("removing test sound")
context.jamClient.SessionRemoveAllPlayTracks(); context.jamClient.SessionRemoveAllPlayTracks();
if(playCheckInterval) { if(playCheckInterval) {

View File

@ -11,6 +11,10 @@
var $step = null; var $step = null;
function getLastNetworkFailAnalytics() {
return networkTest.getLastNetworkFailure();
}
function networkTestDone() { function networkTestDone() {
updateButtons(); updateButtons();
} }
@ -62,6 +66,7 @@
this.beforeHide = beforeHide; this.beforeHide = beforeHide;
this.beforeShow = beforeShow; this.beforeShow = beforeShow;
this.initialize = initialize; this.initialize = initialize;
this.getLastNetworkFailAnalytics = getLastNetworkFailAnalytics;
return this; return this;
} }

View File

@ -5,6 +5,7 @@
context.JK = context.JK || {}; context.JK = context.JK || {};
context.JK.StepSelectGear = function (app, dialog) { context.JK.StepSelectGear = function (app, dialog) {
var ALERT_NAMES = context.JK.ALERT_NAMES;
var EVENTS = context.JK.EVENTS; var EVENTS = context.JK.EVENTS;
var ASSIGNMENT = context.JK.ASSIGNMENT; var ASSIGNMENT = context.JK.ASSIGNMENT;
var VOICE_CHAT = context.JK.VOICE_CHAT; var VOICE_CHAT = context.JK.VOICE_CHAT;
@ -19,6 +20,10 @@
var gearTest = new context.JK.GearTest(app); var gearTest = new context.JK.GearTest(app);
var loopbackShowing = false; var loopbackShowing = false;
// the goal of lastFailureAnalytics and trackedPass are to send only a single AudioTest 'Pass' or 'Failed' event, per FTUE wizard open/close
var lastFailureAnalytics = {};
var trackedPass = false;
var $watchVideoInput = null; var $watchVideoInput = null;
var $watchVideoOutput = null; var $watchVideoOutput = null;
var $audioInput = null; var $audioInput = null;
@ -135,17 +140,11 @@
if(!audioInputDeviceId || audioInputDeviceId == '') { if(!audioInputDeviceId || audioInputDeviceId == '') {
context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'select-input', {}, {positions:['right', 'top']}); context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'select-input', {}, {positions:['right', 'top']});
} }
//context.JK.Banner.showAlert('To be a valid input audio device, the device must have at least 1 input channel.'); else {
return false; // this path should be impossible because we filter output devices with 0 inputs from the input device dropdown
} // but we might flip that, so it's nice to leave this in to catch us later
context.JK.Banner.showAlert('To be a valid input audio device, the device must have at least 1 input port.');
var $allOutputs = $outputChannels.find('input[type="checkbox"]');
if ($allOutputs.length < 2) {
if(!audioOutputDeviceId || audioOutputDeviceId == '') {
context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'select-output', {}, {positions:['right', 'top'], duration:7000});
} }
// ERROR: not enough channels
//context.JK.Banner.showAlert('To be a valid output audio device, the device must have at least 2 output channels.');
return false; return false;
} }
@ -166,6 +165,20 @@
} }
} }
var $allOutputs = $outputChannels.find('input[type="checkbox"]');
if ($allOutputs.length < 2) {
if(!audioOutputDeviceId || audioOutputDeviceId == '') {
context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'select-output', {}, {positions:['right', 'top'], duration:7000});
}
else {
// this path indicates that the user has deliberately chosen a device, so we need to tell them that this device does not work with JamKazam
context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'select-output', {}, {positions:['right', 'top'], duration:7000});
}
return false;
}
// ensure 2 outputs are selected // ensure 2 outputs are selected
var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked'); var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked');
var $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)'); var $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)');
@ -435,10 +448,19 @@
function initializeASIOButtons() { function initializeASIOButtons() {
$asioInputControlBtn.unbind('click').click(function () { $asioInputControlBtn.unbind('click').click(function () {
if(gearTest.isScoring()) {
return false;
}
context.jamClient.FTUEOpenControlPanel(selectedAudioInput()); context.jamClient.FTUEOpenControlPanel(selectedAudioInput());
return false;
}); });
$asioOutputControlBtn.unbind('click').click(function () { $asioOutputControlBtn.unbind('click').click(function () {
if(gearTest.isScoring()) {
return false;
}
context.jamClient.FTUEOpenControlPanel(selectedAudioOutput()); context.jamClient.FTUEOpenControlPanel(selectedAudioOutput());
return false;
}); });
} }
@ -715,7 +737,7 @@
// * reuse IO score if it was good/acceptable // * reuse IO score if it was good/acceptable
// * rescore IO if it was bad or skipped from previous try // * rescore IO if it was bad or skipped from previous try
function attemptScore(refocused) { function attemptScore(refocused) {
gearTest.attemptScore(refocused); gearTest.attemptScore(selectedDeviceInfo, refocused);
} }
function initializeAudioInputChanged() { function initializeAudioInputChanged() {
@ -735,9 +757,42 @@
gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true); gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);
} }
function getLastAudioTestFailAnalytics() {
return lastFailureAnalytics;
}
function storeLastFailureForAnalytics(platform, reason, data) {
if(!trackedPass) {
lastFailureAnalytics = {
platform: platform,
reason: reason,
data: data
}
}
}
function onGearTestFail(e, data) { function onGearTestFail(e, data) {
renderScoringStopped(); renderScoringStopped();
gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true); gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true);
if(data.reason == "latency") {
storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.latency, data.latencyScore);
}
else if(data.reason = "io") {
if(data.ioTarget == 'bad') {
storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.ioTarget, data.ioTargetScore);
}
else {
storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.ioVariance, data.ioVarianceScore);
}
}
else {
logger.error("unknown reason in onGearTestFail: " + data.reason)
}
}
function onGearTestInvalidated(e, data) {
initializeNextButtonState();
} }
function handleNext() { function handleNext() {
@ -775,8 +830,10 @@
return false; return false;
} }
else { else {
savedProfile = true;
context.JK.GA.trackAudioTestCompletion(context.JK.detectOS()); context.JK.GA.trackAudioTestCompletion(context.JK.detectOS());
trackedPass = true;
lastFailureAnalytics = null;
savedProfile = true;
return true; return true;
} }
} }
@ -800,14 +857,21 @@
initializeResync(); initializeResync();
} }
function beforeWizardShow() {
lastFailureAnalytics = null;
trackedPass = false;
}
function beforeShow() { function beforeShow() {
$(window).on('focus', onFocus); $(window).on('focus', onFocus);
initializeNextButtonState(); initializeNextButtonState();
context.JK.onBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'gear_test', gearTest.onInvalidAudioDevice);
} }
function beforeHide() { function beforeHide() {
logger.debug("unregistering focus watch") logger.debug("unregistering focus watch")
$(window).off('focus', onFocus); $(window).off('focus', onFocus);
context.JK.offBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'gear_test', gearTest.onInvalidAudioDevice);
} }
function resetState() { function resetState() {
@ -849,12 +913,15 @@
.on(gearTest.GEAR_TEST_START, onGearTestStarted) .on(gearTest.GEAR_TEST_START, onGearTestStarted)
.on(gearTest.GEAR_TEST_DONE, onGearTestDone) .on(gearTest.GEAR_TEST_DONE, onGearTestDone)
.on(gearTest.GEAR_TEST_FAIL, onGearTestFail) .on(gearTest.GEAR_TEST_FAIL, onGearTestFail)
.on(gearTest.GEAR_TEST_INVALIDATED_ASYNC, onGearTestInvalidated)
} }
this.getLastAudioTestFailAnalytics = getLastAudioTestFailAnalytics;
this.handleNext = handleNext; this.handleNext = handleNext;
this.newSession = newSession; this.newSession = newSession;
this.beforeShow = beforeShow; this.beforeShow = beforeShow;
this.beforeHide = beforeHide; this.beforeHide = beforeHide;
this.beforeWizardShow = beforeWizardShow;
this.initialize = initialize; this.initialize = initialize;
self = this; self = this;

View File

@ -14,6 +14,11 @@
var validIOScore = false; var validIOScore = false;
var latencyScore = null; var latencyScore = null;
var ioScore = null; var ioScore = null;
var lastSavedTime = new Date();
// this should be marked TRUE when the backend sends an invalid_audio_device alert
var asynchronousInvalidDevice = false;
var selectedDeviceInfo = null;
var $scoreReport = null; var $scoreReport = null;
var $ioHeader = null; var $ioHeader = null;
@ -31,6 +36,7 @@
var $ioScoreSection = null; var $ioScoreSection = null;
var $latencyScoreSection = null; var $latencyScoreSection = null;
var $self = $(this); var $self = $(this);
var GEAR_TEST_START = "gear_test.start"; var GEAR_TEST_START = "gear_test.start";
@ -41,9 +47,10 @@
var GEAR_TEST_DONE = "gear_test.done"; var GEAR_TEST_DONE = "gear_test.done";
var GEAR_TEST_FAIL = "gear_test.fail"; var GEAR_TEST_FAIL = "gear_test.fail";
var GEAR_TEST_IO_PROGRESS = "gear_test.io_progress"; var GEAR_TEST_IO_PROGRESS = "gear_test.io_progress";
var GEAR_TEST_INVALIDATED_ASYNC = "gear_test.async_invalidated"; // happens when backend alerts us device is invalid
function isGoodFtue() { function isGoodFtue() {
return validLatencyScore && validIOScore; return validLatencyScore && validIOScore && !asynchronousInvalidDevice;
} }
function processIOScore(io) { function processIOScore(io) {
@ -93,13 +100,15 @@
$self.triggerHandler(GEAR_TEST_DONE) $self.triggerHandler(GEAR_TEST_DONE)
} }
else { else {
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'io'}); $self.triggerHandler(GEAR_TEST_FAIL, {reason:'io', ioTarget: medianIOClass, ioTargetScore: median, ioVariance: stdIOClass, ioVarianceScore: std});
} }
} }
function automaticScore() { function automaticScore() {
logger.debug("automaticScore: calling FTUESave(false)"); logger.debug("automaticScore: calling FTUESave(false)");
lastSavedTime = new Date(); // save before and after FTUESave, because the event happens in a multithreaded way
var result = jamClient.FTUESave(false); var result = jamClient.FTUESave(false);
lastSavedTime = new Date();
if(result && result != "") { if(result && result != "") {
logger.debug("unable to FTUESave(false). reason=" + result); logger.debug("unable to FTUESave(false). reason=" + result);
return false; return false;
@ -125,12 +134,14 @@
// on refocus=true: // on refocus=true:
// * reuse IO score if it was good/acceptable // * reuse IO score if it was good/acceptable
// * rescore IO if it was bad or skipped from previous try // * rescore IO if it was bad or skipped from previous try
function attemptScore(refocused) { function attemptScore(_selectedDeviceInfo, refocused) {
if(scoring) { if(scoring) {
logger.debug("gear-test: already scoring"); logger.debug("gear-test: already scoring");
return; return;
} }
selectedDeviceInfo = _selectedDeviceInfo;
scoring = true; scoring = true;
asynchronousInvalidDevice = false;
$self.triggerHandler(GEAR_TEST_START); $self.triggerHandler(GEAR_TEST_START);
$self.triggerHandler(GEAR_TEST_LATENCY_START); $self.triggerHandler(GEAR_TEST_LATENCY_START);
validLatencyScore = false; validLatencyScore = false;
@ -197,7 +208,7 @@
} }
else { else {
scoring = false; scoring = false;
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency'}) $self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', latencyScore: latencyScore.latency})
} }
}) })
}, 250); }, 250);
@ -276,6 +287,18 @@
return ioScore; return ioScore;
} }
function getLastSavedTime() {
return lastSavedTime;
}
function onInvalidAudioDevice() {
logger.debug("gear_test: onInvalidAudioDevice")
asynchronousInvalidDevice = true;
$self.triggerHandler(GEAR_TEST_INVALIDATED_ASYNC);
context.JK.Banner.showAlert('Invalid Audio Device', 'It appears this audio device is not currently connected. Attach the device to your computer and restart the application, or select a different device.')
}
function showLoopbackDone() { function showLoopbackDone() {
$loopbackCompleted.show(); $loopbackCompleted.show();
} }
@ -301,6 +324,7 @@
function invalidateScore() { function invalidateScore() {
validLatencyScore = false; validLatencyScore = false;
validIOScore = false; validIOScore = false;
asynchronousInvalidDevice = false;
resetScoreReport(); resetScoreReport();
} }
@ -337,6 +361,18 @@
$ioCountdownSecs.text(secondsLeft); $ioCountdownSecs.text(secondsLeft);
} }
function uniqueDeviceName() {
try {
return selectedDeviceInfo.input.info.displayName + '(' + selectedDeviceInfo.input.behavior.shortName + ')' + '-' +
selectedDeviceInfo.output.info.displayName + '(' + selectedDeviceInfo.output.behavior.shortName + ')' + '-' +
context.JK.GetOSAsString();
}
catch(e){
logger.error("unable to devise unique device name for stats: " + e.toString());
return "Unknown";
}
}
function handleUI($testResults) { function handleUI($testResults) {
if(!$testResults.is('.ftue-box.results')) { if(!$testResults.is('.ftue-box.results')) {
@ -394,6 +430,9 @@
function onGearTestDone(e, data) { function onGearTestDone(e, data) {
$resultsText.attr('scored', 'complete'); $resultsText.attr('scored', 'complete');
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.pass, latencyScore);
rest.userCertifiedGear({success: true, client_id: app.clientId, audio_latency: getLatencyScore()}); rest.userCertifiedGear({success: true, client_id: app.clientId, audio_latency: getLatencyScore()});
} }
@ -405,6 +444,21 @@
} }
rest.userCertifiedGear({success: false}); rest.userCertifiedGear({success: false});
if(data.reason == "latency") {
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.latencyFail, data.latencyScore);
}
else if(data.reason = "io") {
if(data.ioTarget == 'bad') {
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.ioTargetFail, data.ioTargetScore);
}
else {
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.ioVarianceFail, data.ioVarianceScore);
}
}
else {
logger.error("unknown reason in onGearTestFail: " + data.reason)
}
} }
$self $self
@ -445,6 +499,7 @@
this.GEAR_TEST_DONE = GEAR_TEST_DONE; this.GEAR_TEST_DONE = GEAR_TEST_DONE;
this.GEAR_TEST_FAIL = GEAR_TEST_FAIL; this.GEAR_TEST_FAIL = GEAR_TEST_FAIL;
this.GEAR_TEST_IO_PROGRESS = GEAR_TEST_IO_PROGRESS; this.GEAR_TEST_IO_PROGRESS = GEAR_TEST_IO_PROGRESS;
this.GEAR_TEST_INVALIDATED_ASYNC = GEAR_TEST_INVALIDATED_ASYNC;
this.initialize = initialize; this.initialize = initialize;
this.isScoring = isScoring; this.isScoring = isScoring;
@ -457,6 +512,8 @@
this.isGoodFtue = isGoodFtue; this.isGoodFtue = isGoodFtue;
this.getLatencyScore = getLatencyScore; this.getLatencyScore = getLatencyScore;
this.getIOScore = getIOScore; this.getIOScore = getIOScore;
this.getLastSavedTime = getLastSavedTime;
this.onInvalidAudioDevice = onInvalidAudioDevice;
return this; return this;
} }

View File

@ -192,7 +192,6 @@
rest.createDiagnostic({ rest.createDiagnostic({
type: 'GEAR_SELECTION', type: 'GEAR_SELECTION',
data: { data: {
logs: logger.logCache,
client_type: context.JK.clientType(), client_type: context.JK.clientType(),
client_id: client_id:
context.JK.JamServer.clientID, context.JK.JamServer.clientID,
@ -249,7 +248,6 @@
} }
}) })
logger.debug("chatInputs:", chatInputs)
return chatInputs; return chatInputs;
} }

View File

@ -28,6 +28,12 @@
var $outputChannels = null; var $outputChannels = null;
var $templateAudioPort = null; var $templateAudioPort = null;
var $scoreReport = null; var $scoreReport = null;
var $audioInputVuLeft = null;
var $audioInputVuRight = null;
var $audioInputFader = null;
var $audioOutputVuLeft = null;
var $audioOutputVuRight = null;
var $audioOutputFader = null;
var faderMap = { var faderMap = {
'loopback-audio-input-fader': jamClient.FTUESetInputVolume, 'loopback-audio-input-fader': jamClient.FTUESetInputVolume,
@ -42,7 +48,7 @@
function attemptScore() { function attemptScore() {
gearTest.attemptScore(); gearTest.attemptScore(selectedDeviceInfo);
} }
@ -220,43 +226,45 @@
} }
function initializeVUMeters() { function initializeVUMeters() {
var vuMeters = [ var vuOptions = {vuType: "horizontal", lightCount: 12, lightWidth: 15, lightHeight: 3};
'#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); context.JK.VuHelpers.renderVU($audioInputVuLeft, vuOptions);
$.each(faders, function () { context.JK.VuHelpers.renderVU($audioInputVuRight, vuOptions);
var fid = this;
context.JK.FaderHelpers.renderFader('#' + fid, context.JK.VuHelpers.renderVU($audioOutputVuLeft, vuOptions);
{faderId: fid, faderType: "horizontal", width: 163}); context.JK.VuHelpers.renderVU($audioOutputVuRight, vuOptions);
context.JK.FaderHelpers.subscribe(fid, faderChange);
}); var faderOptions = {faderId: '', faderType: "horizontal", width: 163};
context.JK.FaderHelpers.renderFader($audioInputFader, faderOptions);
context.JK.FaderHelpers.renderFader($audioOutputFader, faderOptions);
$audioInputFader.on('fader_change', inputFaderChange);
$audioOutputFader.on('fader_change', outputFaderChange);
} }
// renders volumes based on what the backend says // renders volumes based on what the backend says
function renderVolumes() { 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](); // input
var faderPct = db + 80; var $inputFader = $audioInputFader.find('[control="fader"]');
context.JK.FaderHelpers.setHandlePosition($fader, faderPct); var db = context.jamClient.FTUEGetInputVolume();
}); var faderPct = db + 80;
context.JK.FaderHelpers.setHandlePosition($inputFader, faderPct);
// output
var $outputFader = $audioOutputFader.find('[control="fader"]');
var db = context.jamClient.FTUEGetOutputVolume();
var faderPct = db + 80;
context.JK.FaderHelpers.setHandlePosition($outputFader, faderPct);
} }
function faderChange(faderId, newValue, dragging) { function inputFaderChange(e, data) {
var setFunction = faderMap[faderId]; var mixerLevel = data.percentage - 80; // Convert our [0-100] to [-80 - +20] range
// TODO - using hardcoded range of -80 to 20 for output levels. context.jamClient.FTUESetInputVolume(mixerLevel);
var mixerLevel = newValue - 80; // Convert our [0-100] to [-80 - +20] range }
setFunction(mixerLevel);
function outputFaderChange(e, data) {
var mixerLevel = data.percentage - 80;
context.jamClient.FTUESetOutputVolume(mixerLevel);
} }
function registerVuCallbacks() { function registerVuCallbacks() {
@ -329,6 +337,13 @@
$outputChannels = $step.find('.output-ports') $outputChannels = $step.find('.output-ports')
$templateAudioPort = $('#template-audio-port'); $templateAudioPort = $('#template-audio-port');
$scoreReport = $step.find('.results'); $scoreReport = $step.find('.results');
$audioInputVuLeft = $step.find('.audio-input-vu-left');
$audioInputVuRight = $step.find('.audio-input-vu-right');
$audioInputFader= $step.find('.audio-input-fader');
$audioOutputVuLeft = $step.find('.audio-output-vu-left');
$audioOutputVuRight = $step.find('.audio-output-vu-right');
$audioOutputFader= $step.find('.audio-output-fader');
operatingSystem = context.JK.GetOSAsString(); operatingSystem = context.JK.GetOSAsString();
frameBuffers.initialize($step.find('.frame-and-buffers')); frameBuffers.initialize($step.find('.frame-and-buffers'));
@ -348,18 +363,19 @@
initializeASIOButtons(); initializeASIOButtons();
initializeResync(); initializeResync();
}
context.JK.loopbackAudioInputVUCallback = function (dbValue) {
context.JK.ftueVUCallback(dbValue, '#loopback-audio-input-vu-left'); context.JK.loopbackAudioInputVUCallback = function (dbValue) {
context.JK.ftueVUCallback(dbValue, '#loopback-audio-input-vu-right'); context.JK.ftueVUCallback(dbValue, $audioInputVuLeft);
}; context.JK.ftueVUCallback(dbValue, $audioInputVuRight);
context.JK.loopbackAudioOutputVUCallback = function (dbValue) { };
context.JK.ftueVUCallback(dbValue, '#loopback-audio-output-vu-left'); context.JK.loopbackAudioOutputVUCallback = function (dbValue) {
context.JK.ftueVUCallback(dbValue, '#loopback-audio-output-vu-right'); context.JK.ftueVUCallback(dbValue, $audioOutputVuLeft);
}; context.JK.ftueVUCallback(dbValue, $audioOutputVuRight);
context.JK.loopbackChatInputVUCallback = function (dbValue) { };
}; context.JK.loopbackChatInputVUCallback = function (dbValue) {
};
}
this.getGearTest = getGearTest; this.getGearTest = getGearTest;
this.handleNext = handleNext; this.handleNext = handleNext;

View File

@ -139,6 +139,13 @@
function onBeforeShow(args) { function onBeforeShow(args) {
context._.each(STEPS, function(step) {
// let every step know the wizard is being shown
if(step.beforeWizardShow) {
step.beforeWizardShow(args);
}
});
$currentWizardStep = null; $currentWizardStep = null;
previousStep = null; previousStep = null;

View File

@ -38,7 +38,7 @@
float:left; float:left;
vertical-align:top; vertical-align:top;
@include border_box_sizing; @include border_box_sizing;
padding: 20px 20px 0 0; padding: 0 20px 0 0;
} }
.sub-column { .sub-column {
@ -48,6 +48,25 @@
@include border_box_sizing; @include border_box_sizing;
} }
.ftue-box.chat-inputs {
height:224px;
width:90%;
@include border_box_sizing;
}
.vu-meter {
width:10%;
height:224px;
padding: 0 3px;
.ftue-controls {
height: 224px;
}
}
.tab {
padding-top:20px;
}
.tab[tab-id="music-audio"] { .tab[tab-id="music-audio"] {
.column { .column {
&:nth-of-type(1) { &:nth-of-type(1) {

View File

@ -27,10 +27,16 @@
} }
} }
.channels-holder {
.ftue-input {
background-position:4px 6px;
padding-left:14px;
}
}
.unassigned-input-channels { .unassigned-input-channels {
min-height: 22px; min-height: 240px;
overflow-y: auto; overflow-y: auto;
max-height:23%; max-height:93%;
//padding-right:18px; // to keep draggables off of scrollbar. maybe necessary //padding-right:18px; // to keep draggables off of scrollbar. maybe necessary
@ -39,6 +45,7 @@
} }
} }
.ftue-input { .ftue-input {
} }
&.drag-in-progress { &.drag-in-progress {
@ -84,6 +91,7 @@
font-size: 12px; font-size: 12px;
cursor: move; cursor: move;
padding: 4px; padding: 4px;
padding-left:10px;
border: solid 1px #999; border: solid 1px #999;
margin-bottom: 15px; margin-bottom: 15px;
white-space: nowrap; white-space: nowrap;
@ -91,6 +99,9 @@
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: left; text-align: left;
line-height:20px; line-height:20px;
background-image:url('/assets/content/icon_drag_handle.png');
background-position:0 3px;
background-repeat:no-repeat;
//direction: rtl; //direction: rtl;
&.ui-draggable-dragging { &.ui-draggable-dragging {
margin-bottom: 0; margin-bottom: 0;
@ -126,10 +137,13 @@
.ftue-input { .ftue-input {
padding: 0; padding: 0;
padding-left:10px;
border: 0; border: 0;
margin-bottom: 0; margin-bottom: 0;
&.ui-draggable-dragging { &.ui-draggable-dragging {
padding: 4px; padding: 4px;
padding-left:14px;
background-position:4px 6px;
border: solid 1px #999; border: solid 1px #999;
overflow: visible; overflow: visible;
} }
@ -163,7 +177,7 @@
}*/ }*/
} }
&:nth-of-type(2) { &:nth-of-type(2) {
padding-left:2px; padding-left:10px;
float: right; float: right;
} }
} }

View File

@ -9,8 +9,14 @@
display:inline; display:inline;
} }
.recording-controls { .recording-position {
position:relative; margin: 5px 0 0 -15px;
width: 95%;
display:inline-block;
}
.recording-current {
position:absolute;
} }
.icheckbuttons { .icheckbuttons {

View File

@ -40,6 +40,31 @@
top:85px; top:85px;
left:12px; left:12px;
} }
.recording-position {
display:inline-block;
width:80%;
font-family:Arial, Helvetica, sans-serif;
font-size:11px;
height:18px;
vertical-align:top;
margin-left:15px;
}
.recording-controls {
position: absolute;
bottom: 0;
height: 25px;
.play-button {
top:2px;
}
}
.playback-mode-buttons {
display:none;
}
} }
@ -509,18 +534,6 @@ table.vu td {
} }
} }
.recording-controls {
display:none;
.play-button {
outline:none;
}
.play-button img.pausebutton {
display:none;
}
}
#recording-finished-dialog .recording-controls { #recording-finished-dialog .recording-controls {
display:block; display:block;
} }
@ -624,16 +637,6 @@ table.vu td {
text-align:center; text-align:center;
} }
.recording-position {
display:inline-block;
width:80%;
font-family:Arial, Helvetica, sans-serif;
font-size:11px;
height:18px;
vertical-align:top;
}
.recording-time { .recording-time {
display:inline-block; display:inline-block;
height:16px; height:16px;
@ -672,6 +675,22 @@ table.vu td {
font-size:18px; font-size:18px;
} }
.recording-controls {
display:none;
.play-button {
outline:none;
}
.play-button img.pausebutton {
display:none;
}
}
.playback-mode-buttons {
display:none;
}
.currently-recording { .currently-recording {
background-color: $ColorRecordingBackground; background-color: $ColorRecordingBackground;
} }

View File

@ -1,8 +1,47 @@
@import "client/common.css.scss";
@charset "UTF-8";
.dialog.configure-tracks, .dialog.gear-wizard { .dialog.configure-tracks, .dialog.gear-wizard {
table.vu {
position:absolute;
}
.ftue-controls {
position:relative;
width: 55px;
background-color: #222;
float:right;
}
.ftue-vu-left {
position: relative;
left: 0px;
}
.ftue-vu-right {
position: relative;
left: 46px;
}
.ftue-fader {
//margin:5px 6px;
position: absolute;
top: 8px;
left: 14px;
}
.gain-label {
color: $ColorScreenPrimary;
position: absolute;
bottom: -1px;
left: 10px;
}
.voicechat-option { .voicechat-option {
position: relative; position: relative;
float:left;
width:50%;
height:109px;
div { div {
@ -33,14 +72,23 @@
} }
} }
.ftue-box { .vu-meter {
@include border_box_sizing;
float:left;
}
.ftue-box {
@include border_box_sizing;
background-color: #222222; background-color: #222222;
font-size: 13px; font-size: 13px;
padding: 8px; padding: 8px;
&.chat-inputs { &.chat-inputs {
height: 230px !important; //height: 230px !important;
overflow: auto; overflow: auto;
color:white; color:white;
float:left;
&.disabled { &.disabled {
color:gray; color:gray;
@ -50,6 +98,7 @@
display: inline-block; display: inline-block;
height: 32px; height: 32px;
vertical-align: middle; vertical-align: middle;
line-height:20px;
} }
.chat-input { .chat-input {
@ -60,5 +109,7 @@
} }
} }
} }
} }
} }

View File

@ -99,6 +99,7 @@
} }
ul.results-text { ul.results-text {
margin-left:-5px;
padding: 10px 8px; padding: 10px 8px;
li { li {

View File

@ -78,7 +78,7 @@
} }
&.instructions { &.instructions {
height: 268px !important; height: 248px !important;
line-height: 16px; line-height: 16px;
@include border_box_sizing; @include border_box_sizing;
@ -203,14 +203,40 @@
width: 25%; width: 25%;
&:nth-of-type(2) { &:nth-of-type(2) {
width: 50%; width: 75%;
} }
} }
.instructions {
height: 268px !important;
}
.watch-video { .watch-video {
margin-top: 25px; margin-top: 25px;
} }
.ftue-box.chat-inputs {
height:124px;
width:85%;
@include border_box_sizing;
}
.vu-meter {
width:15%;
padding: 0 3px;
.ftue-controls {
height: 124px;
}
.chat-fader {
top:4px !important;
}
}
.voice-chat-header {
margin-top:10px;
}
} }
.wizard-step[layout-wizard-step="4"] { .wizard-step[layout-wizard-step="4"] {

View File

@ -74,7 +74,7 @@
} }
.ftue-controls { .ftue-controls {
margin-top: 16px; margin-top: 10px;
position:relative; position:relative;
height: 48px; height: 48px;
width: 220px; width: 220px;
@ -101,11 +101,12 @@
} }
.ports-header { .ports-header {
margin-top:20px; margin-top:10px;
} }
.ports { .ports {
height:100px; height:100px;
overflow: auto;
} }
.asio-settings-input-btn, .asio-settings-output-btn { .asio-settings-input-btn, .asio-settings-output-btn {
@ -114,7 +115,7 @@
} }
.resync-btn { .resync-btn {
margin-top:35px; margin-top:29px;
} }
.run-test-btn { .run-test-btn {
@ -124,7 +125,7 @@
} }
.frame-and-buffers { .frame-and-buffers {
margin-top:10px; margin-top:4px;
} }
.test-results-header { .test-results-header {

View File

@ -15,6 +15,10 @@ class ArtifactsController < ApiController
render :json => result, :status => :ok render :json => result, :status => :ok
end end
def healthcheck
render :json => {}
end
def versioncheck def versioncheck

View File

@ -5,7 +5,7 @@
<!-- dialog header --> <!-- dialog header -->
<div class="content-head"> <div class="content-head">
<%= image_tag("content/icon_alert.png", :height => '24', :width => '24', :class => "content-icon") %><h1>alert</h1> <%= image_tag("content/icon_alert.png", :height => '24', :width => '24', :class => "content-icon") %><h1></h1>
</div> </div>

View File

@ -48,21 +48,26 @@
.tab{'tab-id' => 'voice-chat'} .tab{'tab-id' => 'voice-chat'}
.column %form.select-voice-chat-option.section.voice
%form.select-voice-chat-option.section .sub-header Select Voice Chat Option
.sub-header Select Voice Chat Option .voicechat-option.reuse-audio-input
.voicechat-option.reuse-audio-input %input{type:"radio", name: "voicechat", checked:"checked"}
%input{type:"radio", name: "voicechat", checked:"checked"} %h3 Use Music Microphone
%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
%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
.voicechat-option.use-chat-input %input{type:"radio", name: "voicechat"}
%input{type:"radio", name: "voicechat"} %h3 Use Chat Microphone
%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
%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 .clearall
.column .select-voice-chat
.select-voice-chat .sub-header Voice Chat Input
.sub-header Voice Chat Input .ftue-box.chat-inputs
.ftue-box.chat-inputs .vu-meter
.ftue-controls
.ftue-vu-left.voice-chat-vu-left
.ftue-fader.chat-fader
.gain-label GAIN
.ftue-vu-right.voice-chat-vu-right
.clearall .clearall
.buttons .buttons

View File

@ -32,4 +32,8 @@
<script type="text/template" id="template-help-move-on-loopback-success"> <script type="text/template" id="template-help-move-on-loopback-success">
You can move to the next step now. You can move to the next step now.
</script>
<script type="text/template" id="template-help-minimum-output-channels">
To be a valid output audio device, it must have at least 2 output ports.
</script> </script>

View File

@ -277,10 +277,6 @@
var testBridgeScreen = new JK.TestBridgeScreen(JK.app); var testBridgeScreen = new JK.TestBridgeScreen(JK.app);
testBridgeScreen.initialize(); testBridgeScreen.initialize();
if(!connected) {
jamServer.initiateReconnect(null, true);
}
JK.app.initialRouting(); JK.app.initialRouting();
JK.hideCurtain(300); JK.hideCurtain(300);
} }
@ -317,8 +313,12 @@
}); });
// this ensures that there is always a CurrentSessionModel, even if it's for a non-active session // this ensures that there is always a CurrentSessionModel, even if it's for a non-active session
JK.CurrentSessionModel = new JK.SessionModel(JK.app, JK.JamServer, window.jamClient); JK.CurrentSessionModel = new JK.SessionModel(JK.app, JK.JamServer, window.jamClient, null);
} }
// make surethe CurrentSessionModel exists before initializing backend alerts
var backendAlerts = new JK.BackendAlerts(JK.app);
backendAlerts.initialize();
JK.bindHoverEvents(); JK.bindHoverEvents();
}) })

View File

@ -42,10 +42,6 @@
function _initAfterConnect(connected) { function _initAfterConnect(connected) {
if (this.didInitAfterConnect) return; if (this.didInitAfterConnect) return;
this.didInitAfterConnect = true this.didInitAfterConnect = true
if(!connected) {
jamServer.initiateReconnect(null, true);
}
} }
JK.app = JK.JamKazam(); JK.app = JK.JamKazam();

View File

@ -21,13 +21,13 @@
.clearall .clearall
%ul.results-text %ul.results-text
%li.latency-good Your latency is good. %li.latency-good Your latency is good.
%li.latency-acceptable Your latency is acceptable. %li.latency-acceptable Your latency is fair.
%li.latency-bad Your latency is poor. %li.latency-bad Your latency is poor.
%li.io-rate-good Your I/O rate is good. %li.io-rate-good Your I/O rate is good.
%li.io-rate-acceptable Your I/O rate is acceptable. %li.io-rate-acceptable Your I/O rate is fair.
%li.io-rate-bad Your I/O rate is poor. %li.io-rate-bad Your I/O rate is poor.
%li.io-var-good Your I/O variance is good. %li.io-var-good Your I/O variance is good.
%li.io-var-acceptable Your I/O variance is acceptable. %li.io-var-acceptable Your I/O variance is fair.
%li.io-var-bad Your I/O variance is poor. %li.io-var-bad Your I/O variance is poor.
%li.success You may proceed to the next step. %li.success You may proceed to the next step.
%li.failure %li.failure

View File

@ -73,7 +73,7 @@
%li Drag and drop the input port(s) from your audio interface to each track. %li Drag and drop the input port(s) from your audio interface to each track.
%li Select the instrument for each track. %li Select the instrument for each track.
.center .center
%a.button-orange.watch-video{href:'#'} WATCH VIDEO %a.button-orange.watch-video{href:'https://www.youtube.com/watch?v=SjMeMZpKNR4', rel:'external'} WATCH VIDEO
.wizard-step-column .wizard-step-column
%h2 Unassigned Ports %h2 Unassigned Ports
.unassigned-input-channels.channels-holder .unassigned-input-channels.channels-holder
@ -97,9 +97,9 @@
%p Determine if you need to set up a voice chat input. %p Determine if you need to set up a voice chat input.
%p If you do, then assign the audio input to use to capture voice chat. %p If you do, then assign the audio input to use to capture voice chat.
.center .center
%a.button-orange.watch-video{href:'#'} WATCH VIDEO %a.button-orange.watch-video{href:'https://www.youtube.com/watch?v=f7niycdWm7Y', rel:'external'} WATCH VIDEO
.wizard-step-column .wizard-step-column
%h2 Select Voice Chat Option %h2.sub-header Select Voice Chat Option
%form.voice %form.voice
.voicechat-option.reuse-audio-input .voicechat-option.reuse-audio-input
%input{type:"radio", name: "voicechat", checked:"checked"} %input{type:"radio", name: "voicechat", checked:"checked"}
@ -109,9 +109,16 @@
%input{type:"radio", name: "voicechat"} %input{type:"radio", name: "voicechat"}
%h3 Use Chat Microphone %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 %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 .clearall
%h2 Voice Chat Input // .wizard-step-column
%h2.sub-header.voice-chat-header Voice Chat Input
.ftue-box.chat-inputs .ftue-box.chat-inputs
.vu-meter
.ftue-controls
.ftue-vu-left.voice-chat-vu-left
.ftue-fader.chat-fader
.gain-label GAIN
.ftue-vu-right.voice-chat-vu-right
.wizard-step{ 'layout-wizard-step' => "4", 'dialog-title' => "Turn Off Direct Monitoring", 'dialog-purpose' => "DirectMonitoring" } .wizard-step{ 'layout-wizard-step' => "4", 'dialog-title' => "Turn Off Direct Monitoring", 'dialog-purpose' => "DirectMonitoring" }
@ -127,7 +134,7 @@
%li If a button, push it into its off position. %li If a button, push it into its off position.
%li If a knob, turn it so that 100% of audio is from your computer, and 0% is from the direct monitor. %li If a knob, turn it so that 100% of audio is from your computer, and 0% is from the direct monitor.
.center .center
%a.button-orange.watch-video{href:'#'} WATCH VIDEO %a.button-orange.watch-video{href:'https://www.youtube.com/watch?v=-nC-D3JBHnk', rel:'external'} WATCH VIDEO
.wizard-step-column .wizard-step-column
.help-content .help-content
When you have fully turned off the direct monitoring control (if any) on your audio interface, When you have fully turned off the direct monitoring control (if any) on your audio interface,

View File

@ -46,10 +46,10 @@
%h2.ports-header Audio Input Ports %h2.ports-header Audio Input Ports
.ftue-box.input-ports.ports .ftue-box.input-ports.ports
.ftue-controls .ftue-controls
.ftue-vu-left#loopback-audio-input-vu-left .ftue-vu-left.audio-input-vu-left
.ftue-fader#loopback-audio-input-fader .ftue-fader.audio-input-fader
.gain-label GAIN .gain-label GAIN
.ftue-vu-right#loopback-audio-input-vu-right .ftue-vu-right.audio-input-vu-right
= render :partial => "/clients/wizard/framebuffers" = render :partial => "/clients/wizard/framebuffers"
.wizard-step-column .wizard-step-column
%h2 %h2
@ -59,10 +59,10 @@
%h2.ports-header Audio Output Ports %h2.ports-header Audio Output Ports
.ftue-box.output-ports.ports .ftue-box.output-ports.ports
.ftue-controls .ftue-controls
.ftue-vu-left#loopback-audio-output-vu-left .ftue-vu-left.audio-output-vu-left
.ftue-fader#loopback-audio-output-fader .ftue-fader.audio-output-fader
.gain-label GAIN .gain-label GAIN
.ftue-vu-right#loopback-audio-output-vu-right .ftue-vu-right.audio-output-vu-right
%a.button-orange.resync-btn RESYNC %a.button-orange.resync-btn RESYNC
.wizard-step-column .wizard-step-column
%h2.test-results-header %h2.test-results-header

View File

@ -408,6 +408,9 @@ SampleApp::Application.routes.draw do
# version check for JamClient # version check for JamClient
match '/versioncheck' => 'artifacts#versioncheck' match '/versioncheck' => 'artifacts#versioncheck'
# no-op method to see if server is running
match '/healthcheck' => 'artifacts#healthcheck'
# list all uris for available clients on mac, windows, linux, if available # list all uris for available clients on mac, windows, linux, if available
match '/artifacts/clients' => 'artifacts#client_downloads' match '/artifacts/clients' => 'artifacts#client_downloads'

View File

@ -3,6 +3,11 @@ description "jam-web"
start on startup start on startup
start on runlevel [2345] start on runlevel [2345]
stop on runlevel [016] stop on runlevel [016]
limit nofile 20000 20000
limit core unlimited unlimited
respawn
respawn limit 10 5
pre-start script pre-start script
set -e set -e

View File

@ -143,7 +143,8 @@ FactoryGirl.define do
addr {JamIsp.ip_to_num(ip_address)} addr {JamIsp.ip_to_num(ip_address)}
locidispid 0 locidispid 0
client_type 'client' client_type 'client'
last_jam_audio_latency { user.last_jam_audio_latency if user } last_jam_audio_latency { user.last_jam_audio_latency if user }
# sequence(:channel_id) { |n| "Channel#{n}"}
end end
factory :friendship, :class => JamRuby::Friendship do factory :friendship, :class => JamRuby::Friendship do

View File

@ -21,7 +21,7 @@ describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => tru
# step 2 - select gear # step 2 - select gear
find('.ftue-step-title', text: 'Select & Test Audio Gear') find('.ftue-step-title', text: 'Select & Test Audio Gear')
jk_select('Built-in', 'div[layout-wizard-step="1"] select.select-audio-input-device') jk_select('Built-in', 'div[layout-wizard-step="1"] select.select-audio-input-device')
find('.btn-next.button-orange').trigger(:click) find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 3 - configure tracks # step 3 - configure tracks
find('.ftue-step-title', text: 'Configure Tracks') find('.ftue-step-title', text: 'Configure Tracks')
@ -31,22 +31,22 @@ describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => tru
track_slot = first('.track-target') track_slot = first('.track-target')
input.drag_to(track_slot) input.drag_to(track_slot)
find('.btn-next.button-orange').trigger(:click) find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 4 - configure voice chat # step 4 - configure voice chat
find('.ftue-step-title', text: 'Configure Voice Chat') find('.ftue-step-title', text: 'Configure Voice Chat')
find('.btn-next.button-orange').trigger(:click) find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 5 - configure direct monitoring # step 5 - configure direct monitoring
find('.ftue-step-title', text: 'Turn Off Direct Monitoring') find('.ftue-step-title', text: 'Turn Off Direct Monitoring')
find('.btn-next.button-orange').trigger(:click) find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 6 - Test Router & Network # step 6 - Test Router & Network
find('.ftue-step-title', text: 'Test Router & Network') find('.ftue-step-title', text: 'Test Router & Network')
find('.button-orange.start-network-test').trigger(:click) find('.button-orange.start-network-test').trigger(:click)
find('.user-btn', text: 'RUN NETWORK TEST ANYWAY').trigger(:click) find('.user-btn', text: 'RUN NETWORK TEST ANYWAY').trigger(:click)
find('.button-orange.start-network-test') find('.button-orange.start-network-test')
find('.btn-next.button-orange').trigger(:click) find('.btn-next.button-orange:not(.disabled)').trigger(:click)
# step 7 - Success # step 7 - Success
find('.ftue-step-title', text: 'Success!') find('.ftue-step-title', text: 'Success!')

View File

@ -73,12 +73,17 @@ describe "User Progression", :js => true, :type => :feature, :capybara_feature
describe "certified gear" do describe "certified gear" do
before(:each) do before(:each) do
sign_in_poltergeist user sign_in_poltergeist user
FactoryGirl.create(:latency_tester)
visit '/client#/account/audio' visit '/client#/account/audio'
# step 1 - intro
find("div.account-audio a[data-purpose='add-profile']").trigger(:click) find("div.account-audio a[data-purpose='add-profile']").trigger(:click)
find('.btn-next').trigger(:click) find('.btn-next').trigger(:click)
jk_select('Built-in', 'div[layout-wizard-step="1"] select.select-audio-input-device')
find('.btn-next.button-orange').trigger(:click) # step 2 - select gear
find('.ftue-step-title', text: 'Select & Test Audio Gear')
jk_select('Built-in', 'div[layout-wizard-step="1"] select.select-audio-input-device')
find('.btn-next.button-orange:not(.disabled)').trigger(:click)
sleep 1 sleep 1
end end

View File

@ -11,22 +11,6 @@ module EventMachine
module WebSocket module WebSocket
class Connection < EventMachine::Connection class Connection < EventMachine::Connection
attr_accessor :encode_json, :channel_id, :client_id, :user_id, :context, :trusted # client_id is uuid we give to each client to track them as we like attr_accessor :encode_json, :channel_id, :client_id, :user_id, :context, :trusted # client_id is uuid we give to each client to track them as we like
# http://stackoverflow.com/questions/11150147/how-to-check-if-eventmachineconnection-is-open
attr_accessor :connected
def connection_completed
connected = true
super
end
def connected?
!!connected
end
def unbind
connected = false
super
end
end end
end end
end end
@ -226,6 +210,40 @@ module JamWebsockets
MQRouter.client_exchange = @clients_exchange MQRouter.client_exchange = @clients_exchange
end end
# this method allows you to translate exceptions into websocket channel messages and behavior safely.
# pass in your block, throw an error in your logic, and have the right things happen on the websocket channel
def websocket_comm(client, original_message_id, &blk)
begin
blk.call
rescue SessionError => e
@log.info "ending client session deliberately due to malformed client behavior. reason=#{e}"
begin
# wrap the message up and send it down
error_msg = @message_factory.server_rejection_error(e.to_s)
send_to_client(client, error_msg)
ensure
cleanup_client(client)
end
rescue PermissionError => e
@log.info "permission error. reason=#{e.to_s}"
@log.info e
# wrap the message up and send it down
error_msg = @message_factory.server_permission_error(original_message_id, e.to_s)
send_to_client(client, error_msg)
rescue => e
@log.error "ending client session due to server programming or runtime error. reason=#{e.to_s}"
@log.error e
begin
# wrap the message up and send it down
error_msg = @message_factory.server_generic_error(e.to_s)
send_to_client(client, error_msg)
ensure
cleanup_client(client)
end
end
end
def new_client(client, is_trusted) def new_client(client, is_trusted)
# default to using json instead of pb # default to using json instead of pb
@ -246,6 +264,9 @@ module JamWebsockets
client.encode_json = false client.encode_json = false
end end
websocket_comm(client, nil) do
handle_login(client, handshake.query)
end
} }
client.onclose { client.onclose {
@ -261,50 +282,26 @@ module JamWebsockets
end end
} }
client.onmessage { |msg| client.onmessage { |data|
# TODO: set a max message size before we put it through PB? # TODO: set a max message size before we put it through PB?
# TODO: rate limit? # TODO: rate limit?
pb_msg = nil msg = nil
begin # extract the message safely
websocket_comm(client, nil) do
if client.encode_json if client.encode_json
#example: {"type":"LOGIN", "target":"server", "login" : {"username":"hi"}} json = JSON.parse(data)
parse = JSON.parse(msg) msg = Jampb::ClientMessage.json_create(json)
pb_msg = Jampb::ClientMessage.json_create(parse)
self.route(pb_msg, client)
else else
pb_msg = Jampb::ClientMessage.parse(msg.to_s) msg = Jampb::ClientMessage.parse(data.to_s)
self.route(pb_msg, client)
end end
rescue SessionError => e end
@log.info "ending client session deliberately due to malformed client behavior. reason=#{e}"
begin
# wrap the message up and send it down
error_msg = @message_factory.server_rejection_error(e.to_s)
send_to_client(client, error_msg)
ensure
cleanup_client(client)
end
rescue PermissionError => e
@log.info "permission error. reason=#{e.to_s}"
@log.info e
# wrap the message up and send it down # then route it internally
error_msg = @message_factory.server_permission_error(pb_msg.message_id, e.to_s) websocket_comm(client, msg.message_id) do
send_to_client(client, error_msg) self.route(msg, client)
rescue => e
@log.error "ending client session due to server programming or runtime error. reason=#{e.to_s}"
@log.error e
begin
# wrap the message up and send it down
error_msg = @message_factory.server_generic_error(e.to_s)
send_to_client(client, error_msg)
ensure
cleanup_client(client)
end
end end
} }
end end
@ -391,9 +388,9 @@ module JamWebsockets
# removes all resources associated with a client # removes all resources associated with a client
def cleanup_client(client) def cleanup_client(client)
@semaphore.synchronize do client.close
client.close if client.connected?
@semaphore.synchronize do
pending = client.context.nil? # presence of context implies this connection has been logged into pending = client.context.nil? # presence of context implies this connection has been logged into
if pending if pending
@ -514,6 +511,7 @@ module JamWebsockets
heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(nil, client_type) heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(nil, client_type)
latency_tester = LatencyTester.connect({ latency_tester = LatencyTester.connect({
client_id: client_id, client_id: client_id,
channel_id: client.channel_id,
ip_address: remote_ip, ip_address: remote_ip,
connection_stale_time: connection_stale_time, connection_stale_time: connection_stale_time,
connection_expire_time: connection_expire_time}) connection_expire_time: connection_expire_time})
@ -543,15 +541,15 @@ module JamWebsockets
end end
end end
def handle_login(login, client) def handle_login(client, options)
username = login.username if login.value_for_tag(1) username = options["username"]
password = login.password if login.value_for_tag(2) password = options["password"]
token = login.token if login.value_for_tag(3) token = options["token"]
client_id = login.client_id if login.value_for_tag(4) client_id = options["client_id"]
reconnect_music_session_id = login.reconnect_music_session_id if login.value_for_tag(5) reconnect_music_session_id = options["music_session_id"]
client_type = login.client_type if login.value_for_tag(6) client_type = options["client_type"]
@log.info("*** handle_login: token=#{token}; client_id=#{client_id}, client_type=#{client_type}") @log.info("handle_login: client_type=#{client_type} token=#{token} client_id=#{client_id} channel_id=#{client.channel_id}")
if client_type == Connection::TYPE_LATENCY_TESTER if client_type == Connection::TYPE_LATENCY_TESTER
handle_latency_tester_login(client_id, client_type, client) handle_latency_tester_login(client_id, client_type, client)
@ -568,17 +566,21 @@ module JamWebsockets
user = valid_login(username, password, token, client_id) user = valid_login(username, password, token, client_id)
# XXX This logic needs to instead be handled by a broadcast out to all websockets indicating dup
# kill any websocket connections that have this same client_id, which can happen in race conditions # kill any websocket connections that have this same client_id, which can happen in race conditions
# this code must happen here, before we go any further, so that there is only one websocket connection per client_id # this code must happen here, before we go any further, so that there is only one websocket connection per client_id
existing_context = @client_lookup[client_id] existing_context = @client_lookup[client_id]
if existing_context if existing_context
# in some reconnect scenarios, we may have in memory a websocket client still. # in some reconnect scenarios, we may have in memory a websocket client still.
# let's whack it, and tell the other client, if still connected, that this is a duplicate login attempt
@log.info "duplicate client: #{existing_context}" @log.info "duplicate client: #{existing_context}"
Diagnostic.duplicate_client(existing_context.user, existing_context) if existing_context.client.connected Diagnostic.duplicate_client(existing_context.user, existing_context)
error_msg = @message_factory.server_duplicate_client_error
send_to_client(existing_context.client, error_msg)
cleanup_client(existing_context.client) cleanup_client(existing_context.client)
end end
connection = JamRuby::Connection.find_by_client_id(client_id) connection = Connection.find_by_client_id(client_id)
# if this connection is reused by a different user (possible in logout/login scenarios), then whack the connection # if this connection is reused by a different user (possible in logout/login scenarios), then whack the connection
# because it will recreate a new connection lower down # because it will recreate a new connection lower down
if connection && user && connection.user != user if connection && user && connection.user != user
@ -608,7 +610,7 @@ module JamWebsockets
recording_id = nil recording_id = nil
ConnectionManager.active_record_transaction do |connection_manager| ConnectionManager.active_record_transaction do |connection_manager|
music_session_id, reconnected = connection_manager.reconnect(connection, reconnect_music_session_id, remote_ip, connection_stale_time, connection_expire_time) music_session_id, reconnected = connection_manager.reconnect(connection, client.channel_id, reconnect_music_session_id, remote_ip, connection_stale_time, connection_expire_time)
if music_session_id.nil? if music_session_id.nil?
# if this is a reclaim of a connection, but music_session_id comes back null, then we need to check if this connection was IN a music session before. # if this is a reclaim of a connection, but music_session_id comes back null, then we need to check if this connection was IN a music session before.
@ -644,7 +646,7 @@ module JamWebsockets
unless connection unless connection
# log this connection in the database # log this connection in the database
ConnectionManager.active_record_transaction do |connection_manager| ConnectionManager.active_record_transaction do |connection_manager|
connection_manager.create_connection(user.id, client.client_id, remote_ip, client_type, connection_stale_time, connection_expire_time) do |conn, count| connection_manager.create_connection(user.id, client.client_id, client.channel_id, remote_ip, client_type, connection_stale_time, connection_expire_time) do |conn, count|
if count == 1 if count == 1
Notification.send_friend_update(user.id, true, conn) Notification.send_friend_update(user.id, true, conn)
end end
@ -755,7 +757,7 @@ module JamWebsockets
if !token.nil? && token != '' if !token.nil? && token != ''
@log.debug "logging in via token" @log.debug "logging in via token"
# attempt login with token # attempt login with token
user = JamRuby::User.find_by_remember_token(token) user = User.find_by_remember_token(token)
if user.nil? if user.nil?
@log.debug "no user found with token #{token}" @log.debug "no user found with token #{token}"

View File

@ -3,6 +3,11 @@ description "websocket-gateway"
start on startup start on startup
start on runlevel [2345] start on runlevel [2345]
stop on runlevel [016] stop on runlevel [016]
limit nofile 20000 20000
limit core unlimited unlimited
respawn
respawn limit 10 5
pre-start script pre-start script
set -e set -e

View File

@ -87,6 +87,7 @@ FactoryGirl.define do
ip_address '1.1.1.1' ip_address '1.1.1.1'
as_musician true as_musician true
client_type 'client' client_type 'client'
sequence(:channel_id) { |n| "Channel#{n}"}
end end
factory :instrument, :class => JamRuby::Instrument do factory :instrument, :class => JamRuby::Instrument do

View File

@ -41,7 +41,7 @@ end
# does a login and returns client # does a login and returns client
def login(router, user, password, client_id) def login(router, user, password, client_id, token, client_type)
message_factory = MessageFactory.new message_factory = MessageFactory.new
client = LoginClient.new client = LoginClient.new
@ -60,15 +60,9 @@ def login(router, user, password, client_id)
@router.new_client(client, false) @router.new_client(client, false)
handshake = double("handshake") handshake = double("handshake")
handshake.should_receive(:query).twice.and_return({ "pb" => "true", "channel_id" => SecureRandom.uuid }) handshake.should_receive(:query).exactly(3).times.and_return({ "pb" => "true", "channel_id" => SecureRandom.uuid, "client_id" => client_id, "token" => token, "client_type" => client_type })
client.onopenblock.call handshake client.onopenblock.call handshake
# create a login message, and pass it into the router via onmsgblock.call
login = message_factory.login_with_user_pass(user.email, password, :client_id => client_id, :client_type => 'client')
# first log in
client.onmsgblock.call login.to_s
client client
end end
@ -98,15 +92,9 @@ def login_latency_tester(router, latency_tester, client_id)
@router.new_client(client, true) @router.new_client(client, true)
handshake = double("handshake") handshake = double("handshake")
handshake.should_receive(:query).twice.and_return({ "pb" => "true", "channel_id" => SecureRandom.uuid }) handshake.should_receive(:query).exactly(3).times.and_return({ "pb" => "true", "channel_id" => SecureRandom.uuid, "client_type" => "latency_tester", "client_id" => client_id })
client.onopenblock.call handshake client.onopenblock.call handshake
# create a login message, and pass it into the router via onmsgblock.call
login = message_factory.login_with_client_id(client_id)
# first log in
client.onmsgblock.call login.to_s
client client
end end
@ -239,7 +227,7 @@ describe Router do
it "should allow login of valid user", :mq => true do it "should allow login of valid user", :mq => true do
@user = FactoryGirl.create(:user, @user = FactoryGirl.create(:user,
:password => "foobar", :password_confirmation => "foobar") :password => "foobar", :password_confirmation => "foobar")
client1 = login(@router, @user, "foobar", "1") client1 = login(@router, @user, "foobar", "1", @user.remember_token, "client")
done done
end end
@ -271,7 +259,7 @@ describe Router do
# make a music_session and define two members # make a music_session and define two members
# create client 1, log him in, and log him in to music session # create client 1, log him in, and log him in to music session
client1 = login(@router, user1, "foobar", "1") client1 = login(@router, user1, "foobar", "1", user1.remember_token, "client")
done done
end end
@ -284,9 +272,9 @@ describe Router do
# create client 1, log him in, and log him in to music session # create client 1, log him in, and log him in to music session
client1 = login(@router, user1, "foobar", "1") client1 = login(@router, user1, "foobar", "1", user1.remember_token, "client")
client2 = login(@router, user2, "foobar", "2") client2 = login(@router, user2, "foobar", "2", user2.remember_token, "client")
# make a music_session and define two members # make a music_session and define two members
@ -305,10 +293,10 @@ describe Router do
music_session = FactoryGirl.create(:active_music_session, :creator => user1) music_session = FactoryGirl.create(:active_music_session, :creator => user1)
# create client 1, log him in, and log him in to music session # create client 1, log him in, and log him in to music session
client1 = login(@router, user1, "foobar", "1") client1 = login(@router, user1, "foobar", "1", user1.remember_token, "client")
#login_music_session(@router, client1, music_session) #login_music_session(@router, client1, music_session)
client2 = login(@router, user2, "foobar", "2") client2 = login(@router, user2, "foobar", "2", user2.remember_token, "client")
#login_music_session(@router, client2, music_session) #login_music_session(@router, client2, music_session)
# by creating # by creating