From 3b3776f0659e035d0e99a7d263b27e4aefd80593 Mon Sep 17 00:00:00 2001 From: Scott Comer Date: Thu, 29 May 2014 13:03:33 -0500 Subject: [PATCH 01/29] get score from Search.musician_filter into json sent to client --- ruby/lib/jam_ruby/models/search.rb | 22 +++++++++++-------- ruby/lib/jam_ruby/models/user.rb | 5 +++++ web/app/views/api_search/index.rabl | 2 +- web/spec/requests/musician_search_api_spec.rb | 2 ++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index b9745984a..5ec766695 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -123,16 +123,16 @@ module JamRuby # produce a list of musicians (users where musician is true) # params: # instrument - instrument to search for or blank - # score_limit - score must be <= this to be included in the result + # score_limit - score must be <= this to be included in the result (this is a test against the raw score) # handled by relation_pagination: # page - page number to fetch (origin 1) # per_page - number of entries per page # handled by order_param: - # orderby - ??? (followed, plays, playing) - # handled by where_latlng: - # distance - defunct - # city - defunct - # remote_ip - defunct + # orderby - what sort of search, also defines order (followed, plays, playing) + # previously handled by where_latlng: + # distance - defunct! + # city - defunct! + # remote_ip - defunct! def self.musician_filter(params={}, user=nil, conn=nil) # puts "================ params #{params.inspect}" # puts "================ user #{user.inspect}" @@ -178,10 +178,10 @@ module JamRuby case ordering when :plays # FIXME: double counting? # sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}" - rel = rel.select('COUNT(records.id)+COUNT(sessions.id) AS play_count') + rel = rel.select('COUNT(records.id)+COUNT(sessions.id) AS search_play_count') rel = rel.joins("LEFT JOIN music_sessions AS sessions ON sessions.user_id = users.id") rel = rel.joins("LEFT JOIN recordings AS records ON records.owner_id = users.id") - rel = rel.order("play_count DESC") + rel = rel.order("search_play_count DESC") when :followed rel = rel.joins('left outer join follows on follows.followable_id = users.id') rel = rel.select('count(follows.user_id) as search_follow_count') @@ -201,9 +201,13 @@ module JamRuby rel, page = self.relation_pagination(rel, params) rel = rel.includes([:instruments, :followings, :friends]) + # puts "======================== sql #{rel.to_sql}" objs = rel.all - # puts "======================== objs #{objs.inspect}" + # if objs.length > 0 + # puts "======================== attributes #{objs[0].attributes}" + # puts "======================== score #{objs[0].score}" + # end srch = Search.new srch.search_type = :musicians_filter diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 0ed32cc68..59b34c405 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -290,6 +290,11 @@ module JamRuby self.music_sessions.size end + def joined_score + nil unless has_attribute?(:score) + read_attribute(:score).to_i + end + # mods comes back as text; so give ourselves a parsed version def mods_json @mods_json ||= mods ? JSON.parse(mods, symbolize_names: true) : {} diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl index 16c1f85fc..fd53ba9c2 100644 --- a/web/app/views/api_search/index.rabl +++ b/web/app/views/api_search/index.rabl @@ -43,7 +43,7 @@ if @search.musicians_filter_search? end child(:results => :musicians) { - attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online, :musician, :photo_url, :biography + attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online, :musician, :photo_url, :biography, :joined_score node :is_friend do |musician| @search.is_friend?(musician) diff --git a/web/spec/requests/musician_search_api_spec.rb b/web/spec/requests/musician_search_api_spec.rb index 975712b0b..88b0eec3d 100644 --- a/web/spec/requests/musician_search_api_spec.rb +++ b/web/spec/requests/musician_search_api_spec.rb @@ -45,6 +45,8 @@ describe "Musician Search API", :type => :api do get_query good_response expect(json['musicians'].count).to be [Search::M_PER_PAGE, User.musicians_geocoded.count].min + puts json.inspect + end context 'score filtering' do From 9b188495445fd5b99f538a60664d346abc25b4cf Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 28 Apr 2014 19:47:24 +0000 Subject: [PATCH 02/29] * working on feature ftue --- web/app/assets/javascripts/JamServer.js | 8 +++++--- web/vendor/assets/javascripts/jquery.icheck.js | 2 ++ websocket-gateway/lib/jam_websockets/router.rb | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 44321fd23..a407453db 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -156,9 +156,10 @@ // for debugging purposes, see if the last time we've sent a heartbeat is way off (500ms) of the target interval var now = new Date(); - if (lastHeartbeatSentTime) { + + if(lastHeartbeatSentTime) { var drift = new Date().getTime() - lastHeartbeatSentTime.getTime() - heartbeatMS; - if (drift > 500) { + if(drift > 500) { logger.error("significant drift between heartbeats: " + drift + 'ms beyond target interval') } } @@ -460,7 +461,8 @@ connectTimeout = setTimeout(function () { connectTimeout = null; - if (connectDeferred.state() === 'pending') { + + if(connectDeferred.state() === 'pending') { server.close(true); connectDeferred.reject(); } diff --git a/web/vendor/assets/javascripts/jquery.icheck.js b/web/vendor/assets/javascripts/jquery.icheck.js index ab4eed092..4e0cffeb9 100644 --- a/web/vendor/assets/javascripts/jquery.icheck.js +++ b/web/vendor/assets/javascripts/jquery.icheck.js @@ -285,10 +285,12 @@ // Check, disable or indeterminate if (/^(ch|di|in)/.test(method) && !active) { + console.log("TAKING ROUTE: ", state); on(input, state); // Uncheck, enable or determinate } else if (/^(un|en|de)/.test(method) && active) { + console.log("TAKING ROUTE2: ", state); off(input, state); // Update diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 8ee1cab67..153738ad0 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -393,6 +393,7 @@ module JamWebsockets @log.debug "cleaned up not-logged-in client #{client}" else @log.debug "cleanup up logged-in client #{client}" + context = @clients.delete(client) if context From d39f91e186c7b0820b4319bbad1f72df32ad21c0 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 9 May 2014 13:39:27 -0500 Subject: [PATCH 03/29] * VRFS-1575 - step 2 of FTUE almost done --- web/app/assets/javascripts/application.js | 1 + web/app/assets/javascripts/fakeJamClient.js | 23 + .../assets/javascripts/gear/gear_wizard.js | 237 +++++ .../javascripts/gear/step_configure_tracks.js | 18 + .../gear/step_configure_voice_chat.js | 18 + .../gear/step_direct_monitoring.js | 18 + .../javascripts/gear/step_network_test.js | 18 + .../javascripts/gear/step_select_gear.js | 952 ++++++++++++++++++ .../assets/javascripts/gear/step_success.js | 18 + .../javascripts/gear/step_understand_gear.js | 27 + web/app/assets/javascripts/gear_wizard.js | 909 ----------------- .../stylesheets/client/gearWizard.css.scss | 79 +- .../views/clients/gear/_gear_wizard.html.haml | 29 +- 13 files changed, 1427 insertions(+), 920 deletions(-) create mode 100644 web/app/assets/javascripts/gear/gear_wizard.js create mode 100644 web/app/assets/javascripts/gear/step_configure_tracks.js create mode 100644 web/app/assets/javascripts/gear/step_configure_voice_chat.js create mode 100644 web/app/assets/javascripts/gear/step_direct_monitoring.js create mode 100644 web/app/assets/javascripts/gear/step_network_test.js create mode 100644 web/app/assets/javascripts/gear/step_select_gear.js create mode 100644 web/app/assets/javascripts/gear/step_success.js create mode 100644 web/app/assets/javascripts/gear/step_understand_gear.js delete mode 100644 web/app/assets/javascripts/gear_wizard.js diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index f9d230ae6..cd7ff04cb 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -39,3 +39,4 @@ //= require utils //= require custom_controls //= require_directory . +//= require_directory ./gear diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index af10cb53b..1175d9b28 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -38,7 +38,15 @@ function RestartApplication() { } + function FTUECancel() { + } + function FTUEGetMusicProfileName() { + return "default" + } + function FTUESetMusicProfileName() { + + } function FTUEGetInputLatency() { dbg("FTUEGetInputLatency"); return 2; @@ -85,6 +93,9 @@ function FTUEGetStatus() { return ftueStatus; } function FTUESetStatus(b) { ftueStatus = b; } function FTUESetMusicDevice(id) { dbg("FTUESetMusicDevice"); } + function FTUEGetAudioDevices() { + return {"devices":[{"display_name":"Built-in","guid":"Built-in","input_count":1,"name":"Built-in","output_count":1,"port_audio_name":"Built-in"},{"display_name":"JamKazam Virtual Monitor","guid":"JamKazam Virtual Monitor","input_count":0,"name":"JamKazam Virtual Monitor","output_count":1,"port_audio_name":"JamKazam Virtual Monitor"}]} + } function FTUEGetDevices() { dbg('FTUEGetMusicDevices'); return { @@ -113,6 +124,11 @@ "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" }; } + function FTUESetInputMusicDevice() { } + function FTUESetOutputMusicDevice() { } + function FTUEGetInputMusicDevice() { return null; } + function FTUEGetOutputMusicDevice() { return null; } + function FTUESetMusicInput() { dbg('FTUESetMusicInput'); } function FTUESetChatInput() { dbg('FTUESetChatInput'); } function FTUESetMusicOutput() { dbg('FTUESetMusicOutput'); } @@ -656,10 +672,17 @@ this.connected = true; // FTUE (round 3) + this.FTUESetInputMusicDevice = FTUESetInputMusicDevice; + this.FTUESetOutputMusicDevice = FTUESetOutputMusicDevice; + this.FTUEGetInputMusicDevice = FTUEGetInputMusicDevice; + this.FTUEGetOutputMusicDevice = FTUEGetOutputMusicDevice; this.FTUEGetChatInputVolume = FTUEGetChatInputVolume; this.FTUEGetChatInputs = FTUEGetChatInputs; this.FTUEGetDevices = FTUEGetDevices; this.FTUEGetFrameSize = FTUEGetFrameSize; + this.FTUECancel = FTUECancel; + this.FTUEGetMusicProfileName = FTUEGetMusicProfileName; + this.FTUESetMusicProfileName = FTUESetMusicProfileName; this.FTUEGetInputLatency = FTUEGetInputLatency; this.FTUEGetInputVolume = FTUEGetInputVolume; this.FTUEGetMusicInputs = FTUEGetMusicInputs; diff --git a/web/app/assets/javascripts/gear/gear_wizard.js b/web/app/assets/javascripts/gear/gear_wizard.js new file mode 100644 index 000000000..bdffef0dc --- /dev/null +++ b/web/app/assets/javascripts/gear/gear_wizard.js @@ -0,0 +1,237 @@ +(function (context, $) { + + "use strict"; + + + context.JK = context.JK || {}; + context.JK.GearWizard = function (app) { + + var $dialog = null; + var $wizardSteps = null; + var $currentWizardStep = null; + var step = 0; + var $templateSteps = null; + var $templateButtons = null; + var $templateAudioPort = null; + var $ftueButtons = null; + var $btnBack = null; + var $btnNext = null; + var $btnClose = null; + var $btnCancel = null; + + var self = this; + + var TOTAL_STEPS = 7; + var STEP_INTRO = 0; + var STEP_SELECT_DEVICE = 1; + var STEP_SELECT_TRACKS = 2; + var STEP_SELECT_CHAT = 3; + var STEP_DIRECT_MONITOR = 4; + var STEP_ROUTER_NETWORK = 5; + var STEP_SUCCESS = 6; + + var stepUnderstandGear = new context.JK.StepUnderstandGear(app, this); + var stepSelectGear = new context.JK.StepSelectGear(app, this); + var stepConfigureTracks = new context.JK.StepConfigureTracks(app, this); + var stepConfigureVoiceChat = new context.JK.StepConfigureVoiceChat(app, this); + var stepDirectMonitoring = new context.JK.StepDirectMonitoring(app, this); + var stepNetworkTest = new context.JK.StepNetworkTest(app, this); + var stepSuccess = new context.JK.StepSuccess(app, this); + + var STEPS = { + 0: stepUnderstandGear, + 1: stepSelectGear, + 2: stepConfigureTracks, + 3: stepConfigureVoiceChat, + 4: stepDirectMonitoring, + 5: stepNetworkTest, + 6: stepSuccess + } + + function beforeShowStep($step) { + var stepInfo = STEPS[step]; + + if (!stepInfo) { + throw "unknown step: " + step; + } + + stepInfo.beforeShow.call(stepInfo); + } + + function moveToStep() { + var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); + + $wizardSteps.hide(); + + $currentWizardStep = $nextWizardStep; + + var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); + var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); + $activeStep.addClass('.active'); + $activeStep.next().show(); // show the .ftue-step-title + $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); + + // update buttons + var $ftueButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); + + + $btnBack = $ftueButtonsContent.find('.btn-back'); + $btnNext = $ftueButtonsContent.find('.btn-next'); + $btnClose = $ftueButtonsContent.find('.btn-close'); + $btnCancel = $ftueButtonsContent.find('.btn-cancel'); + + // hide back button if 1st step or last step + if (step == 0 && step == TOTAL_STEPS - 1) { + $btnBack.hide(); + } + // hide next button if not on last step + if (step == TOTAL_STEPS - 1) { + $btnNext.hide(); + } + // hide close if on last step + if (step != TOTAL_STEPS - 1) { + $btnClose.hide(); + } + // hide cancel if not on last step + if (step == TOTAL_STEPS - 1) { + $btnCancel.hide(); + } + + $btnNext.on('click', next); + $btnBack.on('click', back); + $btnClose.on('click', closeDialog); + $btnCancel.on('click', closeDialog); + + $ftueButtons.empty(); + $ftueButtons.append($ftueButtonsContent); + + beforeShowStep($currentWizardStep); + $currentWizardStep.show(); + + } + + function reset() { + $currentWizardStep = null; + } + + // checks if we already have a profile called 'FTUE...'; if not, create one. if so, re-use it. + function findOrCreateFTUEProfile() { + var profileName = context.jamClient.FTUEGetMusicProfileName(); + + logger.debug("current profile name: " + profileName); + + if(profileName && profileName.indexOf('FTUE') == 0) { + + } + else { + var newProfileName = 'FTUEAttempt-' + new Date().getTime().toString(); + logger.debug("setting FTUE-prefixed profile name to: " + newProfileName); + context.jamClient.FTUESetMusicProfileName(newProfileName); + } + + var profileName = context.jamClient.FTUEGetMusicProfileName(); + + logger.debug("name on exit: " + profileName); + + } + + function beforeShow(args) { + context.jamClient.FTUECancel(); + context.jamClient.FTUESetStatus(false); + findOrCreateFTUEProfile(); + + step = args.d1; + if (!step) step = 0; + step = parseInt(step); + moveToStep(); + } + + function afterShow() { + + } + + function afterHide() { + context.jamClient.FTUESetStatus(true); + context.jamClient.FTUECancel(); + } + + function back() { + if ($(this).is('.button-grey')) return false; + step = step - 1; + moveToStep(); + return false; + } + + function next() { + if ($(this).is('.button-grey')) return false; + + step = step + 1; + + moveToStep(); + return false; + } + + function closeDialog() { + app.layout.closeDialog('gear-wizard'); + return false; + } + + function events() { + + } + + function route() { + + } + + function setNextState(enabled) { + + if(!$btnNext) return; + + $btnNext.removeClass('button-orange button-grey'); + + if (enabled) { + $btnNext.addClass('button-orange'); + } + else { + $btnNext.addClass('button-grey'); + } + } + + function initialize() { + + // on initial page load, we are not in the FTUE. so cancel the FTUE and call FTUESetStatus(true) if needed + if(context.jamClient.FTUEGetStatus() == false) { + context.jamClient.FTUESetStatus(true); + } + context.jamClient.FTUECancel(); + + var dialogBindings = { beforeShow: beforeShow, afterShow: afterShow, afterHide: afterHide }; + + app.bindDialog('gear-wizard', dialogBindings); + + $dialog = $('#gear-wizard-dialog'); + $wizardSteps = $dialog.find('.wizard-step'); + $templateSteps = $('#template-ftuesteps'); + $templateButtons = $('#template-ftue-buttons'); + $ftueButtons = $dialog.find('.ftue-buttons'); + + stepUnderstandGear.initialize($wizardSteps.filter($('[layout-wizard-step=0]'))); + stepSelectGear.initialize($wizardSteps.filter($('[layout-wizard-step=1]'))); + stepConfigureTracks.initialize($wizardSteps.filter($('[layout-wizard-step=2]'))); + stepConfigureVoiceChat.initialize($wizardSteps.filter($('[layout-wizard-step=3]'))); + stepDirectMonitoring.initialize($wizardSteps.filter($('[layout-wizard-step=4]'))); + stepNetworkTest.initialize($wizardSteps.filter($('[layout-wizard-step=5]'))); + stepSuccess.initialize($wizardSteps.filter($('[layout-wizard-step=6]'))); + + events(); + } + + this.setNextState = setNextState; + this.initialize = initialize; + + self = this; + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js new file mode 100644 index 000000000..ab0be8b4b --- /dev/null +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepConfigureTracks = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_configure_voice_chat.js b/web/app/assets/javascripts/gear/step_configure_voice_chat.js new file mode 100644 index 000000000..46d9c9d6a --- /dev/null +++ b/web/app/assets/javascripts/gear/step_configure_voice_chat.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepConfigureVoiceChat = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_direct_monitoring.js b/web/app/assets/javascripts/gear/step_direct_monitoring.js new file mode 100644 index 000000000..a8e6aa1ac --- /dev/null +++ b/web/app/assets/javascripts/gear/step_direct_monitoring.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepDirectMonitoring = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js new file mode 100644 index 000000000..d04e73343 --- /dev/null +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepNetworkTest = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js new file mode 100644 index 000000000..ec4db1c6d --- /dev/null +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -0,0 +1,952 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepSelectGear = function (app, $dialog) { + + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; + var self = null; + var $step = null; + var $watchVideoInput = null; + var $watchVideoOutput = null; + var $audioInput = null; + var $audioOutput = null; + var $bufferIn = null; + var $bufferOut = null; + var $frameSize = null; + var $inputChannels = null; + var $outputChannels = null; + var $knobs = null; + var $scoreReport = null; + var $latencyScoreSection = null; + var $latencyScore = null; + var $latencyHeader = null; + var $ioHeader = null; + var $ioScoreSection = null; + var $ioRate = null; + var $ioRateScore = null; + var $ioVar = null; + var $ioVarScore = null; + var $ioCountdown = null; + var $ioCountdownSecs = null; + var $resultsText = null; + var $asioInputControlBtn = null; + var $asioOutputControlBtn = null; + var $resyncBtn = null; + var $templateAudioPort = null; + + var operatingSystem = null; + var iCheckIgnore = false; + + // cached values between + var deviceInformation = null; + var selectedDeviceInfo = null; + var musicPorts = null; + var validLatencyScore = false; + var validIOScore = false; + + var audioDeviceBehavior = { + MacOSX_builtin: { + display: 'MacOSX Built-In', + videoURL: undefined, + showKnobs: false, + showASIO: false + }, + MacOSX_interface: { + display: 'MacOSX external interface', + videoURL: undefined, + showKnobs: false, + showASIO: false + }, + Win32_wdm: { + display: 'Windows WDM', + videoURL: undefined, + showKnobs: true, + showASIO: false + }, + Win32_asio: { + display: 'Windows ASIO', + videoURL: undefined, + showKnobs: true, + showASIO: false + }, + Win32_asio4all: { + display: 'Windows ASIO4ALL', + videoURL: undefined, + showKnobs: false, + showASIO: true + }, + Linux: { + display: 'Linux', + videoURL: undefined, + showKnobs: true, + showASIO: false + } + } + + var ASIO_SETTINGS_DEFAULT_TEXT = 'ASIO SETTINGS...'; + var ASIO_SETTINGS_INPUT_TEXT = 'ASIO INPUT SETTINGS...'; + var ASIO_SETTINGS_OUTPUT_TEXT = 'ASIO OUTPUT SETTINGS...'; + + // should return one of: + // * MacOSX_builtin + // * MACOSX_interface + // * Win32_wdm + // * Win32_asio + // * Win32_asio4all + // * Linux + function determineDeviceType(deviceId, displayName) { + if (operatingSystem == "MacOSX") { + if (displayName.toLowerCase().trim() == "built-in") { + return "MacOSX_builtin"; + } + else { + return "MacOSX_interface"; + } + } + else if (operatingSystem == "Win32") { + if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) { + return "Win32_wdm"; + } + else if (displayName.toLowerCase().indexOf("asio4all") > -1) { + return "Win32_asio4all" + } + else { + return "Win32_asio"; + } + } + else { + return "Linux"; + } + } + + function loadDevices() { + + var oldDevices = context.jamClient.FTUEGetDevices(false); + var devices = context.jamClient.FTUEGetAudioDevices(); + console.log("oldDevices: " + JSON.stringify(oldDevices)); + console.log("devices: " + JSON.stringify(devices)); + + var loadedDevices = {}; + + // augment these devices by determining their type + context._.each(devices.devices, function (device) { + + if (device.name == "JamKazam Virtual Monitor") { + return; + } + + var deviceInfo = {}; + + deviceInfo.id = device.guid; + deviceInfo.type = determineDeviceType(device.guid, device.display_name); + console.log("deviceInfo.type: " + deviceInfo.type) + deviceInfo.displayType = audioDeviceBehavior[deviceInfo.type].display; + deviceInfo.displayName = device.display_name; + + loadedDevices[device.guid] = deviceInfo; + + logger.debug("loaded device: ", deviceInfo); + }) + + deviceInformation = loadedDevices; + + logger.debug(context.JK.dlen(deviceInformation) + " devices loaded.", deviceInformation); + } + + // returns a deviceInfo hash for the device matching the deviceId, or undefined. + function findDevice(deviceId) { + return deviceInformation[deviceId]; + } + + function selectedAudioInput() { + return $audioInput.val(); + } + + function selectedAudioOutput() { + return $audioOutput.val(); + } + + function selectedFramesize() { + return parseFloat($frameSize.val()); + } + + function selectedBufferIn() { + return parseFloat($frameSize.val()); + } + + function selectedBufferOut() { + return parseFloat($frameSize.val()); + } + + function initializeNextButtonState() { + $dialog.setNextState(validLatencyScore && validIOScore); + } + + function initializeAudioInput() { + var optionsHtml = ''; + optionsHtml = ''; + context._.each(deviceInformation, function (deviceInfo, deviceId) { + + console.log(arguments) + optionsHtml += ''; + }); + $audioInput.html(optionsHtml); + context.JK.dropdown($audioInput); + + initializeAudioInputChanged(); + } + + function initializeAudioOutput() { + var optionsHtml = ''; + optionsHtml = ''; + context._.each(deviceInformation, function (deviceInfo, deviceId) { + optionsHtml += ''; + }); + $audioOutput.html(optionsHtml); + context.JK.dropdown($audioOutput); + + initializeAudioOutputChanged(); + } + + function initializeFramesize() { + context.JK.dropdown($frameSize); + } + + function initializeBuffers() { + context.JK.dropdown($bufferIn); + context.JK.dropdown($bufferOut); + } + + + // reloads the backend's channel state for the currently selected audio devices, + // and update's the UI accordingly + function initializeChannels() { + musicPorts = jamClient.FTUEGetChannels(); + console.log("musicPorts: %o", JSON.stringify(musicPorts)); + + initializeInputPorts(musicPorts); + initializeOutputPorts(musicPorts); + } + + // select 2 (or 1) inputs and 2 outputs for the user. required to get a latency score + // also, arguably convenient + function autoSelectMinimumValidChannels() { + + var $allInputs = $inputChannels.find('input[type="checkbox"]'); + + if ($allInputs.length == 0) { + // ERROR: not enough channels + 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) { + // 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; + } + + // ensure 1, or preferably 2, input channels are selected + var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked'); + var $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)'); + if ($assignedInputs.length == 0) { + if ($allInputs.length >= 2) { + $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); + // this is required because iCheck change handler re-writes the inputs. So we have to refetch unassigned outputs + $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)'); + $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); + } + else { + $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); + } + } + + // ensure 2 outputs are selected + var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked'); + var $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)'); + + console.log("outputs:", $assignedOutputs, $unassignedOutputs); + if ($assignedOutputs.length == 0) { + console.log("selecting both outputs") + $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); + // this is required because iCheck change handler re-writes the inputs. So we have to refetch unassigned outputs + $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)'); + $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); + } + else if ($assignedOutputs.length == 1) { + $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); + } + + return true; + } + + // during this phase of the FTUE, we have to assign selected input channels + // to tracks. The user, however, does not have a way to indicate which channel + // goes to which track (that's not until the next step of the wizard). + // so, we just auto-generate a valid assignment + function newInputAssignment() { + var assigned = 0; + context._.each(musicPorts.inputs, function (inputChannel) { + if (isChannelAssigned(inputChannel)) { + assigned += 1; + } + }); + + var newAssignment = Math.floor(assigned / 2) + 1; + return newAssignment; + } + + function inputChannelChanged() { + if (iCheckIgnore) return; + + var $checkbox = $(this); + var channelId = $checkbox.attr('data-id'); + var isChecked = $checkbox.is(':checked'); + + if (isChecked) { + var newAssignment = newInputAssignment(); + logger.debug("assigning input channel %o to track: %o", channelId, newAssignment); + context.jamClient.TrackSetAssignment(channelId, true, newAssignment); + } + else { + logger.debug("unassigning input channel %o", channelId); + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED); + // unassigning creates a hole in our auto-assigned tracks. reassign them all to keep it consistent + var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked'); + var assigned = 0; + context._.each($assignedInputs, function (assignedInput) { + var $assignedInput = $(assignedInput); + var assignedChannelId = $assignedInput.attr('data-id'); + var newAssignment = Math.floor(assigned / 2) + 1; + logger.debug("re-assigning input channel %o to track: %o", assignedChannelId, newAssignment); + context.jamClient.TrackSetAssignment(assignedChannelId, true, newAssignment); + assigned += 1; + }); + } + + initializeChannels(); + } + + // should be called in a ifChanged callback if you want to cancel. + // you have to use this instead of 'return false' like a typical input 'change' event. + function cancelICheckChange($checkbox) { + iCheckIgnore = true; + var checked = $checkbox.is(':checked'); + setTimeout(function () { + if (checked) $checkbox.iCheck('uncheck').removeAttr('checked'); + else $checkbox.iCheck('check').attr('checked', 'checked'); + iCheckIgnore = false; + }, 1); + } + + function outputChannelChanged() { + if (iCheckIgnore) return; + var $checkbox = $(this); + var channelId = $checkbox.attr('data-id'); + var isChecked = $checkbox.is(':checked'); + + // don't allow more than 2 output channels selected at once + if ($outputChannels.find('input[type="checkbox"]:checked').length > 2) { + context.JK.Banner.showAlert('You can only have a maximum of 2 output ports selected.'); + // can't allow uncheck of last output + cancelICheckChange($checkbox); + return; + } + + if (isChecked) { + logger.debug("assigning output channel %o", channelId); + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT); + } + else { + logger.debug("unassigning output channel %o", channelId); + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED); + } + + initializeChannels(); + } + + // checks if it's an assigned OUTPUT or ASSIGNED CHAT + function isChannelAssigned(channel) { + return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0; + } + + function initializeInputPorts(musicPorts) { + $inputChannels.empty(); + var inputPorts = musicPorts.inputs; + context._.each(inputPorts, function (inputChannel) { + var $inputChannel = $(context._.template($templateAudioPort.html(), inputChannel, { variable: 'data' })); + var $checkbox = $inputChannel.find('input'); + if (isChannelAssigned(inputChannel)) { + $checkbox.attr('checked', 'checked'); + } + context.JK.checkbox($checkbox); + $checkbox.on('ifChanged', inputChannelChanged); + $inputChannels.append($inputChannel); + }); + } + + function initializeOutputPorts(musicPorts) { + $outputChannels.empty(); + var outputChannels = musicPorts.outputs; + context._.each(outputChannels, function (outputChannel) { + var $outputPort = $(context._.template($templateAudioPort.html(), outputChannel, { variable: 'data' })); + var $checkbox = $outputPort.find('input'); + if (isChannelAssigned(outputChannel)) { + $checkbox.attr('checked', 'checked'); + } + context.JK.checkbox($checkbox); + $checkbox.on('ifChanged', outputChannelChanged); + $outputChannels.append($outputPort); + }); + } + + function initializeFormElements() { + if (!deviceInformation) throw "devices are not initialized"; + + initializeAudioInput(); + initializeAudioOutput(); + initializeFramesize(); + initializeBuffers(); + } + + function resetFrameBuffers() { + $frameSize.val('2.5'); + $bufferIn.val('0'); + $bufferOut.val('0'); + } + + function clearInputPorts() { + $inputChannels.empty(); + } + + function clearOutputPorts() { + $outputChannels.empty(); + } + + function resetScoreReport() { + $ioHeader.hide(); + $latencyHeader.hide(); + $ioRate.hide(); + $ioRateScore.empty(); + $ioVar.hide(); + $ioVarScore.empty(); + $latencyScore.empty(); + $resultsText.removeAttr('latency-score'); + $resultsText.removeAttr('io-var-score'); + $resultsText.removeAttr('io-rate-score'); + } + + function renderLatencyScore(latencyValue, latencyClass) { + // latencyValue == null implies starting condition + if (latencyValue) { + $latencyScore.text(latencyValue + ' ms'); + } + else { + $latencyScore.text(''); + } + $latencyHeader.show(); + $resultsText.attr('latency-score', latencyClass); + $latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass); + } + + // std deviation is the worst value between in/out + // media is the worst value between in/out + // io is the value returned by the backend, which has more info + // ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad' + function renderIOScore(std, median, ioData, ioClass, ioRateClass, ioVarClass) { + $ioRateScore.text(median !== null ? median : ''); + $ioVarScore.text(std !== null ? std : ''); + if (ioClass && ioClass != "starting") { + $ioRate.show(); + $ioVar.show(); + } + if(ioClass == 'starting') { + $ioHeader.show(); + } + $resultsText.attr('io-rate-score', ioRateClass); + $resultsText.attr('io-var-score', ioVarClass); + $ioScoreSection.removeClass('good acceptable bad unknown starting skip') + if (ioClass) { + $ioScoreSection.addClass(ioClass); + } + // TODO: show help bubble of all data in IO data + } + + function updateScoreReport(latencyResult) { + var latencyClass = "neutral"; + var latencyValue = 'N/A'; + var validLatency = false; + if (latencyResult && latencyResult.latencyknown) { + var latencyValue = latencyResult.latency; + latencyValue = Math.round(latencyValue * 100) / 100; + if (latencyValue <= 10) { + latencyClass = "good"; + validLatency = true; + } else if (latencyValue <= 20) { + latencyClass = "acceptable"; + validLatency = true; + } else { + latencyClass = "bad"; + } + } + else { + latencyClass = 'unknown'; + } + + validLatencyScore = validLatency; + + renderLatencyScore(latencyValue, latencyClass); + } + + function audioInputDeviceUnselected() { + validLatencyScore = false; + validIOScore = false; + initializeNextButtonState(); + resetFrameBuffers(); + clearInputPorts(); + } + + function renderScoringStarted() { + validLatencyScore = false; + validIOScore = false; + initializeNextButtonState(); + resetScoreReport(); + freezeAudioInteraction(); + renderLatencyScore(null, 'starting'); + renderIOScore(null, null, null, null, null, null); + } + + function renderScoringStopped() { + initializeNextButtonState(); + unfreezeAudioInteraction(); + } + + + function freezeAudioInteraction() { + $audioInput.attr("disabled", "disabled").easyDropDown('disable'); + $audioOutput.attr("disabled", "disabled").easyDropDown('disable'); + $frameSize.attr("disabled", "disabled").easyDropDown('disable'); + $bufferIn.attr("disabled", "disabled").easyDropDown('disable'); + $bufferOut.attr("disabled", "disabled").easyDropDown('disable'); + $asioInputControlBtn.on("click", false); + $asioOutputControlBtn.on("click", false); + $resyncBtn.on('click', false); + iCheckIgnore = true; + $inputChannels.find('input[type="checkbox"]').iCheck('disable'); + $outputChannels.find('input[type="checkbox"]').iCheck('disable'); + } + + function unfreezeAudioInteraction() { + $audioInput.removeAttr("disabled").easyDropDown('enable'); + $audioOutput.removeAttr("disabled").easyDropDown('enable'); + $frameSize.removeAttr("disabled").easyDropDown('enable'); + $bufferIn.removeAttr("disabled").easyDropDown('enable'); + $bufferOut.removeAttr("disabled").easyDropDown('enable'); + $asioInputControlBtn.off("click", false); + $asioOutputControlBtn.off("click", false); + $resyncBtn.off('click', false); + $inputChannels.find('input[type="checkbox"]').iCheck('enable'); + $outputChannels.find('input[type="checkbox"]').iCheck('enable'); + iCheckIgnore = false; + } + + // Given a latency structure, update the view. + function newFtueUpdateLatencyView(latency) { + var $report = $('.ftue-new .latency .report'); + var $instructions = $('.ftue-new .latency .instructions'); + var latencyClass = "neutral"; + var latencyValue = "N/A"; + var $saveButton = $('#btn-ftue-2-save'); + if (latency && latency.latencyknown) { + latencyValue = latency.latency; + // Round latency to two decimal places. + latencyValue = Math.round(latencyValue * 100) / 100; + if (latency.latency <= 10) { + latencyClass = "good"; + setSaveButtonState($saveButton, true); + } else if (latency.latency <= 20) { + latencyClass = "acceptable"; + setSaveButtonState($saveButton, true); + } else { + latencyClass = "bad"; + setSaveButtonState($saveButton, false); + } + } else { + latencyClass = "unknown"; + setSaveButtonState($saveButton, false); + } + + $('.ms-label', $report).html(latencyValue); + $('p', $report).html('milliseconds'); + + $report.removeClass('good acceptable bad unknown'); + $report.addClass(latencyClass); + + var instructionClasses = ['neutral', 'good', 'acceptable', 'unknown', 'bad', 'start', 'loading']; + $.each(instructionClasses, function (idx, val) { + $('p.' + val, $instructions).hide(); + }); + if (latency === 'loading') { + $('p.loading', $instructions).show(); + } else { + $('p.' + latencyClass, $instructions).show(); + renderStopNewFtueLatencyTesting(); + } + } + + function initializeWatchVideo() { + $watchVideoInput.unbind('click').click(function () { + + var audioDevice = findDevice(selectedAudioInput()); + if (!audioDevice) { + context.JK.Banner.showAlert('You must first choose an Audio Input Device so that we can determine which video to show you.'); + } + else { + var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; + + if (videoURL) { + $(this).attr('href', videoURL); + return true; + } + else { + context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')'); + } + } + + return false; + }); + + $watchVideoOutput.unbind('click').click(function () { + + var audioDevice = findDevice(selectedAudioOutput()); + if (!audioDevice) { + throw "this button should be hidden"; + } + else { + var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; + + if (videoURL) { + $(this).attr('href', videoURL); + return true; + } + else { + context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')'); + } + } + + return false; + }); + } + + function initializeASIOButtons() { + $asioInputControlBtn.unbind('click').click(function () { + context.jamClient.FTUEOpenControlPanel(); // TODO: supply with ID when VRFS-1707 is done + }); + $asioOutputControlBtn.unbind('click').click(function () { + context.jamClient.FTUEOpenControlPanel(); // TODO: supply with ID when VRFS-1707 is done + }); + } + + function initializeKnobs() { + $frameSize.unbind('change').change(function () { + jamClient.FTUESetFrameSize(selectedFramesize()); + }); + + $bufferIn.unbind('change').change(function () { + jamClient.FTUESetInputLatency(selectedBufferIn()); + }); + + $bufferOut.unbind('change').change(function () { + jamClient.FTUESetOutputLatency(selectedBufferOut()); + }); + } + + function initializeResync() { + $resyncBtn.unbind('click').click(function () { + attemptScore(); + return false; + }) + } + + function renderIOScoringStarted(secondsLeft) { + $ioCountdownSecs.text(secondsLeft); + $ioCountdown.show(); + } + + function renderIOScoringStopped() { + $ioCountdown.hide(); + } + + function renderIOCountdown(secondsLeft) { + $ioCountdownSecs.text(secondsLeft); + } + + // sets currentlySelectedDeviceInfo, which contains id, behavior, and info for input and output device + function cacheCurrentAudioInfo() { + + var audioInputDeviceId = selectedAudioInput(); + var audioOutputDeviceId = selectedAudioOutput(); + if (!audioInputDeviceId) { + audioInputDeviceUnselected(); + return false; + } + + var audioInputDevice = findDevice(audioInputDeviceId); + if (!audioInputDevice) { + context.JK.alertSupportedNeeded('Unable to find information for input device: ' + audioInputDeviceId); + return false; + } + + if (!audioOutputDeviceId) { + audioOutputDeviceId = audioInputDeviceId; + } + var audioOutputDevice = findDevice(audioOutputDeviceId); + if (!audioInputDevice) { + context.JK.alertSupportedNeeded('Unable to find information for output device: ' + audioOutputDeviceId); + return false; + } + + var input = findDevice(audioInputDeviceId); + var output = findDevice(audioOutputDeviceId); + + var inputBehavior = audioDeviceBehavior[input.type]; + var outputBehavior = audioDeviceBehavior[output.type]; + + selectedDeviceInfo = { + input: { + id: audioInputDeviceId, + info: input, + behavior: inputBehavior + }, + output: { + id: audioOutputDeviceId, + info: output, + behavior: outputBehavior + } + } + console.log("selectedDeviceInfo", selectedDeviceInfo); + } + + function changeDevice() { + + var audioInputDeviceId = selectedDeviceInfo.input.id; + var audioOutputDeviceId = selectedDeviceInfo.output.id; + + // don't re-assign input/output audio devices because it disturbs input/output track association + if (jamClient.FTUEGetInputMusicDevice() != audioInputDeviceId) { + jamClient.FTUESetInputMusicDevice(audioInputDeviceId); + } + if (jamClient.FTUEGetOutputMusicDevice() != audioOutputDeviceId) { + jamClient.FTUESetOutputMusicDevice(audioOutputDeviceId); + } + + initializeChannels(); + + var validDevice = autoSelectMinimumValidChannels(); + + if (!validDevice) { + return false; + } + + jamClient.FTUESetInputLatency(selectedBufferIn()); + jamClient.FTUESetOutputLatency(selectedBufferOut()); + jamClient.FTUESetFrameSize(selectedFramesize()); + + return true; + } + + function audioDeviceChanged() { + cacheCurrentAudioInfo(); + updateDialogForCurrentDevices(); + if (changeDevice()) { + attemptScore(); + } + } + + function updateDialogForCurrentDevices() { + var inputBehavior = selectedDeviceInfo.input.behavior; + var outputBehavior = selectedDeviceInfo.output.behavior; + + // handle framesize/buffers + if (inputBehavior && (inputBehavior.showKnobs || outputBehavior.showKnobs)) { + $knobs.css('visibility', 'visible') + } + else { + $knobs.css('visibility', 'hidden') + } + + // handle ASIO + if (inputBehavior) { + if (inputBehavior.showASIO && !outputBehavior.showASIO) { + // show single ASIO button + $asioInputControlBtn.text(ASIO_SETTINGS_DEFAULT_TEXT).show(); + $asioOutputControlBtn.hide(); + } + else if (!inputBehavior.showASIO && outputBehavior.showASIO) { + // show single ASIO button + $asioInputControlBtn.text(ASIO_SETTINGS_DEFAULT_TEXT).show(); + $asioOutputControlBtn.hide(); + } + else if (inputBehavior.showASIO && outputBehavior.showASIO) { + // show two ASIO buttons + $asioInputControlBtn.text(ASIO_SETTINGS_INPUT_TEXT).show(); + $asioOutputControlBtn.text(ASIO_SETTINGS_OUTPUT_TEXT).show(); + } + else { + // show no ASIO buttons + $asioInputControlBtn.hide(); + $asioOutputControlBtn.hide(); + } + } + else { + // show no ASIO buttons + $asioInputControlBtn.hide(); + $asioOutputControlBtn.hide(); + } + + // handle resync button + if (inputBehavior) { + $resyncBtn.css('visibility', 'visible'); + } + else { + $resyncBtn.css('visibility', 'hidden'); + } + } + + function attemptScore() { + renderScoringStarted(); + + // timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in + setTimeout(function () { + logger.debug("Calling FTUESave(false)"); + jamClient.FTUESave(false); + + var latency = jamClient.FTUEGetExpectedLatency(); + console.log("FTUEGetExpectedLatency: %o", latency); + + updateScoreReport(latency); + + // if there was a valid latency score, go on to the next step + if (validLatencyScore) { + renderIOScore(null, null, null, 'starting', 'starting', 'starting'); + var testTimeSeconds = 10; // allow 10 seconds for IO to establish itself + context.jamClient.FTUEStartIoPerfTest(); + renderIOScoringStarted(testTimeSeconds); + renderIOCountdown(testTimeSeconds); + var interval = setInterval(function () { + testTimeSeconds -= 1; + renderIOCountdown(testTimeSeconds); + if (testTimeSeconds == 0) { + clearInterval(interval); + renderIOScoringStopped(); + var io = context.jamClient.FTUEGetIoPerfData(); + + // take the higher variance, which is apparently actually std dev + var std = io.in_var > io.out_var ? io.in_var : io.out_var; + std = Math.round(std * 100) / 100; + // take the furthest-off-from-target io rate + var median = Math.abs(io.in_median - io.in_target) > Math.abs(io.out_median - io.out_target) ? [io.in_median, io.in_target] : [io.out_median, io.out_target]; + var medianTarget = median[1]; + median = Math.round(median[0]); + + var stdIOClass = 'bad'; + if (std <= 0.50) { + stdIOClass = 'good'; + } + else if (std <= 1.00) { + stdIOClass = 'acceptable'; + } + + var medianIOClass = 'bad'; + if (Math.abs(median - medianTarget) <= 1) { + medianIOClass = 'good'; + } + else if (Math.abs(median - medianTarget) <= 2) { + medianIOClass = 'acceptable'; + } + + // take worst between median or std + var ioClassToNumber = {bad: 2, acceptable: 1, good: 0} + var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass; + + // now base the overall IO score based on both values. + renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass); + + // lie for now until IO questions finalize + validIOScore = true; + + renderScoringStopped(); + } + }, 1000); + } + else { + renderIOScore(null, null, null, 'skip', 'skip', 'skip'); + renderScoringStopped(); + } + }, 250); + } + + function initializeAudioInputChanged() { + $audioInput.unbind('change').change(audioDeviceChanged); + } + + function initializeAudioOutputChanged() { + $audioOutput.unbind('change').change(audioDeviceChanged); + } + + function beforeShow() { + loadDevices(); + initializeFormElements(); + initializeNextButtonState(); + initializeWatchVideo(); + initializeASIOButtons(); + initializeKnobs(); + initializeResync(); + } + + function initialize(_$step) { + $step = _$step; + + $watchVideoInput = $step.find('.watch-video.audio-input'); + $watchVideoOutput = $step.find('.watch-video.audio-output'); + $audioInput = $step.find('.select-audio-input-device'); + $audioOutput = $step.find('.select-audio-output-device'); + $bufferIn = $step.find('.select-buffer-in'); + $bufferOut = $step.find('.select-buffer-out'); + $frameSize = $step.find('.select-frame-size'); + $inputChannels = $step.find('.input-ports'); + $outputChannels = $step.find('.output-ports'); + $knobs = $step.find('.frame-and-buffers'); + $scoreReport = $step.find('.results'); + $latencyScoreSection = $scoreReport.find('.latency-score-section'); + $latencyScore = $scoreReport.find('.latency-score'); + $latencyHeader = $scoreReport.find('.latency'); + $ioHeader = $scoreReport.find('.io'); + $ioScoreSection = $scoreReport.find('.io-score-section'); + $ioRate = $scoreReport.find('.io-rate'); + $ioRateScore = $scoreReport.find('.io-rate-score'); + $ioVar = $scoreReport.find('.io-var'); + $ioVarScore = $scoreReport.find('.io-var-score'); + $ioCountdown = $scoreReport.find('.io-countdown'); + $ioCountdownSecs = $scoreReport.find('.io-countdown .secs'); + $resultsText = $scoreReport.find('.results-text'); + $asioInputControlBtn = $step.find('.asio-settings-input-btn'); + $asioOutputControlBtn = $step.find('.asio-settings-output-btn'); + $resyncBtn = $step.find('.resync-btn'); + $templateAudioPort = $('#template-audio-port'); + + operatingSystem = context.jamClient.GetOSAsString(); + } + + this.beforeShow = beforeShow; + this.initialize = initialize; + + self = this; + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_success.js b/web/app/assets/javascripts/gear/step_success.js new file mode 100644 index 000000000..3dc049d2c --- /dev/null +++ b/web/app/assets/javascripts/gear/step_success.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepSuccess = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_understand_gear.js b/web/app/assets/javascripts/gear/step_understand_gear.js new file mode 100644 index 000000000..d579e84f2 --- /dev/null +++ b/web/app/assets/javascripts/gear/step_understand_gear.js @@ -0,0 +1,27 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepUnderstandGear = function (app) { + + var $step = null; + + function beforeShow() { + var $watchVideo = $step.find('.watch-video'); + var videoUrl = 'https://www.youtube.com/watch?v=VexH4834o9I'; + if (operatingSystem == "Win32") { + $watchVideo.attr('href', 'https://www.youtube.com/watch?v=VexH4834o9I'); + } + $watchVideo.attr('href', videoUrl); + } + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear_wizard.js b/web/app/assets/javascripts/gear_wizard.js deleted file mode 100644 index 9012c91be..000000000 --- a/web/app/assets/javascripts/gear_wizard.js +++ /dev/null @@ -1,909 +0,0 @@ -(function (context, $) { - - "use strict"; - - - context.JK = context.JK || {}; - context.JK.GearWizard = function (app) { - - var ASSIGNMENT = context.JK.ASSIGNMENT; - var VOICE_CHAT = context.JK.VOICE_CHAT; - - var $dialog = null; - var $wizardSteps = null; - var $currentWizardStep = null; - var step = 0; - var $templateSteps = null; - var $templateButtons = null; - var $templateAudioPort = null; - var $ftueButtons = null; - var self = null; - var operatingSystem = null; - - // populated by loadDevices - var deviceInformation = null; - var musicPorts = null; - - - var validLatencyScore = false; - var validIOScore = false; - - // SELECT TRACKS STATE - - var TOTAL_STEPS = 7; - var STEP_INTRO = 0; - var STEP_SELECT_DEVICE = 1; - var STEP_SELECT_TRACKS = 2; - var STEP_SELECT_CHAT = 3; - var STEP_DIRECT_MONITOR = 4; - var STEP_ROUTER_NETWORK = 5; - var STEP_SUCCESS = 6; - - var PROFILE_DEV_SEP_TOKEN = '^'; - - var iCheckIgnore = false; - - var audioDeviceBehavior = { - MacOSX_builtin: { - display: 'MacOSX Built-In', - videoURL: undefined - }, - MacOSX_interface: { - display: 'MacOSX external interface', - videoURL: undefined - }, - Win32_wdm: { - display: 'Windows WDM', - videoURL: undefined - }, - Win32_asio: { - display: 'Windows ASIO', - videoURL: undefined - }, - Win32_asio4all: { - display: 'Windows ASIO4ALL', - videoURL: undefined - }, - Linux: { - display: 'Linux', - videoURL: undefined - } - } - - function beforeShowIntro() { - var $watchVideo = $currentWizardStep.find('.watch-video'); - var videoUrl = 'https://www.youtube.com/watch?v=VexH4834o9I'; - if (operatingSystem == "Win32") { - $watchVideo.attr('href', 'https://www.youtube.com/watch?v=VexH4834o9I'); - } - $watchVideo.attr('href', videoUrl); - } - - function beforeSelectDevice() { - - var $watchVideoInput = $currentWizardStep.find('.watch-video.audio-input'); - var $watchVideoOutput = $currentWizardStep.find('.watch-video.audio-output'); - var $audioInput = $currentWizardStep.find('.select-audio-input-device'); - var $audioOutput = $currentWizardStep.find('.select-audio-output-device'); - var $bufferIn = $currentWizardStep.find('.select-buffer-in'); - var $bufferOut = $currentWizardStep.find('.select-buffer-out'); - var $frameSize = $currentWizardStep.find('.select-frame-size'); - var $inputChannels = $currentWizardStep.find('.input-ports'); - var $outputChannels = $currentWizardStep.find('.output-ports'); - var $scoreReport = $currentWizardStep.find('.results'); - var $latencyScoreSection = $scoreReport.find('.latency-score-section'); - var $latencyScore = $scoreReport.find('.latency-score'); - var $ioScoreSection = $scoreReport.find('.io-score-section'); - var $ioRateScore = $scoreReport.find('.io-rate-score'); - var $ioVarScore = $scoreReport.find('.io-var-score'); - var $ioCountdown = $scoreReport.find('.io-countdown'); - var $ioCountdownSecs = $scoreReport.find('.io-countdown .secs'); - var $nextButton = $ftueButtons.find('.btn-next'); - var $asioControlPanelBtn = $currentWizardStep.find('.asio-settings-btn'); - var $resyncBtn = $currentWizardStep.find('resync-btn') - - // should return one of: - // * MacOSX_builtin - // * MACOSX_interface - // * Win32_wdm - // * Win32_asio - // * Win32_asio4all - // * Linux - function determineDeviceType(deviceId, displayName) { - if (operatingSystem == "MacOSX") { - if (displayName.toLowerCase().trim() == "built-in") { - return "MacOSX_builtin"; - } - else { - return "MacOSX_interface"; - } - } - else if (operatingSystem == "Win32") { - if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) { - return "Win32_wdm"; - } - else if (displayName.toLowerCase().indexOf("asio4all") > -1) { - return "Win32_asio4all" - } - else { - return "Win32_asio"; - } - } - else { - return "Linux"; - } - } - - function loadDevices() { - - var oldDevices = context.jamClient.FTUEGetDevices(false); - var devices = context.jamClient.FTUEGetAudioDevices(); - console.log("oldDevices: " + JSON.stringify(oldDevices)); - console.log("devices: " + JSON.stringify(devices)); - - var loadedDevices = {}; - - // augment these devices by determining their type - context._.each(devices.devices, function (device) { - - if(device.name == "JamKazam Virtual Monitor") { - return; - } - - var deviceInfo = {}; - - deviceInfo.id = device.guid; - deviceInfo.type = determineDeviceType(device.guid, device.display_name); - console.log("deviceInfo.type: " + deviceInfo.type) - deviceInfo.displayType = audioDeviceBehavior[deviceInfo.type].display; - deviceInfo.displayName = device.display_name; - - loadedDevices[device.guid] = deviceInfo; - - logger.debug("loaded device: ", deviceInfo); - }) - - deviceInformation = loadedDevices; - - logger.debug(context.JK.dlen(deviceInformation) + " devices loaded.", deviceInformation); - } - - // returns a deviceInfo hash for the device matching the deviceId, or undefined. - function findDevice(deviceId) { - return deviceInformation[deviceId]; - } - - function selectedAudioInput() { - return $audioInput.val(); - } - - function selectedAudioOutput() { - return $audioOutput.val(); - } - - function selectedFramesize() { - return parseFloat($frameSize.val()); - } - - function selectedBufferIn() { - return parseFloat($frameSize.val()); - } - - function selectedBufferOut() { - return parseFloat($frameSize.val()); - } - - function initializeNextButtonState() { - $nextButton.removeClass('button-orange button-grey'); - - if (validLatencyScore) $nextButton.addClass('button-orange'); - else $nextButton.addClass('button-grey'); - } - - function initializeAudioInput() { - var optionsHtml = ''; - optionsHtml = ''; - context._.each(deviceInformation, function (deviceInfo, deviceId) { - - console.log(arguments) - optionsHtml += ''; - }); - $audioInput.html(optionsHtml); - context.JK.dropdown($audioInput); - - initializeAudioInputChanged(); - } - - function initializeAudioOutput() { - var optionsHtml = ''; - optionsHtml = ''; - context._.each(deviceInformation, function (deviceInfo, deviceId) { - optionsHtml += ''; - }); - $audioOutput.html(optionsHtml); - context.JK.dropdown($audioOutput); - - initializeAudioOutputChanged(); - } - - function initializeFramesize() { - context.JK.dropdown($frameSize); - } - - function initializeBuffers() { - context.JK.dropdown($bufferIn); - context.JK.dropdown($bufferOut); - } - - - // reloads the backend's channel state for the currently selected audio devices, - // and update's the UI accordingly - function initializeChannels() { - musicPorts = jamClient.FTUEGetChannels(); - console.log("musicPorts: %o", JSON.stringify(musicPorts)); - - initializeInputPorts(musicPorts); - initializeOutputPorts(musicPorts); - } - - // during this phase of the FTUE, we have to assign selected input channels - // to tracks. The user, however, does not have a way to indicate which channel - // goes to which track (that's not until the next step of the wizard). - // so, we just auto-generate a valid assignment - function newInputAssignment() { - var assigned = 0; - context._.each(musicPorts.inputs, function(inputChannel) { - if(isChannelAssigned(inputChannel)) { - assigned += 1; - } - }); - - var newAssignment = Math.floor(assigned / 2) + 1; - return newAssignment; - } - - function inputChannelChanged() { - if(iCheckIgnore) return; - - var $checkbox = $(this); - var channelId = $checkbox.attr('data-id'); - var isChecked = $checkbox.is(':checked'); - - if(isChecked) { - var newAssignment = newInputAssignment(); - logger.debug("assigning input channel %o to track: %o", channelId, newAssignment); - context.jamClient.TrackSetAssignment(channelId, true, newAssignment); - } - else { - logger.debug("unassigning input channel %o", channelId); - context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED); - // unassigning creates a hole in our auto-assigned tracks. reassign them all to keep it consistent - var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked'); - var assigned = 0; - context._.each($assignedInputs, function(assignedInput) { - var $assignedInput = $(assignedInput); - var assignedChannelId = $assignedInput.attr('data-id'); - var newAssignment = Math.floor(assigned / 2) + 1; - logger.debug("re-assigning input channel %o to track: %o", assignedChannelId, newAssignment); - context.jamClient.TrackSetAssignment(assignedChannelId, true, newAssignment); - assigned += 1; - }); - } - - initializeChannels(); - } - - // should be called in a ifChanged callback if you want to cancel. - // you have to use this instead of 'return false' like a typical input 'change' event. - function cancelICheckChange($checkbox) { - iCheckIgnore = true; - var checked = $checkbox.is(':checked'); - setTimeout(function() { - if(checked) $checkbox.iCheck('uncheck').removeAttr('checked'); - else $checkbox.iCheck('check').attr('checked', 'checked'); - iCheckIgnore = false; - }, 1); - } - - function outputChannelChanged() { - if(iCheckIgnore) return; - var $checkbox = $(this); - var channelId = $checkbox.attr('data-id'); - var isChecked = $checkbox.is(':checked'); - - // don't allow more than 2 output channels selected at once - if($outputChannels.find('input[type="checkbox"]:checked').length > 2) { - context.JK.Banner.showAlert('You can only have a maximum of 2 output ports selected.'); - // can't allow uncheck of last output - cancelICheckChange($checkbox); - return; - } - - if(isChecked) { - logger.debug("assigning output channel %o", channelId); - context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT); - } - else { - logger.debug("unassigning output channel %o", channelId); - context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED); - } - - initializeChannels(); - } - - // checks if it's an assigned OUTPUT or ASSIGNED CHAT - function isChannelAssigned(channel) { - return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0; - } - - function initializeInputPorts(musicPorts) { - $inputChannels.empty(); - var inputPorts = musicPorts.inputs; - context._.each(inputPorts, function(inputChannel) { - var $inputChannel = $(context._.template($templateAudioPort.html(), inputChannel, { variable: 'data' })); - var $checkbox = $inputChannel.find('input'); - if(isChannelAssigned(inputChannel)) { - $checkbox.attr('checked', 'checked'); - } - context.JK.checkbox($checkbox); - $checkbox.on('ifChanged', inputChannelChanged); - $inputChannels.append($inputChannel); - }); - } - - function initializeOutputPorts(musicPorts) { - $outputChannels.empty(); - var outputChannels = musicPorts.outputs; - context._.each(outputChannels, function(outputChannel) { - var $outputPort = $(context._.template($templateAudioPort.html(), outputChannel, { variable: 'data' })); - var $checkbox = $outputPort.find('input'); - if(isChannelAssigned(outputChannel)) { - $checkbox.attr('checked', 'checked'); - } - context.JK.checkbox($checkbox); - $checkbox.on('ifChanged', outputChannelChanged); - $outputChannels.append($outputPort); - }); - } - - function initializeFormElements() { - if (!deviceInformation) throw "devices are not initialized"; - - initializeAudioInput(); - initializeAudioOutput(); - initializeFramesize(); - initializeBuffers(); - } - - function resetFrameBuffers() { - $frameSize.val('2.5'); - $bufferIn.val('0'); - $bufferOut.val('0'); - } - - function clearInputPorts() { - $inputChannels.empty(); - } - - function clearOutputPorts() { - $outputChannels.empty(); - } - - function resetScoreReport() { - $ioRateScore.empty(); - $ioVarScore.empty(); - $latencyScore.empty(); - } - - function renderLatencyScore(latencyValue, latencyClass) { - if(latencyValue) { - $latencyScore.text(latencyValue + ' ms'); - } - else { - $latencyScore.text(''); - } - $latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass); - } - - // std deviation is the worst value between in/out - // media is the worst value between in/out - // io is the value returned by the backend, which has more info - // ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad' - function renderIOScore(std, median, ioData, ioClass) { - $ioRateScore.text(median ? median : ''); - $ioVarScore.text(std ? std : ''); - $ioScoreSection.removeClass('good acceptable bad unknown starting skip').addClass(ioClass); - // TODO: show help bubble of all data in IO data - } - - function updateScoreReport(latencyResult) { - var latencyClass = "neutral"; - var latencyValue = 'N/A'; - var validLatency = false; - if (latencyResult && latencyResult.latencyknown) { - var latencyValue = latencyResult.latency; - latencyValue = Math.round(latencyValue * 100) / 100; - if (latencyValue <= 10) { - latencyClass = "good"; - validLatency = true; - } else if (latencyValue <= 20) { - latencyClass = "acceptable"; - validLatency = true; - } else { - latencyClass = "bad"; - } - } - else { - latencyClass = 'unknown'; - } - - validLatencyScore = validLatency; - - renderLatencyScore(latencyValue, latencyClass); - } - - function audioInputDeviceUnselected() { - validLatencyScore = false; - initializeNextButtonState(); - resetFrameBuffers(); - clearInputPorts(); - } - - function renderScoringStarted() { - validLatencyScore = false; - initializeNextButtonState(); - resetScoreReport(); - freezeAudioInteraction(); - renderLatencyScore(null, 'starting'); - } - - function renderScoringStopped() { - initializeNextButtonState(); - unfreezeAudioInteraction(); - } - - - function freezeAudioInteraction() { - $audioInput.attr("disabled", "disabled").easyDropDown('disable'); - $audioOutput.attr("disabled", "disabled").easyDropDown('disable'); - $frameSize.attr("disabled", "disabled").easyDropDown('disable'); - $bufferIn.attr("disabled", "disabled").easyDropDown('disable'); - $bufferOut.attr("disabled", "disabled").easyDropDown('disable'); - $asioControlPanelBtn.on("click", false); - $resyncBtn.on('click', false); - iCheckIgnore = true; - $inputChannels.find('input[type="checkbox"]').iCheck('disable'); - $outputChannels.find('input[type="checkbox"]').iCheck('disable'); - } - - function unfreezeAudioInteraction() { - $audioInput.removeAttr("disabled").easyDropDown('enable'); - $audioOutput.removeAttr("disabled").easyDropDown('enable'); - $frameSize.removeAttr("disabled").easyDropDown('enable'); - $bufferIn.removeAttr("disabled").easyDropDown('enable'); - $bufferOut.removeAttr("disabled").easyDropDown('enable'); - $asioControlPanelBtn.off("click", false); - $resyncBtn.off('click', false); - $inputChannels.find('input[type="checkbox"]').iCheck('enable'); - $outputChannels.find('input[type="checkbox"]').iCheck('enable'); - iCheckIgnore = false; - } - - // Given a latency structure, update the view. - function newFtueUpdateLatencyView(latency) { - var $report = $('.ftue-new .latency .report'); - var $instructions = $('.ftue-new .latency .instructions'); - var latencyClass = "neutral"; - var latencyValue = "N/A"; - var $saveButton = $('#btn-ftue-2-save'); - if (latency && latency.latencyknown) { - latencyValue = latency.latency; - // Round latency to two decimal places. - latencyValue = Math.round(latencyValue * 100) / 100; - if (latency.latency <= 10) { - latencyClass = "good"; - setSaveButtonState($saveButton, true); - } else if (latency.latency <= 20) { - latencyClass = "acceptable"; - setSaveButtonState($saveButton, true); - } else { - latencyClass = "bad"; - setSaveButtonState($saveButton, false); - } - } else { - latencyClass = "unknown"; - setSaveButtonState($saveButton, false); - } - - $('.ms-label', $report).html(latencyValue); - $('p', $report).html('milliseconds'); - - $report.removeClass('good acceptable bad unknown'); - $report.addClass(latencyClass); - - var instructionClasses = ['neutral', 'good', 'acceptable', 'unknown', 'bad', 'start', 'loading']; - $.each(instructionClasses, function (idx, val) { - $('p.' + val, $instructions).hide(); - }); - if (latency === 'loading') { - $('p.loading', $instructions).show(); - } else { - $('p.' + latencyClass, $instructions).show(); - renderStopNewFtueLatencyTesting(); - } - } - - function initializeWatchVideo() { - $watchVideoInput.unbind('click').click(function () { - - var audioDevice = findDevice(selectedAudioInput()); - if (!audioDevice) { - context.JK.Banner.showAlert('You must first choose an Audio Input Device so that we can determine which video to show you.'); - } - else { - var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; - - if (videoURL) { - $(this).attr('href', videoURL); - return true; - } - else { - context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')'); - } - } - - return false; - }); - - $watchVideoOutput.unbind('click').click(function () { - - var audioDevice = findDevice(selectedAudioOutput()); - if (!audioDevice) { - throw "this button should be hidden"; - } - else { - var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; - - if (videoURL) { - $(this).attr('href', videoURL); - return true; - } - else { - context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')'); - } - } - - return false; - }); - } - - function renderIOScoringStarted(secondsLeft) { - $ioCountdownSecs.text(secondsLeft); - $ioCountdown.show(); - } - - function renderIOScoringStopped() { - $ioCountdown.hide(); - } - - function renderIOCountdown(secondsLeft) { - $ioCountdownSecs.text(secondsLeft); - } - - function attemptScore() { - var audioInputDeviceId = selectedAudioInput(); - var audioOutputDeviceId = selectedAudioOutput(); - if (!audioInputDeviceId) { - audioInputDeviceUnselected(); - return false; - } - - var audioInputDevice = findDevice(audioInputDeviceId); - if (!audioInputDevice) { - context.JK.alertSupportedNeeded('Unable to find information for input device: ' + audioInputDeviceId); - return false; - } - - if(!audioOutputDeviceId) { - audioOutputDeviceId = audioInputDeviceId; - } - var audioOutputDevice = findDevice(audioOutputDeviceId); - if (!audioInputDevice) { - context.JK.alertSupportedNeeded('Unable to find information for output device: ' + audioOutputDeviceId); - return false; - } - - jamClient.FTUESetInputMusicDevice(audioInputDeviceId); - jamClient.FTUESetOutputMusicDevice(audioOutputDeviceId); - - initializeChannels(); - - jamClient.FTUESetInputLatency(selectedBufferIn()); - jamClient.FTUESetOutputLatency(selectedBufferOut()); - jamClient.FTUESetFrameSize(selectedFramesize()); - - renderScoringStarted(); - logger.debug("Calling FTUESave(false)"); - jamClient.FTUESave(false); - - var latency = jamClient.FTUEGetExpectedLatency(); - console.log("FTUEGetExpectedLatency: %o", latency); - - updateScoreReport(latency); - - // if there was a valid latency score, go on to the next step - if(validLatencyScore) { - renderIOScore(null, null, null, 'starting'); - var testTimeSeconds = 10; // allow 10 seconds for IO to establish itself - context.jamClient.FTUEStartIoPerfTest(); - renderIOScoringStarted(testTimeSeconds); - renderIOCountdown(testTimeSeconds); - var interval = setInterval(function() { - testTimeSeconds -= 1; - renderIOCountdown(testTimeSeconds); - if(testTimeSeconds == 0) { - clearInterval(interval); - renderIOScoringStopped(); - var io = context.jamClient.FTUEGetIoPerfData(); - - console.log("io: ", io); - - // take the higher variance, which is apparently actually std dev - var std = io.in_var > io.out_var ? io.in_var : io.out_var; - std = Math.round(std * 100) / 100; - // take the furthest-off-from-target io rate - var median = Math.abs(io.in_median - io.in_target ) > Math.abs(io.out_median - io.out_target ) ? [io.in_median, io.in_target] : [io.out_median, io.out_target]; - var medianTarget = median[1]; - median = Math.round(median[0]); - - var stdIOClass = 'bad'; - if(std <= 0.50) { - stdIOClass = 'good'; - } - else if(std <= 1.00) { - stdIOClass = 'acceptable'; - } - - var medianIOClass = 'bad'; - if(Math.abs(median - medianTarget) <= 1) { - medianIOClass = 'good'; - } - else if(Math.abs(median - medianTarget) <= 2) { - medianIOClass = 'acceptable'; - } - - // now base the overall IO score based on both values. - renderIOScore(std, median, io, ioClass); - - // lie for now until IO questions finalize - validIOScore = true; - - renderScoringStopped(); - } - }, 1000); - } - else { - renderIOScore(null, null, null, 'skip'); - renderScoringStopped(); - } - - } - - function initializeAudioInputChanged() { - $audioInput.unbind('change').change(attemptScore); - } - - function initializeAudioOutputChanged() { - - } - - function initializeStep() { - loadDevices(); - initializeFormElements(); - initializeNextButtonState(); - initializeWatchVideo(); - } - - initializeStep(); - } - - function beforeSelectTracks() { - - } - - function beforeSelectChat() { - - } - - function beforeDirectMonitor() { - - } - - function beforeTestNetwork() { - - } - - function beforeSuccess() { - - } - - var STEPS = { - 0: { - beforeShow: beforeShowIntro - }, - 1: { - beforeShow: beforeSelectDevice - }, - 2: { - beforeShow: beforeSelectTracks - }, - 3: { - beforeShow: beforeSelectChat - }, - 4: { - beforeShow: beforeDirectMonitor - }, - 5: { - beforeShow: beforeTestNetwork - }, - 6: { - beforeShow: beforeSuccess - } - } - - function beforeShowStep($step) { - var stepInfo = STEPS[step]; - - if (!stepInfo) { - throw "unknown step: " + step; - } - - stepInfo.beforeShow.call(self); - } - - function moveToStep() { - var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); - - $wizardSteps.hide(); - - $currentWizardStep = $nextWizardStep; - - var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); - var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); - $activeStep.addClass('.active'); - $activeStep.next().show(); // show the .ftue-step-title - $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); - beforeShowStep($currentWizardStep); - $currentWizardStep.show(); - - // update buttons - var $ftueButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); - - - var $btnBack = $ftueButtonsContent.find('.btn-back'); - var $btnNext = $ftueButtonsContent.find('.btn-next'); - var $btnClose = $ftueButtonsContent.find('.btn-close'); - var $btnCancel = $ftueButtonsContent.find('.btn-cancel'); - - // hide back button if 1st step or last step - if (step == 0 && step == TOTAL_STEPS - 1) { - $btnBack.hide(); - } - // hide next button if not on last step - if (step == TOTAL_STEPS - 1) { - $btnNext.hide(); - } - // hide close if on last step - if (step != TOTAL_STEPS - 1) { - $btnClose.hide(); - } - // hide cancel if not on last step - if (step == TOTAL_STEPS - 1) { - $btnCancel.hide(); - } - - $btnNext.on('click', next); - $btnBack.on('click', back); - $btnClose.on('click', closeDialog); - $btnCancel.on('click', closeDialog); - - $ftueButtons.empty(); - $ftueButtons.append($ftueButtonsContent); - } - - function reset() { - $currentWizardStep = null; - } - - // checks if we already have a profile called 'FTUE...'; if not, create one. if so, re-use it. - function findOrCreateFTUEProfile() { - var profileName = context.jamClient.FTUEGetMusicProfileName(); - - logger.debug("current profile name: " + profileName); - - if(profileName && profileName.indexOf('FTUE') == 0) { - - } - else { - var newProfileName = 'FTUEAttempt-' + new Date().getTime().toString(); - logger.debug("setting FTUE-prefixed profile name to: " + newProfileName); - context.jamClient.FTUESetMusicProfileName(newProfileName); - } - - var profileName = context.jamClient.FTUEGetMusicProfileName(); - - logger.debug("name on exit: " + profileName); - - } - - function beforeShow(args) { - context.jamClient.FTUECancel(); - findOrCreateFTUEProfile(); - - step = args.d1; - if (!step) step = 0; - step = parseInt(step); - moveToStep(); - } - - function afterShow() { - - } - - function afterHide() { - context.jamClient.FTUECancel(); - } - - function back() { - if ($(this).is('.button-grey')) return; - step = step - 1; - moveToStep(); - return false; - } - - function next() { - if ($(this).is('.button-grey')) return; - - step = step + 1; - - moveToStep(); - return false; - } - - function closeDialog() { - app.layout.closeDialog('gear-wizard'); - return false; - } - - function events() { - } - - function route() { - - } - - function initialize() { - - var dialogBindings = { beforeShow: beforeShow, afterShow: afterShow, afterHide: afterHide }; - - app.bindDialog('gear-wizard', dialogBindings); - - $dialog = $('#gear-wizard-dialog'); - $wizardSteps = $dialog.find('.wizard-step'); - $templateSteps = $('#template-ftuesteps'); - $templateButtons = $('#template-ftue-buttons'); - $templateAudioPort = $('#template-audio-port'); - $ftueButtons = $dialog.find('.ftue-buttons'); - - operatingSystem = context.jamClient.GetOSAsString(); - - events(); - } - - this.initialize = initialize; - - self = this; - return this; - }; - -})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index aa2ac4bf6..bf8e1384c 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -115,6 +115,7 @@ &.list.ports { height:100px; + overflow:auto; } &.instructions { @@ -151,18 +152,32 @@ margin-bottom:20px; } - .asio-settings-btn, .resync-btn { + .asio-settings-input-btn, .asio-settings-output-btn, .resync-btn { width:80%; display:inline-block; text-align:center; } - .asio-settings-btn { + .asio-settings-input-btn, .asio-settings-output-btn { margin-top:10px; } + + .asio-settings-input-btn { + display:none; + } + + .asio-settings-output-btn { + display:none; + } + .resync-btn { margin-top:10px; + visibility:hidden; + } + + .frame-and-buffers { + display:none; } .framesize { @@ -197,12 +212,24 @@ } } + .audio-port { + white-space: nowrap; + } + + .audio-channels { + margin-top:15px; + } + .ftue-box.results { height: 230px !important; padding:0; + .io, .latency { + display:none; + } + .scoring-section { font-size:15px; @include border_box_sizing; @@ -220,6 +247,11 @@ &.unknown { background-color:#999; } + &.skip { + .io-skip-msg { + display:inline; + } + } } .io-countdown { @@ -236,9 +268,48 @@ .io-skip-msg { display:none; + } - .scoring-section.skip & { - display:inline; + .io-rate { + display:none; + } + .io-var { + display:none; + } + + ul.results-text { + padding:10px 8px; + + li { + display:none + } + + &[latency-score="good"] li.latency-good { + display:list-item; + } + &[latency-score="acceptable"] li.latency-acceptable { + display:list-item; + } + &[latency-score="bad"] li.latency-bad { + display:list-item; + } + &[io-var-score="good"] li.io-var-good { + display:list-item; + } + &[io-var-score="acceptable"] li.io-var-acceptable { + display:list-item; + } + &[io-var-score="bad"] li.io-var-bad { + display:list-item; + } + &[io-rate-score="good"] li.io-rate-good { + display:list-item; + } + &[io-rate-score="acceptable"] li.io-rate-acceptable { + display:list-item; + } + &[io-rate-score="bad"] li.io-rate-bad { + display:list-item; } } } diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 57f0ef1f8..01b4637de 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -37,15 +37,16 @@ %h2 Audio Input Device %select.w100.select-audio-input-device %option None - %h2 Audio Input Ports + %h2.audio-channels Audio Input Ports .ftue-box.list.ports.input-ports - %a.button-orange.asio-settings-btn ASIO SETTINGS... + %a.button-orange.asio-settings-input-btn ASIO SETTINGS... + %a.button-orange.asio-settings-output-btn ASIO SETTINGS... %a.button-orange.resync-btn RESYNC .wizard-step-column %h2 Audio Output Device %select.w100.select-audio-output-device %option Same as input - %h2 Audio Output Ports + %h2.audio-channels Audio Output Ports .ftue-box.list.ports.output-ports .frame-and-buffers .framesize @@ -91,13 +92,27 @@ .p5 .io I/O %span.io-skip-msg - Skipped + Not Tested %span.io-countdown %span.secs seconds left - %span.io-rate-score - %span.io-var-score - + %span.io-rate< + Rate= + %span.io-rate-score> + %span.io-var< + Var= + %span.io-var-score> + .clearall + %ul.results-text + %li.latency-good Your latency is good. + %li.latency-acceptable Your latency is acceptable. + %li.latency-bad Your latency is poor. + %li.io-rate-good Your I/O rate is good. + %li.io-rate-acceptable Your I/O rate is acceptable. + %li.io-rate-bad Your I/O rate is poor. + %li.io-var-good Your I/O variance is good. + %li.io-var-acceptable Your I/O variance is acceptable. + %li.io-var-bad Your I/O variance is poor. .clearall .wizard-step{ 'layout-wizard-step' => "2", 'dialog-title' => "Configure Tracks", 'dialog-purpose' => "ConfigureTracks" } From df5fae1981ff06dae9fc983fbe7e9563fcbad70a Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 13 May 2014 08:20:41 -0500 Subject: [PATCH 04/29] * wip --- .../javascripts/accounts_audio_profile.js | 2 +- web/app/assets/javascripts/application.js | 1 + web/app/assets/javascripts/fakeJamClient.js | 81 ++++ .../assets/javascripts/gear/gear_wizard.js | 39 +- .../javascripts/gear/step_configure_tracks.js | 105 +++++- .../gear/step_configure_voice_chat.js | 5 + .../gear/step_direct_monitoring.js | 5 + .../javascripts/gear/step_network_test.js | 5 + .../javascripts/gear/step_select_gear.js | 353 ++++++++++++------ .../assets/javascripts/gear/step_success.js | 5 + .../javascripts/gear/step_understand_gear.js | 4 + web/app/assets/javascripts/globals.js | 2 + web/app/assets/javascripts/utils.js | 36 +- .../assets/stylesheets/client/banner.css.scss | 16 + .../assets/stylesheets/client/ftue.css.scss | 9 +- .../stylesheets/client/gearWizard.css.scss | 103 ++++- .../stylesheets/client/session.css.scss | 46 +-- web/app/helpers/client_helper.rb | 1 + web/app/views/clients/_banner.html.erb | 2 +- web/app/views/clients/_help.html.erb | 12 + .../views/clients/gear/_gear_wizard.html.haml | 27 +- web/config/application.rb | 3 + web/config/environments/test.rb | 2 + web/spec/features/gear_wizard_spec.rb | 18 + web/vendor/assets/javascripts/jquery.bt.js | 12 +- 25 files changed, 720 insertions(+), 174 deletions(-) create mode 100644 web/spec/features/gear_wizard_spec.rb diff --git a/web/app/assets/javascripts/accounts_audio_profile.js b/web/app/assets/javascripts/accounts_audio_profile.js index f76e1f08b..9cc73ac57 100644 --- a/web/app/assets/javascripts/accounts_audio_profile.js +++ b/web/app/assets/javascripts/accounts_audio_profile.js @@ -67,7 +67,7 @@ function handleStartAudioQualification() { - if(true) { + if(false) { app.layout.startNewFtue(); } else { diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index cd7ff04cb..891b4d180 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -14,6 +14,7 @@ //= require jquery.monkeypatch //= require jquery_ujs //= require jquery.ui.draggable +//= require jquery.ui.droppable //= require jquery.bt //= require jquery.icheck //= require jquery.color diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 1175d9b28..bdede9b5f 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -124,6 +124,83 @@ "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" }; } + function FTUEGetChannels() { + return { + "inputs": [ + { + "assignment": 1, + "chat": false, + "device_id": "Built-in Microph", + "device_type": 5, + "id": "i~5~Built-in Microph~0~0~Built-in", + "name": "Built-in Microph - Left", + "number": 0 + }, + { + "assignment": 0, + "chat": false, + "device_id": "Built-in Microph", + "device_type": 5, + "id": "i~5~Built-in Microph~1~0~Built-in", + "name": "Built-in Microph - Right", + "number": 1 + } + ], + "outputs": [ + { + "assignment": -1, + "chat": false, + "device_id": "Built-in Output", + "device_type": 5, + "id": "o~5~Built-in Output~0~0~Built-in", + "name": "Built-in Output - Left", + "number": 0 + }, + { + "assignment": -1, + "chat": false, + "device_id": "Built-in Output", + "device_type": 5, + "id": "o~5~Built-in Output~1~0~Built-in", + "name": "Built-in Output - Right", + "number": 1 + } + ] + }; + } + function FTUEGetAudioDevices() { + return { + "devices": [ + { + "display_name": "Built-in", + "guid": "Built-in", + "input_count": 1, + "name": "Built-in", + "output_count": 1, + "port_audio_name": "Built-in" + }, + { + "display_name": "JamKazam Virtual Monitor", + "guid": "JamKazam Virtual Monitor", + "input_count": 0, + "name": "JamKazam Virtual Monitor", + "output_count": 1, + "port_audio_name": "JamKazam Virtual Monitor" + } + ] + }; + } + function FTUEStartIoPerfTest() {} + function FTUEGetIoPerfData() { + return { + "in_var" : 0.15, + "out_var" : 0.25, + "in_median" : 399.9, + "out_median" : 400.3, + "in_target" : 400, + "out_target" : 400 + }; + } function FTUESetInputMusicDevice() { } function FTUESetOutputMusicDevice() { } function FTUEGetInputMusicDevice() { return null; } @@ -678,6 +755,10 @@ this.FTUEGetOutputMusicDevice = FTUEGetOutputMusicDevice; this.FTUEGetChatInputVolume = FTUEGetChatInputVolume; this.FTUEGetChatInputs = FTUEGetChatInputs; + this.FTUEGetChannels = FTUEGetChannels; + this.FTUEGetAudioDevices = FTUEGetAudioDevices; + this.FTUEStartIoPerfTest = FTUEStartIoPerfTest; + this.FTUEGetIoPerfData = FTUEGetIoPerfData; this.FTUEGetDevices = FTUEGetDevices; this.FTUEGetFrameSize = FTUEGetFrameSize; this.FTUECancel = FTUECancel; diff --git a/web/app/assets/javascripts/gear/gear_wizard.js b/web/app/assets/javascripts/gear/gear_wizard.js index bdffef0dc..a1c4b382b 100644 --- a/web/app/assets/javascripts/gear/gear_wizard.js +++ b/web/app/assets/javascripts/gear/gear_wizard.js @@ -14,8 +14,8 @@ var $templateButtons = null; var $templateAudioPort = null; var $ftueButtons = null; - var $btnBack = null; var $btnNext = null; + var $btnBack = null; var $btnClose = null; var $btnCancel = null; @@ -48,6 +48,18 @@ 6: stepSuccess } + function beforeHideStep($step) { + var stepInfo = STEPS[step]; + + if (!stepInfo) { + throw "unknown step: " + step; + } + + if(stepInfo.beforeHide) { + stepInfo.beforeHide.call(stepInfo); + } + } + function beforeShowStep($step) { var stepInfo = STEPS[step]; @@ -61,6 +73,8 @@ function moveToStep() { var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); + beforeHideStep($currentWizardStep); + $wizardSteps.hide(); $currentWizardStep = $nextWizardStep; @@ -165,6 +179,12 @@ function next() { if ($(this).is('.button-grey')) return false; + var stepInfo = STEPS[step]; + if(stepInfo.handleNext) { + var result = stepInfo.handleNext.call(stepInfo); + if(!result) {return false;} + } + step = step + 1; moveToStep(); @@ -172,6 +192,7 @@ } function closeDialog() { + beforeHideStep($currentWizardStep); app.layout.closeDialog('gear-wizard'); return false; } @@ -198,6 +219,21 @@ } } + function setBackState(enabled) { + + if(!$btnBack) return; + + $btnBack.removeClass('button-orange button-grey'); + + if (enabled) { + $btnBack.addClass('button-orange'); + } + else { + $btnBack.addClass('button-grey'); + } + } + + function initialize() { // on initial page load, we are not in the FTUE. so cancel the FTUE and call FTUESetStatus(true) if needed @@ -228,6 +264,7 @@ } this.setNextState = setNextState; + this.setBackState = setBackState; this.initialize = initialize; self = this; diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js index ab0be8b4b..0351496a1 100644 --- a/web/app/assets/javascripts/gear/step_configure_tracks.js +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -5,12 +5,111 @@ context.JK = context.JK || {}; context.JK.StepConfigureTracks = function (app) { - var $step = null; + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; + var MAX_TRACKS = context.JK.MAX_TRACKS; - function initialize(_$step) { - $step = _$step; + + var $step = null; + var $templateAssignablePort = null; + var $templateTrackTarget = null; + var $unassignedChannelsHolder = null; + var $tracksHolder = null; + + + + function loadChannels() { + var musicPorts = jamClient.FTUEGetChannels(); + + $unassignedChannelsHolder.empty(); + $tracksHolder.find('.ftue-inputport').remove(); + + var inputChannels = musicPorts.inputs; + + context._.each(inputChannels, function (inputChannel) { + if(inputChannel.assignment == ASSIGNMENT.UNASSIGNED) { + var $unassigned = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); + $unassignedChannelsHolder.append($unassigned); + $unassigned.draggable(); + + } + else { + var $assigned = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); + + // find the track this belongs in + + var trackNumber = inputChannel.assignment - 1; + + var $track = $tracksHolder.find('.track[data-num="' + trackNumber + '"]') + + if($track.length == 0) { + context.JK.alertSupportedNeeded('Unable to find a track for channel with assignment ' + inputChannel.assignment); + return false; + } + addChannelToTrack($assigned, $track); + $assigned.draggable(); + } + }) } + + function beforeShow() { + + loadChannels(); + } + + function removeChannelFromTrack() { + + } + function addChannelToTrack($channel, $track) { + $track.find('.track-target').append($channel); + } + + function initializeUnassignedDroppable() { + $unassignedChannelsHolder.droppable( + { + activeClass: 'drag-in-progress', + hoverClass: 'drag-hovering', + drop: function( event, ui ) { + console.log("event, ui", event, ui) + + } + }); + } + + function initializeTrackDroppables() { + var i; + for(i = 0; i < MAX_TRACKS; i++) { + var $target = $(context._.template($templateTrackTarget.html(), {num: i }, { variable: 'data' })); + $tracksHolder.append($target); + $target.droppable( + { + activeClass: 'drag-in-progress', + hoverClass: 'drag-hovering', + drop: function( event, ui ) { + console.log("event, ui", event, ui) + + var $target = $(this); + var $channel = ui.draggable; + addChannelToTrack($channel, $target); + } + }); + } + } + function initialize(_$step) { + $step = _$step; + + $templateAssignablePort = $('#template-assignable-port'); + $templateTrackTarget = $('#template-track-target'); + $unassignedChannelsHolder = $step.find('.unassigned-channels'); + $tracksHolder = $step.find('.tracks'); + + + initializeUnassignedDroppable(); + initializeTrackDroppables(); + } + + this.beforeShow = beforeShow; this.initialize = initialize; return this; diff --git a/web/app/assets/javascripts/gear/step_configure_voice_chat.js b/web/app/assets/javascripts/gear/step_configure_voice_chat.js index 46d9c9d6a..1e9be3ad4 100644 --- a/web/app/assets/javascripts/gear/step_configure_voice_chat.js +++ b/web/app/assets/javascripts/gear/step_configure_voice_chat.js @@ -7,10 +7,15 @@ var $step = null; + function beforeShow() { + + } + function initialize(_$step) { $step = _$step; } + this.beforeShow = beforeShow; this.initialize = initialize; return this; diff --git a/web/app/assets/javascripts/gear/step_direct_monitoring.js b/web/app/assets/javascripts/gear/step_direct_monitoring.js index a8e6aa1ac..80d8a83cb 100644 --- a/web/app/assets/javascripts/gear/step_direct_monitoring.js +++ b/web/app/assets/javascripts/gear/step_direct_monitoring.js @@ -7,10 +7,15 @@ var $step = null; + function beforeShow() { + + } + function initialize(_$step) { $step = _$step; } + this.beforeShow = beforeShow; this.initialize = initialize; return this; diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js index d04e73343..91d05ba34 100644 --- a/web/app/assets/javascripts/gear/step_network_test.js +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -7,10 +7,15 @@ var $step = null; + function beforeShow() { + + } + function initialize(_$step) { $step = _$step; } + this.beforeShow = beforeShow; this.initialize = initialize; return this; diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js index ec4db1c6d..4b97529ee 100644 --- a/web/app/assets/javascripts/gear/step_select_gear.js +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -9,6 +9,7 @@ var VOICE_CHAT = context.JK.VOICE_CHAT; var self = null; var $step = null; + var rest = context.JK.Rest(); var $watchVideoInput = null; var $watchVideoOutput = null; var $audioInput = null; @@ -32,20 +33,31 @@ var $ioCountdown = null; var $ioCountdownSecs = null; var $resultsText = null; + var $unknownText = null; var $asioInputControlBtn = null; var $asioOutputControlBtn = null; var $resyncBtn = null; var $templateAudioPort = null; + var $launchLoopbackBtn = null; + var $instructions = null; var operatingSystem = null; var iCheckIgnore = false; + var scoring = false; // are we currently scoring + var validDevice = false; // do we currently have a device selected that we can score against? // cached values between var deviceInformation = null; + var lastSelectedDeviceInfo = null; + var shownOutputProdOnce = false; + var shownInputProdOnce = false; + var selectedDeviceInfo = null; var musicPorts = null; var validLatencyScore = false; var validIOScore = false; + var lastLatencyScore = null; + var lastIOScore = null; var audioDeviceBehavior = { MacOSX_builtin: { @@ -86,10 +98,6 @@ } } - var ASIO_SETTINGS_DEFAULT_TEXT = 'ASIO SETTINGS...'; - var ASIO_SETTINGS_INPUT_TEXT = 'ASIO INPUT SETTINGS...'; - var ASIO_SETTINGS_OUTPUT_TEXT = 'ASIO OUTPUT SETTINGS...'; - // should return one of: // * MacOSX_builtin // * MACOSX_interface @@ -126,8 +134,7 @@ var oldDevices = context.jamClient.FTUEGetDevices(false); var devices = context.jamClient.FTUEGetAudioDevices(); - console.log("oldDevices: " + JSON.stringify(oldDevices)); - console.log("devices: " + JSON.stringify(devices)); + logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices)); var loadedDevices = {}; @@ -185,16 +192,19 @@ $dialog.setNextState(validLatencyScore && validIOScore); } + function initializeBackButtonState() { + $dialog.setBackState(!scoring); + } + function initializeAudioInput() { var optionsHtml = ''; optionsHtml = ''; context._.each(deviceInformation, function (deviceInfo, deviceId) { - - console.log(arguments) optionsHtml += ''; }); $audioInput.html(optionsHtml); context.JK.dropdown($audioInput); + $audioInput.easyDropDown('enable') initializeAudioInputChanged(); } @@ -207,6 +217,7 @@ }); $audioOutput.html(optionsHtml); context.JK.dropdown($audioOutput); + $audioOutput.easyDropDown('disable'); // enable once they pick something in input initializeAudioOutputChanged(); } @@ -225,7 +236,6 @@ // and update's the UI accordingly function initializeChannels() { musicPorts = jamClient.FTUEGetChannels(); - console.log("musicPorts: %o", JSON.stringify(musicPorts)); initializeInputPorts(musicPorts); initializeOutputPorts(musicPorts); @@ -255,12 +265,14 @@ var $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)'); if ($assignedInputs.length == 0) { if ($allInputs.length >= 2) { + logger.debug("selecting 2 inputs") $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); // this is required because iCheck change handler re-writes the inputs. So we have to refetch unassigned outputs $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)'); $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); } else { + logger.debug("selecting 1 inputs") $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); } } @@ -269,15 +281,15 @@ var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked'); var $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)'); - console.log("outputs:", $assignedOutputs, $unassignedOutputs); if ($assignedOutputs.length == 0) { - console.log("selecting both outputs") + logger.debug("selecting both outputs") $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); // this is required because iCheck change handler re-writes the inputs. So we have to refetch unassigned outputs $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)'); $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); } else if ($assignedOutputs.length == 1) { + logger.debug("selecting 1 output to round out 2 total") $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); } @@ -404,6 +416,14 @@ }); } + function initializeLoopback() { + $launchLoopbackBtn.unbind('click').click(function() { + app.setWizardStep(1); + app.layout.showDialog('ftue'); + return false; + }) + } + function initializeFormElements() { if (!deviceInformation) throw "devices are not initialized"; @@ -411,6 +431,7 @@ initializeAudioOutput(); initializeFramesize(); initializeBuffers(); + initializeLoopback(); } function resetFrameBuffers() { @@ -438,6 +459,8 @@ $resultsText.removeAttr('latency-score'); $resultsText.removeAttr('io-var-score'); $resultsText.removeAttr('io-rate-score'); + $resultsText.removeAttr('scored'); + $unknownText.hide(); } function renderLatencyScore(latencyValue, latencyClass) { @@ -448,6 +471,13 @@ else { $latencyScore.text(''); } + + + if(latencyClass == 'unknown') { + $latencyScore.text('Unknown'); + $unknownText.show(); + } + $latencyHeader.show(); $resultsText.attr('latency-score', latencyClass); $latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass); @@ -460,11 +490,11 @@ function renderIOScore(std, median, ioData, ioClass, ioRateClass, ioVarClass) { $ioRateScore.text(median !== null ? median : ''); $ioVarScore.text(std !== null ? std : ''); - if (ioClass && ioClass != "starting") { + if (ioClass && ioClass != "starting" && ioClass != "skip") { $ioRate.show(); $ioVar.show(); } - if(ioClass == 'starting') { + if(ioClass == 'starting' || ioClass == 'skip') { $ioHeader.show(); } $resultsText.attr('io-rate-score', ioRateClass); @@ -478,7 +508,7 @@ function updateScoreReport(latencyResult) { var latencyClass = "neutral"; - var latencyValue = 'N/A'; + var latencyValue = null; var validLatency = false; if (latencyResult && latencyResult.latencyknown) { var latencyValue = latencyResult.latency; @@ -503,6 +533,7 @@ } function audioInputDeviceUnselected() { + validDevice = false; validLatencyScore = false; validIOScore = false; initializeNextButtonState(); @@ -511,10 +542,8 @@ } function renderScoringStarted() { - validLatencyScore = false; - validIOScore = false; - initializeNextButtonState(); resetScoreReport(); + initializeNextButtonState(); freezeAudioInteraction(); renderLatencyScore(null, 'starting'); renderIOScore(null, null, null, null, null, null); @@ -523,10 +552,13 @@ function renderScoringStopped() { initializeNextButtonState(); unfreezeAudioInteraction(); + $resultsText.attr('scored', 'complete'); + scoring = false; + initializeBackButtonState(); } - function freezeAudioInteraction() { + logger.debug("freezing audio interaction"); $audioInput.attr("disabled", "disabled").easyDropDown('disable'); $audioOutput.attr("disabled", "disabled").easyDropDown('disable'); $frameSize.attr("disabled", "disabled").easyDropDown('disable'); @@ -541,6 +573,7 @@ } function unfreezeAudioInteraction() { + logger.debug("unfreezing audio interaction"); $audioInput.removeAttr("disabled").easyDropDown('enable'); $audioOutput.removeAttr("disabled").easyDropDown('enable'); $frameSize.removeAttr("disabled").easyDropDown('enable'); @@ -554,50 +587,6 @@ iCheckIgnore = false; } - // Given a latency structure, update the view. - function newFtueUpdateLatencyView(latency) { - var $report = $('.ftue-new .latency .report'); - var $instructions = $('.ftue-new .latency .instructions'); - var latencyClass = "neutral"; - var latencyValue = "N/A"; - var $saveButton = $('#btn-ftue-2-save'); - if (latency && latency.latencyknown) { - latencyValue = latency.latency; - // Round latency to two decimal places. - latencyValue = Math.round(latencyValue * 100) / 100; - if (latency.latency <= 10) { - latencyClass = "good"; - setSaveButtonState($saveButton, true); - } else if (latency.latency <= 20) { - latencyClass = "acceptable"; - setSaveButtonState($saveButton, true); - } else { - latencyClass = "bad"; - setSaveButtonState($saveButton, false); - } - } else { - latencyClass = "unknown"; - setSaveButtonState($saveButton, false); - } - - $('.ms-label', $report).html(latencyValue); - $('p', $report).html('milliseconds'); - - $report.removeClass('good acceptable bad unknown'); - $report.addClass(latencyClass); - - var instructionClasses = ['neutral', 'good', 'acceptable', 'unknown', 'bad', 'start', 'loading']; - $.each(instructionClasses, function (idx, val) { - $('p.' + val, $instructions).hide(); - }); - if (latency === 'loading') { - $('p.loading', $instructions).show(); - } else { - $('p.' + latencyClass, $instructions).show(); - renderStopNewFtueLatencyTesting(); - } - } - function initializeWatchVideo() { $watchVideoInput.unbind('click').click(function () { @@ -665,6 +654,10 @@ }); } + function postDiagnostic() { + // TODO + } + function initializeResync() { $resyncBtn.unbind('click').click(function () { attemptScore(); @@ -716,6 +709,9 @@ var inputBehavior = audioDeviceBehavior[input.type]; var outputBehavior = audioDeviceBehavior[output.type]; + lastSelectedDeviceInfo = selectedDeviceInfo; + shownOutputProdOnce = false; + shownInputProdOnce = false; selectedDeviceInfo = { input: { id: audioInputDeviceId, @@ -728,6 +724,9 @@ behavior: outputBehavior } } + + // prod the user to watch the video if the input or output changes type + console.log("selectedDeviceInfo", selectedDeviceInfo); } @@ -736,6 +735,10 @@ var audioInputDeviceId = selectedDeviceInfo.input.id; var audioOutputDeviceId = selectedDeviceInfo.output.id; + if(audioInputDeviceId) { + $audioOutput.easyDropDown('enable'); + } + // don't re-assign input/output audio devices because it disturbs input/output track association if (jamClient.FTUEGetInputMusicDevice() != audioInputDeviceId) { jamClient.FTUESetInputMusicDevice(audioInputDeviceId); @@ -746,7 +749,7 @@ initializeChannels(); - var validDevice = autoSelectMinimumValidChannels(); + validDevice = autoSelectMinimumValidChannels(); if (!validDevice) { return false; @@ -767,10 +770,33 @@ } } + function isInputAudioTypeDifferentFromLastTime() { + return lastSelectedDeviceInfo && (lastSelectedDeviceInfo.input.type != selectedDeviceInfo.input.type); + } + + function isOutputAudioTypeDifferentFromLastTime() { + return lastSelectedDeviceInfo && isInputOutputDifferentTypes() && (lastSelectedDeviceInfo.output.type != selectedDeviceInfo.output.type) + } + + function isInputOutputDifferentTypes() { + return selectedDeviceInfo.input.type != selectedDeviceInfo.output.type; + } + function updateDialogForCurrentDevices() { var inputBehavior = selectedDeviceInfo.input.behavior; var outputBehavior = selectedDeviceInfo.output.behavior; + // deal with watch video + if(isInputOutputDifferentTypes()) { + // if we have two types of devices, you need two different videos + $watchVideoOutput.show().find('.video-type').show(); + $watchVideoInput.addClass('audio-output-showing').find('.video-type').show(); + } + else { + $watchVideoOutput.hide(); + $watchVideoInput.removeClass('audio-output-showing').find('.video-type').hide(); + } + // handle framesize/buffers if (inputBehavior && (inputBehavior.showKnobs || outputBehavior.showKnobs)) { $knobs.css('visibility', 'visible') @@ -783,18 +809,18 @@ if (inputBehavior) { if (inputBehavior.showASIO && !outputBehavior.showASIO) { // show single ASIO button - $asioInputControlBtn.text(ASIO_SETTINGS_DEFAULT_TEXT).show(); + $asioInputControlBtn.show(); $asioOutputControlBtn.hide(); } else if (!inputBehavior.showASIO && outputBehavior.showASIO) { // show single ASIO button - $asioInputControlBtn.text(ASIO_SETTINGS_DEFAULT_TEXT).show(); + $asioInputControlBtn.show(); $asioOutputControlBtn.hide(); } else if (inputBehavior.showASIO && outputBehavior.showASIO) { // show two ASIO buttons - $asioInputControlBtn.text(ASIO_SETTINGS_INPUT_TEXT).show(); - $asioOutputControlBtn.text(ASIO_SETTINGS_OUTPUT_TEXT).show(); + $asioInputControlBtn.show(); + $asioOutputControlBtn.show(); } else { // show no ASIO buttons @@ -817,71 +843,110 @@ } } - function attemptScore() { + function processIOScore(io) { + // take the higher variance, which is apparently actually std dev + var std = io.in_var > io.out_var ? io.in_var : io.out_var; + std = Math.round(std * 100) / 100; + // take the furthest-off-from-target io rate + var median = Math.abs(io.in_median - io.in_target) > Math.abs(io.out_median - io.out_target) ? [io.in_median, io.in_target] : [io.out_median, io.out_target]; + var medianTarget = median[1]; + median = Math.round(median[0]); + + var stdIOClass = 'bad'; + if (std <= 0.50) { + stdIOClass = 'good'; + } + else if (std <= 1.00) { + stdIOClass = 'acceptable'; + } + + var medianIOClass = 'bad'; + if (Math.abs(median - medianTarget) <= 1) { + medianIOClass = 'good'; + } + else if (Math.abs(median - medianTarget) <= 2) { + medianIOClass = 'acceptable'; + } + + // take worst between median or std + var ioClassToNumber = {bad: 2, acceptable: 1, good: 0} + var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass; + + // now base the overall IO score based on both values. + renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass); + + // lie for now until IO questions finalize + validIOScore = true; + + renderScoringStopped(); + } + + // refocused affects how IO testing occurs. + // on refocus=true: + // * reuse IO score if it was good/acceptable + // * rescore IO if it was bad or skipped from previous try + function attemptScore(refocused) { + if(scoring) {return;} + scoring = true; + initializeBackButtonState(); + validLatencyScore = false; + if(!refocused) { + // don't reset a valid IO score on refocus + validIOScore = false; + } renderScoringStarted(); - // timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in + // this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in setTimeout(function () { logger.debug("Calling FTUESave(false)"); jamClient.FTUESave(false); var latency = jamClient.FTUEGetExpectedLatency(); - console.log("FTUEGetExpectedLatency: %o", latency); + lastLatencyScore = latency; + + // prod user to watch video if the previous type and new type changed + if(!shownInputProdOnce && isInputAudioTypeDifferentFromLastTime()) { + context.JK.prodBubble($watchVideoInput, 'ftue-watch-video', {}, {positions:['top', 'right']}); + shownInputProdOnce = true; + } + + // prod user to watch video if the previous type and new type changed + if(!shownOutputProdOnce && isOutputAudioTypeDifferentFromLastTime()) { + context.JK.prodBubble($watchVideoOutput, 'ftue-watch-video', {}, {positions:['top', 'right']}); + shownOutputProdOnce = true; + } updateScoreReport(latency); + if(refocused) { + context.JK.prodBubble($scoreReport, 'refocus-rescore', {validIOScore: validIOScore}, {positions:['top', 'left']}); + } + // if there was a valid latency score, go on to the next step if (validLatencyScore) { - renderIOScore(null, null, null, 'starting', 'starting', 'starting'); - var testTimeSeconds = 10; // allow 10 seconds for IO to establish itself - context.jamClient.FTUEStartIoPerfTest(); - renderIOScoringStarted(testTimeSeconds); - renderIOCountdown(testTimeSeconds); - var interval = setInterval(function () { - testTimeSeconds -= 1; + // reuse valid IO score if this is on refocus + if(refocused && validIOScore) { + renderIOScore(null, null, null, 'starting', 'starting', 'starting'); + processIOScore(lastIOScore); + } + else { + renderIOScore(null, null, null, 'starting', 'starting', 'starting'); + var testTimeSeconds = gon.ftue_io_wait_time; // allow time for IO to establish itself + context.jamClient.FTUEStartIoPerfTest(); + renderIOScoringStarted(testTimeSeconds); renderIOCountdown(testTimeSeconds); - if (testTimeSeconds == 0) { - clearInterval(interval); - renderIOScoringStopped(); - var io = context.jamClient.FTUEGetIoPerfData(); - - // take the higher variance, which is apparently actually std dev - var std = io.in_var > io.out_var ? io.in_var : io.out_var; - std = Math.round(std * 100) / 100; - // take the furthest-off-from-target io rate - var median = Math.abs(io.in_median - io.in_target) > Math.abs(io.out_median - io.out_target) ? [io.in_median, io.in_target] : [io.out_median, io.out_target]; - var medianTarget = median[1]; - median = Math.round(median[0]); - - var stdIOClass = 'bad'; - if (std <= 0.50) { - stdIOClass = 'good'; + var interval = setInterval(function () { + testTimeSeconds -= 1; + renderIOCountdown(testTimeSeconds); + if (testTimeSeconds == 0) { + clearInterval(interval); + renderIOScoringStopped(); + var io = context.jamClient.FTUEGetIoPerfData(); + lastIOScore = io; + processIOScore(io); } - else if (std <= 1.00) { - stdIOClass = 'acceptable'; - } - - var medianIOClass = 'bad'; - if (Math.abs(median - medianTarget) <= 1) { - medianIOClass = 'good'; - } - else if (Math.abs(median - medianTarget) <= 2) { - medianIOClass = 'acceptable'; - } - - // take worst between median or std - var ioClassToNumber = {bad: 2, acceptable: 1, good: 0} - var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass; - - // now base the overall IO score based on both values. - renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass); - - // lie for now until IO questions finalize - validIOScore = true; - - renderScoringStopped(); - } - }, 1000); + }, 1000); + } } else { renderIOScore(null, null, null, 'skip', 'skip', 'skip'); @@ -898,6 +963,45 @@ $audioOutput.unbind('change').change(audioDeviceChanged); } + function handleNext() { + + var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked'); + var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked'); + + var errors = []; + if($assignedInputs.length == 0) { + errors.push("There must be at least one selected input ports."); + } + if($assignedInputs.length > 12) { + errors.push("There can only be up to 12 selected inputs ports."); + } + if($assignedOutputs.length != 2) { + errors.push("There must be exactly 2 selected output ports."); + } + var $errors = $('
    '); + context._.each(errors, function(error) { + $errors.append('
  • ' + error + '
  • '); + }); + + if(errors.length > 0) { + context.JK.Banner.showAlert({html:$errors}); + return false; + } + else { + context.jamClient.FTUESave(true); + return true; + } + + } + + function onFocus() { + if(!scoring && validDevice) { + // in the case the user has been unselecting ports, re-enforce minimum viable channels + validDevice = autoSelectMinimumValidChannels(); + attemptScore(true); + } + } + function beforeShow() { loadDevices(); initializeFormElements(); @@ -906,6 +1010,11 @@ initializeASIOButtons(); initializeKnobs(); initializeResync(); + $(window).on('focus', onFocus); + } + + function beforeHide() { + $(window).off('focus', onFocus); } function initialize(_$step) { @@ -934,15 +1043,19 @@ $ioCountdown = $scoreReport.find('.io-countdown'); $ioCountdownSecs = $scoreReport.find('.io-countdown .secs'); $resultsText = $scoreReport.find('.results-text'); + $unknownText = $scoreReport.find('.unknown-text'); $asioInputControlBtn = $step.find('.asio-settings-input-btn'); $asioOutputControlBtn = $step.find('.asio-settings-output-btn'); $resyncBtn = $step.find('.resync-btn'); $templateAudioPort = $('#template-audio-port'); - - operatingSystem = context.jamClient.GetOSAsString(); + $launchLoopbackBtn = $('.loopback-test'); + $instructions = $('.instructions'); + operatingSystem = context.JK.GetOSAsString(); } + this.handleNext = handleNext; this.beforeShow = beforeShow; + this.beforeHide = beforeHide; this.initialize = initialize; self = this; diff --git a/web/app/assets/javascripts/gear/step_success.js b/web/app/assets/javascripts/gear/step_success.js index 3dc049d2c..ab1656eb8 100644 --- a/web/app/assets/javascripts/gear/step_success.js +++ b/web/app/assets/javascripts/gear/step_success.js @@ -7,10 +7,15 @@ var $step = null; + function beforeShow() { + + } + function initialize(_$step) { $step = _$step; } + this.beforeShow = beforeShow; this.initialize = initialize; return this; diff --git a/web/app/assets/javascripts/gear/step_understand_gear.js b/web/app/assets/javascripts/gear/step_understand_gear.js index d579e84f2..c4c235cf1 100644 --- a/web/app/assets/javascripts/gear/step_understand_gear.js +++ b/web/app/assets/javascripts/gear/step_understand_gear.js @@ -6,6 +6,7 @@ context.JK.StepUnderstandGear = function (app) { var $step = null; + var operatingSystem; function beforeShow() { var $watchVideo = $step.find('.watch-video'); @@ -18,8 +19,11 @@ function initialize(_$step) { $step = _$step; + + operatingSystem = context.JK.GetOSAsString(); } + this.beforeShow = beforeShow; this.initialize = initialize; return this; diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 7c3a37ec3..49fbd0aae 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -27,6 +27,8 @@ CHAT: "1" }; + context.JK.MAX_TRACKS = 6; + // TODO: store these client_id values in instruments table, or store // server_id as the client_id to prevent maintenance nightmares. As it's // set up now, we will have to deploy each time we add new instruments. diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index a56a382f0..e45ef62ec 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -15,6 +15,7 @@ "April", "May", "June", "July", "August", "September", "October", "November", "December"); + var os = null; context.JK.stringToBool = function (s) { switch (s.toLowerCase()) { @@ -96,7 +97,6 @@ * @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.erb * @param data (optional) data for your template, if applicable * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips - * */ context.JK.helpBubble = function ($element, templateName, data, options) { if (!data) { @@ -110,6 +110,33 @@ context.JK.hoverBubble($element, helpText, options); } + /** + * Associates a help bubble immediately with the specified $element, using jquery.bt.js (BeautyTips) + * By 'prod' it means to literally prod the user, to make them aware of something important because they did something else + * + * This will open a bubble immediately and show it for 4 seconds, + * if you call it again before the 4 second timer is up, it will renew the 4 second timer. + * @param $element The element that should show the help when hovered + * @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.erb + * @param data (optional) data for your template, if applicable + * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips + */ + context.JK.prodBubble = function($element, templateName, data, options) { + options['trigger'] = 'now'; + var existingTimer = $element.data("prodTimer"); + if(existingTimer) { + clearTimeout(existingTimer); + $element.btOn(); + } + else { + context.JK.helpBubble($element, templateName, data, options); + $element.btOn(); + } + $element.data("prodTimer", setTimeout(function() { + $element.data("prodTimer", null); + $element.btOff(); + }, 4000)); + } /** * Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) * @param $element The element that should show the bubble when hovered @@ -487,6 +514,13 @@ return $item; } + context.JK.GetOSAsString = function() { + if(!os) { + os = context.jamClient.GetOSAsString(); + } + return os; + } + context.JK.search = function (query, app, callback) { //logger.debug("search: "+ query) $.ajax({ diff --git a/web/app/assets/stylesheets/client/banner.css.scss b/web/app/assets/stylesheets/client/banner.css.scss index 4db4a318d..c4d34d1bf 100644 --- a/web/app/assets/stylesheets/client/banner.css.scss +++ b/web/app/assets/stylesheets/client/banner.css.scss @@ -26,5 +26,21 @@ .close-btn { display:none; } + + ul { + list-style:disc; + margin-left:20px; + } + li { + margin: 15px 12px 15px 36px; + } + + .end-content { + height: 0; + line-height: 0; + display: block; + font-size: 0; + content: " "; + } } diff --git a/web/app/assets/stylesheets/client/ftue.css.scss b/web/app/assets/stylesheets/client/ftue.css.scss index a5f6796de..88e1230fd 100644 --- a/web/app/assets/stylesheets/client/ftue.css.scss +++ b/web/app/assets/stylesheets/client/ftue.css.scss @@ -83,15 +83,14 @@ div.dialog.ftue .ftue-inner div[layout-wizard-step="0"] { font-size: 0.9em; } + div.dialog.ftue { - min-width: 800px; - max-width: 800px; - min-height: 400px; - max-height: 400px; + min-height: 500px; + max-height: 500px; + width: 800px; .ftue-inner { line-height: 1.3em; - width: auto; a { text-decoration: underline; diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index bf8e1384c..7c21e27b8 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -119,7 +119,14 @@ } &.instructions { - height: 230px !important + height: 248px !important; + line-height:16px; + @include border_box_sizing; + + .video-type { + font-size:10px; + display:none; + } } ul li { @@ -128,7 +135,7 @@ } .watch-video { - margin:8px 0 15px 0; + margin:8px 0 3px 0; } } @@ -223,13 +230,27 @@ .ftue-box.results { - height: 230px !important; + height: 248px !important; padding:0; + @include border_box_sizing; .io, .latency { display:none; } + .loopback-button-holder { + width:100%; + text-align:center; + } + a.loopback-test { + margin-top:10px; + } + + .unknown-text { + display:none; + padding:8px; + } + .scoring-section { font-size:15px; @include border_box_sizing; @@ -241,10 +262,10 @@ &.acceptable { background-color:#cc9900; } - &.bad, &.skip { + &.bad { background-color:#660000; } - &.unknown { + &.unknown, &.skip { background-color:#999; } &.skip { @@ -284,6 +305,9 @@ display:none } + &[latency-score="unknown"] { + display:none; + } &[latency-score="good"] li.latency-good { display:list-item; } @@ -311,6 +335,26 @@ &[io-rate-score="bad"] li.io-rate-bad { display:list-item; } + &[scored="complete"] { + li.success { + display:list-item; + } + + &[io-rate-score="bad"], &[io-var-score="bad"], &[latency-score="bad"]{ + li.success { + display:none; + } + li.failure { + display:list-item; + } + } + + &[latency-score="unknown"] { + li.success { + display:none; + } + } + } } } @@ -323,6 +367,50 @@ .wizard-step-content .wizard-step-column { width:25%; } + + + .ftue-inputport { + height: 37px; + + .num { + float: left; + display: inline-block; + height: 34px; + line-height: 28px; + width: 16px; + } + } + + .num { + position:absolute; + } + .track { + + } + + + .ftue-input { + cursor: move; + padding: 4px; + border: solid 1px #999; + margin-bottom: 6px; + } + + .track-target { + + cursor: move; + padding: 4px; + border: solid 1px #999; + margin-left: 15px; + margin-bottom: 6px; + height:20px; + + .ftue-input { + padding:0; + border:0; + margin-bottom:0; + } + } } .wizard-step[layout-wizard-step="3"] { @@ -657,6 +745,11 @@ } .audio-input { left:0px; + margin-top:30px; + + &.audio-output-showing { + margin-top:0; + } } .voice-chat-input { left:50%; diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index 387352b0d..460c988de 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -15,6 +15,25 @@ overflow-x:hidden; position:relative; } + + + .track { + width:70px; + height:290px; + display:inline-block; + margin-right:8px; + position:relative; + background-color:#242323; + + .disabled-track-overlay { + width:100%; + height:100%; + position:absolute; + background-color:#555; + opacity:0.5; + display:none; + } + } } @@ -25,16 +44,6 @@ color:#666; } - -.track { - width:70px; - height:290px; - display:inline-block; - margin-right:8px; - position:relative; - background-color:#242323; -} - .track-empty { min-width:230px; height:201px; @@ -268,23 +277,6 @@ table.vu td { } -.track { - width:70px; - height:290px; - display:inline-block; - margin-right:8px; - position:relative; - background-color:#242323; - - .disabled-track-overlay { - width:100%; - height:100%; - position:absolute; - background-color:#555; - opacity:0.5; - display:none; - } -} .track-empty { diff --git a/web/app/helpers/client_helper.rb b/web/app/helpers/client_helper.rb index 045b50636..0011cfe7f 100644 --- a/web/app/helpers/client_helper.rb +++ b/web/app/helpers/client_helper.rb @@ -33,6 +33,7 @@ module ClientHelper gon.fp_apikey = Rails.application.config.filepicker_rails.api_key gon.fp_upload_dir = Rails.application.config.filepicker_upload_dir gon.allow_force_native_client = Rails.application.config.allow_force_native_client + gon.ftue_io_wait_time = Rails.application.config.ftue_io_wait_time # is this the native client or browser? @nativeClient = is_native_client? diff --git a/web/app/views/clients/_banner.html.erb b/web/app/views/clients/_banner.html.erb index b9858269b..80ba450ae 100644 --- a/web/app/views/clients/_banner.html.erb +++ b/web/app/views/clients/_banner.html.erb @@ -13,7 +13,7 @@ -
    +
    CLOSE diff --git a/web/app/views/clients/_help.html.erb b/web/app/views/clients/_help.html.erb index 5e7ddaa83..4cf34c068 100644 --- a/web/app/views/clients/_help.html.erb +++ b/web/app/views/clients/_help.html.erb @@ -1,3 +1,15 @@ + + + + \ No newline at end of file diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 01b4637de..e91229d2c 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -31,8 +31,14 @@ %li Configure interface settings. %li View test results. .center - %a.button-orange.watch-video.audio-input{href:'#', rel:'external'} WATCH VIDEO - %a.button-orange.watch-video.audio-output{href:'#', rel:'external'} WATCH VIDEO + %a.button-orange.watch-video.audio-input{href:'#', rel:'external'} + WATCH VIDEO + %br + %span.video-type (for input device) + %a.button-orange.watch-video.audio-output{href:'#', rel:'external'} + WATCH VIDEO + %br + %span.video-type (for output device) .wizard-step-column %h2 Audio Input Device %select.w100.select-audio-input-device @@ -40,7 +46,6 @@ %h2.audio-channels Audio Input Ports .ftue-box.list.ports.input-ports %a.button-orange.asio-settings-input-btn ASIO SETTINGS... - %a.button-orange.asio-settings-output-btn ASIO SETTINGS... %a.button-orange.resync-btn RESYNC .wizard-step-column %h2 Audio Output Device @@ -48,6 +53,7 @@ %option Same as input %h2.audio-channels Audio Output Ports .ftue-box.list.ports.output-ports + %a.button-orange.asio-settings-output-btn ASIO SETTINGS... .frame-and-buffers .framesize %h2 Frame @@ -113,6 +119,12 @@ %li.io-var-good Your I/O variance is good. %li.io-var-acceptable Your I/O variance is acceptable. %li.io-var-bad Your I/O variance is poor. + %li.success You may proceed to the next step. + %li.failure We're sorry, but your audio gear has failed. Please watch video or click HELP button below. + .unknown-text + %div We cannot accurately predict the latency of your audio gear. To proceed, you must run an audio loopback test. Click button below to do this. + %div.loopback-button-holder + %a.button-orange.loopback-test{href:'#'} Run Loopback Test .clearall .wizard-step{ 'layout-wizard-step' => "2", 'dialog-title' => "Configure Tracks", 'dialog-purpose' => "ConfigureTracks" } @@ -131,8 +143,10 @@ %a.button-orange.watch-video{href:'#'} WATCH VIDEO .wizard-step-column %h2 Unassigned Ports + .unassigned-channels .wizard-step-column %h2 Track Input Port(s) + .tracks .wizard-step-column %h2 Track Instrument @@ -230,5 +244,10 @@ %span = '{{data.name}}' +%script{type: 'text/template', id: 'template-assignable-port'} + .ftue-input{id: '{{data.id}}'} {{data.name}} - +%script{type: 'text/template', id: 'template-track-target'} + .track{'data-num' => '{{data.num}}'} + .num {{data.num + 1}}: + .track-target{'data-num' => '{{data.num}}'} diff --git a/web/config/application.rb b/web/config/application.rb index 7dff3b4c1..1ba0d5f0a 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -231,5 +231,8 @@ if defined?(Bundler) # we have to do this for a while until all www.jamkazam.com cookies are gone, # and only .jamkazam.com cookies are around.. 2016? config.middleware.insert_before "ActionDispatch::Cookies", "Middlewares::ClearDuplicatedSession" + + # how long should the frontend wait for the IO to stabilize before asking for a IO score? + config.ftue_io_wait_time = 10 end end diff --git a/web/config/environments/test.rb b/web/config/environments/test.rb index 7b18deef2..40eeee247 100644 --- a/web/config/environments/test.rb +++ b/web/config/environments/test.rb @@ -81,5 +81,7 @@ SampleApp::Application.configure do config.vanilla_url = '/forums' config.vanilla_login_url = '/forums/entry/jsconnect' + + config.ftue_io_wait_time = 1 end diff --git a/web/spec/features/gear_wizard_spec.rb b/web/spec/features/gear_wizard_spec.rb new file mode 100644 index 000000000..8f40dce6a --- /dev/null +++ b/web/spec/features/gear_wizard_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => true, :slow => true do + + subject { page } + + let(:user) { FactoryGirl.create(:user) } + + before(:each) do + sign_in_poltergeist user + end + + it "success path" do + visit "/client#/home/gear-wizard" + + end +end + diff --git a/web/vendor/assets/javascripts/jquery.bt.js b/web/vendor/assets/javascripts/jquery.bt.js index 71e3dd6b6..fddbcd9c5 100644 --- a/web/vendor/assets/javascripts/jquery.bt.js +++ b/web/vendor/assets/javascripts/jquery.bt.js @@ -143,10 +143,10 @@ jQuery.bt = {version: '0.9.7'}; // toggle the on/off right now // note that 'none' gives more control (see below) if ($(this).hasClass('bt-active')) { - this.btOff(); + $(this).btOff(); } else { - this.btOn(); + $(this).btOn(); } } else if (opts.trigger[0] == 'none') { @@ -158,20 +158,20 @@ jQuery.bt = {version: '0.9.7'}; else if (opts.trigger.length > 1 && opts.trigger[0] != opts.trigger[1]) { $(this) .bind(opts.trigger[0], function() { - this.btOn(); + $(this).btOn(); }) .bind(opts.trigger[1], function() { - this.btOff(); + $(this).btOff(); }); } else { // toggle using the same event $(this).bind(opts.trigger[0], function() { if ($(this).hasClass('bt-active')) { - this.btOff(); + $(this).btOff(); } else { - this.btOn(); + $(this).btOn(); } }); } From cc3d48ed0a31030d2a800a219fe361c665551586 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 13 May 2014 11:49:43 -0500 Subject: [PATCH 05/29] * wip --- .../javascripts/gear/step_configure_tracks.js | 55 +++++++++++----- .../stylesheets/client/gearWizard.css.scss | 66 +++++++++++++++---- 2 files changed, 95 insertions(+), 26 deletions(-) diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js index 0351496a1..31a0141a0 100644 --- a/web/app/assets/javascripts/gear/step_configure_tracks.js +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -28,13 +28,11 @@ context._.each(inputChannels, function (inputChannel) { if(inputChannel.assignment == ASSIGNMENT.UNASSIGNED) { - var $unassigned = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); - $unassignedChannelsHolder.append($unassigned); - $unassigned.draggable(); - + var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); + unassignChannel($channel) } else { - var $assigned = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); + var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); // find the track this belongs in @@ -46,9 +44,29 @@ context.JK.alertSupportedNeeded('Unable to find a track for channel with assignment ' + inputChannel.assignment); return false; } - addChannelToTrack($assigned, $track); - $assigned.draggable(); + addChannelToTrack($channel, $track.find('.track-target')); } + + $channel.draggable({ + revert: true, + start: function() { + var $channel = $(this); + var $track = $channel.closest('.track-target'); + var isUnassigned = $track.length == 0; + if(isUnassigned) { + $tracksHolder.find('.track-target').addClass('possible-target'); + } + else { + $tracksHolder.find('.track-target').addClass('possible-target'); + $unassignedChannelsHolder.addClass('possible-target'); + } + }, + stop: function() { + $tracksHolder.find('.track-target').removeClass('possible-target'); + $unassignedChannelsHolder.removeClass('possible-target') + } + }); + }) } @@ -58,11 +76,17 @@ loadChannels(); } - function removeChannelFromTrack() { + function unassignChannel($channel) { + var $originallyAssignedTrack = $channel.closest('.track-target'); + $unassignedChannelsHolder.append($channel); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input').length) } function addChannelToTrack($channel, $track) { - $track.find('.track-target').append($channel); + var $originallyAssignedTrack = $channel.closest('.track-target'); + $track.append($channel); + $track.attr('track-count', $track.find('.ftue-input').length); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input').length) } function initializeUnassignedDroppable() { @@ -72,7 +96,9 @@ hoverClass: 'drag-hovering', drop: function( event, ui ) { console.log("event, ui", event, ui) - + var $channel = ui.draggable; + $channel.css('left', '0').css('top', '0'); + unassignChannel($channel); } }); } @@ -82,16 +108,15 @@ for(i = 0; i < MAX_TRACKS; i++) { var $target = $(context._.template($templateTrackTarget.html(), {num: i }, { variable: 'data' })); $tracksHolder.append($target); - $target.droppable( + $target.find('.track-target').droppable( { activeClass: 'drag-in-progress', hoverClass: 'drag-hovering', drop: function( event, ui ) { - console.log("event, ui", event, ui) - - var $target = $(this); + var $track = $(this); var $channel = ui.draggable; - addChannelToTrack($channel, $target); + $channel.css('left', '0').css('top', '0'); + addChannelToTrack($channel, $track); } }); } diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index 7c21e27b8..82d262344 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -368,16 +368,22 @@ width:25%; } + .unassigned-channels { + min-height:240px; + overflow-y:scroll; + //padding-right:18px; // to keep draggables off of scrollbar. maybe necessary - .ftue-inputport { - height: 37px; + &.drag-in-progress { + overflow-y:visible; + overflow-x:visible; + } - .num { - float: left; - display: inline-block; - height: 34px; - line-height: 28px; - width: 16px; + &.possible-target { + border: solid 1px white; + + &.drag-hovering { + border: solid 1px #ED3618; + } } } @@ -386,14 +392,31 @@ } .track { - } + .track-target { + &.possible-target { + border-color:white; + } + &.drag-hovering { + border-color:#ED3618; + } + } + } .ftue-input { cursor: move; padding: 4px; border: solid 1px #999; - margin-bottom: 6px; + margin-bottom: 15px; + white-space: nowrap; + overflow:hidden; + text-align:left; + direction:rtl; + font-size:12px; + + &.ui-draggable-dragging{ + margin-bottom:0; + } } .track-target { @@ -402,13 +425,34 @@ padding: 4px; border: solid 1px #999; margin-left: 15px; - margin-bottom: 6px; + margin-bottom: 15px; height:20px; + overflow:hidden; + + &.drag-in-progress { + overflow:visible; + } .ftue-input { padding:0; border:0; margin-bottom:0; + &.ui-draggable-dragging { + padding:4px; + border: solid 1px #999; + overflow:visible; + } + } + + &[track-count="2"]:not(.drag-in-progress) { + .ftue-input { + width:50%; + display:inline-block; + } + .ftue-input:nth-child(1)::before { + content: ' , ' + } + ; } } } From ebe1278e5ec019ed82fd1b3e16b876bc27bcaf8e Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 14 May 2014 15:12:42 -0500 Subject: [PATCH 06/29] * wip --- web/app/assets/javascripts/bandProfile.js | 2 +- web/app/assets/javascripts/findBand.js | 2 +- web/app/assets/javascripts/findMusician.js | 2 +- .../javascripts/gear/step_configure_tracks.js | 116 ++++++++++++++++-- .../javascripts/gear/step_select_gear.js | 4 +- web/app/assets/javascripts/jam_rest.js | 9 +- web/app/assets/javascripts/profile.js | 2 +- web/app/assets/javascripts/searchResults.js | 2 +- web/app/assets/javascripts/sessionList.js | 2 +- web/app/assets/javascripts/utils.js | 61 +++++++-- web/app/assets/stylesheets/client/client.css | 1 + .../stylesheets/client/gearWizard.css.scss | 50 ++++++-- .../client/iconInstrumentSelect.css.scss | 47 +++++++ .../stylesheets/client/sidebar.css.scss | 2 +- .../clients/_iconInstrumentSelect.html.haml | 12 ++ .../views/clients/gear/_gear_wizard.html.haml | 8 +- web/app/views/clients/index.html.erb | 1 + 17 files changed, 282 insertions(+), 41 deletions(-) create mode 100644 web/app/assets/stylesheets/client/iconInstrumentSelect.css.scss create mode 100644 web/app/views/clients/_iconInstrumentSelect.html.haml diff --git a/web/app/assets/javascripts/bandProfile.js b/web/app/assets/javascripts/bandProfile.js index 6812e751e..46ed0b72c 100644 --- a/web/app/assets/javascripts/bandProfile.js +++ b/web/app/assets/javascripts/bandProfile.js @@ -344,7 +344,7 @@ var instrument = musician.instruments[j]; var inst = '../assets/content/icon_instrument_default24.png'; if (instrument.instrument_id in instrument_logo_map) { - inst = instrument_logo_map[instrument.instrument_id]; + inst = instrument_logo_map[instrument.instrument_id].asset; } instrumentLogoHtml += ' '; } diff --git a/web/app/assets/javascripts/findBand.js b/web/app/assets/javascripts/findBand.js index 144bf4541..644df8a9d 100644 --- a/web/app/assets/javascripts/findBand.js +++ b/web/app/assets/javascripts/findBand.js @@ -97,7 +97,7 @@ for (var kk=0, klen=iter_pinstruments.length; kk'; } diff --git a/web/app/assets/javascripts/findMusician.js b/web/app/assets/javascripts/findMusician.js index e10458b3b..ee2847b68 100644 --- a/web/app/assets/javascripts/findMusician.js +++ b/web/app/assets/javascripts/findMusician.js @@ -100,7 +100,7 @@ instr_logos = ''; for (var jj=0, ilen=musician['instruments'].length; jj'; } diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js index 31a0141a0..7a5c76915 100644 --- a/web/app/assets/javascripts/gear/step_configure_tracks.js +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -15,6 +15,7 @@ var $templateTrackTarget = null; var $unassignedChannelsHolder = null; var $tracksHolder = null; + var $instrumentsHolder = null; @@ -22,7 +23,7 @@ var musicPorts = jamClient.FTUEGetChannels(); $unassignedChannelsHolder.empty(); - $tracksHolder.find('.ftue-inputport').remove(); + $tracksHolder.find('.ftue-input').remove(); var inputChannels = musicPorts.inputs; @@ -48,7 +49,7 @@ } $channel.draggable({ - revert: true, + helper: 'clone', start: function() { var $channel = $(this); var $track = $channel.closest('.track-target'); @@ -70,23 +71,108 @@ }) } + // iterates through the dom and returns a pure data structure for track associations + function trackAssociations() { + + var tracks = {}; + tracks.tracks = []; + tracks.unassignedChannels = []; + var $unassignedChannels = $unassignedChannelsHolder.find('.ftue-input'); + var $tracks = $tracksHolder.find('.track-target'); + + context._.each($unassignedChannels, function($unassignedTrack) { + $unassignedTrack = $($unassignedTrack); + var channelId = $unassignedTrack.attr('data-input-id'); + tracks.unassignedChannels.push(channelId); + }) + + context._.each($tracks, function($track, index) { + $track = $($track); + var $assignedChannels = $track.find('.ftue-input'); + + var track = {index: index, channels:[]}; + context._.each($assignedChannels, function($assignedChannel) { + $assignedChannel = $($assignedChannel); + track.channels.push($assignedChannel.attr('data-input-id')) + }); + + // sparse array + if(track.channels.length > 0) { + tracks.tracks.push(track); + } + var $instrument = $instrumentsHolder.find('.icon-instrument-select[data-num="' + index + '"]'); + track.instrument_id = $instrument.data('instrument_id'); + }) + return tracks; + } + + function validate(tracks) { + // there must be at least one assigned channel + + if(tracks.tracks.length == 0) { + logger.debug("ConfigureTracks validation error: must have assigned at least one input port to a track."); + context.JK.Banner.showAlert('Must have assigned at least one input port to a track.'); + return false; + } + + context._.each(tracks.tracks, function(track) { + if(!track.instrument_id) { + logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument."); + context.JK.Banner.showAlert('All tracks with ports assigned must specify an instrument.'); + return false; + } + }); + + return true; + } + + function save(tracks) { + + context._.each(tracks.unassignedChannels, function(unassignedChannelId) { + context.jamClient.TrackSetAssignment(unassignedChannelId, true, ASSIGNMENT.UNASSIGNED); + }); + + context._.each(tracks.tracks, function(track, index) { + + var trackNumber = index + 1; + + context._.each(track.channels, function(channelId) { + context.jamClient.TrackSetAssignment(channelId, true, trackNumber); + + }); + context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id); + }); + + context.jamClient.TrackSaveAssignments(); + } + + function handleNext() { + var tracks = trackAssociations(); + + if(!validate(tracks)) { + return false; + } + + save(tracks); + + return true; + } function beforeShow() { - loadChannels(); } function unassignChannel($channel) { var $originallyAssignedTrack = $channel.closest('.track-target'); $unassignedChannelsHolder.append($channel); - $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input').length) + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length) } function addChannelToTrack($channel, $track) { var $originallyAssignedTrack = $channel.closest('.track-target'); $track.append($channel); - $track.attr('track-count', $track.find('.ftue-input').length); - $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input').length) + $track.attr('track-count', $track.find('.ftue-input:not(.ui-draggable-dragging)').length); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length) } function initializeUnassignedDroppable() { @@ -95,9 +181,8 @@ activeClass: 'drag-in-progress', hoverClass: 'drag-hovering', drop: function( event, ui ) { - console.log("event, ui", event, ui) var $channel = ui.draggable; - $channel.css('left', '0').css('top', '0'); + //$channel.css('left', '0').css('top', '0'); unassignChannel($channel); } }); @@ -115,12 +200,22 @@ drop: function( event, ui ) { var $track = $(this); var $channel = ui.draggable; - $channel.css('left', '0').css('top', '0'); + //$channel.css('left', '0').css('top', '0'); addChannelToTrack($channel, $track); } }); } } + + function initializeInstrumentDropdown() { + var i; + for(i = 0; i < MAX_TRACKS; i++) { + var $instrumentSelect = context.JK.iconInstrumentSelect(); + $instrumentSelect.attr('data-num', i); + $instrumentsHolder.append($instrumentSelect); + } + } + function initialize(_$step) { $step = _$step; @@ -128,12 +223,15 @@ $templateTrackTarget = $('#template-track-target'); $unassignedChannelsHolder = $step.find('.unassigned-channels'); $tracksHolder = $step.find('.tracks'); + $instrumentsHolder = $step.find('.instruments'); initializeUnassignedDroppable(); initializeTrackDroppables(); + initializeInstrumentDropdown(); } + this.handleNext = handleNext; this.beforeShow = beforeShow; this.initialize = initialize; diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js index 4b97529ee..5a14006df 100644 --- a/web/app/assets/javascripts/gear/step_select_gear.js +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -149,7 +149,7 @@ deviceInfo.id = device.guid; deviceInfo.type = determineDeviceType(device.guid, device.display_name); - console.log("deviceInfo.type: " + deviceInfo.type) + logger.debug("deviceInfo.type: " + deviceInfo.type) deviceInfo.displayType = audioDeviceBehavior[deviceInfo.type].display; deviceInfo.displayName = device.display_name; @@ -727,7 +727,7 @@ // prod the user to watch the video if the input or output changes type - console.log("selectedDeviceInfo", selectedDeviceInfo); + logger.debug("selectedDeviceInfo", selectedDeviceInfo); } function changeDevice() { diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 3334d5c38..8a092562f 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -989,12 +989,19 @@ }; function createDiagnostic(options) { + var data = null; + try { + data = JSON.stringify(options) + } + catch(e) { + data = JSON.stringify({data_error: "unable to JSON.stringify debug data:" + e.toString()}) + } return $.ajax({ type: "POST", url: '/api/diagnostics', dataType: "json", contentType: 'application/json', - data: JSON.stringify(options) + data: data, }); } diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index a8b15cdd0..b8e2a5810 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -614,7 +614,7 @@ var instrument = musician.instruments[j]; var inst = '/assets/content/icon_instrument_default24.png'; if (instrument.instrument_id in instrument_logo_map) { - inst = instrument_logo_map[instrument.instrument_id]; + inst = instrument_logo_map[instrument.instrument_id].asset; } instrumentLogoHtml += ' '; } diff --git a/web/app/assets/javascripts/searchResults.js b/web/app/assets/javascripts/searchResults.js index 1af9398e5..ddea50655 100644 --- a/web/app/assets/javascripts/searchResults.js +++ b/web/app/assets/javascripts/searchResults.js @@ -226,7 +226,7 @@ for (var i=0; i < instruments.length; i++) { var inst = '../assets/content/icon_instrument_default24.png'; if (instruments[i].instrument_id in instrument_logo_map) { - inst = instrument_logo_map[instruments[i].instrument_id]; + inst = instrument_logo_map[instruments[i].instrument_id].asset; instrumentLogoHtml += ' '; } } diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index 6563f8544..456ee9c02 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -93,7 +93,7 @@ logger.debug("Find:Finding instruments. Participant tracks:", participant.tracks); var inst = '../assets/content/icon_instrument_default24.png'; if (track.instrument_id in instrument_logo_map) { - inst = instrument_logo_map[track.instrument_id]; + inst = instrument_logo_map[track.instrument_id].asset; } instrumentLogoHtml += ' '; } diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index e45ef62ec..eb58ab543 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -86,11 +86,54 @@ $.each(context._.keys(icon_map_base), function (index, instrumentId) { var icon = icon_map_base[instrumentId]; - instrumentIconMap24[instrumentId] = "/assets/content/icon_instrument_" + icon + "24.png"; - instrumentIconMap45[instrumentId] = "/assets/content/icon_instrument_" + icon + "45.png"; - instrumentIconMap256[instrumentId] = "/assets/content/icon_instrument_" + icon + "256.png"; + instrumentIconMap24[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "24.png", name: instrumentId}; + instrumentIconMap45[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "45.png", name: instrumentId}; + instrumentIconMap256[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "256.png", name: instrumentId}; }); + context.JK.iconInstrumentSelect = function() { + var $select = $(context._.template($('#template-icon-instrument-select').html(), {instruments:instrumentIconMap24}, { variable: 'data' })); + var $ul = $select.find('ul'); + var $currentInstrument = $select.find('.current-instrument'); + + context.JK.hoverBubble($currentInstrument, $ul.html(), { + trigger:'click', + cssClass: 'icon-instrument-selector-popup', + spikeGirth:0, + spikeLength:0, + width:150, + closeWhenOthersOpen: true, + preShow: function() { + }, + postShow:function(container) { + $(container).find('li').click(onInstrumentSelected) + }}); + + function select(instrument_id) { + $currentInstrument.empty(); + $currentInstrument.removeClass('none'); + $currentInstrument.append(''); + } + function close() { + $currentInstrument.btOff(); + } + + function onInstrumentSelected() { + var $li = $(this); + var instrument_id = $li.attr('data-instrument-id'); + + select(instrument_id); + close(); + $select.data('instrument_id', instrument_id); + $select.triggerHandler('instrument_selected', {instrument_id: instrument_id}); + return false; + }; + + $currentInstrument.text('?'); + + return $select; + } + /** * Associates a help bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) * @param $element The element that should show the help when hovered @@ -366,26 +409,26 @@ context.JK.getInstrumentIcon24 = function (instrument) { if (instrument in instrumentIconMap24) { - return instrumentIconMap24[instrument]; + return instrumentIconMap24[instrument].asset; } - return instrumentIconMap24["default"]; + return instrumentIconMap24["default"].asset; }; context.JK.getInstrumentIcon45 = function (instrument) { if (instrument in instrumentIconMap45) { - return instrumentIconMap45[instrument]; + return instrumentIconMap45[instrument].asset; } - return instrumentIconMap45["default"]; + return instrumentIconMap45["default"].asset; }; context.JK.getInstrumentIcon256 = function (instrument) { if (instrument in instrumentIconMap256) { - return instrumentIconMap256[instrument]; + return instrumentIconMap256[instrument].asset; } - return instrumentIconMap256["default"]; + return instrumentIconMap256["default"].asset; }; // meant to pass in a bunch of images with an instrument-id attribute on them. diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 844f9ca42..b01a9bd31 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -46,6 +46,7 @@ *= require ./textMessageDialog *= require ./acceptFriendRequestDialog *= require ./launchAppDialog + *= require ./iconInstrumentSelect *= require ./terms *= require ./createSession *= require ./feed diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index 82d262344..c0367d69f 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -366,11 +366,27 @@ .wizard-step[layout-wizard-step="2"] { .wizard-step-content .wizard-step-column { width:25%; + + + &:nth-of-type(3) { + width:40%; + } + + + &:nth-of-type(4) { + width:10%; + } + } + + .icon-instrument-select { + padding:3px 0; // to combine 24 of .current-instrument + 3x on either side + margin:0 auto 15px; // 15 margin-bottom to match tracks on the left + width:30px; } .unassigned-channels { min-height:240px; - overflow-y:scroll; + overflow-y:auto; //padding-right:18px; // to keep draggables off of scrollbar. maybe necessary &.drag-in-progress { @@ -391,7 +407,7 @@ position:absolute; } .track { - + margin-bottom: 15px; .track-target { &.possible-target { border-color:white; @@ -404,6 +420,7 @@ .ftue-input { + font-size:12px; cursor: move; padding: 4px; border: solid 1px #999; @@ -412,11 +429,14 @@ overflow:hidden; text-align:left; direction:rtl; - font-size:12px; - + text-overflow:ellipses; &.ui-draggable-dragging{ margin-bottom:0; } + + &:hover { + color:white; + } } .track-target { @@ -425,7 +445,6 @@ padding: 4px; border: solid 1px #999; margin-left: 15px; - margin-bottom: 15px; height:20px; overflow:hidden; @@ -444,15 +463,26 @@ } } - &[track-count="2"]:not(.drag-in-progress) { + .placeholder { + display:none; + font-size:12px; + } + + &[track-count="0"] { + .placeholder { + display:inline; + } + } + + &[track-count="2"] { .ftue-input { width:50%; display:inline-block; } - .ftue-input:nth-child(1)::before { - content: ' , ' - } - ; + /**.ftue-input:nth-child(1)::before { + content: ','; + padding-right:3px; + }*/ } } } diff --git a/web/app/assets/stylesheets/client/iconInstrumentSelect.css.scss b/web/app/assets/stylesheets/client/iconInstrumentSelect.css.scss new file mode 100644 index 000000000..601222f00 --- /dev/null +++ b/web/app/assets/stylesheets/client/iconInstrumentSelect.css.scss @@ -0,0 +1,47 @@ +.icon-instrument-select { + .current-instrument { + width:24px; + height:24px; + font-size:20px; + cursor:pointer; + + &.none { + height:22px; + width:22px; + border:1px solid white; + -webkit-border-radius:12px; + -moz-border-radius:12px; + border-radius:12px; + line-height:22px; + vertical-align: middle; + text-align:center; + } + } + ul { + display:none; + border:1px solid white; + max-height:250px; + position:absolute; + overflow:auto; + width:150px; + background-color:#333; + text-align:left; + } +} + +.icon-instrument-selector-popup { + .bt-content { + height:150px; + width:150px; + background-color:#333; + text-align:left; + overflow:auto; + border:1px solid #ED3618; + + li { + margin-left:5px; + list-style-type: none; + } + } + +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/sidebar.css.scss b/web/app/assets/stylesheets/client/sidebar.css.scss index 9491f54c0..ff21873e4 100644 --- a/web/app/assets/stylesheets/client/sidebar.css.scss +++ b/web/app/assets/stylesheets/client/sidebar.css.scss @@ -206,7 +206,7 @@ .chat-message-sender { font-weight:bold; margin-right:10px; - color: #020C81; + color: #ED3618; &:after { content:':' diff --git a/web/app/views/clients/_iconInstrumentSelect.html.haml b/web/app/views/clients/_iconInstrumentSelect.html.haml new file mode 100644 index 000000000..50ba7271d --- /dev/null +++ b/web/app/views/clients/_iconInstrumentSelect.html.haml @@ -0,0 +1,12 @@ +%script{type: 'text/template', id: 'template-icon-instrument-select'} + .icon-instrument-select + .current-instrument.none + %a.arrow-down + %ul + = '{% _.each(data.instruments, function(instrument, instrumentId) { %}' + %li{'data-instrument-id' => '{{instrumentId}}'} + %a{href:'#'} + %img{src: '{{instrument.asset}}'} + = '{{instrument.name}}' + = '{% }) %}' + diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index e91229d2c..7aeaa2e4c 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -148,7 +148,8 @@ %h2 Track Input Port(s) .tracks .wizard-step-column - %h2 Track Instrument + %h2 Instrument + .instruments .wizard-step{ 'layout-wizard-step' => "3", 'dialog-title' => "Configure Voice Chat", 'dialog-purpose' => "ConfigureVoiceChat" } .ftuesteps @@ -245,9 +246,10 @@ = '{{data.name}}' %script{type: 'text/template', id: 'template-assignable-port'} - .ftue-input{id: '{{data.id}}'} {{data.name}} + .ftue-input{'data-input-id' => '{{data.id}}'} {{data.name}} %script{type: 'text/template', id: 'template-track-target'} .track{'data-num' => '{{data.num}}'} .num {{data.num + 1}}: - .track-target{'data-num' => '{{data.num}}'} + .track-target{'data-num' => '{{data.num}}', 'track-count' => 0} + %span.placeholder None diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 9b1b8cbfc..ee8f3bbae 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -18,6 +18,7 @@ <%= render "vu_meters" %> <%= render "ftue" %> <%= render "jamServer" %> +<%= render "iconInstrumentSelect" %> <%= render "clients/gear/gear_wizard" %> <%= render "terms" %> <%= render "leaveSessionWarning" %> From 05666f79271f7d684d9e536790d05882e95f6508 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 16 May 2014 14:39:37 -0500 Subject: [PATCH 07/29] wip --- db/manifest | 1 + db/up/latency_tester.sql | 8 ++ ruby/Gemfile | 1 + ruby/lib/jam_ruby.rb | 1 + ruby/lib/jam_ruby/connection_manager.rb | 4 +- .../jam_ruby/constants/validation_messages.rb | 2 +- ruby/lib/jam_ruby/models/connection.rb | 12 +- ruby/lib/jam_ruby/models/latency_tester.rb | 67 +++++++++ ruby/spec/factories.rb | 5 + .../jam_ruby/models/latency_tester_spec.rb | 55 ++++++++ ruby/spec/spec_db.rb | 8 +- ruby/spec/spec_helper.rb | 1 + ruby/spec/support/profile.rb | 22 +++ .../assets/javascripts/gear/gear_wizard.js | 29 +++- .../javascripts/gear/step_configure_tracks.js | 44 ++++-- .../gear/step_configure_voice_chat.js | 127 ++++++++++++++++++ .../javascripts/gear/step_select_gear.js | 71 +++++++++- .../javascripts/jquery.instrumentSelector.js | 85 ++++++++++++ web/app/assets/javascripts/utils.js | 49 +------ .../stylesheets/client/gearWizard.css.scss | 32 +++++ .../stylesheets/client/session.css.scss | 11 +- .../api_invited_users_controller.rb | 2 +- .../api_latency_testers_controller.rb | 15 +++ web/app/controllers/clients_controller.rb | 10 +- web/app/views/api_latency_testers/match.rabl | 3 + web/app/views/api_latency_testers/show.rabl | 3 + web/app/views/api_music_sessions/show.rabl | 2 +- web/app/views/clients/_help.html.erb | 4 + .../views/clients/gear/_gear_wizard.html.haml | 16 ++- .../views/clients/latency_tester.html.haml | 110 +++++++++++++++ web/config/routes.rb | 4 + web/spec/spec_db.rb | 7 +- 32 files changed, 726 insertions(+), 85 deletions(-) create mode 100644 db/up/latency_tester.sql create mode 100644 ruby/lib/jam_ruby/models/latency_tester.rb create mode 100644 ruby/spec/jam_ruby/models/latency_tester_spec.rb create mode 100644 ruby/spec/support/profile.rb create mode 100644 web/app/assets/javascripts/jquery.instrumentSelector.js create mode 100644 web/app/controllers/api_latency_testers_controller.rb create mode 100644 web/app/views/api_latency_testers/match.rabl create mode 100644 web/app/views/api_latency_testers/show.rabl create mode 100644 web/app/views/clients/latency_tester.html.haml diff --git a/db/manifest b/db/manifest index 6bc1571b3..f10027f6e 100755 --- a/db/manifest +++ b/db/manifest @@ -159,3 +159,4 @@ update_get_work_for_larger_radius.sql periodic_emails.sql remember_extra_scoring_data.sql indexing_for_regions.sql +latency_tester.sql diff --git a/db/up/latency_tester.sql b/db/up/latency_tester.sql new file mode 100644 index 000000000..000126654 --- /dev/null +++ b/db/up/latency_tester.sql @@ -0,0 +1,8 @@ +CREATE TABLE latency_testers ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + client_id VARCHAR(64) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE connections ALTER COLUMN user_id DROP NOT NULL; \ No newline at end of file diff --git a/ruby/Gemfile b/ruby/Gemfile index 7ecd9ef31..3ae116dfd 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -56,6 +56,7 @@ group :test do gem 'faker' gem 'resque_spec' #, :path => "/home/jam/src/resque_spec/" gem 'timecop' + gem 'rspec-prof' end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 93f414273..64544ad7d 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -78,6 +78,7 @@ require "jam_ruby/models/band_invitation" require "jam_ruby/models/band_musician" require "jam_ruby/models/connection" require "jam_ruby/models/diagnostic" +require "jam_ruby/models/latency_tester" require "jam_ruby/models/friendship" require "jam_ruby/models/active_music_session" require "jam_ruby/models/music_session_comment" diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 5d4eefd2c..efd6dd12b 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -153,11 +153,11 @@ SQL # NOTE this is only used for testing purposes; # actual deletes will be processed in the websocket context which cleans up dependencies def expire_stale_connections() - self.stale_connection_client_ids().each { |client| self.delete_connection(client[:client_id]) } + self.stale_connection_client_ids.each { |client| self.delete_connection(client[:client_id]) } end # expiring connections in stale state, which deletes them - def stale_connection_client_ids() + def stale_connection_client_ids clients = [] ConnectionManager.active_record_transaction do |connection_manager| conn = connection_manager.pg_conn diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 2a13a87b7..20edf028d 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -41,7 +41,7 @@ module ValidationMessages INVALID_FPFILE = "is not valid" #connection - + USER_OR_LATENCY_TESTER_PRESENT = "user or latency_tester must be present" SELECT_AT_LEAST_ONE = "Please select at least one track" # DO NOT CHANGE THIS TEXT MESSAGE UNLESS YOU CHANGE createSession.js.erb, which is looking for it FAN_CAN_NOT_JOIN_AS_MUSICIAN = "A fan can not join a music session as a musician" MUSIC_SESSION_MUST_BE_SPECIFIED = "A music session must be specified" diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 82236b295..2fb66d0ae 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -6,6 +6,7 @@ module JamRuby # client_types TYPE_CLIENT = 'client' TYPE_BROWSER = 'browser' + TYPE_LATENCY_TESTER = 'latency_tester' attr_accessor :joining_session @@ -13,11 +14,14 @@ module JamRuby belongs_to :user, :class_name => "JamRuby::User" belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id + has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all validates :as_musician, :inclusion => {:in => [true, false]} - validates :client_type, :inclusion => {:in => [TYPE_CLIENT, TYPE_BROWSER]} + validates :client_type, :inclusion => {:in => [TYPE_CLIENT, TYPE_BROWSER, TYPE_LATENCY_TESTER]} validate :can_join_music_session, :if => :joining_session? + validate :user_or_latency_tester_present + after_save :require_at_least_one_track_when_in_session, :if => :joining_session? after_create :did_create after_save :report_add_participant @@ -190,5 +194,11 @@ module JamRuby end end + def user_or_latency_tester_present + if user.nil? && client_type != TYPE_LATENCY_TESTER + errors.add(:connection, ValidationMessages::USER_OR_LATENCY_TESTER_PRESENT) + end + end + end end diff --git a/ruby/lib/jam_ruby/models/latency_tester.rb b/ruby/lib/jam_ruby/models/latency_tester.rb new file mode 100644 index 000000000..3b254e5d3 --- /dev/null +++ b/ruby/lib/jam_ruby/models/latency_tester.rb @@ -0,0 +1,67 @@ +module JamRuby + class LatencyTester < ActiveRecord::Base + + belongs_to :connection, class_name: 'JamRuby::Connection', foreign_key: :client_id, primary_key: :client_id + + + # we need to find that latency_tester with the specified connection (and reconnect it) + # or bootstrap a new latency_tester + def self.connect(options) + client_id = options[:client_id] + ip_address = options[:ip_address] + connection_stale_time = options[:connection_stale_time] + connection_expire_time = options[:connection_expire_time] + # first try to find a LatencyTester with that client_id + latency_tester = LatencyTester.find_by_client_id(client_id) + + if latency_tester + if latency_tester.connection + connection = latency_tester.connection + else + connection = Connection.new + connection.client_id = client_id + latency_tester.connection = connection + end + else + latency_tester = LatencyTester.new + unless latency_tester.save + return latency_tester + end + connection = Connection.new + connection.latency_tester = latency_tester + connection.client_id = client_id + end + + if ip_address and !ip_address.eql?(connection.ip_address) + # locidispid stuff + addr = JamIsp.ip_to_num(ip_address) + isp = JamIsp.lookup(addr) + if isp.nil? then ispid = 0 else ispid = isp.coid end + block = GeoIpBlocks.lookup(addr) + if block.nil? then locid = 0 else locid = block.locid end + location = GeoIpLocations.lookup(locid) + if location.nil? + # todo what's a better default location? + locidispid = 0 + else + locidispid = locid*1000000+ispid + end + + connection.ip_address = ip_address + connection.addr = addr + connection.locidispid = locidispid + end + + connection.client_type = 'latency_tester' + connection.aasm_state = Connection::CONNECT_STATE.to_s + connection.stale_time = connection_stale_time + connection.expire_time = connection_expire_time + connection.as_musician = false + unless connection.save + return connection + end + + return latency_tester + end + end +end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 0d6dd9e16..ab776bce5 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -112,6 +112,7 @@ FactoryGirl.define do addr 0 locidispid 0 client_type 'client' + association :user, factory: :user end factory :invitation, :class => JamRuby::Invitation do @@ -464,4 +465,8 @@ FactoryGirl.define do creator JamRuby::Diagnostic::CLIENT data Faker::Lorem.sentence end + + factory :latency_tester, :class => JamRuby::LatencyTester do + association :connection + end end diff --git a/ruby/spec/jam_ruby/models/latency_tester_spec.rb b/ruby/spec/jam_ruby/models/latency_tester_spec.rb new file mode 100644 index 000000000..901072fd2 --- /dev/null +++ b/ruby/spec/jam_ruby/models/latency_tester_spec.rb @@ -0,0 +1,55 @@ +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} } + + it "success" do + latency_tester = FactoryGirl.create(:latency_tester) + latency_tester.connection.should_not be_nil + latency_tester.connection.latency_tester.should_not be_nil + + end + + describe "connect" do + it "no existing latency tester" do + latency_tester = LatencyTester.connect(params) + latency_tester.errors.any?.should be_false + latency_tester.connection.ip_address.should == params[:ip_address] + latency_tester.connection.stale_time.should == params[:connection_stale_time] + latency_tester.connection.expire_time.should == params[:connection_expire_time] + end + + it "existing latency tester, no connection" do + latency_tester = FactoryGirl.create(:latency_tester, connection: nil) + latency_tester.connection.should be_nil + + latency_tester.client_id = params[:client_id ] + latency_tester.save! + + found = LatencyTester.connect(params) + found.should == latency_tester + found.connection.should_not be_nil + found.connection.aasm_state.should == Connection::CONNECT_STATE.to_s + found.connection.client_id.should == latency_tester.client_id + end + + it "existing latency tester, existing connection" do + latency_tester = FactoryGirl.create(:latency_tester) + + latency_tester.connection.aasm_state = Connection::STALE_STATE.to_s + latency_tester.save! + set_updated_at(latency_tester.connection, 1.days.ago) + + params[:client_id] = latency_tester.connection.client_id + + found = LatencyTester.connect(params) + found.should == latency_tester + found.connection.should == latency_tester.connection + # state should have refreshed from stale to connected + found.connection.aasm_state.should == Connection::CONNECT_STATE.to_s + # updated_at needs to be poked on connection to keep stale non-stale + (found.connection.updated_at - latency_tester.connection.updated_at).to_i.should == 60 * 60 * 24 # 1 day + end + end +end diff --git a/ruby/spec/spec_db.rb b/ruby/spec/spec_db.rb index b825973f5..184b6f4c1 100644 --- a/ruby/spec/spec_db.rb +++ b/ruby/spec/spec_db.rb @@ -6,7 +6,13 @@ class SpecDb def self.recreate_database conn = PG::Connection.open("dbname=postgres user=postgres password=postgres host=localhost") conn.exec("DROP DATABASE IF EXISTS #{TEST_DB_NAME}") - conn.exec("CREATE DATABASE #{TEST_DB_NAME}") + if ENV['TABLESPACE'] + conn.exec("CREATE DATABASE #{TEST_DB_NAME} WITH TABLESPACE=#{ENV['TABLESPACE']}") + else + conn.exec("CREATE DATABASE #{TEST_DB_NAME}") + end + + JamDb::Migrator.new.migrate(:dbname => TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost") end end diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 1ab24989e..faaeab0b8 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -3,6 +3,7 @@ ENV["RAILS_ENV"] = "test" require 'simplecov' require 'support/utilities' +require 'support/profile' require 'active_record' require 'jam_db' require 'spec_db' diff --git a/ruby/spec/support/profile.rb b/ruby/spec/support/profile.rb new file mode 100644 index 000000000..60eab2a83 --- /dev/null +++ b/ruby/spec/support/profile.rb @@ -0,0 +1,22 @@ +if ENV['PROFILE'] + require 'ruby-prof' + RSpec.configure do |c| + def profile + result = RubyProf.profile { yield } + name = example.metadata[:full_description].downcase.gsub(/[^a-z0-9_-]/, "-").gsub(/-+/, "-") + printer = RubyProf::CallTreePrinter.new(result) + Dir.mkdir('tmp/performance') + open("tmp/performance/callgrind.#{name}.#{Time.now.to_i}.trace", "w") do |f| + printer.print(f) + end + end + + c.around(:each) do |example| + if ENV['PROFILE'] == 'all' or (example.metadata[:profile] and ENV['PROFILE']) + profile { example.run } + else + example.run + end + end + end +end diff --git a/web/app/assets/javascripts/gear/gear_wizard.js b/web/app/assets/javascripts/gear/gear_wizard.js index a1c4b382b..09d1714ab 100644 --- a/web/app/assets/javascripts/gear/gear_wizard.js +++ b/web/app/assets/javascripts/gear/gear_wizard.js @@ -9,7 +9,8 @@ var $dialog = null; var $wizardSteps = null; var $currentWizardStep = null; - var step = 0; + var step = null; + var previousStep = null; var $templateSteps = null; var $templateButtons = null; var $templateAudioPort = null; @@ -48,11 +49,21 @@ 6: stepSuccess } - function beforeHideStep($step) { - var stepInfo = STEPS[step]; + function newSession() { + context._.each(STEPS, function(stepInfo, stepNumber) { + if(stepInfo.newSession) { + stepInfo.newSession.call(stepInfo); + } + }); + } + + function beforeHideStep() { + if(!previousStep) {return} + + var stepInfo = STEPS[previousStep]; if (!stepInfo) { - throw "unknown step: " + step; + throw "unknown step: " + previousStep; } if(stepInfo.beforeHide) { @@ -73,7 +84,7 @@ function moveToStep() { var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); - beforeHideStep($currentWizardStep); + beforeHideStep(); $wizardSteps.hide(); @@ -141,6 +152,7 @@ var newProfileName = 'FTUEAttempt-' + new Date().getTime().toString(); logger.debug("setting FTUE-prefixed profile name to: " + newProfileName); context.jamClient.FTUESetMusicProfileName(newProfileName); + newSession(); } var profileName = context.jamClient.FTUEGetMusicProfileName(); @@ -150,6 +162,8 @@ } function beforeShow(args) { + previousStep = null; + context.jamClient.FTUECancel(); context.jamClient.FTUESetStatus(false); findOrCreateFTUEProfile(); @@ -157,7 +171,7 @@ step = args.d1; if (!step) step = 0; step = parseInt(step); - moveToStep(); + moveToStep(null); } function afterShow() { @@ -165,12 +179,14 @@ } function afterHide() { + step = null; context.jamClient.FTUESetStatus(true); context.jamClient.FTUECancel(); } function back() { if ($(this).is('.button-grey')) return false; + previousStep = step; step = step - 1; moveToStep(); return false; @@ -185,6 +201,7 @@ if(!result) {return false;} } + previousStep = step; step = step + 1; moveToStep(); diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js index 7a5c76915..655f98596 100644 --- a/web/app/assets/javascripts/gear/step_configure_tracks.js +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -18,7 +18,6 @@ var $instrumentsHolder = null; - function loadChannels() { var musicPorts = jamClient.FTUEGetChannels(); @@ -100,7 +99,7 @@ if(track.channels.length > 0) { tracks.tracks.push(track); } - var $instrument = $instrumentsHolder.find('.icon-instrument-select[data-num="' + index + '"]'); + var $instrument = $instrumentsHolder.find('[data-num="' + index + '"]').find('.icon-instrument-select'); track.instrument_id = $instrument.data('instrument_id'); }) return tracks; @@ -140,10 +139,36 @@ context.jamClient.TrackSetAssignment(channelId, true, trackNumber); }); - context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id); + logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", trackNumber, track.instrument_id); + context.jamClient.TrackSetInstrument(trackNumber, context.JK.instrument_id_to_instrument[track.instrument_id].client_id); }); - context.jamClient.TrackSaveAssignments(); + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + return true; + } + else { + context.JK.Banner.showAlert('Unable to save assignments. ' + result); + return false; + } + } + + function loadTrackInstruments() { + var $trackInstruments = $instrumentsHolder.find('.track-instrument'); + + context._.each($trackInstruments, function(trackInstrument) { + var $trackInstrument = $(trackInstrument); + + var trackIndex = parseInt($trackInstrument.attr('data-num')) + 1; + + var clientInstrument = context.jamClient.TrackGetInstrument(trackIndex); + + var instrument = context.JK.client_to_server_instrument_map[clientInstrument]; + + $trackInstrument.instrumentSelectorSet(instrument ? instrument.server_id : instrument); + }); } function handleNext() { @@ -153,13 +178,12 @@ return false; } - save(tracks); - - return true; + return save(tracks); } function beforeShow() { loadChannels(); + loadTrackInstruments(); } function unassignChannel($channel) { @@ -210,9 +234,9 @@ function initializeInstrumentDropdown() { var i; for(i = 0; i < MAX_TRACKS; i++) { - var $instrumentSelect = context.JK.iconInstrumentSelect(); - $instrumentSelect.attr('data-num', i); - $instrumentsHolder.append($instrumentSelect); + var $root = $('
    '); + $root.instrumentSelector().attr('data-num', i); + $instrumentsHolder.append($root); } } diff --git a/web/app/assets/javascripts/gear/step_configure_voice_chat.js b/web/app/assets/javascripts/gear/step_configure_voice_chat.js index 1e9be3ad4..a967a0564 100644 --- a/web/app/assets/javascripts/gear/step_configure_voice_chat.js +++ b/web/app/assets/javascripts/gear/step_configure_voice_chat.js @@ -5,16 +5,143 @@ context.JK = context.JK || {}; context.JK.StepConfigureVoiceChat = function (app) { + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; + var $step = null; + var $reuseAudioInputRadio = null; + var $useChatInputRadio = null; + var $chatInputs = null; + var $templateChatInput = null; + + function newSession() { + $reuseAudioInputRadio.attr('checked', 'checked').iCheck('check'); + } + + function isChannelAvailableForChat(chatChannelId, musicPorts) { + var result = true; + context._.each(musicPorts.input, function(inputChannel) { + // if the channel is currently assigned to a track, it not unassigned + if(inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) { + result = false; + return false; // break + } + }); + + return result; + } + + function isChatEnabled() { + return $useChatInputRadio.is(':checked'); + } function beforeShow() { + if(isChatEnabled()) { + enableChat(); + } + else { + disableChat(); + } + + var musicPorts = jamClient.FTUEGetChannels(); + var chatInputs = context.jamClient.FTUEGetChatInputs(); + + $chatInputs.empty(); + + context._.each(chatInputs, function(chatChannelName, chatChannelId) { + if(isChannelAvailableForChat(chatChannelId, musicPorts)) { + var $chat = $(context._.template($templateChatInput.html(), {id: chatChannelId, name: chatChannelName}, { variable: 'data' })); + $chat.hide(); // we'll show it once it's styled with iCheck + $chatInputs.append($chat); + } + }); + + var $radioButtons = $chatInputs.find('input[name="chat-device"]'); + context.JK.checkbox($radioButtons).on('iChecked', function(e) { + var $input = $(e.currentTarget); + var channelId = $input.attr('data-channel-id'); + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT); + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + } + else { + context.JK.Banner.showAlert('Unable to save assignments. ' + result); + return false; + } + }); + + if(!isChatEnabled()) { + $radioButtons.iCheck('disable'); + } + + $chatInputs.find('.chat-input').show().on('click', function() { + if(!isChatEnabled()) { + context.JK.prodBubble($step.find('.use-chat-input h3'), 'chat-not-enabled', {}, { positions:['left']}); + } + }) + } + + function disableChat() { + logger.debug("FTUE: disabling chat"); + context.jamClient.TrackSetChatEnable(false); + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + } + else { + context.JK.Banner.showAlert('Unable to disable chat. ' + result); + return false; + } + + var $radioButtons = $chatInputs.find('input[name="chat-device"]'); + $radioButtons.iCheck('disable'); + } + + function enableChat() { + logger.debug("FTUE: enabling chat"); + context.jamClient.TrackSetChatEnable(true); + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + } + else { + context.JK.Banner.showAlert('Unable to enable chat. ' + result); + return false; + } + + var $radioButtons = $chatInputs.find('input[name="chat-device"]'); + $radioButtons.iCheck('enable'); + } + + function handleChatEnabledToggle() { + context.JK.checkbox($reuseAudioInputRadio); + context.JK.checkbox($useChatInputRadio); + + // plugin sets to relative on the element; have to do this as an override + $reuseAudioInputRadio.closest('.iradio_minimal').css('position', 'absolute'); + $useChatInputRadio.closest('.iradio_minimal').css('position', 'absolute'); + + $reuseAudioInputRadio.on('ifChecked', disableChat); + $useChatInputRadio.on('ifChecked', enableChat) } function initialize(_$step) { $step = _$step; + + $reuseAudioInputRadio = $step.find('.reuse-audio-input input'); + $useChatInputRadio = $step.find('.use-chat-input input'); + $chatInputs = $step.find('.chat-inputs'); + $templateChatInput = $('#template-chat-input'); + + handleChatEnabledToggle(); } + this.newSession = newSession; this.beforeShow = beforeShow; this.initialize = initialize; diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js index 5a14006df..1f8049e41 100644 --- a/web/app/assets/javascripts/gear/step_select_gear.js +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -181,11 +181,23 @@ } function selectedBufferIn() { - return parseFloat($frameSize.val()); + return parseFloat($bufferIn.val()); } function selectedBufferOut() { - return parseFloat($frameSize.val()); + return parseFloat($bufferOut.val()); + } + + function setFramesize(value) { + $frameSize.val(value); + } + + function setBufferIn(value) { + $bufferIn.val(value) + } + + function setBufferOut(value) { + $bufferOut.val(value) } function initializeNextButtonState() { @@ -633,15 +645,16 @@ function initializeASIOButtons() { $asioInputControlBtn.unbind('click').click(function () { - context.jamClient.FTUEOpenControlPanel(); // TODO: supply with ID when VRFS-1707 is done + context.jamClient.FTUEOpenControlPanel(selectedAudioInput()); }); $asioOutputControlBtn.unbind('click').click(function () { - context.jamClient.FTUEOpenControlPanel(); // TODO: supply with ID when VRFS-1707 is done + context.jamClient.FTUEOpenControlPanel(selectedAudioOutput()); }); } function initializeKnobs() { $frameSize.unbind('change').change(function () { + updateDefaultBuffers(); jamClient.FTUESetFrameSize(selectedFramesize()); }); @@ -841,6 +854,46 @@ else { $resyncBtn.css('visibility', 'hidden'); } + + updateDefaultFrameSize(); + + updateDefaultBuffers(); + } + + function updateDefaultFrameSize() { + if(selectedDeviceInfo.input.type == 'Win32_wdm' || selectedDeviceInfo.output.type == 'Win32_wdm') { + setFramesize('10'); + } + + jamClient.FTUESetFrameSize(selectedFramesize()); + } + + function updateDefaultBuffers() { + + // handle specific framesize settings + if(selectedDeviceInfo.input.type == 'Win32_wdm' || selectedDeviceInfo.output.type == 'Win32_wdm') { + var framesize = selectedFramesize(); + + if(framesize == 2.5) { + setBufferIn('1'); + setBufferOut('1'); + } + else if(framesize == 5) { + setBufferIn('3'); + setBufferOut('2'); + } + else { + setBufferIn('6'); + setBufferOut('5'); + } + } + else { + setBufferIn(0); + setBufferOut(0); + } + + jamClient.FTUESetInputLatency(selectedBufferIn()); + jamClient.FTUESetOutputLatency(selectedBufferOut()); } function processIOScore(io) { @@ -932,15 +985,22 @@ else { renderIOScore(null, null, null, 'starting', 'starting', 'starting'); var testTimeSeconds = gon.ftue_io_wait_time; // allow time for IO to establish itself - context.jamClient.FTUEStartIoPerfTest(); + var startTime = testTimeSeconds / 2; // start measuring half way through the test, to get past IO oddities renderIOScoringStarted(testTimeSeconds); renderIOCountdown(testTimeSeconds); var interval = setInterval(function () { testTimeSeconds -= 1; renderIOCountdown(testTimeSeconds); + + if(testTimeSeconds == startTime) { + logger.debug("Starting IO Perf Test starting at " + startTime + "s in") + context.jamClient.FTUEStartIoPerfTest(); + } + if (testTimeSeconds == 0) { clearInterval(interval); renderIOScoringStopped(); + logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in") var io = context.jamClient.FTUEGetIoPerfData(); lastIOScore = io; processIOScore(io); @@ -1014,6 +1074,7 @@ } function beforeHide() { + console.log("beforeHide:"); $(window).off('focus', onFocus); } diff --git a/web/app/assets/javascripts/jquery.instrumentSelector.js b/web/app/assets/javascripts/jquery.instrumentSelector.js new file mode 100644 index 000000000..8f054800a --- /dev/null +++ b/web/app/assets/javascripts/jquery.instrumentSelector.js @@ -0,0 +1,85 @@ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + + // creates an iconic/graphical instrument selector. useful when there is minimal real-estate + + $.fn.instrumentSelector = function(options) { + + return this.each(function(index) { + + function select(instrument_id) { + if(instrument_id == null) { + $currentInstrument.text('?'); + $currentInstrument.addClass('none'); + $select.data('instrument_id', null); + } + else { + $currentInstrument.empty(); + $currentInstrument.removeClass('none'); + $currentInstrument.append(''); + $select.data('instrument_id', instrument_id); + } + } + + function close() { + $currentInstrument.btOff(); + } + + function onInstrumentSelected() { + var $li = $(this); + var instrument_id = $li.attr('data-instrument-id'); + + select(instrument_id); + close(); + $select.triggerHandler('instrument_selected', {instrument_id: instrument_id}); + return false; + }; + + var $select = $(context._.template($('#template-icon-instrument-select').html(), {instruments:context.JK.getInstrumentIconMap24()}, { variable: 'data' })); + var $ul = $select.find('ul'); + var $currentInstrument = $select.find('.current-instrument'); + + context.JK.hoverBubble($currentInstrument, $ul.html(), { + trigger:'click', + cssClass: 'icon-instrument-selector-popup', + spikeGirth:0, + spikeLength:0, + width:150, + closeWhenOthersOpen: true, + preShow: function() { + }, + postShow:function(container) { + $(container).find('li').click(onInstrumentSelected) + } + }); + + $currentInstrument.text('?'); + + $(this).append($select); + + this.instrumentSelectorClose = close; + this.instrumentSelectorSet = select; + }); + } + + $.fn.instrumentSelectorClose = function() { + return this.each(function(index){ + if (jQuery.isFunction(this.instrumentSelectorClose)) { + this.instrumentSelectorClose(); + } + }); + } + + $.fn.instrumentSelectorSet = function(instrumentId) { + return this.each(function(index){ + if (jQuery.isFunction(this.instrumentSelectorSet)) { + this.instrumentSelectorSet(instrumentId); + } + }); + } + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index eb58ab543..c4fc28ad3 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -91,49 +91,6 @@ instrumentIconMap256[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "256.png", name: instrumentId}; }); - context.JK.iconInstrumentSelect = function() { - var $select = $(context._.template($('#template-icon-instrument-select').html(), {instruments:instrumentIconMap24}, { variable: 'data' })); - var $ul = $select.find('ul'); - var $currentInstrument = $select.find('.current-instrument'); - - context.JK.hoverBubble($currentInstrument, $ul.html(), { - trigger:'click', - cssClass: 'icon-instrument-selector-popup', - spikeGirth:0, - spikeLength:0, - width:150, - closeWhenOthersOpen: true, - preShow: function() { - }, - postShow:function(container) { - $(container).find('li').click(onInstrumentSelected) - }}); - - function select(instrument_id) { - $currentInstrument.empty(); - $currentInstrument.removeClass('none'); - $currentInstrument.append(''); - } - function close() { - $currentInstrument.btOff(); - } - - function onInstrumentSelected() { - var $li = $(this); - var instrument_id = $li.attr('data-instrument-id'); - - select(instrument_id); - close(); - $select.data('instrument_id', instrument_id); - $select.triggerHandler('instrument_selected', {instrument_id: instrument_id}); - return false; - }; - - $currentInstrument.text('?'); - - return $select; - } - /** * Associates a help bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) * @param $element The element that should show the help when hovered @@ -145,6 +102,7 @@ if (!data) { data = {} } + var helpText = context._.template($('#template-help-' + templateName).html(), data, { variable: 'data' }); var holder = $('
    '); @@ -165,7 +123,8 @@ * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips */ context.JK.prodBubble = function($element, templateName, data, options) { - options['trigger'] = 'now'; + options['trigger'] = 'none'; + options['clickAnywhereToClose'] = false var existingTimer = $element.data("prodTimer"); if(existingTimer) { clearTimeout(existingTimer); @@ -800,7 +759,7 @@ } context.JK.checkbox = function ($checkbox) { - $checkbox.iCheck({ + return $checkbox.iCheck({ checkboxClass: 'icheckbox_minimal', radioClass: 'iradio_minimal', inheritClass: true diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index c0367d69f..9574ee3de 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -405,6 +405,8 @@ .num { position:absolute; + height:29px; + line-height:29px; } .track { margin-bottom: 15px; @@ -506,8 +508,17 @@ } + h3 { + padding-left:30px; + margin-top:14px; + font-weight:bold; + display:inline-block; + } + p { padding-left:30px; + margin-top:5px; + display:inline-block; } input { @@ -515,11 +526,32 @@ margin:auto; width:30px; } + + .iradio_minimal { + margin-top:15px; + display:inline-block; + } } .ftue-box { &.chat-inputs { height: 230px !important; + overflow:auto; + + p { + white-space: nowrap; + display:inline-block; + height:32px; + vertical-align: middle; + } + + .chat-input { + white-space:nowrap; + + .iradio_minimal { + display:inline-block; + } + } } .watch-video { diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index 460c988de..302d345b7 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -34,6 +34,12 @@ display:none; } } + + .track-instrument { + position:absolute; + top:85px; + left:12px; + } } @@ -404,11 +410,6 @@ table.vu td { border-radius:22px; } -.track-instrument { - position:absolute; - top:85px; - left:12px; -} .track-gain { position:absolute; diff --git a/web/app/controllers/api_invited_users_controller.rb b/web/app/controllers/api_invited_users_controller.rb index 87210a778..33a118fad 100644 --- a/web/app/controllers/api_invited_users_controller.rb +++ b/web/app/controllers/api_invited_users_controller.rb @@ -1,4 +1,4 @@ - class ApiInvitedUsersController < ApiController +class ApiInvitedUsersController < ApiController # have to be signed in currently to see this screen before_filter :api_signed_in_user diff --git a/web/app/controllers/api_latency_testers_controller.rb b/web/app/controllers/api_latency_testers_controller.rb new file mode 100644 index 000000000..e9f98f68c --- /dev/null +++ b/web/app/controllers/api_latency_testers_controller.rb @@ -0,0 +1,15 @@ +class ApiLatencyTestersController < ApiController + + # have to be signed in currently to see this screen + before_filter :api_signed_in_user + + respond_to :json + + def match + # some day we can find the best latency tester to test against, now there is only one. + @latency_tester = LatencyTester.first + + respond_with_model(@latency_tester) + end +end + diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb index 637688b11..85822f47f 100644 --- a/web/app/controllers/clients_controller.rb +++ b/web/app/controllers/clients_controller.rb @@ -3,6 +3,10 @@ class ClientsController < ApplicationController include ClientHelper include UsersHelper + + AUTHED = %W{friend} + + def index # we want to enforce that /client is always the client view prefix @@ -15,7 +19,10 @@ class ClientsController < ApplicationController render :layout => 'client' end - AUTHED = %W{friend} + def latency_tester + gon_properties + render :layout => 'client' + end def auth_action if current_user @@ -31,5 +38,4 @@ class ClientsController < ApplicationController redirect_to client_url end end - end diff --git a/web/app/views/api_latency_testers/match.rabl b/web/app/views/api_latency_testers/match.rabl new file mode 100644 index 000000000..c88daa8ad --- /dev/null +++ b/web/app/views/api_latency_testers/match.rabl @@ -0,0 +1,3 @@ +object @latency_tester + +extends "api_latency_testers/show" \ No newline at end of file diff --git a/web/app/views/api_latency_testers/show.rabl b/web/app/views/api_latency_testers/show.rabl new file mode 100644 index 000000000..fd955b185 --- /dev/null +++ b/web/app/views/api_latency_testers/show.rabl @@ -0,0 +1,3 @@ +object @latency_tester + +attribute :id, :client_id \ No newline at end of file diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index 87fcfc683..9e24bb634 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -1,4 +1,4 @@ - object @music_session +object @music_session if !current_user # there should be more data returned, but we need to think very carefully about what data is public for a music session diff --git a/web/app/views/clients/_help.html.erb b/web/app/views/clients/_help.html.erb index 4cf34c068..26c90bfca 100644 --- a/web/app/views/clients/_help.html.erb +++ b/web/app/views/clients/_help.html.erb @@ -12,4 +12,8 @@ + + \ No newline at end of file diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 7aeaa2e4c..ca8afbd2b 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -154,7 +154,7 @@ .wizard-step{ 'layout-wizard-step' => "3", 'dialog-title' => "Configure Voice Chat", 'dialog-purpose' => "ConfigureVoiceChat" } .ftuesteps .clearall - .help-text In this step, you will select, configure, and test your audio gear. Please watch the video for best instructions. + .help-text In this step, you may set up a microphone to use for voice chat. Please watch the video for best instructions. .wizard-step-content .wizard-step-column %h2 Instructions @@ -167,12 +167,14 @@ %h2 Select Voice Chat Option .voicechat-option.reuse-audio-input %input{type:"radio", name: "voicechat", checked:"checked"} + %h3 Use Music Microphone %p I am already using a microphone to capture my vocal or instrumental music, so I can talk with other musicians using that microphone .voicechat-option.use-chat-input - %input{type:"radio", name: "voicechat", checked:"unchecked"} + %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 Track Input Port(s) + %h2 Voice Chat Input .ftue-box.chat-inputs @@ -193,7 +195,7 @@ .wizard-step-column .help-content When you have fully turned off the direct monitoring control (if any) on your audio interface, - please click the Play busson below. If you hear the audio clearly, then your settings are correct, + please click the Play button below. If you hear the audio clearly, then your settings are correct, and you can move ahead to the next step. If you use your audio interface for recording, and use the direct monitoring feature for recording, please note that you will need to remember to turn this feature off every time that you use the JamKazam service. @@ -253,3 +255,9 @@ .num {{data.num + 1}}: .track-target{'data-num' => '{{data.num}}', 'track-count' => 0} %span.placeholder None + +%script{type: 'text/template', id: 'template-chat-input'} + .chat-input + %input{type:"radio", name: "chat-device", 'data-channel-id' => '{{data.id}}'} + %p + = '{{data.name}}' diff --git a/web/app/views/clients/latency_tester.html.haml b/web/app/views/clients/latency_tester.html.haml new file mode 100644 index 000000000..078213ca3 --- /dev/null +++ b/web/app/views/clients/latency_tester.html.haml @@ -0,0 +1,110 @@ += render :partial => "jamServer" + +:javascript + $(function() { + JK = JK || {}; + + JK.root_url = "#{root_url}" + + <% if Rails.env == "development" %> + // if in development mode, we assume you are running websocket-gateway + // on the same host as you hit your server. + JK.websocket_gateway_uri = "ws://" + location.hostname + ":6767/websocket"; + <% else %> + // but in any other mode, just trust the config coming through gon + JK.websocket_gateway_uri = gon.websocket_gateway_uri + <% end %> + if (console) { console.log("websocket_gateway_uri:" + JK.websocket_gateway_uri); } + + + // If no trackVolumeObject (when not running in native client) + // create a fake one. + if (!(window.trackVolumeObject)) { + window.trackVolumeObject = { + bIsMediaFile: false, + broadcast: false, + clientID: "", + instrumentID: "", + master: false, + monitor: false, + mute: false, + name: "", + objectName: "", + record: false, + volL: 0, + volR: 0, + wigetID: "" + }; + } + + + // Some things can't be initialized until we're connected. Put them here. + function _initAfterConnect(connected) { + if (this.didInitAfterConnect) return; + this.didInitAfterConnect = true + + if(!connected) { + jamServer.initiateReconnect(null, true); + } + } + + JK.app = JK.JamKazam(); + var jamServer = new JK.JamServer(JK.app); + jamServer.initialize(); + + // If no jamClient (when not running in native client) + // create a fake one. + if (!(window.jamClient)) { + var p2pMessageFactory = new JK.FakeJamClientMessages(); + window.jamClient = new JK.FakeJamClient(JK.app, p2pMessageFactory); + window.jamClient.SetFakeRecordingImpl(new JK.FakeJamClientRecordings(JK.app, jamClient, p2pMessageFactory)); + } + else if(false) { // set to true to time long running bridge calls + var originalJamClient = window.jamClient; + var interceptedJamClient = {}; + $.each(Object.keys(originalJamClient), function(i, key) { + if(key.indexOf('(') > -1) { + // this is a method. time it + var jsKey = key.substring(0, key.indexOf('(')) + console.log("replacing " + jsKey) + interceptedJamClient[jsKey] = function() { + var original = originalJamClient[key] + var start = new Date(); + if(key == "FTUEGetDevices()") { + var returnVal = eval('originalJamClient.FTUEGetDevices(' + arguments[0] + ')'); + } + else { + var returnVal = original.apply(originalJamClient, arguments); + } + var time = new Date().getTime() - start.getTime(); + if(time >= 0) { // if 0, you'll see ALL bridge calls. If you set it to a higher value, you'll only see calls that are beyond that threshold + console.error(time + "ms jamClient." + jsKey + ' returns=', returnVal); + } + + return returnVal; + } + } + else { + // we need to intercept properties... but how? + } + }); + + + window.jamClient = interceptedJamClient; + } + + // Let's get things rolling... + //if (JK.currentUserId) { + + // JK.app.initialize(); + + + JK.JamServer.connect() // singleton here defined in JamServer.js + .done(function() { + _initAfterConnect(true); + }) + .fail(function() { + _initAfterConnect(false); + }); + } + }) \ No newline at end of file diff --git a/web/config/routes.rb b/web/config/routes.rb index 39482be79..d4ee8c1b0 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -34,6 +34,7 @@ SampleApp::Application.routes.draw do match '/isp/ping:isp', :to => 'users#jnlp', :constraints => {:format => :jnlp}, :as => 'isp_ping' match '/client', to: 'clients#index' + match '/latency_tester', to: 'clients#latency_tester' match '/confirm/:signup_token', to: 'users#signup_confirm', as: 'signup_confirm' @@ -411,6 +412,9 @@ SampleApp::Application.routes.draw do # diagnostic match '/diagnostics' => 'api_diagnostics#create', :via => :post + + # latency_tester + match '/latency_testers' => 'api_latency_testers#match', :via => :get end end diff --git a/web/spec/spec_db.rb b/web/spec/spec_db.rb index 104e63fb5..94432e42e 100644 --- a/web/spec/spec_db.rb +++ b/web/spec/spec_db.rb @@ -13,7 +13,12 @@ class SpecDb db_config["database"] = "postgres" ActiveRecord::Base.establish_connection(db_config) ActiveRecord::Base.connection.execute("DROP DATABASE IF EXISTS #{db_test_name}") - ActiveRecord::Base.connection.execute("CREATE DATABASE #{db_test_name}") + if ENV['TABLESPACE'] + ActiveRecord::Base.connection.execute("CREATE DATABASE #{db_test_name} WITH tablespace=#{ENV["TABLESPACE"]}") + else + ActiveRecord::Base.connection.execute("CREATE DATABASE #{db_test_name}") + end + db_config["database"] = db_test_name JamDb::Migrator.new.migrate(:dbname => db_config["database"], :user => db_config["username"], :password => db_config["password"], :host => db_config["host"]) end From e4da30f39e467e2b6447d58b8128e51ec9a03da2 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 19 May 2014 08:46:03 -0500 Subject: [PATCH 08/29] * wip --- admin/app/admin/latency_tester.rb | 32 +++++ admin/spec/factories.rb | 25 ++++ admin/spec/features/latency_testers_spec.rb | 36 ++++++ ruby/lib/jam_ruby/message_factory.rb | 26 ++++- ruby/lib/jam_ruby/models/connection.rb | 1 + ruby/lib/jam_ruby/models/diagnostic.rb | 3 + ruby/lib/jam_ruby/models/latency_tester.rb | 1 + ruby/spec/factories.rb | 13 ++- .../jam_ruby/models/latency_tester_spec.rb | 4 +- web/Gemfile | 2 +- web/app/assets/javascripts/AAA_Log.js | 33 +++++- .../assets/javascripts/AAB_message_factory.js | 12 ++ web/app/assets/javascripts/JamServer.js | 63 ++++++---- web/app/assets/javascripts/fakeJamClient.js | 17 +++ .../stylesheets/client/jamServer.css.scss | 4 + web/app/controllers/clients_controller.rb | 25 ++++ web/app/helpers/client_helper.rb | 9 ++ web/app/views/clients/index.html.erb | 5 + .../views/clients/latency_tester.html.haml | 69 +++-------- web/config/application.rb | 3 +- web/config/initializers/eventmachine.rb | 3 +- web/spec/factories.rb | 2 + web/spec/spec_helper.rb | 3 +- websocket-gateway/Gemfile | 1 + websocket-gateway/bin/websocket_gateway | 3 +- websocket-gateway/config/application.yml | 1 + websocket-gateway/lib/jam_websockets.rb | 1 + .../lib/jam_websockets/router.rb | 92 ++++++++++++--- .../lib/jam_websockets/server.rb | 24 +++- .../lib/jam_websockets/trust_check.rb | 35 ++++++ websocket-gateway/spec/factories.rb | 15 +++ .../spec/jam_websockets/router_spec.rb | 110 ++++++++++++------ .../spec/jam_websockets/trust_check_spec.rb | 45 +++++++ 33 files changed, 567 insertions(+), 151 deletions(-) create mode 100644 admin/app/admin/latency_tester.rb create mode 100644 admin/spec/features/latency_testers_spec.rb create mode 100644 websocket-gateway/lib/jam_websockets/trust_check.rb create mode 100644 websocket-gateway/spec/jam_websockets/trust_check_spec.rb diff --git a/admin/app/admin/latency_tester.rb b/admin/app/admin/latency_tester.rb new file mode 100644 index 000000000..a62e506d4 --- /dev/null +++ b/admin/app/admin/latency_tester.rb @@ -0,0 +1,32 @@ +ActiveAdmin.register JamRuby::LatencyTester, :as => 'LatencyTester' do + + config.filters = true + config.per_page = 50 + config.clear_action_items! + config.sort_order = "client_id" + menu :parent => 'Operations' + + controller do + def scoped_collection + @latency_testers ||= end_of_association_chain + .order('client_id') + end + end + + + index :as => :block do |latency_tester| + div :for => latency_tester do + h3 "#{latency_tester.client_id}" + columns do + column do + panel 'Details' do + attributes_table_for(latency_tester) do + row :connection do |latency_tester| latency_tester.connection ? "last updated at: #{latency_tester.connection.updated_at}" : "no connection" end + end + end + end + end + end + end +end + diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb index bb947342b..a2d058d80 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -28,6 +28,17 @@ FactoryGirl.define do end end end + + factory :connection, :class => JamRuby::Connection do + sequence(:client_id) { |n| "Client#{n}" } + ip_address "1.1.1.1" + as_musician true + addr 0 + locidispid 0 + client_type 'client' + association :user, factory: :user + end + factory :artifact_update, :class => JamRuby::ArtifactUpdate do sequence(:version) { |n| "0.1.#{n}" } uri { "http://somewhere/jkclient.msi" } @@ -173,4 +184,18 @@ FactoryGirl.define do end end + factory :latency_tester, :class => JamRuby::LatencyTester do + ignore do + connection nil + make_connection true + end + + sequence(:client_id) { |n| "LatencyTesterClientId-#{n}" } + + after(:create) do |latency_tester, evaluator| + latency_tester.connection = evaluator.connection if evaluator.connection + latency_tester.connection = FactoryGirl.create(:connection, client_type: Connection::TYPE_LATENCY_TESTER, client_id: latency_tester.client_id) if evaluator.make_connection + latency_tester.save + end + end end diff --git a/admin/spec/features/latency_testers_spec.rb b/admin/spec/features/latency_testers_spec.rb new file mode 100644 index 000000000..16e72fa9a --- /dev/null +++ b/admin/spec/features/latency_testers_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'Feeds' do + + subject { page } + + before(:each) do + end + + + describe "latency_tester with connection" do + let!(:latency_tester) {FactoryGirl.create(:latency_tester)} + + before(:each) do + visit admin_latency_testers_path + end + + it "shows connection info" do + should have_selector('td', text: "last updated at: #{latency_tester.connection.updated_at}") + end + end + + describe "latency_tester with no connection" do + let!(:latency_tester) {FactoryGirl.create(:latency_tester, client_id: 'abc', make_connection: false)} + + before(:each) do + visit admin_latency_testers_path + end + + it "shows no connection" do + should have_selector('td', text: "no connection") + end + end + + +end diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index f2da3c8c9..988d277ef 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -22,13 +22,11 @@ module JamRuby Jampb::ClientMessage.parse(payload) end - # create a login message using user/pass - def login_with_user_pass(username, password, options = {}) + # create a login message using client_id (used by latency_tester) + def login_with_client_id(client_id) login = Jampb::Login.new( - :username => username, - :password => password, - :client_id => options[:client_id], - :client_type => options[:client_type] + :client_id => client_id, + :client_type => Connection::TYPE_LATENCY_TESTER ) Jampb::ClientMessage.new( @@ -38,6 +36,22 @@ module JamRuby ) end + # create a login message using user/pass + def login_with_user_pass(username, password, options = {}) + login = Jampb::Login.new( + :username => username, + :password => password, + :client_id => options[:client_id], + :client_type => options[:client_type] + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::LOGIN, + :route_to => SERVER_TARGET, + :login => login + ) + end + # create a login message using token (a cookie or similar) def login_with_token(token, options = {}) login = Jampb::Login.new( diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index 2fb66d0ae..9def6a7c1 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -196,6 +196,7 @@ module JamRuby def user_or_latency_tester_present if user.nil? && client_type != TYPE_LATENCY_TESTER + puts client_type errors.add(:connection, ValidationMessages::USER_OR_LATENCY_TESTER_PRESENT) end end diff --git a/ruby/lib/jam_ruby/models/diagnostic.rb b/ruby/lib/jam_ruby/models/diagnostic.rb index a4be2dab5..e7a289b42 100644 --- a/ruby/lib/jam_ruby/models/diagnostic.rb +++ b/ruby/lib/jam_ruby/models/diagnostic.rb @@ -17,6 +17,9 @@ module JamRuby # this implies a coding error MISSING_CLIENT_STATE = 'MISSING_CLIENT_STATE' + # the underlying database connection is gone when the heartbeat comes in + MISSING_CONNECTION = 'MISSING_CONNECTION' + # websocket gateway did not recognize message. indicates out-of-date websocket-gateway UNKNOWN_MESSAGE_TYPE = 'UNKNOWN_MESSAGE_TYPE' diff --git a/ruby/lib/jam_ruby/models/latency_tester.rb b/ruby/lib/jam_ruby/models/latency_tester.rb index 3b254e5d3..2962f3bc2 100644 --- a/ruby/lib/jam_ruby/models/latency_tester.rb +++ b/ruby/lib/jam_ruby/models/latency_tester.rb @@ -24,6 +24,7 @@ module JamRuby end else latency_tester = LatencyTester.new + latency_tester.client_id = client_id unless latency_tester.save return latency_tester end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index ab776bce5..b5f1d57df 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -467,6 +467,17 @@ FactoryGirl.define do end factory :latency_tester, :class => JamRuby::LatencyTester do - association :connection + ignore do + connection nil + make_connection true + end + + sequence(:client_id) { |n| "LatencyTesterClientId-#{n}" } + + after(:create) do |latency_tester, evaluator| + latency_tester.connection = evaluator.connection if evaluator.connection + latency_tester.connection = FactoryGirl.create(:connection, client_type: Connection::TYPE_LATENCY_TESTER, client_id: latency_tester.client_id) if evaluator.make_connection + latency_tester.save + end end end diff --git a/ruby/spec/jam_ruby/models/latency_tester_spec.rb b/ruby/spec/jam_ruby/models/latency_tester_spec.rb index 901072fd2..e0d3071d2 100644 --- a/ruby/spec/jam_ruby/models/latency_tester_spec.rb +++ b/ruby/spec/jam_ruby/models/latency_tester_spec.rb @@ -21,10 +21,10 @@ describe LatencyTester do end it "existing latency tester, no connection" do - latency_tester = FactoryGirl.create(:latency_tester, connection: nil) + latency_tester = FactoryGirl.create(:latency_tester, client_id: params[:client_id], make_connection: false) latency_tester.connection.should be_nil - latency_tester.client_id = params[:client_id ] + latency_tester.client_id = params[:client_id] latency_tester.save! found = LatencyTester.connect(params) diff --git a/web/Gemfile b/web/Gemfile index 2595343ce..e8a9e2c7e 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -71,7 +71,7 @@ gem 'resque_mailer' #gem 'typescript-src', path: '../../typescript-src-ruby' #gem 'typescript-node', path: '../../typescript-node-ruby' #gem 'typescript-rails', path: '../../typescript-rails' - +gem 'netaddr' gem 'quiet_assets', :group => :development gem 'bugsnag' gem 'multi_json', '1.9.0' diff --git a/web/app/assets/javascripts/AAA_Log.js b/web/app/assets/javascripts/AAA_Log.js index bdd411e38..90851d08a 100644 --- a/web/app/assets/javascripts/AAA_Log.js +++ b/web/app/assets/javascripts/AAA_Log.js @@ -19,6 +19,14 @@ 'log':null, 'debug':null, 'info':null, 'warn':null, 'error':null, 'assert':null, 'trace':null, 'exception':null } + var backend_methods = { + "log" : 4, + "debug" : 4, + "info" : 3, + "warn" : 2, + "error" : 1 + } + var logCache = []; if ('undefined' === typeof(context.console)) { @@ -33,15 +41,28 @@ context.console.debug = function() { console.log(arguments); } } + console.proxy_logs_to_backend = false; + // http://tobyho.com/2012/07/27/taking-over-console-log/ function takeOverConsole(){ var console = window.console - if (!console) return + if (!console) return; + var i = null; function intercept(method){ var original = console[method] console[method] = function(){ - logCache.push([method].concat(arguments)); + var logAsString = []; + for(i in arguments) { + var arg = arguments[i]; + try { + logAsString.push(JSON.stringify(arg)); + } + catch(e) { + logAsString.push("unable to parse node: " + e.toString()); + } + } + logCache.push([method].concat(logAsString)); if(logCache.length > 50) { // keep the cache size 50 or lower logCache.pop(); @@ -55,9 +76,15 @@ var message = Array.prototype.slice.apply(arguments).join(' ') original(message) } + if(console.proxy_logs_to_backend && context.jamClient) { + var backendLevel = backend_methods[method]; + if(backendLevel) { + context.jamClient.log(backendLevel, logAsString.join(', ')); + } + } } } - var methods = ['log', 'warn', 'error'] + var methods = ['log', 'warn', 'error', 'debug', 'info'] for (var i = 0; i < methods.length; i++) intercept(methods[i]) } diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 3f974b0a2..424754227 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -126,6 +126,18 @@ return client_container(msg.LOGIN, route_to.SERVER, login); }; + // create a login message using only the client_id. only valid for latency_tester + factory.login_with_client_id = function(client_id, client_type) { + if(client_type != 'latency_tester') { + throw "client_type must be latency_tester in login_with_client_id"; + } + var login = { + client_id : client_id, + client_type : client_type + }; + return client_container(msg.LOGIN, route_to.SERVER, login); + }; + // create a music session login message factory.login_music_session = function(music_session) { var login_music_session = { music_session : music_session }; diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index a407453db..748490320 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -19,6 +19,8 @@ // uniquely identify the websocket connection var channelId = null; var clientType = null; + var mode = null; + var rest = context.JK.Rest(); // heartbeat var heartbeatInterval = null; @@ -159,8 +161,8 @@ if(lastHeartbeatSentTime) { var drift = new Date().getTime() - lastHeartbeatSentTime.getTime() - heartbeatMS; - if(drift > 500) { - logger.error("significant drift between heartbeats: " + drift + 'ms beyond target interval') + if (drift > 500) { + logger.warn("significant drift between heartbeats: " + drift + 'ms beyond target interval') } } lastHeartbeatSentTime = now; @@ -169,6 +171,10 @@ } } + function isClientMode() { + return mode == "client"; + } + function loggedIn(header, payload) { if (!connectTimeout) { @@ -180,21 +186,21 @@ app.clientId = payload.client_id; - // tell the backend that we have logged in - context.jamClient.OnLoggedIn(payload.user_id, payload.token); - - $.cookie('client_id', payload.client_id); - + if(isClientMode()) { + // tell the backend that we have logged in + context.jamClient.OnLoggedIn(payload.user_id, payload.token); + $.cookie('client_id', payload.client_id); + } heartbeatMS = payload.heartbeat_interval * 1000; connection_expire_time = payload.connection_expire_time * 1000; - logger.debug("jamkazam.js.loggedIn(): clientId=" + app.clientId + ", heartbeat=" + payload.heartbeat_interval + "s, expire_time=" + payload.connection_expire_time + 's'); + logger.info("jamkazam.js.loggedIn(): clientId=" + app.clientId + ", heartbeat=" + payload.heartbeat_interval + "s, expire_time=" + payload.connection_expire_time + 's'); heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat connectDeferred.resolve(); - activeElementEvent('afterConnect', payload); + activeElementEvent('afterConnect', payload); } function heartbeatAck(header, payload) { @@ -265,7 +271,7 @@ .always(function() { if ($currentDisplay.is('.no-websocket-connection')) { // this path is the 'not in session path'; so there is nothing else to do - $currentDisplay.hide(); + $currentDisplay.removeClass('active'); // TODO: tell certain elements that we've reconnected } @@ -299,7 +305,7 @@ $inSituContent.find('.reconnect-countdown').html(formatDelaySecs(reconnectDelaySecs())); $messageContents.empty(); $messageContents.append($inSituContent); - $inSituBannerHolder.show(); + $inSituBannerHolder.addClass('active'); content = $inSituBannerHolder; } @@ -447,6 +453,10 @@ if(!clientType) { clientType = context.JK.clientType(); } + if(!mode) { + mode = context.jamClient.getOperatingMode(); + } + connectDeferred = new $.Deferred(); channelId = context.JK.generateUUID(); // create a new channel ID for every websocket connection @@ -472,7 +482,7 @@ }; server.close = function (in_error) { - logger.log("closing websocket"); + logger.info("closing websocket"); clientClosedConnection = true; server.socket.close(); @@ -488,9 +498,20 @@ server.send(loginMessage); }; + server.latencyTesterLogin = function() { + var loginMessage = msg_factory.login_with_client_id(context.jamClient.clientID, 'latency_tester'); + server.send(loginMessage); + } + server.onOpen = function () { - logger.log("server.onOpen"); - server.rememberLogin(); + logger.debug("server.onOpen"); + + if(isClientMode()) { + server.rememberLogin(); + } + else { + server.latencyTesterLogin(); + } }; server.onMessage = function (e) { @@ -500,7 +521,7 @@ callbacks = server.dispatchTable[message.type]; if (message.type != context.JK.MessageType.HEARTBEAT_ACK && message.type != context.JK.MessageType.PEER_MESSAGE) { - logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); + logger.info("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); } if (callbacks !== undefined) { @@ -515,13 +536,13 @@ } } else { - logger.log("Unexpected message type %s.", message.type); + logger.info("Unexpected message type %s.", message.type); } }; // onClose is called if either client or server closes connection server.onClose = function () { - logger.log("Socket to server closed."); + logger.info("Socket to server closed."); if (connectDeferred.state() === "pending") { connectDeferred.reject(); @@ -535,12 +556,12 @@ var jsMessage = JSON.stringify(message); if (message.type != context.JK.MessageType.HEARTBEAT && message.type != context.JK.MessageType.PEER_MESSAGE) { - logger.log("server.send(" + jsMessage + ")"); + logger.info("server.send(" + jsMessage + ")"); } if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) { server.socket.send(jsMessage); } else { - logger.log("Dropped message because server connection is closed."); + logger.warn("Dropped message because server connection is closed."); } }; @@ -548,7 +569,7 @@ var loginMessage; if (!server.signedIn) { - logger.log("Not signed in!"); + logger.warn("Not signed in!"); // TODO: surface the error return; } @@ -601,7 +622,7 @@ server.connected = true; if (context.jamClient !== undefined) { - logger.debug("... (handling LOGIN_ACK) Updating backend client, connected to true and clientID to " + + logger.info("... (handling LOGIN_ACK) Updating backend client, connected to true and clientID to " + payload.client_id); context.jamClient.connected = true; context.jamClient.clientID = server.clientID; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index bdede9b5f..4752d26aa 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -685,6 +685,19 @@ } + function log(level, message) { + console.log("beep : " + message) + } + + function getOperatingMode() { + if (location.pathname == '/latency_tester') { + return 'server'; + } + else { + return 'client'; + } + } + // passed an array of recording objects from the server function GetLocalRecordingState(recordings) { var result = { recordings:[]}; @@ -884,6 +897,10 @@ // fake calls; not a part of the actual jam client this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; this.SetFakeRecordingImpl = SetFakeRecordingImpl; + + this.log = log; + this.getOperatingMode = getOperatingMode; + this.clientID = "devtester"; }; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/jamServer.css.scss b/web/app/assets/stylesheets/client/jamServer.css.scss index 95e1ecf34..44955e99f 100644 --- a/web/app/assets/stylesheets/client/jamServer.css.scss +++ b/web/app/assets/stylesheets/client/jamServer.css.scss @@ -3,6 +3,10 @@ text-align:center; width:100%; position:absolute; + + &.active { + display:block; + } } .server-connection { diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb index 85822f47f..1a44261dd 100644 --- a/web/app/controllers/clients_controller.rb +++ b/web/app/controllers/clients_controller.rb @@ -38,4 +38,29 @@ class ClientsController < ApplicationController redirect_to client_url end end +<<<<<<< HEAD +======= + + private + + def gon_properties + # use gon to pass variables into javascript + gon.websocket_gateway_uri = Rails.application.config.websocket_gateway_uri + gon.websocket_gateway_trusted_uri = Rails.application.config.websocket_gateway_trusted_uri + gon.check_for_client_updates = Rails.application.config.check_for_client_updates + gon.fp_apikey = Rails.application.config.filepicker_rails.api_key + gon.fp_upload_dir = Rails.application.config.filepicker_upload_dir + gon.allow_force_native_client = Rails.application.config.allow_force_native_client + gon.ftue_io_wait_time = Rails.application.config.ftue_io_wait_time + + # is this the native client or browser? + @nativeClient = is_native_client? + + # let javascript have access to the server's opinion if this is a native client + gon.isNativeClient = @nativeClient + + gon.use_cached_session_scores = Rails.application.config.use_cached_session_scores + gon.allow_both_find_algos = Rails.application.config.allow_both_find_algos + end +>>>>>>> * wip end diff --git a/web/app/helpers/client_helper.rb b/web/app/helpers/client_helper.rb index 0011cfe7f..f70eddec9 100644 --- a/web/app/helpers/client_helper.rb +++ b/web/app/helpers/client_helper.rb @@ -29,6 +29,15 @@ module ClientHelper gon.websocket_gateway_uri = Rails.application.config.websocket_gateway_uri end + if Rails.env == "development" + # if in development mode, we assume you are running websocket-gateway + # on the same host as you hit your server. + gon.websocket_gateway_trusted_uri = "ws://" + request.host + ":6768/websocket"; + else + # but in any other mode, just use config + gon.websocket_gateway_trusted_uri = Rails.application.config.websocket_gateway_trusted_uri + end + gon.check_for_client_updates = Rails.application.config.check_for_client_updates gon.fp_apikey = Rails.application.config.filepicker_rails.api_key gon.fp_upload_dir = Rails.application.config.filepicker_upload_dir diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index ee8f3bbae..7dd8a344f 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -258,6 +258,11 @@ JK.initJamClient(); + // latency_tester does not want to be here + if(window.jamClient.getOperatingMode() == "server") { + window.location.href = "/latency_tester"; + } + // Let's get things rolling... if (JK.currentUserId) { diff --git a/web/app/views/clients/latency_tester.html.haml b/web/app/views/clients/latency_tester.html.haml index 078213ca3..fd8d61bbb 100644 --- a/web/app/views/clients/latency_tester.html.haml +++ b/web/app/views/clients/latency_tester.html.haml @@ -1,19 +1,19 @@ += render :partial => "banner" += render :partial => "clients/banners/disconnected" = render :partial => "jamServer" :javascript $(function() { JK = JK || {}; + JK.logger.proxy_logs_to_backend = true; + JK.root_url = "#{root_url}" - <% if Rails.env == "development" %> - // if in development mode, we assume you are running websocket-gateway - // on the same host as you hit your server. - JK.websocket_gateway_uri = "ws://" + location.hostname + ":6767/websocket"; - <% else %> - // but in any other mode, just trust the config coming through gon - JK.websocket_gateway_uri = gon.websocket_gateway_uri - <% end %> + // if in development mode, we assume you are running websocket-gateway + // on the same host as you hit your server. + JK.websocket_gateway_uri = #{Rails.env == "development" ? '"ws://" + location.hostname + ":6768/websocket"' : 'gon.websocket_gateway_trusted_uri'}; + if (console) { console.log("websocket_gateway_uri:" + JK.websocket_gateway_uri); } @@ -59,52 +59,15 @@ window.jamClient = new JK.FakeJamClient(JK.app, p2pMessageFactory); window.jamClient.SetFakeRecordingImpl(new JK.FakeJamClientRecordings(JK.app, jamClient, p2pMessageFactory)); } - else if(false) { // set to true to time long running bridge calls - var originalJamClient = window.jamClient; - var interceptedJamClient = {}; - $.each(Object.keys(originalJamClient), function(i, key) { - if(key.indexOf('(') > -1) { - // this is a method. time it - var jsKey = key.substring(0, key.indexOf('(')) - console.log("replacing " + jsKey) - interceptedJamClient[jsKey] = function() { - var original = originalJamClient[key] - var start = new Date(); - if(key == "FTUEGetDevices()") { - var returnVal = eval('originalJamClient.FTUEGetDevices(' + arguments[0] + ')'); - } - else { - var returnVal = original.apply(originalJamClient, arguments); - } - var time = new Date().getTime() - start.getTime(); - if(time >= 0) { // if 0, you'll see ALL bridge calls. If you set it to a higher value, you'll only see calls that are beyond that threshold - console.error(time + "ms jamClient." + jsKey + ' returns=', returnVal); - } - return returnVal; - } - } - else { - // we need to intercept properties... but how? - } + //JK.app.initialize(); + + JK.JamServer.connect() // singleton here defined in JamServer.js + .done(function() { + _initAfterConnect(true); + }) + .fail(function() { + _initAfterConnect(false); }); - - window.jamClient = interceptedJamClient; - } - - // Let's get things rolling... - //if (JK.currentUserId) { - - // JK.app.initialize(); - - - JK.JamServer.connect() // singleton here defined in JamServer.js - .done(function() { - _initAfterConnect(true); - }) - .fail(function() { - _initAfterConnect(false); - }); - } }) \ No newline at end of file diff --git a/web/config/application.rb b/web/config/application.rb index 1ba0d5f0a..2c8ae1125 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -109,11 +109,12 @@ if defined?(Bundler) config.websocket_gateway_connect_time_expire_client = 60 # 60 matches production config.websocket_gateway_connect_time_stale_browser = 40 # 40 matches production config.websocket_gateway_connect_time_expire_browser = 60 # 60 matches production - + config.websocket_gateway_cidr = ['0.0.0.0/0'] config.websocket_gateway_internal_debug = false config.websocket_gateway_port = 6767 + ENV['JAM_INSTANCE'].to_i # Runs the websocket gateway within the web app config.websocket_gateway_uri = "ws://localhost:#{config.websocket_gateway_port}/websocket" + config.websocket_gateway_trusted_uri = "ws://localhost:#{config.websocket_gateway_port + 1}/websocket" config.external_hostname = ENV['EXTERNAL_HOSTNAME'] || 'localhost' config.external_port = ENV['EXTERNAL_PORT'] || 3000 diff --git a/web/config/initializers/eventmachine.rb b/web/config/initializers/eventmachine.rb index 53858b4d9..324d304de 100644 --- a/web/config/initializers/eventmachine.rb +++ b/web/config/initializers/eventmachine.rb @@ -15,7 +15,8 @@ unless $rails_rake_task :connect_time_expire_browser=> APP_CONFIG.websocket_gateway_connect_time_expire_browser, :rabbitmq_host => APP_CONFIG.rabbitmq_host, :rabbitmq_port => APP_CONFIG.rabbitmq_port, - :calling_thread => current) + :calling_thread => current, + :cidr => APP_CONFIG.websocket_gateway_cidr) end Thread.stop end diff --git a/web/spec/factories.rb b/web/spec/factories.rb index cf2ba2998..6c5598e9a 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -468,4 +468,6 @@ FactoryGirl.define do message Faker::Lorem.characters(10) end end + + end diff --git a/web/spec/spec_helper.rb b/web/spec/spec_helper.rb index 94960bf13..7a816616c 100644 --- a/web/spec/spec_helper.rb +++ b/web/spec/spec_helper.rb @@ -81,7 +81,8 @@ Thread.new do :connect_time_expire_browser => 6, :rabbitmq_host => 'localhost', :rabbitmq_port => 5672, - :calling_thread => current) + :calling_thread => current, + :cidr => ['0.0.0.0/0']) rescue Exception => e puts "websocket-gateway failed: #{e}" end diff --git a/websocket-gateway/Gemfile b/websocket-gateway/Gemfile index 24daf06e9..912e4475f 100644 --- a/websocket-gateway/Gemfile +++ b/websocket-gateway/Gemfile @@ -47,6 +47,7 @@ gem 'geokit' gem 'geokit-rails', '2.0.1' gem 'mime-types', '1.25.1' gem 'rest-client' +gem 'netaddr' group :development do gem 'pry' diff --git a/websocket-gateway/bin/websocket_gateway b/websocket-gateway/bin/websocket_gateway index e4a544209..274760390 100755 --- a/websocket-gateway/bin/websocket_gateway +++ b/websocket-gateway/bin/websocket_gateway @@ -52,4 +52,5 @@ Server.new.run(:port => config["port"], :connect_time_stale_browser => config["connect_time_stale_browser"], :connect_time_expire_browser => config["connect_time_expire_browser"], :rabbitmq_host => config['rabbitmq_host'], - :rabbitmq_port => config['rabbitmq_port']) + :rabbitmq_port => config['rabbitmq_port'], + :cidr => config['cidr']) diff --git a/websocket-gateway/config/application.yml b/websocket-gateway/config/application.yml index 039a2316a..345eb0ed5 100644 --- a/websocket-gateway/config/application.yml +++ b/websocket-gateway/config/application.yml @@ -3,6 +3,7 @@ Defaults: &defaults connect_time_expire_client: 60 connect_time_stale_browser: 40 connect_time_expire_browser: 60 + cidr: [0.0.0.0/0] development: port: 6767 diff --git a/websocket-gateway/lib/jam_websockets.rb b/websocket-gateway/lib/jam_websockets.rb index 36aba1b0b..5fbd91abb 100644 --- a/websocket-gateway/lib/jam_websockets.rb +++ b/websocket-gateway/lib/jam_websockets.rb @@ -6,6 +6,7 @@ require "jam_websockets/session_error" require "jam_websockets/permission_error" require "jam_websockets/client_context" require "jam_websockets/message" +require "jam_websockets/trust_check" require "jam_websockets/router" require "jam_websockets/server" diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 153738ad0..43ab68cc9 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -10,7 +10,7 @@ include Jampb module EventMachine module WebSocket class Connection < EventMachine::Connection - attr_accessor :encode_json, :channel_id, :client_id, :user_id, :context # client_id is uuid we give to each client to track them as we like + attr_accessor :encode_json, :channel_id, :client_id, :user_id, :context, :trusted # client_id is uuid we give to each client to track them as we like # http://stackoverflow.com/questions/11150147/how-to-check-if-eventmachineconnection-is-open attr_accessor :connected @@ -35,8 +35,14 @@ module JamWebsockets class Router - attr_accessor :user_context_lookup, :heartbeat_interval_client, :connect_time_expire_client, :connect_time_stale_client, - :heartbeat_interval_browser, :connect_time_expire_browser, :connect_time_stale_browser + attr_accessor :user_context_lookup, + :amqp_connection_manager, + :heartbeat_interval_client, + :connect_time_expire_client, + :connect_time_stale_client, + :heartbeat_interval_browser, + :connect_time_expire_browser, + :connect_time_stale_browser def initialize() @log = Logging.logger[self] @@ -221,9 +227,10 @@ module JamWebsockets end - def new_client(client) + def new_client(client, is_trusted) # default to using json instead of pb client.encode_json = true + client.trusted = is_trusted client.onopen { |handshake| # a unique ID for this TCP connection, to aid in debugging @@ -473,10 +480,10 @@ module JamWebsockets default_expire = @connect_time_expire_client end - heartbeat_interval = user.heartbeat_interval_client || default_heartbeat + heartbeat_interval = user.try(:heartbeat_interval_client) || default_heartbeat heartbeat_interval = heartbeat_interval.to_i heartbeat_interval = default_heartbeat if heartbeat_interval == 0 # protect against bad config - connection_expire_time = user.connection_expire_time_client || default_expire + connection_expire_time = user.try(:connection_expire_time_client) || default_expire connection_expire_time = connection_expire_time.to_i connection_expire_time = default_expire if connection_expire_time == 0 # protect against bad config connection_stale_time = default_stale # no user override exists for this; not a very meaningful time right now @@ -491,6 +498,51 @@ module JamWebsockets [heartbeat_interval, connection_stale_time, connection_expire_time] end + def add_tracker(user, client, client_type, client_id) + # add a tracker for this user + context = ClientContext.new(user, client, client_type) + @clients[client] = context + add_user(context) + add_client(client_id, context) + context + end + + def handle_latency_tester_login(client_id, client_type, client) + + # respond with LOGIN_ACK to let client know it was successful + remote_ip = extract_ip(client) + heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(nil, client_type) + latency_tester = LatencyTester.connect({ + client_id: client_id, + ip_address: remote_ip, + connection_stale_time: connection_stale_time, + connection_expire_time: connection_expire_time}) + if latency_tester.errors.any? + @log.warn "unable to log in latency_tester with errors: #{latency_tester.errors.inspect}" + raise SessionError, "invalid login: #{latency_tester.errors.inspect}" + end + + client.client_id = client_id + client.user_id = latency_tester.id if latency_tester + + @semaphore.synchronize do + + context = add_tracker(latency_tester, client, client_type, client_id) + + @log.debug "logged in context created: #{context}" + + login_ack = @message_factory.login_ack(remote_ip, + client_id, + nil, + heartbeat_interval, + nil, + false, + latency_tester.id, + connection_expire_time) + send_to_client(client, login_ack) + 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) @@ -500,6 +552,12 @@ module JamWebsockets client_type = login.client_type if login.value_for_tag(6) @log.info("*** handle_login: token=#{token}; client_id=#{client_id}, client_type=#{client_type}") + + if client_type == Connection::TYPE_LATENCY_TESTER + handle_latency_tester_login(client_id, client_type, client) + return + end + reconnected = false # you don't have to supply client_id in login--if you don't, we'll generate one @@ -579,10 +637,7 @@ module JamWebsockets @semaphore.synchronize do # add a tracker for this user - context = ClientContext.new(user, client, client_type) - @clients[client] = context - add_user(context) - add_client(client_id, context) + context = add_tracker(user, client, client_type, client_id) @log.debug "logged in context created: #{context}" @@ -618,7 +673,7 @@ module JamWebsockets Diagnostic.missing_client_state(client.user_id, client.context) raise SessionError, 'context state is gone. please reconnect.' else - connection = Connection.find_by_user_id_and_client_id(context.user.id, context.client.client_id) + connection = Connection.find_by_client_id(context.client.client_id) track_changes_counter = nil if connection.nil? @log.warn "*** WARNING: unable to find connection when handling heartbeat. context= #{context}; killing session" @@ -638,14 +693,15 @@ module JamWebsockets # update user's notification_seen_at field if the heartbeat indicates it saw one # first we try to use the notification id, which should usually exist. # if not, then fallback to notification_seen_at, which is approximately the last time we saw a notification - update_notification_seen_at(connection, context, heartbeat) + update_notification_seen_at(connection, context, heartbeat) if client.context.client_type != Connection::TYPE_LATENCY_TESTER end - - ConnectionManager.active_record_transaction do |connection_manager| - heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(context.user, context.client_type) - connection_manager.reconnect(connection, connection.music_session_id, nil, connection_stale_time, connection_expire_time) - end if connection.stale? + if connection.stale? + ConnectionManager.active_record_transaction do |connection_manager| + heartbeat_interval, connection_stale_time, connection_expire_time = determine_connection_times(context.user, context.client_type) + connection_manager.reconnect(connection, connection.music_session_id, nil, connection_stale_time, connection_expire_time) + end + end end heartbeat_ack = @message_factory.heartbeat_ack(track_changes_counter) @@ -829,7 +885,7 @@ module JamWebsockets end def extract_ip(client) - return Socket.unpack_sockaddr_in(client.get_peername)[1] + Socket.unpack_sockaddr_in(client.get_peername)[1] end private diff --git a/websocket-gateway/lib/jam_websockets/server.rb b/websocket-gateway/lib/jam_websockets/server.rb index 200bcfe8b..830d8c40e 100644 --- a/websocket-gateway/lib/jam_websockets/server.rb +++ b/websocket-gateway/lib/jam_websockets/server.rb @@ -1,6 +1,5 @@ require 'em-websocket' require 'bugsnag' - module JamWebsockets class Server @@ -16,6 +15,7 @@ module JamWebsockets def run(options={}) host = "0.0.0.0" port = options[:port] + trust_port = port + 1 connect_time_stale_client = options[:connect_time_stale_client].to_i connect_time_expire_client = options[:connect_time_expire_client].to_i connect_time_stale_browser = options[:connect_time_stale_browser].to_i @@ -23,6 +23,7 @@ module JamWebsockets rabbitmq_host = options[:rabbitmq_host] rabbitmq_port = options[:rabbitmq_port].to_i calling_thread = options[:calling_thread] + trust_check = TrustCheck.new(trust_port, options[:cidr]) @log.info "starting server #{host}:#{port} staleness_time=#{connect_time_stale_client}; reconnect time = #{connect_time_expire_client}, rabbitmq=#{rabbitmq_host}:#{rabbitmq_port}" @@ -35,7 +36,7 @@ module JamWebsockets @router.start(connect_time_stale_client, connect_time_expire_client, connect_time_stale_browser, connect_time_expire_browser, host: rabbitmq_host, port: rabbitmq_port) do start_connection_expiration start_connection_flagger - start_websocket_listener(host, port, options[:emwebsocket_debug]) + start_websocket_listener(host, port, trust_port, trust_check, options[:emwebsocket_debug]) calling_thread.wakeup if calling_thread end @@ -51,10 +52,21 @@ module JamWebsockets EventMachine::stop_event_loop end - def start_websocket_listener(listen_ip, port, emwebsocket_debug) + def start_websocket_listener(listen_ip, port, trust_port, trust_check, emwebsocket_debug) EventMachine::WebSocket.run(:host => listen_ip, :port => port, :debug => emwebsocket_debug) do |ws| @log.info "new client #{ws}" - @router.new_client(ws) + @router.new_client(ws, false) + end + EventMachine::WebSocket.run(:host => listen_ip, :port => trust_port, :debug => emwebsocket_debug) do |ws| + @log.info "new latency_tester client #{ws}" + # verify this connection came in from a valid subnet, if specified + ip = extract_ip(ws) + if trust_check.trusted?(ip, trust_port) + @router.new_client(ws, true) + else + @log.warn("untrusted client attempted to connect to #{listen_ip}:#{trust_port} from #{ip}") + ws.close + end end @log.debug("started websocket") end @@ -105,6 +117,10 @@ module JamWebsockets end end + private + def extract_ip(client) + Socket.unpack_sockaddr_in(client.get_peername)[1] + end end end diff --git a/websocket-gateway/lib/jam_websockets/trust_check.rb b/websocket-gateway/lib/jam_websockets/trust_check.rb new file mode 100644 index 000000000..71c57a9d7 --- /dev/null +++ b/websocket-gateway/lib/jam_websockets/trust_check.rb @@ -0,0 +1,35 @@ +require 'netaddr' + +module JamWebsockets + class TrustCheck + + attr_accessor :match, :port + + def initialize(port = 0, cidr = '0.0.0.0/0') + + @match = [] + @port = port + + if cidr.kind_of?(Array) + cidr.each do |c| + @match << NetAddr::CIDR.create(c) + end + else + @match << NetAddr::CIDR.create(cidr) + end + end + + def trusted? (remote_ip, port) + trusted = false + if @port == 0 || port == @port + @match.each do |cidr| + if cidr.matches?(remote_ip) + trusted = true + break + end + end + end + trusted + end + end +end diff --git a/websocket-gateway/spec/factories.rb b/websocket-gateway/spec/factories.rb index a2d4f4ec8..cf4fe8586 100644 --- a/websocket-gateway/spec/factories.rb +++ b/websocket-gateway/spec/factories.rb @@ -98,4 +98,19 @@ FactoryGirl.define do proficiency_level 1 priority 0 end + + factory :latency_tester, :class => JamRuby::LatencyTester do + ignore do + connection nil + make_connection true + end + + sequence(:client_id) { |n| "LatencyTesterClientId-#{n}" } + + after(:create) do |latency_tester, evaluator| + latency_tester.connection = evaluator.connection if evaluator.connection + latency_tester.connection = FactoryGirl.create(:connection, client_type: Connection::TYPE_LATENCY_TESTER, client_id: latency_tester.client_id) if evaluator.make_connection + latency_tester.save + end + end end diff --git a/websocket-gateway/spec/jam_websockets/router_spec.rb b/websocket-gateway/spec/jam_websockets/router_spec.rb index aeb6a93b6..092cd9dbe 100644 --- a/websocket-gateway/spec/jam_websockets/router_spec.rb +++ b/websocket-gateway/spec/jam_websockets/router_spec.rb @@ -46,8 +46,6 @@ def login(router, user, password, client_id) message_factory = MessageFactory.new client = LoginClient.new - login_ack = message_factory.login_ack("127.0.0.1", client_id, user.remember_token, 15, nil, false, user.id, 30) - router.should_receive(:send_to_client) do |*args| args.count.should == 2 args[0].should == client @@ -57,32 +55,59 @@ def login(router, user, password, client_id) client.should_receive(:onclose) client.should_receive(:onerror) + + #client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") + + @router.new_client(client, false) + handshake = double("handshake") + handshake.should_receive(:query).twice.and_return({ "pb" => "true", "channel_id" => SecureRandom.uuid }) + 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 + +def heartbeat(router, client) + message_factory = MessageFactory.new + heartbeat = message_factory.heartbeat() + + client.onmsgblock.call heartbeat.to_s +end + +# does a login and returns client +def login_latency_tester(router, latency_tester, client_id) + + client = LoginClient.new + message_factory = MessageFactory.new + + router.should_receive(:send_to_client).at_least(1).times do |*args| + args.count.should == 2 + args[0].should == client + args[1].is_a?(Jampb::ClientMessage).should be_true + end + router.should_receive(:extract_ip).at_least(:once).with(client).and_return("127.0.0.1") + client.should_receive(:onclose) + client.should_receive(:onerror) + #client.should_receive(:get_peername).and_return("\x00\x02\x93\v\x7F\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00") - @router.new_client(client) + @router.new_client(client, true) handshake = double("handshake") handshake.should_receive(:query).twice.and_return({ "pb" => "true", "channel_id" => SecureRandom.uuid }) client.onopenblock.call handshake # create a login message, and pass it into the router via onmsgblock.call - # todo client_type browser or client? i just guessed... [scott] - login = message_factory.login_with_user_pass(user.email, password, :client_id => client_id, :client_type => 'client') + login = message_factory.login_with_client_id(client_id) # first log in client.onmsgblock.call login.to_s - # then join music session - return client -end - -# currently commented out; we have deprecated logging in for jam sessions via websocket-gateway; -# use rest API instead (or direct db access with factory-girl) -def login_music_session(router, client, music_session) - #message_factory = MessageFactory.new - #login_music_session = message_factory.login_music_session(music_session.id) - #login_ack = message_factory.login_music_session_ack(false, nil); - #router.should_receive(:send_to_client).with(client, login_ack) - #client.onmsgblock.call login_music_session.to_s + client end @@ -99,6 +124,7 @@ describe Router do @router.connect_time_expire_browser = 60 @router.connect_time_stale_browser = 40 @router.heartbeat_interval_browser = @router.connect_time_stale_browser / 2 + @router.amqp_connection_manager = AmqpConnectionManager.new(true, 4, host: 'localhost', port: 5672) end subject { @router } @@ -210,32 +236,42 @@ describe Router do end it "should allow login of valid user", :mq => true do - #em do - @user = FactoryGirl.create(:user, - :password => "foobar", :password_confirmation => "foobar") - client1 = login(@router, @user, "foobar", "1") - done - #end + @user = FactoryGirl.create(:user, + :password => "foobar", :password_confirmation => "foobar") + client1 = login(@router, @user, "foobar", "1") + done + end + + + it "should allow login of a latency_tester", :mq => true do + @latency_tester = FactoryGirl.create(:latency_tester) + client1 = login_latency_tester(@router, @latency_tester, @latency_tester.client_id) + done + end + + it "should allow heartbeat of a latency_tester", :mq => true do + @latency_tester = FactoryGirl.create(:latency_tester) + client1 = login_latency_tester(@router, @latency_tester, @latency_tester.client_id) + heartbeat(@router, client1) + done end it "should allow music_session_join of valid user", :mq => true do - #em do - user1 = FactoryGirl.create(:user) # in the music session - user2 = FactoryGirl.create(:user) # in the music session - user3 = FactoryGirl.create(:user) # not in the music session - music_session = FactoryGirl.create(:active_music_session, :creator => user1) + user1 = FactoryGirl.create(:user) # in the music session + user2 = FactoryGirl.create(:user) # in the music session + user3 = FactoryGirl.create(:user) # not in the music session - music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :client_id => "4") - music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :client_id => "5") + music_session = FactoryGirl.create(:active_music_session, :creator => user1) - # make a music_session and define two members + music_session_member1 = FactoryGirl.create(:connection, :user => user1, :music_session => music_session, :client_id => "4") + music_session_member2 = FactoryGirl.create(:connection, :user => user2, :music_session => music_session, :client_id => "5") - # create client 1, log him in, and log him in to music session - client1 = login(@router, user1, "foobar", "1") - login_music_session(@router, client1, music_session) - done - #end + # 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") + done end it "should allow two valid subscribers to communicate with session-directed messages", :mq => true do @@ -248,10 +284,8 @@ describe Router do # create client 1, log him in, and log him in to music session client1 = login(@router, user1, "foobar", "1") - login_music_session(@router, client1, music_session) client2 = login(@router, user2, "foobar", "2") - login_music_session(@router, client2, music_session) # make a music_session and define two members diff --git a/websocket-gateway/spec/jam_websockets/trust_check_spec.rb b/websocket-gateway/spec/jam_websockets/trust_check_spec.rb new file mode 100644 index 000000000..3f6026136 --- /dev/null +++ b/websocket-gateway/spec/jam_websockets/trust_check_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe TrustCheck do + + it "defaults to all" do + trust_check = TrustCheck.new + + trust_check.trusted?('127.0.0.1', 6767).should be_true + trust_check.trusted?('127.0.0.1', 6768).should be_true + trust_check.trusted?('192.0.0.1', 6768).should be_true + trust_check.trusted?('10.0.0.1', 6768).should be_true + trust_check.trusted?('11.0.0.1', 6768).should be_true + end + + it "verifies port when specified" do + trust_check = TrustCheck.new(6768) + + trust_check.trusted?('127.0.0.1', 6767).should be_false + trust_check.trusted?('127.0.0.1', 6768).should be_true + trust_check.trusted?('192.0.0.1', 6768).should be_true + trust_check.trusted?('10.0.0.1', 6768).should be_true + trust_check.trusted?('11.0.0.1', 6768).should be_true + end + + it "verifies cidr when specified" do + trust_check = TrustCheck.new(6768, '127.0.0.1/8') + + trust_check.trusted?('127.0.0.1', 6767).should be_false + trust_check.trusted?('127.0.0.1', 6768).should be_true + trust_check.trusted?('192.0.0.1', 6768).should be_false + trust_check.trusted?('10.0.0.1', 6768).should be_false + trust_check.trusted?('11.0.0.1', 6768).should be_false + end + + it "verifies mutltiple cidrs" do + trust_check = TrustCheck.new(6768, ['192.168.1.2', '192.168.1.3']) + + trust_check.trusted?('192.168.1.1', 6767).should be_false + trust_check.trusted?('192.168.1.2', 6767).should be_false + trust_check.trusted?('192.168.1.1', 6768).should be_false + trust_check.trusted?('192.168.1.2', 6768).should be_true + trust_check.trusted?('192.168.1.3', 6768).should be_true + trust_check.trusted?('192.168.1.4', 6768).should be_false + end +end \ No newline at end of file From fcc7130536a64c46f7c4be06a9337d8255528f8e Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 22 May 2014 11:26:56 -0500 Subject: [PATCH 09/29] * wip --- ruby/lib/jam_ruby/models/latency_tester.rb | 4 + web/app/assets/javascripts/JamServer.js | 1 - web/app/assets/javascripts/fakeJamClient.js | 2 + .../assets/javascripts/gear/gear_wizard.js | 86 +------------ .../javascripts/gear/loopback_wizard.js | 15 +++ .../javascripts/gear/step_configure_tracks.js | 1 + .../gear/step_configure_voice_chat.js | 1 + .../gear/step_direct_monitoring.js | 72 +++++++++++ .../javascripts/gear/step_network_test.js | 80 +++++++++++- .../javascripts/gear/step_select_gear.js | 1 + web/app/assets/javascripts/gear/wizard.js | 121 ++++++++++++++++++ web/app/assets/javascripts/jam_rest.js | 11 ++ web/app/assets/javascripts/session.js | 4 + web/app/assets/javascripts/utils.js | 4 + .../stylesheets/client/gearWizard.css.scss | 86 ++++++++++++- .../api_latency_testers_controller.rb | 2 +- .../views/clients/gear/_gear_wizard.html.haml | 73 +++++++++-- web/app/views/clients/index.html.erb | 3 +- .../api_latency_tests_controller_spec.rb | 7 + 19 files changed, 474 insertions(+), 100 deletions(-) create mode 100644 web/app/assets/javascripts/gear/loopback_wizard.js create mode 100644 web/app/assets/javascripts/gear/wizard.js create mode 100644 web/spec/controllers/api_latency_tests_controller_spec.rb diff --git a/ruby/lib/jam_ruby/models/latency_tester.rb b/ruby/lib/jam_ruby/models/latency_tester.rb index 2962f3bc2..bebcc9cdf 100644 --- a/ruby/lib/jam_ruby/models/latency_tester.rb +++ b/ruby/lib/jam_ruby/models/latency_tester.rb @@ -4,6 +4,10 @@ module JamRuby belongs_to :connection, class_name: 'JamRuby::Connection', foreign_key: :client_id, primary_key: :client_id + def self.select_latency_tester + LatencyTester.joins(:connection).first + end + # we need to find that latency_tester with the specified connection (and reconnect it) # or bootstrap a new latency_tester def self.connect(options) diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 748490320..c61b8b6fe 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -20,7 +20,6 @@ var channelId = null; var clientType = null; var mode = null; - var rest = context.JK.Rest(); // heartbeat var heartbeatInterval = null; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 4752d26aa..e07dcb406 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -444,6 +444,7 @@ function SessionStartRecording() {} function SessionStopPlay() {} function SessionStopRecording() {} + function SessionRemoveAllPlayTracks(){} function isSessionTrackPlaying() { return false; } function SessionCurrrentPlayPosMs() { return 0; } function SessionGetTracksPlayDurationMs() { return 0; } @@ -825,6 +826,7 @@ this.SessionStartPlay = SessionStartPlay; this.SessionStartRecording = SessionStartRecording; this.SessionStopPlay = SessionStopPlay; + this.SessionRemoveAllPlayTracks = SessionRemoveAllPlayTracks; this.SessionStopRecording = SessionStopRecording; this.isSessionTrackPlaying = isSessionTrackPlaying; this.SessionCurrrentPlayPosMs = SessionCurrrentPlayPosMs; diff --git a/web/app/assets/javascripts/gear/gear_wizard.js b/web/app/assets/javascripts/gear/gear_wizard.js index 09d1714ab..26ec35fe4 100644 --- a/web/app/assets/javascripts/gear/gear_wizard.js +++ b/web/app/assets/javascripts/gear/gear_wizard.js @@ -2,11 +2,11 @@ "use strict"; - context.JK = context.JK || {}; context.JK.GearWizard = function (app) { var $dialog = null; + var wizard = null; var $wizardSteps = null; var $currentWizardStep = null; var step = null; @@ -57,83 +57,6 @@ }); } - function beforeHideStep() { - if(!previousStep) {return} - - var stepInfo = STEPS[previousStep]; - - if (!stepInfo) { - throw "unknown step: " + previousStep; - } - - if(stepInfo.beforeHide) { - stepInfo.beforeHide.call(stepInfo); - } - } - - function beforeShowStep($step) { - var stepInfo = STEPS[step]; - - if (!stepInfo) { - throw "unknown step: " + step; - } - - stepInfo.beforeShow.call(stepInfo); - } - - function moveToStep() { - var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); - - beforeHideStep(); - - $wizardSteps.hide(); - - $currentWizardStep = $nextWizardStep; - - var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); - var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); - $activeStep.addClass('.active'); - $activeStep.next().show(); // show the .ftue-step-title - $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); - - // update buttons - var $ftueButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); - - - $btnBack = $ftueButtonsContent.find('.btn-back'); - $btnNext = $ftueButtonsContent.find('.btn-next'); - $btnClose = $ftueButtonsContent.find('.btn-close'); - $btnCancel = $ftueButtonsContent.find('.btn-cancel'); - - // hide back button if 1st step or last step - if (step == 0 && step == TOTAL_STEPS - 1) { - $btnBack.hide(); - } - // hide next button if not on last step - if (step == TOTAL_STEPS - 1) { - $btnNext.hide(); - } - // hide close if on last step - if (step != TOTAL_STEPS - 1) { - $btnClose.hide(); - } - // hide cancel if not on last step - if (step == TOTAL_STEPS - 1) { - $btnCancel.hide(); - } - - $btnNext.on('click', next); - $btnBack.on('click', back); - $btnClose.on('click', closeDialog); - $btnCancel.on('click', closeDialog); - - $ftueButtons.empty(); - $ftueButtons.append($ftueButtonsContent); - - beforeShowStep($currentWizardStep); - $currentWizardStep.show(); - - } function reset() { $currentWizardStep = null; @@ -168,10 +91,7 @@ context.jamClient.FTUESetStatus(false); findOrCreateFTUEProfile(); - step = args.d1; - if (!step) step = 0; - step = parseInt(step); - moveToStep(null); + wizard.beforeShow(args); } function afterShow() { @@ -266,7 +186,7 @@ $dialog = $('#gear-wizard-dialog'); $wizardSteps = $dialog.find('.wizard-step'); $templateSteps = $('#template-ftuesteps'); - $templateButtons = $('#template-ftue-buttons'); + $templateButtons = $('#template-wizard-buttons'); $ftueButtons = $dialog.find('.ftue-buttons'); stepUnderstandGear.initialize($wizardSteps.filter($('[layout-wizard-step=0]'))); diff --git a/web/app/assets/javascripts/gear/loopback_wizard.js b/web/app/assets/javascripts/gear/loopback_wizard.js new file mode 100644 index 000000000..4b3f48784 --- /dev/null +++ b/web/app/assets/javascripts/gear/loopback_wizard.js @@ -0,0 +1,15 @@ +(function (context, $) { + + "use strict"; + + + context.JK = context.JK || {}; + context.JK.LoopbackWizard = function (app) { + + var $dialog = null; + var $wizardSteps = null; + + + + } +})(window, jQuery); diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js index 655f98596..a6ef48b70 100644 --- a/web/app/assets/javascripts/gear/step_configure_tracks.js +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -9,6 +9,7 @@ var VOICE_CHAT = context.JK.VOICE_CHAT; var MAX_TRACKS = context.JK.MAX_TRACKS; + var logger = context.JK.logger; var $step = null; var $templateAssignablePort = null; diff --git a/web/app/assets/javascripts/gear/step_configure_voice_chat.js b/web/app/assets/javascripts/gear/step_configure_voice_chat.js index a967a0564..e50bb5850 100644 --- a/web/app/assets/javascripts/gear/step_configure_voice_chat.js +++ b/web/app/assets/javascripts/gear/step_configure_voice_chat.js @@ -7,6 +7,7 @@ var ASSIGNMENT = context.JK.ASSIGNMENT; var VOICE_CHAT = context.JK.VOICE_CHAT; + var logger = context.JK.logger; var $step = null; var $reuseAudioInputRadio = null; diff --git a/web/app/assets/javascripts/gear/step_direct_monitoring.js b/web/app/assets/javascripts/gear/step_direct_monitoring.js index 80d8a83cb..c1cb4f514 100644 --- a/web/app/assets/javascripts/gear/step_direct_monitoring.js +++ b/web/app/assets/javascripts/gear/step_direct_monitoring.js @@ -6,16 +6,88 @@ context.JK.StepDirectMonitoring = function (app) { var $step = null; + var $directMonitoringBtn = null; + var isPlaying = false; + var playCheckInterval = null; + var trackDurationMs = null; + + function checkIfPlaying() { + var currentPositionMs = context.jamClient.SessionCurrrentPlayPosMs(); + var atEnd = currentPositionMs == 0 || trackDurationMs == currentPositionMs; + + if(atEnd) { + context.jamClient.SessionStopPlay(); + startPlay(); + } + } + + function startPlay() { + context.jamClient.SessionTrackSeekMs(0); + context.jamClient.SessionStartPlay(1); + $directMonitoringBtn.removeClass('playing paused').addClass('playing'); + + trackDurationMs = context.jamClient.SessionGetTracksPlayDurationMs(); + if(!playCheckInterval) { + playCheckInterval = setInterval(checkIfPlaying, 333); + } + isPlaying = true; + } + + function stopPlay() { + context.jamClient.SessionStopPlay(); + $directMonitoringBtn.removeClass('playing paused').addClass('paused'); + isPlaying = false; + } + + function togglePlay() { + if(isPlaying) { + stopPlay(); + } + else { + startPlay(); + } + } + + function handleNext() { + + } + + function newSession() { + + } function beforeShow() { + context.jamClient.SessionRemoveAllPlayTracks(); + if(!context.jamClient.SessionAddPlayTrack("skin:jktest-audio.wav")) { + context.JK.alertSupportedNeeded('Unable to open test sound'); + } + } + function beforeHide() { + if(isPlaying) { + stopPlay(); + } + + context.jamClient.SessionRemoveAllPlayTracks(); + + if(playCheckInterval) { + clearTimeout(playCheckInterval); + playCheckInterval = null; + } } function initialize(_$step) { $step = _$step; + + $directMonitoringBtn = $step.find('.test-direct-monitoring'); + + $directMonitoringBtn.on('click', togglePlay); } + this.handleNext = handleNext; + this.newSession = newSession; this.beforeShow = beforeShow; + this.beforeHide = beforeHide; this.initialize = initialize; return this; diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js index 91d05ba34..715e8b4e5 100644 --- a/web/app/assets/javascripts/gear/step_network_test.js +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -3,21 +3,97 @@ "use strict"; context.JK = context.JK || {}; - context.JK.StepNetworkTest = function (app) { + context.JK.StepNetworkTest = function (app, $dialog) { + var rest = context.JK.Rest(); + var logger = context.JK.logger; var $step = null; + var TEST_SUCCESS_CALLBACK = 'JK.HandleNetworkTestSuccess'; + var TEST_TIMEOUT_CALLBACK = 'JK.HandleNetworkTestTimeout'; + + var $startNetworkTestBtn = null; + var $testResults = null; + var $testScore = null; + var $testText = null; + var testedSuccessfully = false; + var serverClientId = null; + var isScoring = false; + + + function startNetworkTest() { + isScoring = true; + rest.getLatencyTester() + .done(function(response) { + serverClientId = response.client_id; + + logger.info("beginning network test against client_id: " + clientId); + + context.jamClient.TestNetworkPktBwRate(serverClientId, TEST_SUCCESS_CALLBACK, TEST_TIMEOUT_CALLBACK); + }) + .fail(function() { + isScoring = false; + logger.error("arguments:", arguments); + + if(context.JK.isNetworkError(arguments)) { + context.JK.Banner.showAlert("Please try again latery. Your network appears down."); + } + else { + logger.error("unable to get latency tester from server"); + } + }) + logger.info("starting network test"); + return false; + } + + function networkTestSuccess() { + console.log("success arguments: ", arguments); + context.jamClient.StopNetworkTest(serverClientId); + } + + function networkTestTimeout() { + console.log("timeout arguments:", arguments); + context.jamClient.StopNetworkTest(serverClientId); + } + + function hasScoredNetworkSuccessfully() { + return testedSuccessfully; + } + + function initializeNextButtonState() { + $dialog.setNextState(hasScoredNetworkSuccessfully()); + } + + function initializeBackButtonState() { + $dialog.setNextState(); + } + + function newSession() { + isScoring = false; + // XXX context.jamClient.stopNetworkTest(); + } function beforeShow() { - + initializeNextButtonState(); } function initialize(_$step) { $step = _$step; + + $startNetworkTestBtn = $step.find('.start-network-test'); + $testResults = $step.find('.network-test-results'); + $testScore = $step.find('.network-test-score'); + $testText = $step.find('.network-test-text'); + + $startNetworkTestBtn.on('click', startNetworkTest); } + this.newSession = newSession; this.beforeShow = beforeShow; this.initialize = initialize; + context.JK.HandleNetworkTestSuccess = networkTestSuccess; // pin to global for bridge callback + context.JK.HandleNetworkTestTimeout = networkTestTimeout; // pin to global for bridge callback + return this; } })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js index 1f8049e41..5f520db66 100644 --- a/web/app/assets/javascripts/gear/step_select_gear.js +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -9,6 +9,7 @@ var VOICE_CHAT = context.JK.VOICE_CHAT; var self = null; var $step = null; + var logger = context.JK.logger; var rest = context.JK.Rest(); var $watchVideoInput = null; var $watchVideoOutput = null; diff --git a/web/app/assets/javascripts/gear/wizard.js b/web/app/assets/javascripts/gear/wizard.js new file mode 100644 index 000000000..01cd910db --- /dev/null +++ b/web/app/assets/javascripts/gear/wizard.js @@ -0,0 +1,121 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.Wizard = function (app) { + + var $dialog = null; + var $templateButtons = null; + var $wizardSteps = null; + var STEPS = null; + var step = null; + var $btnNext = null; + var $btnBack = null; + var $btnClose = null; + var $btnCancel = null; + var $currentWizardStep = null; + + + function beforeHideStep() { + if(!previousStep) {return} + + var stepInfo = STEPS[previousStep]; + + if (!stepInfo) { + throw "unknown step: " + previousStep; + } + + if(stepInfo.beforeHide) { + stepInfo.beforeHide.call(stepInfo); + } + } + + function beforeShowStep($step) { + var stepInfo = STEPS[step]; + + if (!stepInfo) { + throw "unknown step: " + step; + } + + stepInfo.beforeShow.call(stepInfo); + } + + function moveToStep() { + var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); + + beforeHideStep(); + + $wizardSteps.hide(); + + $currentWizardStep = $nextWizardStep; + + var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); + var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); + $activeStep.addClass('.active'); + $activeStep.next().show(); // show the .ftue-step-title + $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); + + // update buttons + var $wizardButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); + + + $btnBack = $wizardButtonsContent.find('.btn-back'); + $btnNext = $wizardButtonsContent.find('.btn-next'); + $btnClose = $wizardButtonsContent.find('.btn-close'); + $btnCancel = $wizardButtonsContent.find('.btn-cancel'); + + // hide back button if 1st step or last step + if (step == 0 && step == TOTAL_STEPS - 1) { + $btnBack.hide(); + } + // hide next button if not on last step + if (step == TOTAL_STEPS - 1) { + $btnNext.hide(); + } + // hide close if on last step + if (step != TOTAL_STEPS - 1) { + $btnClose.hide(); + } + // hide cancel if not on last step + if (step == TOTAL_STEPS - 1) { + $btnCancel.hide(); + } + + $btnNext.on('click', next); + $btnBack.on('click', back); + $btnClose.on('click', closeDialog); + $btnCancel.on('click', closeDialog); + + $ftueButtons.empty(); + $ftueButtons.append($wizardButtonsContent); + + beforeShowStep($currentWizardStep); + $currentWizardStep.show(); + + } + + + function beforeShow(args) { + + step = args.d1; + if (!step) step = 0; + step = parseInt(step); + moveToStep(null); + } + + function initialize(_$dialog, _$wizardSteps, _STEPS) { + + $dialog = _$dialog; + $wizardSteps = _$wizardSteps; + STEPS = _STEPS; + + + $templateButtons = $('#template-wizard-buttons'); + } + + this.initialize = initialize; + + } + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 8a092562f..859fc82b1 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1005,6 +1005,16 @@ }); } + function getLatencyTester(options) { + return $.ajax({ + type: "GET", + url: '/api/latency_testers', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }); + } + function initialize() { return self; } @@ -1092,6 +1102,7 @@ this.createChatMessage = createChatMessage; this.getChatMessages = getChatMessages; this.createDiagnostic = createDiagnostic; + this.getLatencyTester = getLatencyTester; return this; }; diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 2fc9e00b1..20fae6390 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -1544,6 +1544,10 @@ 'beforeDisconnect' : beforeDisconnect, }; app.bindScreen('session', screenBindings); + + // make sure no previous plays are still going on by accident + context.jamClient.SessionStopPlay(); + context.jamClient.SessionRemoveAllPlayTracks(); }; diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index c4fc28ad3..57b0a5c74 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -686,6 +686,10 @@ } } + context.JK.isNetworkError = function(failArgs) { + return false; + } + context.JK.clientType = function () { if (context.jamClient) { return context.jamClient.IsNativeClient() ? 'client' : 'browser'; diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index 9574ee3de..1e4404b66 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -90,7 +90,7 @@ } .wizard-step-column:last-child { - padding-right:0; + //padding-right:0; } p:nth-of-type(1) { @@ -581,8 +581,21 @@ .test-direct-monitoring { margin-top:40px; display:inline-block; + text-decoration: none; + width:90px; - span { + &.paused { + .playing { + display:none; + } + } + &.playing { + .paused { + display:none; + } + } + + .direct-monitoring-btn { padding-left:5px; font-size:16px; } @@ -595,10 +608,77 @@ .wizard-step[layout-wizard-step="5"] { + .wizard-step-content .wizard-step-column { + &:nth-of-type(1) { + width:25%; + } + &:nth-of-type(2) { + width:50%; + } + &:nth-of-type(3) { + width:25%; + } + } + + .summary { + margin-left:20px; + margin-top:22px; + } + + a.start-network-test { + margin-top:20px; + } + + .network-test-score { + height:24px; + padding:10px; + color:white; + font-size:20px; + background-color:#222; + + &.good { + background-color: #72a43b; + } + &.acceptable { + background-color: #D6A800; + } + &.bad { + background-color: #7B0C00; + } + } + + .network-test-text { + + } + + .network-test-results { + height: 248px ! important; + @include border_box_sizing; + &.testing { + .network-test-score { + font-size:16px; + } + + .network-test-text { + background-image: url('/assets/shared/spinner.gif'); + background-repeat:no-repeat; + background-position:center; + //width:128px; + height:128px; + } + } + } } .wizard-step[layout-wizard-step="6"] { - + .wizard-step-content .wizard-step-column { + &:nth-of-type(1) { + width:50%; + } + &:nth-of-type(2) { + width:50%; + } + } } p { diff --git a/web/app/controllers/api_latency_testers_controller.rb b/web/app/controllers/api_latency_testers_controller.rb index e9f98f68c..19b4293ac 100644 --- a/web/app/controllers/api_latency_testers_controller.rb +++ b/web/app/controllers/api_latency_testers_controller.rb @@ -7,7 +7,7 @@ class ApiLatencyTestersController < ApiController def match # some day we can find the best latency tester to test against, now there is only one. - @latency_tester = LatencyTester.first + @latency_tester = LatencyTester.select_latency_tester respond_with_model(@latency_tester) end diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index ca8afbd2b..63ccd5bfa 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -141,13 +141,13 @@ %li Select the instrument for each track. .center %a.button-orange.watch-video{href:'#'} WATCH VIDEO - .wizard-step-column + .wizard-step-column.no-selection-range %h2 Unassigned Ports .unassigned-channels - .wizard-step-column + .wizard-step-column.no-selection-range %h2 Track Input Port(s) .tracks - .wizard-step-column + .wizard-step-column.no-selection-range %h2 Instrument .instruments @@ -200,19 +200,74 @@ If you use your audio interface for recording, and use the direct monitoring feature for recording, please note that you will need to remember to turn this feature off every time that you use the JamKazam service. .center - %a.ftue-box.test-direct-monitoring - %img{src:'assets/content/icon_playbutton.png', width:20, height:20, align:'top'} - %span Play + %a.ftue-box.test-direct-monitoring.paused.no-selection-range + .playing + %img{src:'assets/content/icon_pausebutton.png', width:20, height:20, align:'top'} + %span.direct-monitoring-btn Pause + .paused + %img{src:'assets/content/icon_playbutton.png', width:20, height:20, align:'top'} + %span.direct-monitoring-btn Play .wizard-step{ 'layout-wizard-step' => "5", 'dialog-title' => "Test Router & Network", 'dialog-purpose' => "TestRouterNetwork" } .ftuesteps + .clearall + .help-text In this step, you will test your router and Internet connection to ensure that you can play in online sessions, and to see how many musicians can be in a session with you based on your internet connection. + .wizard-step-content + .wizard-step-column + %h2 Instructions + .ftue-box.instructions + Find the Direct Monitoring control on your audio interface.
    + %ul + %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 + .wizard-step-column + .summary + %p Ensure that your computer is connected to your home router using an Ethernet cable rather than using Wi-Fi wireless access. If necessary, find or purchase a long Ethernet cable, up to 100 ft. + %p Then click on the Start Network Test button below. + .center + %a.button-orange.start-network-test{href:'#'} START NETWORK TEST + .wizard-step-column + %h2 Test Results + .network-test-results.ftue-box + .network-test-score + .network-test-text .wizard-step{ 'layout-wizard-step' => "6", 'dialog-title' => "Success!", 'dialog-purpose' => "Success" } .ftuesteps - - + .clearall + .wizard-step-content + .wizard-step-column + %p Congratulations! You are now ready to create, join, and play in online sessions with other JamKazam musicians! + %p Please feel free to watch any of the videos to the right for information on how to connect and play with other musicians, and how to get the most out of JamKazam. When you're ready to go, click the Close button. + %p Have fun and thanks for joining us! + %p — Team JamKazam + .wizard-step-column + %h2 + Tutorial Videos + %ul + %li + %a Creating a Session + %li + %a Finding a Session + %li + %a Playing in a Session + %li + %a Connecting with Other Musicians + %li + %a Making and Sharing Recordings + %li + %a Broadcasting Your Sessions + %h2 + Other Valuable Resource Links + %ul + %li + %a JamKazam Support Center + %li + %a JamKazam Community Forum .ftue-buttons %script{type: 'text/template', id: 'template-ftuesteps'} @@ -233,7 +288,7 @@ .ftue-step-title Success! -%script{type: 'text/template', id: 'template-ftue-buttons'} +%script{type: 'text/template', id: 'template-wizard-buttons'} .ftue-buttons-holder %a.button-grey.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL %a.button-grey{href: '#'} HELP diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 7dd8a344f..513d49d75 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -260,7 +260,8 @@ // latency_tester does not want to be here if(window.jamClient.getOperatingMode() == "server") { - window.location.href = "/latency_tester"; + window.location = "/latency_tester"; + return; } // Let's get things rolling... diff --git a/web/spec/controllers/api_latency_tests_controller_spec.rb b/web/spec/controllers/api_latency_tests_controller_spec.rb new file mode 100644 index 000000000..8ef24e6c2 --- /dev/null +++ b/web/spec/controllers/api_latency_tests_controller_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe ApiFeedsController do + render_views + + +end From a2bf56beac68d32b93a4a11d56464a383149435d Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 22 May 2014 22:05:05 -0500 Subject: [PATCH 10/29] * wip --- web/app/assets/javascripts/fakeJamClient.js | 2 + .../assets/javascripts/gear/gear_wizard.js | 92 ++++---------- web/app/assets/javascripts/gear/wizard.js | 119 +++++++++++++++--- .../views/clients/gear/_gear_wizard.html.haml | 2 +- 4 files changed, 125 insertions(+), 90 deletions(-) diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index e07dcb406..2691c7d4c 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -444,6 +444,7 @@ function SessionStartRecording() {} function SessionStopPlay() {} function SessionStopRecording() {} + function SessionAddPlayTrack() {} function SessionRemoveAllPlayTracks(){} function isSessionTrackPlaying() { return false; } function SessionCurrrentPlayPosMs() { return 0; } @@ -826,6 +827,7 @@ this.SessionStartPlay = SessionStartPlay; this.SessionStartRecording = SessionStartRecording; this.SessionStopPlay = SessionStopPlay; + this.SessionAddPlayTrack = SessionAddPlayTrack; this.SessionRemoveAllPlayTracks = SessionRemoveAllPlayTracks; this.SessionStopRecording = SessionStopRecording; this.isSessionTrackPlaying = isSessionTrackPlaying; diff --git a/web/app/assets/javascripts/gear/gear_wizard.js b/web/app/assets/javascripts/gear/gear_wizard.js index 26ec35fe4..b75f239a9 100644 --- a/web/app/assets/javascripts/gear/gear_wizard.js +++ b/web/app/assets/javascripts/gear/gear_wizard.js @@ -5,24 +5,15 @@ context.JK = context.JK || {}; context.JK.GearWizard = function (app) { + var logger = context.JK.logger; + var $dialog = null; var wizard = null; var $wizardSteps = null; - var $currentWizardStep = null; - var step = null; - var previousStep = null; var $templateSteps = null; - var $templateButtons = null; - var $templateAudioPort = null; - var $ftueButtons = null; - var $btnNext = null; - var $btnBack = null; - var $btnClose = null; - var $btnCancel = null; var self = this; - var TOTAL_STEPS = 7; var STEP_INTRO = 0; var STEP_SELECT_DEVICE = 1; var STEP_SELECT_TRACKS = 2; @@ -57,9 +48,16 @@ }); } + function onStepChanged(e, data) { + var step = wizard.getCurrentStep(); + var $currentWizardStep = wizard.getCurrentWizardStep(); - function reset() { - $currentWizardStep = null; + // update ftue step numbers + var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); + var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); + $activeStep.addClass('.active'); + $activeStep.next().show(); // show the .ftue-step-title + $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); } // checks if we already have a profile called 'FTUE...'; if not, create one. if so, re-use it. @@ -85,13 +83,12 @@ } function beforeShow(args) { - previousStep = null; context.jamClient.FTUECancel(); context.jamClient.FTUESetStatus(false); findOrCreateFTUEProfile(); - wizard.beforeShow(args); + wizard.onBeforeShow(args); } function afterShow() { @@ -99,75 +96,31 @@ } function afterHide() { - step = null; + wizard.onAfterHide(); + context.jamClient.FTUESetStatus(true); context.jamClient.FTUECancel(); } - function back() { - if ($(this).is('.button-grey')) return false; - previousStep = step; - step = step - 1; - moveToStep(); - return false; - } - - function next() { - if ($(this).is('.button-grey')) return false; - - var stepInfo = STEPS[step]; - if(stepInfo.handleNext) { - var result = stepInfo.handleNext.call(stepInfo); - if(!result) {return false;} - } - - previousStep = step; - step = step + 1; - - moveToStep(); - return false; - } function closeDialog() { - beforeHideStep($currentWizardStep); + wizard.onCloseDialog(); app.layout.closeDialog('gear-wizard'); return false; } function events() { - - } - - function route() { - + $(wizard).on('step_changed', onStepChanged); + $(wizard).on('wizard_cancel', closeDialog); + $(wizard).on('wizard_close', closeDialog); } function setNextState(enabled) { - - if(!$btnNext) return; - - $btnNext.removeClass('button-orange button-grey'); - - if (enabled) { - $btnNext.addClass('button-orange'); - } - else { - $btnNext.addClass('button-grey'); - } + wizard.setNextState(enabled); } function setBackState(enabled) { - - if(!$btnBack) return; - - $btnBack.removeClass('button-orange button-grey'); - - if (enabled) { - $btnBack.addClass('button-orange'); - } - else { - $btnBack.addClass('button-grey'); - } + wizard.setBackState(enabled); } @@ -186,8 +139,6 @@ $dialog = $('#gear-wizard-dialog'); $wizardSteps = $dialog.find('.wizard-step'); $templateSteps = $('#template-ftuesteps'); - $templateButtons = $('#template-wizard-buttons'); - $ftueButtons = $dialog.find('.ftue-buttons'); stepUnderstandGear.initialize($wizardSteps.filter($('[layout-wizard-step=0]'))); stepSelectGear.initialize($wizardSteps.filter($('[layout-wizard-step=1]'))); @@ -197,6 +148,9 @@ stepNetworkTest.initialize($wizardSteps.filter($('[layout-wizard-step=5]'))); stepSuccess.initialize($wizardSteps.filter($('[layout-wizard-step=6]'))); + wizard = new context.JK.Wizard(app); + wizard.initialize($dialog, $wizardSteps, STEPS); + events(); } diff --git a/web/app/assets/javascripts/gear/wizard.js b/web/app/assets/javascripts/gear/wizard.js index 01cd910db..9f608a7f4 100644 --- a/web/app/assets/javascripts/gear/wizard.js +++ b/web/app/assets/javascripts/gear/wizard.js @@ -5,17 +5,24 @@ context.JK = context.JK || {}; context.JK.Wizard = function (app) { + var STEPS = null; + var step = null; + var previousStep = null; var $dialog = null; var $templateButtons = null; var $wizardSteps = null; - var STEPS = null; - var step = null; + var $currentWizardStep = null; + var $wizardButtons = null; var $btnNext = null; var $btnBack = null; var $btnClose = null; var $btnCancel = null; - var $currentWizardStep = null; + var self = this; + var $self = $(this); + function totalSteps() { + return STEPS.length; + } function beforeHideStep() { if(!previousStep) {return} @@ -41,6 +48,30 @@ stepInfo.beforeShow.call(stepInfo); } + function back() { + if ($(this).is('.button-grey')) return false; + previousStep = step; + step = step - 1; + moveToStep(); + return false; + } + + function next() { + if ($(this).is('.button-grey')) return false; + + var stepInfo = STEPS[step]; + if(stepInfo.handleNext) { + var result = stepInfo.handleNext.call(stepInfo); + if(!result) {return false;} + } + + previousStep = step; + step = step + 1; + + moveToStep(); + return false; + } + function moveToStep() { var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); @@ -50,58 +81,99 @@ $currentWizardStep = $nextWizardStep; - var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); - var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); - $activeStep.addClass('.active'); - $activeStep.next().show(); // show the .ftue-step-title - $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); + $self.triggerHandler('step_changed', {step:step}); // update buttons var $wizardButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); - $btnBack = $wizardButtonsContent.find('.btn-back'); $btnNext = $wizardButtonsContent.find('.btn-next'); $btnClose = $wizardButtonsContent.find('.btn-close'); $btnCancel = $wizardButtonsContent.find('.btn-cancel'); // hide back button if 1st step or last step - if (step == 0 && step == TOTAL_STEPS - 1) { + if (step == 0 && step == totalSteps() - 1) { $btnBack.hide(); } // hide next button if not on last step - if (step == TOTAL_STEPS - 1) { + if (step == totalSteps() - 1) { $btnNext.hide(); } // hide close if on last step - if (step != TOTAL_STEPS - 1) { + if (step != totalSteps() - 1) { $btnClose.hide(); } // hide cancel if not on last step - if (step == TOTAL_STEPS - 1) { + if (step == totalSteps() - 1) { $btnCancel.hide(); } $btnNext.on('click', next); $btnBack.on('click', back); - $btnClose.on('click', closeDialog); - $btnCancel.on('click', closeDialog); + $btnClose.on('click', function() {$self.triggerHandler('wizard_close'); return false;}); + $btnCancel.on('click', function() {$self.triggerHandler('wizard_cancel'); return false;}); - $ftueButtons.empty(); - $ftueButtons.append($wizardButtonsContent); + $wizardButtons.empty(); + $wizardButtons.append($wizardButtonsContent); beforeShowStep($currentWizardStep); $currentWizardStep.show(); } + // called by owner whenever + function onCloseDialog() { + beforeHideStep($currentWizardStep); + } - function beforeShow(args) { + function onBeforeShow(args) { + + previousStep = null; step = args.d1; if (!step) step = 0; step = parseInt(step); - moveToStep(null); + moveToStep(); + } + + function onAfterHide() { + step = null; + } + + function setNextState(enabled) { + + if(!$btnNext) return; + + $btnNext.removeClass('button-orange button-grey'); + + if (enabled) { + $btnNext.addClass('button-orange'); + } + else { + $btnNext.addClass('button-grey'); + } + } + + function setBackState(enabled) { + + if(!$btnBack) return; + + $btnBack.removeClass('button-orange button-grey'); + + if (enabled) { + $btnBack.addClass('button-orange'); + } + else { + $btnBack.addClass('button-grey'); + } + } + + function getCurrentStep() { + return step; + } + + function getCurrentWizardStep() { + return $currentWizardStep; } function initialize(_$dialog, _$wizardSteps, _STEPS) { @@ -110,10 +182,17 @@ $wizardSteps = _$wizardSteps; STEPS = _STEPS; - + $wizardButtons = $dialog.find('.wizard-buttons'); $templateButtons = $('#template-wizard-buttons'); } + this.setNextState = setNextState; + this.setBackState = setBackState; + this.getCurrentStep = getCurrentStep; + this.getCurrentWizardStep = getCurrentWizardStep; + this.onCloseDialog = onCloseDialog; + this.onBeforeShow = onBeforeShow; + this.onAfterHide = onAfterHide; this.initialize = initialize; } diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 63ccd5bfa..7d5e5195a 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -268,7 +268,7 @@ %a JamKazam Support Center %li %a JamKazam Community Forum - .ftue-buttons + .wizard-buttons %script{type: 'text/template', id: 'template-ftuesteps'} .ftuesteps-inner From dff3fa48706144c43441ce76f779ae2255ebd7f4 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Tue, 27 May 2014 10:23:16 -0500 Subject: [PATCH 11/29] *wip --- ruby/lib/jam_ruby/models/diagnostic.rb | 5 +- ruby/lib/jam_ruby/models/latency_tester.rb | 9 +- .../assets/javascripts/createSession.js.erb | 1 + web/app/assets/javascripts/fakeJamClient.js | 45 ++- web/app/assets/javascripts/ga.js | 22 ++ .../assets/javascripts/gear/gear_wizard.js | 25 +- .../javascripts/gear/step_configure_tracks.js | 8 +- .../javascripts/gear/step_network_test.js | 352 ++++++++++++++++-- .../javascripts/gear/step_select_gear.js | 8 + web/app/assets/javascripts/gear/wizard.js | 21 +- web/app/assets/javascripts/layout.js | 6 +- web/app/assets/javascripts/trackHelpers.js | 8 +- .../stylesheets/client/gearWizard.css.scss | 135 ++++++- web/app/controllers/clients_controller.rb | 3 + web/app/views/clients/gear/_buttons.html.haml | 6 +- .../views/clients/gear/_gear_wizard.html.haml | 59 +-- web/config/application.rb | 6 + .../api_latency_tests_controller_spec.rb | 15 +- .../lib/jam_websockets/router.rb | 6 +- 19 files changed, 646 insertions(+), 94 deletions(-) diff --git a/ruby/lib/jam_ruby/models/diagnostic.rb b/ruby/lib/jam_ruby/models/diagnostic.rb index e7a289b42..8385fff40 100644 --- a/ruby/lib/jam_ruby/models/diagnostic.rb +++ b/ruby/lib/jam_ruby/models/diagnostic.rb @@ -29,9 +29,12 @@ module JamRuby # websocket gateway got a client with the same client_id as an already-connected client DUPLICATE_CLIENT = 'DUPLICATE_CLIENT' + # info about how the test went + NETWORK_TEST_RESULT = 'NETWORK_TEST_RESULT' + DIAGNOSTIC_TYPES = [NO_HEARTBEAT_ACK, WEBSOCKET_CLOSED_REMOTELY, EXPIRED_STALE_CONNECTION, MISSING_CLIENT_STATE, UNKNOWN_MESSAGE_TYPE, MISSING_ROUTE_TO, - DUPLICATE_CLIENT, WEBSOCKET_CLOSED_LOCALLY] + DUPLICATE_CLIENT, WEBSOCKET_CLOSED_LOCALLY, NETWORK_TEST_RESULT] # creator types # CLIENT = 'client' diff --git a/ruby/lib/jam_ruby/models/latency_tester.rb b/ruby/lib/jam_ruby/models/latency_tester.rb index bebcc9cdf..2bc1071ae 100644 --- a/ruby/lib/jam_ruby/models/latency_tester.rb +++ b/ruby/lib/jam_ruby/models/latency_tester.rb @@ -3,9 +3,16 @@ module JamRuby belongs_to :connection, class_name: 'JamRuby::Connection', foreign_key: :client_id, primary_key: :client_id + def heartbeat_interval_client + nil + end + + def connection_expire_time_client + nil + end def self.select_latency_tester - LatencyTester.joins(:connection).first + LatencyTester.joins(:connection).first! end # we need to find that latency_tester with the specified connection (and reconnect it) diff --git a/web/app/assets/javascripts/createSession.js.erb b/web/app/assets/javascripts/createSession.js.erb index 8532d162c..cdde53f31 100644 --- a/web/app/assets/javascripts/createSession.js.erb +++ b/web/app/assets/javascripts/createSession.js.erb @@ -132,6 +132,7 @@ if (!(context.JK.hasOneConfiguredDevice())) { app.afterFtue = function() { submitForm(evt); }; app.layout.startNewFtue(); + alert("dere") return false; } diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 2691c7d4c..67eff0355 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -444,7 +444,7 @@ function SessionStartRecording() {} function SessionStopPlay() {} function SessionStopRecording() {} - function SessionAddPlayTrack() {} + function SessionAddPlayTrack() {return true;} function SessionRemoveAllPlayTracks(){} function isSessionTrackPlaying() { return false; } function SessionCurrrentPlayPosMs() { return 0; } @@ -675,17 +675,39 @@ fakeJamClientRecordings = fakeRecordingsImpl; } - function OnLoggedIn(userId, sessionToken) { - - } - - function OnLoggedOut() { - - } - - function UserAttention(option) { + function TestNetworkPktBwRate(targetClientId, successCallback, timeoutCallback, testType, duration, numClients, payloadSize) { + var progress = {progress:true, + upthroughput:.95, + downthroughput:.95, + upjitter: 2.3, + downjitter: 2.3 + } + var count = 0; + var interval = setInterval(function() { + eval(successCallback + "(" + JSON.stringify(progress) + ");"); + + if(progress.upthroughput < 1) { + progress.upthroughput += .05; + } + if(progress.downthroughput < 1) { + progress.downthroughput += .05; + } + + count++; + if(count == duration) { + clearInterval(interval); + + delete progress['progress'] + progress.pass = true; + eval(successCallback + "(" + JSON.stringify(progress) + ");"); + } + }, 1000); } + function StopNetworkTest(targetClientId) {} + function OnLoggedIn(userId, sessionToken) {} + function OnLoggedOut() {} + function UserAttention(option) {} function log(level, message) { console.log("beep : " + message) @@ -902,6 +924,9 @@ this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; this.SetFakeRecordingImpl = SetFakeRecordingImpl; + // network test + this.TestNetworkPktBwRate = TestNetworkPktBwRate; + this.StopNetworkTest = StopNetworkTest; this.log = log; this.getOperatingMode = getOperatingMode; this.clientID = "devtester"; diff --git a/web/app/assets/javascripts/ga.js b/web/app/assets/javascripts/ga.js index 9b5ea6d24..bcbd8835c 100644 --- a/web/app/assets/javascripts/ga.js +++ b/web/app/assets/javascripts/ga.js @@ -89,6 +89,8 @@ register : "Register", download : "DownloadClient", audioTest : "AudioTest", + trackConfig : "AudioTrackConfig", + networkTest : "NetworkTest", sessionCount : "SessionCount", sessionMusicians : "SessionMusicians", sessionQuality : "SessionQuality", @@ -281,6 +283,23 @@ context.ga('send', 'event', category, target, data); } + function trackNetworkTest(platform, numUsers) { + var normalizedPlatform = translatePlatformForGA(platform); + + context.ga('send', 'event', categories.networkTest, 'Passed', normalizedPlatform, numUsers); + } + + function trackAudioTestCompletion(platform) { + var normalizedPlatform = translatePlatformForGA(platform); + + context.ga('send', 'event', categories.audioTest, 'Passed', normalizedPlatform); + } + + function trackConfigureTracksCompletion(platform) { + var normalizedPlatform = translatePlatformForGA(platform); + + context.ga('send', 'event', categories.trackConfig, 'Passed', normalizedPlatform); + } var GA = {}; GA.Categories = categories; @@ -294,6 +313,9 @@ GA.trackRegister = trackRegister; GA.trackDownload = trackDownload; GA.trackFTUECompletion = trackFTUECompletion; + GA.trackNetworkTest = trackNetworkTest; + GA.trackAudioTestCompletion = trackAudioTestCompletion; + GA.trackConfigureTracksCompletion = trackConfigureTracksCompletion; GA.trackSessionCount = trackSessionCount; GA.trackSessionMusicians = trackSessionMusicians; GA.trackSessionQuality = trackSessionQuality; diff --git a/web/app/assets/javascripts/gear/gear_wizard.js b/web/app/assets/javascripts/gear/gear_wizard.js index b75f239a9..dfd313cb1 100644 --- a/web/app/assets/javascripts/gear/gear_wizard.js +++ b/web/app/assets/javascripts/gear/gear_wizard.js @@ -103,6 +103,27 @@ } + function onCanceled() { + if (app.cancelFtue) { + app.cancelFtue(); + app.afterFtue = null; + app.cancelFtue = null; + } + + return closeDialog(); + } + + function onClosed() { + if (app.afterFtue) { + // If there's a function to invoke, invoke it. + app.afterFtue(); + app.afterFtue = null; + app.cancelFtue = null; + } + + return closeDialog(); + } + function closeDialog() { wizard.onCloseDialog(); app.layout.closeDialog('gear-wizard'); @@ -111,8 +132,8 @@ function events() { $(wizard).on('step_changed', onStepChanged); - $(wizard).on('wizard_cancel', closeDialog); - $(wizard).on('wizard_close', closeDialog); + $(wizard).on('wizard_cancel', onCanceled); + $(wizard).on('wizard_close', onClosed); } function setNextState(enabled) { diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js index a6ef48b70..5531ccb86 100644 --- a/web/app/assets/javascripts/gear/step_configure_tracks.js +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -179,7 +179,13 @@ return false; } - return save(tracks); + var saved = save(tracks); + + if(saved) { + context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS()); + } + + return saved; } function beforeShow() { diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js index 715e8b4e5..5a32647eb 100644 --- a/web/app/assets/javascripts/gear/step_network_test.js +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -5,6 +5,18 @@ context.JK = context.JK || {}; context.JK.StepNetworkTest = function (app, $dialog) { + var NETWORK_TEST_TYPES = { + RestPhase : 0, + PktTest100NormalLatency : 1, + PktTest200MediumLatency : 2, + PktTest400LowLatency : 3, + PktTestRateSweep : 4, + RcvOnly : 5 + } + var STARTING_NUM_CLIENTS = 4; + var PAYLOAD_SIZE = 100; + var MINIMUM_ACCEPTABLE_SESSION_SIZE = 2; + var rest = context.JK.Rest(); var logger = context.JK.logger; var $step = null; @@ -16,64 +28,355 @@ var $testScore = null; var $testText = null; var testedSuccessfully = false; - var serverClientId = null; + var $scoringBar = null; + var $goodMarker = null; + var $goodLine = null; + var $currentScore = null; + var $scoredClients = null; + var $subscore = null; + + var serverClientId = ''; var isScoring = false; + var numClientsToTest = STARTING_NUM_CLIENTS; + var testSummary = {attempts : [], final:null} + + var scoringZoneWidth = 100;//px + + + function reset() { + serverClientId = ''; + isScoring = false; + numClientsToTest = STARTING_NUM_CLIENTS; + testSummary = {attempts : []}; + } + + function renderStartTest() { + $scoredClients.empty(); + $testResults.removeClass('good acceptable bad').addClass('testing'); + $testText.empty(); + $subscore.empty(); + updateControlsState(); + $currentScore.width(0); + $goodLine.css('left', (gon.ftue_packet_rate_treshold * 100) + '%'); + $goodMarker.css('left', (gon.ftue_packet_rate_treshold * 100) + '%'); + } + + function renderStopTest(score, text) { + $scoredClients.html(score); + $testText.html(text); + $testResults.removeClass('testing'); + } + + function postDiagnostic() { + rest.createDiagnostic({ + type: 'NETWORK_TEST_RESULT', + data: {client_type: context.JK.clientType(), client_id: context.JK.JamServer.clientID, summary:testSummary} + }); + } + + function testFinished() { + var attempt = getCurrentAttempt(); + + if(!attempt.final) { + attempt.final = {reason : attempt.reason}; + } + + var reason = attempt.final.reason; + + if(reason == "success") { + renderStopTest(attempt.num_clients, "Your router and Internet service will support sessions of up to " + attempt.num_clients + " JamKazam musicians.") + testedSuccessfully = true; + if(!attempt.final.num_clients) { + attempt.final.num_clients = attempt.num_clients; + } + context.JK.GA.trackNetworkTest(context.JK.detectOS(), attempt.final.num_clients); + //context.jamClient.updateLatencyTestScore(attempt.num_clients); + if(attempt.final.num_clients == 2) { + $testResults.addClass('acceptable'); + } + else { + $testResults.addClass('good'); + } + } + else if(reason == "minimum_client_threshold") { + renderStopTest('', "We're sorry, but your router and Internet service will not effectively support JamKazam sessions. Please click the HELP button for more information.") + } + else if(reason == "unreachable") { + renderStopTest('', "We're sorry, but your router will not support JamKazam in its current configuration. Please click the HELP button for more information."); + } + else if(reason == "internal_error") { + context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection."); + renderStopTest('', ''); + } + else if(reason == "remote_peer_cant_test") { + context.JK.alertSupportedNeeded("JamKazam is experiencing technical difficulties."); + renderStopTest('', ''); + } + else if(reason == "invalid_response") { + context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection. Reason=" + attempt.backend_data.reason + '.'); + renderStopTest('', ''); + } + else if(reason == 'no_servers') { + context.JK.alertSupportedNeeded("No network test servers are available. You can skip this step for now."); + renderStopTest('', ''); + testedSuccessfully = true; + } + else if(reason == 'no_network') { + context.JK.Banner.showAlert("Please try again later. Your network appears down."); + renderStopTest('', ''); + } + else if(reason == "rest_api_error") { + context.JK.alertSupportedNeeded("Unable to network test servers are available. You can skip this step for now."); + testedSuccessfully = true; + renderStopTest('', ''); + } + else if(reason == "timeout") { + context.JK.alertSupportedNeeded("Unable to network test servers timed out. You can skip this step for now."); + testedSuccessfully = true; + renderStopTest('', ''); + } + else { + context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection."); + renderStopTest('', ''); + } + + numClientsToTest = STARTING_NUM_CLIENTS; + isScoring = false; + updateControlsState(); + postDiagnostic(); + } + + function getCurrentAttempt() { + return testSummary.attempts[testSummary.attempts.length - 1]; + } + + + function attemptTestPass() { + + var attempt = {}; + attempt.payload_size = PAYLOAD_SIZE; + attempt.duration = gon.ftue_network_test_duration; + attempt.test_type = 'PktTest400LowLatency'; + attempt.num_clients = numClientsToTest; + attempt.server_client_id = serverClientId; + attempt.received_progress = false; + testSummary.attempts.push(attempt); + + //context.jamClient.StopNetworkTest(''); + + $testText.text("Simulating the network traffic of a " + numClientsToTest + "-person session."); + + updateProgress(0, false); + + $currentScore.css + context.jamClient.TestNetworkPktBwRate(serverClientId, TEST_SUCCESS_CALLBACK, TEST_TIMEOUT_CALLBACK, + NETWORK_TEST_TYPES.PktTest400LowLatency, + gon.ftue_network_test_duration, + numClientsToTest, + PAYLOAD_SIZE); + } function startNetworkTest() { isScoring = true; + numClientsToTest = STARTING_NUM_CLIENTS; + renderStartTest(); rest.getLatencyTester() .done(function(response) { + // ensure there are no tests ongoing + serverClientId = response.client_id; - logger.info("beginning network test against client_id: " + clientId); + logger.info("beginning network test against client_id: " + serverClientId); - context.jamClient.TestNetworkPktBwRate(serverClientId, TEST_SUCCESS_CALLBACK, TEST_TIMEOUT_CALLBACK); + attemptTestPass(); }) - .fail(function() { - isScoring = false; - logger.error("arguments:", arguments); - - if(context.JK.isNetworkError(arguments)) { - context.JK.Banner.showAlert("Please try again latery. Your network appears down."); + .fail(function(jqXHR) { + if(jqXHR.status == 404) { + // means there are no network testers available. + // we have to skip this part of the UI + testSummary.final = {reason: 'no_servers'} } else { - logger.error("unable to get latency tester from server"); + if(context.JK.isNetworkError(arguments)) { + testSummary.final = {reason: 'no_network'} + } + else { + testSummary.final = {reason: 'rest_api_error'} + } } + testFinished(); }) logger.info("starting network test"); return false; } - function networkTestSuccess() { - console.log("success arguments: ", arguments); - context.jamClient.StopNetworkTest(serverClientId); + function updateProgress(throughput, showSubscore) { + var width = throughput * 100; + + $currentScore.stop().data('showSubscore', showSubscore); + + if(!showSubscore) { + $subscore.text(''); + } + + $currentScore.animate({ + duration: 1000, + width: width + '%' + }, { + step: function (now, fx) { + if(showSubscore) { + var newWidth = ( 100 * parseFloat($currentScore.css('width')) / parseFloat($currentScore.parent().css('width')) ); + $subscore.text((Math.round(newWidth * 10) / 10) + '%'); + } + } + }).css('overflow', 'visible'); + ; } - function networkTestTimeout() { - console.log("timeout arguments:", arguments); - context.jamClient.StopNetworkTest(serverClientId); + function networkTestSuccess(data) { + var attempt = getCurrentAttempt(); + + function refineTest(up) { + if(up) { + if(numClientsToTest == gon.ftue_network_test_max_clients) { + attempt.reason = "success"; + testFinished(); + } + else { + numClientsToTest++; + logger.debug("increasing number of clients to " + numClientsToTest); + setTimeout(attemptTestPass, 1); + } + } + else { + // reduce numclients if we can + if(numClientsToTest == MINIMUM_ACCEPTABLE_SESSION_SIZE) { + // we are too low already. fail the user + attempt.reason = "minimum_client_threshold"; + testFinished(); + } + else if(numClientsToTest > STARTING_NUM_CLIENTS) { + // this means we've gone up before... so don't go back down (i.e., creating a loop) + attempt.reason = "success"; + attempt.final = { reason:'success', num_clients: numClientsToTest - 1 } + testFinished(); + } + else { + numClientsToTest--; + logger.debug("reducing number of clients to " + numClientsToTest); + setTimeout(attemptTestPass, 1); + } + } + } + + attempt.backend_data = data; + + if(data.progress === true) { + + var animate = true; + if(data.downthroughput && data.upthroughput) { + + if(data.downthroughput > 0 || data.upthroughput > 0) { + attempt.received_progress = true; + animate = true; + } + + if(attempt.received_progress) { + // take the lower + var throughput= data.downthroughput < data.upthroughput ? data.downthroughput : data.upthroughput; + + updateProgress(throughput, true); + } + } + } + else { + + logger.debug("network test pass success. data: ", data); + + if(data.reason == "unreachable") { + // STUN + logger.debug("network test: unreachable (STUN issue or similar)"); + attempt.reason = data.reason; + testFinished(); + } + else if(data.reason == "internal_error") { + // oops + logger.debug("network test: internal_error (client had a unexpected problem)"); + attempt.reason = data.reason; + testFinished(); + } + else if(data.reason == "remote_peer_cant_test") { + // old client + logger.debug("network test: remote_peer_cant_test (old client)") + attempt.reason = data.reason; + testFinished(); + } + else { + if(!data.downthroughput || !data.upthroughput) { + // we have to assume this is bad. just not a reason we know about in code + logger.debug("network test: no test data (unknown issue? " + data.reason + ")") + attempt.reason = "invalid_response"; + testFinished(); + } + else { + // success... but we still have to verify if this data is within threshold + if(data.downthroughput < gon.ftue_packet_rate_treshold) { + logger.debug("network test: downthroughput too low. downthroughput: " + data.downthroughput + ", threshold: " + gon.ftue_packet_rate_treshold); + refineTest(false); + } + else if(data.upthroughput < gon.ftue_packet_rate_treshold) { + logger.debug("network test: upthroughput too low. upthroughput: " + data.upthroughput + ", threshold: " + gon.ftue_packet_rate_treshold); + refineTest(false); + } + else { + // true success. we can accept this score + logger.debug("network test: success") + refineTest(true); + } + } + } + + // VRFS-1742 + // context.jamClient.StopNetworkTest(serverClientId); + } + + } + + function networkTestTimeout(data) { + logger.warn("network timeout when testing latency test: " + data); + + var attempt = getCurrentAttempt(); + attempt.reason = 'timeout'; + attempt.backend_data = data; + testFinished(); } function hasScoredNetworkSuccessfully() { return testedSuccessfully; } + function updateControlsState() { + initializeNextButtonState(); + initializeBackButtonState(); + } + function initializeNextButtonState() { - $dialog.setNextState(hasScoredNetworkSuccessfully()); + $dialog.setNextState(hasScoredNetworkSuccessfully() && !isScoring); } function initializeBackButtonState() { - $dialog.setNextState(); + $dialog.setBackState(!isScoring); } function newSession() { - isScoring = false; - // XXX context.jamClient.stopNetworkTest(); + reset(); + //context.jamClient.StopNetworkTest(''); } function beforeShow() { - initializeNextButtonState(); + reset(); + updateControlsState(); } function initialize(_$step) { @@ -83,7 +386,12 @@ $testResults = $step.find('.network-test-results'); $testScore = $step.find('.network-test-score'); $testText = $step.find('.network-test-text'); - + $scoringBar = $step.find('.scoring-bar'); + $goodMarker = $step.find('.good-marker'); + $goodLine =$step.find('.good-line'); + $currentScore = $step.find('.current-score'); + $scoredClients= $step.find('.scored-clients'); + $subscore = $step.find('.subscore'); $startNetworkTestBtn.on('click', startNetworkTest); } diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js index 5f520db66..3b79d38ba 100644 --- a/web/app/assets/javascripts/gear/step_select_gear.js +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -932,9 +932,15 @@ // lie for now until IO questions finalize validIOScore = true; + onSuccessfulScore(); + renderScoringStopped(); } + function onSuccessfulScore() { + + } + // refocused affects how IO testing occurs. // on refocus=true: // * reuse IO score if it was good/acceptable @@ -1049,7 +1055,9 @@ return false; } else { + //XXX rename profile context.jamClient.FTUESave(true); + context.JK.GA.trackAudioTestCompletion(context.JK.detectOS()); return true; } diff --git a/web/app/assets/javascripts/gear/wizard.js b/web/app/assets/javascripts/gear/wizard.js index 9f608a7f4..2a6fea575 100644 --- a/web/app/assets/javascripts/gear/wizard.js +++ b/web/app/assets/javascripts/gear/wizard.js @@ -13,6 +13,7 @@ var $wizardSteps = null; var $currentWizardStep = null; var $wizardButtons = null; + var $btnHelp = null; var $btnNext = null; var $btnBack = null; var $btnClose = null; @@ -21,7 +22,7 @@ var $self = $(this); function totalSteps() { - return STEPS.length; + return context.JK.dkeys(STEPS).length; } function beforeHideStep() { @@ -62,7 +63,7 @@ var stepInfo = STEPS[step]; if(stepInfo.handleNext) { var result = stepInfo.handleNext.call(stepInfo); - if(!result) {return false;} + if(result === false) {return false;} } previousStep = step; @@ -81,25 +82,32 @@ $currentWizardStep = $nextWizardStep; + context.JK.GA.virtualPageView(location.pathname + location.search + location.hash, $currentWizardStep.attr('dialog-title')); + $self.triggerHandler('step_changed', {step:step}); // update buttons var $wizardButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); + $btnHelp = $wizardButtonsContent.find('.btn-help'); $btnBack = $wizardButtonsContent.find('.btn-back'); $btnNext = $wizardButtonsContent.find('.btn-next'); $btnClose = $wizardButtonsContent.find('.btn-close'); $btnCancel = $wizardButtonsContent.find('.btn-cancel'); + // hide help button if on last step + if (step == totalSteps() - 1) { + $btnHelp.hide(); + } // hide back button if 1st step or last step - if (step == 0 && step == totalSteps() - 1) { + if (step == 0 || step == totalSteps() - 1) { $btnBack.hide(); } - // hide next button if not on last step + // hide next button if on last step if (step == totalSteps() - 1) { $btnNext.hide(); } - // hide close if on last step + // hide close if not on last step if (step != totalSteps() - 1) { $btnClose.hide(); } @@ -130,7 +138,7 @@ previousStep = null; - step = args.d1; + step = args != null ? args.d1 : 0; if (!step) step = 0; step = parseInt(step); moveToStep(); @@ -177,7 +185,6 @@ } function initialize(_$dialog, _$wizardSteps, _STEPS) { - $dialog = _$dialog; $wizardSteps = _$wizardSteps; STEPS = _STEPS; diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index a32df7763..526daa62f 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -678,9 +678,9 @@ function startNewFtue() { var step = 0; - setWizardStep(step); - wizardShowFunctions[step](); - showDialog('ftue'); + //setWizardStep(step); + //wizardShowFunctions[step](); + showDialog('gear-wizard'); } function setWizardStep(targetStepId) { diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js index 185645b81..384fd5dc3 100644 --- a/web/app/assets/javascripts/trackHelpers.js +++ b/web/app/assets/javascripts/trackHelpers.js @@ -46,7 +46,13 @@ for (i=0; i < localMusicTracks.length; i++) { var track = {}; track.client_track_id = localMusicTracks[i].id; - track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id; + + if(localMusicTracks[i].instrument_id === 0) { + track.instrument_id = context.JK.server_to_client_instrument_map["Other"].server_id; + } + else { + track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id; + } if (localMusicTracks[i].stereo) { track.sound = "stereo"; } diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index 1e4404b66..05ec3e591 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -57,7 +57,7 @@ margin-top:20px; } - .ftue-buttons { + .wizard-buttons { position: absolute; bottom: 0; width:100%; @@ -67,7 +67,7 @@ } - .ftue-buttons-holder { + .wizard-buttons-holder { float:right; } @@ -151,6 +151,9 @@ width:25%; } + .watch-video { + margin-top:29px; + } .select-audio-input-device { margin-bottom:20px; } @@ -378,6 +381,10 @@ } } + .watch-video { + margin-top:45px; + } + .icon-instrument-select { padding:3px 0; // to combine 24 of .current-instrument + 3x on either side margin:0 auto 15px; // 15 margin-bottom to match tracks on the left @@ -478,13 +485,21 @@ &[track-count="2"] { .ftue-input { - width:50%; + width:49%; display:inline-block; + + &:nth-of-type(1) { + float:left; + &:after { + float:left; + content: ','; + padding-right:3px; + } + } + &:nth-of-type(2) { + float:right; + } } - /**.ftue-input:nth-child(1)::before { - content: ','; - padding-right:3px; - }*/ } } } @@ -499,6 +514,9 @@ } } + .watch-video { + margin-top:97px; + } .voicechat-option { @@ -625,6 +643,10 @@ margin-top:22px; } + .watch-video { + margin-top:90px; + } + a.start-network-test { margin-top:20px; } @@ -635,6 +657,8 @@ color:white; font-size:20px; background-color:#222; + text-align:center; + margin-bottom:20px; &.good { background-color: #72a43b; @@ -647,6 +671,63 @@ } } + .scoring-bar { + width:100%; + height:20px; + left:0; + position:relative; + //display:inline-block; + display:none; + + .current-score { + background-color:gray; + border-right:1px solid white; + border-top:1px solid #ccc; + border-bottom:1px solid #ccc; + border-left:1px solid #ccc; + height:20px; + display:inline-block; + position:relative; + left:0; + min-width:55px; + text-align:left; + padding-left:5px; + @include border_box_sizing; + font-size:12px; + color:white; + + .subscore { + font-size:10px; + color:white; + bottom:-15px; + right:-16px; + position:absolute; + } + } + + .good-marker { + position:absolute; + text-align:center; + left:95%; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #72a43b; + margin-left:-5px; + top:-7px; + } + + .good-line { + position:absolute; + height:100%; + left:95%; + width:1px; + background-color: #72a43b; + margin-left:-0.5px; + top:0; + } + } .network-test-text { } @@ -655,16 +736,41 @@ height: 248px ! important; @include border_box_sizing; &.testing { + + text-align:left; .network-test-score { - font-size:16px; + display:none; + } + + .scoring-bar { + display:inline-block; + margin-bottom:10px; } .network-test-text { - background-image: url('/assets/shared/spinner.gif'); - background-repeat:no-repeat; - background-position:center; + //background-image: url('/assets/shared/spinner.gif'); + //background-repeat:no-repeat; + //background-position:center; //width:128px; - height:128px; + //height:128px; + } + } + + &.good { + .network-test-score { + background-color: #72a43b; + } + } + + &.acceptable { + .network-test-score { + background-color: #D6A800; + } + } + + &.bad { + .network-test-score { + background-color: #7B0C00; } } } @@ -674,10 +780,15 @@ .wizard-step-content .wizard-step-column { &:nth-of-type(1) { width:50%; + height:350px; } &:nth-of-type(2) { width:50%; } + + ul { + margin-bottom:20px; + } } } diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb index 1a44261dd..f904fd7d5 100644 --- a/web/app/controllers/clients_controller.rb +++ b/web/app/controllers/clients_controller.rb @@ -52,6 +52,9 @@ class ClientsController < ApplicationController gon.fp_upload_dir = Rails.application.config.filepicker_upload_dir gon.allow_force_native_client = Rails.application.config.allow_force_native_client gon.ftue_io_wait_time = Rails.application.config.ftue_io_wait_time + gon.ftue_packet_rate_treshold = Rails.application.config.ftue_packet_rate_treshold + gon.ftue_network_test_duration = Rails.application.config.ftue_network_test_duration + gon.ftue_network_test_max_clients = Rails.application.config.ftue_network_test_max_clients # is this the native client or browser? @nativeClient = is_native_client? diff --git a/web/app/views/clients/gear/_buttons.html.haml b/web/app/views/clients/gear/_buttons.html.haml index 5f3209ce9..3ab9f4629 100644 --- a/web/app/views/clients/gear/_buttons.html.haml +++ b/web/app/views/clients/gear/_buttons.html.haml @@ -4,9 +4,9 @@ - total_steps = 7 -.ftue-buttons - .ftue-buttons-holder - %a.button-grey{href: '#'} HELP +.wizard-buttons + .wizard-buttons-holder + %a.button-grey.btn-help{href: '#'} HELP - if step > 0 && step != total_steps %a.button-orange.btn-back{href:'#'} BACK - if step != total_steps diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 7d5e5195a..81274aabd 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -217,10 +217,10 @@ .wizard-step-column %h2 Instructions .ftue-box.instructions - Find the Direct Monitoring control on your audio interface.
    %ul - %li If a button, push it into its off position - %li If a knob, turn it so that 100% of audio is from your computer, and 0% is from the direct monitor + %li Check that computer is connected to router using Ethernet cable. + %li Click Start Network Test button. + %li View test results. .center %a.button-orange.watch-video{href:'#'} WATCH VIDEO .wizard-step-column @@ -232,7 +232,14 @@ .wizard-step-column %h2 Test Results .network-test-results.ftue-box + .scoring-bar + .current-score + testing... + .subscore + .good-marker + .good-line .network-test-score + .scored-clients .network-test-text @@ -246,28 +253,26 @@ %p Have fun and thanks for joining us! %p — Team JamKazam .wizard-step-column - %h2 - Tutorial Videos - %ul - %li - %a Creating a Session - %li - %a Finding a Session - %li - %a Playing in a Session - %li - %a Connecting with Other Musicians - %li - %a Making and Sharing Recordings - %li - %a Broadcasting Your Sessions - %h2 - Other Valuable Resource Links - %ul - %li - %a JamKazam Support Center - %li - %a JamKazam Community Forum + %h2 Tutorial Videos + %ul + %li + %a Creating a Session + %li + %a Finding a Session + %li + %a Playing in a Session + %li + %a Connecting with Other Musicians + %li + %a Making and Sharing Recordings + %li + %a Broadcasting Your Sessions + %h2 Other Valuable Resource Links + %ul + %li + %a JamKazam Support Center + %li + %a JamKazam Community Forum .wizard-buttons %script{type: 'text/template', id: 'template-ftuesteps'} @@ -289,9 +294,9 @@ %script{type: 'text/template', id: 'template-wizard-buttons'} - .ftue-buttons-holder + .wizard-buttons-holder %a.button-grey.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL - %a.button-grey{href: '#'} HELP + %a.button-grey.btn-help{href: '#'} HELP %a.button-orange.btn-back{href:'#'} BACK %a.button-orange.btn-next{href:'#'} NEXT %a.button-orange.btn-close{href:'#', 'layout-action' => 'close'} CLOSE diff --git a/web/config/application.rb b/web/config/application.rb index 2c8ae1125..1654b9fb0 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -235,5 +235,11 @@ if defined?(Bundler) # how long should the frontend wait for the IO to stabilize before asking for a IO score? config.ftue_io_wait_time = 10 + # what should the threshold be for us to say, 'this person can't play at this rate' during the network test + config.ftue_packet_rate_treshold = 0.95 + # how long to test at each network test step + config.ftue_network_test_duration = 10 + # max number of people to test + config.ftue_network_test_max_clients = 8 end end diff --git a/web/spec/controllers/api_latency_tests_controller_spec.rb b/web/spec/controllers/api_latency_tests_controller_spec.rb index 8ef24e6c2..fb773b3ef 100644 --- a/web/spec/controllers/api_latency_tests_controller_spec.rb +++ b/web/spec/controllers/api_latency_tests_controller_spec.rb @@ -1,7 +1,20 @@ require 'spec_helper' -describe ApiFeedsController do +describe ApiLatencyTestersController do render_views + before(:each) do + LatencyTester.delete_all + end + + describe "match" do + it "insists on login" do + get :match + response.status.should == 403 + end + + it "throws 404 if no latency testers" + end end + diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 43ab68cc9..3caee5042 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -480,10 +480,10 @@ module JamWebsockets default_expire = @connect_time_expire_client end - heartbeat_interval = user.try(:heartbeat_interval_client) || default_heartbeat + heartbeat_interval = (user && user.heartbeat_interval_client) || default_heartbeat heartbeat_interval = heartbeat_interval.to_i heartbeat_interval = default_heartbeat if heartbeat_interval == 0 # protect against bad config - connection_expire_time = user.try(:connection_expire_time_client) || default_expire + connection_expire_time = (user && user.connection_expire_time_client) || default_expire connection_expire_time = connection_expire_time.to_i connection_expire_time = default_expire if connection_expire_time == 0 # protect against bad config connection_stale_time = default_stale # no user override exists for this; not a very meaningful time right now @@ -825,7 +825,7 @@ module JamWebsockets # by not catching any exception here, a PermissionError will be thrown if this isn't valid # if for some reason the client is trying to send to a client that it doesn't # belong to - access_p2p(to_client_id, context.user, client_msg) + #access_p2p(to_client_id, context.user, client_msg) if to_client_id.nil? || to_client_id == 'undefined' # javascript translates to 'undefined' in many cases raise SessionError, "empty client_id specified in peer-to-peer message" From 7da75e94d97218cad74c116631eaed1383b61f94 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 29 May 2014 14:14:46 -0500 Subject: [PATCH 12/29] * VRFS-1575 --- ruby/lib/jam_ruby/models/diagnostic.rb | 5 +- web/app/assets/javascripts/accounts.js | 23 +- .../javascripts/accounts_audio_profile.js | 65 +++- web/app/assets/javascripts/banner.js | 34 +- web/app/assets/javascripts/chatPanel.js | 2 +- .../assets/javascripts/createSession.js.erb | 1 - web/app/assets/javascripts/fakeJamClient.js | 16 + .../javascripts/gear/loopback_wizard.js | 82 +++++ .../javascripts/gear/step_configure_tracks.js | 5 +- .../gear/step_configure_voice_chat.js | 17 +- .../javascripts/gear/step_network_test.js | 41 ++- .../javascripts/gear/step_select_gear.js | 299 ++++++++---------- .../assets/javascripts/gear/step_success.js | 2 +- web/app/assets/javascripts/gear/wizard.js | 20 +- web/app/assets/javascripts/globals.js | 39 +++ .../javascripts/jquery.instrumentSelector.js | 1 + web/app/assets/javascripts/layout.js | 2 +- web/app/assets/javascripts/utils.js | 112 ++++++- .../stylesheets/client/account.css.scss | 4 + .../assets/stylesheets/client/banner.css.scss | 5 + .../stylesheets/client/gearWizard.css.scss | 66 ++-- .../stylesheets/easydropdown_jk.css.scss | 1 + web/app/controllers/clients_controller.rb | 1 + web/app/views/clients/_account.html.erb | 48 +-- .../clients/_account_audio_profile.html.erb | 14 +- web/app/views/clients/_help.html.erb | 12 + web/app/views/clients/_sidebar.html.erb | 2 +- .../views/clients/gear/_gear_wizard.html.haml | 10 +- web/config/application.rb | 2 + web/spec/features/chat_message_spec.rb | 2 +- web/spec/features/in_session_spec.rb | 2 +- web/spec/features/user_progression_spec.rb | 6 +- 32 files changed, 675 insertions(+), 266 deletions(-) diff --git a/ruby/lib/jam_ruby/models/diagnostic.rb b/ruby/lib/jam_ruby/models/diagnostic.rb index 8385fff40..1bb12ac90 100644 --- a/ruby/lib/jam_ruby/models/diagnostic.rb +++ b/ruby/lib/jam_ruby/models/diagnostic.rb @@ -32,9 +32,12 @@ module JamRuby # info about how the test went NETWORK_TEST_RESULT = 'NETWORK_TEST_RESULT' + # step 2 of the FTUE... could the user select their gear? + GEAR_SELECTION = 'GEAR_SELECTION' + DIAGNOSTIC_TYPES = [NO_HEARTBEAT_ACK, WEBSOCKET_CLOSED_REMOTELY, EXPIRED_STALE_CONNECTION, MISSING_CLIENT_STATE, UNKNOWN_MESSAGE_TYPE, MISSING_ROUTE_TO, - DUPLICATE_CLIENT, WEBSOCKET_CLOSED_LOCALLY, NETWORK_TEST_RESULT] + DUPLICATE_CLIENT, WEBSOCKET_CLOSED_LOCALLY, NETWORK_TEST_RESULT, GEAR_SELECTION] # creator types # CLIENT = 'client' diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index b346b467c..046a09772 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -29,16 +29,19 @@ var validProfiles = prettyPrintAudioProfiles(context.JK.getGoodConfigMap()); var invalidProfiles = prettyPrintAudioProfiles(context.JK.getBadConfigMap()); - var template = context.JK.fillTemplate($('#template-account-main').html(), { - email: userDetail.email, - name: userDetail.name, - location : userDetail.location, - instruments : prettyPrintInstruments(userDetail.instruments), - photoUrl : context.JK.resolveAvatarUrl(userDetail.photo_url), - validProfiles : validProfiles, - invalidProfiles : invalidProfiles - }); - $('#account-content-scroller').html(template); + var $template = $(context._.template($('#template-account-main').html(), { + email: userDetail.email, + name: userDetail.name, + location : userDetail.location, + instruments : prettyPrintInstruments(userDetail.instruments), + photoUrl : context.JK.resolveAvatarUrl(userDetail.photo_url), + validProfiles : validProfiles, + invalidProfiles : invalidProfiles, + isNativeClient: gon.isNativeClient, + musician: context.JK.currentUserMusician + } , { variable: 'data' })); + + $('#account-content-scroller').html($template); } function prettyPrintAudioProfiles(profileMap) { diff --git a/web/app/assets/javascripts/accounts_audio_profile.js b/web/app/assets/javascripts/accounts_audio_profile.js index 9cc73ac57..3b1b7ee60 100644 --- a/web/app/assets/javascripts/accounts_audio_profile.js +++ b/web/app/assets/javascripts/accounts_audio_profile.js @@ -35,18 +35,30 @@ } function populateAccountAudio() { + var all = context.jamClient.FTUEGetAllAudioConfigurations(); + var good = context.jamClient.FTUEGetGoodAudioConfigurations(); + var current = context.jamClient.FTUEGetMusicProfileName(); - // load Audio Driver dropdown - var devices = context.jamClient.TrackGetDevices(); + var profiles = []; + context._.each(all, function(item) { + profiles.push({id: item, good: false, class:'bad', current: current == item, active_text: current == item ? '(active)' : ''}) + }); - var options = { - devices: devices + if(good) { + for(var i = 0; i < good.length; i++) { + for(var j = 0; j < profiles.length; j++) { + if(good[i] == profiles[j].id) { + profiles[j].good = true; + profiles[j].class = 'good'; + break; + } + } + } } - var template = context._.template($('#template-account-audio').html(), options, {variable: 'data'}); + var template = context._.template($('#template-account-audio').html(), {profiles: profiles}, {variable: 'data'}); appendAudio(template); - } function appendAudio(template) { @@ -65,9 +77,29 @@ populateAccountAudio(); } + function handleActivateAudioProfile(audioProfileId) { + logger.debug("activating audio profile: " + audioProfileId); + + if(audioProfileId == context.jamClient.FTUEGetMusicProfileName()) { + context.JK.Banner.showAlert("This profile is already active."); + return; + } + + var result = context.jamClient.FTUELoadAudioConfiguration(audioProfileId); + + if(!result) { + logger.error("unable to activate audio configuration: " + audioProfileId); + context.JK.alertSupportedNeeded("Unable to activate audio configuration for profile named: " + audioProfileId); + } + + // redraw after activation of profile + populateAccountAudio(); + } function handleStartAudioQualification() { - if(false) { + if(true) { + app.afterFtue = function() { populateAccountAudio() }; + app.cancelFtue = function() { populateAccountAudio() }; app.layout.startNewFtue(); } else { @@ -91,12 +123,27 @@ // events for main screen function events() { // wire up main panel clicks - $('#account-audio-content-scroller').on('click', 'a[data-purpose=delete-audio-profile]', function (evt) { + var $root = $('#account-audio-content-scroller'); + $root.on('click', 'a[data-purpose=delete-audio-profile]', function (evt) { evt.stopPropagation(); handleDeleteAudioProfile($(this).attr('data-id')); return false; }); - $('#account-audio-content-scroller').on('click', 'a[data-purpose=add-profile]', function (evt) { + + $root.on('click', 'a[data-purpose=activate-audio-profile]', function (evt) { + evt.stopPropagation(); + var $btn = $(this); + var status = $btn.closest('tr').attr('data-status'); + if(status == "good") { + handleActivateAudioProfile($btn.attr('data-id')); + } + else { + context.JK.Banner.showAlert("Unable to activate this profile. Please verify that the devices associated are connected."); + } + return false; + }); + + $root.on('click', 'a[data-purpose=add-profile]', function (evt) { evt.stopPropagation(); handleStartAudioQualification(); return false; diff --git a/web/app/assets/javascripts/banner.js b/web/app/assets/javascripts/banner.js index 7b11a3211..5bb003440 100644 --- a/web/app/assets/javascripts/banner.js +++ b/web/app/assets/javascripts/banner.js @@ -8,7 +8,7 @@ context.JK.Banner = (function () { var self = this; var logger = context.JK.logger; - var $banner = $('#banner'); + var $banner = null; function showAlert(options) { if (typeof options == 'string' || options instanceof String) { @@ -35,13 +35,33 @@ throw "unable to show banner for empty message"; } - if(options.type == "alert" || options.close) { - var $closeBtn = $('#banner').find('.close-btn'); + var $closeBtn = $banner.find('.close-btn'); - $closeBtn.click(function() { + if((options.type == "alert" && !options.buttons) || options.close) { + + $closeBtn.show().click(function() { hide(); return false; - }).show(); + }); + } + else { + $closeBtn.hide(); + } + + if(options.buttons) { + var $buttons = $banner.find('.buttons') + context._.each(options.buttons, function(button) { + if(!button.name) throw "button.name must be specified"; + if(!button.click) throw "button.click must be specified"; + + var $btn = $('' + button.name + ''); + $btn.click(function() { + button.click(); + hide(); + return false; + }); + $buttons.append($btn); + }); } $('#banner').attr('data-type', options.type).show() @@ -52,13 +72,15 @@ } function hide() { - $('#banner').hide(); + $banner.hide(); + $banner.find('.user-btn').remove(); $('#banner_overlay .dialog-inner').html(""); $('#banner_overlay').hide(); } function initialize() { + $banner = $('#banner'); return self; } diff --git a/web/app/assets/javascripts/chatPanel.js b/web/app/assets/javascripts/chatPanel.js index 7b7496a53..7d3f995fc 100644 --- a/web/app/assets/javascripts/chatPanel.js +++ b/web/app/assets/javascripts/chatPanel.js @@ -338,7 +338,7 @@ this.initialize = initialize; this.sessionStarted = sessionStarted; - this.sessionStopped = sessionStopped; + this.sessionStopped = sessionStopped; this.registerChatMessage = registerChatMessage; }; diff --git a/web/app/assets/javascripts/createSession.js.erb b/web/app/assets/javascripts/createSession.js.erb index cdde53f31..8532d162c 100644 --- a/web/app/assets/javascripts/createSession.js.erb +++ b/web/app/assets/javascripts/createSession.js.erb @@ -132,7 +132,6 @@ if (!(context.JK.hasOneConfiguredDevice())) { app.afterFtue = function() { submitForm(evt); }; app.layout.startNewFtue(); - alert("dere") return false; } diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 67eff0355..368808ab5 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -343,6 +343,19 @@ eval(js); } + function IsMyNetworkWireless() { + // 1=true, 0 = false, -1=unknown + return 1; + } + + function SetNetworkTestScore(numClients) { + + } + + function GetNetworkTestScore() { + return 8; + } + function GetASIODevices() { var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FW AP Multi","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; return response; @@ -783,6 +796,9 @@ this.StopRecording = StopRecording; this.TestASIOLatency = TestASIOLatency; this.TestLatency = TestLatency; + this.IsMyNetworkWireless = IsMyNetworkWireless; + this.SetNetworkTestScore = SetNetworkTestScore; + this.GetNetworkTestScore = GetNetworkTestScore; this.connected = true; // FTUE (round 3) diff --git a/web/app/assets/javascripts/gear/loopback_wizard.js b/web/app/assets/javascripts/gear/loopback_wizard.js index 4b3f48784..6202cb76f 100644 --- a/web/app/assets/javascripts/gear/loopback_wizard.js +++ b/web/app/assets/javascripts/gear/loopback_wizard.js @@ -6,10 +6,92 @@ context.JK = context.JK || {}; context.JK.LoopbackWizard = function (app) { + var logger = context.JK.logger; + var $dialog = null; + var wizard = null; var $wizardSteps = null; + var step1 = new context.JK.Step1(app, this); + var step2 = new context.JK.Step2(app, this); + var step3 = new context.JK.Step3(app, this); + var STEPS = { + 0: step1, + 1: step2, + 2: step3 + } + + function beforeShow(args) { + wizard.onBeforeShow(args); + } + + function afterHide() { + wizard.onAfterHide(); + } + + function closeDialog() { + wizard.onCloseDialog(); + app.layout.closeDialog('your-wizard'); + } + + function setNextState(enabled) { + wizard.setNextState(enabled); + } + + function setBackState(enabled) { + wizard.setBackState(enabled); + } + + + function onStepChanged(e, data) { + var step = wizard.getCurrentStep(); + var $currentWizardStep = wizard.getCurrentWizardStep(); + } + + + function onCanceled() { + closeDialog(); + return false; + } + + function onClosed() { + closeDialog(); + return false; + } + + + function events() { + $(wizard).on('step_changed', onStepChanged); + $(wizard).on('wizard_cancel', onCanceled); + $(wizard).on('wizard_close', onClosed); + } + + + function initialize() { + + + var dialogBindings = { beforeShow: beforeShow, afterHide: afterHide }; + + app.bindDialog('your-wizard', dialogBindings); + $dialog = $('#your-wizard'); + $wizardSteps = $dialog.find('.wizard-step'); + + step1.initialize($wizardSteps.filter($('[layout-wizard-step=0]'))); + step2.initialize($wizardSteps.filter($('[layout-wizard-step=1]'))); + step3.initialize($wizardSteps.filter($('[layout-wizard-step=2]'))); + + wizard = new context.JK.Wizard(app); + wizard.initialize($dialog, $wizardSteps, STEPS); + + events(); + } + + this.setNextState = setNextState; + this.setBackState = setBackState; + this.initialize = initialize; + + return this; } })(window, jQuery); diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js index 5531ccb86..cfcabc988 100644 --- a/web/app/assets/javascripts/gear/step_configure_tracks.js +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -30,7 +30,7 @@ context._.each(inputChannels, function (inputChannel) { if(inputChannel.assignment == ASSIGNMENT.UNASSIGNED) { var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); - unassignChannel($channel) + unassignChannel($channel); } else { var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); @@ -67,7 +67,6 @@ $unassignedChannelsHolder.removeClass('possible-target') } }); - }) } @@ -196,7 +195,7 @@ function unassignChannel($channel) { var $originallyAssignedTrack = $channel.closest('.track-target'); $unassignedChannelsHolder.append($channel); - $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length) + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length); } function addChannelToTrack($channel, $track) { diff --git a/web/app/assets/javascripts/gear/step_configure_voice_chat.js b/web/app/assets/javascripts/gear/step_configure_voice_chat.js index e50bb5850..fddc56cf0 100644 --- a/web/app/assets/javascripts/gear/step_configure_voice_chat.js +++ b/web/app/assets/javascripts/gear/step_configure_voice_chat.js @@ -14,6 +14,7 @@ var $useChatInputRadio = null; var $chatInputs = null; var $templateChatInput = null; + var $selectedChatInput = null;// should only be used if isChatEnabled = true function newSession() { $reuseAudioInputRadio.attr('checked', 'checked').iCheck('check'); @@ -59,8 +60,9 @@ }); var $radioButtons = $chatInputs.find('input[name="chat-device"]'); - context.JK.checkbox($radioButtons).on('iChecked', function(e) { + context.JK.checkbox($radioButtons).on('ifChecked', function(e) { var $input = $(e.currentTarget); + $selectedChatInput = $input; // for use in handleNext var channelId = $input.attr('data-channel-id'); context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT); var result = context.jamClient.TrackSaveAssignments(); @@ -131,6 +133,18 @@ $useChatInputRadio.on('ifChecked', enableChat) } + function handleNext() { + var selectedDeviceInfo = context.JK.selectedDeviceInfo(context.jamClient.FTUEGetInputMusicDevice(), context.jamClient.FTUEGetOutputMusicDevice()); + + var chatName = null; + if(isChatEnabled()) { + chatName = $selectedChatInput.attr('data-channel-name'); + } + context.jamClient.FTUESetMusicProfileName(context.JK.createProfileName(selectedDeviceInfo, chatName)); + + return true; + } + function initialize(_$step) { $step = _$step; @@ -142,6 +156,7 @@ handleChatEnabledToggle(); } + this.handleNext = handleNext; this.newSession = newSession; this.beforeShow = beforeShow; this.initialize = initialize; diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js index 5a32647eb..40e7c3361 100644 --- a/web/app/assets/javascripts/gear/step_network_test.js +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -48,6 +48,7 @@ isScoring = false; numClientsToTest = STARTING_NUM_CLIENTS; testSummary = {attempts : []}; + updateControlsState(); } function renderStartTest() { @@ -90,7 +91,7 @@ attempt.final.num_clients = attempt.num_clients; } context.JK.GA.trackNetworkTest(context.JK.detectOS(), attempt.final.num_clients); - //context.jamClient.updateLatencyTestScore(attempt.num_clients); + context.jamClient.SetNetworkTestScore(attempt.num_clients); if(attempt.final.num_clients == 2) { $testResults.addClass('acceptable'); } @@ -177,7 +178,28 @@ } - function startNetworkTest() { + function startNetworkTest(checkWireless) { + + if(isScoring) return false; + + if(checkWireless) { + // check if on Wifi 1st + var isWireless = context.jamClient.IsMyNetworkWireless(); + if(isWireless == -1) { + logger.warn("unable to determine if the user is on wireless or not for network test. skipping prompt.") + } + else if(isWireless == 1) { + context.JK.Banner.showAlert({buttons: [ + {name: 'RUN NETWORK TEST ANYWAY', click: function() {startNetworkTest(false)}}, + {name: 'CANCEL', click: function() {}}], + html: "

    It appears that your computer is connected to your network using WiFi.

    " + + "

    We strongly advise against running the JamKazam application on a WiFi connection. " + + "We recommend using a wired Ethernet connection from your computer to your router. " + + "A WiFi connection is likely to cause significant issues in both latency and audio quality.

    "}) + return false; + } + } + isScoring = true; numClientsToTest = STARTING_NUM_CLIENTS; renderStartTest(); @@ -246,7 +268,7 @@ else { numClientsToTest++; logger.debug("increasing number of clients to " + numClientsToTest); - setTimeout(attemptTestPass, 1); + setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm } } else { @@ -265,7 +287,7 @@ else { numClientsToTest--; logger.debug("reducing number of clients to " + numClientsToTest); - setTimeout(attemptTestPass, 1); + setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm } } } @@ -356,9 +378,19 @@ return testedSuccessfully; } + function configureStartButton() { + if(isScoring) { + $startNetworkTestBtn.text('NETWORK TEST RUNNING...').removeClass('button-orange').addClass('button-grey') + } + else { + $startNetworkTestBtn.text('START NETWORK TEST').removeClass('button-grey').addClass('button-orange'); + } + + } function updateControlsState() { initializeNextButtonState(); initializeBackButtonState(); + configureStartButton(); } function initializeNextButtonState() { @@ -376,7 +408,6 @@ function beforeShow() { reset(); - updateControlsState(); } function initialize(_$step) { diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js index 3b79d38ba..ba8260460 100644 --- a/web/app/assets/javascripts/gear/step_select_gear.js +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -7,6 +7,8 @@ var ASSIGNMENT = context.JK.ASSIGNMENT; var VOICE_CHAT = context.JK.VOICE_CHAT; + var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR; + var self = null; var $step = null; var logger = context.JK.logger; @@ -58,110 +60,13 @@ var validLatencyScore = false; var validIOScore = false; var lastLatencyScore = null; - var lastIOScore = null; + var ioScore = null; + var latencyScore = null; - var audioDeviceBehavior = { - MacOSX_builtin: { - display: 'MacOSX Built-In', - videoURL: undefined, - showKnobs: false, - showASIO: false - }, - MacOSX_interface: { - display: 'MacOSX external interface', - videoURL: undefined, - showKnobs: false, - showASIO: false - }, - Win32_wdm: { - display: 'Windows WDM', - videoURL: undefined, - showKnobs: true, - showASIO: false - }, - Win32_asio: { - display: 'Windows ASIO', - videoURL: undefined, - showKnobs: true, - showASIO: false - }, - Win32_asio4all: { - display: 'Windows ASIO4ALL', - videoURL: undefined, - showKnobs: false, - showASIO: true - }, - Linux: { - display: 'Linux', - videoURL: undefined, - showKnobs: true, - showASIO: false - } - } - // should return one of: - // * MacOSX_builtin - // * MACOSX_interface - // * Win32_wdm - // * Win32_asio - // * Win32_asio4all - // * Linux - function determineDeviceType(deviceId, displayName) { - if (operatingSystem == "MacOSX") { - if (displayName.toLowerCase().trim() == "built-in") { - return "MacOSX_builtin"; - } - else { - return "MacOSX_interface"; - } - } - else if (operatingSystem == "Win32") { - if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) { - return "Win32_wdm"; - } - else if (displayName.toLowerCase().indexOf("asio4all") > -1) { - return "Win32_asio4all" - } - else { - return "Win32_asio"; - } - } - else { - return "Linux"; - } - } - function loadDevices() { - - var oldDevices = context.jamClient.FTUEGetDevices(false); - var devices = context.jamClient.FTUEGetAudioDevices(); - logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices)); - - var loadedDevices = {}; - - // augment these devices by determining their type - context._.each(devices.devices, function (device) { - - if (device.name == "JamKazam Virtual Monitor") { - return; - } - - var deviceInfo = {}; - - deviceInfo.id = device.guid; - deviceInfo.type = determineDeviceType(device.guid, device.display_name); - logger.debug("deviceInfo.type: " + deviceInfo.type) - deviceInfo.displayType = audioDeviceBehavior[deviceInfo.type].display; - deviceInfo.displayName = device.display_name; - - loadedDevices[device.guid] = deviceInfo; - - logger.debug("loaded device: ", deviceInfo); - }) - - deviceInformation = loadedDevices; - - logger.debug(context.JK.dlen(deviceInformation) + " devices loaded.", deviceInformation); + function isGoodFtue() { + return validLatencyScore && validIOScore; } // returns a deviceInfo hash for the device matching the deviceId, or undefined. @@ -190,19 +95,19 @@ } function setFramesize(value) { - $frameSize.val(value); + context.JK.dropdown($frameSize.val(value).easyDropDown('select', value.toString())) } function setBufferIn(value) { - $bufferIn.val(value) + context.JK.dropdown($bufferIn.val(value).easyDropDown('select', value.toString())); } function setBufferOut(value) { - $bufferOut.val(value) + context.JK.dropdown($bufferOut.val(value).easyDropDown('select', value.toString())) } function initializeNextButtonState() { - $dialog.setNextState(validLatencyScore && validIOScore); + $dialog.setNextState(isGoodFtue()); } function initializeBackButtonState() { @@ -213,7 +118,9 @@ var optionsHtml = ''; optionsHtml = ''; context._.each(deviceInformation, function (deviceInfo, deviceId) { - optionsHtml += ''; + if(deviceInfo.inputCount > 0) { + optionsHtml += ''; + } }); $audioInput.html(optionsHtml); context.JK.dropdown($audioInput); @@ -226,7 +133,9 @@ var optionsHtml = ''; optionsHtml = ''; context._.each(deviceInformation, function (deviceInfo, deviceId) { - optionsHtml += ''; + if(deviceInfo.outputCount > 0) { + optionsHtml += ''; + } }); $audioOutput.html(optionsHtml); context.JK.dropdown($audioOutput); @@ -258,18 +167,27 @@ // also, arguably convenient function autoSelectMinimumValidChannels() { + var audioInputDeviceId = selectedAudioInput(); + var audioOutputDeviceId = selectedAudioOutput(); + var $allInputs = $inputChannels.find('input[type="checkbox"]'); if ($allInputs.length == 0) { // ERROR: not enough channels - context.JK.Banner.showAlert('To be a valid input audio device, the device must have at least 1 input channel.'); + 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}); + } // ERROR: not enough channels - context.JK.Banner.showAlert('To be a valid output audio device, the device must have at least 2 output channels.'); + //context.JK.Banner.showAlert('To be a valid output audio device, the device must have at least 2 output channels.'); return false; } @@ -474,6 +392,8 @@ $resultsText.removeAttr('io-rate-score'); $resultsText.removeAttr('scored'); $unknownText.hide(); + $ioScoreSection.removeClass('good acceptable bad unknown starting skip'); + $latencyScoreSection.removeClass('good acceptable bad unknown starting') } function renderLatencyScore(latencyValue, latencyClass) { @@ -529,7 +449,7 @@ if (latencyValue <= 10) { latencyClass = "good"; validLatency = true; - } else if (latencyValue <= 20) { + } else if (latencyValue <= gon.ftue_maximum_gear_latency) { latencyClass = "acceptable"; validLatency = true; } else { @@ -608,7 +528,7 @@ context.JK.Banner.showAlert('You must first choose an Audio Input Device so that we can determine which video to show you.'); } else { - var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; + var videoURL = AUDIO_DEVICE_BEHAVIOR[audioDevice.type].videoURL; if (videoURL) { $(this).attr('href', videoURL); @@ -629,7 +549,7 @@ throw "this button should be hidden"; } else { - var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; + var videoURL = AUDIO_DEVICE_BEHAVIOR[audioDevice.type].videoURL; if (videoURL) { $(this).attr('href', videoURL); @@ -644,6 +564,13 @@ }); } + function invalidateScore() { + validLatencyScore = false; + validIOScore = false; + resetScoreReport(); + initializeNextButtonState(); + } + function initializeASIOButtons() { $asioInputControlBtn.unbind('click').click(function () { context.jamClient.FTUEOpenControlPanel(selectedAudioInput()); @@ -655,21 +582,59 @@ function initializeKnobs() { $frameSize.unbind('change').change(function () { + logger.debug("frameize changed: " + selectedFramesize()); + context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['bottom']}); updateDefaultBuffers(); jamClient.FTUESetFrameSize(selectedFramesize()); + invalidateScore(); }); $bufferIn.unbind('change').change(function () { + logger.debug("buffer-in changed: " + selectedBufferIn()); + context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['bottom']}); jamClient.FTUESetInputLatency(selectedBufferIn()); + invalidateScore(); }); $bufferOut.unbind('change').change(function () { + logger.debug("buffer-out changed: " + selectedBufferOut()); + context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['bottom']}); jamClient.FTUESetOutputLatency(selectedBufferOut()); + invalidateScore(); }); } + function ftueSummary() { + return { + os: operatingSystem, + version: context.jamClient.ClientUpdateVersion(), + success: isGoodFtue(), + score: { + validLatencyScore: validLatencyScore, + validIOScore: validIOScore, + latencyScore: latencyScore, + ioScore : ioScore, + }, + audioParameters: { + frameSize: selectedFramesize(), + bufferIn: selectedBufferIn(), + bufferOut: selectedBufferOut(), + }, + devices: deviceInformation, + selectedDevice: selectedDeviceInfo + } + } + function postDiagnostic() { - // TODO + rest.createDiagnostic({ + type: 'GEAR_SELECTION', + data: { + logs: logger.logCache, + client_type: context.JK.clientType(), + client_id: + context.JK.JamServer.clientID, + summary:ftueSummary()} + }); } function initializeResync() { @@ -692,7 +657,7 @@ $ioCountdownSecs.text(secondsLeft); } - // sets currentlySelectedDeviceInfo, which contains id, behavior, and info for input and output device + // sets selectedDeviceInfo, which contains id, behavior, and info for input and output device function cacheCurrentAudioInfo() { var audioInputDeviceId = selectedAudioInput(); @@ -717,27 +682,12 @@ return false; } - var input = findDevice(audioInputDeviceId); - var output = findDevice(audioOutputDeviceId); - - var inputBehavior = audioDeviceBehavior[input.type]; - var outputBehavior = audioDeviceBehavior[output.type]; - - lastSelectedDeviceInfo = selectedDeviceInfo; shownOutputProdOnce = false; shownInputProdOnce = false; - selectedDeviceInfo = { - input: { - id: audioInputDeviceId, - info: input, - behavior: inputBehavior - }, - output: { - id: audioOutputDeviceId, - info: output, - behavior: outputBehavior - } - } + + lastSelectedDeviceInfo = selectedDeviceInfo; + + selectedDeviceInfo = context.JK.selectedDeviceInfo(audioInputDeviceId, audioOutputDeviceId, deviceInformation) // prod the user to watch the video if the input or output changes type @@ -785,15 +735,15 @@ } function isInputAudioTypeDifferentFromLastTime() { - return lastSelectedDeviceInfo && (lastSelectedDeviceInfo.input.type != selectedDeviceInfo.input.type); + return lastSelectedDeviceInfo && (lastSelectedDeviceInfo.input.info.type != selectedDeviceInfo.input.info.type); } function isOutputAudioTypeDifferentFromLastTime() { - return lastSelectedDeviceInfo && isInputOutputDifferentTypes() && (lastSelectedDeviceInfo.output.type != selectedDeviceInfo.output.type) + return lastSelectedDeviceInfo && isInputOutputDifferentTypes() && (lastSelectedDeviceInfo.output.info.type != selectedDeviceInfo.output.info.type) } function isInputOutputDifferentTypes() { - return selectedDeviceInfo.input.type != selectedDeviceInfo.output.type; + return selectedDeviceInfo.input.info.type != selectedDeviceInfo.output.info.type; } function updateDialogForCurrentDevices() { @@ -811,34 +761,28 @@ $watchVideoInput.removeClass('audio-output-showing').find('.video-type').hide(); } + logger.debug("inputBehavior, outputbehavior", inputBehavior, outputBehavior) + // handle framesize/buffers if (inputBehavior && (inputBehavior.showKnobs || outputBehavior.showKnobs)) { - $knobs.css('visibility', 'visible') + $knobs.show(); } else { - $knobs.css('visibility', 'hidden') + $knobs.hide(); } // handle ASIO if (inputBehavior) { - if (inputBehavior.showASIO && !outputBehavior.showASIO) { - // show single ASIO button + if (inputBehavior.showASIO) { $asioInputControlBtn.show(); - $asioOutputControlBtn.hide(); } - else if (!inputBehavior.showASIO && outputBehavior.showASIO) { - // show single ASIO button - $asioInputControlBtn.show(); - $asioOutputControlBtn.hide(); + else { + $asioInputControlBtn.hide(); } - else if (inputBehavior.showASIO && outputBehavior.showASIO) { - // show two ASIO buttons - $asioInputControlBtn.show(); + if(outputBehavior.showASIO && (selectedDeviceInfo.input.id != selectedDeviceInfo.output.id)) { $asioOutputControlBtn.show(); } else { - // show no ASIO buttons - $asioInputControlBtn.hide(); $asioOutputControlBtn.hide(); } } @@ -862,7 +806,7 @@ } function updateDefaultFrameSize() { - if(selectedDeviceInfo.input.type == 'Win32_wdm' || selectedDeviceInfo.output.type == 'Win32_wdm') { + if(selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm') { setFramesize('10'); } @@ -872,23 +816,27 @@ function updateDefaultBuffers() { // handle specific framesize settings - if(selectedDeviceInfo.input.type == 'Win32_wdm' || selectedDeviceInfo.output.type == 'Win32_wdm') { + if(selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm') { var framesize = selectedFramesize(); if(framesize == 2.5) { + logger.debug("setting default buffers to 1/1"); setBufferIn('1'); setBufferOut('1'); } else if(framesize == 5) { + logger.debug("setting default buffers to 3/2"); setBufferIn('3'); setBufferOut('2'); } else { + logger.debug("setting default buffers to 6/5"); setBufferIn('6'); setBufferOut('5'); } } else { + logger.debug("setting default buffers to 0/0"); setBufferIn(0); setBufferOut(0); } @@ -929,16 +877,30 @@ // now base the overall IO score based on both values. renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass); - // lie for now until IO questions finalize - validIOScore = true; + if(aggregrateIOClass == "bad") { + validIOScore = false; + } + else { + validIOScore = true; + } - onSuccessfulScore(); + if(isGoodFtue()) { + onSuccessfulScore(); + } + else { + onFailedScore(); + } renderScoringStopped(); + postDiagnostic(); + } + + function onFailedScore() { + rest.userCertifiedGear({success: false}); } function onSuccessfulScore() { - + rest.userCertifiedGear({success: true}); } // refocused affects how IO testing occurs. @@ -950,10 +912,13 @@ scoring = true; initializeBackButtonState(); validLatencyScore = false; + latencyScore = null; if(!refocused) { // don't reset a valid IO score on refocus validIOScore = false; + ioScore = null; } + renderScoringStarted(); // this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in @@ -962,7 +927,7 @@ jamClient.FTUESave(false); var latency = jamClient.FTUEGetExpectedLatency(); - lastLatencyScore = latency; + latencyScore = latency; // prod user to watch video if the previous type and new type changed if(!shownInputProdOnce && isInputAudioTypeDifferentFromLastTime()) { @@ -987,7 +952,7 @@ // reuse valid IO score if this is on refocus if(refocused && validIOScore) { renderIOScore(null, null, null, 'starting', 'starting', 'starting'); - processIOScore(lastIOScore); + processIOScore(ioScore); } else { renderIOScore(null, null, null, 'starting', 'starting', 'starting'); @@ -1009,15 +974,17 @@ renderIOScoringStopped(); logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in") var io = context.jamClient.FTUEGetIoPerfData(); - lastIOScore = io; + ioScore = io; processIOScore(io); } }, 1000); } } else { + onFailedScore(); renderIOScore(null, null, null, 'skip', 'skip', 'skip'); renderScoringStopped(); + postDiagnostic(); } }, 250); } @@ -1055,12 +1022,12 @@ return false; } else { - //XXX rename profile + context.jamClient.FTUESetMusicProfileName(context.JK.createProfileName(selectedDeviceInfo)); context.jamClient.FTUESave(true); + // XXX what happens on name clash? context.JK.GA.trackAudioTestCompletion(context.JK.detectOS()); return true; } - } function onFocus() { @@ -1072,7 +1039,8 @@ } function beforeShow() { - loadDevices(); + deviceInformation = context.JK.loadDeviceInfo(); + resetState(); initializeFormElements(); initializeNextButtonState(); initializeWatchVideo(); @@ -1082,8 +1050,13 @@ $(window).on('focus', onFocus); } + function resetState() { + invalidateScore(); + clearInputPorts(); + clearOutputPorts(); + resetFrameBuffers(); + } function beforeHide() { - console.log("beforeHide:"); $(window).off('focus', onFocus); } diff --git a/web/app/assets/javascripts/gear/step_success.js b/web/app/assets/javascripts/gear/step_success.js index ab1656eb8..5a1f03fc4 100644 --- a/web/app/assets/javascripts/gear/step_success.js +++ b/web/app/assets/javascripts/gear/step_success.js @@ -3,7 +3,7 @@ "use strict"; context.JK = context.JK || {}; - context.JK.StepSuccess = function (app) { + context.JK.StepSuccess = function (app, $dialog) { var $step = null; diff --git a/web/app/assets/javascripts/gear/wizard.js b/web/app/assets/javascripts/gear/wizard.js index 2a6fea575..88e53a630 100644 --- a/web/app/assets/javascripts/gear/wizard.js +++ b/web/app/assets/javascripts/gear/wizard.js @@ -26,12 +26,13 @@ } function beforeHideStep() { - if(!previousStep) {return} + var currentStep = getCurrentStep(); + if(currentStep === null) return; - var stepInfo = STEPS[previousStep]; + var stepInfo = STEPS[currentStep]; if (!stepInfo) { - throw "unknown step: " + previousStep; + throw "unknown step: " + currentStep; } if(stepInfo.beforeHide) { @@ -82,7 +83,7 @@ $currentWizardStep = $nextWizardStep; - context.JK.GA.virtualPageView(location.pathname + location.search + location.hash, $currentWizardStep.attr('dialog-title')); + context.JK.GA.virtualPageView(location.pathname + location.search + location.hash + '/d1=' + step, $currentWizardStep.attr('dialog-title')); $self.triggerHandler('step_changed', {step:step}); @@ -131,11 +132,12 @@ // called by owner whenever function onCloseDialog() { - beforeHideStep($currentWizardStep); + beforeHideStep(); } function onBeforeShow(args) { + $currentWizardStep = null; previousStep = null; step = args != null ? args.d1 : 0; @@ -177,7 +179,13 @@ } function getCurrentStep() { - return step; + if($currentWizardStep) { + return parseInt($currentWizardStep.attr('layout-wizard-step')); + } + else { + return null; + } + } function getCurrentWizardStep() { diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 49fbd0aae..9f32fb61d 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -104,4 +104,43 @@ context.JK.entityToPrintable = { music_session: "music session" } + + context.JK.AUDIO_DEVICE_BEHAVIOR = { + MacOSX_builtin: { + display: 'MacOSX Built-In', + videoURL: undefined, + showKnobs: false, + showASIO: false + }, + MacOSX_interface: { + display: 'MacOSX external interface', + videoURL: undefined, + showKnobs: false, + showASIO: false + }, + Win32_wdm: { + display: 'Windows WDM', + videoURL: undefined, + showKnobs: true, + showASIO: false + }, + Win32_asio: { + display: 'Windows ASIO', + videoURL: undefined, + showKnobs: false, + showASIO: true + }, + Win32_asio4all: { + display: 'Windows ASIO4ALL', + videoURL: undefined, + showKnobs: false, + showASIO: true + }, + Linux: { + display: 'Linux', + videoURL: undefined, + showKnobs: true, + showASIO: false + } + } })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jquery.instrumentSelector.js b/web/app/assets/javascripts/jquery.instrumentSelector.js index 8f054800a..eca325fc1 100644 --- a/web/app/assets/javascripts/jquery.instrumentSelector.js +++ b/web/app/assets/javascripts/jquery.instrumentSelector.js @@ -27,6 +27,7 @@ function close() { $currentInstrument.btOff(); + $currentInstrument.focus(); } function onInstrumentSelected() { diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 526daa62f..60a87ee4d 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -677,7 +677,7 @@ } function startNewFtue() { - var step = 0; + // var step = 0; //setWizardStep(step); //wizardShowFunctions[step](); showDialog('gear-wizard'); diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 57b0a5c74..f8c23c374 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -7,6 +7,7 @@ context.JK = context.JK || {}; var logger = context.JK.logger; + var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR; var days = new Array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"); @@ -123,8 +124,11 @@ * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips */ context.JK.prodBubble = function($element, templateName, data, options) { + if(!options) options = {}; options['trigger'] = 'none'; options['clickAnywhereToClose'] = false + if(!options['duration']) options['duration'] = 4000; + var existingTimer = $element.data("prodTimer"); if(existingTimer) { clearTimeout(existingTimer); @@ -137,7 +141,7 @@ $element.data("prodTimer", setTimeout(function() { $element.data("prodTimer", null); $element.btOff(); - }, 4000)); + }, options['duration'])); } /** * Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) @@ -564,6 +568,112 @@ return keys; }; + context.JK.createProfileName = function(deviceInfo, chatName) { + var isSameInOut = deviceInfo.input.id == deviceInfo.output.id; + + var name = null; + if(isSameInOut) { + name = "In/Out: " + deviceInfo.input.info.displayName; + } + else { + name = "In: " + deviceInfo.input.info.displayName + ", Out: " + deviceInfo.output.info.displayName + } + + logger.debug("creating profile name: " + name); + return name; + } + + context.JK.selectedDeviceInfo = function(audioInputDeviceId, audioOutputDeviceId, deviceInformation) { + + if(!deviceInformation) { + deviceInformation = context.JK.loadDeviceInfo(); + } + + var input = deviceInformation[audioInputDeviceId]; + var output = deviceInformation[audioOutputDeviceId]; + + var inputBehavior = AUDIO_DEVICE_BEHAVIOR[input.type]; + var outputBehavior = AUDIO_DEVICE_BEHAVIOR[output.type]; + + return { + input: { + id: audioInputDeviceId, + info: input, + behavior: inputBehavior + }, + output: { + id: audioOutputDeviceId, + info: output, + behavior: outputBehavior + } + } + } + + context.JK.loadDeviceInfo = function() { + + var operatingSystem = context.JK.GetOSAsString(); + // should return one of: + // * MacOSX_builtin + // * MACOSX_interface + // * Win32_wdm + // * Win32_asio + // * Win32_asio4all + // * Linux + function determineDeviceType(deviceId, displayName) { + if (operatingSystem == "MacOSX") { + if (displayName.toLowerCase().trim() == "built-in") { + return "MacOSX_builtin"; + } + else { + return "MacOSX_interface"; + } + } + else if (operatingSystem == "Win32") { + if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) { + return "Win32_wdm"; + } + else if (displayName.toLowerCase().indexOf("asio4all") > -1) { + return "Win32_asio4all" + } + else { + return "Win32_asio"; + } + } + else { + return "Linux"; + } + } + + var devices = context.jamClient.FTUEGetAudioDevices(); + logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices)); + + var loadedDevices = {}; + + // augment these devices by determining their type + context._.each(devices.devices, function (device) { + + if (device.name == "JamKazam Virtual Monitor") { + return; + } + + var deviceInfo = {}; + + deviceInfo.id = device.guid; + deviceInfo.type = determineDeviceType(device.guid, device.display_name); + deviceInfo.displayType = AUDIO_DEVICE_BEHAVIOR[deviceInfo.type].display; + deviceInfo.displayName = device.display_name; + deviceInfo.inputCount = device.input_count; + deviceInfo.outputCount = device.output_count; + + loadedDevices[device.guid] = deviceInfo; + logger.debug("loaded device: ", deviceInfo); + }) + + logger.debug(context.JK.dlen(loadedDevices) + " devices loaded.", loadedDevices); + + return loadedDevices; + } + /** * Finds the first error associated with the field. * @param fieldName the name of the field diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index 35beafe14..1d5152b1d 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -237,6 +237,10 @@ .actions { text-align: center; } + + td span[data-current="true"] { + font-size:11px; + } } } diff --git a/web/app/assets/stylesheets/client/banner.css.scss b/web/app/assets/stylesheets/client/banner.css.scss index c4d34d1bf..897d09f0e 100644 --- a/web/app/assets/stylesheets/client/banner.css.scss +++ b/web/app/assets/stylesheets/client/banner.css.scss @@ -42,5 +42,10 @@ font-size: 0; content: " "; } + + p { + line-height:20px; + margin-bottom:10px; + } } diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index 05ec3e591..8ca9bc17e 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -103,6 +103,7 @@ font-size: 15px; font-weight: normal; margin-bottom: 6px; + white-space:nowrap; } .ftue-box { background-color: #222222; @@ -119,7 +120,7 @@ } &.instructions { - height: 248px !important; + height: 268px !important; line-height:16px; @include border_box_sizing; @@ -151,9 +152,18 @@ width:25%; } - .watch-video { + .watch-video.audio-input { margin-top:29px; + + &.audio-output-showing { + margin-top:10px; + } } + + .watch-video.audio-output { + margin-top:10px; + } + .select-audio-input-device { margin-bottom:20px; } @@ -172,7 +182,6 @@ margin-top:10px; } - .asio-settings-input-btn { display:none; } @@ -188,6 +197,7 @@ .frame-and-buffers { display:none; + margin-top:5px; } .framesize { @@ -196,11 +206,6 @@ width:30%; } - .buffers { - float:left; - width:60%; - } - .select-buffer-in { width:45%; } @@ -211,6 +216,16 @@ .buffers { + float:left; + width:60%; + + h2 { + margin-left:5px; + } + + .dropdown-container { + width:48px; + } .easydropdown-wrapper:nth-of-type(1) { left:5px; } @@ -218,7 +233,7 @@ left:35px; } .easydropdown, .easydropdown-wrapper { - width:15px; + width:30px; } } @@ -233,7 +248,7 @@ .ftue-box.results { - height: 248px !important; + height: 268px !important; padding:0; @include border_box_sizing; @@ -370,14 +385,18 @@ .wizard-step-content .wizard-step-column { width:25%; + &:nth-of-type(2) { + width:21%; + } + &:nth-of-type(3) { - width:40%; + width:45%; } &:nth-of-type(4) { - width:10%; + width:9%; } } @@ -387,7 +406,7 @@ .icon-instrument-select { padding:3px 0; // to combine 24 of .current-instrument + 3x on either side - margin:0 auto 15px; // 15 margin-bottom to match tracks on the left + margin:0 0 15px 25px; // 15 margin-bottom to match tracks on the left width:30px; } @@ -438,14 +457,25 @@ overflow:hidden; text-align:left; direction:rtl; - text-overflow:ellipses; &.ui-draggable-dragging{ margin-bottom:0; } &:hover { - color:white; + color:white; + font-weight: bold; } + + /** + &:hover { + color:white; + overflow:visible; + background-color:#333; + width: auto !important; + direction:ltr; + position:absolute; + line-height:19.5px; + }*/ } .track-target { @@ -490,11 +520,11 @@ &:nth-of-type(1) { float:left; - &:after { - float:left; + /**&:after { + float:right; content: ','; padding-right:3px; - } + }*/ } &:nth-of-type(2) { float:right; diff --git a/web/app/assets/stylesheets/easydropdown_jk.css.scss b/web/app/assets/stylesheets/easydropdown_jk.css.scss index ed51ce5a2..4b2ff52b6 100644 --- a/web/app/assets/stylesheets/easydropdown_jk.css.scss +++ b/web/app/assets/stylesheets/easydropdown_jk.css.scss @@ -32,6 +32,7 @@ body.jam { left: -1px; top: 100%; margin-top: -1px; + margin-left:1px; background: #fff; border: 1px solid #ccc; border-top: 1px solid #eee; diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb index f904fd7d5..65accb87c 100644 --- a/web/app/controllers/clients_controller.rb +++ b/web/app/controllers/clients_controller.rb @@ -55,6 +55,7 @@ class ClientsController < ApplicationController gon.ftue_packet_rate_treshold = Rails.application.config.ftue_packet_rate_treshold gon.ftue_network_test_duration = Rails.application.config.ftue_network_test_duration gon.ftue_network_test_max_clients = Rails.application.config.ftue_network_test_max_clients + gon.ftue_maximum_gear_latency = Rails.application.config.ftue_maximum_gear_latency # is this the native client or browser? @nativeClient = is_native_client? diff --git a/web/app/views/clients/_account.html.erb b/web/app/views/clients/_account.html.erb index b9baa122f..7f4acb741 100644 --- a/web/app/views/clients/_account.html.erb +++ b/web/app/views/clients/_account.html.erb @@ -25,12 +25,29 @@
    -
    + {% if (data.musician) { %} +
    - <% if current_user && current_user.musician? %> @@ -99,22 +116,7 @@

    -
    - - - - - -
    - UPDATE -
    -
    - <% end %> + {% } %} diff --git a/web/app/views/clients/_account_audio_profile.html.erb b/web/app/views/clients/_account_audio_profile.html.erb index 57588f1d7..72570a67f 100644 --- a/web/app/views/clients/_account_audio_profile.html.erb +++ b/web/app/views/clients/_account_audio_profile.html.erb @@ -27,7 +27,7 @@ NAME - DELETE + ACTIONS @@ -44,11 +44,13 @@ + + + + + + \ No newline at end of file diff --git a/web/app/views/clients/_sidebar.html.erb b/web/app/views/clients/_sidebar.html.erb index 8c79172f6..5a48671e6 100644 --- a/web/app/views/clients/_sidebar.html.erb +++ b/web/app/views/clients/_sidebar.html.erb @@ -102,7 +102,7 @@
    - Chat is available when session is connected. + Chat is available when in session.
    diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 81274aabd..56fd3cd0a 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -34,11 +34,11 @@ %a.button-orange.watch-video.audio-input{href:'#', rel:'external'} WATCH VIDEO %br - %span.video-type (for input device) + %span.video-type (FOR INPUT DEVICE) %a.button-orange.watch-video.audio-output{href:'#', rel:'external'} WATCH VIDEO %br - %span.video-type (for output device) + %span.video-type (FOR OUTPUT DEVICE) .wizard-step-column %h2 Audio Input Device %select.w100.select-audio-input-device @@ -62,7 +62,7 @@ %option{val:'5'} 5 %option{val:'10'} 10 .buffers - %h2 Buffers In/Out + %h2 Buffer In/Out %select.select-buffer-in %option{val:'0'} 0 %option{val:'1'} 1 @@ -124,7 +124,7 @@ .unknown-text %div We cannot accurately predict the latency of your audio gear. To proceed, you must run an audio loopback test. Click button below to do this. %div.loopback-button-holder - %a.button-orange.loopback-test{href:'#'} Run Loopback Test + %a.button-orange.loopback-test{href:'#'} RUN LOOPBACK TEST .clearall .wizard-step{ 'layout-wizard-step' => "2", 'dialog-title' => "Configure Tracks", 'dialog-purpose' => "ConfigureTracks" } @@ -318,6 +318,6 @@ %script{type: 'text/template', id: 'template-chat-input'} .chat-input - %input{type:"radio", name: "chat-device", 'data-channel-id' => '{{data.id}}'} + %input{type:"radio", name: "chat-device", 'data-channel-id' => '{{data.id}}', 'data-channel-name' => '{{data.name}}'} %p = '{{data.name}}' diff --git a/web/config/application.rb b/web/config/application.rb index 1654b9fb0..ebc2bb375 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -241,5 +241,7 @@ if defined?(Bundler) config.ftue_network_test_duration = 10 # max number of people to test config.ftue_network_test_max_clients = 8 + # the maximum amount of allowable latency + config.ftue_maximum_gear_latency = 20 end end diff --git a/web/spec/features/chat_message_spec.rb b/web/spec/features/chat_message_spec.rb index baaaf18ad..248860d41 100644 --- a/web/spec/features/chat_message_spec.rb +++ b/web/spec/features/chat_message_spec.rb @@ -25,7 +25,7 @@ describe "Chat Message", :js => true, :type => :feature, :capybara_feature => tr sign_in_poltergeist(user1) find("[layout-id=\"panelChat\"] .panel-header").trigger(:click) - find(".chat-status", text: 'Chat is available when session is connected.') + find(".chat-status", text: 'Chat is available when in session.') end end diff --git a/web/spec/features/in_session_spec.rb b/web/spec/features/in_session_spec.rb index 8398837df..2e57bacb5 100644 --- a/web/spec/features/in_session_spec.rb +++ b/web/spec/features/in_session_spec.rb @@ -55,7 +55,7 @@ describe "In a Session", :js => true, :type => :feature, :capybara_feature => tr find('#btn-leave-session-test').trigger(:click) wait_for_ajax - expect(page).to have_selector('h1', text: 'audio gear settings') + expect(page).to have_selector('h1', text: 'audio gear setup') end leave_music_session_sleep_delay diff --git a/web/spec/features/user_progression_spec.rb b/web/spec/features/user_progression_spec.rb index 68c1ee5bf..f4ff4393e 100644 --- a/web/spec/features/user_progression_spec.rb +++ b/web/spec/features/user_progression_spec.rb @@ -75,8 +75,10 @@ describe "User Progression", :js => true, :type => :feature, :capybara_feature sign_in_poltergeist user visit '/client#/account/audio' find("div.account-audio a[data-purpose='add-profile']").trigger(:click) - jk_select('ASIO4ALL v2 - ASIO', 'div[layout-wizard-step="0"] select.select-audio-device') - find('#btn-ftue-2-save').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) sleep 1 end From 5752f2d7dd6a47894e11f4e5dffb12b988cebc6a Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 29 May 2014 15:34:19 -0500 Subject: [PATCH 13/29] * fixing tests --- web/app/controllers/clients_controller.rb | 30 ------------------- web/app/helpers/client_helper.rb | 4 +++ .../spec/jam_websockets/router_spec.rb | 5 ++-- 3 files changed, 7 insertions(+), 32 deletions(-) diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb index 65accb87c..928715adf 100644 --- a/web/app/controllers/clients_controller.rb +++ b/web/app/controllers/clients_controller.rb @@ -20,7 +20,6 @@ class ClientsController < ApplicationController end def latency_tester - gon_properties render :layout => 'client' end @@ -38,33 +37,4 @@ class ClientsController < ApplicationController redirect_to client_url end end -<<<<<<< HEAD -======= - - private - - def gon_properties - # use gon to pass variables into javascript - gon.websocket_gateway_uri = Rails.application.config.websocket_gateway_uri - gon.websocket_gateway_trusted_uri = Rails.application.config.websocket_gateway_trusted_uri - gon.check_for_client_updates = Rails.application.config.check_for_client_updates - gon.fp_apikey = Rails.application.config.filepicker_rails.api_key - gon.fp_upload_dir = Rails.application.config.filepicker_upload_dir - gon.allow_force_native_client = Rails.application.config.allow_force_native_client - gon.ftue_io_wait_time = Rails.application.config.ftue_io_wait_time - gon.ftue_packet_rate_treshold = Rails.application.config.ftue_packet_rate_treshold - gon.ftue_network_test_duration = Rails.application.config.ftue_network_test_duration - gon.ftue_network_test_max_clients = Rails.application.config.ftue_network_test_max_clients - gon.ftue_maximum_gear_latency = Rails.application.config.ftue_maximum_gear_latency - - # is this the native client or browser? - @nativeClient = is_native_client? - - # let javascript have access to the server's opinion if this is a native client - gon.isNativeClient = @nativeClient - - gon.use_cached_session_scores = Rails.application.config.use_cached_session_scores - gon.allow_both_find_algos = Rails.application.config.allow_both_find_algos - end ->>>>>>> * wip end diff --git a/web/app/helpers/client_helper.rb b/web/app/helpers/client_helper.rb index f70eddec9..f8f52caf6 100644 --- a/web/app/helpers/client_helper.rb +++ b/web/app/helpers/client_helper.rb @@ -43,6 +43,10 @@ module ClientHelper gon.fp_upload_dir = Rails.application.config.filepicker_upload_dir gon.allow_force_native_client = Rails.application.config.allow_force_native_client gon.ftue_io_wait_time = Rails.application.config.ftue_io_wait_time + gon.ftue_packet_rate_treshold = Rails.application.config.ftue_packet_rate_treshold + gon.ftue_network_test_duration = Rails.application.config.ftue_network_test_duration + gon.ftue_network_test_max_clients = Rails.application.config.ftue_network_test_max_clients + gon.ftue_maximum_gear_latency = Rails.application.config.ftue_maximum_gear_latency # is this the native client or browser? @nativeClient = is_native_client? diff --git a/websocket-gateway/spec/jam_websockets/router_spec.rb b/websocket-gateway/spec/jam_websockets/router_spec.rb index 092cd9dbe..6d1743265 100644 --- a/websocket-gateway/spec/jam_websockets/router_spec.rb +++ b/websocket-gateway/spec/jam_websockets/router_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require 'thread' LoginClient = Class.new do - attr_accessor :onmsgblock, :onopenblock, :encode_json, :channel_id, :client_id, :user_id, :context + attr_accessor :onmsgblock, :onopenblock, :encode_json, :channel_id, :client_id, :user_id, :context, :trusted def initialize() @@ -149,8 +149,9 @@ describe Router do client.should_receive(:onerror) client.should_receive(:onmessage) client.should_receive(:encode_json=) + client.should_receive(:trusted=) - @router.new_client(client) + @router.new_client(client, false) done #end end From 6ad607cd245d6cc43b1cfa50eeb0d957f73a3818 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 29 May 2014 16:52:50 -0500 Subject: [PATCH 14/29] * fixing latency tester's page --- web/app/assets/javascripts/fakeJamClient.js | 4 +-- .../javascripts/gear/step_network_test.js | 18 +++++----- web/app/assets/javascripts/utils.js | 1 - web/app/views/clients/index.html.erb | 2 +- .../views/clients/latency_tester.html.haml | 2 +- web/config/environments/test.rb | 3 ++ web/spec/features/gear_wizard_spec.rb | 35 ++++++++++++++++++- 7 files changed, 50 insertions(+), 15 deletions(-) diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 368808ab5..0f580bca0 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -203,8 +203,8 @@ } function FTUESetInputMusicDevice() { } function FTUESetOutputMusicDevice() { } - function FTUEGetInputMusicDevice() { return null; } - function FTUEGetOutputMusicDevice() { return null; } + function FTUEGetInputMusicDevice() { return 'Built-in'; } + function FTUEGetOutputMusicDevice() { return 'Built-in'; } function FTUESetMusicInput() { dbg('FTUESetMusicInput'); } function FTUESetChatInput() { dbg('FTUESetChatInput'); } diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js index 40e7c3361..7cbc6fea1 100644 --- a/web/app/assets/javascripts/gear/step_network_test.js +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -78,21 +78,21 @@ function testFinished() { var attempt = getCurrentAttempt(); - if(!attempt.final) { - attempt.final = {reason : attempt.reason}; + if(!testSummary.final) { + testSummary.final = {reason : attempt.reason}; } - var reason = attempt.final.reason; + var reason = testSummary.final.reason; if(reason == "success") { renderStopTest(attempt.num_clients, "Your router and Internet service will support sessions of up to " + attempt.num_clients + " JamKazam musicians.") testedSuccessfully = true; - if(!attempt.final.num_clients) { - attempt.final.num_clients = attempt.num_clients; + if(!testSummary.final.num_clients) { + testSummary.final.num_clients = attempt.num_clients; } - context.JK.GA.trackNetworkTest(context.JK.detectOS(), attempt.final.num_clients); + context.JK.GA.trackNetworkTest(context.JK.detectOS(), testSummary.final.num_clients); context.jamClient.SetNetworkTestScore(attempt.num_clients); - if(attempt.final.num_clients == 2) { + if(testSummary.final.num_clients == 2) { $testResults.addClass('acceptable'); } else { @@ -162,6 +162,7 @@ attempt.server_client_id = serverClientId; attempt.received_progress = false; testSummary.attempts.push(attempt); + console.log("pushing attempt: " + testSummary.attempts.length) //context.jamClient.StopNetworkTest(''); @@ -169,7 +170,6 @@ updateProgress(0, false); - $currentScore.css context.jamClient.TestNetworkPktBwRate(serverClientId, TEST_SUCCESS_CALLBACK, TEST_TIMEOUT_CALLBACK, NETWORK_TEST_TYPES.PktTest400LowLatency, gon.ftue_network_test_duration, @@ -281,7 +281,7 @@ else if(numClientsToTest > STARTING_NUM_CLIENTS) { // this means we've gone up before... so don't go back down (i.e., creating a loop) attempt.reason = "success"; - attempt.final = { reason:'success', num_clients: numClientsToTest - 1 } + testSummary.final = { reason:'success', num_clients: numClientsToTest - 1 } testFinished(); } else { diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index f8c23c374..470f60e5e 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -666,7 +666,6 @@ deviceInfo.outputCount = device.output_count; loadedDevices[device.guid] = deviceInfo; - logger.debug("loaded device: ", deviceInfo); }) logger.debug(context.JK.dlen(loadedDevices) + " devices loaded.", loadedDevices); diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 513d49d75..74ca23a43 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -253,7 +253,7 @@ } JK.app = JK.JamKazam(); - var jamServer = new JK.JamServer(JK.app, function(event_type) {JK.app.activeElementEvent(event_type)}); + var jamServer = new JK.JamServer(JK.app, function(event_type) {}); jamServer.initialize(); JK.initJamClient(); diff --git a/web/app/views/clients/latency_tester.html.haml b/web/app/views/clients/latency_tester.html.haml index fd8d61bbb..27e632c41 100644 --- a/web/app/views/clients/latency_tester.html.haml +++ b/web/app/views/clients/latency_tester.html.haml @@ -49,7 +49,7 @@ } JK.app = JK.JamKazam(); - var jamServer = new JK.JamServer(JK.app); + var jamServer = new JK.JamServer(JK.app, function(event_type) {}); jamServer.initialize(); // If no jamClient (when not running in native client) diff --git a/web/config/environments/test.rb b/web/config/environments/test.rb index 40eeee247..cbaab4cb2 100644 --- a/web/config/environments/test.rb +++ b/web/config/environments/test.rb @@ -82,6 +82,9 @@ SampleApp::Application.configure do config.vanilla_url = '/forums' config.vanilla_login_url = '/forums/entry/jsconnect' + # very low waits to speed up automated tests config.ftue_io_wait_time = 1 + config.ftue_network_test_duration = 1 + config.ftue_network_test_max_clients = 5 end diff --git a/web/spec/features/gear_wizard_spec.rb b/web/spec/features/gear_wizard_spec.rb index 8f40dce6a..c94f7891e 100644 --- a/web/spec/features/gear_wizard_spec.rb +++ b/web/spec/features/gear_wizard_spec.rb @@ -11,7 +11,40 @@ describe "Gear Wizard", :js => true, :type => :feature, :capybara_feature => tru end it "success path" do - visit "/client#/home/gear-wizard" + visit '/client#/account/audio' + # step 1 - intro + find("div.account-audio a[data-purpose='add-profile']").trigger(:click) + find('.btn-next').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').trigger(:click) + + # step 3 - configure tracks + find('.ftue-step-title', text: 'Configure Tracks') + find('.btn-next.button-orange').trigger(:click) + + # step 4 - configure voice chat + find('.ftue-step-title', text: 'Configure Voice Chat') + find('.btn-next.button-orange').trigger(:click) + + # step 5 - configure direct monitoring + find('.ftue-step-title', text: 'Turn Off Direct Monitoring') + find('.btn-next.button-orange').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-grey.start-network-test').trigger(:click) + find('.button-orange.start-network-test') + find('.btn-next.button-orange').trigger(:click) + + # step 7 - Success + find('.ftue-step-title', text: 'Success!') + find('.btn-close.button-orange').trigger(:click) + end end From 00de83fc5c1cda037fff91db5c49ba68c3a54b04 Mon Sep 17 00:00:00 2001 From: Anthony Davis Date: Thu, 29 May 2014 20:01:02 -0500 Subject: [PATCH 15/29] Fixing VRFS-1701 - API requires countrycode in the value attrib --- web/app/views/users/new.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/views/users/new.html.erb b/web/app/views/users/new.html.erb index 13d101d5f..18218f7ee 100644 --- a/web/app/views/users/new.html.erb +++ b/web/app/views/users/new.html.erb @@ -40,7 +40,7 @@ <% @countriesx.each do |country| %> <% unless country[:countrycode].blank? %> - + <% end %> <% end %> From 7d246ef81784869bfc56cfcce9cc2f346cce26a4 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 29 May 2014 20:58:40 -0500 Subject: [PATCH 16/29] * fix reconnect and limit bands 3 test --- .../javascripts/gear/step_network_test.js | 17 +++++++++++++++++ .../assets/javascripts/gear/step_select_gear.js | 5 +++-- web/app/assets/javascripts/textMessageDialog.js | 2 ++ web/app/views/clients/index.html.erb | 2 +- web/spec/features/bands_spec.rb | 6 +++--- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js index 7cbc6fea1..220ccf068 100644 --- a/web/app/assets/javascripts/gear/step_network_test.js +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -34,6 +34,7 @@ var $currentScore = null; var $scoredClients = null; var $subscore = null; + var backendGuardTimeout = null; var serverClientId = ''; var isScoring = false; @@ -113,6 +114,10 @@ context.JK.alertSupportedNeeded("JamKazam is experiencing technical difficulties."); renderStopTest('', ''); } + else if(reason == 'backend_gone') { + context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties."); + renderStopTest('', ''); + } else if(reason == "invalid_response") { context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection. Reason=" + attempt.backend_data.reason + '.'); renderStopTest('', ''); @@ -151,6 +156,11 @@ return testSummary.attempts[testSummary.attempts.length - 1]; } + function backendTimedOut() { + testSummary.final = {reason: 'backend_gone'} + testFinished(); + } + function attemptTestPass() { @@ -170,6 +180,8 @@ updateProgress(0, false); + backendGuardTimeout = setTimeout(function(){backendTimedOut()}, 10000); + context.jamClient.TestNetworkPktBwRate(serverClientId, TEST_SUCCESS_CALLBACK, TEST_TIMEOUT_CALLBACK, NETWORK_TEST_TYPES.PktTest400LowLatency, gon.ftue_network_test_duration, @@ -410,6 +422,10 @@ reset(); } + function afterHide() { + + } + function initialize(_$step) { $step = _$step; @@ -427,6 +443,7 @@ } this.newSession = newSession; + this.afterHide = afterHide; this.beforeShow = beforeShow; this.initialize = initialize; diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js index ba8260460..9f1afd38c 100644 --- a/web/app/assets/javascripts/gear/step_select_gear.js +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -1056,7 +1056,8 @@ clearOutputPorts(); resetFrameBuffers(); } - function beforeHide() { + function afterHide() { + logger.debug("unregistering focus watch") $(window).off('focus', onFocus); } @@ -1098,7 +1099,7 @@ this.handleNext = handleNext; this.beforeShow = beforeShow; - this.beforeHide = beforeHide; + this.afterHide = afterHide; this.initialize = initialize; self = this; diff --git a/web/app/assets/javascripts/textMessageDialog.js b/web/app/assets/javascripts/textMessageDialog.js index 8099da112..a00a65671 100644 --- a/web/app/assets/javascripts/textMessageDialog.js +++ b/web/app/assets/javascripts/textMessageDialog.js @@ -201,11 +201,13 @@ } function renderNotConnected() { + console.log("RENDER NOT CONNECTED!!!!!!!!!") $interactionBlocker.addClass('active'); $disconnectedMsg.addClass('active'); } function renderConnected() { + console.log("RENDER CONNECTED!!!!!!!!!") $interactionBlocker.removeClass('active'); $disconnectedMsg.removeClass('active'); } diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 74ca23a43..513d49d75 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -253,7 +253,7 @@ } JK.app = JK.JamKazam(); - var jamServer = new JK.JamServer(JK.app, function(event_type) {}); + var jamServer = new JK.JamServer(JK.app, function(event_type) {JK.app.activeElementEvent(event_type)}); jamServer.initialize(); JK.initJamClient(); diff --git a/web/spec/features/bands_spec.rb b/web/spec/features/bands_spec.rb index 43183d082..e2e65b1b8 100644 --- a/web/spec/features/bands_spec.rb +++ b/web/spec/features/bands_spec.rb @@ -88,15 +88,15 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do end it "limits genres to 3" do + genres = Genre.limit(4) navigate_band_setup within('#band-setup-form') do fill_in 'band-name', with: "whatever" fill_in 'band-biography', with: "a good story" - all('#band-genres input[type="checkbox"]').each_with_index do |cb, i| - cb.trigger(:click) unless i > 3 + genres.each do |genre| + find("#band-genres input[value='#{genre.id}']").trigger(:click) end end - sleep 1 find('#btn-band-setup-next').trigger(:click) expect(page).to have_selector('#tdBandGenres .error-text li', text: "No more than 3 genres are allowed.") end From 4919b9277127b5c7477ee77a43adfcd944cb1084 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 29 May 2014 21:13:50 -0500 Subject: [PATCH 17/29] * adding backend guard to network test step --- web/app/assets/javascripts/gear/step_network_test.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js index 220ccf068..784d3809d 100644 --- a/web/app/assets/javascripts/gear/step_network_test.js +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -161,6 +161,12 @@ testFinished(); } + function clearBackendGuard() { + if(backendGuardTimeout) { + clearTimeout(backendGuardTimeout); + backendGuardTimeout = null; + } + } function attemptTestPass() { @@ -180,7 +186,7 @@ updateProgress(0, false); - backendGuardTimeout = setTimeout(function(){backendTimedOut()}, 10000); + backendGuardTimeout = setTimeout(function(){backendTimedOut()}, gon.ftue_network_test_duration + 1000); context.jamClient.TestNetworkPktBwRate(serverClientId, TEST_SUCCESS_CALLBACK, TEST_TIMEOUT_CALLBACK, NETWORK_TEST_TYPES.PktTest400LowLatency, @@ -269,6 +275,8 @@ } function networkTestSuccess(data) { + clearBackendGuard(); + var attempt = getCurrentAttempt(); function refineTest(up) { @@ -378,6 +386,8 @@ } function networkTestTimeout(data) { + clearBackendGuard(); + logger.warn("network timeout when testing latency test: " + data); var attempt = getCurrentAttempt(); From 5cd947729cc20e8f303f7ffbe80b104cf500fa17 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Thu, 29 May 2014 21:51:02 -0500 Subject: [PATCH 18/29] * don't track user-agent of 'monitor' --- web/app/assets/javascripts/gear/step_network_test.js | 1 - web/app/views/shared/_ga.html.erb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js index 784d3809d..34f6710ee 100644 --- a/web/app/assets/javascripts/gear/step_network_test.js +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -178,7 +178,6 @@ attempt.server_client_id = serverClientId; attempt.received_progress = false; testSummary.attempts.push(attempt); - console.log("pushing attempt: " + testSummary.attempts.length) //context.jamClient.StopNetworkTest(''); diff --git a/web/app/views/shared/_ga.html.erb b/web/app/views/shared/_ga.html.erb index e3957a4d5..f22ac3a63 100644 --- a/web/app/views/shared/_ga.html.erb +++ b/web/app/views/shared/_ga.html.erb @@ -1,4 +1,4 @@ -<% if current_user.nil? || !Rails.application.config.ga_suppress_admin || !current_user.admin? # remove admin users from GA %> +<% if request.user_agent != "monitor" && (current_user.nil? || !Rails.application.config.ga_suppress_admin || !current_user.admin?) # remove admin users from GA %>