(function(context,$) { "use strict"; context.JK = context.JK || {}; context.JK.SessionScreen = function(app) { var logger = context.JK.logger; var self = this; 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 promptLeave = false; var backendMixerAlertThrottleTimer = null; var rateSessionDialog = null; var rest = context.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": "High Packet Jitter", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // PACKET_JTR, 4: {"title": "High 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": "High Packet Late", "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_LATE, 6: {"title": "Large Jitter Queue", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // JTR_QUEUE_DEPTH, 7: {"title": "High Network Jitter", "message": "Your network connection is currently experiencing network jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // NETWORK_JTR, 8: {"title": "High 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": "Bandwidth Throttled", "message": "The available bandwidth on your network has diminished, and this may impact your audio quality. For troubleshooting tips, click here."}, // 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": "Variable 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": "High Input Jitter", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here."}, // INPUT_IO_JTR, 13:{"title": "Variable 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": "High Output Jitter", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here."}, // 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": "Decode Violations", "message": ""}, // DECODE_VIOLATIONS, 17: {"title": "", "message": ""}, // LAST_THRESHOLD 18: {"title": "Wifi Alert", "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, 20: {"title": "Audio Device Not Present", "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": "Failed to Broadcast", "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": "Client Pinned", "message": "This client will be the source of a broadcast."}, // SESSION_LIVEBROADCAST_PINNED, //node pinned by user 33: {"title": "Client No Longer Pinned", "message": "This client is no longer designated as the source of the broadcast."}, // SESSION_LIVEBROADCAST_UNPINNED, //node unpinned by user 34: {"title": "", "message": ""}, // BACKEND_STATUS_MSG, //status/informational message 35: {"title": "LAN Unpredictable", "message": "Your local network is adding considerable variance to transmit times. For troubleshooting tips, click here."}, // 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": "LAN High Latency", "message": "Your local network is adding considerable latency. For troubleshooting tips, click here."}, // LOCAL_NETWORK_LATENCY_HIGH, 37: {"title": "", "message": ""}, // RECORDING_CLOSE, //update and remove tracks from front-end 38: {"title": "No Audio Sent", "message": ""}, // PEER_REPORTS_NO_AUDIO_RECV, //update and remove tracks from front-end 39: {"title": "", "message": ""} // LAST_ALERT }; function beforeShow(data) { sessionId = data.id; promptLeave = true; $('#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 beforeDisconnect() { return { freezeInteraction: true }; } function alertCallback(type, text) { function timeCallback() { var start = new Date(); setTimeout(function() { var timed = new Date().getTime() - start.getTime(); if(timed > 250) { logger.warn("SLOW AlERT_CALLBACK. type: %o text: %o time: %o", type, text, timed); } }, 1); } timeCallback(); logger.debug("alert callback", type, text); if (type === 2) { // BACKEND_MIXER_CHANGE logger.debug("BACKEND_MIXER_CHANGE alert. reason:" + text); if(sessionModel.id() && text == "RebuildAudioIoControl") { // the backend will send these events rapid-fire back to back. // the server can still perform correctly, but it is nicer to wait 100 ms to let them all fall through if(backendMixerAlertThrottleTimer) {clearTimeout(backendMixerAlertThrottleTimer);} backendMixerAlertThrottleTimer = setTimeout(function() { // 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() { }) .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" }); }) }, 100); } else if(sessionModel.id() && (text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl')) { sessionModel.refreshCurrentSession(true); } } 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 === 26) { var clientId = text; var participant = sessionModel.getParticipant(clientId); if(participant) { app.notify({ "title": alert_type[type].title, "text": participant.user.name + " is no longer sending audio.", "icon_url": context.JK.resolveAvatarUrl(participant.user.photo_url) }); var $track = $('div.track[client-id="' + clientId + '"]'); $('.disabled-track-overlay', $track).show(); } } else if (type === 27) { // WINDOW_CLOSE_BACKGROUND_MODE // the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen promptLeave = false; context.location = '/client#/home' } else if(type != 30 && type != 31 && type != 21){ // these are handled elsewhere context.setTimeout(function() { var alert = alert_type[type]; if(alert && alert.title) { 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 initializeSession() { // 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 afterShow(data) { if(!context.JK.JamServer.connected) { promptLeave = false; app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); window.location = '/client#/home' return; } if (!context.JK.hasOneConfiguredDevice() || context.JK.TrackHelpers.getUserTracks(context.jamClient).length == 0) { app.afterFtue = function() { initializeSession(); }; app.cancelFtue = function() { promptLeave = false; window.location = '/client#/home' }; app.layout.startNewFtue(); } else { initializeSession(); } } 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) if(context.JK.CurrentSessionModel) { context.JK.CurrentSessionModel.unsubscribe(); } 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 promptLeave = false; context.window.location = "/client#/findSession"; app.notify( { title: "Unable to Join Session", text: "The session you attempted to join is over." }, null, true); } else { app.notifyServerError(xhr, 'Unable to Join Session'); } }); } // not leave session but leave screen function beforeLeave(data) { if(promptLeave) { var leaveSessionWarningDialog = new context.JK.LeaveSessionWarningDialog(context.JK.app, function() { promptLeave = false; context.location.hash = data.hash }); leaveSessionWarningDialog.initialize(); app.layout.showDialog('leave-session-warning'); return false; } return true; } function beforeHide(data) { if(screenActive) { // this path is possible if FTUE is invoked on session page, and they cancel sessionModel.leaveCurrentSession() .fail(app.ajaxError); } screenActive = false; } 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; } function _userMusicInputMixerForClientId(clientId) { var found = null; $.each(mixers, function(index, mixer) { if (mixer.group_id === ChannelGroupIds.UserMusicInputGroup && mixer.client_id == clientId) { found = mixer; return false; } }); return found; } function _clientIdForUserInputMixer(mixerId) { var found = null; $.each(mixers, function(index, mixer) { if (mixer.group_id === ChannelGroupIds.UserMusicInputGroup && mixer.id == mixerId) { found = mixer.client_id; return false; } }); return found; } // 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 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; trackData.noaudio = false; context.jamClient.SessionSetUserName(participant.client_id,name); } else { // No mixer to match, yet lookingForMixers[track.id] = participant.client_id; trackData.noaudio = true; if (!(lookingForMixersTimer)) { logger.debug("waiting for mixer to show up for track: " + track.id) lookingForMixersTimer = context.setInterval(lookForMixers, 500); } } var allowDelete = myTrack && index > 0; _addTrack(allowDelete, 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, self); } 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="' + clientId + '"]'); $track.find('.track-icon-mute').attr('mixer-id', mixer.id); // hide overlay for all tracks associated with this client id (if one mixer is present, then all tracks are valid) $('.disabled-track-overlay', $track).hide(); $('.track-connection', $track).removeClass('red yellow green').addClass('grey'); // Set mute state _toggleVisualMuteControl($track.find('.track-icon-mute'), mixer.mute); } else { // if 1 second has gone by and still no mixer, then we gray the participant's tracks if(lookingForMixersCount == 2) { var $track = $('div.track[client-id="' + clientId + '"]'); $('.disabled-track-overlay', $track).show(); $('.track-connection', $track).removeClass('red yellow green').addClass('red'); } } } for (var i=0; i 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(allowDelete, 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)); var audioOverlay = $('.disabled-track-overlay', newTrack); audioOverlay.hide(); // always start with overlay hidden, and only show if no audio persists $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 (!allowDelete) { $closeButton.hide(); } else { $closeButton.click(deleteTrack); } tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); } function _addMediaTrack(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 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 bailOut() { promptLeave = false; context.window.location = '/client#/home'; } function sessionLeave(evt) { evt.preventDefault(); rateSession(); bailOut(); return false; } function rateSession() { if (rateSessionDialog === null) { rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); rateSessionDialog.initialize(); } rateSessionDialog.showDialog(); return true; } 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 = $("(0:00)"); var $recordingStatus = $('').append("Stop Recording").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-leave').on('click', sessionLeave); $('#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.refresh(); 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, 'beforeLeave' : beforeLeave, 'beforeDisconnect' : beforeDisconnect, }; app.bindScreen('session', screenBindings); }; this.tracks = tracks; this.getCurrentSession = function() { return sessionModel.getCurrentSession(); }; this.refreshCurrentSession = function(force) { sessionModel.refreshCurrentSession(force); }; this.setPromptLeave = function(_promptLeave) { promptLeave = _promptLeave; } context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback; context.JK.HandleBridgeCallback = handleBridgeCallback; context.JK.AlertCallback = alertCallback; }; })(window,jQuery);