This commit is contained in:
Seth Call 2014-05-16 14:39:37 -05:00
parent ebe1278e5e
commit 05666f7927
32 changed files with 726 additions and 85 deletions

View File

@ -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

8
db/up/latency_tester.sql Normal file
View File

@ -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;

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

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

View File

@ -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 = $('<div class="track-instrument"></div>');
$root.instrumentSelector().attr('data-num', i);
$instrumentsHolder.append($root);
}
}

View File

@ -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;

View File

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

View File

@ -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('<img src="' + context.JK.getInstrumentIconMap24()[instrument_id].asset + '" />');
$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);

View File

@ -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('<img src="' + instrumentIconMap24[instrument_id].asset + '" />');
}
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 = $('<div class="hover-bubble help-bubble"></div>');
@ -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

View File

@ -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 {

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
object @latency_tester
extends "api_latency_testers/show"

View File

@ -0,0 +1,3 @@
object @latency_tester
attribute :id, :client_id

View File

@ -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

View File

@ -12,4 +12,8 @@
<script type="text/template" id="template-help-ftue-watch-video">
Be sure to watch the help video.
</script>
<script type="text/template" id="template-help-chat-not-enabled">
You must first chose this option in order to activate a chat input.
</script>

View File

@ -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}}'

View File

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

View File

@ -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

View File

@ -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