jam-cloud/app/assets/javascripts/session.js

443 lines
17 KiB
JavaScript

(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.SessionScreen = function(app) {
var logger = context.JK.logger;
var sessionId;
var session = null;
var users = {}; // Light cache of user info for session users.
var tracks = {};
var mixers = [];
var $draggingFaderHandle = null;
var $draggingFader = null;
var currentMixerId = null;
var currentMixerRangeMin = null;
var currentMixerRangeMax = null;
var defaultParticipant = {
user: {
first_name: 'Unknown',
last_name: 'User',
photo_url: null
}
};
// Replicate the Channel Group enum from C++
var ChannelGroupIds = {
0: "MasterGroup",
1: "MonitorGroup",
2: "AudioInputMusicGroup",
3: "AudioInputChatGroup",
4: "MediaTrackGroup",
5: "StreamOutMusicGroup",
6: "StreamOutChatGroup",
7: "UserMusicInputGroup",
8: "UserChatInputGroup",
9: "PeerAudioInputMusicGroup"
};
// Group 0 is the master mix. Mixer ID should go with the top horizontal volume slider, and possibly new VU meter.
// Group 1 is the monitor mix. Currently no UI component.
// Group 2 is my local music input. Any tracks here go into "My Tracks"
// Group 3 is my local voice chat.
// Group 4 is media files -- for sure for me locally, possibly from others?
// Results for a live session where David joined my session.
// P2P message from my client to Davids (JamServer.js:121)
// Events fired:
// add,User@208.191.152.98#,0 (session.js:233)
// add,User@208.191.152.98_*,0
// add,User@208.191.152.98#,0
// add,User@208.191.152.98_*,0
// jamkazam.js:50 -- various PEER_MESSAGE here and there
// jamkazam.js:50 - USER_JOINED_MUSIC_SESSION (session_id, user_id, username: "David Wilson")
// my session refreshed (refreshSession.js:39) with 2 participants
// more PEER_MESSAGE
// Mixers updated (session.js:97)
// 4 now (instead of 2). New ones:
// client_id: David's Client Id
// group_id: 7
// id: User@208.191.152.98#
// volume_left: 0
// volume_right: 0
//
// client_id: ""
// group_id: 9
// id: User@208.191.152.98_*
// volume_left: 12
// volume:right: 15
//
// more add,User@208.191.152.98#,0 events.
//
// Eventually, Socket to server closed. (JamServer.js:86)
function beforeShow(data) {
sessionId = data.id;
}
function afterShow(data) {
context.JK.joinMusicSession(sessionId, app);
// Subscribe for callbacks on audio events
context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback");
$.ajax({
type: "GET",
url: "/api/sessions/" + sessionId,
success: updateSession
});
}
function beforeHide(data) {
// Move joinSession function to this file for ease of finding it?
context.JK.leaveMusicSession(sessionId);
// 'unregister' for callbacks
context.jamClient.SessionRegisterCallback("");
}
function updateSession(sessionData) {
session = sessionData;
updateParticipants(function() { renderSession(); });
}
/**
* Make sure that for each participant in the session, we have user info.
*/
function updateParticipants(onComplete) {
var callCount = 0;
$.each(session.participants, function(index, value) {
if (!(this.user.id in users)) {
callCount += 1;
$.ajax({
type: "GET",
url: "/api/users/" + this.user.id
}).done(function(user) {
callCount -= 1;
users[user.id] = user;
});
}
});
if (!(onComplete)) {
return;
}
// TODO: generalize this pattern. Likely needed elsewhere.
function checker() {
if (callCount === 0) {
onComplete();
} else {
context.setTimeout(checker, 10);
}
}
checker();
}
function renderSession() {
_updateMixers();
_fixClientIds();
_renderTracks();
}
// Get the latest list of underlying audio mixer channels
function _updateMixers() {
var mixerIds = context.jamClient.SessionGetIDs();
var holder = $.extend(true, {}, {mixers: context.jamClient.SessionGetControlState(mixerIds)});
mixers = holder.mixers;
}
function _fixClientIds() {
// Set the client_id of all the mixers that are in my local groups
for (var i=0; i< mixers.length; i++) {
if (mixers[i].group_id === 2) {
mixers[i].client_id = app.clientId;
}
}
}
function _participantForClientId(clientId) {
var foundParticipant = null;
$.each(session.participants, function(index, participant) {
if (participant.client_id === clientId) {
foundParticipant = participant;
}
});
return foundParticipant; // no matching participant
}
function _renderTracks() {
$.each(mixers, function(index, mixer) {
// Only handle local music input and peer music here.
if (mixer.group_id === 2 || mixer.group_id === 7) {
var participant = _participantForClientId(mixer.client_id);
if (!(participant)) {
participant = defaultParticipant;
}
var name = participant.user.name;
if (!(name)) {
name = participant.user.first_name + ' ' + participant.user.last_name;
}
var trackData = {
clientId: mixer.client_id,
name: name,
part: "Keyboard", // TODO - need this
avatar: participant.user.photo_url,
latency: "good",
vu: 0.0,
gain: 0.5,
mute: false,
mixerId: mixer.id
};
_addTrack(trackData);
}
});
}
// Given a mixerID and a value between 0.0-1.0,
// light up the proper VU lights.
function _updateVU(mixerId, value) {
// Minor tweak to support VU values -- the mixerId here will
// have a suffix added _vul or _vur to indicate the channel.
// There are 13 VU lights. Figure out how many to
// light based on the incoming value.
var i = 0;
var state = 'on';
var lights = Math.round(13 * value);
var selector = null;
var $light = null;
var colorClass = 'vu-green-';
// Remove all light classes from all lights
var allLightsSelector = '#tracks table[mixer-id="' + mixerId + '"] td.vulight';
$(allLightsSelector).removeClass('vu-green-off vu-green-on vu-red-off vu-red-on');
// Set the lights
for (i=0; i<13; i++) {
colorClass = 'vu-green-';
state = 'on';
if (i > 8) {
colorClass = 'vu-red-';
}
if (i >= lights) {
state = 'off';
}
selector = '#tracks table[mixer-id="' + mixerId + '"] td.vu' + i;
$light = $(selector);
$light.addClass(colorClass + state);
}
}
function _addTrack(trackData) {
var $destination = $('#session-mytracks-container');
if (trackData.clientId !== app.clientId) {
$destination = $('#session-livetracks-container');
}
trackData["left-vu"] = $('#template-vu').html();
trackData["right-vu"] = trackData["left-vu"];
var template = $('#template-session-track').html();
var newTrack = context.JK.fillTemplate(template, trackData);
$destination.append(newTrack);
tracks[trackData.clientId] = new context.JK.SessionTrack(trackData.clientId);
}
function _userJoinedSession(header, payload) {
// Just refetch the session and update.
$.ajax({
type: "GET",
url: "/api/sessions/" + sessionId
}).done(function(response) {
updateSession(response);
});
}
function handleBridgeCallback() {
var eventName = null;
var mixerId = null;
var value = null;
var tuples = arguments.length / 3;
for (var i=0; i<tuples; i++) {
eventName = arguments[3*i];
mixerId = arguments[(3*i)+1];
value = arguments[(3*i)+2];
var vuVal = 0.0;
if (eventName === 'left_vu' || eventName === 'right_vu') {
// TODO - no guarantee range will be -80 to 20. Get from the
// GetControlState for this mixer which returns min/max
// value is a DB value from -80 to 20. Convert to float from 0.0-1.0
vuVal = (value + 80) / 100;
if (eventName === 'left_vu') {
mixerId = mixerId + "_vul";
} else {
mixerId = mixerId + "_vur";
}
_updateVU(mixerId, vuVal);
} else {
// Examples of other events
// Add media file track: "add", "The_Abyss_4T", 0
logger.debug('non-vu event: ' + eventName + ',' + mixerId + ',' + value);
}
}
}
function deleteSession(evt) {
var sessionId = $(evt.currentTarget).attr("action-id");
if (sessionId) {
$.ajax({
type: "DELETE",
url: "/api/sessions/" + sessionId
}).done(
function() { context.location="#/home"; }
);
}
}
function _toggleVisualMuteControl($control, muting) {
if (muting) {
$control.removeClass('enabled');
$control.addClass('muted');
} else {
$control.removeClass('muted');
$control.addClass('enabled');
}
}
function _toggleAudioMute(mixerId, muting) {
context.trackVolumeObject.mute = muting;
context.jamClient.SessionSetControlState(mixerId);
}
function toggleMute(evt) {
var $control = $(evt.currentTarget);
var mixerId = $control.attr('mixer-id');
fillTrackVolumeObject(currentMixerId);
var muting = ($control.hasClass('enabled'));
_toggleVisualMuteControl($control, muting);
_toggleAudioMute(mixerId, muting);
}
function getFaderPercent(eventY, $fader) {
var faderPosition = $fader.offset();
var faderHeight = $fader.height();
var handleValue = faderHeight - (eventY - faderPosition.top);
var faderPct = Math.round(handleValue/faderHeight * 100);
if (faderPct < 0) {
faderPct = 0;
}
if (faderPct > 100) {
faderPct = 100;
}
return faderPct;
}
function fillTrackVolumeObject(mixerId) {
var mixer = null;
for (var i=0; i<mixers.length; i++) {
mixer = mixers[i];
if (mixer.id === mixerId) {
context.trackVolumeObject.clientID = mixer.client_id;
context.trackVolumeObject.master = mixer.master;
context.trackVolumeObject.monitor = mixer.monitor;
context.trackVolumeObject.mute = mixer.mute;
context.trackVolumeObject.name = mixer.name;
context.trackVolumeObject.record = mixer.record;
context.trackVolumeObject.volL = mixer.volume_left;
context.trackVolumeObject.volR = mixer.volume_right;
// trackVolumeObject doesn't have a place for range min/max
currentMixerRangeMin = mixer.range_low;
currentMixerRangeMax = mixer.range_high;
break;
}
}
}
// Given a volumne percent (0-100), set the underlying
// audio volume level of the passed mixerId to the correct
// value.
function setMixerVolume(mixerId, volumePercent) {
// The context.trackVolumeObject has been filled with the mixer values
// that go with mixerId, and the range of that mixer
// has been set in currentMixerRangeMin-Max.
// All that needs doing is to translate the incoming percent
// into the real value ont the sliders range. Set Left/Right
// volumes on trackVolumeObject, and call SetControlState to stick.
var sliderRange = currentMixerRangeMax - currentMixerRangeMin;
volumePercent = volumePercent/100;
var sliderValue = currentMixerRangeMin + (volumePercent * sliderRange);
context.trackVolumeObject.volL = sliderValue;
context.trackVolumeObject.volR = sliderValue;
context.jamClient.SessionSetControlState(mixerId);
}
function faderClick(evt) {
evt.stopPropagation();
if ($draggingFaderHandle) {
return;
}
var $fader = $(evt.currentTarget);
var $handle = $fader.find('div[control="fader-handle"]');
var faderPct = getFaderPercent(evt.clientY, $fader);
var mixerId = $fader.closest('[mixer-id]').attr('mixer-id');
fillTrackVolumeObject(mixerId);
setMixerVolume(mixerId, faderPct);
if (faderPct > 90) { faderPct = 90; } // Visual limit
$handle.css('bottom', faderPct + '%');
return false;
}
function faderHandleDown(evt) {
evt.stopPropagation();
$draggingFaderHandle = $(evt.currentTarget);
$draggingFader = $draggingFaderHandle.closest('div[control="fader"]');
currentMixerId = $draggingFader.closest('[mixer-id]').attr('mixer-id');
fillTrackVolumeObject(currentMixerId);
return false;
}
function tracksMouseUp(evt) {
evt.stopPropagation();
if ($draggingFaderHandle) {
$draggingFaderHandle = null;
$draggingFader = null;
currentMixerId = null;
}
return false;
}
function tracksMouseMove(evt) {
// bail out early if there's no in-process drag
if (!($draggingFaderHandle)) {
return false;
}
evt.stopPropagation();
var faderPct = getFaderPercent(evt.clientY, $draggingFader);
setMixerVolume(currentMixerId, faderPct);
if (faderPct > 90) { faderPct = 90; } // Visual limit
$draggingFaderHandle.css('bottom', faderPct + '%');
return false;
}
function events() {
$('#session-contents').on("click", '[action="delete"]', deleteSession);
$('#tracks').on('click', 'div[control="mute"]', toggleMute);
$('#tracks').on('click', 'div[control="fader"]', faderClick);
$('#tracks').on('mousedown', 'div[control="fader-handle"]', faderHandleDown);
$('body').on('mouseup', tracksMouseUp);
$('#tracks').on('mousemove', tracksMouseMove);
}
this.initialize = function() {
events();
var screenBindings = {
'beforeShow': beforeShow,
'afterShow': afterShow,
'beforeHide': beforeHide
};
app.bindScreen('session', screenBindings);
app.subscribe(context.JK.MessageType.USER_JOINED_MUSIC_SESSION, _userJoinedSession);
};
this.tracks = tracks;
context.JK.HandleBridgeCallback = handleBridgeCallback;
};
})(window,jQuery);