* master mix controls for creator or delegated

This commit is contained in:
Seth Call 2015-12-07 20:25:43 -06:00
parent 949ce8a513
commit 886df5d092
20 changed files with 286 additions and 32 deletions

View File

@ -314,4 +314,5 @@ giftcard.sql
add_description_to_crash_dumps.sql
acappella.sql
purchasable_gift_cards.sql
versionable_jamtracks.sql
versionable_jamtracks.sql
session_controller.sql

View File

@ -0,0 +1 @@
ALTER TABLE music_sessions ADD COLUMN session_controller_id VARCHAR(64) REFERENCES users(id);

View File

@ -28,13 +28,13 @@ module JamRuby
##### TODO: refactored to notification.rb but left here for backwards compatibility w/ connection_manager_spec.rb
def gather_friends(connection, user_id)
friend_ids = []
connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results|
friend_results.each do |friend_result|
friend_ids.push(friend_result['friend_id'])
end
friend_ids = []
connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results|
friend_results.each do |friend_result|
friend_ids.push(friend_result['friend_id'])
end
return friend_ids
end
return friend_ids
end
# this simulates music_session destroy callbacks with activerecord
@ -42,7 +42,7 @@ module JamRuby
music_session = ActiveMusicSession.find_by_id(music_session_id)
music_session.before_destroy if music_session
end
# reclaim the existing connection, if ip_address is not nil then perhaps a new address as well
def reconnect(conn, channel_id, reconnect_music_session_id, ip_address, connection_stale_time, connection_expire_time, udp_reachable, gateway)
music_session_id = nil
@ -65,11 +65,19 @@ module JamRuby
isp = JamIsp.lookup(addr)
#puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============")
if isp.nil? then ispid = 0 else ispid = isp.coid end
if isp.nil? then
ispid = 0
else
ispid = isp.coid
end
block = GeoIpBlocks.lookup(addr)
#puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============")
if block.nil? then locid = 0 else locid = block.locid end
if block.nil? then
locid = 0
else
locid = block.locid
end
location = GeoIpLocations.find_by_locid(locid)
if location.nil? || isp.nil? || block.nil?
@ -183,11 +191,19 @@ SQL
isp = JamIsp.lookup(addr)
#puts("============= JamIsp.lookup returns #{isp.inspect} for #{addr} =============")
if isp.nil? then ispid = 0 else ispid = isp.coid end
if isp.nil? then
ispid = 0
else
ispid = isp.coid
end
block = GeoIpBlocks.lookup(addr)
#puts("============= GeoIpBlocks.lookup returns #{block.inspect} for #{addr} =============")
if block.nil? then locid = 0 else locid = block.locid end
if block.nil? then
locid = 0
else
locid = block.locid
end
location = GeoIpLocations.find_by_locid(locid)
if location.nil? || isp.nil? || block.nil?
@ -199,11 +215,11 @@ SQL
lock_connections(conn)
conn.exec("INSERT INTO connections (user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, aasm_state, stale_time, expire_time, udp_reachable, gateway) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
[user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear
[user_id, client_id, channel_id, ip_address, client_type, addr, locidispid, Connection::CONNECT_STATE.to_s, connection_stale_time, connection_expire_time, udp_reachable, gateway]).clear
# we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends
conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result|
count = result.getvalue(0, 0) .to_i
count = result.getvalue(0, 0).to_i
# we're passing all this stuff so that the user record might be updated as well...
blk.call(conn, count) unless blk.nil?
end
@ -291,7 +307,7 @@ SQL
# destroy the music_session if it's empty
num_participants = nil
conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1",
conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1",
[previous_music_session_id]) do |result|
num_participants = result.getvalue(0, 0).to_i
end
@ -324,11 +340,65 @@ SQL
conn.exec("UPDATE active_music_sessions set jam_track_id = NULL, jam_track_initiator_id = NULL where jam_track_initiator_id = $1 and id = $2",
[user_id, previous_music_session_id])
update_session_controller(previous_music_session_id)
end
end
end
def update_session_controller(music_session_id)
active_music_session = ActiveMusicSession.find(music_session_id)
if active_music_session
music_session = active_music_session.music_session
if music_session.session_controller_id && !active_music_session.users.exists?(music_session.session_controller)
# find next in line, because the current 'session controller' is not part of the session
next_in_line(music_session, active_music_session)
end
end
end
# determine who should be session controller after someone leaves
def next_in_line(music_session, active_music_session)
session_users = active_music_session.users
# check friends 1st
session_friends = music_session.creator.friends && session_users
if session_friends.length > 0
music_session.session_controller = session_friends[0]
if music_session.save
active_music_session.tick_track_changes
Notification.send_tracks_changed(active_music_session)
return
end
end
# check invited 2nd
invited = music_session.invited_musicians && session_users
if invited.length > 0
music_session.session_controller = invited[0]
if music_session.save
active_music_session.tick_track_changes
Notification.send_tracks_changed(active_music_session)
return
end
end
# go by who joined earliest
earliest = active_music_sessions.connections.order(:joined_session_at).first
if earliest
music_session.session_controller = earliest
if music_session.save
active_music_session.tick_track_changes
Notification.send_tracks_changed(active_music_session)
return
end
end
music_session.creator
end
def join_music_session(user, client_id, music_session, as_musician, tracks, audio_latency, video_sources=nil)
connection = nil
@ -349,7 +419,10 @@ SQL
if connection.errors.any?
raise ActiveRecord::Rollback
else
update_session_controller(music_session.id)
end
end
connection
@ -383,6 +456,8 @@ SQL
if result.cmd_tuples == 1
@log.debug("disassociated music_session with connection for client_id=#{client_id}, user_id=#{user_id}")
update_session_controller(music_session.id)
JamRuby::MusicSessionUserHistory.removed_music_session(user_id, music_session_id)
session_checks(conn, previous_music_session_id, user_id)
blk.call() unless blk.nil?

View File

@ -36,6 +36,8 @@ module JamRuby
belongs_to :active_music_session, :class_name => 'JamRuby::ActiveMusicSession', foreign_key: :music_session_id
belongs_to :session_controller, :class_name => 'JamRuby::User', :foreign_key => :session_controller_id, :inverse_of => :controlled_sessions
has_many :music_session_user_histories, :class_name => "JamRuby::MusicSessionUserHistory", :foreign_key => "music_session_id", :dependent => :delete_all
has_many :comments, :class_name => "JamRuby::MusicSessionComment", :foreign_key => "music_session_id"
has_many :session_info_comments, :class_name => "JamRuby::SessionInfoComment", :foreign_key => "music_session_id"
@ -116,6 +118,7 @@ module JamRuby
new_session.open_rsvps = self.open_rsvps
new_session.is_unstructured_rsvp = self.is_unstructured_rsvp
new_session.legal_terms = true
new_session.session_controller = self.session_controller
# copy rsvp_slots, rsvp_requests, and rsvp_requests_rsvp_slots
RsvpSlot.find_each(:conditions => "music_session_id = '#{self.id}'") do |slot|
@ -255,6 +258,30 @@ module JamRuby
end
end
def set_session_controller(current_user, user)
# only allow update of session controller by the creator or the currently marked user
should_tick = false
if current_user != creator && current_user != self.session_controller
return should_tick
end
if active_music_session
if user
if active_music_session.users.exists?(user)
self.session_controller = user
should_tick = save
end
else
self.session_controller = nil
should_tick = save
end
end
should_tick
end
def self.index(current_user, user_id, band_id = nil, genre = nil)
hide_private = false
if current_user.id != user_id
@ -343,6 +370,7 @@ module JamRuby
ms.legal_terms = true
ms.open_rsvps = options[:open_rsvps] if options[:open_rsvps]
ms.creator = user
ms.session_controller = user
ms.create_type = options[:create_type]
ms.is_unstructured_rsvp = options[:isUnstructuredRsvp] if options[:isUnstructuredRsvp]
ms.scheduled_start = parse_scheduled_start(options[:start], options[:timezone]) if options[:start] && options[:timezone]

View File

@ -44,6 +44,8 @@ module JamRuby
belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id'
has_many :controlled_sessions, :class_name=> "JamRuby::MusicSession", inverse_of: :session_controller, foreign_key: :session_controller_id
# authorizations (for facebook, etc -- omniauth)
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"

View File

@ -66,6 +66,20 @@
$('#session-settings-fan-access').val('listen-chat-band');
}
var $controllerSelect = $('#session-settings-master-mix-controller')
$controllerSelect.empty()
var sessionUsers = context.SessionStore.helper.users()
$controllerSelect.append('<option value="">Any one can control the Master Mix</option>')
$.each(sessionUsers, function(userId, user) {
var selected = currentSession.session_controller_id == userId ? 'selected="selected"' : ''
$controllerSelect.append('<option value="' + userId + '"' + selected + '>' + user.name +'</option>')
})
var canEditController = currentSession.session_controller_id == context.JK.currentUserId || context.JK.currentUserId == currentSession.user_id
$controllerSelect.easyDropDown(canEditController ? 'enable' : 'disable')
/**
// notation files in the account screen. ugh.
$selectedFilenames.empty();
@ -84,10 +98,12 @@
context.JK.dropdown($('#session-settings-language'));
context.JK.dropdown($('#session-settings-musician-access'));
context.JK.dropdown($('#session-settings-fan-access'));
context.JK.dropdown($('#session-settings-master-mix-controller'));
var easyDropDownState = canPlayWithOthers.canPlay ? 'enable' : 'disable'
$('#session-settings-musician-access').easyDropDown(easyDropDownState)
$('#session-settings-fan-access').easyDropDown(easyDropDownState)
}
function addNotation(notation) {
@ -121,6 +137,7 @@
data.name = $('#session-settings-name').val();
data.description = $('#session-settings-description').val();
data.language = $('#session-settings-language').val();
data.session_controller = $('#session-settings-master-mix-controller').val()
// musician access
var musicianAccess = $('#session-settings-musician-access').val();
@ -148,7 +165,13 @@
data.fan_chat = true;
}
rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved);
rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved)
.done(function(response) {
context.SessionActions.updateSession.trigger(response);
})
.fail(function() {
app.notify({title: "Can't Update", text: "Unable to update session settings."})
})
return false;
}

View File

@ -23,7 +23,14 @@
var $fader = $(this);
var floaterConvert = $fader.data('floaterConverter')
var sessionModel = window.JK.CurrentSessionModel || null;
/**
if(!$fader.data('has-session-control')) {
var sessionControllerName = $fader.data('session-controller-name');
window.JK.prodBubble($fader, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['left', 'right'], offsetParent: $fader.closest('.top-parent'), duration:12000})
return false;
}*/
var mediaControlsDisabled = $fader.data('media-controls-disabled');
if(mediaControlsDisabled) {
var mediaTrackOpener = $fader.data('media-track-opener');
@ -173,7 +180,14 @@
var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled');
var mediaTrackOpener = $draggingFaderHandle.data('media-track-opener');
var sessionModel = window.JK.CurrentSessionModel || null;
/**
if(!$draggingFaderHandle.data('has-session-control')) {
var sessionControllerName = $draggingFaderHandle.data('session-controller-name');
window.JK.prodBubble($draggingFaderHandle, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['left', 'right'], offsetParent: $draggingFaderHandle.closest('.top-parent'), duration:12000})
return false;
}*/
if(mediaControlsDisabled) {
return false;
}
@ -267,16 +281,34 @@
throw ("renderFader: userOptions is required");
}
var renderDefaults = {
faderType: "vertical"
faderType: "vertical",
sessionController: null
};
var options = $.extend({}, renderDefaults, userOptions);
var sessionCanControl = true
var sessionControllerName = null
if(userOptions.sessionController) {
if(!userOptions.sessionController.can_control) {
sessionCanControl = false
console.log("userOptions.sessionController", userOptions.sessionController)
sessionControllerName = userOptions.sessionController.session_controller.name
}
}
selector.find('div[data-control="fader"]')
.data('media-controls-disabled', selector.data('media-controls-disabled'))
.data('media-track-opener', selector.data('media-track-opener'))
.data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers'))
.data('floaterConverter', floaterConverter)
.data('snap', userOptions.snap)
.data('has-session-control', sessionCanControl)
.data('session-controller-name', sessionControllerName)
if(userOptions.sessionController) {
}
selector.find('div[data-control="fader-handle"]').draggable({
drag: onFaderDrag,
@ -289,6 +321,8 @@
.data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers'))
.data('floaterConverter', floaterConverter)
.data('snap', userOptions.snap)
.data('has-session-control', sessionCanControl)
.data('session-controller-name', sessionControllerName)
// Embed any custom styles, applied to the .fader below selector
if ("style" in options) {

View File

@ -4,7 +4,17 @@ context = window
openDialog: (e) ->
e.preventDefault()
context.JK.app.layout.showDialog('session-master-mix-dialog')
sessionController = context.SessionStore.helper.sessionController()
# does this user have access to control the master mixer?
if sessionController.can_control
context.JK.app.layout.showDialog('session-master-mix-dialog')
else
sessionControllerName = sessionController.session_controller?.name
$node = $(this.getDOMNode())
window.JK.prodBubble($node, 'not-session-controller', {sessionControllerName:sessionControllerName}, {positions:['bottom'], offsetParent: $node.closest('.top-parent'), duration:12000})
render: () ->
`<a className="session-mixer button-grey left" onClick={this.openDialog}>

View File

@ -2,6 +2,7 @@ context = window
logger = context.JK.logger
ChannelGroupIds = context.JK.ChannelGroupIds
CategoryGroupIds = context.JK.CategoryGroupIds
MIX_MODES = context.JK.MIX_MODES
@SessionTrackGain = React.createClass({
@ -20,6 +21,8 @@ CategoryGroupIds = context.JK.CategoryGroupIds
groupId = $target.data('groupId')
mixers = @state.mixers.mixer
# if this is a media track, jam track , or media category, affect volume of both mixer and opposing mixer
if @state.mixers.mixer.group_id == ChannelGroupIds.MediaTrackGroup || @state.mixers.mixer.group_id == ChannelGroupIds.JamTrackGroup || ((@state.mixers.mixer.group_id == ChannelGroupIds.MonitorCatGroup || @state.mixers.mixer.group_id == ChannelGroupIds.MasterCatGroup) && @state.mixers.mixer.name == CategoryGroupIds.MediaTrack)
MixerActions.faderChanged(data, [@state.mixers.mixer, @state.mixers.oppositeMixer], @props.gainType)
@ -47,13 +50,18 @@ CategoryGroupIds = context.JK.CategoryGroupIds
if !$root.is('.track-gain')
logger.error("unknown root node")
context.JK.FaderHelpers.renderFader2($root, {faderType: 'vertical'});
# Initialize gain position
mixer = @state.mixers?.mixer
if mixer && $.isArray(mixer)
mixer = mixer[0]
sessionController = null
if mixer.mode == MIX_MODES.MASTER
# sessionController is only relevant for master mode
sessionController = this.props.sessionController
context.JK.FaderHelpers.renderFader2($root, {faderType: 'vertical', sessionController:sessionController});
MixerActions.initGain(mixer)
# watch for fader change events

View File

@ -24,10 +24,13 @@ ptrCount = 0
newMixers = mixers.refreshMixer(@state.mixers)
newMixers = {} unless newMixers?
this.setState({mixers: newMixers})
sessionController = sessionMixers.session.sessionController()
logger.debug("session controller", sessionController)
this.setState({mixers: newMixers, sessionController: sessionController})
getInitialState: () ->
{mixers: this.props.mixers, ptr: "STVH#{ptrCount++}" }
{mixers: this.props.mixers, ptr: "STVH#{ptrCount++}", sessionController: window.SessionStore.helper.sessionController()}
handleMute: (e) ->
e.preventDefault()
@ -85,7 +88,7 @@ ptrCount = 0
<div>Volume</div>
<div>{volume_left}dB</div>
</div>
<SessionTrackGain mixers={this.state.mixers} gainType={this.props.gainType} />
<SessionTrackGain mixers={this.state.mixers} gainType={this.props.gainType} sessionController={this.state.sessionController} />
<div className={classes} data-control="mute" data-mixer-id={muteMixerId} onClick={this.handleMute}/>
<input type="checkbox" name="mute"/>

View File

@ -18,6 +18,14 @@ context = window
else
[]
users: () ->
found = {}
for participant in @participants()
found[participant.user.id] = participant.user
found
otherParticipants: () ->
others = []
for participant in @participants()
@ -26,6 +34,26 @@ context = window
others.push(participant) unless myTrack
others
sessionController: () ->
info = {}
# XXX testing:
info["can_control"] = false
info["session_controller"] = @participants()[0].user
if @session
if @session.session_controller_id == null
info['session_controller'] = null
info['can_control'] = true
else
for participant in @participants()
if participant.user.id == @session.session_controller_id
info['session_controller'] = participant.user
info['can_control'] = participant.user.id == context.JK.currentUserId
break
info
# if any participant has the metronome open, then we say this session has the metronome open
isMetronomeOpen: () ->
@session? && @session.metronome_active

View File

@ -60,6 +60,11 @@ body.jam, body.web, .dialog{
}
}
.larger-text {
margin:10px;
font-size:12px;
line-height:125%;
}

View File

@ -171,7 +171,7 @@ class ApiMusicSessionsController < ApiController
end
def update
@music_session = MusicSessionManager.new.update(
@music_session = MusicSessionManager.new.update(current_user,
@music_session.music_session,
params[:name],
params[:description],
@ -180,14 +180,16 @@ class ApiMusicSessionsController < ApiController
params[:musician_access],
params[:approval_required],
params[:fan_chat],
params[:fan_access])
params[:fan_access],
params[:session_controller])
if @music_session.errors.any?
# we have to do this because api_session_detail_url will fail with a bad @music_session
response.status = :unprocessable_entity
respond_with @music_session
else
respond_with @music_session, responder: ApiResponder, :location => api_session_detail_url(@music_session)
@music_session = @music_session.active_music_session
respond_with_model(@music_session)
end
end
@ -573,7 +575,6 @@ class ApiMusicSessionsController < ApiController
end
end
def jam_track_open
unless @music_session.users.exists?(current_user)
raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR

View File

@ -0,0 +1,3 @@
object @music_session
extends "api_music_sessions/show"

View File

@ -37,12 +37,17 @@ else
end
end
node :share_url do |music_session|
node do |music_session|
session_props = {}
unless music_session.music_session.share_token.nil?
share_token_url(music_session.music_session.share_token.token)
session_props[:share_url] = share_token_url(music_session.music_session.share_token.token)
end
session_props[:session_controller_id] = music_session.music_session.session_controller_id
session_props
end
child(:connections => :participants) {
collection @music_sessions, :object_root => false
attributes :ip_address, :client_id, :joined_session_at, :audio_latency, :id, :metronome_open

View File

@ -0,0 +1,3 @@
object @music_session
extends "api_music_sessions/show"

View File

@ -222,6 +222,16 @@ script type="text/template" id="template-help-media-controls-disabled"
| Only the person who opened the recording can control the volume levels.
| {% } %}
script type="text/template" id="template-help-not-session-controller"
.not-session-controller.larger-text
| This feature controls the master mix for the session, which is used to set the mix levels for recordings and session broadcasts,
| &nbsp;so changes to the master mix affect all musicians in the session.
| &nbsp;Only {{data.sessionControllerName}} has control of the master mix for this session.
br
br
| If you want to change the personal mix i.e. the mix/levels that you personally hear you can use the volume sliders on each track on the session screen to do this, and it wont affect what other musicians in the session hear, so you can do this safely.
script type="text/template" id="template-help-jamtrack-controls-disabled"
| During a recording, volume and mute controls for JamTracks are disabled. So, get the session volume levels right before starting the recording.

View File

@ -35,6 +35,11 @@
%option{:value => "#{language[:id]}"}
= language[:label]
.clearall.left.w20.ib.mb10
Mix Controller:
.right.w75.ib.mb10
%select{:name => "master_mix_controller", :id => "session-settings-master-mix-controller"}
.clearall.left.w20.ib.mb10
Musician Access:
.right.w75.ib.mb10

View File

@ -227,6 +227,7 @@ SampleApp::Application.routes.draw do
match '/sessions/:id/backing_tracks/close' => 'api_music_sessions#backing_track_close', :via => :post
match '/sessions/:id/metronome/open' => 'api_music_sessions#metronome_open', :via => :post
match '/sessions/:id/metronome/close' => 'api_music_sessions#metronome_close', :via => :post
match '/sessions/:id/session_controller' => 'api_music_sessions#session_controller', :via => :post
# music session tracks
match '/sessions/:id/tracks' => 'api_music_sessions#track_create', :via => :post

View File

@ -67,7 +67,7 @@ class MusicSessionManager < BaseManager
end
# Update the session. If a field is left out (meaning, it's set to nil), it's not updated.
def update(music_session, name, description, genre, language, musician_access, approval_required, fan_chat, fan_access)
def update(current_user, music_session, name, description, genre, language, musician_access, approval_required, fan_chat, fan_access, session_controller_id)
music_session.name = name unless name.nil?
music_session.description = description unless description.nil?
@ -77,7 +77,15 @@ class MusicSessionManager < BaseManager
music_session.approval_required = approval_required unless approval_required.nil?
music_session.fan_chat = fan_chat unless fan_chat.nil?
music_session.fan_access = fan_access unless fan_access.nil?
session_controller = User.find(session_controller_id) if session_controller_id.present?
should_tick = music_session.set_session_controller(current_user, session_controller)
music_session.save
if should_tick && music_session.active_music_session
music_session.active_music_session.tick_track_changes
Notification.send_tracks_changed(music_session.active_music_session)
end
music_session
end