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