(function(context,$) { "use strict"; context.JK = context.JK || {}; context.JK.SessionScreen = function(app) { var EVENTS = context.JK.EVENTS; var MIX_MODES = context.JK.MIX_MODES; var NAMED_MESSAGES = context.JK.NAMED_MESSAGES; var gearUtils = context.JK.GearUtils; var sessionUtils = context.JK.SessionUtils; var modUtils = context.JK.ModUtils; 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 rateSessionDialog = null; var friendInput = null; var sessionPageDone = null; var $recordingManagerViewer = null; var $screen = null; var $mixModeDropdown = null; var $templateMixerModeChange = 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 }; function beforeShow(data) { sessionId = data.id; if(!sessionId) { window.location = '/client#/home'; } 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 initializeSession() { // 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.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; } // The SessionModel is 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.ensureEnded(); } context.JK.CurrentSessionModel = sessionModel = new context.JK.SessionModel( context.JK.app, context.JK.JamServer, context.jamClient, self ); sessionModel.start(sessionId); // indicate that the screen is active, so that // body-scoped drag handlers can go active screenActive = true; gearUtils.guardAgainstInvalidConfiguration(app) .fail(function() { promptLeave = false; window.location = '/client#/home' }) .done(function(){ var result = sessionUtils.SessionPageEnter(); gearUtils.guardAgainstActiveProfileMissing(app, result) .fail(function(data) { promptLeave = false; if(data && data.reason == 'handled') { if(data.nav == 'BACK') { window.history.go(-1); } else { window.location = data.nav; } } else { window.location = '/client#/home'; } }) .done(function(){ sessionModel.waitForSessionPageEnterDone() .done(function(userTracks) { context.JK.CurrentSessionModel.setUserTracks(userTracks); initializeSession(); }) .fail(function(data) { if(data == "timeout") { context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.') } else if(data == 'session_over') { // do nothing; session ended before we got the user track info. just bail } else { contetx.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) } promptLeave = false; window.location = '/client#/home' }); }) }) } 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() { var sessionModel = context.JK.CurrentSessionModel; $(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. If this appears incorrect, try restarting JamKazam.'); } 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) { logger.warn("Recording Discarded: ", data); 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 if(xhr.status == 422) { var response = JSON.parse(xhr.responseText); if(response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track") { app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')); } else if(response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"]) { promptLeave = false; context.window.location = "/client#/findSession"; app.notify( { title: "Unable to Join Session", text: "The session is currently recording." }, null, true); } else { app.notifyServerError(xhr, 'Unable to Join Session'); } } 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(function(jqXHR) { if(jqXHR.status != 404) { logger.debug("leave session failed"); app.ajaxError(arguments) } }); } screenActive = false; sessionUtils.SessionPageLeave(); } 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; console.log("mixers", mixers) // grab the first mixer, and check the mode var newMixerMode;; if(mixers.length > 0) { newMixerMode = mixers[0]["mode"] if(newMixerMode === undefined) { logger.error("mixer mode not present. defaulting to personal") newMixerMode = MIX_MODES.PERSONAL; } } else { logger.error("no mixers present. defaulting mixer mode to personal") newMixerMode = MIX_MODES.PERSONAL; } sessionModel.setMixerMode(newMixerMode) // 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) { //logger.debug("clientId", clientId, "groupIds", groupIds, "mixers", mixers) 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 mixers = _groupedMixersForClientId( participant.client_id, [ ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.PeerAudioInputMusicGroup, ChannelGroupIds.UserMusicInputGroup ], usedMixers); var mixer = null; var oppositeMixer = null; if(mixers) { if(mixers[ChannelGroupIds.AudioInputMusicGroup]) { mixer = [ChannelGroupIds.AudioInputMusicGroup][0] } else if(sessionModel.isMasterMixMode() && mixers[ChannelGroupIds.PeerAudioInputMusicGroup]) { mixer = [ChannelGroupIds.PeerAudioInputMusicGroup][0] oppositeMixer = [ChannelGroupIds.UserMusicInputGroup][0] } else if(!sessionModel.isMasterMixMode() && mixers[ChannelGroupIds.UserMusicInputGroup]) { mixer = [ChannelGroupIds.UserMusicInputGroup][0] oppositeMixer = [ChannelGroupIds.PeerAudioInputMusicGroup][0] } } 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; trackData.group_id = mixer.group_id; trackData.oppositeMixer = oppositeMixer; 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, groupId) { 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 $fader = $(faderSelector).attr('mixer-id', mixerId).data('groupId', groupId) 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').data('groupId', groupId) context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts); $track.find('.track-vu-right').attr('mixer-id', mixerId + '_vur').data('groupId', groupId) context.JK.FaderHelpers.renderFader($fader, faderOpts); // Set gain position context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent); $fader.on('fader_change', 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 = null; var mixer = null; var oppositeMixer = null; if(mixers) { if(mixers[ChannelGroupIds.AudioInputMusicGroup]) { mixer = [ChannelGroupIds.AudioInputMusicGroup][0] } else if(sessionModel.isMasterMixMode() && mixers[ChannelGroupIds.PeerAudioInputMusicGroup]) { mixer = [ChannelGroupIds.PeerAudioInputMusicGroup][0] oppositeMixer = [ChannelGroupIds.UserMusicInputGroup][0] } else if(!sessionModel.isMasterMixMode() && mixers[ChannelGroupIds.UserMusicInputGroup]) { mixer = [ChannelGroupIds.UserMusicInputGroup][0] oppositeMixer = [ChannelGroupIds.PeerAudioInputMusicGroup][0] } } if (mixer) { var participant = (sessionModel.getParticipant(clientId) || {name:'unknown'}).name; logger.debug("found mixer=" + mixer.id + ", participant=" + participant) 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, mixer.group_id); var $track = $('div.track[client-id="' + clientId + '"]'); $track.find('.track-icon-mute').attr('mixer-id', mixer.id).attr('opposite-mixer-id', oppositeMixer.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, oppositeMixer.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'); } var participant = (sessionModel.getParticipant(clientId) || { user: {name: 'unknown'}}).user.name; logger.debug("still looking for mixer for participant=" + participant + ", clientId=" + clientId) } } 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, trackData.group_id); 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, null); 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(e, data) { var $target = $(this); var faderId = $target.attr('mixer-id'); var groupId = $target.data('groupId'); var mixerIds = faderId.split(','); $.each(mixerIds, function(i,v) { var broadcast = !(data.dragging); // If fader is still dragging, don't broadcast fillTrackVolumeObject(v, broadcast); setMixerVolume(v, data.percentage); if(groupId == ChannelGroupIds.UserMusicInputGroup) { // there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well context.JK.FaderHelpers.setFaderValue(v, data.percentage); } }); } 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 = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); context.trackVolumeObject.volR = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); // 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').one(EVENTS.DIALOG_CLOSED, function(e, data) { if(data.result && data.result.keep){ context.JK.prodBubble($recordingManagerViewer, 'file-manager-poke', {}, {positions:['top', 'left', 'right', 'bottom'], offsetParent: $screen.parent()}) } }) }) .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() { friendInput = inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians', sessionId); inviteMusiciansUtil.loadFriends(); $(friendInput).show(); } function onMixerModeChanged(e, data) { $mixModeDropdown.easyDropDown('select', data.mode, true); setTimeout(renderSession, 1); } function onUserChangeMixMode(e) { var mode = $mixModeDropdown.val() == "master" ? MIX_MODES.MASTER : MIX_MODES.PERSONAL; context.jamClient.SetMixerMode(mode) modUtils.shouldShow(NAMED_MESSAGES.MASTER_VS_PERSONAL_MIX).done(function(shouldShow) { if(shouldShow) { var modeChangeHtml = $($templateMixerModeChange.html()); context.JK.Banner.show({title: 'Master vs. Personal Mix', text: modeChangeHtml, no_show: NAMED_MESSAGES.MASTER_VS_PERSONAL_MIX}); } }) return true; } 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); $('#session-invite-musicians2').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); $(friendInput).focus(function() { $(this).val(''); }) $(document).on(EVENTS.MIXER_MODE_CHANGED, onMixerModeChanged) $mixModeDropdown.change(onUserChangeMixMode) } this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, friendSelectorDialog) { inviteMusiciansUtil = new JK.InviteMusiciansUtil(JK.app); inviteMusiciansUtil.initialize(friendSelectorDialog); localRecordingsDialog = localRecordingsDialogInstance; recordingFinishedDialog = recordingFinishedDialogInstance; context.jamClient.SetVURefreshRate(150); context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback"); playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls')); var screenBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow, 'beforeHide': beforeHide, 'beforeLeave' : beforeLeave, 'beforeDisconnect' : beforeDisconnect, }; app.bindScreen('session', screenBindings); $recordingManagerViewer = $('#recording-manager-viewer'); $screen = $('#session-screen'); $mixModeDropdown = $screen.find('select.monitor-mode') $templateMixerModeChange = $('#template-mixer-mode-change'); events(); // make sure no previous plays are still going on by accident context.jamClient.SessionStopPlay(); if(context.jamClient.SessionRemoveAllPlayTracks) { // upgrade guard context.jamClient.SessionRemoveAllPlayTracks(); } }; 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; }; })(window,jQuery);