(function (context, $) { "use strict"; context.JK = context.JK || {}; context.JK.GearTest = function (app) { var logger = context.JK.logger; var isAutomated = false; var drawUI = false; var scoring = false; var validLatencyScore = false; var validIOScore = false; var latencyScore = null; var ioScore = null; var lastSavedTime = new Date(); // this should be marked TRUE when the backend sends an invalid_audio_device alert var asynchronousInvalidDevice = false; var selectedDeviceInfo = null; var $scoreReport = null; var $ioHeader = null; var $latencyHeader = null; var $ioRate = null; var $ioRateScore = null; var $ioVar = null; var $ioVarScore = null; var $ioCountdown = null; var $ioCountdownSecs = null; var $latencyScore = null; var $resultsText = null; var $unknownText = null; var $loopbackCompleted = null; var $adjustGearSpeedCompleted = null; var $adjustGearForIoFail = null; var $ioScoreSection = null; var $latencyScoreSection = null; var $self = $(this); var GEAR_TEST_START = "gear_test.start"; var GEAR_TEST_IO_START = "gear_test.io_start"; var GEAR_TEST_IO_DONE = "gear_test.io_done"; var GEAR_TEST_LATENCY_START = "gear_test.latency_start"; var GEAR_TEST_LATENCY_DONE = "gear_test.latency_done"; var GEAR_TEST_DONE = "gear_test.done"; var GEAR_TEST_FAIL = "gear_test.fail"; var GEAR_TEST_IO_PROGRESS = "gear_test.io_progress"; var GEAR_TEST_INVALIDATED_ASYNC = "gear_test.async_invalidated"; // happens when backend alerts us device is invalid function isGoodFtue() { return validLatencyScore && validIOScore && !asynchronousInvalidDevice; } 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'; } // uncomment one to force a particular type of I/O failure // medianIOClass = "bad"; // stdIOClass = "bad" // take worst between median or std var ioClassToNumber = {bad: 2, acceptable: 1, good: 0} var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass; // now base the overall IO score based on both values. $self.triggerHandler(GEAR_TEST_IO_DONE, {std:std, median:median, io:io, aggregrateIOClass: aggregrateIOClass, medianIOClass : medianIOClass, stdIOClass: stdIOClass}) //renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass); if(aggregrateIOClass == "bad") { validIOScore = false; } else { validIOScore = true; } scoring = false; if(isGoodFtue()) { $self.triggerHandler(GEAR_TEST_DONE) } else { $self.triggerHandler(GEAR_TEST_FAIL, {reason:'io', ioTarget: medianIOClass, ioTargetScore: median, ioVariance: stdIOClass, ioVarianceScore: std}); } } function automaticScore() { logger.debug("automaticScore: calling FTUESave(false)"); lastSavedTime = new Date(); // save before and after FTUESave, because the event happens in a multithreaded way var result = context.jamClient.FTUESave(false); lastSavedTime = new Date(); if(result && result.error) { logger.debug("unable to FTUESave(false). reason=" + result.detail); context.JK.GearTest.testDeferred.reject(result); return false; } else { var latency = context.jamClient.FTUEGetExpectedLatency(); context.JK.GearTest.testDeferred.resolve(latency); } return true; } function loopbackScore() { var cbFunc = 'JK.loopbackLatencyCallback'; logger.debug("Registering loopback latency callback: " + cbFunc); context.jamClient.FTUERegisterLatencyCallback('JK.loopbackLatencyCallback'); var now = new Date(); logger.debug("Starting Latency Test..." + now); context.jamClient.FTUEStartLatency(); } // refocused affects how IO testing occurs. // on refocus=true: // * reuse IO score if it was good/acceptable // * rescore IO if it was bad or skipped from previous try function attemptScore(_selectedDeviceInfo, refocused) { if(scoring) { logger.debug("gear-test: already scoring"); return; } selectedDeviceInfo = _selectedDeviceInfo; scoring = true; asynchronousInvalidDevice = false; $self.triggerHandler(GEAR_TEST_START); $self.triggerHandler(GEAR_TEST_LATENCY_START); validLatencyScore = false; latencyScore = null; if(!refocused) { // don't reset a valid IO score on refocus validIOScore = false; ioScore = null; } // this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in setTimeout(function () { context.JK.GearTest.testDeferred = new $.Deferred(); if(isAutomated) { // use automated latency score mechanism automaticScore() } else { // use loopback score mechanism loopbackScore(); } context.JK.GearTest.testDeferred.done(function(latency) { latencyScore = latency; if(isAutomated) { // uncomment to do a manual loopback test //latency.latencyknown = false; } updateScoreReport(latency, refocused); // if there was a valid latency score, go on to the next step if (validLatencyScore) { $self.triggerHandler(GEAR_TEST_IO_START); // reuse valid IO score if this is on refocus if(refocused && validIOScore) { processIOScore(ioScore); } else { var testTimeSeconds = gon.ftue_io_wait_time; // allow time for IO to establish itself var startTime = testTimeSeconds / 2; // start measuring half way through the test, to get past IO oddities $self.trigger(GEAR_TEST_IO_PROGRESS, {countdown:testTimeSeconds, first:true}) var interval = setInterval(function () { testTimeSeconds -= 1; $self.trigger(GEAR_TEST_IO_PROGRESS, {countdown:testTimeSeconds, first:false}) if(testTimeSeconds == startTime) { logger.debug("Starting IO Perf Test starting at " + startTime + "s in") context.jamClient.FTUEStartIoPerfTest(); } if (testTimeSeconds == 0) { clearInterval(interval); logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in") var io = context.jamClient.FTUEGetIoPerfData(); ioScore = io; processIOScore(io); } }, 1000); } } else { scoring = false; $self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', latencyScore: latencyScore.latency}) } }) .fail(function(ftueSaveResult) { scoring = false; $self.triggerHandler(GEAR_TEST_FAIL, {reason:'invalid_configuration', data: ftueSaveResult}) }) }, 250); } function updateScoreReport(latencyResult, refocused) { var latencyClass = "neutral"; var latencyValue = null; var validLatency = false; if (latencyResult && latencyResult.latencyknown) { var latencyValue = latencyResult.latency; latencyValue = Math.round(latencyValue * 100) / 100; if (latencyValue <= 10) { latencyClass = "good"; validLatency = true; } else if (latencyValue <= gon.ftue_maximum_gear_latency) { latencyClass = "acceptable"; validLatency = true; } else { latencyClass = "bad"; } } else { latencyClass = 'unknown'; } // uncomment these two lines to fail test due to latency // latencyClass = "bad"; // validLatency = false; validLatencyScore = validLatency; if(refocused) { context.JK.prodBubble($scoreReport, 'refocus-rescore', {validIOScore: validIOScore}, {positions:['top', 'left']}); } $self.triggerHandler(GEAR_TEST_LATENCY_DONE, {latencyValue: latencyValue, latencyClass: latencyClass, refocused: refocused}); } // std deviation is the worst value between in/out // media is the worst value between in/out // io is the value returned by the backend, which has more info // ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad' function renderIOScore(std, median, ioData, ioClass, ioRateClass, ioVarClass) { $ioRateScore.text(median !== null ? median : ''); $ioVarScore.text(std !== null ? std : ''); if (ioClass && ioClass != "starting" && ioClass != "skip") { $ioRate.show(); $ioVar.show(); } if(ioClass == 'starting' || ioClass == 'skip') { $ioHeader.show(); } $resultsText.attr('io-rate-score', ioRateClass); $resultsText.attr('io-var-score', ioVarClass); $ioScoreSection.removeClass('good acceptable bad unknown starting skip') if (ioClass) { $ioScoreSection.addClass(ioClass); } // TODO: show help bubble of all data in IO data } function isScoring() { return scoring; } function isValidLatencyScore() { return validLatencyScore; } function isValidIOScore() { return validIOScore; } function getLatencyScore() { return latencyScore; } function getIOScore() { return ioScore; } function getLastSavedTime() { return lastSavedTime; } function onInvalidAudioDevice() { logger.debug("gear_test: onInvalidAudioDevice") asynchronousInvalidDevice = true; $self.triggerHandler(GEAR_TEST_INVALIDATED_ASYNC); context.JK.Banner.showAlert('Invalid Audio Device', 'It appears this audio device is not currently connected. Attach the device to your computer and restart the application, or select a different device.

If you think your gear is connected and working, this support article can help.') } function showLoopbackDone() { $loopbackCompleted.show(); } function showGearAdjustmentDone() { $adjustGearSpeedCompleted.show(); } function resetScoreReport() { $ioHeader.hide(); $latencyHeader.hide(); $ioRate.hide(); $ioRateScore.empty(); $ioVar.hide(); $ioVarScore.empty(); $latencyScore.empty(); $resultsText.removeAttr('latency-score'); $resultsText.removeAttr('io-var-score'); $resultsText.removeAttr('io-rate-score'); $resultsText.removeAttr('scored'); $unknownText.hide(); $loopbackCompleted.hide(); $adjustGearSpeedCompleted.hide(); $ioScoreSection.removeClass('good acceptable bad unknown starting skip'); $latencyScoreSection.removeClass('good acceptable bad unknown starting') } function invalidateScore() { validLatencyScore = false; validIOScore = false; asynchronousInvalidDevice = false; resetScoreReport(); } function renderLatencyScore(latencyValue, latencyClass) { // latencyValue == null implies starting condition if (latencyValue) { $latencyScore.text(latencyValue + ' ms'); } else { $latencyScore.text(''); } if(latencyClass == 'unknown') { $latencyScore.text('Unknown'); $unknownText.show(); } $latencyHeader.show(); $resultsText.attr('latency-score', latencyClass); $latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass); } function renderIOScoringStarted(secondsLeft) { $ioCountdownSecs.text(secondsLeft); $ioCountdown.show(); } function renderIOScoringStopped() { $ioCountdown.hide(); } function renderIOCountdown(secondsLeft) { $ioCountdownSecs.text(secondsLeft); } function uniqueDeviceName() { try { return selectedDeviceInfo.input.info.displayName + '(' + selectedDeviceInfo.input.behavior.shortName + ')' + '-' + selectedDeviceInfo.output.info.displayName + '(' + selectedDeviceInfo.output.behavior.shortName + ')' + '-' + context.JK.GetOSAsString(); } catch(e){ logger.error("unable to devise unique device name for stats: " + e.toString()); return "Unknown"; } } function handleUI($testResults) { if(!$testResults.is('.ftue-box.results')) { throw "GearTest != .ftue-box.results" } $scoreReport = $testResults; $ioHeader = $scoreReport.find('.io');; $latencyHeader = $scoreReport.find('.latency'); $ioRate = $scoreReport.find('.io-rate'); $ioRateScore = $scoreReport.find('.io-rate-score'); $ioVar = $scoreReport.find('.io-var'); $ioVarScore = $scoreReport.find('.io-var-score'); $ioScoreSection = $scoreReport.find('.io-score-section'); $latencyScore = $scoreReport.find('.latency-score'); $ioCountdown = $scoreReport.find('.io-countdown'); $ioCountdownSecs = $scoreReport.find('.io-countdown .secs'); $resultsText = $scoreReport.find('.results-text'); $unknownText = $scoreReport.find('.unknown-text'); $loopbackCompleted = $scoreReport.find('.loopback-completed') $adjustGearSpeedCompleted = $scoreReport.find('.adjust-gear-speed-completed'); $adjustGearForIoFail = $scoreReport.find(".adjust-gear-for-io-fail") $latencyScoreSection = $scoreReport.find('.latency-score-section'); function onGearTestStart(e, data) { renderIOScore(null, null, null, null, null, null); } function onGearTestIOStart(e, data) { renderIOScore(null, null, null, 'starting', 'starting', 'starting'); } function onGearTestLatencyStart(e, data) { resetScoreReport(); renderLatencyScore(null, 'starting'); } function onGearTestLatencyDone(e, data) { renderLatencyScore(data.latencyValue, data.latencyClass); } function onGearTestIOProgress(e, data) { if(data.first) { renderIOScoringStarted(data.countdown); } renderIOCountdown(data.countdown); if(data.countdown == 0) { renderIOScoringStopped(); } } function onGearTestIODone(e, data) { renderIOScore(data.std, data.median, data.io, data.aggregrateIOClass, data.medianIOClass, data.stdIOClass); } function onGearTestDone(e, data) { $resultsText.attr('scored', 'complete'); context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.pass, latencyScore); rest.userCertifiedGear({success: true, client_id: app.clientId, audio_latency: getLatencyScore().latency}); } function onGearTestFail(e, data) { $resultsText.attr('scored', 'complete'); if(data.reason == "latency") { renderIOScore(null, null, null, 'skip', 'skip', 'skip'); } rest.userCertifiedGear({success: false, client_id: app.clientId}); if(data.reason == "latency") { context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.latencyFail, data.latencyScore); } else if(data.reason = "io") { if(data.ioTarget == 'bad') { context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.ioTargetFail, data.ioTargetScore); } else { context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.ioVarianceFail, data.ioVarianceScore); } } else if(data.reason == 'invalid_configuration') { logger.error("invalid configuration returned by gearTest." + data.data.detail) } else { logger.error("unknown reason in onGearTestFail: " + data.reason) } } $self .on(GEAR_TEST_START, onGearTestStart) .on(GEAR_TEST_IO_START, onGearTestIOStart) .on(GEAR_TEST_LATENCY_START, onGearTestLatencyStart) .on(GEAR_TEST_LATENCY_DONE, onGearTestLatencyDone) .on(GEAR_TEST_IO_PROGRESS, onGearTestIOProgress) .on(GEAR_TEST_IO_DONE, onGearTestIODone) .on(GEAR_TEST_DONE, onGearTestDone) .on(GEAR_TEST_FAIL, onGearTestFail); } function initialize($testResults, automated, noUI) { isAutomated = automated; drawUI = !noUI; if(drawUI) { handleUI($testResults); } } // Latency Test Callback context.JK.loopbackLatencyCallback = function (latencyMS) { // Unregister callback: context.jamClient.FTUERegisterLatencyCallback(''); logger.debug("loopback test done: " + latencyMS); context.JK.GearTest.testDeferred.resolve({latency: latencyMS, latencyknown:true}) }; this.GEAR_TEST_START = GEAR_TEST_START; this.GEAR_TEST_IO_START = GEAR_TEST_IO_START; this.GEAR_TEST_IO_DONE = GEAR_TEST_IO_DONE; this.GEAR_TEST_LATENCY_START = GEAR_TEST_LATENCY_START; this.GEAR_TEST_LATENCY_DONE = GEAR_TEST_LATENCY_DONE; this.GEAR_TEST_DONE = GEAR_TEST_DONE; this.GEAR_TEST_FAIL = GEAR_TEST_FAIL; this.GEAR_TEST_IO_PROGRESS = GEAR_TEST_IO_PROGRESS; this.GEAR_TEST_INVALIDATED_ASYNC = GEAR_TEST_INVALIDATED_ASYNC; this.initialize = initialize; this.isScoring = isScoring; this.attemptScore = attemptScore; this.resetScoreReport = resetScoreReport; this.showLoopbackDone = showLoopbackDone; this.showGearAdjustmentDone = showGearAdjustmentDone; this.invalidateScore = invalidateScore; this.isValidLatencyScore = isValidLatencyScore; this.isValidIOScore = isValidIOScore; this.isGoodFtue = isGoodFtue; this.getLatencyScore = getLatencyScore; this.getIOScore = getIOScore; this.getLastSavedTime = getLastSavedTime; this.onInvalidAudioDevice = onInvalidAudioDevice; return this; } })(window, jQuery);