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(); } }); }