1429 lines
64 KiB
JavaScript
1429 lines
64 KiB
JavaScript
(function(context,$) {
|
|
|
|
"use strict";
|
|
|
|
context.JK = context.JK || {};
|
|
context.JK.SessionScreen = function(app) {
|
|
var logger = context.JK.logger;
|
|
var sessionModel = null;
|
|
var sessionId;
|
|
var tracks = {};
|
|
var myTracks = [];
|
|
var mixers = [];
|
|
var configureTrackDialog;
|
|
var addNewGearDialog;
|
|
var localRecordingsDialog = null;
|
|
var recordingFinishedDialog = null;
|
|
var friendSelectorDialog = null;
|
|
var inviteMusiciansUtil = null;
|
|
var screenActive = false;
|
|
var currentMixerRangeMin = null;
|
|
var currentMixerRangeMax = null;
|
|
var lookingForMixersCount = 0;
|
|
var lookingForMixersTimer = null;
|
|
var lookingForMixers = {};
|
|
var $recordingTimer = null;
|
|
var recordingTimerInterval = null;
|
|
var startTimeDate = null;
|
|
var startingRecording = false; // double-click guard
|
|
var claimedRecording = null;
|
|
var playbackControls = null;
|
|
|
|
|
|
var rest = JK.Rest();
|
|
|
|
var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there.
|
|
|
|
var defaultParticipant = {
|
|
tracks: [{
|
|
instrument_id: "unknown"
|
|
}],
|
|
user: {
|
|
first_name: 'Unknown',
|
|
last_name: 'User',
|
|
photo_url: null
|
|
}
|
|
};
|
|
|
|
// Be sure to copy/extend these instead of modifying in place
|
|
var trackVuOpts = {
|
|
vuType: "vertical",
|
|
lightCount: 13,
|
|
lightWidth: 3,
|
|
lightHeight: 17
|
|
};
|
|
// Must add faderId key to this
|
|
var trackFaderOpts = {
|
|
faderType: "vertical",
|
|
height: 83
|
|
};
|
|
|
|
// Recreate ChannelGroupIDs ENUM from C++
|
|
var ChannelGroupIds = {
|
|
"MasterGroup": 0,
|
|
"MonitorGroup": 1,
|
|
"AudioInputMusicGroup": 2,
|
|
"AudioInputChatGroup": 3,
|
|
"MediaTrackGroup": 4,
|
|
"StreamOutMusicGroup": 5,
|
|
"StreamOutChatGroup": 6,
|
|
"UserMusicInputGroup": 7,
|
|
"UserChatInputGroup": 8,
|
|
"PeerAudioInputMusicGroup": 9,
|
|
"PeerMediaTrackGroup": 10
|
|
};
|
|
|
|
// recreate eThresholdType enum from MixerDialog.h
|
|
var alert_type = {
|
|
0: {"title": "", "message": ""}, // NO_EVENT,
|
|
1: {"title": "", "message": ""}, // BACKEND_ERROR: generic error - eg P2P message error
|
|
2: {"title": "", "message": ""}, // BACKEND_MIXER_CHANGE, - event that controls have been regenerated
|
|
3: {"title": "", "message": ""}, // PACKET_JTR,
|
|
4: { "title": "Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click here." }, // PACKET_LOSS
|
|
5: {"title": "", "message": ""}, // PACKET_LATE,
|
|
6: {"title": "", "message": ""}, // JTR_QUEUE_DEPTH,
|
|
7: {"title": "", "message": ""}, // NETWORK_JTR,
|
|
8: { "title": "Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, click here." }, // NETWORK_PING,
|
|
9: {"title": "", "message": ""}, // BITRATE_THROTTLE_WARN,
|
|
10: { "title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low,and this may impact your audio quality. For troubleshooting tips, click here." }, // BANDWIDTH_LOW
|
|
|
|
// IO related events
|
|
11: { "title": "Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here." }, // INPUT_IO_RATE
|
|
12: {"title": "", "message": ""}, // INPUT_IO_JTR,
|
|
13: { "title": "Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here." }, // OUTPUT_IO_RATE
|
|
14: {"title": "", "message": ""}, // OUTPUT_IO_JTR,
|
|
|
|
// CPU load related
|
|
15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, click here." }, // CPU_LOAD
|
|
16: {"title": "", "message": ""}, // DECODE_VIOLATIONS,
|
|
17: {"title": "", "message": ""}, // LAST_THRESHOLD
|
|
18: {"title": "", "message": ""}, // WIFI_NETWORK_ALERT, //user or peer is using wifi
|
|
19: {"title": "No Audio Configuration", "message": "You cannot join the session because you do not have a valid audio configuration."}, // NO_VALID_AUDIO_CONFIG, // alert the user to popup a config
|
|
20: {"title": "", "message": ""}, // AUDIO_DEVICE_NOT_PRESENT, // the audio device is not connected
|
|
21: {"title": "", "message": ""}, // RECORD_PLAYBACK_STATE, // record/playback events have occurred
|
|
22: {"title": "", "message": ""}, // RUN_UPDATE_CHECK_BACKGROUND, //this is auto check - do
|
|
23: {"title": "", "message": ""}, // RUN_UPDATE_CHECK_INTERACTIVE, //this is initiated by user
|
|
24: {"title": "", "message": ""}, // STUN_EVENT, // system completed stun test... come get the result
|
|
25: {"title": "No Audio", "message": "Your system is no longer transmitting audio. Other session members are unable to hear you."}, // DEAD_USER_WARN_EVENT, //the backend is not receiving audio from this peer
|
|
26: {"title": "No Audio", "message": "Your system is no longer transmitting audio. Other session members are unable to hear you."}, // DEAD_USER_REMOVE_EVENT, //the backend is removing the user from session as no audio is coming from this peer
|
|
27: {"title": "", "message": ""}, // WINDOW_CLOSE_BACKGROUND_MODE, //the user has closed the window and the client is now in background mode
|
|
28: {"title": "", "message": ""}, // WINDOW_OPEN_FOREGROUND_MODE, //the user has opened the window and the client is now in forground mode/
|
|
|
|
29: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_FAIL, //error of some sort - so can't broadcast
|
|
30: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_ACTIVE, //active
|
|
31: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_STOPPED, //stopped by server/user
|
|
32: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_PINNED, //node pinned by user
|
|
33: {"title": "", "message": ""}, // SESSION_LIVEBROADCAST_UNPINNED, //node unpinned by user
|
|
|
|
34: {"title": "", "message": ""}, // BACKEND_STATUS_MSG, //status/informational message
|
|
35: {"title": "", "message": ""}, // LOCAL_NETWORK_VARIANCE_HIGH,//the ping time via a hairpin for the user network is unnaturally high or variable.
|
|
|
|
//indicates problem with user computer stack or network itself (wifi, antivirus etc)
|
|
36: {"title": "", "message": ""}, // LOCAL_NETWORK_LATENCY_HIGH,
|
|
37: {"title": "", "message": ""}, // RECORDING_CLOSE, //update and remove tracks from front-end
|
|
38: {"title": "", "message": ""} // LAST_ALERT
|
|
};
|
|
|
|
|
|
function beforeShow(data) {
|
|
sessionId = data.id;
|
|
$('#session-mytracks-container').empty();
|
|
displayDoneRecording(); // assumption is that you can't join a recording session, so this should be safe
|
|
|
|
var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session");
|
|
shareDialog.initialize(context.JK.FacebookHelperInstance);
|
|
}
|
|
|
|
function alertCallback(type, text) {
|
|
if (type === 2) { // BACKEND_MIXER_CHANGE
|
|
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 if (type === 19) { // NO_VALID_AUDIO_CONFIG
|
|
app.notify({
|
|
"title": alert_type[type].title,
|
|
"text": text,
|
|
"icon_url": "/assets/content/icon_alert_big.png"
|
|
});
|
|
context.location = "/client#"; // leaveSession will be called in beforeHide below
|
|
}
|
|
else if (type === 24) { // STUN_EVENT
|
|
var testResults = context.jamClient.NetworkTestResult();
|
|
|
|
$.each(testResults, function(index, val) {
|
|
if (val.bStunFailed) {
|
|
// if true we could not reach a stun server
|
|
}
|
|
else if (val.bRemoteUdpBocked) {
|
|
// if true the user cannot communicate with peer via UDP, although they could do LAN based session
|
|
}
|
|
});
|
|
}
|
|
else if (type === 25) { // DEAD_USER_WARN_EVENT
|
|
app.notify({
|
|
"title": alert_type[type].title,
|
|
"text": text,
|
|
"icon_url": "/assets/content/icon_alert_big.png"
|
|
});
|
|
}
|
|
else if (type === 26) { // DEAD_USER_REMOVE_EVENT
|
|
app.notify({
|
|
"title": alert_type[type].title,
|
|
"text": text,
|
|
"icon_url": "/assets/content/icon_alert_big.png"
|
|
});
|
|
}
|
|
else {
|
|
context.setTimeout(function() {
|
|
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);
|
|
|
|
}
|
|
}
|
|
|
|
function afterShow(data) {
|
|
|
|
// indicate that the screen is active, so that
|
|
// body-scoped drag handlers can go active
|
|
screenActive = true;
|
|
|
|
// Subscribe for callbacks on audio events
|
|
context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback");
|
|
context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
|
|
context.jamClient.SessionSetAlertCallback("JK.AlertCallback");
|
|
context.jamClient.SessionSetConnectionStatusRefreshRate(1000);
|
|
|
|
// If you load this page directly, the loading of the current user
|
|
// is happening in parallel. We can't join the session until the
|
|
// current user has been completely loaded. Poll for the current user
|
|
// before proceeding with session joining.
|
|
function checkForCurrentUser() {
|
|
if (context.JK.userMe) {
|
|
afterCurrentUserLoaded();
|
|
} else {
|
|
context.setTimeout(checkForCurrentUser, 100);
|
|
}
|
|
}
|
|
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() {
|
|
// 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
|
|
// about the current session, if any (for example, reconnect logic)
|
|
context.JK.CurrentSessionModel = sessionModel = new context.JK.SessionModel(
|
|
context.JK.app,
|
|
context.JK.JamServer,
|
|
context.jamClient
|
|
);
|
|
|
|
$(sessionModel.recordingModel)
|
|
.on('startingRecording', function(e, data) {
|
|
displayStartingRecording();
|
|
})
|
|
.on('startedRecording', function(e, data) {
|
|
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 if(data.reason == 'rest') {
|
|
var jqXHR = detail[0];
|
|
app.notifyServerError(jqXHR);
|
|
}
|
|
else {
|
|
notifyWithUserInfo(title, 'Error Reason: ' + reason);
|
|
}
|
|
displayDoneRecording();
|
|
}
|
|
else
|
|
{
|
|
displayStartedRecording();
|
|
displayWhoCreated(data.clientId);
|
|
}
|
|
})
|
|
.on('stoppingRecording', function(e, data) {
|
|
displayStoppingRecording(data);
|
|
})
|
|
.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') {
|
|
notifyWithUserInfo(title, 'did not respond to the stop signal.', detail);
|
|
}
|
|
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 {
|
|
displayDoneRecording();
|
|
promptUserToSave(data.recordingId);
|
|
}
|
|
|
|
})
|
|
.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();
|
|
|
|
})
|
|
|
|
sessionModel.subscribe('sessionScreen', sessionChanged);
|
|
sessionModel.joinSession(sessionId)
|
|
.fail(function(xhr, textStatus, errorMessage) {
|
|
if(xhr.status == 404) {
|
|
// we tried to join the session, but it's already gone. kick user back to join session screen
|
|
context.window.location = "/client#/findSession";
|
|
app.notify(
|
|
{ title: "Unable to Join Session",
|
|
text: "The session you attempted to join is over."
|
|
},
|
|
{ no_cancel: true });
|
|
}
|
|
else {
|
|
app.ajaxError(xhr, textStatus, errorMessage);
|
|
}
|
|
});
|
|
}
|
|
|
|
function beforeHide(data) {
|
|
// track that the screen is inactive, to disable body-level handlers
|
|
screenActive = false;
|
|
sessionModel.leaveCurrentSession()
|
|
.fail(app.ajaxError);
|
|
}
|
|
|
|
function handleTransitionsInRecordingPlayback() {
|
|
// let's see if we detect a transition to start playback or stop playback
|
|
|
|
var currentSession = sessionModel.getCurrentSession();
|
|
|
|
if(claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) {
|
|
// this is a 'started with a claimed_recording' transition.
|
|
// we need to start a timer to watch for the state of the play session
|
|
playbackControls.startMonitor();
|
|
}
|
|
else if(claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) {
|
|
playbackControls.stopMonitor();
|
|
}
|
|
|
|
claimedRecording = currentSession == null ? null : currentSession.claimed_recording;
|
|
|
|
}
|
|
function sessionChanged() {
|
|
|
|
handleTransitionsInRecordingPlayback();
|
|
// TODO - in the specific case of a user changing their tracks using the configureTrack dialog,
|
|
// this event appears to fire before the underlying mixers have updated. I have no event to
|
|
// know definitively when the underlying mixers are up to date, so for now, we just delay slightly.
|
|
// This obviously has the possibility of introducing time-based bugs.
|
|
context.setTimeout(renderSession, RENDER_SESSION_DELAY);
|
|
}
|
|
|
|
/**
|
|
* the mixers object is a list. In order to find one by key,
|
|
* you must iterate. Convenience method to locate a particular
|
|
* mixer by id.
|
|
*/
|
|
function getMixer(mixerId) {
|
|
var foundMixer = null;
|
|
$.each(mixers, function(index, mixer) {
|
|
if (mixer.id === mixerId) {
|
|
foundMixer = mixer;
|
|
}
|
|
});
|
|
return foundMixer;
|
|
}
|
|
|
|
function renderSession() {
|
|
$('#session-mytracks-container').empty();
|
|
$('.session-track').remove(); // Remove previous tracks
|
|
var $voiceChat = $('#voice-chat');
|
|
$voiceChat.hide();
|
|
_updateMixers();
|
|
_renderTracks();
|
|
_renderLocalMediaTracks();
|
|
_wireTopVolume();
|
|
_wireTopMix();
|
|
_addVoiceChat();
|
|
_initDialogs();
|
|
if ($('.session-livetracks .track').length === 0) {
|
|
$('.session-livetracks .when-empty').show();
|
|
}
|
|
if ($('.session-recordings .track').length === 0) {
|
|
$('.session-recordings .when-empty').show();
|
|
$('.session-recording-name-wrapper').hide();
|
|
$('.session-recordings .recording-controls').hide();
|
|
}
|
|
}
|
|
|
|
function _initDialogs() {
|
|
configureTrackDialog.initialize();
|
|
addNewGearDialog.initialize();
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Always add a hard-coded simplified 'mixer' for the L2M mix
|
|
var l2m_mixer = {
|
|
id: '__L2M__',
|
|
range_low: -80,
|
|
range_high: 20,
|
|
volume_left: context.jamClient.SessionGetMasterLocalMix()
|
|
};
|
|
mixers.push(l2m_mixer);
|
|
}
|
|
|
|
function _mixersForGroupId(groupId) {
|
|
var foundMixers = [];
|
|
$.each(mixers, function(index, mixer) {
|
|
if (mixer.group_id === groupId) {
|
|
foundMixers.push(mixer);
|
|
}
|
|
|
|
});
|
|
return foundMixers;
|
|
}
|
|
|
|
// TODO FIXME - This needs to support multiple tracks for an individual
|
|
// client id and group.
|
|
function _mixerForClientId(clientId, groupIds, usedMixers) {
|
|
var foundMixer = null;
|
|
$.each(mixers, function(index, mixer) {
|
|
if (mixer.client_id === clientId) {
|
|
for (var i=0; i<groupIds.length; i++) {
|
|
if (mixer.group_id === groupIds[i]) {
|
|
if (!(mixer.id in usedMixers)) {
|
|
foundMixer = mixer;
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return foundMixer;
|
|
}
|
|
|
|
function _wireTopVolume() {
|
|
var gainPercent = 0;
|
|
var mixerIds = [];
|
|
$.each(mixers, function(index, mixer) {
|
|
if (mixer.group_id === ChannelGroupIds.MasterGroup) {
|
|
mixerIds.push(mixer.id);
|
|
gainPercent = percentFromMixerValue(
|
|
mixer.range_low, mixer.range_high, mixer.volume_left);
|
|
}
|
|
if (mixer.group_id === ChannelGroupIds.MonitorGroup) {
|
|
mixerIds.push(mixer.id);
|
|
}
|
|
});
|
|
var faderId = mixerIds.join(',');
|
|
$('#volume').attr('mixer-id', faderId);
|
|
var faderOpts = {
|
|
faderId: faderId,
|
|
faderType: "horizontal",
|
|
width: 50,
|
|
style: {
|
|
"background-image": "none",
|
|
"background-repeat":"no-repeat",
|
|
"height": "24px"
|
|
}
|
|
};
|
|
context.JK.FaderHelpers.renderFader("#volume", faderOpts);
|
|
context.JK.FaderHelpers.subscribe(faderId, faderChanged);
|
|
// Visually update fader to underlying mixer start value.
|
|
// Always do this, even if gainPercent is zero.
|
|
|
|
context.JK.FaderHelpers.setFaderValue(faderId, gainPercent);
|
|
}
|
|
|
|
/**
|
|
* This control has it's own Set/Get methods, so we don't need to
|
|
* line it up with some mixer later. We'll use a special mixer-id value
|
|
* to let us know we're dealing with the mix control.
|
|
*/
|
|
function _wireTopMix() {
|
|
var $mixSlider = $('#l2m');
|
|
var l2m_mixer = {
|
|
range_low: -80,
|
|
range_high: 20,
|
|
volume_left: context.jamClient.SessionGetMasterLocalMix()
|
|
};
|
|
// var gainPercent = percentFromMixerValue(
|
|
// l2m_mixer.range_low, l2m_mixer.range_high, l2m_mixer.volume_left);
|
|
|
|
var faderId = '#l2m'; // also the selector for renderFader
|
|
|
|
var faderOpts = {
|
|
faderId: faderId,
|
|
faderType: "horizontal",
|
|
width: 70,
|
|
style: {
|
|
"background-image": "none",
|
|
"background-repeat":"no-repeat",
|
|
"height": "24px"
|
|
}
|
|
};
|
|
context.JK.FaderHelpers.renderFader(faderId, faderOpts);
|
|
context.JK.FaderHelpers.subscribe(faderId, l2mChanged);
|
|
|
|
// initialize to middle (50%, 0dB) per Peter's request
|
|
context.JK.FaderHelpers.setFaderValue(faderId, 50);
|
|
context.jamClient.SessionSetMasterLocalMix(0);
|
|
}
|
|
|
|
/**
|
|
* This has a specialized jamClient call, so custom handler.
|
|
*/
|
|
function l2mChanged(faderId, newValue, dragging) {
|
|
var dbValue = context.JK.FaderHelpers.convertLinearToDb(newValue);
|
|
context.jamClient.SessionSetMasterLocalMix(dbValue);
|
|
}
|
|
|
|
function _addVoiceChat() {
|
|
// If, and only if, there is a mixer in group 3 (voice chat)
|
|
// Add the voice chat controls below my tracks, and hook up the mixer.
|
|
// Assumption is that there is only ever one, so we just take the first one.
|
|
$.each(mixers, function(index, mixer) {
|
|
if (mixer.group_id === ChannelGroupIds.AudioInputChatGroup) {
|
|
var $voiceChat = $('#voice-chat');
|
|
$voiceChat.show();
|
|
$voiceChat.attr('mixer-id', mixer.id);
|
|
$('#voice-chat .voicechat-mute').attr('mixer-id', mixer.id);
|
|
var gainPercent = percentFromMixerValue(
|
|
mixer.range_low, mixer.range_high, mixer.volume_left);
|
|
var faderOpts = {
|
|
faderId: mixer.id,
|
|
faderType: "horizontal",
|
|
width: 50
|
|
};
|
|
context.JK.FaderHelpers.renderFader("#voice-chat .voicechat-gain", faderOpts);
|
|
context.JK.FaderHelpers.subscribe(mixer.id, faderChanged);
|
|
context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent);
|
|
if (mixer.mute) {
|
|
var $mute = $voiceChat.find('.voicechat-mute');
|
|
_toggleVisualMuteControl($mute, true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function _renderLocalMediaTracks() {
|
|
var localMediaMixers = _mixersForGroupId(ChannelGroupIds.MediaTrackGroup);
|
|
if(localMediaMixers.length == 0) {
|
|
localMediaMixers = _mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup);
|
|
}
|
|
|
|
var recordedTracks = sessionModel.recordedTracks();
|
|
|
|
console.log("recorded tracks=%o local_media=%o", recordedTracks, localMediaMixers);
|
|
|
|
if(recordedTracks && localMediaMixers.length == 0) {
|
|
// if we are the creator, then rather than raise an error, tell the server the recording is over.
|
|
// this shoudl only happen if we get temporarily disconnected by forced reload, which isn't a very normal scenario
|
|
if(sessionModel.getCurrentSession().claimed_recording_initiator_id == context.JK.userMe.id) {
|
|
closeRecording();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(recordedTracks) {
|
|
|
|
$('.session-recording-name').text(sessionModel.getCurrentSession().claimed_recording.name);
|
|
|
|
var noCorrespondingTracks = false;
|
|
$.each(localMediaMixers, function(index, mixer) {
|
|
var preMasteredClass = "";
|
|
// find the track or tracks that correspond to the mixer
|
|
var correspondingTracks = []
|
|
$.each(recordedTracks, function(i, recordedTrack) {
|
|
if(mixer.id.indexOf("L") == 0) {
|
|
if(mixer.id.substring(1) == recordedTrack.client_track_id) {
|
|
correspondingTracks.push(recordedTrack);
|
|
}
|
|
}
|
|
else if(mixer.id.indexOf("C") == 0) {
|
|
if(mixer.id.substring(1) == recordedTrack.client_id) {
|
|
correspondingTracks.push(recordedTrack);
|
|
preMasteredClass = "pre-mastered-track";
|
|
}
|
|
}
|
|
else {
|
|
// this should not be possible
|
|
alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id");
|
|
}
|
|
});
|
|
|
|
if(correspondingTracks.length == 0) {
|
|
noCorrespondingTracks = true;
|
|
app.notify({
|
|
title: "Unable to Open Recording",
|
|
text: "Could not correlate server and client tracks",
|
|
icon_url: "/assets/content/icon_alert_big.png"});
|
|
return false;
|
|
}
|
|
|
|
// prune found recorded tracks
|
|
recordedTracks = $.grep(recordedTracks, function(value) {
|
|
return $.inArray(value, correspondingTracks) < 0;
|
|
});
|
|
|
|
var oneOfTheTracks = correspondingTracks[0];
|
|
var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id);
|
|
var photoUrl = "/assets/content/icon_recording.png";
|
|
|
|
var name = oneOfTheTracks.user.name;
|
|
if (!(name)) {
|
|
name = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name;
|
|
}
|
|
|
|
|
|
// Default trackData to participant + no Mixer state.
|
|
var trackData = {
|
|
trackId: oneOfTheTracks.id,
|
|
clientId: oneOfTheTracks.client_id,
|
|
name: name,
|
|
instrumentIcon: instrumentIcon,
|
|
avatar: photoUrl,
|
|
latency: "good",
|
|
gainPercent: 0,
|
|
muteClass: 'muted',
|
|
mixerId: "",
|
|
avatarClass : 'avatar-recording',
|
|
preMasteredClass: preMasteredClass
|
|
};
|
|
|
|
var gainPercent = percentFromMixerValue(
|
|
mixer.range_low, mixer.range_high, mixer.volume_left);
|
|
var muteClass = "enabled";
|
|
if (mixer.mute) {
|
|
muteClass = "muted";
|
|
}
|
|
trackData.gainPercent = gainPercent;
|
|
trackData.muteClass = muteClass;
|
|
trackData.mixerId = mixer.id;
|
|
|
|
_addMediaTrack(index, trackData);
|
|
});
|
|
|
|
if(!noCorrespondingTracks && recordedTracks.length > 0) {
|
|
logger.error("unable to find all recorded tracks against client tracks");
|
|
app.notify({title:"All tracks not found",
|
|
text: "Some tracks in the recording are not present in the playback",
|
|
icon_url: "/assets/content/icon_alert_big.png"})
|
|
}
|
|
}
|
|
}
|
|
|
|
function _renderTracks() {
|
|
myTracks = [];
|
|
|
|
// Participants are here now, but the mixers don't update right away.
|
|
// Draw tracks from participants, then setup timers to look for the
|
|
// mixers that go with those participants, if they're missing.
|
|
lookingForMixersCount = 0;
|
|
$.each(sessionModel.participants(), function(index, participant) {
|
|
|
|
var name = participant.user.name;
|
|
if (!(name)) {
|
|
name = participant.user.first_name + ' ' + participant.user.last_name;
|
|
}
|
|
|
|
var usedMixers = {}; // Once we use a mixer, we add it here to allow us to find 'second' tracks
|
|
|
|
// loop through all tracks for each participant
|
|
$.each(participant.tracks, function(index, track) {
|
|
var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id);
|
|
var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url);
|
|
|
|
var myTrack = false;
|
|
|
|
// Default trackData to participant + no Mixer state.
|
|
var trackData = {
|
|
trackId: track.id,
|
|
connection_id: track.connection_id,
|
|
clientId: participant.client_id,
|
|
name: name,
|
|
instrumentIcon: instrumentIcon,
|
|
avatar: photoUrl,
|
|
latency: "good",
|
|
gainPercent: 0,
|
|
muteClass: 'muted',
|
|
mixerId: "",
|
|
avatarClass: 'avatar-med',
|
|
preMasteredClass: ""
|
|
};
|
|
|
|
// This is the likely cause of multi-track problems.
|
|
// This should really become _mixersForClientId and return a list.
|
|
// With multiple tracks, there will be more than one mixer for a
|
|
// particular client, in a particular group, and I'll need to further
|
|
// identify by track id or something similar.
|
|
var mixer = _mixerForClientId(
|
|
participant.client_id,
|
|
[
|
|
ChannelGroupIds.AudioInputMusicGroup,
|
|
ChannelGroupIds.PeerAudioInputMusicGroup
|
|
],
|
|
usedMixers);
|
|
if (mixer) {
|
|
usedMixers[mixer.id] = true;
|
|
myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup);
|
|
var gainPercent = percentFromMixerValue(
|
|
mixer.range_low, mixer.range_high, mixer.volume_left);
|
|
var muteClass = "enabled";
|
|
if (mixer.mute) {
|
|
muteClass = "muted";
|
|
}
|
|
trackData.gainPercent = gainPercent;
|
|
trackData.muteClass = muteClass;
|
|
trackData.mixerId = mixer.id;
|
|
context.jamClient.SessionSetUserName(participant.client_id,name);
|
|
} else { // No mixer to match, yet
|
|
lookingForMixers[track.id] = participant.client_id;
|
|
if (!(lookingForMixersTimer)) {
|
|
lookingForMixersTimer = context.setInterval(lookForMixers, 500);
|
|
}
|
|
}
|
|
|
|
_addTrack(index, trackData);
|
|
|
|
// Show settings icons only for my tracks
|
|
if (myTrack) {
|
|
myTracks.push(trackData);
|
|
}
|
|
// TODO: UNCOMMENT THIS WHEN TESTING LOCALLY IN BROWSER
|
|
//myTracks.push(trackData);
|
|
});
|
|
});
|
|
|
|
configureTrackDialog = new context.JK.ConfigureTrackDialog(app, myTracks, sessionId, sessionModel);
|
|
addNewGearDialog = new context.JK.AddNewGearDialog(app, ftueCallback);
|
|
}
|
|
|
|
function ftueCallback() {
|
|
context.location = "/client#/home";
|
|
app.layout.showDialog('ftue');
|
|
}
|
|
|
|
function connectTrackToMixer(trackSelector, clientId, mixerId, gainPercent) {
|
|
var vuOpts = $.extend({}, trackVuOpts);
|
|
var faderOpts = $.extend({}, trackFaderOpts);
|
|
faderOpts.faderId = mixerId;
|
|
var vuLeftSelector = trackSelector + " .track-vu-left";
|
|
var vuRightSelector = trackSelector + " .track-vu-right";
|
|
var faderSelector = trackSelector + " .track-gain";
|
|
var $track = $(trackSelector);
|
|
// Set mixer-id attributes and render VU/Fader
|
|
context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts);
|
|
$track.find('.track-vu-left').attr('mixer-id', mixerId + '_vul');
|
|
context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts);
|
|
$track.find('.track-vu-right').attr('mixer-id', mixerId + '_vur');
|
|
context.JK.FaderHelpers.renderFader(faderSelector, faderOpts);
|
|
// Set gain position
|
|
context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent);
|
|
context.JK.FaderHelpers.subscribe(mixerId, faderChanged);
|
|
}
|
|
|
|
// Function called on an interval when participants change. Mixers seem to
|
|
// show up later, so we render the tracks from participants, but keep track
|
|
// of the ones there weren't any mixers for, and continually try to find them
|
|
// and get them connected to the mixers underneath.
|
|
function lookForMixers() {
|
|
lookingForMixersCount++;
|
|
_updateMixers();
|
|
var usedMixers = {};
|
|
var keysToDelete = [];
|
|
for (var key in lookingForMixers) {
|
|
var clientId = lookingForMixers[key];
|
|
var mixer = _mixerForClientId(
|
|
clientId,
|
|
[
|
|
ChannelGroupIds.AudioInputMusicGroup,
|
|
ChannelGroupIds.PeerAudioInputMusicGroup
|
|
],
|
|
usedMixers);
|
|
if (mixer) {
|
|
usedMixers[mixer.id] = true;
|
|
keysToDelete.push(key);
|
|
var gainPercent = percentFromMixerValue(
|
|
mixer.range_low, mixer.range_high, mixer.volume_left);
|
|
var trackSelector = 'div.track[track-id="' + key + '"]';
|
|
connectTrackToMixer(trackSelector, key, mixer.id, gainPercent);
|
|
var $track = $('div.track[client-id="' + key + '"]');
|
|
$track.find('.track-icon-mute').attr('mixer-id', mixer.id);
|
|
// Set mute state
|
|
_toggleVisualMuteControl($track.find('.track-icon-mute'), mixer.mute);
|
|
}
|
|
}
|
|
|
|
for (var i=0; i<keysToDelete.length; i++) {
|
|
delete lookingForMixers[keysToDelete[i]];
|
|
}
|
|
|
|
if (context.JK.dlen(lookingForMixers) === 0 ||
|
|
lookingForMixersCount > 20) {
|
|
lookingForMixersCount = 0;
|
|
lookingForMixers = {};
|
|
context.clearTimeout(lookingForMixersTimer);
|
|
lookingForMixersTimer = null;
|
|
}
|
|
}
|
|
|
|
// Given a mixerID and a value between 0.0-1.0,
|
|
// light up the proper VU lights.
|
|
function _updateVU(mixerId, value) {
|
|
|
|
// Special-case for mono tracks. If mono, and it's a _vul id,
|
|
// update both sides, otherwise do nothing.
|
|
// If it's a stereo track, just do the normal thing.
|
|
var selector;
|
|
var pureMixerId = mixerId.replace("_vul", "");
|
|
pureMixerId = pureMixerId.replace("_vur", "");
|
|
var mixer = getMixer(pureMixerId);
|
|
if (mixer) {
|
|
if (!(mixer.stereo)) { // mono track
|
|
if (mixerId.substr(-4) === "_vul") {
|
|
// Do the left
|
|
selector = '#tracks [mixer-id="' + pureMixerId + '_vul"]';
|
|
context.JK.VuHelpers.updateVU(selector, value);
|
|
// Do the right
|
|
selector = '#tracks [mixer-id="' + pureMixerId + '_vur"]';
|
|
context.JK.VuHelpers.updateVU(selector, value);
|
|
} // otherwise, it's a mono track, _vur event - ignore.
|
|
} else { // stereo track
|
|
selector = '#tracks [mixer-id="' + mixerId + '"]';
|
|
context.JK.VuHelpers.updateVU(selector, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
function _addTrack(index, trackData) {
|
|
var parentSelector = '#session-mytracks-container';
|
|
var $destination = $(parentSelector);
|
|
if (trackData.clientId !== app.clientId) {
|
|
parentSelector = '#session-livetracks-container';
|
|
$destination = $(parentSelector);
|
|
$('.session-livetracks .when-empty').hide();
|
|
}
|
|
var template = $('#template-session-track').html();
|
|
var newTrack = $(context.JK.fillTemplate(template, trackData));
|
|
$destination.append(newTrack);
|
|
|
|
// Render VU meters and gain fader
|
|
var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
|
|
var gainPercent = trackData.gainPercent || 0;
|
|
connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent);
|
|
|
|
var $closeButton = $('#div-track-close', 'div[track-id="' + trackData.trackId + '"]');
|
|
if (index === 0) {
|
|
$closeButton.hide();
|
|
}
|
|
else {
|
|
$closeButton.click(deleteTrack);
|
|
}
|
|
|
|
tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
|
|
}
|
|
|
|
|
|
|
|
function _addMediaTrack(index, trackData) {
|
|
var parentSelector = '#session-recordedtracks-container';
|
|
var $destination = $(parentSelector);
|
|
$('.session-recordings .when-empty').hide();
|
|
$('.session-recording-name-wrapper').show();
|
|
$('.session-recordings .recording-controls').show();
|
|
|
|
var template = $('#template-session-track').html();
|
|
var newTrack = $(context.JK.fillTemplate(template, trackData));
|
|
$destination.append(newTrack);
|
|
if(trackData.preMasteredClass) {
|
|
context.JK.helpBubble($('.track-instrument', newTrack), 'pre-processed-track', {}, {offsetParent: newTrack.closest('.content-body')});
|
|
}
|
|
|
|
// Render VU meters and gain fader
|
|
var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
|
|
var gainPercent = trackData.gainPercent || 0;
|
|
connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent);
|
|
|
|
tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
|
|
}
|
|
|
|
|
|
/**
|
|
* Will be called when fader changes. The fader id (provided at subscribe time),
|
|
* the new value (0-100) and whether the fader is still being dragged are passed.
|
|
*/
|
|
function faderChanged(faderId, newValue, dragging) {
|
|
var mixerIds = faderId.split(',');
|
|
$.each(mixerIds, function(i,v) {
|
|
var broadcast = !(dragging); // If fader is still dragging, don't broadcast
|
|
fillTrackVolumeObject(v, broadcast);
|
|
setMixerVolume(v, newValue);
|
|
});
|
|
}
|
|
|
|
function handleVolumeChangeCallback(mixerId, isLeft, value, isMuted) {
|
|
// Visually update mixer
|
|
// There is no need to actually set the back-end mixer value as the
|
|
// back-end will already have updated the audio mixer directly prior to sending
|
|
// me this event. I simply need to visually show the new fader position.
|
|
// TODO: Use mixer's range
|
|
var faderValue = percentFromMixerValue(-80, 20, value);
|
|
context.JK.FaderHelpers.setFaderValue(mixerId, faderValue);
|
|
var $muteControl = $('[control="mute"][mixer-id="' + mixerId + '"]');
|
|
_toggleVisualMuteControl($muteControl, isMuted);
|
|
}
|
|
|
|
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 if (eventName === 'connection_status') {
|
|
// Connection Quality Change
|
|
var connectionClass = 'green';
|
|
if (value < 7) {
|
|
connectionClass = 'yellow';
|
|
}
|
|
if (value < 4) {
|
|
connectionClass = 'red';
|
|
}
|
|
|
|
var $connection = $('[mixer-id="' + mixerId + '_connection"]');
|
|
$connection.removeClass('green yellow red');
|
|
$connection.addClass(connectionClass);
|
|
|
|
} else if (eventName === 'add' || eventName === 'remove') {
|
|
// TODO - _renderSession. Note I get streams of these in
|
|
// sequence, so have Nat fix, or buffer/spam protect
|
|
// Note - this is already handled from websocket events.
|
|
// However, there may be use of these two events to avoid
|
|
// the polling-style check for when a mixer has been added
|
|
// to match a participant track.
|
|
} 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,
|
|
success: function(response) {
|
|
context.location="/client#/home";
|
|
},
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
logger.error("Error deleting session " + sessionId);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function deleteTrack(evt) {
|
|
var trackId = $(evt.currentTarget).attr("track-id");
|
|
sessionModel.deleteTrack(sessionId, trackId);
|
|
}
|
|
|
|
function _toggleVisualMuteControl($control, muting) {
|
|
if (muting) {
|
|
$control.removeClass('enabled');
|
|
$control.addClass('muted');
|
|
} else {
|
|
$control.removeClass('muted');
|
|
$control.addClass('enabled');
|
|
}
|
|
}
|
|
|
|
function _toggleAudioMute(mixerId, muting) {
|
|
fillTrackVolumeObject(mixerId);
|
|
context.trackVolumeObject.mute = muting;
|
|
context.jamClient.SessionSetControlState(mixerId);
|
|
}
|
|
|
|
function toggleMute(evt) {
|
|
var $control = $(evt.currentTarget);
|
|
var muting = ($control.hasClass('enabled'));
|
|
var mixerIds = $control.attr('mixer-id').split(',');
|
|
$.each(mixerIds, function(i,v) {
|
|
_toggleAudioMute(v, muting);
|
|
});
|
|
_toggleVisualMuteControl($control, muting);
|
|
}
|
|
|
|
function fillTrackVolumeObject(mixerId, broadcast) {
|
|
_updateMixers();
|
|
var mixer = null;
|
|
var _broadcast = true;
|
|
if (broadcast !== undefined) {
|
|
_broadcast = broadcast;
|
|
}
|
|
for (var i=0; i<mixers.length; i++) {
|
|
mixer = mixers[i];
|
|
if (mixer.id === mixerId) {
|
|
context.trackVolumeObject.clientID = mixer.client_id;
|
|
context.trackVolumeObject.broadcast = _broadcast;
|
|
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 mixer's min/max and current value, return it as
|
|
// a percent from 0-100. Return an integer.
|
|
function percentFromMixerValue(min, max, value) {
|
|
try {
|
|
var range = Math.abs(max - min);
|
|
var magnitude = value - min;
|
|
var percent = Math.round(100*(magnitude/range));
|
|
return percent;
|
|
} catch(err) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Given a mixer's min/max and a percent value, return it as
|
|
// the mixer's value. Returns an integer.
|
|
function percentToMixerValue(min, max, percent) {
|
|
var range = Math.abs(max - min);
|
|
var multiplier = percent/100; // Change 85 into 0.85
|
|
var value = min + (multiplier * range);
|
|
// Protect against percents < 0 and > 100
|
|
if (value < min) {
|
|
value = min;
|
|
}
|
|
if (value > max) {
|
|
value = max;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// Given a volume 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 sliderValue = percentToMixerValue(
|
|
currentMixerRangeMin, currentMixerRangeMax, volumePercent);
|
|
context.trackVolumeObject.volL = sliderValue;
|
|
context.trackVolumeObject.volR = sliderValue;
|
|
// Special case for L2M mix:
|
|
if (mixerId === '__L2M__') {
|
|
logger.debug("L2M volumePercent=" + volumePercent);
|
|
var dbValue = context.JK.FaderHelpers.convertLinearToDb(volumePercent);
|
|
context.jamClient.SessionSetMasterLocalMix(dbValue);
|
|
// context.jamClient.SessionSetMasterLocalMix(sliderValue);
|
|
} else {
|
|
context.jamClient.SessionSetControlState(mixerId);
|
|
}
|
|
}
|
|
|
|
function sessionResync(evt) {
|
|
evt.preventDefault();
|
|
var response = context.jamClient.SessionAudioResync();
|
|
if (response) {
|
|
app.notify({
|
|
"title": "Error",
|
|
"text": response,
|
|
"icon_url": "/assets/content/icon_alert_big.png"});
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// http://stackoverflow.com/questions/2604450/how-to-create-a-jquery-clock-timer
|
|
function updateRecordingTimer() {
|
|
|
|
function pretty_time_string(num) {
|
|
return ( num < 10 ? "0" : "" ) + num;
|
|
}
|
|
|
|
var total_seconds = (new Date - startTimeDate) / 1000;
|
|
|
|
var hours = Math.floor(total_seconds / 3600);
|
|
total_seconds = total_seconds % 3600;
|
|
|
|
var minutes = Math.floor(total_seconds / 60);
|
|
total_seconds = total_seconds % 60;
|
|
|
|
var seconds = Math.floor(total_seconds);
|
|
|
|
hours = pretty_time_string(hours);
|
|
minutes = pretty_time_string(minutes);
|
|
seconds = pretty_time_string(seconds);
|
|
|
|
if(hours > 0) {
|
|
var currentTimeString = hours + ":" + minutes + ":" + seconds;
|
|
}
|
|
else {
|
|
var currentTimeString = minutes + ":" + seconds;
|
|
}
|
|
|
|
$recordingTimer.text('(' + currentTimeString + ')');
|
|
}
|
|
|
|
function displayStartingRecording() {
|
|
|
|
$('#recording-start-stop').addClass('currently-recording');
|
|
|
|
$('#recording-status').text("Starting...")
|
|
}
|
|
|
|
function displayStartedRecording() {
|
|
startTimeDate = new Date;
|
|
$recordingTimer = $("<span id='recording-timer'>(0:00)</span>");
|
|
var $recordingStatus = $('<span></span>').append("<span>Stop Recording</span>").append($recordingTimer);
|
|
$('#recording-status').html( $recordingStatus );
|
|
recordingTimerInterval = setInterval(updateRecordingTimer, 1000);
|
|
}
|
|
|
|
function displayStoppingRecording(data) {
|
|
if(data) {
|
|
if(data.reason) {
|
|
app.notify({
|
|
"title": "Recording Aborted",
|
|
"text": "The recording was aborted due to '" + data.reason + '"',
|
|
"icon_url": "/assets/content/icon_alert_big.png"
|
|
});
|
|
}
|
|
}
|
|
|
|
$('#recording-status').text("Stopping...");
|
|
}
|
|
|
|
function displayDoneRecording() {
|
|
if(recordingTimerInterval) {
|
|
clearInterval(recordingTimerInterval);
|
|
recordingTimerInterval = null;
|
|
startTimeDate = null;
|
|
}
|
|
|
|
$recordingTimer = null;
|
|
|
|
$('#recording-start-stop').removeClass('currently-recording');
|
|
$('#recording-status').text("Make a Recording");
|
|
}
|
|
|
|
function displayWhoCreated(clientId) {
|
|
if(app.clientId != clientId) { // don't show to creator
|
|
sessionModel.findUserBy({clientId: clientId})
|
|
.done(function(user) {
|
|
app.notify({
|
|
"title": "Recording Started",
|
|
"text": user.name + " started a recording",
|
|
"icon_url": context.JK.resolveAvatarUrl(user.photo_url)
|
|
});
|
|
})
|
|
.fail(function() {
|
|
app.notify({
|
|
"title": "Recording Started",
|
|
"text": "Oops! Can't determine who started this recording",
|
|
"icon_url": "/assets/content/icon_alert_big.png"
|
|
});
|
|
})
|
|
}
|
|
}
|
|
|
|
function promptUserToSave(recordingId) {
|
|
rest.getRecording( {id: recordingId} )
|
|
.done(function(recording) {
|
|
recordingFinishedDialog.setRecording(recording);
|
|
app.layout.showDialog('recordingFinished');
|
|
})
|
|
.fail(app.ajaxError);
|
|
}
|
|
|
|
function openRecording(e) {
|
|
// just ignore the click if they are currently recording for now
|
|
if(sessionModel.recordingModel.isRecording()) {
|
|
app.notify({
|
|
"title": "Currently Recording",
|
|
"text": "You can't open a recording while creating a recording.",
|
|
"icon_url": "/assets/content/icon_alert_big.png"
|
|
});
|
|
return false;
|
|
}
|
|
|
|
if(!localRecordingsDialog.isShowing()) {
|
|
app.layout.showDialog('localRecordings');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function closeRecording() {
|
|
rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id})
|
|
.done(function() {
|
|
sessionModel.refreshCurrentSession();
|
|
})
|
|
.fail(function(jqXHR) {
|
|
app.notify({
|
|
"title": "Couldn't Stop Recording Playback",
|
|
"text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText,
|
|
"icon_url": "/assets/content/icon_alert_big.png"
|
|
});
|
|
});
|
|
|
|
context.jamClient.CloseRecording();
|
|
|
|
return false;
|
|
}
|
|
|
|
function onPause() {
|
|
logger.debug("calling jamClient.SessionStopPlay");
|
|
context.jamClient.SessionStopPlay();
|
|
}
|
|
|
|
function onPlay(e, data) {
|
|
logger.debug("calling jamClient.SessionStartPlay");
|
|
context.jamClient.SessionStartPlay(data.playbackMode);
|
|
}
|
|
|
|
function onChangePlayPosition(e, data){
|
|
logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")");
|
|
context.jamClient.SessionTrackSeekMs(data.positionMs);
|
|
}
|
|
|
|
function startStopRecording() {
|
|
if(sessionModel.recordingModel.isRecording()) {
|
|
sessionModel.recordingModel.stopRecording();
|
|
}
|
|
else {
|
|
sessionModel.recordingModel.startRecording();
|
|
}
|
|
}
|
|
|
|
function inviteMusicians() {
|
|
inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians', sessionId);
|
|
}
|
|
|
|
function events() {
|
|
$('#session-resync').on('click', sessionResync);
|
|
$('#session-contents').on("click", '[action="delete"]', deleteSession);
|
|
$('#tracks').on('click', 'div[control="mute"]', toggleMute);
|
|
$('#recording-start-stop').on('click', startStopRecording);
|
|
$('#open-a-recording').on('click', openRecording);
|
|
$('#session-invite-musicians').on('click', inviteMusicians);
|
|
$('#track-settings').click(function() {
|
|
configureTrackDialog.showVoiceChatPanel(true);
|
|
configureTrackDialog.showMusicAudioPanel(true);
|
|
});
|
|
|
|
$('#close-playback-recording').on('click', closeRecording);
|
|
$(playbackControls)
|
|
.on('pause', onPause)
|
|
.on('play', onPlay)
|
|
.on('change-position', onChangePlayPosition);
|
|
}
|
|
|
|
this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, inviteMusiciansUtilInstance) {
|
|
localRecordingsDialog = localRecordingsDialogInstance;
|
|
recordingFinishedDialog = recordingFinishedDialogInstance;
|
|
inviteMusiciansUtil = inviteMusiciansUtilInstance;
|
|
context.jamClient.SetVURefreshRate(150);
|
|
context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback");
|
|
playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls'));
|
|
events();
|
|
|
|
var screenBindings = {
|
|
'beforeShow': beforeShow,
|
|
'afterShow': afterShow,
|
|
'beforeHide': beforeHide
|
|
};
|
|
app.bindScreen('session', screenBindings);
|
|
};
|
|
|
|
|
|
this.tracks = tracks;
|
|
|
|
this.getCurrentSession = function() {
|
|
return sessionModel.getCurrentSession();
|
|
};
|
|
|
|
this.refreshCurrentSession = function() {
|
|
sessionModel.refreshCurrentSession();
|
|
};
|
|
|
|
context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback;
|
|
context.JK.HandleBridgeCallback = handleBridgeCallback;
|
|
context.JK.AlertCallback = alertCallback;
|
|
};
|
|
|
|
})(window,jQuery); |