* VRFS-813 - done for this feature

This commit is contained in:
Seth Call 2013-11-16 04:35:40 +00:00
parent f4401fc36a
commit 9805775496
35 changed files with 793 additions and 303 deletions

View File

@ -1,9 +1,9 @@
-- so that columns can live on
-- so that rows can live on after session is over
ALTER TABLE recordings DROP CONSTRAINT "recordings_music_session_id_fkey";
-- unambiguous declartion that the recording is over or not
ALTER TABLE recordings ADD COLUMN is_done BOOLEAN DEFAULT FALSE;
--ALTER TABLE music_session ADD COLUMN is_recording BOOLEAN DEFAULT FALSE;
-- add name and description on claimed_recordings, which is the user's individual view of a recording
ALTER TABLE claimed_recordings ADD COLUMN description VARCHAR(8000);
ALTER TABLE claimed_recordings ADD COLUMN description_tsv tsvector;
ALTER TABLE claimed_recordings ADD COLUMN name_tsv tsvector;
@ -19,12 +19,9 @@ tsvector_update_trigger(name_tsv, 'public.jamenglish', name);
CREATE INDEX claimed_recordings_description_tsv_index ON claimed_recordings USING gin(description_tsv);
CREATE INDEX claimed_recordings_name_tsv_index ON claimed_recordings USING gin(name_tsv);
--ALTER TABLE recordings ADD COLUMN is_kept BOOLEAN NOT NULL DEFAULT false;
--ALTER TABLE recordings ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT true;
--ALTER TABLE recordings ADD COLUMN is_downloadable BOOLEAN NOT NULL DEFAULT true;
--ALTER TABLE recordings ADD COLUMN genre_id VARCHAR(64) NOT NULL REFERENCES genres(id);
-- copies of connection.client_id and track.id
ALTER TABLE recorded_tracks ADD COLUMN client_id VARCHAR(64) NOT NULL;
ALTER TABLE recorded_tracks ADD COLUMN track_id VARCHAR(64) NOT NULL;
-- so that server can correlate to client track
ALTER TABLE tracks ADD COLUMN client_track_id VARCHAR(64) NOT NULL;

View File

@ -362,6 +362,7 @@ SQL
t.instrument = instrument
t.connection = connection
t.sound = track["sound"]
t.client_track_id = track["client_track_id"]
t.save
connection.tracks << t
end

View File

@ -0,0 +1,21 @@
require 'json'
require 'resque'
module JamRuby
@queue = :audiomixer
class AudioMixer
def self.perform(manifest)
tmp = Dir::Tmpname.make_tmpname "/var/tmp/audiomixer/manifest-#{manifest['recordingId']}", nil
File.open(tmp,"w") do |f|
f.write(manifest.to_json)
end
system("tar zxvf some_big_tarball.tar.gz"))
end
end
end

View File

@ -72,7 +72,7 @@ module JamRuby
if as_musician
unless self.user.musician
errors.add(:as_musician, ValidationMesages::FAN_CAN_NOT_JOIN_AS_MUSICIAN)
errors.add(:as_musician, ValidationMessages::FAN_CAN_NOT_JOIN_AS_MUSICIAN)
return false
end

View File

@ -38,7 +38,68 @@ module JamRuby
return query
end
def self.save(id, connection_id, instrument_id, sound)
# this is a bit different from a normal track synchronization in that the client just sends up all tracks,
# ... some may already exist
def self.sync(clientId, tracks)
result = []
Track.transaction do
connection = Connection.find_by_client_id!(clientId)
if tracks.length == 0
connection.tracks.delete_all
else
connection_tracks = connection.tracks
# we will prune from this as we find matching tracks
to_delete = Set.new(connection_tracks)
to_add = Array.new(tracks)
connection_tracks.each do |connection_track|
tracks.each do |track|
if track[:id] == connection_track.id || track[:client_track_id] == connection_track.client_track_id;
to_delete.delete(connection_track)
to_add.delete(track)
# don't update connection_id or client_id; it's unknown what would happen if these changed mid-session
connection_track.instrument = Instrument.find(track[:instrument_id])
connection_track.sound = track[:sound]
connection_track.client_track_id = track[:client_track_id]
if connection_track.save
result.push(connection_track)
next
else
result = connection_track
raise ActiveRecord::Rollback
end
end
end
end
to_add.each do |track|
connection_track = Track.new
connection_track.connection = connection
connection_track.instrument = Instrument.find(track[:instrument_id])
connection_track.sound = track[:sound]
connection_track.client_track_id = track[:client_track_id]
if connection_track.save
result.push(connection_track)
else
result = connection_track
raise ActiveRecord::Rollback
end
end
to_delete.each do| delete_me |
delete_me.delete
end
end
end
result
end
def self.save(id, connection_id, instrument_id, sound, client_track_id)
if id.nil?
track = Track.new()
track.connection_id = connection_id
@ -54,6 +115,10 @@ module JamRuby
track.sound = sound
end
unless client_track_id.nil?
track.client_track_id = client_track_id
end
track.updated_at = Time.now.getutc
track.save
return track

View File

@ -114,7 +114,7 @@ module JamRuby
validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email
validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password?

View File

@ -90,7 +90,7 @@ FactoryGirl.define do
factory :track, :class => JamRuby::Track do
sound "mono"
sequence(:client_track_id) { |n| "client_track_id#{n}"}
end
factory :recorded_track, :class => JamRuby::RecordedTrack do

View File

@ -3,7 +3,7 @@ require 'spec_helper'
# these tests avoid the use of ActiveRecord and FactoryGirl to do blackbox, non test-instrumented tests
describe ConnectionManager do
TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono"}]
TRACKS = [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "some_client_track_id"}]
before do
@conn = PG::Connection.new(:dbname => SpecDb::TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost")

View File

@ -9,7 +9,7 @@ describe Mix do
@music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true)
@music_session.connections << @connection
@music_session.save
@recording = Recording.start(@music_session.id, @user)
@recording = Recording.start(@music_session, @user)
@recording.stop
@mix = Mix.schedule(@recording, "{}")
end

View File

@ -0,0 +1,94 @@
require 'spec_helper'
describe Track do
let (:connection) { FactoryGirl.create(:connection) }
let (:track) { FactoryGirl.create(:track, :connection => connection)}
let (:track2) { FactoryGirl.create(:track, :connection => connection)}
let (:track_hash) { {:client_track_id => 'client_guid', :sound => 'stereo', :instrument_id => 'drums'} }
before(:each) do
end
describe "sync" do
it "create one track" do
tracks = Track.sync(connection.client_id, [track_hash])
tracks.length.should == 1
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
track.sound = track_hash[:sound]
track.instrument.should == Instrument.find('drums')
end
it "create two tracks" do
tracks = Track.sync(connection.client_id, [track_hash, track_hash])
tracks.length.should == 2
track = tracks[0]
track.client_track_id.should == track_hash[:client_track_id]
track.sound = track_hash[:sound]
track.instrument.should == Instrument.find('drums')
track = tracks[1]
track.client_track_id.should == track_hash[:client_track_id]
track.sound = track_hash[:sound]
track.instrument.should == Instrument.find('drums')
end
it "delete only track" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [])
tracks.length.should == 0
end
it "delete one of two tracks using .id to correlate" do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
found.sound.should == 'mono'
found.client_track_id.should == 'client_guid_new'
end
it "delete one of two tracks using .client_track_id to correlate" do
track.id.should_not be_nil
track2.id.should_not be_nil
connection.tracks.length.should == 2
tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
found.sound.should == 'mono'
found.client_track_id.should == track.client_track_id
end
it "updates a single track using .id to correlate" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [{:id => track.id, :client_track_id => 'client_guid_new', :sound => 'mono', :instrument_id => 'drums'}])
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
found.sound.should == 'mono'
found.client_track_id.should == 'client_guid_new'
end
it "updates a single track using .client_track_id to correlate" do
track.id.should_not be_nil
connection.tracks.length.should == 1
tracks = Track.sync(connection.client_id, [{:client_track_id => track.client_track_id, :sound => 'mono', :instrument_id => 'drums'}])
tracks.length.should == 1
found = tracks[0]
found.id.should == track.id
found.sound.should == 'mono'
found.client_track_id.should == track.client_track_id
end
end
end

View File

@ -104,7 +104,9 @@
payload = message[messageType],
callbacks = server.dispatchTable[message.type];
logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload));
if(message.type != context.JK.MessageType.HEARTBEAT_ACK) {
logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload));
}
if (callbacks !== undefined) {
var len = callbacks.length;
@ -132,7 +134,9 @@
var jsMessage = JSON.stringify(message);
logger.log("server.send(" + jsMessage + ")");
if(message.type != context.JK.MessageType.HEARTBEAT) {
logger.log("server.send(" + jsMessage + ")");
}
if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) {
server.socket.send(jsMessage);
} else {

View File

@ -71,7 +71,6 @@
// set arrays
inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false);
console.log("inputUnassignedList: " + JSON.stringify(inputUnassignedList));
track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false);
}
@ -134,13 +133,16 @@
}
saveTrack();
app.layout.closeDialog('add-track');
}
function saveTrack() {
// TRACK 2 INPUTS
var trackId = null;
$("#add-track2-input > option").each(function() {
logger.debug("Saving track 2 input = " + this.value);
trackId = this.value;
context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.TRACK2);
});
@ -154,12 +156,52 @@
// UPDATE SERVER
logger.debug("Adding track with instrument " + instrumentText);
var data = {};
// use the first track's connection_id (not sure why we need this on the track data model)
logger.debug("myTracks[0].connection_id=" + myTracks[0].connection_id);
data.connection_id = myTracks[0].connection_id;
data.instrument_id = instrumentText;
data.sound = "stereo";
sessionModel.addTrack(sessionId, data);
context.jamClient.TrackSaveAssignments();
/**
setTimeout(function() {
var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 2);
// this is some ugly logic coming up, here's why:
// we need the id (guid) that the backend generated for the new track we just added
// to get it, we need to make sure 2 tracks come back, and then grab the track that
// is not the one we just added.
if(inputTracks.length != 2) {
var msg = "because we just added a track, there should be 2 available, but we found: " + inputTracks.length;
logger.error(msg);
alert(msg);
throw new Error(msg);
}
var client_track_id = null;
$.each(inputTracks, function(index, track) {
console.log("track: %o, myTrack: %o", track, myTracks[0]);
if(track.id != myTracks[0].id) {
client_track_id = track.id;
return false;
}
});
if(client_track_id == null)
{
var msg = "unable to find matching backend track for id: " + this.value;
logger.error(msg);
alert(msg);
throw new Error(msg);
}
// use the first track's connection_id (not sure why we need this on the track data model)
data.connection_id = myTracks[0].connection_id;
data.instrument_id = instrumentText;
data.sound = "stereo";
data.client_track_id = client_track_id;
sessionModel.addTrack(sessionId, data);
}, 1000);
*/
}
function validateSettings() {

View File

@ -232,7 +232,6 @@
// remove option 1 from voice chat type dropdown if no music (based on what's unused on the Music Audio tab) or chat inputs are available
if ($('#audio-inputs-unused > option').size() === 0 && chatOtherUnassignedList.length === 0 && chatOtherAssignedList.length === 0) {
logger.debug("Removing Option 1 from Voice Chat dropdown.");
$option1.remove();
}
else {
@ -494,13 +493,13 @@
function _initMusicTabData() {
inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false);
logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList));
//logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList));
track1AudioInputChannels = _loadList(ASSIGNMENT.TRACK1, true, false);
logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels));
//logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels));
track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false);
logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels));
//logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels));
outputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, false, false);
outputAssignedList = _loadList(ASSIGNMENT.OUTPUT, false, false);
@ -508,16 +507,16 @@
function _initVoiceChatTabData() {
chatUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false);
logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList));
//logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList));
chatAssignedList = _loadList(ASSIGNMENT.CHAT, true, false);
logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList));
//logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList));
chatOtherUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, true);
logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList));
//logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList));
chatOtherAssignedList = _loadList(ASSIGNMENT.CHAT, true, true);
logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList));
//logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList));
}
// TODO: copied in addTrack.js - refactor to common place
@ -592,7 +591,7 @@
app.layout.closeDialog('configure-audio');
// refresh Session screen
sessionModel.refreshCurrentSession();
//sessionModel.refreshCurrentSession();
}
function saveAudioSettings() {
@ -817,7 +816,6 @@
});
originalVoiceChat = context.jamClient.TrackGetChatEnable() ? VOICE_CHAT.CHAT : VOICE_CHAT.NO_CHAT;
logger.debug("originalVoiceChat=" + originalVoiceChat);
$('#voice-chat-type').val(originalVoiceChat);
@ -830,7 +828,6 @@
// remove option 1 from voice chat if none are available and not already assigned
if (inputUnassignedList.length === 0 && chatAssignedList.length === 0 && chatOtherAssignedList.length === 0 && chatOtherUnassignedList.length === 0) {
logger.debug("Removing Option 1 from Voice Chat dropdown.");
$option1.remove();
}
// add it if it doesn't exist
@ -846,7 +843,6 @@
events();
_init();
myTrackCount = myTracks.length;
logger.debug("initialize:myTrackCount=" + myTrackCount);
toggleTrack2ConfigDetails(myTrackCount > 1);
};

View File

@ -283,8 +283,8 @@
];
}
function RecordingRegisterCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, requestStopCallbackName) {
fakeJamClientRecordings.RecordingRegisterCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, requestStopCallbackName);
function RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName, stoppedRecordingCallbackName, abortedRecordingCallbackName) {
fakeJamClientRecordings.RegisterRecordingCallbacks(startRecordingCallbackName, stopRecordingCallbackName, startedRecordingCallbackName,stoppedRecordingCallbackName, abortedRecordingCallbackName);
}
function SessionRegisterCallback(callbackName) {
@ -601,7 +601,7 @@
this.SessionAddTrack = SessionAddTrack;
this.SessionGetControlState = SessionGetControlState;
this.SessionGetIDs = SessionGetIDs;
this.RecordingRegisterCallbacks = RecordingRegisterCallbacks;
this.RegisterRecordingCallbacks = RegisterRecordingCallbacks;
this.SessionRegisterCallback = SessionRegisterCallback;
this.SessionSetAlertCallback = SessionSetAlertCallback;
this.SessionSetControlState = SessionSetControlState;

View File

@ -26,13 +26,14 @@
return msg;
}
function stopRecording(recordingId, errorReason, errorDetail) {
function stopRecording(recordingId, success, reason, detail) {
var msg = {};
msg.type = self.Types.STOP_RECORDING;
msg.msgId = context.JK.generateUUID();
msg.recordingId = recordingId;
msg.errorReason = errorReason;
msg.errorDetail = errorDetail;
msg.success = success === undefined ? true : success;
msg.reason = reason;
msg.detail = detail;
return msg;
}
@ -47,13 +48,14 @@
return msg;
}
function abortRecording(recordingId, errorReason, errorDetail) {
function abortRecording(recordingId, reason, detail) {
var msg = {};
msg.type = self.Types.ABORT_RECORDING;
msg.msgId = context.JK.generateUUID();
msg.recordingId = recordingId;
msg.errorReason = errorReason;
msg.errorDetail = errorDetail;
msg.success = false;
msg.reason = reason;
msg.detail = detail;
return msg;
}

View File

@ -13,72 +13,78 @@
var stopRecordingResultCallbackName = null;
var startedRecordingResultCallbackName = null;
var stoppedRecordingEventCallbackName = null;
var requestStopCallbackName = null;
var abortedRecordingEventCallbackName = null;
var startingSessionState = null;
var stoppingSessionState = null;
var currentRecordingId = null;
var currentRecordingCreatorClientId = null;
var currentRecordingClientIds = null;
function timeoutStartRecordingTimer() {
eval(startRecordingResultCallbackName).call(this, startingSessionState.recordingId, false, 'client-no-response', startingSessionState.groupedClientTracks);
eval(startRecordingResultCallbackName).call(this, startingSessionState.recordingId, {success:false, reason:'client-no-response', detail:startingSessionState.groupedClientTracks[0]});
startingSessionState = null;
}
function timeoutStopRecordingTimer() {
eval(stopRecordingResultCallbackName).call(this, stoppingSessionState.recordingId, false, 'client-no-response', stoppingSessionState.groupedClientTracks);
eval(stopRecordingResultCallbackName).call(this, stoppingSessionState.recordingId, {success:false, reason:'client-no-response', detail:stoppingSessionState.groupedClientTracks[0]});
}
function StartRecording(recordingId, groupedClientTracks) {
function StartRecording(recordingId, clients) {
startingSessionState = {};
// we expect all clients to respond within 3 seconds to mimic the reliable UDP layer
startingSessionState.aggegratingStartResultsTimer = setTimeout(timeoutStartRecordingTimer, 3000);
startingSessionState.recordingId = recordingId;
startingSessionState.groupedClientTracks = copyTracks(groupedClientTracks, app.clientId); // we will manipulate this new one
startingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId); // we will manipulate this new one
// store the current recording's data
currentRecordingId = recordingId;
currentRecordingCreatorClientId = app.clientId;
currentRecordingClientIds = copyClientIds(clients, app.clientId);
if(context.JK.dlen(startingSessionState.groupedClientTracks) == 0) {
if(startingSessionState.groupedClientTracks.length == 0) {
// if there are no clients but 'self', then you can declare a successful recording immediately
finishSuccessfulStart(recordingId);
}
else {
// signal all other connected clients that the recording has started
for(var clientId in startingSessionState.groupedClientTracks) {
for(var i = 0; i < startingSessionState.groupedClientTracks.length; i++) {
var clientId = startingSessionState.groupedClientTracks[i];
context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.startRecording(recordingId)));
}
}
}
function StopRecording(recordingId, groupedClientTracks, errorReason, errorDetail) {
function StopRecording(recordingId, clients, result) {
if(startingSessionState) {
// we are currently starting a session.
// TODO
}
if(!result) {
result = {success:true}
}
stoppingSessionState = {};
// we expect all clients to respond within 3 seconds to mimic the reliable UDP layer
stoppingSessionState.aggegratingStopResultsTimer = setTimeout(timeoutStopRecordingTimer, 3000);
stoppingSessionState.recordingId = recordingId;
stoppingSessionState.groupedClientTracks = copyTracks(groupedClientTracks, app.clientId);
stoppingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId);
if(context.JK.dlen(stoppingSessionState.groupedClientTracks) == 0) {
if(stoppingSessionState.groupedClientTracks.length == 0) {
finishSuccessfulStop(recordingId);
}
else {
// signal all other connected clients that the recording has started
for(var clientId in stoppingSessionState.groupedClientTracks) {
context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.stopRecording(recordingId, errorReason, errorDetail)));
// signal all other connected clients that the recording has stopped
for(var i = 0; i < stoppingSessionState.groupedClientTracks.length; i++) {
var clientId = stoppingSessionState.groupedClientTracks[i];
context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.stopRecording(recordingId, result.success, result.reason, result.detail)));
}
}
//eval(stopRecordingResultCallbackName).call(this, recordingId, true, null, null);
}
function AbortRecording(recordingId, errorReason, errorDetail) {
@ -101,7 +107,7 @@
currentRecordingCreatorClientId = from;
context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.startRecordingAck(payload.recordingId, true, null, null)));
eval(startedRecordingResultCallbackName).call(this, from, payload.recordingId);
eval(startedRecordingResultCallbackName).call(this, payload.recordingId, {success:true}, from);
}
}
@ -112,9 +118,10 @@
if(startingSessionState) {
if(payload.success) {
delete startingSessionState.groupedClientTracks[from];
var index = startingSessionState.groupedClientTracks.indexOf(from);
startingSessionState.groupedClientTracks.splice(index, 1);
if(context.JK.dlen(startingSessionState.groupedClientTracks) == 0) {
if(startingSessionState.groupedClientTracks.length == 0) {
finishSuccessfulStart(payload.recordingId);
}
}
@ -137,7 +144,7 @@
// this means we should keep a list of the last N recordings that we've seen, rather than just keeping the current
context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.stopRecordingAck(payload.recordingId, true)));
eval(stopRecordingResultCallbackName).call(this, payload.recordingId, !payload.errorReason, payload.errorReason, payload.errorDetail);
eval(stopRecordingResultCallbackName).call(this, payload.recordingId, {success:payload.success, reason:payload.reason, detail:from});
}
function onStopRecordingAck(from, payload) {
@ -147,14 +154,16 @@
if(stoppingSessionState) {
if(payload.success) {
delete stoppingSessionState.groupedClientTracks[from];
var index = stoppingSessionState.groupedClientTracks.indexOf(from);
stoppingSessionState.groupedClientTracks.splice(index, 1);
if(context.JK.dlen(stoppingSessionState.groupedClientTracks) == 0) {
if(stoppingSessionState.groupedClientTracks.length == 0) {
finishSuccessfulStop(payload.recordingId);
}
}
else {
// TOOD: a client responded with error; what now?
logger.error("client responded with error: ", payload);
}
}
else {
@ -170,48 +179,62 @@
// if creator, tell everyone else to stop
if(app.clientId == currentRecordingCreatorClientId) {
// ask the front end to stop the recording because it has the full track listing
eval(requestStopCallbackName).call(this, payload.errorReason, payload.errorDetail);
for(var i = 0; i < currentRecordingClientIds.length; i++) {
var clientId = currentRecordingClientIds[i];
context.JK.JamServer.sendP2PMessage(clientId, JSON.stringify(p2pMessageFactory.abortRecording(currentRecordingId, payload.reason, from)));
}
}
else {
logger.warn("only the creator currently deals with the abort request. abort request sent from:" + from + " with a reason of: " + payload.errorReason);
logger.debug("only the creator currently deals with the abort request. abort request sent from:" + from + " with a reason of: " + payload.errorReason);
}
eval(abortedRecordingEventCallbackName).call(this, payload.recordingId, {success:payload.success, reason:payload.reason, detail:from});
}
function RecordingRegisterCallbacks(startRecordingCallbackName,
function RegisterRecordingCallbacks(startRecordingCallbackName,
stopRecordingCallbackName,
startedRecordingCallbackName,
stoppedRecordingCallbackName,
_requestStopCallbackName) {
abortedRecordingCallbackName) {
startRecordingResultCallbackName = startRecordingCallbackName;
stopRecordingResultCallbackName = stopRecordingCallbackName;
startedRecordingResultCallbackName = startedRecordingCallbackName;
stoppedRecordingEventCallbackName = stoppedRecordingCallbackName;
requestStopCallbackName = _requestStopCallbackName;
abortedRecordingEventCallbackName = abortedRecordingCallbackName;
}
// copies all tracks, but removes current client ID because we don't want to message that user
function copyTracks(tracks, myClientId) {
var newTracks = {};
for(var clientId in tracks) {
// copies all clientIds, but removes current client ID because we don't want to message that user
function copyClientIds(clientIds, myClientId) {
var newClientIds = [];
for(var i = 0; i < clientIds.length; i++) {
var clientId = clientIds[i]
if(clientId != myClientId) {
newTracks[clientId] = tracks[clientId];
newClientIds.push(clientId);
}
}
return newTracks;
return newClientIds;
}
function finishSuccessfulStart(recordingId) {
// all clients have responded.
clearTimeout(startingSessionState.aggegratingStartResultsTimer);
startingSessionState = null;
eval(startRecordingResultCallbackName).call(this, recordingId, true);
eval(startRecordingResultCallbackName).call(this, recordingId, {success:true});
}
function finishSuccessfulStop(recordingId, errorReason) {
// all clients have responded.
clearTimeout(stoppingSessionState.aggegratingStopResultsTimer);
stoppingSessionState = null;
eval(stopRecordingResultCallbackName).call(this, recordingId, true, errorReason);
var result = { success: true }
if(errorReason)
{
result.success = false;
result.reason = errorReason
result.detail = ""
}
eval(stopRecordingResultCallbackName).call(this, recordingId, result);
}
@ -226,7 +249,7 @@
this.StartRecording = StartRecording;
this.StopRecording = StopRecording;
this.AbortRecording = AbortRecording;
this.RecordingRegisterCallbacks = RecordingRegisterCallbacks;
this.RegisterRecordingCallbacks = RegisterRecordingCallbacks;
}
})(window, jQuery);

View File

@ -245,6 +245,16 @@
jamClient.TrackSetChatEnable(false);
}
var defaultInstrumentId;
if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) {
defaultInstrumentId = context.JK.instrument_id_to_instrument[context.JK.userMe.instruments[0].instrument_id].client_id;
}
else {
defaultInstrumentId = context.JK.server_to_client_instrument_map['Other'].client_id;
}
jamClient.TrackSetInstrument(1, defaultInstrumentId);
logger.debug("Calling FTUESave(" + persist + ")");
var response = jamClient.FTUESave(persist);
setLevels(0);
@ -252,7 +262,6 @@
logger.warn(response);
// TODO - we may need to do something about errors on save.
// per VRFS-368, I'm hiding the alert, and logging a warning.
// context.alert(response);
}
} else {
logger.debug("Aborting FTUESave as we need input + output selected.");
@ -430,13 +439,10 @@
}
function setAsioSettingsVisibility() {
logger.debug("jamClient.FTUEHasControlPanel()=" + jamClient.FTUEHasControlPanel());
if (jamClient.FTUEHasControlPanel()) {
logger.debug("Showing ASIO button");
$('#btn-asio-control-panel').show();
}
else {
logger.debug("Hiding ASIO button");
$('#btn-asio-control-panel').hide();
}
}

View File

@ -73,6 +73,15 @@
250: { "server_id": "other" }
};
context.JK.instrument_id_to_instrument = {};
(function() {
$.each(context.JK.server_to_client_instrument_map, function(key, value) {
context.JK.instrument_id_to_instrument[value.server_id] = { client_id: value.client_id, display: key }
});
})();
context.JK.entityToPrintable = {
music_session: "music session"
}

View File

@ -13,7 +13,6 @@
var logger = context.JK.logger;
function createJoinRequest(joinRequest) {
logger.debug("joinRequest=" + JSON.stringify(joinRequest));
return $.ajax({
type: "POST",
dataType: "json",
@ -341,6 +340,20 @@
})
}
function putTrackSyncChange(options) {
var musicSessionId = options["id"]
delete options["id"];
return $.ajax({
type: "PUT",
dataType: "json",
url: '/api/sessions/' + musicSessionId + '/tracks',
contentType: 'application/json',
processData: false,
data: JSON.stringify(options)
});
}
function initialize() {
return self;
}
@ -374,6 +387,7 @@
this.startRecording = startRecording;
this.stopRecording = stopRecording;
this.getRecording = getRecording;
this.putTrackSyncChange = putTrackSyncChange;
return this;
};

View File

@ -253,6 +253,11 @@
this.layout.notify(message, descriptor);
};
/** Shows an alert notification. Expects text, title */
this.notifyAlert = function(title ,text) {
this.notify({title:title, text:text, icon_url: "/assets/content/icon_alert_big.png"});
}
/**
* Initialize any common events.
*/
@ -300,7 +305,7 @@
if (context.jamClient) {
// Unregister for callbacks.
context.jamClient.RecordingRegisterCallbacks("", "", "", "", "");
context.jamClient.RegisterRecordingCallbacks("", "", "", "", "");
context.jamClient.SessionRegisterCallback("");
context.jamClient.SessionSetAlertCallback("");
context.jamClient.FTUERegisterVUCallbacks("", "", "");

View File

@ -476,17 +476,27 @@
* also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one
*/
function stackDialogs($dialog, $overlay) {
// don't push a dialog on the stack that is already on there; remove it from where ever it is currently
// and the rest of the code will make it end up at the top
var layoutId = $dialog.attr('layout-id');
for(var i = openDialogs.length - 1; i >= 0; i--) {
if(openDialogs[i].attr('layout-id') === layoutId) {
openDialogs.splice(i, 1);
}
}
openDialogs.push($dialog);
var zIndex = 1000;
for(var i in openDialogs) {
var $dialog = openDialogs[i];
$dialog.css('zIndex', zIndex);
var $openDialog = openDialogs[i];
$openDialog.css('zIndex', zIndex);
zIndex++;
}
$overlay.css('zIndex', zIndex - 1);
}
function unstackDialogs($overlay) {
console.log("unstackDialogs. openDialogs: %o", openDialogs);
if(openDialogs.length > 0) {
openDialogs.pop();
}

View File

@ -46,8 +46,6 @@
/** called every time a session is joined, to ensure clean state */
function reset() {
stoppingRecording = false;
startingRecording = false;
currentlyRecording = false;
waitingOnServerStop = false;
waitingOnClientStop = false;
@ -73,23 +71,15 @@
}
tracksForClient.push(recordingTracks[i]);
}
return groupedTracks;
return context.JK.dkeys(groupedTracks);
}
function startRecording() {
if(currentlyRecording) {
logger.warn("ignoring request to start recording because we are currently recording");
return false;
}
if(startingRecording) {
logger.warn("ignoring request to start recording because recording currently started");
return false;
}
startingRecording = true;
$self.triggerHandler('startingRecording', {});
currentlyRecording = true;
currentRecording = rest.startRecording({"music_session_id": sessionModel.id()})
.done(function(recording) {
currentRecordingId = recording.id;
@ -100,34 +90,20 @@
})
.fail(function() {
$self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments });
startingRecording = false;
currentlyRecording = false;
})
return true;
}
/** Nulls can be passed for all 3 currently; that's a user request. */
function stopRecording(recordingId, errorReason, errorDetail) {
if(recordingId && recordingId != currentRecordingId) {
logger.debug("asked to stop an unknown recording: %o", recordingId);
return false;
}
if(!currentlyRecording) {
logger.debug("ignoring request to stop recording because there is not currently a recording");
return false;
}
if(stoppingRecording) {
logger.debug("request to stop recording ignored because recording currently stopping")
return false;
}
stoppingRecording = true;
function stopRecording(recordingId, reason, detail) {
waitingOnServerStop = waitingOnClientStop = true;
waitingOnStopTimer = setTimeout(timeoutTransitionToStop, 5000);
$self.triggerHandler('stoppingRecording', {reason: errorReason, detail: errorDetail});
$self.triggerHandler('stoppingRecording', {reason: reason, detail: detail});
// this path assumes that the currentRecording info has, or can be, retrieved
// failure for currentRecording is handled elsewhere
@ -140,12 +116,12 @@
rest.stopRecording( { "id": recording.id } )
.done(function() {
waitingOnServerStop = false;
attemptTransitionToStop(recording.id, errorReason, errorDetail);
attemptTransitionToStop(recording.id, reason, detail);
})
.fail(function(jqXHR) {
if(jqXHR.status == 422) {
waitingOnServerStop = false;
attemptTransitionToStop(recording.id, errorReason, errorDetail);
attemptTransitionToStop(recording.id, reason, detail);
}
else {
logger.error("unable to stop recording %o", arguments);
@ -159,7 +135,7 @@
}
function abortRecording(recordingId, errorReason, errorDetail) {
jamClient.AbortRecording(recordingId, errorReason, errorDetail);
jamClient.AbortRecording(recordingId, {reason: errorReason, detail: errorDetail, success:false});
}
function timeoutTransitionToStop() {
@ -178,7 +154,6 @@
}
function transitionToStopped() {
stoppingRecording = false;
currentlyRecording = false;
currentRecording = null;
currentRecordingId = null;
@ -196,21 +171,28 @@
stopRecording(recordingId, null, null);
}
function handleRecordingStartResult(recordingId, success, reason, detail) {
function handleRecordingStartResult(recordingId, result) {
var success = result.success;
var reason = result.reason;
var detail = result.detail;
startingRecording = false;
currentlyRecording = true;
if(success) {
$self.triggerHandler('startedRecording', {clientId: app.clientId})
}
else {
currentlyRecording = false;
logger.error("unable to start the recording %o, %o", reason, detail);
$self.triggerHandler('startedRecording', { clientId: app.clientId, reason: reason, detail: detail});
}
}
function handleRecordingStopResult(recordingId, success, reason, detail) {
function handleRecordingStopResult(recordingId, result) {
var success = result.success;
var reason = result.reason;
var detail = result.detail;
waitingOnClientStop = false;
@ -224,7 +206,11 @@
}
}
function handleRecordingStarted(clientId, recordingId) {
function handleRecordingStarted(recordingId, result, clientId) {
var success = result.success;
var reason = result.reason;
var detail = result.detail;
// in this scenario, we don't know all the tracks of the user.
// we need to ask sessionModel to populate us with the recording data ASAP
@ -236,47 +222,93 @@
currentRecordingId = recording.id;
});
startingRecording = true;
$self.triggerHandler('startingRecording', {recordingId: recordingId});
startingRecording = false;
currentlyRecording = true;
$self.triggerHandler('startedRecording', {clientId: clientId, recordingId: recordingId});
}
function handleRecordingStopped(recordingId, success, errorReason, errorDetail) {
stoppingRecording = true;
$self.triggerHandler('stoppingRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail });
function handleRecordingStopped(recordingId, result) {
var success = result.success;
var reason = result.reason;
var detail = result.detail;
$self.triggerHandler('stoppingRecording', {recordingId: recordingId, reason: reason, detail: detail });
// the backend says the recording must be stopped.
// tell the server to stop it too
rest.stopRecording({
recordingId: recordingId
id: recordingId
})
.always(function() {
stoppingRecording = false;
currentlyRecording = false;
transitionToStopped();
})
.fail(function(jqXHR, textStatus, errorMessage) {
if(jqXHR.status == 422) {
logger.debug("recording already stopped %o", arguments);
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail});
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
}
else if(jqXHR.status == 404) {
logger.debug("recording is already deleted %o", arguments);
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail});
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
}
else {
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: textStatus, detail: errorMessage});
}
})
.done(function() {
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail});
$self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
})
}
function handleRequestRecordingStop(recordingId, errorReason, errorDetail) {
// TODO: check recordingId
// this is always an error case, when the backend autonomously asks tho frontend to stop
stopRecording(recordingId, errorReason, errorDetail);
function handleRecordingAborted(recordingId, result) {
var success = result.success;
var reason = result.reason;
var detail = result.detail;
stoppingRecording = false;
$self.triggerHandler('abortedRecording', {recordingId: recordingId, reason: reason, detail: detail });
// the backend says the recording must be stopped.
// tell the server to stop it too
rest.stopRecording({
id: recordingId
})
.always(function() {
currentlyRecording = false;
})
}
/**
* If a stop is needed, it will be issued, and the deferred object will fire done()
* If a stop is not needed (i.e., there is no recording), then the deferred object will fire immediately
* @returns {$.Deferred} in all cases, only .done() is fired.
*/
function stopRecordingIfNeeded() {
var deferred = new $.Deferred();
function resolved() {
$self.off('stoppedRecording.stopRecordingIfNeeded', resolved);
deferred.resolve(arguments);
}
if(!currentlyRecording) {
deferred = new $.Deferred();
deferred.resolve();
}
else {
// wait for the next stoppedRecording event message
$self.on('stoppedRecording.stopRecordingIfNeeded', resolved);
if(!stopRecording()) {
// no event is coming, so satisfy the deferred immediately
$self.off('stoppedRecording.stopRecordingIfNeeded', resolved);
deferred = new $.Deferred();
deferred.resolve();
}
}
return deferred;
}
this.initialize = function() {
@ -287,12 +319,14 @@
this.onServerStopRecording = onServerStopRecording;
this.isRecording = isRecording;
this.reset = reset;
this.stopRecordingIfNeeded = stopRecordingIfNeeded;
context.JK.HandleRecordingStartResult = handleRecordingStartResult;
context.JK.HandleRecordingStopResult = handleRecordingStopResult;
context.JK.HandleRecordingStopped = handleRecordingStopped;
context.JK.HandleRecordingStarted = handleRecordingStarted;
context.JK.HandleRequestRecordingStop = handleRequestRecordingStop;
context.JK.HandleRecordingAborted = handleRecordingAborted;
};

View File

@ -101,14 +101,54 @@
function alertCallback(type, text) {
if (type === 2) { // BACKEND_MIXER_CHANGE
sessionModel.refreshCurrentSession();
logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text);
if(sessionModel.id() && text == "RebuildAudioIoControl") {
// this is a local change to our tracks. we need to tell the server about our updated track information
var inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
// create a trackSync request based on backend data
var syncTrackRequest = {};
syncTrackRequest.client_id = app.clientId;
syncTrackRequest.tracks = inputTracks;
syncTrackRequest.id = sessionModel.id();
rest.putTrackSyncChange(syncTrackRequest)
.done(function() {
sessionModel.refreshCurrentSession();
})
.fail(function() {
app.notify({
"title": "Can't Sync Local Tracks",
"text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
"icon_url": "/assets/content/icon_alert_big.png"
});
})
}
else {
// this is wrong; we shouldn't be using the server to decide what tracks are shown
// however, we have to without a refactor, and if we wait a second, then it should be enough time
// for the client that initialed this to have made the change
// https://jamkazam.atlassian.net/browse/VRFS-854
setTimeout(function() {
sessionModel.refreshCurrentSession(); // XXX: race condition possible here; other client may not have updated server yet
}, 1000);
}
} else {
context.setTimeout(function() {
app.notify({
"title": alert_type[type].title,
"text": text,
"icon_url": "/assets/content/icon_alert_big.png"
}); }, 1);
var alert = alert_type[type];
if(alert) {
app.notify({
"title": alert_type[type].title,
"text": text,
"icon_url": "/assets/content/icon_alert_big.png"
});
}
else {
logger.debug("Unknown Backend Event type %o, data %o", type, text)
}
}, 1);
}
}
@ -121,7 +161,7 @@
// Subscribe for callbacks on audio events
context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback");
context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback");
context.jamClient.RecordingRegisterCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRequestRecordingStop");
context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
context.jamClient.SessionSetAlertCallback("JK.AlertCallback");
// If you load this page directly, the loading of the current user
@ -138,8 +178,26 @@
checkForCurrentUser();
}
function notifyWithUserInfo(title , text, clientId) {
sessionModel.findUserBy({clientId: clientId})
.done(function(user) {
app.notify({
"title": title,
"text": user.name + " " + text,
"icon_url": context.JK.resolveAvatarUrl(user.photo_url)
});
})
.fail(function() {
app.notify({
"title": title,
"text": 'Someone ' + text,
"icon_url": "/assets/content/icon_alert_big.png"
});
});
}
function afterCurrentUserLoaded() {
logger.debug("afterCurrentUserLoaded");
// It seems the SessionModel should be a singleton.
// a client can only be in one session at a time,
// and other parts of the code want to know at any certain times
@ -152,21 +210,49 @@
$(sessionModel.recordingModel)
.on('startingRecording', function(e, data) {
if(data.reason) {
// error path
displayDoneRecording();
app.notify({
"title": "Unable to Start Recording",
"text": "Unable to start the recording due to '" + data.reason + "'",
"icon_url": "/assets/content/icon_alert_big.png"});
}
else {
displayStartingRecording();
}
displayStartingRecording();
})
.on('startedRecording', function(e, data) {
displayStartedRecording();
displayWhoCreated(data.clientId)
if(data.reason) {
var reason = data.reason;
var detail = data.detail;
var title = "Could Not Start Recording";
if(data.reason == 'client-no-response') {
notifyWithUserInfo(title, 'did not respond to the start signal.', detail);
}
else if(data.reason == 'empty-recording-id') {
app.notifyAlert(title, "No recording ID specified.");
}
else if(data.reason == 'missing-client') {
notifyWithUserInfo(title, 'could not be signalled to start recording.', detail);
}
else if(data.reason == 'already-recording') {
app.notifyAlert(title, 'Already recording.');
}
else if(data.reason == 'recording-engine-unspecified') {
notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail);
}
else if(data.reason == 'recording-engine-create-directory') {
notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail);
}
else if(data.reason == 'recording-engine-create-file') {
notifyWithUserInfo(title, 'had a problem creating a recording file.', detail);
}
else if(data.reason == 'recording-engine-sample-rate') {
notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail);
}
else {
notifyWithUserInfo(title, 'Error Reason: ' + reason);
}
displayDoneRecording();
}
else
{
displayStartedRecording();
displayWhoCreated(data.clientId);
}
})
.on('stoppingRecording', function(e, data) {
displayStoppingRecording(data);
@ -174,15 +260,35 @@
.on('stoppedRecording', function(e, data) {
if(data.reason) {
var reason = data.reason;
var detail = data.detail;
var title = "Recording Discarded";
if(data.reason == 'client-no-response') {
reason = 'someone in the session has disconnected';
notifyWithUserInfo(title, 'did not respond to the stop signal.', detail);
}
var text = "This recording has been thrown out because " + reason + "."
app.notify({
"title": "Recording Deleted",
"text": text,
"icon_url": "/assets/content/icon_alert_big.png"
});
else if(data.reason == 'missing-client') {
notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail);
}
else if(data.reason == 'empty-recording-id') {
app.notifyAlert(title, "No recording ID specified.");
}
else if(data.reason == 'wrong-recording-id') {
app.notifyAlert(title, "Wrong recording ID specified.");
}
else if(data.reason == 'not-recording') {
app.notifyAlert(title, "Not currently recording.");
}
else if(data.reason == 'already-stopping') {
app.notifyAlert(title, "Already stopping the current recording.");
}
else if(data.reason == 'start-before-stop') {
notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail);
}
else {
app.notifyAlert(title, "Error reason: " + reason);
}
displayDoneRecording();
}
else {
@ -191,12 +297,40 @@
}
})
.on('startedRecordingFailed', function(e, data) {
.on('abortedRecording', function(e, data) {
var reason = data.reason;
var detail = data.detail;
var title = "Recording Cancelled";
if(data.reason == 'client-no-response') {
notifyWithUserInfo(title, 'did not respond to the start signal.', detail);
}
else if(data.reason == 'missing-client') {
notifyWithUserInfo(title, 'could not be signalled to start recording.', detail);
}
else if(data.reason == 'populate-recording-info') {
notifyWithUserInfo(title, 'could not synchronize with the server.', detail);
}
else if(data.reason == 'recording-engine-unspecified') {
notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail);
}
else if(data.reason == 'recording-engine-create-directory') {
notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail);
}
else if(data.reason == 'recording-engine-create-file') {
notifyWithUserInfo(title, 'had a problem creating a recording file.', detail);
}
else if(data.reason == 'recording-engine-sample-rate') {
notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail);
}
else {
app.notifyAlert(title, "Error reason: " + reason);
}
displayDoneRecording();
})
.on('stoppedRecordingFailed', function(data) {
});
sessionModel.subscribe('sessionScreen', sessionChanged);
sessionModel.joinSession(sessionId)
@ -497,7 +631,7 @@
addNewGearDialog = new context.JK.AddNewGearDialog(app, ftueCallback);
// # NO LONGER HIDING ADD TRACK even when there are 2 tracks (VRFS-537)
$('#div-add-track').click(function() {
$('#div-add-track').unbind('click').click(function() {
if (myTracks.length === 2) {
$('#btn-error-ok').click(function() {
app.layout.closeDialog('error-dialog');

View File

@ -18,7 +18,7 @@
var pendingSessionRefresh = false;
var recordingModel = new context.JK.RecordingModel(app, this, rest, context.jamClient);
function id() {
return currentSession.id;
return currentSession ? currentSession.id : null;
}
function participants() {
@ -80,41 +80,41 @@
return deferred;
}
function performLeaveSession(deferred) {
logger.debug("SessionModel.leaveCurrentSession()");
// TODO - sessionChanged will be called with currentSession = null
server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession);
server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession);
// leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long
// time, for that entire duration you'll still be sending voice data to the other users.
// this may be bad if someone decides to badmouth others in the left-session during this time
logger.debug("calling jamClient.LeaveSession for clientId=" + clientId);
client.LeaveSession({ sessionID: currentSessionId });
deferred = leaveSessionRest(currentSessionId);
deferred.done(function() {
sessionChanged();
});
// 'unregister' for callbacks
context.jamClient.SessionRegisterCallback("");
context.jamClient.SessionSetAlertCallback("");
updateCurrentSession(null);
currentSessionId = null;
}
/**
* Leave the current session, if there is one.
* callback: called in all conditions; either after an attempt is made to tell the server that we are leaving,
* or immediately if there is no session
*/
function leaveCurrentSession() {
var deferred;
var deferred = new $.Deferred();
if(currentSessionId) {
logger.debug("SessionModel.leaveCurrentSession()");
// TODO - sessionChanged will be called with currentSession = null
server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession);
server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession);
// leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long
// time, for that entire duration you'll still be sending voice data to the other users.
// this may be bad if someone decides to badmouth others in the left-session during this time
logger.debug("calling jamClient.LeaveSession for clientId=" + clientId);
client.LeaveSession({ sessionID: currentSessionId });
deferred = leaveSessionRest(currentSessionId);
deferred.done(function() {
sessionChanged();
recordingModel.stopRecordingIfNeeded()
.always(function(){
performLeaveSession(deferred);
});
// 'unregister' for callbacks
//context.jamClient.RecordingRegisterCallbacks("", "", "", "", "");
context.jamClient.SessionRegisterCallback("");
context.jamClient.SessionSetAlertCallback("");
updateCurrentSession(null);
currentSessionId = null;
}
else {
deferred = new $.Deferred();
deferred.resolve();
}
return deferred;
}
@ -122,6 +122,7 @@
* Refresh the current session, and participants.
*/
function refreshCurrentSession() {
// XXX use backend instead: https://jamkazam.atlassian.net/browse/VRFS-854
logger.debug("SessionModel.refreshCurrentSession()");
refreshCurrentSessionRest(sessionChanged);
}
@ -167,8 +168,6 @@
async: false,
success: function(response) {
sendClientParticipantChanges(currentSession, response);
logger.debug("Current Session Refreshed:");
logger.debug(response);
updateCurrentSession(response);
if(callback != null) {
callback();
@ -262,7 +261,7 @@
}
function addTrack(sessionId, data) {
logger.debug("track data = " + JSON.stringify(data));
logger.debug("updating tracks on the server %o", data);
var url = "/api/sessions/" + sessionId + "/tracks";
$.ajax({
type: "POST",
@ -274,9 +273,8 @@
processData:false,
success: function(response) {
// save to the backend
context.jamClient.TrackSaveAssignments();
logger.debug("Successfully added track (" + JSON.stringify(data) + ")");
refreshCurrentSession();
logger.debug("successfully updated tracks on the server");
//refreshCurrentSession();
},
error: ajaxError
});
@ -300,7 +298,13 @@
}
function deleteTrack(sessionId, trackId) {
if (trackId) {
client.TrackSetCount(1);
client.TrackSaveAssignments();
/**
$.ajax({
type: "DELETE",
url: "/api/sessions/" + sessionId + "/tracks/" + trackId,
@ -319,6 +323,7 @@
logger.error("Error deleting track " + trackId);
}
});
*/
}
}

View File

@ -14,6 +14,21 @@
// take all necessary arguments to complete its work.
context.JK.TrackHelpers = {
getTracks: function(jamClient, groupId) {
var tracks = [];
var trackIds = jamClient.SessionGetIDs();
var allTracks = jamClient.SessionGetControlState(trackIds);
// get client's tracks
for (var i=0; i < allTracks.length; i++) {
if (allTracks[i].group_id === groupId) {
tracks.push(allTracks[i]);
}
}
return tracks;
},
/**
* This function resolves which tracks to configure for a user
* when creating or joining a session. By default, tracks are pulled
@ -22,80 +37,25 @@
*/
getUserTracks: function(jamClient) {
var trackIds = jamClient.SessionGetIDs();
var allTracks = jamClient.SessionGetControlState(trackIds);
var localMusicTracks = [];
var i;
var instruments = [];
var localTrackExists = false;
// get client's tracks
for (i=0; i < allTracks.length; i++) {
if (allTracks[i].group_id === 2) {
localMusicTracks.push(allTracks[i]);
console.log("allTracks[" + i + "].instrument_id=" + allTracks[i].instrument_id);
// check if local track config exists
if (allTracks[i].instrument_id !== 0) {
localTrackExists = true;
}
}
}
localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, 2);
var trackObjects = [];
console.log("localTrackExists=" + localTrackExists);
// get most proficient instrument from API if no local track config exists
if (!localTrackExists) {
if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) {
var track = {
instrument_id: context.JK.userMe.instruments[0].instrument_id,
sound: "stereo"
};
trackObjects.push(track);
var desc = context.JK.userMe.instruments[0].description;
jamClient.TrackSetInstrument(1, context.JK.server_to_client_instrument_map[desc]);
jamClient.TrackSaveAssignments();
}
}
// use all tracks previously configured
else {
console.log("localMusicTracks.length=" + localMusicTracks.length);
for (i=0; i < localMusicTracks.length; i++) {
var track = {};
var instrument_description = '';
console.log("localMusicTracks[" + i + "].instrument_id=" + localMusicTracks[i].instrument_id);
// no instruments configured
if (localMusicTracks[i].instrument_id === 0) {
if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) {
track.instrument_id = context.JK.userMe.instruments[0].instrument_id;
}
else {
track.instrument_id = context.JK.client_to_server_instrument_map[250].server_id;
}
}
// instruments are configured
else {
if (context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id]) {
track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id;
}
// fall back to Other
else {
track.instrument_id = context.JK.client_to_server_instrument_map[250].server_id;
jamClient.TrackSetInstrument(i+1, 250);
jamClient.TrackSaveAssignments();
}
}
if (localMusicTracks[i].stereo) {
track.sound = "stereo";
}
else {
track.sound = "mono";
}
trackObjects.push(track);
for (i=0; i < localMusicTracks.length; i++) {
var track = {};
track.client_track_id = localMusicTracks[i].id;
track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id;
if (localMusicTracks[i].stereo) {
track.sound = "stereo";
}
else {
track.sound = "mono";
}
trackObjects.push(track);
}
return trackObjects;
}

View File

@ -220,6 +220,19 @@
return count;
};
/*
* Get the keys of a dictionary as an array (same as Object.keys, but works in all browsers)
*/
context.JK.dkeys = function(d) {
var keys = []
for (var i in d) {
if (d.hasOwnProperty(i)) {
keys.push(i);
}
}
return keys;
};
/**
* Finds the first error associated with the field.
* @param fieldName the name of the field

View File

@ -141,11 +141,24 @@ class ApiMusicSessionsController < ApiController
end
end
def track_sync
@tracks = Track.sync(params[:client_id], params[:tracks])
unless @tracks.kind_of? Array
# we have to do this because api_session_detail_url will fail with a bad @music_session
response.status = :unprocessable_entity
respond_with @tracks
else
respond_with @tracks, responder: ApiResponder
end
end
def track_create
@track = Track.save(nil,
params[:connection_id],
params[:instrument_id],
params[:sound])
params[:sound],
params[:client_track_id])
respond_with @track, responder: ApiResponder, :status => 201, :location => api_session_track_detail_url(@track.connection.music_session, @track)
end
@ -155,7 +168,8 @@ class ApiMusicSessionsController < ApiController
@track = Track.save(params[:track_id],
nil,
params[:instrument_id],
params[:sound])
params[:sound],
params[:client_track_id])
respond_with @track, responder: ApiResponder, :status => 200

View File

@ -21,7 +21,7 @@ child(:connections => :participants) {
end
child(:tracks => :tracks) {
attributes :id, :connection_id, :instrument_id, :sound
attributes :id, :connection_id, :instrument_id, :sound, :client_track_id
}
}

View File

@ -1,3 +1,3 @@
object @track
attributes :id, :connection_id, :instrument_id, :sound
attributes :id, :connection_id, :instrument_id, :sound, :client_track_id

View File

@ -91,6 +91,7 @@ SampleApp::Application.routes.draw do
# music session tracks
match '/sessions/:id/tracks' => 'api_music_sessions#track_create', :via => :post
match '/sessions/:id/tracks' => 'api_music_sessions#track_sync', :via => :put
match '/sessions/:id/tracks' => 'api_music_sessions#track_index', :via => :get
match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_update', :via => :post
match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_show', :via => :get, :as => 'api_session_track_detail'

View File

@ -120,6 +120,7 @@ FactoryGirl.define do
factory :track, :class => JamRuby::Track do
sound "mono"
sequence(:client_track_id) { |n| "client_track_id_seq_#{n}"}
end

View File

@ -13,7 +13,7 @@ describe MusicSessionManager do
@band = FactoryGirl.create(:band)
@genre = FactoryGirl.create(:genre)
@instrument = FactoryGirl.create(:instrument)
@tracks = [{"instrument_id" => @instrument.id, "sound" => "mono"}]
@tracks = [{"instrument_id" => @instrument.id, "sound" => "mono", "client_track_id" => "abcd"}]
@connection = FactoryGirl.create(:connection, :user => @user)
end

View File

@ -23,7 +23,7 @@ describe "Music Session API ", :type => :api do
let(:user) { FactoryGirl.create(:user) }
# defopts are used to setup default options for the session
let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}], :legal_terms => true, :intellectual_property => true} }
let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}], :legal_terms => true, :intellectual_property => true} }
before do
#sign_in user
MusicSession.delete_all
@ -115,7 +115,7 @@ describe "Music Session API ", :type => :api do
# create a 2nd track for this session
conn_id = updated_track["connection_id"]
post "/api/sessions/#{music_session["id"]}/tracks.json", { :connection_id => "#{conn_id}", :instrument_id => "electric guitar", :sound => "mono" }.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/tracks.json", { :connection_id => "#{conn_id}", :instrument_id => "electric guitar", :sound => "mono", :client_track_id => "client_track_guid" }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should == 201
get "/api/sessions/#{music_session["id"]}/tracks.json", "CONTENT_TYPE" => 'application/json'
@ -239,7 +239,7 @@ describe "Music Session API ", :type => :api do
musician["client_id"].should == client.client_id
login(user2)
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
@ -311,7 +311,7 @@ describe "Music Session API ", :type => :api do
original_count = MusicSession.all().length
client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1")
post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "mom", "sound" => "mono"}]}).to_json, "CONTENT_TYPE" => 'application/json'
post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "mom", "sound" => "mono", "client_track_id" => "client_track_guid"}]}).to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(404)
# check that the transaction was rolled back
@ -322,7 +322,7 @@ describe "Music Session API ", :type => :api do
original_count = MusicSession.all().length
client = FactoryGirl.create(:connection, :user => user, :ip_address => "1.1.1.1")
post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mom"}]}).to_json, "CONTENT_TYPE" => 'application/json'
post '/api/sessions.json', defopts.merge({:client_id => client.client_id, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mom", "client_track_id" => "client_track_guid"}]}).to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
JSON.parse(last_response.body)["errors"]["tracks"][0].should == "is invalid"
@ -411,7 +411,7 @@ describe "Music Session API ", :type => :api do
# users are friends, but no invitation... so we shouldn't be able to join as user 2
login(user2)
post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}] }.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
join_response = JSON.parse(last_response.body)
join_response["errors"]["musician_access"].should == [ValidationMessages::INVITE_REQUIRED]
@ -422,7 +422,7 @@ describe "Music Session API ", :type => :api do
last_response.status.should eql(201)
login(user2)
post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}] }.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}] }.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
end
@ -492,7 +492,7 @@ describe "Music Session API ", :type => :api do
client2 = FactoryGirl.create(:connection, :user => user2, :ip_address => "2.2.2.2")
login(user2)
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
rejected_join_attempt = JSON.parse(last_response.body)
rejected_join_attempt["errors"]["approval_required"] = [ValidationMessages::INVITE_REQUIRED]
@ -514,7 +514,7 @@ describe "Music Session API ", :type => :api do
# finally, go back to user2 and attempt to join again
login(user2)
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
end
@ -544,7 +544,7 @@ describe "Music Session API ", :type => :api do
track["sound"].should == "mono"
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
@ -581,7 +581,7 @@ describe "Music Session API ", :type => :api do
# user 2 should not be able to join
login(user2)
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(422)
JSON.parse(last_response.body)["errors"]["music_session"][0].should == ValidationMessages::CANT_JOIN_RECORDING_SESSION
end
@ -636,6 +636,34 @@ describe "Music Session API ", :type => :api do
msuh.rating.should == 0
end
it "track sync" do
user = FactoryGirl.create(:single_user_session)
instrument = FactoryGirl.create(:instrument, :description => 'a great instrument')
music_session = FactoryGirl.create(:music_session, :creator => user)
client = FactoryGirl.create(:connection, :user => user, :music_session => music_session)
track = FactoryGirl.create(:track, :connection => client, :instrument => instrument)
existing_track = {:id => track.id, :client_track_id => track.client_track_id, :sound => track.sound, :instrument_id => track.instrument_id }
new_track = {:client_track_id => "client_track_id1", :instrument_id => instrument.id, :sound => 'stereo'}
# let's add a new track, and leave the existing one alone
tracks = [existing_track, new_track]
login(user)
put "/api/sessions/#{music_session.id}/tracks.json", { :client_id => client.client_id, :tracks => tracks }.to_json, "CONTENT_TYPE" => "application/json"
last_response.status.should == 204
get "/api/sessions/#{music_session.id}/tracks.json", "CONTENT_TYPE" => 'application/json'
last_response.status.should == 200
tracks = JSON.parse(last_response.body)
tracks.size.should == 2
tracks[0]["id"].should == track.id
tracks[0]["instrument_id"].should == instrument.id
tracks[0]["sound"].should == "mono"
tracks[0]["client_track_id"].should == track.client_track_id
tracks[1]["instrument_id"].should == instrument.id
tracks[1]["sound"].should == "stereo"
tracks[1]["client_track_id"].should == "client_track_id1"
end
end

View File

@ -17,7 +17,7 @@ describe "User Progression", :type => :api do
describe "user progression" do
let(:user) { FactoryGirl.create(:user) }
let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono"}], :legal_terms => true, :intellectual_property => true} }
let(:defopts) { { :description => "a session", :fan_chat => true, :fan_access => true, :approval_required => false, :genres => ["classical"], :musician_access => true, :tracks => [{"instrument_id" => "electric guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}], :legal_terms => true, :intellectual_property => true} }
before do
login(user)
@ -105,11 +105,11 @@ describe "User Progression", :type => :api do
music_session = JSON.parse(last_response.body)
login(user2)
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client2.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
login(user3)
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client3.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono"}]}.to_json, "CONTENT_TYPE" => 'application/json'
post "/api/sessions/#{music_session["id"]}/participants.json", { :client_id => client3.client_id, :as_musician => true, :tracks => [{"instrument_id" => "bass guitar", "sound" => "mono", "client_track_id" => "client_track_guid"}]}.to_json, "CONTENT_TYPE" => 'application/json'
last_response.status.should eql(201)
# instrument the created_at of the music_history field to be at the beginning of time, so that we cross the 15 minute threshold of a 'real session

View File

@ -61,6 +61,7 @@ module JamWebsockets
end
def add_client(client_id, client_context)
@log.debug "adding client #{client_id} to @client_lookup"
@client_lookup[client_id] = client_context
end
@ -172,20 +173,26 @@ module JamWebsockets
client_id = routing_key["client.".length..-1]
@semaphore.synchronize do
client_context = @client_lookup[client_id]
client = client_context.client
msg = Jampb::ClientMessage.parse(msg)
if !client_context.nil?
@log.debug "client-directed message received from #{msg.from} to client #{client_id}"
client = client_context.client
unless client.nil?
msg = Jampb::ClientMessage.parse(msg)
EM.schedule do
@log.debug "sending client-directed down websocket to #{client_id}"
send_to_client(client, msg)
@log.debug "client-directed message received from #{msg.from} to client #{client_id}"
unless client.nil?
EM.schedule do
@log.debug "sending client-directed down websocket to #{client_id}"
send_to_client(client, msg)
end
else
@log.debug "client-directed message unroutable to disconnected client #{client_id}"
end
else
@log.debug "client-directed message unroutable to disconnected client #{client_id}"
@log.debug "Can't route message: no client connected with id #{client_id}"
end
end
rescue => e
@ -633,6 +640,10 @@ module JamWebsockets
# belong to
access_p2p(to_client_id, context.user, client_msg)
if to_client_id.nil? || to_client_id == 'undefined' # javascript translates to 'undefined' in many cases
raise SessionError, "empty client_id specified in peer-to-peer message"
end
# populate routing data
client_msg.from = client.client_id