diff --git a/admin/app/admin/feeds.rb b/admin/app/admin/feeds.rb index da8fde49a..a58509708 100644 --- a/admin/app/admin/feeds.rb +++ b/admin/app/admin/feeds.rb @@ -116,8 +116,8 @@ ActiveAdmin.register_page 'Feed' do mixes.each do |mix| li do 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 "Completed At: #{mix.completed_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 ? mix.completed_at.strftime('%b %d %Y, %H:%M') : ''}, " text_node "Error Count: #{mix.error_count}, " text_node "Error Reason: #{mix.error_reason}, " text_node "Error Detail: #{mix.error_detail}, " diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index 79f138df0..b6afa5e35 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -10,7 +10,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do filter :email filter :first_name filter :last_name - filter :internet_service_provider filter :created_at filter :updated_at @@ -28,7 +27,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do ff.input :musician ff.input :can_invite ff.input :photo_url - ff.input :internet_service_provider ff.input :session_settings end ff.inputs "Signup" do @@ -53,7 +51,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do row :last_name row :birth_date row :gender - row :internet_service_provider row :email_confirmed row :image do user.photo_url ? image_tag(user.photo_url) : '' end row :session_settings @@ -81,7 +78,6 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do column :last_name column :birth_date column :gender - column :internet_service_provider column :email_confirmed column :photo_url column :session_settings diff --git a/admin/config/initializers/jam_ruby_user.rb b/admin/config/initializers/jam_ruby_user.rb index 75d18197d..980c82b9b 100644 --- a/admin/config/initializers/jam_ruby_user.rb +++ b/admin/config/initializers/jam_ruby_user.rb @@ -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) - 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 '' diff --git a/admin/script/package/jam-admin.conf b/admin/script/package/jam-admin.conf index 90e39314c..7b8e2deff 100644 --- a/admin/script/package/jam-admin.conf +++ b/admin/script/package/jam-admin.conf @@ -3,6 +3,11 @@ description "jam-admin" start on startup start on runlevel [2345] stop on runlevel [016] +limit nofile 20000 20000 +limit core unlimited unlimited + +respawn +respawn limit 10 5 pre-start script set -e diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb index a2d058d80..93063a175 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -36,6 +36,7 @@ FactoryGirl.define do addr 0 locidispid 0 client_type 'client' + sequence(:channel_id) { |n| "Channel#{n}"} association :user, factory: :user end diff --git a/db/manifest b/db/manifest index bceb5bfa8..85280cce1 100755 --- a/db/manifest +++ b/db/manifest @@ -179,4 +179,5 @@ sms_index.sql music_sessions_description_search.sql rsvp_slots_prof_level.sql add_file_name_music_notation.sql -change_scheduled_start_music_session.sql \ No newline at end of file +change_scheduled_start_music_session.sql +connection_channel_id.sql \ No newline at end of file diff --git a/db/up/connection_channel_id.sql b/db/up/connection_channel_id.sql new file mode 100644 index 000000000..4ea21524f --- /dev/null +++ b/db/up/connection_channel_id.sql @@ -0,0 +1 @@ +ALTER TABLE connections ADD COLUMN channel_id VARCHAR(256) NOT NULL; \ No newline at end of file diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 9641f7881..88bd060c2 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -86,6 +86,7 @@ message ClientMessage { SERVER_REJECTION_ERROR = 1005; SERVER_PERMISSION_ERROR = 1010; SERVER_BAD_STATE_ERROR = 1015; + SERVER_DUPLICATE_CLIENT_ERROR = 1016; } // Identifies which inner message is filled in @@ -179,6 +180,7 @@ message ClientMessage { optional ServerRejectionError server_rejection_error = 1005; optional ServerPermissionError server_permission_error = 1010; optional ServerBadStateError server_bad_state_error = 1015; + optional ServerDuplicateClientError server_duplicate_client_error = 1016; } // route_to: server @@ -636,4 +638,9 @@ message ServerBadStateError { optional string error_msg = 1; } +// route_to: client +// this indicates that we detected another client with the same client ID +message ServerDuplicateClientError { +} + diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 4916da854..30d31c1eb 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -44,7 +44,7 @@ module JamRuby end # 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 reconnected = false @@ -86,7 +86,7 @@ module JamRuby end sql =< ClientMessage::Type::SERVER_DUPLICATE_CLIENT_ERROR, + :route_to => CLIENT_TARGET, + :server_duplicate_client_error => error + ) + end + ###################################### NOTIFICATIONS ###################################### # create a friend update message diff --git a/ruby/lib/jam_ruby/models/latency_tester.rb b/ruby/lib/jam_ruby/models/latency_tester.rb index 2bc1071ae..e4e36170e 100644 --- a/ruby/lib/jam_ruby/models/latency_tester.rb +++ b/ruby/lib/jam_ruby/models/latency_tester.rb @@ -19,6 +19,7 @@ module JamRuby # or bootstrap a new latency_tester def self.connect(options) client_id = options[:client_id] + channel_id = options[:channel_id] ip_address = options[:ip_address] connection_stale_time = options[:connection_stale_time] connection_expire_time = options[:connection_expire_time] @@ -69,6 +70,7 @@ module JamRuby connection.stale_time = connection_stale_time connection.expire_time = connection_expire_time connection.as_musician = false + connection.channel_id = channel_id unless connection.save return connection end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 773e52adb..9bf6d117f 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -125,7 +125,8 @@ FactoryGirl.define do addr 0 locidispid 0 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 end diff --git a/ruby/spec/jam_ruby/connection_manager_spec.rb b/ruby/spec/jam_ruby/connection_manager_spec.rb index bdb397262..d6976fe50 100644 --- a/ruby/spec/jam_ruby/connection_manager_spec.rb +++ b/ruby/spec/jam_ruby/connection_manager_spec.rb @@ -9,6 +9,8 @@ describe ConnectionManager do STALE_BUT_NOT_EXPIRED = 50 DEFINITELY_EXPIRED = 70 + let(:channel_id) {'1'} + before do @conn = PG::Connection.new(:dbname => SpecDb::TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost") @connman = ConnectionManager.new(:conn => @conn) @@ -46,8 +48,8 @@ describe ConnectionManager do user.save! user = nil - @connman.create_connection(user_id, client_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) + @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, channel_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) }.to raise_error(PG::Error) end it "create connection then delete it" do @@ -56,7 +58,7 @@ describe ConnectionManager do #user_id = create_user("test", "user2", "user2@jamkazam.com") 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 @@ -86,7 +88,7 @@ describe ConnectionManager do #user_id = create_user("test", "user2", "user2@jamkazam.com") 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 @@ -102,7 +104,7 @@ describe ConnectionManager do cc.addr.should == 0x01010101 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.connected?.should be_true @@ -215,7 +217,7 @@ describe ConnectionManager do it "flag stale connection" do client_id = "client_id8" 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.should == 1 @@ -256,7 +258,7 @@ describe ConnectionManager do it "expires stale connection" do client_id = "client_id8" 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) set_updated_at(conn, Time.now - STALE_BUT_NOT_EXPIRED) @@ -282,7 +284,7 @@ describe ConnectionManager do music_session_id = music_session.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.errors.any?.should be_false @@ -318,8 +320,8 @@ describe ConnectionManager do client_id2 = "client_id10.12" 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) - @connman.create_connection(user_id, client_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_id, client_id, channel_id, "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_id = music_session.id @@ -338,7 +340,7 @@ describe ConnectionManager do client_id = "client_id10.2" 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) user = User.find(user_id) @@ -354,8 +356,8 @@ describe ConnectionManager do fan_client_id = "client_id10.4" musician_id = create_user("test", "user10.3", "user10.3@jamkazam.com") 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(fan_id, fan_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, 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_id = music_session.id @@ -379,7 +381,7 @@ describe ConnectionManager do music_session_id = music_session.id 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 expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound) end @@ -391,7 +393,7 @@ describe ConnectionManager do user = User.find(user_id) 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.errors.size.should == 1 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 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 expect { @connman.join_music_session(user, client_id, music_session, true, TRACKS, 10) } .to raise_error(ActiveRecord::RecordNotFound) end @@ -419,7 +421,7 @@ describe ConnectionManager do user = User.find(user_id) 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) end @@ -434,7 +436,7 @@ describe ConnectionManager do dummy_music_session = ActiveMusicSession.new - @connman.create_connection(user_id, client_id, "1.1.1.1", 'client', STALE_TIME, EXPIRE_TIME) + @connman.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) expect { @connman.leave_music_session(user, Connection.find_by_client_id(client_id), dummy_music_session) }.to raise_error(JamRuby::StateError) end @@ -447,7 +449,7 @@ describe ConnectionManager do music_session_id = music_session.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) assert_session_exists(music_session_id, true) @@ -490,7 +492,7 @@ describe ConnectionManager do user = User.find(user_id) 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) connection1 = @connman.join_music_session(user, client_id1, music_session1, true, TRACKS, 10) connection1.errors.size.should == 0 diff --git a/ruby/spec/jam_ruby/models/latency_tester_spec.rb b/ruby/spec/jam_ruby/models/latency_tester_spec.rb index e0d3071d2..4bae7633d 100644 --- a/ruby/spec/jam_ruby/models/latency_tester_spec.rb +++ b/ruby/spec/jam_ruby/models/latency_tester_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' 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 latency_tester = FactoryGirl.create(:latency_tester) diff --git a/web/app/assets/images/content/icon_drag_handle.png b/web/app/assets/images/content/icon_drag_handle.png new file mode 100644 index 000000000..ac55d4e69 Binary files /dev/null and b/web/app/assets/images/content/icon_drag_handle.png differ diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 2959e8485..477af18c3 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -75,7 +75,8 @@ SERVER_BAD_STATE_RECOVERED: "SERVER_BAD_STATE_RECOVERED", SERVER_GENERIC_ERROR : "SERVER_GENERIC_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 = { diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index ffd1142f6..60b69685c 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -33,6 +33,7 @@ var notificationLastSeenAt = undefined; var notificationLastSeen = undefined; var clientClosedConnection = false; + var initialConnectAttempt = true; // reconnection logic 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 function closedCleanup(in_error) { + server.connected = false; // stop future heartbeats if (heartbeatInterval != null) { @@ -98,15 +100,16 @@ heartbeatAckCheckInterval = null; } - if (server.connected) { - 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; - } + clearConnectTimeout(); - 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'); @@ -174,12 +177,16 @@ return mode == "client"; } - function loggedIn(header, payload) { - - if (!connectTimeout) { + function clearConnectTimeout() { + if (connectTimeout) { clearTimeout(connectTimeout); connectTimeout = null; } + } + + function loggedIn(header, payload) { + + clearConnectTimeout(); heartbeatStateReset(); @@ -191,6 +198,13 @@ $.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; 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'); @@ -240,7 +254,7 @@ var start = new Date().getTime(); server.connect() .done(function () { - guardAgainstRapidTransition(start, performReconnect); + guardAgainstRapidTransition(start, finishReconnect); }) .fail(function () { guardAgainstRapidTransition(start, closedOnReconnectAttempt); @@ -252,7 +266,7 @@ failedReconnect(); } - function performReconnect() { + function finishReconnect() { if(!clientClosedConnection) { lastDisconnectedReason = 'WEBSOCKET_CLOSED_REMOTELY' @@ -277,7 +291,6 @@ else { window.location.reload(); } - server.reconnecting = false; }); @@ -459,16 +472,29 @@ connectDeferred = new $.Deferred(); 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); + server.connecting = true; server.socket = new context.WebSocket(uri); server.socket.onopen = server.onOpen; server.socket.onmessage = server.onMessage; server.socket.onclose = server.onClose; connectTimeout = setTimeout(function () { + logger.debug("connection timeout fired") connectTimeout = null; if(connectDeferred.state() === 'pending') { @@ -505,12 +531,7 @@ server.onOpen = function () { logger.debug("server.onOpen"); - if(isClientMode()) { - server.rememberLogin(); - } - else { - server.latencyTesterLogin(); - } + // we should receive LOGIN_ACK very soon. we already set a timer elsewhere to give 4 seconds to receive it }; server.onMessage = function (e) { @@ -675,7 +696,6 @@ } this.initialize = initialize; - this.initiateReconnect = initiateReconnect; return this; } diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 5e9fe071f..368d9ad6c 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -33,11 +33,11 @@ //= require jquery.pulse //= require jquery.browser //= require jquery.custom-protocol +//= require AAC_underscore //= require AAA_Log //= require globals //= require AAB_message_factory //= require jam_rest -//= require AAC_underscore //= require utils //= require custom_controls //= require_directory . diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js new file mode 100644 index 000000000..55e1636e3 --- /dev/null +++ b/web/app/assets/javascripts/backend_alerts.js @@ -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); \ No newline at end of file diff --git a/web/app/assets/javascripts/banner.js b/web/app/assets/javascripts/banner.js index 5bb003440..7eda9fa0f 100644 --- a/web/app/assets/javascripts/banner.js +++ b/web/app/assets/javascripts/banner.js @@ -10,9 +10,17 @@ var logger = context.JK.logger; var $banner = null; + // you can also do + // * showAlert('title', 'text') + // * showAlert('text') function showAlert(options) { 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' return show(options); @@ -24,6 +32,13 @@ var text = options.text; var html = options.html; + if(!options.title) { + options.title = 'alert' + } + + var $h1 = $banner.find('h1'); + $h1.html(options.title); + var newContent = null; if (html) { newContent = $('#banner .dialog-inner').html(html); diff --git a/web/app/assets/javascripts/chatPanel.js b/web/app/assets/javascripts/chatPanel.js index bf5b05eca..45a13014b 100644 --- a/web/app/assets/javascripts/chatPanel.js +++ b/web/app/assets/javascripts/chatPanel.js @@ -274,7 +274,6 @@ if(response.next == null) { // if we less results than asked for, end searching $chatMessagesScroller.infinitescroll('pause'); - logger.debug("end of chatss"); if(currentPage > 0) { // there are bugs with infinitescroll not removing the 'loading'. diff --git a/web/app/assets/javascripts/configureTrackDialog.js b/web/app/assets/javascripts/configureTrackDialog.js index 495d62e0b..0a0e415c9 100644 --- a/web/app/assets/javascripts/configureTrackDialog.js +++ b/web/app/assets/javascripts/configureTrackDialog.js @@ -80,7 +80,8 @@ } function validateVoiceChatSettings() { - return voiceChatHelper.trySave(); + //return voiceChatHelper.trySave(); // not necessary since we use saveImmediate now + return true; } function showMusicAudioPanel() { @@ -117,7 +118,9 @@ }); $btnCancel.click(function() { - app.layout.closeDialog('configure-tracks') + if(voiceChatHelper.cancel()) { + app.layout.closeDialog('configure-tracks') + } return false; }); @@ -185,10 +188,12 @@ configureTracksHelper.reset(); voiceChatHelper.reset(); + voiceChatHelper.beforeShow(); } function afterHide() { + voiceChatHelper.beforeHide(); } @@ -212,11 +217,11 @@ $btnAddNewGear = $dialog.find('.btn-add-new-audio-gear'); $btnUpdateTrackSettings = $dialog.find('.btn-update-settings'); - configureTracksHelper = new JK.ConfigureTracksHelper(app); + configureTracksHelper = new context.JK.ConfigureTracksHelper(app); configureTracksHelper.initialize($dialog); - voiceChatHelper = new JK.VoiceChatHelper(app); - voiceChatHelper.initialize($dialog, false); + voiceChatHelper = new context.JK.VoiceChatHelper(app); + voiceChatHelper.initialize($dialog, 'configure_track_dialog', true, {vuType: "vertical", lightCount: 10, lightWidth: 3, lightHeight: 17}, 191); events(); } diff --git a/web/app/assets/javascripts/configureTracksHelper.js b/web/app/assets/javascripts/configureTracksHelper.js index a1f8d2b85..e2beb0656 100644 --- a/web/app/assets/javascripts/configureTracksHelper.js +++ b/web/app/assets/javascripts/configureTracksHelper.js @@ -22,25 +22,30 @@ var $instrumentsHolder = null; var isDragging = false; + function removeHoverer($hoverChannel) { + var $channel = $hoverChannel.data('original') + $channel.data('cloned', null); + $hoverChannel.remove(); + } + function hoverIn($channel) { if(isDragging) return; - $channel.css('color', 'white') - var $container = $channel.closest('.target'); var inTarget = $container.length > 0; if(!inTarget) { $container = $channel.closest('.channels-holder') } - $channel.data('container', $container) - $channel.addClass('hovering'); var $inputs = $container.find('.ftue-input'); var index = $inputs.index($channel); - $channel.css('background-color', '#333'); // $channel.css('padding', '0 5px'); if(inTarget) { + $channel.data('container', $container) + $channel.addClass('hovering'); + $channel.css('color', 'white') + $channel.css('background-color', '#333'); $channel.css('border', '#333'); $channel.css('border-radius', '2px'); $channel.css('min-width', '49%'); @@ -49,10 +54,44 @@ $container.css('overflow', 'visible'); } else { - // TODO: make the unassigned work - // $channel.css('min-width', $channel.css('width')); - // $channel.css('position', 'absolute'); - // $container.addClass('compensate'); + var $offsetParent = $channel.offsetParent(); + var parentOffset = $offsetParent.offset(); + + 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) @@ -69,6 +108,12 @@ } 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 .removeClass('hovering') .css('color', '') @@ -77,6 +122,8 @@ .css('width', '') .css('background-color', '') .css('padding', '') + .css('padding-left', '') + .css('background-position', '') .css('border', '') .css('border-radius', '') .css('right', '') @@ -88,9 +135,9 @@ //var $container = $channel.closest('.target'); var $container = $channel.data('container'); - $container.css('overflow', '') - $container.removeClass('compensate'); - + if($container) { + $container.css('overflow', '') + } } function fixClone($clone) { diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index a53939602..98b901626 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -13,7 +13,6 @@ var $draggingFader = null; var draggingOrientation = null; - var subscribers = {}; var logger = g.JK.logger; function faderClick(e) { @@ -21,7 +20,6 @@ var $fader = $(this); draggingOrientation = $fader.attr('orientation'); - var faderId = $fader.attr("fader-id"); var offset = $fader.offset(); var position = { top: e.pageY - offset.top, left: e.pageX - offset.left} @@ -31,12 +29,7 @@ return false; } - // Notify subscribers of value change - g._.each(subscribers, function (changeFunc, index, list) { - if (faderId === index) { - changeFunc(faderId, faderPct, false); - } - }); + $fader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: false}) setHandlePosition($fader, faderPct); return false; @@ -110,7 +103,6 @@ } function onFaderDrag(e, ui) { - var faderId = $draggingFader.attr("fader-id"); var faderPct = faderValue($draggingFader, e, ui.position); // protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows @@ -118,12 +110,7 @@ return false; } - // Notify subscribers of value change - g._.each(subscribers, function (changeFunc, index, list) { - if (faderId === index) { - changeFunc(faderId, faderPct, true); - } - }); + $draggingFader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: true}) } function onFaderDragStart(e, ui) { @@ -133,7 +120,6 @@ } function onFaderDragStop(e, ui) { - var faderId = $draggingFader.attr("fader-id"); var faderPct = faderValue($draggingFader, e, ui.position); // protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows @@ -142,12 +128,8 @@ return; } - // Notify subscribers of value change - g._.each(subscribers, function (changeFunc, index, list) { - if (faderId === index) { - changeFunc(faderId, faderPct, false); - } - }); + $draggingFader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: false}); + $draggingFaderHandle = null; $draggingFader = null; draggingOrientation = null; @@ -159,26 +141,15 @@ 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 * selector, with the provided options. */ renderFader: function (selector, userOptions) { + selector = $(selector); if (userOptions === undefined) { throw ("renderFader: userOptions is required"); } - if (!(userOptions.hasOwnProperty("faderId"))) { - throw ("renderFader: userOptions.faderId is required"); - } var renderDefaults = { faderType: "vertical", height: 83, // only used for vertical @@ -189,9 +160,9 @@ "#template-fader-h" : '#template-fader-v'; 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, start: onFaderDragStart, stop: onFaderDragStop, @@ -202,7 +173,7 @@ // Embed any custom styles, applied to the .fader below selector if ("style" in options) { for (var key in options.style) { - $(selector + ' .fader').css(key, options.style[key]); + selector.find(' .fader').css(key, options.style[key]); } } }, diff --git a/web/app/assets/javascripts/ftue.js b/web/app/assets/javascripts/ftue.js index 4215270e5..ec8d37e4e 100644 --- a/web/app/assets/javascripts/ftue.js +++ b/web/app/assets/javascripts/ftue.js @@ -110,11 +110,8 @@ }); } - function faderChange(faderId, newValue, dragging) { - var setFunction = faderMap[faderId]; - // TODO - using hardcoded range of -80 to 20 for output levels. - var mixerLevel = newValue - 80; // Convert our [0-100] to [-80 - +20] range - setFunction(mixerLevel); + function faderChange(e, data) { + } function setSaveButtonState($save, enabled) { @@ -1043,7 +1040,7 @@ context.JK.ftueVUCallback = function (dbValue, selector) { // Convert DB into a value from 0.0 - 1.0 var floatValue = (dbValue + 80) / 100; - context.JK.VuHelpers.updateVU(selector, floatValue); + context.JK.VuHelpers.updateVU($(selector), floatValue); }; context.JK.ftueAudioInputVUCallback = function (dbValue) { diff --git a/web/app/assets/javascripts/ga.js b/web/app/assets/javascripts/ga.js index bcbd8835c..4fe5e565d 100644 --- a/web/app/assets/javascripts/ga.js +++ b/web/app/assets/javascripts/ga.js @@ -85,10 +85,33 @@ 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 = { register : "Register", download : "DownloadClient", audioTest : "AudioTest", + audioTestData : 'AudioTestData', trackConfig : "AudioTrackConfig", networkTest : "NetworkTest", sessionCount : "SessionCount", @@ -105,7 +128,6 @@ jkFollow : 'jkFollow', jkFavorite : 'jkFavorite', jkComment : 'jkComment' - }; function translatePlatformForGA(platform) { @@ -289,18 +311,48 @@ 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) { var normalizedPlatform = translatePlatformForGA(platform); 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) { var normalizedPlatform = translatePlatformForGA(platform); 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 = {}; GA.Categories = categories; GA.SessionCreationTypes = sessionCreationTypes; @@ -310,11 +362,17 @@ GA.RecordingActions = recordingActions; GA.BandActions = bandActions; GA.JKSocialTargets = jkSocialTargets; + GA.AudioTestFailReasons = audioTestFailReasons; + GA.AudioTestDataReasons = audioTestDataReasons; + GA.NetworkTestFailReasons = networkTestFailReasons; GA.trackRegister = trackRegister; GA.trackDownload = trackDownload; GA.trackFTUECompletion = trackFTUECompletion; GA.trackNetworkTest = trackNetworkTest; + GA.trackNetworkTestFailure = trackNetworkTestFailure; GA.trackAudioTestCompletion = trackAudioTestCompletion; + GA.trackAudioTestFailure = trackAudioTestFailure; + GA.trackAudioTestData = trackAudioTestData; GA.trackConfigureTracksCompletion = trackConfigureTracksCompletion; GA.trackSessionCount = trackSessionCount; GA.trackSessionMusicians = trackSessionMusicians; diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 623670af6..86cf69dcf 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -31,6 +31,118 @@ 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, click here."}, // 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, click here." }, // 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, click here."}, // 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, click here."}, // 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, click here."}, // 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, click here." }, // 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, click here."}, // 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, click here." }, // 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, click here." }, // 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, click here."}, // 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, click here." }, // 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, click here."}, // 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, click here." }, // 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, click here."}, // 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, click here."}, // 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_OUTPUTS = 2; @@ -113,36 +225,42 @@ context.JK.AUDIO_DEVICE_BEHAVIOR = { MacOSX_builtin: { display: 'MacOSX Built-In', - videoURL: undefined, + shortName: 'Built-In', + videoURL: "https://www.youtube.com/watch?v=7-9PW50ygHk", showKnobs: false, showASIO: false }, MacOSX_interface: { display: 'MacOSX external interface', - videoURL: undefined, + shortName: 'External', + videoURL: "https://www.youtube.com/watch?v=7BLld6ogm14", showKnobs: false, showASIO: false }, Win32_wdm: { display: 'Windows WDM', - videoURL: undefined, + shortName : 'WDM', + videoURL: "https://www.youtube.com/watch?v=L36UBkAV14c", showKnobs: true, showASIO: false }, Win32_asio: { display: 'Windows ASIO', - videoURL: undefined, + shortName : 'ASIO', + videoURL: "https://www.youtube.com/watch?v=PGUmISTVVMY", showKnobs: false, showASIO: true }, Win32_asio4all: { display: 'Windows ASIO4ALL', - videoURL: undefined, + shortName : 'ASIO4ALL', + videoURL: "https://www.youtube.com/watch?v=PGUmISTVVMY", showKnobs: false, showASIO: true }, Linux: { display: 'Linux', + shortName : 'linux', videoURL: undefined, showKnobs: true, showASIO: false diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 59243a1af..07b38b036 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -718,10 +718,9 @@ /** check if the server is alive */ function serverHealthCheck(options) { - logger.debug("serverHealthCheck") return $.ajax({ type: "GET", - url: "/api/versioncheck" + url: "/api/healthcheck" }); } diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index 4891c3509..390beca45 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -94,22 +94,30 @@ 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() { - logger.debug("register for server_bad_state_error"); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_ERROR, serverBadStateError); } function registerBadStateRecovered() { - logger.debug("register for server_bad_state_recovered"); context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered); } function registerDownloadAvailable() { - logger.debug("register for download_available"); 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. */ @@ -330,6 +338,7 @@ registerBadStateRecovered(); registerBadStateError(); registerDownloadAvailable(); + registerDuplicateClientError(); context.JK.FaderHelpers.initialize(); context.window.onunload = this.unloadFunction; diff --git a/web/app/assets/javascripts/networkTest.js b/web/app/assets/javascripts/networkTestHelper.js similarity index 79% rename from web/app/assets/javascripts/networkTest.js rename to web/app/assets/javascripts/networkTestHelper.js index b9b8db12e..c93b5dce4 100644 --- a/web/app/assets/javascripts/networkTest.js +++ b/web/app/assets/javascripts/networkTestHelper.js @@ -34,6 +34,7 @@ var $currentScore = null; var $scoredClients = null; var $subscore = null; + var $watchVideo = null; var backendGuardTimeout = null; var serverClientId = ''; @@ -43,6 +44,12 @@ var $self = $(this); var scoringZoneWidth = 100;//px 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_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() { + trackedPass = false; + lastNetworkFailure = null; + resetTestState(); + } + + function resetTestState() { serverClientId = ''; scoring = false; numClientsToTest = STARTING_NUM_CLIENTS; @@ -77,6 +104,8 @@ $testText.empty(); $subscore.empty(); $currentScore.width(0); + bandwidthSamples = []; + } function renderStartTest() { @@ -107,6 +136,17 @@ return ''; } } + + function getLastNetworkFailure() { + return lastNetworkFailure; + } + + function storeLastNetworkFailure(reason, data) { + if(!trackedPass) { + lastNetworkFailure = {reason:reason, data:data}; + } + } + function testFinished() { var attempt = getCurrentAttempt(); @@ -123,7 +163,14 @@ if(!testSummary.final.num_clients) { testSummary.final.num_clients = attempt.num_clients; } - context.JK.GA.trackNetworkTest(context.JK.detectOS(), testSummary.final.num_clients); + + // context.jamClient.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); if(testSummary.final.num_clients == 2) { $testResults.addClass('acceptable'); @@ -136,49 +183,65 @@ else if(reason == "minimum_client_threshold") { context.jamClient.SetNetworkTestScore(0); renderStopTest('', "We're sorry, but your router and Internet service will not effectively support JamKazam sessions. Please click the HELP button for more information.") + 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); 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") { context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection."); renderStopTest('', ''); + storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if(reason == "remote_peer_cant_test") { context.JK.alertSupportedNeeded("The JamKazam service is experiencing technical difficulties."); 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') { context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties."); renderStopTest('', ''); + storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } 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.

Reason: " + attempt.backend_data.reason + '.'); renderStopTest('', ''); + storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if(reason == 'no_servers') { context.JK.alertSupportedNeeded("No network test servers are available." + appendContextualStatement()); renderStopTest('', ''); testedSuccessfully = true; + storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if(reason == 'no_network') { context.JK.Banner.showAlert("Please try again later. Your network appears down."); renderStopTest('', ''); + storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.noNetwork); } else if(reason == "rest_api_error") { context.JK.alertSupportedNeeded("Unable to acquire a network test server." + appendContextualStatement()); testedSuccessfully = true; renderStopTest('', ''); + storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } 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; renderStopTest('', ''); + storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else { context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection."); renderStopTest('', ''); + storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } numClientsToTest = STARTING_NUM_CLIENTS; @@ -204,8 +267,9 @@ } function cancel() { - clearBackendGuard(); + } + function clearBackendGuard() { if(backendGuardTimeout) { clearTimeout(backendGuardTimeout); @@ -213,6 +277,11 @@ } } + function setBackendGuard() { + clearBackendGuard(); + backendGuardTimeout = setTimeout(function(){backendTimedOut()}, (gon.ftue_network_test_duration + 1) * 1000); + } + function attemptTestPass() { var attempt = {}; @@ -230,7 +299,7 @@ updateProgress(0, false); - backendGuardTimeout = setTimeout(function(){backendTimedOut()}, (gon.ftue_network_test_duration + 1) * 1000); + setBackendGuard(); context.jamClient.TestNetworkPktBwRate(serverClientId, createSuccessCallbackName(), createTimeoutCallbackName(), NETWORK_TEST_TYPES.PktTest400LowLatency, @@ -262,7 +331,7 @@ } } - reset(); + resetTestState(); scoring = true; $self.triggerHandler(NETWORK_TEST_START); renderStartTest(); @@ -300,6 +369,7 @@ } function updateProgress(throughput, showSubscore) { + var width = throughput * 100; $currentScore.stop().data('showSubscore', showSubscore); @@ -322,9 +392,7 @@ ; } - function networkTestSuccess(data) { - clearBackendGuard(); - + function networkTestComplete(data) { var attempt = getCurrentAttempt(); function refineTest(up) { @@ -364,6 +432,8 @@ if(data.progress === true) { + setBackendGuard(); + var animate = true; if(data.downthroughput && data.upthroughput) { @@ -376,12 +446,14 @@ // take the lower var throughput= data.downthroughput < data.upthroughput ? data.downthroughput : data.upthroughput; + bandwidthSamples.push(data.upthroughput); + updateProgress(throughput, true); } } } else { - + clearBackendGuard(); logger.debug("network test pass success. data: ", data); if(data.reason == "unreachable") { @@ -390,6 +462,11 @@ attempt.reason = data.reason; 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") { // oops logger.debug("network test: internal_error (client had a unexpected problem)"); @@ -402,6 +479,11 @@ attempt.reason = data.reason; 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 { if(!data.downthroughput || !data.upthroughput) { // we have to assume this is bad. just not a reason we know about in code @@ -467,9 +549,17 @@ } 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) { $step = _$step; inGearWizard = _inGearWizard; @@ -487,15 +577,19 @@ $currentScore = $step.find('.current-score'); $scoredClients= $step.find('.scored-clients'); $subscore = $step.find('.subscore'); + $watchVideo = $step.find('.watch-video'); $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(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 } 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 } } @@ -505,6 +599,7 @@ this.initialize = initialize; this.reset = reset; this.cancel = cancel; + this.getLastNetworkFailure = getLastNetworkFailure; this.NETWORK_TEST_START = NETWORK_TEST_START; this.NETWORK_TEST_DONE = NETWORK_TEST_DONE; diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index b72fc9511..6f1cf5d95 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -30,7 +30,6 @@ var claimedRecording = null; var playbackControls = null; var promptLeave = false; - var backendMixerAlertThrottleTimer = null; var rateSessionDialog = null; var rest = context.JK.Rest(); @@ -76,60 +75,6 @@ "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, click here."}, // 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, click here." }, // 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, click here."}, // 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, click here."}, // 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, click here."}, // 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, click here." }, // 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, click here."}, // 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, click here." }, // 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, click here." }, // 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, click here."}, // 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, click here." }, // 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, click here."}, // 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, click here." }, // 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, click here."}, // 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, click here."}, // 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) { sessionId = data.id; promptLeave = true; @@ -144,114 +89,6 @@ 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() { // indicate that the screen is active, so that // body-scoped drag handlers can go active @@ -327,7 +164,8 @@ context.JK.CurrentSessionModel = sessionModel = new context.JK.SessionModel( context.JK.app, context.JK.JamServer, - context.jamClient + context.jamClient, + self ); $(sessionModel.recordingModel) @@ -653,7 +491,8 @@ } }); var faderId = mixerIds.join(','); - $('#volume').attr('mixer-id', faderId); + var $volume = $('#volume'); + $volume.attr('mixer-id', faderId); var faderOpts = { faderId: faderId, faderType: "horizontal", @@ -664,8 +503,9 @@ "height": "24px" } }; - context.JK.FaderHelpers.renderFader("#volume", faderOpts); - context.JK.FaderHelpers.subscribe(faderId, faderChanged); + context.JK.FaderHelpers.renderFader($volume, faderOpts); + + $volume.on('fader_change', faderChanged); // Visually update fader to underlying mixer start value. // Always do this, even if gainPercent is zero. @@ -699,8 +539,8 @@ "height": "24px" } }; - context.JK.FaderHelpers.renderFader(faderId, faderOpts); - context.JK.FaderHelpers.subscribe(faderId, l2mChanged); + context.JK.FaderHelpers.renderFader($mixSlider, faderOpts); + $mixSlider.on('fader_change', l2mChanged); var value = context.jamClient.SessionGetMasterLocalMix(); context.JK.FaderHelpers.setFaderValue(faderId, percentFromMixerValue(-80, 20, value)); @@ -709,9 +549,9 @@ /** * 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); - context.jamClient.SessionSetMasterLocalMix(newValue - 80); + context.jamClient.SessionSetMasterLocalMix(data.percentage - 80); } function _addVoiceChat() { @@ -723,7 +563,8 @@ var $voiceChat = $('#voice-chat'); $voiceChat.show(); $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( mixer.range_low, mixer.range_high, mixer.volume_left); var faderOpts = { @@ -731,12 +572,11 @@ faderType: "horizontal", width: 50 }; - context.JK.FaderHelpers.renderFader("#voice-chat .voicechat-gain", faderOpts); - context.JK.FaderHelpers.subscribe(mixer.id, faderChanged); + context.JK.FaderHelpers.renderFader($voiceChatGain, faderOpts); + $voiceChatGain.on('fader_change', faderChanged); context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent); if (mixer.mute) { - var $mute = $voiceChat.find('.voicechat-mute'); - _toggleVisualMuteControl($mute, true); + _toggleVisualMuteControl($voiceChatMute, true); } } }); @@ -945,16 +785,17 @@ var vuLeftSelector = trackSelector + " .track-vu-left"; var vuRightSelector = trackSelector + " .track-vu-right"; var faderSelector = trackSelector + " .track-gain"; + var $fader = $(faderSelector).attr('mixer-id', mixerId); var $track = $(trackSelector); // Set mixer-id attributes and render VU/Fader context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts); $track.find('.track-vu-left').attr('mixer-id', mixerId + '_vul'); context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts); $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 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 @@ -976,6 +817,8 @@ ], usedMixers); if (mixer) { + var participant = (sessionModel.getParticipant(clientId) || {name:'unknown'}).name; + logger.debug("found mixer=" + mixer.id + ", participant=" + participant) usedMixers[mixer.id] = true; keysToDelete.push(key); var gainPercent = percentFromMixerValue( @@ -998,6 +841,8 @@ $('.disabled-track-overlay', $track).show(); $('.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 (mixerId.substr(-4) === "_vul") { // Do the left - selector = '#tracks [mixer-id="' + pureMixerId + '_vul"]'; + selector = $('#tracks [mixer-id="' + pureMixerId + '_vul"]'); context.JK.VuHelpers.updateVU(selector, value); // Do the right - selector = '#tracks [mixer-id="' + pureMixerId + '_vur"]'; + selector = $('#tracks [mixer-id="' + pureMixerId + '_vur"]'); context.JK.VuHelpers.updateVU(selector, value); } // otherwise, it's a mono track, _vur event - ignore. } else { // stereo track - selector = '#tracks [mixer-id="' + mixerId + '"]'; + selector = $('#tracks [mixer-id="' + mixerId + '"]'); context.JK.VuHelpers.updateVU(selector, value); } } @@ -1102,12 +947,14 @@ * 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. */ - function faderChanged(faderId, newValue, dragging) { + function faderChanged(e, data) { + var $target = $(this); + var faderId = $target.attr('mixer-id'); var mixerIds = faderId.split(','); $.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); - setMixerVolume(v, newValue); + setMixerVolume(v, data.percentage); }); } @@ -1568,7 +1415,6 @@ context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback; context.JK.HandleBridgeCallback = handleBridgeCallback; - context.JK.AlertCallback = alertCallback; }; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 16898fad0..0adb1fdc9 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -7,7 +7,10 @@ context.JK = context.JK || {}; 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 currentSessionId = null; // Set on join, prior to setting currentSession. var currentSession = null; @@ -19,6 +22,7 @@ var pendingSessionRefresh = false; var recordingModel = new context.JK.RecordingModel(app, this, rest, context.jamClient); 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 var participantsEverSeen = {}; var $self = $(this); @@ -29,6 +33,10 @@ return currentSession ? currentSession.id : null; } + function inSession() { + return !!currentSessionId; + } + function participants() { if (currentSession) { return currentSession.participants; @@ -131,7 +139,7 @@ // 'unregister' for callbacks context.jamClient.SessionRegisterCallback(""); - context.jamClient.SessionSetAlertCallback(""); + //context.jamClient.SessionSetAlertCallback(""); context.jamClient.SessionSetConnectionStatusRefreshRate(0); updateCurrentSession(null); $(document).trigger('jamkazam.session_stopped', {session: {id: currentSessionId}}); @@ -214,6 +222,12 @@ * the provided callback when complete. */ function refreshCurrentSessionRest(callback, force) { + + if(!inSession()) { + logger.debug("refreshCurrentSession skipped: ") + return; + } + var url = "/api/sessions/" + currentSessionId; if(requestingSessionRefresh) { // 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); } }, - 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() { requestingSessionRefresh = false; if(pendingSessionRefresh) { @@ -411,6 +432,73 @@ 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 this.id = id; this.recordedTracks = recordedTracks; @@ -425,6 +513,12 @@ this.onWebsocketDisconnected = onWebsocketDisconnected; this.recordingModel = recordingModel; this.findUserBy = findUserBy; + + // ALERT HANDLERS + this.onBackendMixerChanged = onBackendMixerChanged; + this.onDeadUserRemove = onDeadUserRemove; + this.onWindowBackgrounded = onWindowBackgrounded; + this.getCurrentSession = function() { return currentSession; }; diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index bee2195ac..31ecbde41 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -9,6 +9,9 @@ var logger = context.JK.logger; 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", "Wed", "Thu", "Fri", "Sat"); @@ -448,6 +451,20 @@ $('.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 * to the id_field and the option text to text_field. It will preselect the option with @@ -529,8 +546,8 @@ return ret; } - context.JK.alertSupportedNeeded = function(additionalContext) { - var $item = context.JK.Banner.showAlert(additionalContext + ' Please contact support'); + context.JK.alertSupportedNeeded = function(additionalContext) { + var $item = context.JK.Banner.showAlert(additionalContext + '

Please contact support.'); context.JK.popExternalLinks($item); return $item; } diff --git a/web/app/assets/javascripts/voiceChatHelper.js b/web/app/assets/javascripts/voiceChatHelper.js index 55b670e8d..e78aa00cb 100644 --- a/web/app/assets/javascripts/voiceChatHelper.js +++ b/web/app/assets/javascripts/voiceChatHelper.js @@ -5,6 +5,7 @@ context.JK = context.JK || {}; context.JK.VoiceChatHelper = function (app) { var logger = context.JK.logger; + var ALERT_NAMES = context.JK.ALERT_NAMES; var ASSIGNMENT = context.JK.ASSIGNMENT; var VOICE_CHAT = context.JK.VOICE_CHAT; var MAX_TRACKS = context.JK.MAX_TRACKS; @@ -18,10 +19,19 @@ var $chatInputs = null; var $templateChatInput = null; var $selectedChatInput = null;// should only be used if isChatEnabled = true - var saveImmediate = null; // if true, then every action by the user results in a save to the backend immediately, false means you have to call trySave to persist + 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) 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() { suppressChange(function(){ @@ -34,6 +44,48 @@ 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) { $selectedChatInput = null; @@ -62,24 +114,36 @@ $selectedChatInput = $chatInput; $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); }); var $radioButtons = $chatInputs.find('input[name="chat-device"]'); context.JK.checkbox($radioButtons).on('ifChecked', function(e) { + userInteracted(); var $input = $(e.currentTarget); $selectedChatInput = $input; // for use in handleNext if(saveImmediate) { var channelId = $input.attr('data-channel-id'); - context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT); + lastSavedTime = new Date(); + context.jamClient.TrackSetChatInput(channelId); + lastSavedTime = new Date(); + //context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT); var result = context.jamClient.TrackSaveAssignments(); if(!result || result.length == 0) { // success } else { - context.JK.Banner.showAlert('Unable to save assignments. ' + result); + 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; } } @@ -89,18 +153,17 @@ disableChatButtonsUI(); } - $chatInputs.find('.chat-input').show().on('click', function() { - // obnoxious; remove soon XXX - // if(!isChatEnabled()) { - // context.JK.prodBubble($parent.find('.use-chat-input h3'), 'chat-not-enabled', {}, { positions:['left']}); - // } - }) + renderVolumes(); + + startingState = getCurrentState(); } function disableChatButtonsUI() { var $radioButtons = $chatInputs.find('input[name="chat-device"]'); $radioButtons.iCheck('disable') $chatInputs.addClass('disabled'); + $radioButtons.iCheck('uncheck'); + $selectedChatInput = null; } function enableChatButtonsUI() { @@ -133,6 +196,7 @@ var result = context.jamClient.TrackSaveAssignments(); if(!result || result.length == 0) { + renderNoVolume(); // success suppressChange(function() { $reuseAudioInputRadio.iCheck('check').attr('checked', 'checked'); @@ -151,7 +215,7 @@ $useChatInputRadio.removeAttr('checked'); }) } - disableChatButtonsUI() + disableChatButtonsUI(); } function enableChat(applyToBackend) { @@ -181,6 +245,7 @@ } enableChatButtonsUI(); + renderVolumes(); } function handleChatEnabledToggle() { @@ -191,8 +256,18 @@ $reuseAudioInputRadio.closest('.iradio_minimal').css('position', 'absolute'); $useChatInputRadio.closest('.iradio_minimal').css('position', 'absolute'); - $reuseAudioInputRadio.on('ifChecked', function() { if(!ignoreICheckEvent) disableChat(true) }); - $useChatInputRadio.on('ifChecked', function() { if(!ignoreICheckEvent) enableChat(true) }); + $reuseAudioInputRadio.on('ifChecked', function() { + if(!ignoreICheckEvent) { + userInteracted(); + disableChat(true); + } + }); + $useChatInputRadio.on('ifChecked', function() { + if(!ignoreICheckEvent) { + userInteracted(); + enableChat(true) + } + }); } // gets the state of the UI @@ -208,9 +283,17 @@ 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) { logger.debug("enabling chat. chat_channel=" + state.chat_channel); @@ -231,6 +314,9 @@ if(!result || result.length == 0) { // success + if(!state.enabled) { + renderNoVolume(); + } return true; } 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; saveImmediate = _saveImmediate; - + vuOptions = _vuOptions; + faderHeight = _faderHeight; $reuseAudioInputRadio = $parent.find('.reuse-audio-input input'); $useChatInputRadio = $parent.find('.use-chat-input input'); $chatInputs = $parent.find('.chat-inputs'); $templateChatInput = $('#template-chat-input'); + $voiceChatVuLeft = $parent.find('.voice-chat-vu-left'); + $voiceChatVuRight = $parent.find('.voice-chat-vu-right'); + $voiceChatFader = $parent.find('.chat-fader') 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.trySave = trySave; + this.cancel = cancel; this.initialize = initialize; + this.beforeShow = beforeShow; + this.beforeHide = beforeHide; return this; }; diff --git a/web/app/assets/javascripts/vuHelpers.js b/web/app/assets/javascripts/vuHelpers.js index c63567321..93b34c4f2 100644 --- a/web/app/assets/javascripts/vuHelpers.js +++ b/web/app/assets/javascripts/vuHelpers.js @@ -19,6 +19,7 @@ * vuType can be either "horizontal" or "vertical" */ renderVU: function(selector, userOptions) { + selector = $(selector); /** * The default options for rendering a VU */ @@ -35,25 +36,25 @@ templateSelector = "#template-vu-h"; } 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 * 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 // light based on the incoming value. var countSelector = 'tr'; - var horizontal = ($('table.horizontal', selector).length); + var horizontal = ($selector.find('table.horizontal').length); if (horizontal) { countSelector = 'td'; } - var lightCount = $(countSelector, selector).length; + var lightCount = $selector.find(countSelector).length; var i = 0; var state = 'on'; var lights = Math.round(value * lightCount); @@ -61,15 +62,15 @@ var $light = null; var colorClass = 'vu-green-'; - var lightSelectorPrefix = selector + ' td.vu'; + var lightSelectorPrefix = $selector.find('td.vu'); var thisLightSelector = null; // 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'); // Set the lights - for (i=0; i= redSwitch) { @@ -78,9 +79,9 @@ if (i >= lights) { state = 'off'; } - thisLightSelector = lightSelectorPrefix + i; - $light = $(thisLightSelector); - $light.addClass(colorClass + state); + + var lightIndex = horizontal ? i : lightCount - i - 1; + allLightsSelector.eq(lightIndex).addClass(colorClass + state); } } diff --git a/web/app/assets/javascripts/wizard/gear/gear_wizard.js b/web/app/assets/javascripts/wizard/gear/gear_wizard.js index 7fdddf1e6..2a052d2a9 100644 --- a/web/app/assets/javascripts/wizard/gear/gear_wizard.js +++ b/web/app/assets/javascripts/wizard/gear/gear_wizard.js @@ -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() { + reportFailureAnalytics(); + if (app.cancelFtue) { app.cancelFtue(); app.afterFtue = null; @@ -116,6 +139,8 @@ } function onClosed() { + reportFailureAnalytics(); + if (app.afterFtue) { // If there's a function to invoke, invoke it. app.afterFtue(); diff --git a/web/app/assets/javascripts/wizard/gear/step_configure_voice_chat.js b/web/app/assets/javascripts/wizard/gear/step_configure_voice_chat.js index df656b42a..98e189dea 100644 --- a/web/app/assets/javascripts/wizard/gear/step_configure_voice_chat.js +++ b/web/app/assets/javascripts/wizard/gear/step_configure_voice_chat.js @@ -28,10 +28,14 @@ var forceDisabledChat = firstTimeShown; voiceChatHelper.reset(forceDisabledChat); - + voiceChatHelper.beforeShow(); firstTimeShown = false; } + function beforeHide() { + voiceChatHelper.beforeHide(); + } + function handleNext() { return true; } @@ -39,13 +43,14 @@ function initialize(_$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.newSession = newSession; this.beforeShow = beforeShow; + this.beforeHide = beforeHide; this.initialize = initialize; return this; diff --git a/web/app/assets/javascripts/wizard/gear/step_direct_monitoring.js b/web/app/assets/javascripts/wizard/gear/step_direct_monitoring.js index c1cb4f514..aeb79bec3 100644 --- a/web/app/assets/javascripts/wizard/gear/step_direct_monitoring.js +++ b/web/app/assets/javascripts/wizard/gear/step_direct_monitoring.js @@ -5,6 +5,7 @@ context.JK = context.JK || {}; context.JK.StepDirectMonitoring = function (app) { + var logger = context.JK.logger; var $step = null; var $directMonitoringBtn = null; var isPlaying = false; @@ -58,6 +59,7 @@ function beforeShow() { context.jamClient.SessionRemoveAllPlayTracks(); + logger.debug("adding test sound"); if(!context.jamClient.SessionAddPlayTrack("skin:jktest-audio.wav")) { context.JK.alertSupportedNeeded('Unable to open test sound'); } @@ -68,6 +70,7 @@ stopPlay(); } + logger.debug("removing test sound") context.jamClient.SessionRemoveAllPlayTracks(); if(playCheckInterval) { diff --git a/web/app/assets/javascripts/wizard/gear/step_network_test.js b/web/app/assets/javascripts/wizard/gear/step_network_test.js index 030441276..4b8df386e 100644 --- a/web/app/assets/javascripts/wizard/gear/step_network_test.js +++ b/web/app/assets/javascripts/wizard/gear/step_network_test.js @@ -11,6 +11,10 @@ var $step = null; + function getLastNetworkFailAnalytics() { + return networkTest.getLastNetworkFailure(); + } + function networkTestDone() { updateButtons(); } @@ -62,6 +66,7 @@ this.beforeHide = beforeHide; this.beforeShow = beforeShow; this.initialize = initialize; + this.getLastNetworkFailAnalytics = getLastNetworkFailAnalytics; return this; } diff --git a/web/app/assets/javascripts/wizard/gear/step_select_gear.js b/web/app/assets/javascripts/wizard/gear/step_select_gear.js index 4efede27e..d0c5b10a2 100644 --- a/web/app/assets/javascripts/wizard/gear/step_select_gear.js +++ b/web/app/assets/javascripts/wizard/gear/step_select_gear.js @@ -5,6 +5,7 @@ context.JK = context.JK || {}; context.JK.StepSelectGear = function (app, dialog) { + var ALERT_NAMES = context.JK.ALERT_NAMES; var EVENTS = context.JK.EVENTS; var ASSIGNMENT = context.JK.ASSIGNMENT; var VOICE_CHAT = context.JK.VOICE_CHAT; @@ -19,6 +20,10 @@ var gearTest = new context.JK.GearTest(app); 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 $watchVideoOutput = null; var $audioInput = null; @@ -135,17 +140,11 @@ if(!audioInputDeviceId || audioInputDeviceId == '') { 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.'); - return false; - } - - 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 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.'); } - // 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; } @@ -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 var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked'); var $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)'); @@ -435,10 +448,19 @@ function initializeASIOButtons() { $asioInputControlBtn.unbind('click').click(function () { + if(gearTest.isScoring()) { + return false; + } context.jamClient.FTUEOpenControlPanel(selectedAudioInput()); + return false; }); $asioOutputControlBtn.unbind('click').click(function () { + if(gearTest.isScoring()) { + return false; + } + context.jamClient.FTUEOpenControlPanel(selectedAudioOutput()); + return false; }); } @@ -715,7 +737,7 @@ // * reuse IO score if it was good/acceptable // * rescore IO if it was bad or skipped from previous try function attemptScore(refocused) { - gearTest.attemptScore(refocused); + gearTest.attemptScore(selectedDeviceInfo, refocused); } function initializeAudioInputChanged() { @@ -735,9 +757,42 @@ 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) { renderScoringStopped(); 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() { @@ -775,8 +830,10 @@ return false; } else { - savedProfile = true; context.JK.GA.trackAudioTestCompletion(context.JK.detectOS()); + trackedPass = true; + lastFailureAnalytics = null; + savedProfile = true; return true; } } @@ -800,14 +857,21 @@ initializeResync(); } + function beforeWizardShow() { + lastFailureAnalytics = null; + trackedPass = false; + } + function beforeShow() { $(window).on('focus', onFocus); initializeNextButtonState(); + context.JK.onBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'gear_test', gearTest.onInvalidAudioDevice); } function beforeHide() { logger.debug("unregistering focus watch") $(window).off('focus', onFocus); + context.JK.offBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'gear_test', gearTest.onInvalidAudioDevice); } function resetState() { @@ -849,12 +913,15 @@ .on(gearTest.GEAR_TEST_START, onGearTestStarted) .on(gearTest.GEAR_TEST_DONE, onGearTestDone) .on(gearTest.GEAR_TEST_FAIL, onGearTestFail) + .on(gearTest.GEAR_TEST_INVALIDATED_ASYNC, onGearTestInvalidated) } + this.getLastAudioTestFailAnalytics = getLastAudioTestFailAnalytics; this.handleNext = handleNext; this.newSession = newSession; this.beforeShow = beforeShow; this.beforeHide = beforeHide; + this.beforeWizardShow = beforeWizardShow; this.initialize = initialize; self = this; diff --git a/web/app/assets/javascripts/wizard/gear_test.js b/web/app/assets/javascripts/wizard/gear_test.js index e84e85454..cb9b01355 100644 --- a/web/app/assets/javascripts/wizard/gear_test.js +++ b/web/app/assets/javascripts/wizard/gear_test.js @@ -14,6 +14,11 @@ var validIOScore = false; var latencyScore = 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 $ioHeader = null; @@ -31,6 +36,7 @@ var $ioScoreSection = null; var $latencyScoreSection = null; + var $self = $(this); var GEAR_TEST_START = "gear_test.start"; @@ -41,9 +47,10 @@ var GEAR_TEST_DONE = "gear_test.done"; var GEAR_TEST_FAIL = "gear_test.fail"; 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() { - return validLatencyScore && validIOScore; + return validLatencyScore && validIOScore && !asynchronousInvalidDevice; } function processIOScore(io) { @@ -93,13 +100,15 @@ $self.triggerHandler(GEAR_TEST_DONE) } 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() { 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); + lastSavedTime = new Date(); if(result && result != "") { logger.debug("unable to FTUESave(false). reason=" + result); return false; @@ -125,12 +134,14 @@ // on refocus=true: // * reuse IO score if it was good/acceptable // * rescore IO if it was bad or skipped from previous try - function attemptScore(refocused) { + function attemptScore(_selectedDeviceInfo, refocused) { if(scoring) { logger.debug("gear-test: already scoring"); return; } + selectedDeviceInfo = _selectedDeviceInfo; scoring = true; + asynchronousInvalidDevice = false; $self.triggerHandler(GEAR_TEST_START); $self.triggerHandler(GEAR_TEST_LATENCY_START); validLatencyScore = false; @@ -197,7 +208,7 @@ } else { scoring = false; - $self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency'}) + $self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', latencyScore: latencyScore.latency}) } }) }, 250); @@ -276,6 +287,18 @@ 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() { $loopbackCompleted.show(); } @@ -301,6 +324,7 @@ function invalidateScore() { validLatencyScore = false; validIOScore = false; + asynchronousInvalidDevice = false; resetScoreReport(); } @@ -337,6 +361,18 @@ $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) { if(!$testResults.is('.ftue-box.results')) { @@ -394,6 +430,9 @@ function onGearTestDone(e, data) { $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()}); } @@ -405,6 +444,21 @@ } 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 @@ -445,6 +499,7 @@ this.GEAR_TEST_DONE = GEAR_TEST_DONE; this.GEAR_TEST_FAIL = GEAR_TEST_FAIL; this.GEAR_TEST_IO_PROGRESS = GEAR_TEST_IO_PROGRESS; + this.GEAR_TEST_INVALIDATED_ASYNC = GEAR_TEST_INVALIDATED_ASYNC; this.initialize = initialize; this.isScoring = isScoring; @@ -457,6 +512,8 @@ this.isGoodFtue = isGoodFtue; this.getLatencyScore = getLatencyScore; this.getIOScore = getIOScore; + this.getLastSavedTime = getLastSavedTime; + this.onInvalidAudioDevice = onInvalidAudioDevice; return this; } diff --git a/web/app/assets/javascripts/wizard/gear_utils.js b/web/app/assets/javascripts/wizard/gear_utils.js index dd5d52ed2..30ba2e686 100644 --- a/web/app/assets/javascripts/wizard/gear_utils.js +++ b/web/app/assets/javascripts/wizard/gear_utils.js @@ -192,7 +192,6 @@ rest.createDiagnostic({ type: 'GEAR_SELECTION', data: { - logs: logger.logCache, client_type: context.JK.clientType(), client_id: context.JK.JamServer.clientID, @@ -249,7 +248,6 @@ } }) - logger.debug("chatInputs:", chatInputs) return chatInputs; } diff --git a/web/app/assets/javascripts/wizard/loopback/step_loopback_test.js b/web/app/assets/javascripts/wizard/loopback/step_loopback_test.js index be6843dad..d22732519 100644 --- a/web/app/assets/javascripts/wizard/loopback/step_loopback_test.js +++ b/web/app/assets/javascripts/wizard/loopback/step_loopback_test.js @@ -28,6 +28,12 @@ var $outputChannels = null; var $templateAudioPort = null; var $scoreReport = null; + var $audioInputVuLeft = null; + var $audioInputVuRight = null; + var $audioInputFader = null; + var $audioOutputVuLeft = null; + var $audioOutputVuRight = null; + var $audioOutputFader = null; var faderMap = { 'loopback-audio-input-fader': jamClient.FTUESetInputVolume, @@ -42,7 +48,7 @@ function attemptScore() { - gearTest.attemptScore(); + gearTest.attemptScore(selectedDeviceInfo); } @@ -220,43 +226,45 @@ } function initializeVUMeters() { - var vuMeters = [ - '#loopback-audio-input-vu-left', - '#loopback-audio-input-vu-right', - '#loopback-audio-output-vu-left', - '#loopback-audio-output-vu-right' - ]; - $.each(vuMeters, function () { - context.JK.VuHelpers.renderVU(this, - {vuType: "horizontal", lightCount: 12, lightWidth: 15, lightHeight: 3}); - }); + var vuOptions = {vuType: "horizontal", lightCount: 12, lightWidth: 15, lightHeight: 3}; - var faders = context._.keys(faderMap); - $.each(faders, function () { - var fid = this; - context.JK.FaderHelpers.renderFader('#' + fid, - {faderId: fid, faderType: "horizontal", width: 163}); - context.JK.FaderHelpers.subscribe(fid, faderChange); - }); + context.JK.VuHelpers.renderVU($audioInputVuLeft, vuOptions); + context.JK.VuHelpers.renderVU($audioInputVuRight, vuOptions); + + context.JK.VuHelpers.renderVU($audioOutputVuLeft, vuOptions); + context.JK.VuHelpers.renderVU($audioOutputVuRight, vuOptions); + + 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 function renderVolumes() { - $.each(context._.keys(faderReadMap), function (index, faderId) { - // faderChange takes a value from 0-100 - var $fader = $('[fader-id="' + faderId + '"]'); - var db = faderReadMap[faderId](); - var faderPct = db + 80; - context.JK.FaderHelpers.setHandlePosition($fader, faderPct); - }); + // input + var $inputFader = $audioInputFader.find('[control="fader"]'); + 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) { - 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 inputFaderChange(e, data) { + var mixerLevel = data.percentage - 80; // Convert our [0-100] to [-80 - +20] range + context.jamClient.FTUESetInputVolume(mixerLevel); + } + + function outputFaderChange(e, data) { + var mixerLevel = data.percentage - 80; + context.jamClient.FTUESetOutputVolume(mixerLevel); } function registerVuCallbacks() { @@ -329,6 +337,13 @@ $outputChannels = $step.find('.output-ports') $templateAudioPort = $('#template-audio-port'); $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(); frameBuffers.initialize($step.find('.frame-and-buffers')); @@ -348,18 +363,19 @@ initializeASIOButtons(); initializeResync(); - } - context.JK.loopbackAudioInputVUCallback = function (dbValue) { - context.JK.ftueVUCallback(dbValue, '#loopback-audio-input-vu-left'); - context.JK.ftueVUCallback(dbValue, '#loopback-audio-input-vu-right'); - }; - context.JK.loopbackAudioOutputVUCallback = function (dbValue) { - context.JK.ftueVUCallback(dbValue, '#loopback-audio-output-vu-left'); - context.JK.ftueVUCallback(dbValue, '#loopback-audio-output-vu-right'); - }; - context.JK.loopbackChatInputVUCallback = function (dbValue) { - }; + + context.JK.loopbackAudioInputVUCallback = function (dbValue) { + context.JK.ftueVUCallback(dbValue, $audioInputVuLeft); + context.JK.ftueVUCallback(dbValue, $audioInputVuRight); + }; + context.JK.loopbackAudioOutputVUCallback = function (dbValue) { + context.JK.ftueVUCallback(dbValue, $audioOutputVuLeft); + context.JK.ftueVUCallback(dbValue, $audioOutputVuRight); + }; + context.JK.loopbackChatInputVUCallback = function (dbValue) { + }; + } this.getGearTest = getGearTest; this.handleNext = handleNext; diff --git a/web/app/assets/javascripts/wizard/wizard.js b/web/app/assets/javascripts/wizard/wizard.js index 8005930c2..3d8bf4307 100644 --- a/web/app/assets/javascripts/wizard/wizard.js +++ b/web/app/assets/javascripts/wizard/wizard.js @@ -139,6 +139,13 @@ 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; previousStep = null; diff --git a/web/app/assets/stylesheets/client/configureTracksDialog.css.scss b/web/app/assets/stylesheets/client/configureTracksDialog.css.scss index 18ad08458..46b1bd830 100644 --- a/web/app/assets/stylesheets/client/configureTracksDialog.css.scss +++ b/web/app/assets/stylesheets/client/configureTracksDialog.css.scss @@ -38,7 +38,7 @@ float:left; vertical-align:top; @include border_box_sizing; - padding: 20px 20px 0 0; + padding: 0 20px 0 0; } .sub-column { @@ -48,6 +48,25 @@ @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"] { .column { &:nth-of-type(1) { diff --git a/web/app/assets/stylesheets/client/dragDropTracks.css.scss b/web/app/assets/stylesheets/client/dragDropTracks.css.scss index a85a659a0..986f97236 100644 --- a/web/app/assets/stylesheets/client/dragDropTracks.css.scss +++ b/web/app/assets/stylesheets/client/dragDropTracks.css.scss @@ -27,10 +27,16 @@ } } + .channels-holder { + .ftue-input { + background-position:4px 6px; + padding-left:14px; + } + } .unassigned-input-channels { - min-height: 22px; + min-height: 240px; overflow-y: auto; - max-height:23%; + max-height:93%; //padding-right:18px; // to keep draggables off of scrollbar. maybe necessary @@ -39,6 +45,7 @@ } } .ftue-input { + } &.drag-in-progress { @@ -84,6 +91,7 @@ font-size: 12px; cursor: move; padding: 4px; + padding-left:10px; border: solid 1px #999; margin-bottom: 15px; white-space: nowrap; @@ -91,6 +99,9 @@ text-overflow: ellipsis; text-align: left; line-height:20px; + background-image:url('/assets/content/icon_drag_handle.png'); + background-position:0 3px; + background-repeat:no-repeat; //direction: rtl; &.ui-draggable-dragging { margin-bottom: 0; @@ -126,10 +137,13 @@ .ftue-input { padding: 0; + padding-left:10px; border: 0; margin-bottom: 0; &.ui-draggable-dragging { padding: 4px; + padding-left:14px; + background-position:4px 6px; border: solid 1px #999; overflow: visible; } @@ -163,7 +177,7 @@ }*/ } &:nth-of-type(2) { - padding-left:2px; + padding-left:10px; float: right; } } diff --git a/web/app/assets/stylesheets/client/recordingFinishedDialog.css.scss b/web/app/assets/stylesheets/client/recordingFinishedDialog.css.scss index a96bfd3d5..a738fa57b 100644 --- a/web/app/assets/stylesheets/client/recordingFinishedDialog.css.scss +++ b/web/app/assets/stylesheets/client/recordingFinishedDialog.css.scss @@ -9,8 +9,14 @@ display:inline; } - .recording-controls { - position:relative; + .recording-position { + margin: 5px 0 0 -15px; + width: 95%; + display:inline-block; + } + + .recording-current { + position:absolute; } .icheckbuttons { diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index 302d345b7..9444d015f 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -40,6 +40,31 @@ top:85px; 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 { display:block; } @@ -624,16 +637,6 @@ table.vu td { 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 { display:inline-block; height:16px; @@ -672,6 +675,22 @@ table.vu td { font-size:18px; } +.recording-controls { + display:none; + + .play-button { + outline:none; + } + + .play-button img.pausebutton { + display:none; + } +} + +.playback-mode-buttons { + display:none; +} + .currently-recording { background-color: $ColorRecordingBackground; } diff --git a/web/app/assets/stylesheets/client/voiceChatHelper.css.scss b/web/app/assets/stylesheets/client/voiceChatHelper.css.scss index 8fd6b3e81..deefe77c3 100644 --- a/web/app/assets/stylesheets/client/voiceChatHelper.css.scss +++ b/web/app/assets/stylesheets/client/voiceChatHelper.css.scss @@ -1,8 +1,47 @@ +@import "client/common.css.scss"; +@charset "UTF-8"; + .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 { position: relative; + float:left; + width:50%; + height:109px; div { @@ -33,14 +72,23 @@ } } - .ftue-box { + .vu-meter { + @include border_box_sizing; + float:left; + } + + .ftue-box { + @include border_box_sizing; background-color: #222222; font-size: 13px; padding: 8px; + &.chat-inputs { - height: 230px !important; + //height: 230px !important; overflow: auto; color:white; + float:left; + &.disabled { color:gray; @@ -50,6 +98,7 @@ display: inline-block; height: 32px; vertical-align: middle; + line-height:20px; } .chat-input { @@ -60,5 +109,7 @@ } } } + + } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/wizard/gearResults.css.scss b/web/app/assets/stylesheets/client/wizard/gearResults.css.scss index 44be59140..36393b0c5 100644 --- a/web/app/assets/stylesheets/client/wizard/gearResults.css.scss +++ b/web/app/assets/stylesheets/client/wizard/gearResults.css.scss @@ -99,6 +99,7 @@ } ul.results-text { + margin-left:-5px; padding: 10px 8px; li { diff --git a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss index ecabff486..bf3910f36 100644 --- a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss @@ -78,7 +78,7 @@ } &.instructions { - height: 268px !important; + height: 248px !important; line-height: 16px; @include border_box_sizing; @@ -203,14 +203,40 @@ width: 25%; &:nth-of-type(2) { - width: 50%; + width: 75%; } } + .instructions { + height: 268px !important; + } + .watch-video { 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"] { diff --git a/web/app/assets/stylesheets/client/wizard/loopbackWizard.css.scss b/web/app/assets/stylesheets/client/wizard/loopbackWizard.css.scss index 1be48b04d..225a60c91 100644 --- a/web/app/assets/stylesheets/client/wizard/loopbackWizard.css.scss +++ b/web/app/assets/stylesheets/client/wizard/loopbackWizard.css.scss @@ -74,7 +74,7 @@ } .ftue-controls { - margin-top: 16px; + margin-top: 10px; position:relative; height: 48px; width: 220px; @@ -101,11 +101,12 @@ } .ports-header { - margin-top:20px; + margin-top:10px; } .ports { height:100px; + overflow: auto; } .asio-settings-input-btn, .asio-settings-output-btn { @@ -114,7 +115,7 @@ } .resync-btn { - margin-top:35px; + margin-top:29px; } .run-test-btn { @@ -124,7 +125,7 @@ } .frame-and-buffers { - margin-top:10px; + margin-top:4px; } .test-results-header { diff --git a/web/app/controllers/artifacts_controller.rb b/web/app/controllers/artifacts_controller.rb index 180f12286..755c37fce 100644 --- a/web/app/controllers/artifacts_controller.rb +++ b/web/app/controllers/artifacts_controller.rb @@ -15,6 +15,10 @@ class ArtifactsController < ApiController render :json => result, :status => :ok end + def healthcheck + render :json => {} + end + def versioncheck diff --git a/web/app/views/clients/_banner.html.erb b/web/app/views/clients/_banner.html.erb index 80ba450ae..fec9325bd 100644 --- a/web/app/views/clients/_banner.html.erb +++ b/web/app/views/clients/_banner.html.erb @@ -5,7 +5,7 @@
- <%= image_tag("content/icon_alert.png", :height => '24', :width => '24', :class => "content-icon") %>

alert

+ <%= image_tag("content/icon_alert.png", :height => '24', :width => '24', :class => "content-icon") %>

diff --git a/web/app/views/clients/_configure_tracks_dialog.html.haml b/web/app/views/clients/_configure_tracks_dialog.html.haml index 252ad6066..5611533d1 100644 --- a/web/app/views/clients/_configure_tracks_dialog.html.haml +++ b/web/app/views/clients/_configure_tracks_dialog.html.haml @@ -48,21 +48,26 @@ .tab{'tab-id' => 'voice-chat'} - .column - %form.select-voice-chat-option.section - .sub-header Select Voice Chat Option - .voicechat-option.reuse-audio-input - %input{type:"radio", name: "voicechat", checked:"checked"} - %h3 Use Music Microphone - %p I am already using a microphone to capture my vocal or instrumental music, so I can talk with other musicians using that microphone - .voicechat-option.use-chat-input - %input{type:"radio", name: "voicechat"} - %h3 Use Chat Microphone - %p I am not using a microphone for acoustic instruments or vocals, so use the input selected to the right for voice chat during my sessions - .column - .select-voice-chat - .sub-header Voice Chat Input - .ftue-box.chat-inputs + %form.select-voice-chat-option.section.voice + .sub-header Select Voice Chat Option + .voicechat-option.reuse-audio-input + %input{type:"radio", name: "voicechat", checked:"checked"} + %h3 Use Music Microphone + %p I am already using a microphone to capture my vocal or instrumental music, so I can talk with other musicians using that microphone + .voicechat-option.use-chat-input + %input{type:"radio", name: "voicechat"} + %h3 Use Chat Microphone + %p I am not using a microphone for acoustic instruments or vocals, so use the input selected to the right for voice chat during my sessions + .clearall + .select-voice-chat + .sub-header Voice Chat Input + .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 .buttons diff --git a/web/app/views/clients/_help.html.erb b/web/app/views/clients/_help.html.erb index 4d6d5ad1c..12a71f07c 100644 --- a/web/app/views/clients/_help.html.erb +++ b/web/app/views/clients/_help.html.erb @@ -32,4 +32,8 @@ + + \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index d1d2f502a..02888531b 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -277,10 +277,6 @@ var testBridgeScreen = new JK.TestBridgeScreen(JK.app); testBridgeScreen.initialize(); - if(!connected) { - jamServer.initiateReconnect(null, true); - } - JK.app.initialRouting(); JK.hideCurtain(300); } @@ -317,8 +313,12 @@ }); // 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(); }) diff --git a/web/app/views/clients/latency_tester.html.haml b/web/app/views/clients/latency_tester.html.haml index 27e632c41..e64a92259 100644 --- a/web/app/views/clients/latency_tester.html.haml +++ b/web/app/views/clients/latency_tester.html.haml @@ -42,10 +42,6 @@ function _initAfterConnect(connected) { if (this.didInitAfterConnect) return; this.didInitAfterConnect = true - - if(!connected) { - jamServer.initiateReconnect(null, true); - } } JK.app = JK.JamKazam(); diff --git a/web/app/views/clients/wizard/_gear_test.html.haml b/web/app/views/clients/wizard/_gear_test.html.haml index 5d79bcfe4..e52d48e6e 100644 --- a/web/app/views/clients/wizard/_gear_test.html.haml +++ b/web/app/views/clients/wizard/_gear_test.html.haml @@ -21,13 +21,13 @@ .clearall %ul.results-text %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.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-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.success You may proceed to the next step. %li.failure diff --git a/web/app/views/clients/wizard/gear/_gear_wizard.html.haml b/web/app/views/clients/wizard/gear/_gear_wizard.html.haml index 28d4accef..37450ac88 100644 --- a/web/app/views/clients/wizard/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/wizard/gear/_gear_wizard.html.haml @@ -73,7 +73,7 @@ %li Drag and drop the input port(s) from your audio interface to each track. %li Select the instrument for each track. .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 %h2 Unassigned Ports .unassigned-input-channels.channels-holder @@ -97,9 +97,9 @@ %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. .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 - %h2 Select Voice Chat Option + %h2.sub-header Select Voice Chat Option %form.voice .voicechat-option.reuse-audio-input %input{type:"radio", name: "voicechat", checked:"checked"} @@ -109,9 +109,16 @@ %input{type:"radio", name: "voicechat"} %h3 Use Chat Microphone %p I am not using a microphone for acoustic instruments or vocals, so use the input selected to the right for voice chat during my sessions - .wizard-step-column - %h2 Voice Chat Input + .clearall + // .wizard-step-column + %h2.sub-header.voice-chat-header Voice Chat Input .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" } @@ -127,7 +134,7 @@ %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. .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 .help-content When you have fully turned off the direct monitoring control (if any) on your audio interface, diff --git a/web/app/views/clients/wizard/loopback/_loopback_wizard.html.haml b/web/app/views/clients/wizard/loopback/_loopback_wizard.html.haml index 3d9b18483..a16768ed9 100644 --- a/web/app/views/clients/wizard/loopback/_loopback_wizard.html.haml +++ b/web/app/views/clients/wizard/loopback/_loopback_wizard.html.haml @@ -46,10 +46,10 @@ %h2.ports-header Audio Input Ports .ftue-box.input-ports.ports .ftue-controls - .ftue-vu-left#loopback-audio-input-vu-left - .ftue-fader#loopback-audio-input-fader + .ftue-vu-left.audio-input-vu-left + .ftue-fader.audio-input-fader .gain-label GAIN - .ftue-vu-right#loopback-audio-input-vu-right + .ftue-vu-right.audio-input-vu-right = render :partial => "/clients/wizard/framebuffers" .wizard-step-column %h2 @@ -59,10 +59,10 @@ %h2.ports-header Audio Output Ports .ftue-box.output-ports.ports .ftue-controls - .ftue-vu-left#loopback-audio-output-vu-left - .ftue-fader#loopback-audio-output-fader + .ftue-vu-left.audio-output-vu-left + .ftue-fader.audio-output-fader .gain-label GAIN - .ftue-vu-right#loopback-audio-output-vu-right + .ftue-vu-right.audio-output-vu-right %a.button-orange.resync-btn RESYNC .wizard-step-column %h2.test-results-header diff --git a/web/config/routes.rb b/web/config/routes.rb index 4e69d14b8..4085b0b54 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -408,6 +408,9 @@ SampleApp::Application.routes.draw do # version check for JamClient 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 match '/artifacts/clients' => 'artifacts#client_downloads' diff --git a/web/script/package/jam-web.conf b/web/script/package/jam-web.conf index 628bd55f9..b385a1ea4 100755 --- a/web/script/package/jam-web.conf +++ b/web/script/package/jam-web.conf @@ -3,6 +3,11 @@ description "jam-web" start on startup start on runlevel [2345] stop on runlevel [016] +limit nofile 20000 20000 +limit core unlimited unlimited + +respawn +respawn limit 10 5 pre-start script set -e diff --git a/web/spec/factories.rb b/web/spec/factories.rb index 632e8fb9c..57caddff0 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -143,7 +143,8 @@ FactoryGirl.define do addr {JamIsp.ip_to_num(ip_address)} locidispid 0 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 factory :friendship, :class => JamRuby::Friendship do diff --git a/web/spec/features/gear_wizard_spec.rb b/web/spec/features/gear_wizard_spec.rb index ba5e2b222..325fc8bc3 100644 --- a/web/spec/features/gear_wizard_spec.rb +++ b/web/spec/features/gear_wizard_spec.rb @@ -21,7 +21,7 @@ describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => tru # 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').trigger(:click) + find('.btn-next.button-orange:not(.disabled)').trigger(:click) # step 3 - 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') 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 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 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 find('.ftue-step-title', text: 'Test Router & Network') find('.button-orange.start-network-test').trigger(:click) find('.user-btn', text: 'RUN NETWORK TEST ANYWAY').trigger(:click) find('.button-orange.start-network-test') - find('.btn-next.button-orange').trigger(:click) + find('.btn-next.button-orange:not(.disabled)').trigger(:click) # step 7 - Success find('.ftue-step-title', text: 'Success!') diff --git a/web/spec/features/user_progression_spec.rb b/web/spec/features/user_progression_spec.rb index 9c925c937..d3060ac73 100644 --- a/web/spec/features/user_progression_spec.rb +++ b/web/spec/features/user_progression_spec.rb @@ -73,12 +73,17 @@ describe "User Progression", :js => true, :type => :feature, :capybara_feature describe "certified gear" do before(:each) do sign_in_poltergeist user + FactoryGirl.create(:latency_tester) visit '/client#/account/audio' + # step 1 - intro find("div.account-audio a[data-purpose='add-profile']").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 end diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 3caee5042..f39852132 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -11,22 +11,6 @@ module EventMachine module WebSocket 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 - - # 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 @@ -226,6 +210,40 @@ module JamWebsockets MQRouter.client_exchange = @clients_exchange 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) # default to using json instead of pb @@ -246,6 +264,9 @@ module JamWebsockets client.encode_json = false end + websocket_comm(client, nil) do + handle_login(client, handshake.query) + end } client.onclose { @@ -261,50 +282,26 @@ module JamWebsockets end } - client.onmessage { |msg| + client.onmessage { |data| # TODO: set a max message size before we put it through PB? # TODO: rate limit? - pb_msg = nil + msg = nil - begin + # extract the message safely + websocket_comm(client, nil) do if client.encode_json - #example: {"type":"LOGIN", "target":"server", "login" : {"username":"hi"}} - parse = JSON.parse(msg) - pb_msg = Jampb::ClientMessage.json_create(parse) - self.route(pb_msg, client) + json = JSON.parse(data) + msg = Jampb::ClientMessage.json_create(json) else - pb_msg = Jampb::ClientMessage.parse(msg.to_s) - self.route(pb_msg, client) + msg = Jampb::ClientMessage.parse(data.to_s) end - 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 + end - # wrap the message up and send it down - error_msg = @message_factory.server_permission_error(pb_msg.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 + # then route it internally + websocket_comm(client, msg.message_id) do + self.route(msg, client) end } end @@ -391,9 +388,9 @@ module JamWebsockets # removes all resources associated with a client def cleanup_client(client) - @semaphore.synchronize do - client.close if client.connected? + client.close + @semaphore.synchronize do pending = client.context.nil? # presence of context implies this connection has been logged into if pending @@ -514,6 +511,7 @@ module JamWebsockets heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(nil, client_type) latency_tester = LatencyTester.connect({ client_id: client_id, + channel_id: client.channel_id, ip_address: remote_ip, connection_stale_time: connection_stale_time, connection_expire_time: connection_expire_time}) @@ -543,15 +541,15 @@ module JamWebsockets end end - def handle_login(login, client) - username = login.username if login.value_for_tag(1) - password = login.password if login.value_for_tag(2) - token = login.token if login.value_for_tag(3) - client_id = login.client_id if login.value_for_tag(4) - reconnect_music_session_id = login.reconnect_music_session_id if login.value_for_tag(5) - client_type = login.client_type if login.value_for_tag(6) + def handle_login(client, options) + username = options["username"] + password = options["password"] + token = options["token"] + client_id = options["client_id"] + reconnect_music_session_id = options["music_session_id"] + 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 handle_latency_tester_login(client_id, client_type, client) @@ -568,17 +566,21 @@ module JamWebsockets 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 # 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] 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}" - 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) 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 # because it will recreate a new connection lower down if connection && user && connection.user != user @@ -608,7 +610,7 @@ module JamWebsockets recording_id = nil 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 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 # log this connection in the database 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 Notification.send_friend_update(user.id, true, conn) end @@ -755,7 +757,7 @@ module JamWebsockets if !token.nil? && token != '' @log.debug "logging in via token" # attempt login with token - user = JamRuby::User.find_by_remember_token(token) + user = User.find_by_remember_token(token) if user.nil? @log.debug "no user found with token #{token}" diff --git a/websocket-gateway/script/package/websocket-gateway.conf b/websocket-gateway/script/package/websocket-gateway.conf index dc00b7fb3..106ea2c7d 100755 --- a/websocket-gateway/script/package/websocket-gateway.conf +++ b/websocket-gateway/script/package/websocket-gateway.conf @@ -3,6 +3,11 @@ description "websocket-gateway" start on startup start on runlevel [2345] stop on runlevel [016] +limit nofile 20000 20000 +limit core unlimited unlimited + +respawn +respawn limit 10 5 pre-start script set -e diff --git a/websocket-gateway/spec/factories.rb b/websocket-gateway/spec/factories.rb index cf4fe8586..5b37ae1c4 100644 --- a/websocket-gateway/spec/factories.rb +++ b/websocket-gateway/spec/factories.rb @@ -87,6 +87,7 @@ FactoryGirl.define do ip_address '1.1.1.1' as_musician true client_type 'client' + sequence(:channel_id) { |n| "Channel#{n}"} end factory :instrument, :class => JamRuby::Instrument do diff --git a/websocket-gateway/spec/jam_websockets/router_spec.rb b/websocket-gateway/spec/jam_websockets/router_spec.rb index 6d1743265..31020c738 100644 --- a/websocket-gateway/spec/jam_websockets/router_spec.rb +++ b/websocket-gateway/spec/jam_websockets/router_spec.rb @@ -41,7 +41,7 @@ end # 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 client = LoginClient.new @@ -60,15 +60,9 @@ def login(router, user, password, client_id) @router.new_client(client, false) 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 - # 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 end @@ -98,15 +92,9 @@ def login_latency_tester(router, latency_tester, client_id) @router.new_client(client, true) 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 - # 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 end @@ -239,7 +227,7 @@ describe Router do it "should allow login of valid user", :mq => true do @user = FactoryGirl.create(:user, :password => "foobar", :password_confirmation => "foobar") - client1 = login(@router, @user, "foobar", "1") + client1 = login(@router, @user, "foobar", "1", @user.remember_token, "client") done end @@ -271,7 +259,7 @@ describe Router do # make a music_session and define two members # 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 end @@ -284,9 +272,9 @@ describe Router do # 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 @@ -305,10 +293,10 @@ describe Router do music_session = FactoryGirl.create(:active_music_session, :creator => user1) # 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) - client2 = login(@router, user2, "foobar", "2") + client2 = login(@router, user2, "foobar", "2", user2.remember_token, "client") #login_music_session(@router, client2, music_session) # by creating