* VRFS-2456 - add listen function to find session page, and update of join/rsvp icons

This commit is contained in:
Seth Call 2014-12-19 14:00:49 -06:00
parent 3bac8252e9
commit 9876c00a7a
19 changed files with 276 additions and 80 deletions

View File

@ -14,11 +14,11 @@ module JamRuby
end
def self.mount_source_up_requested(mount)
Notification.send_subscription_message('mount', mount.id, {type: 'source_up_requested'}.to_json )
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_UP_REQUEST}.to_json )
end
def self.mount_source_down_requested(mount)
Notification.send_subscription_message('mount', mount.id, {type: 'source_down_requested'}.to_json )
Notification.send_subscription_message('mount', mount.id, {change_type: IcecastSourceChange::CHANGE_TYPE_MOUNT_DOWN_REQUEST}.to_json )
end
end
end

View File

@ -69,7 +69,7 @@ module JamRuby
#IcecastSourceChange.delete_all(["icecast_mount_id = ?", self.id]) if source_direction
# and tell anyone listening that the direction has changed
SubscriptionMessage.mount_source_direction(self)
# SubscriptionMessage.mount_source_direction(self)
end
# Note:
@ -139,7 +139,7 @@ module JamRuby
first = source_changes.first
# don't check source_changes if actual source state mirrors desired source state... just say we are good, and pass down relevant sourcing user ID if present
result = success_state('source_' + (source_direction ? 'up' : 'down'), first.nil? ? nil : first.user_id)
result = success_state('source_' + (sourced ? 'up' : 'down'), first.nil? ? nil : first.user_id)
elsif source_changes.count > 0
# if the desired source direction is up, but we haven't sourced yet... let's try and find out why
@ -200,7 +200,7 @@ module JamRuby
def source_up
with_lock do
self.sourced = true
self.sourced_needs_changing_at = nil
self.sourced_needs_changing_at = Time.now
self.no_config_changed = true
save(validate: false)
end
@ -209,7 +209,7 @@ module JamRuby
def source_down
with_lock do
self.sourced = false
self.sourced_needs_changing_at = nil
self.sourced_needs_changing_at = Time.now
self.no_config_changed = true
save(validate: false)
end

View File

@ -296,7 +296,7 @@ describe IcecastMount do
let(:mount_needing_source) {FactoryGirl.create(:iceast_mount_with_music_session, sourced: false, sourced_needs_changing_at: Time.now, listeners: 1)}
it "happy sourced mount" do
sourced_mount.state.should == success_state('source_down')
sourced_mount.state.should == success_state('source_up')
end
it "just transitioned" do
@ -317,7 +317,7 @@ describe IcecastMount do
it "happy sourced mount" do
mount_needing_source.sourced = true
mount_needing_source.save!
mount_needing_source.state.should == success_state('source_down', change1.user.id)
mount_needing_source.state.should == success_state('source_up', change1.user.id)
end
it "succeeded recently in correct transition" do

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -44,7 +44,8 @@
MUTE_SELECTED: 'mute_selected',
SUBSCRIBE_NOTIFICATION: 'subscribe_notification',
CONNECTION_UP: 'connection_up',
CONNECTION_DOWN: 'connection_down'
CONNECTION_DOWN: 'connection_down',
SCREEN_CHANGED: 'screen_changed'
};
context.JK.ALERT_NAMES = {

View File

@ -35,7 +35,6 @@
var WAIT_FOR_BUFFER_TIMEOUT = 5000;
var RETRY_ATTEMPTS = 5; // we try 4 times, so the user will wait up until RETRY_ATTEMPTS * WAIT_FOR_BUFFER_TIMEOUTS
var logger = context.JK.logger;
var rest = context.JK.Rest();
var $parent = $parentElement;
@ -44,10 +43,15 @@
var musicSessionId = null;
var waitForBufferingTimeout = null;
var fanAccess = null;
var audioSrc = null;
var audioType = null;
var retryAttempts = 0;
var self = this;
var mountInfo = null;
var $mountState = null;
var lazyAudioInit = options && options.lazyAudioInit;
var hoverOptions = (options && options.hoverOptions) ? options.hoverOptions : {}
var $detailHelper = options && options.detailHelper;
var destroyed = false;
@ -73,6 +77,18 @@
e.stopPropagation();
}
if(lazyAudioInit) {
if($audio.length == 0) {
$audio =
$('<audio preload="none">' +
'<source src="' + audioSrc + '" type="' + audioType + '" />' +
'</audio>')
$parent.append($audio)
audioDomElement = $audio.get(0);
audioBind();
}
}
if(destroyed) return;
if(!audioDomElement) throw "no audio element supplied; the user should not be able to attempt a play"
@ -117,7 +133,7 @@
if(destroyed) return;
if(!audioDomElement) throw "no audio element supplied; the user should not be able to attempt a pause"
if(!lazyAudioInit && !audioDomElement) throw "no audio element supplied; the user should not be able to attempt a pause"
transition(PlayStateNone);
@ -133,6 +149,12 @@
}
}
function onScreenChanged(e, data) {
if(context.JK.ListenBroadcastCurrentlyPlaying) {
context.JK.ListenBroadcastCurrentlyPlaying.pause();
}
}
// this is the only way to make audio stop buffering after the user hits pause
function recreateAudioElement() {
// jeez: http://stackoverflow.com/questions/4071872/html5-video-force-abort-of-buffering/13302599#13302599
@ -408,6 +430,8 @@
logger.error("unknown state: " + playState)
}
$parent.data('listenbroadcast-playstate', playState);
$parent.triggerHandler('statechange.listenBroadcast',
{
element: $parent,
@ -509,7 +533,9 @@
logger.debug("subscription notification received: type:" + data.type)
updateMountInfo(data.body.mount);
if(data.body.mount) {
updateMountInfo(data.body.mount);
}
var id = data.id
var type = data.type
@ -519,7 +545,7 @@
var $detailHolder = $mountState.find('.detail-items')
var msgType = data.body.type;
var sourceChange = data.body.source_change;
var sourceChange = data.body;
if(sourceChange) {
var $detail = addSourceChange($detailHolder, sourceChange)
if ($detail) {
@ -549,8 +575,8 @@
$detailText.text('Start Broadcast Requested')
}
else if(sourceChange.change_type == 'source_down_request') {
$who.text('<span>Server</span>')
$detailText.text('Start Broadcast Requested')
$who.html('<span>Server</span>')
$detailText.text('Stop Broadcast Requested')
}
else {
@ -597,43 +623,61 @@
return $detail;
}
function cleanupDetails() {
var mountId = $parent.data('mount-id')
if(mountId) {
context.JK.SubscriptionUtils.unsubscribe('mount', mountId)
}
$mountState = null;
mountInfo = null;
}
function openBubble() {
checkServer().done(function(response) {
var mountId = response.mount ? response.mount.id : null
if(mountId) {
rest.getMount({id: mountId})
.done(function (mount) {
mountInfo = mount
$parent.data('mount-id', mountId)
context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent)
$parent.btOn()
})
.fail(context.JK.app.ajaxError)
}
else {
mountInfo = null;
context.JK.app.layout.notify('This session can not currently broadcast')
}
})
.fail(function() {
logger.debug("session is over")
})
}
function bindHoverDetail() {
var stateHolderHtml = context._.template($('#template-listen-broadcast-state').html(), {}, {variable: 'data'});
context.JK.hoverBubble($parent, stateHolderHtml, {trigger: 'none', preShow: broadcastDetailPreShow, positions:['bottom'], width:'300px', height:'250px'})
var btOptions = $.extend({}, {trigger: 'none', preShow: broadcastDetailPreShow, postHide: cleanupDetails, closeWhenOthersOpen: true, positions:['bottom', 'right', 'top', 'left'], width:'300px', height:'250px'}, hoverOptions)
context.JK.hoverBubble($parent, stateHolderHtml, btOptions)
$parent.hoverIntent({
over: function() {
checkServer().done(function(response) {
var mountId = response.mount ? response.mount.id : null
// in case the bt is destroyed
$parent.on('remove', cleanupDetails);
if(mountId) {
rest.getMount({id: mountId})
.done(function (mount) {
mountInfo = mount
$parent.data('mount-id', mountId)
context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent)
$parent.btOn()
})
.fail(context.JK.app.ajaxError)
}
else {
mountInfo = null;
context.JK.app.layout.notify('This session can not currently broadcast')
}
})
.fail(function() {
logger.debug("session is over")
})
},
out: function() {
var mountId = $parent.data('mount-id')
context.JK.SubscriptionUtils.unsubscribe('mount', mountId)
$parent.btOff();
$mountState = null;
mountInfo = null;
}
})
if($detailHelper) {
$detailHelper.click(function() { openBubble(); return false; })
}
else {
$parent.hoverIntent({
over: function() {
openBubble();
},
out: function() {
}
})
}
}
function initialize() {
@ -645,23 +689,35 @@
if(fanAccess === null) throw 'fan-access must be specified in $parentElement';
fanAccess = $parent.attr('fan-access') === 'true' // coerce to boolean
if(lazyAudioInit) {
audioSrc = $parent.attr('data-audio-src');
if(audioSrc === null) throw 'data-audio-src must be specified in $parentElement';
audioType = $parent.attr('data-audio-type');
if(audioType === null) throw 'data-audio-type must be specified in $parentElement';
}
bindHoverDetail();
$audio = $('audio', $parent);
if($audio.length == 0) {
if($audio.length == 0 && !lazyAudioInit) {
return;
}
if($audio.length > 1) {
throw "more than one <audio> element found";
}
audioDomElement = $audio.get(0);
audioBind();
if(!lazyAudioInit) {
audioDomElement = $audio.get(0);
audioBind();
}
$parent.bind('play.listenBroadcast', play);
$parent.bind('pause.listenBroadcast', pause);
$parent.bind('destroy.listenBroadcast', destroy);
$(document).on(context.JK.EVENTS.SCREEN_CHANGED, onScreenChanged);
}
initialize();

View File

@ -542,6 +542,8 @@
logger.debug("Changing screen to " + currentScreen);
$(document).triggerHandler(EVENTS.SCREEN_CHANGED, {previousScreen: previousScreen, newScreen: currentScreen})
screenEvent(currentScreen, 'beforeShow', data);
// For now -- it seems we want it open always.

View File

@ -18,11 +18,63 @@
var $latencyTemplate = $('#template-latency');
var $musicianTemplate = $('#template-musician-info');
var showJoinLink = true;
var showListenLink = true;
var showRsvpLink = true;
function renderActiveSession(session, tbGroup, myAudioLatency) {
// related to listen
function stateChange(e, data) {
var $listenLink = e.element;
var $listenText = $('.listen-link-text', $listenLink);
var $listenDetails = $('.listen-link-details', $listenLink);
$('#actionHeader', tbGroup).html('JOIN');
if(data.displayText)
{
if(data.state == 'playing') {
$listenText.text('Stop Listening')
}
else {
$listenText.text(context.JK.toTitleCase(data.displayText))
$listenDetails.addClass('statusing')
}
}
if(data.isEnd) {
$listenText.text('Listen').removeClass('statusing')
stopPlay();
}
if(data.isSessionOver) {
$listenLink.removeClass('inprogress').addClass('ended')
}
}
function startPlay($listenLink) {
$listenLink.find('img').attr('src', '/assets/content/pause-icon.jpg');
$listenLink.trigger('play.listenBroadcast');
}
function stopPlay($listenLink) {
$listenLink.find('img').attr('src', '/assets/content/listen-icon.jpg');
$listenLink.trigger('pause.listenBroadcast');
}
function togglePlay() {
var $listenLink = $(this)
var $listenText = $('.listen-link-text', $listenLink);
var $listenDetails = $('.listen-link-details', $listenLink);
if($listenLink.data('listenbroadcast-playstate') == 'playing') {
$listenText.text('Listen')
$listenDetails.removeClass('statusing')
stopPlay($listenLink);
}
else {
startPlay($listenLink);
}
return false;
}
function renderActiveSession(session, tbGroup, myAudioLatency) {
var i = 0;
var inSessionUsersHtml = '', rsvpFirst3UsersHtml = '', rsvpRemainingUsersHtml = '', openSlotsFirst3Html = '', openSlotsRemainingHtml = '', latencyInSessionHtml = '', latencyFirst3Html = '', latencyRemainingHtml = '', notationFileHtml = '';
@ -32,6 +84,8 @@
var inSessionUsers = [];
showJoinLink = session.musician_access;
console.log('session', session)
showListenLink = session.fan_access && session.active_music_session && session.active_music_session.mount;
// render musicians who are already in the session
if (session.active_music_session && "participants" in session.active_music_session && session.active_music_session.participants.length > 0) {
@ -100,6 +154,7 @@
var sessionVals = buildSessionObject(session, notationFileHtml, rsvpFirst3UsersHtml, rsvpRemainingUsersHtml, openSlotsFirst3Html, openSlotsRemainingHtml, latencyFirst3Html, latencyRemainingHtml, latencyInSessionHtml);
sessionVals.in_session_musicians = inSessionUsersHtml.length > 0 ? inSessionUsersHtml : 'N/A';
sessionVals.join_link_display_style = showJoinLink ? "block" : "none";
sessionVals.listen_link_display_style = showListenLink ? "inline-block" : "none";
var $row = $(context.JK.fillTemplate($activeSessionTemplate.html(), sessionVals));
var $offsetParent = $(tbGroup).closest('.content');
@ -134,6 +189,26 @@
});
});
// enable listen button
if(showListenLink) {
var $listenLink = $('.listen-link', $parentRow)
var $listenText = $('.listen-link-text', $parentRow)
var $listenDetailHover = $('.listen-detail-hover', $parentRow)
$listenLink.parent().hoverIntent({
over: function() {
$listenDetailHover.show();
},
out: function() {
$listenDetailHover.hide();
}
})
$listenLink.attr('data-music-session', session.id).attr('fan-access', session.fan_access).attr('data-audio-src', session.active_music_session.mount.url).attr('data-audio-type', session.active_music_session.mount.mime_type)
$listenLink.listenBroadcast({lazyAudioInit:true, hoverOptions: {offsetParent: $offsetParent}, detailHelper: $listenDetailHover});
$listenLink.bind('statechange.listenBroadcast', stateChange);
$listenLink.click(togglePlay);
}
if (showJoinLink) {
// wire up the Join Link to the T&Cs dialog
$('.join-link', $parentRow).click(function(evt) {
@ -171,8 +246,6 @@
var hasPendingOrDeclinedRsvp = false;
var openRsvps = session.open_rsvps;
$('#actionHeader', tbGroup).html('RSVP');
var i = 0;
var rsvpFirst3UsersHtml = '', rsvpRemainingUsersHtml = '', openSlotsFirst3Html = '', openSlotsRemainingHtml = '', latencyFirst3Html = '', latencyRemainingHtml = '', notationFileHtml = '';
@ -285,7 +358,7 @@
if (approvedRsvpId) {
showRsvpLink = false;
noLinkText = $('<span class="text">You have been confirmed for this session. <a href="#">Cancel</a></span>');
noLinkText = $('<span class="text">You have been confirmed for this session. <a href="#" style="color: #fc0">Cancel</a></span>');
noLinkText.find('a').click(function() {
ui.launchRsvpCancelDialog(session.id, approvedRsvpId)
.one(EVENTS.RSVP_CANCELED, function() {
@ -302,7 +375,7 @@
}
else if (pendingRsvpId) {
showRsvpLink = false;
noLinkText = $('<span class="text">You have RSVP\'ed to this session. <a href="#">Cancel</a></span>');
noLinkText = $('<span class="text">You have RSVP\'ed to this session. <a href="#" style="color: #fc0">Cancel</a></span>');
noLinkText.find('a').click(function() {
ui.launchRsvpCancelDialog(session.id, pendingRsvpId)
.one(EVENTS.RSVP_CANCELED, function() {

View File

@ -213,6 +213,10 @@
return;
}
$element.on('remove', function() {
$element.btOff();
})
if ($element instanceof jQuery) {
if ($element.length == 0) {
logger.error("hoverBubble: no element specified with text %o", text);
@ -449,6 +453,7 @@
});
}
// http://stackoverflow.com/questions/4878756/javascript-how-to-capitalize-first-letter-of-each-word-like-a-2-word-city
context.JK.toTitleCase = function(str) {
return str.replace(/\w\S*/g, function(txt){
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();

View File

@ -85,4 +85,42 @@
.latency-value {
@include border-radius(2px);
}
.action-links {
a { margin-top:10px; }
}
.listen-link-details {
position:relative;
left:6px;
&.statusing {
left:0;
}
}
.listen-detail-hover {
margin-left:5px;
margin-top:0 !important;
color: #fc0;
display:none;
}
.join-link {
position:relative;
left:-3px;
}
.join-link-text {
position:relative;
left:6px;
}
.rsvp-link {
position:relative;
left:0px;
}
.rsvp-link-text {
position:relative;
left:6px;
}
}

View File

@ -43,12 +43,12 @@
&[data-status=source_up] {.source_up { display:inline-block; }}
&[data-status=source_down] {.source_down { display:inline-block; }}
&[data-status=source_wrong_up] {.source_up { display:inline-block; }}
&[data-status=source_wrong_down] {.source_up { display:inline-block; }}
&[data-status=transition_up] {.source_up { display:inline-block; }}
&[data-status=transition_down] {.source_up { display:inline-block; }}
&[data-status=transition_timeout_up] {.source_up { display:inline-block; }}
&[data-status=transition_timeout_down] {.source_up { display:inline-block; }}
&[data-status=source_wrong_up] {.source_wrong_up { display:inline-block; }}
&[data-status=source_wrong_down] {.source_wrong_down { display:inline-block; }}
&[data-status=transition_up] {.transition_up { display:inline-block; }}
&[data-status=transition_down] {.transition_down { display:inline-block; }}
&[data-status=transition_timeout_up] {.transition_timeout_up { display:inline-block; }}
&[data-status=transition_timeout_down] {.transition_timeout_down { display:inline-block; }}
&[data-status=unknown] {.unknown { display:inline-block; }}
&[data-status=loading] {.loading { display:inline-block; }}
&[data-status=db_error] {.db_error{ display:inline-block; }}
@ -82,7 +82,7 @@
.detail-items {
height:85px;
height:80px;
overflow:auto;
}

View File

@ -278,6 +278,12 @@
.link-contents {
margin-bottom:20px;
float:left;
}
#btn-share-copy {
top:-4px;
position:relative;
}
}

View File

@ -4,7 +4,7 @@ node do |source_change|
partial("api_icecast/show_source_change", :object => source_change)
end
child(:mount) { |mount|
child(:mount => :mount) { |mount|
attributes :id, :listeners, :source_direction, :sourced
node :state do |mount|

View File

@ -91,6 +91,21 @@
<td>Notation Files:</td>
<td>{notation_files}</td>
</tr>
<tr class="action-links">
<td>
<a class="listen-link" style="display:{listen_link_display_style};">
<%= image_tag "content/listen-icon.jpg", :size => "40x40" %>
</a>
<br/>
<span class="listen-link-details"><span class="listen-link-text">Listen</span><a href="#" class="listen-detail-hover">?</a></span>
</td>
<td>
<a class="join-link" style="display:{join_link_display_style};">
<%= image_tag "content/join-icon.jpg", :size => "37x40" %>
</a>
<span class="join-link-text">Join</span>
</td>
</tr>
</table>
</td>
<td width="35%">
@ -156,11 +171,6 @@
<tr><td><span class="bold">Legal Policy:</span><br/>{legal_policy}</td></tr>
</table>
</td>
<td class="noborder" style="text-align:center; vertical-align:middle;">
<a class="join-link" style="display:{join_link_display_style};">
<%= image_tag "content/icon_join.png", :size => "19x22" %>
</a>
</td>
</tr>
</script>
@ -183,6 +193,18 @@
<tr>
<td colspan="2">{scheduled_start}</td>
</tr>
<tr>
<td>
</td>
<td>
<span class="rsvp-msg" style="display:none;">You cannot RSVP to this session.</span>
<a class="rsvp-link">
<%= image_tag "content/rsvp-icon.jpg", :size => "40x40" %>
</a>
<br/>
<span class="rsvp-link-text">RSVP</span>
</td>
</tr>
</table>
</td>
<td width="35%">
@ -235,12 +257,6 @@
<tr><td><span class="bold">Legal Policy:</span><br/>{legal_policy}</td></tr>
</table>
</td>
<td class="noborder rsvp-section">
<span class="rsvp-msg" style="display:none;">You cannot RSVP to this session.</span>
<a class="rsvp-link">
<%= image_tag "content/icon_join.png", :size => "19x22" %>
</a>
</td>
</tr>
</script>

View File

@ -7,8 +7,7 @@
<th align="left" width="30%">SESSION</th>
<th align="left" width="35%">MUSICIANS</th>
<th width="10%" style="text-align:center">LATENCY</th>
<th align="left" width="20%">POLICIES</th>
<th id="actionHeader" class="noborder" width="30" style="text-align:center"></th>
<th align="left" width="20%">POLICIES</th>
</tr>
<!-- session row goes here -->
</table>

View File

@ -26,12 +26,12 @@ UnusedMusicNotationCleaner:
description: "Remove unused music notations"
UserProgressEmailer:
cron: "30 21 * * *"
# cron: "30 21 * * *"
class: "JamRuby::UserProgressEmailer"
description: "Sends periodic user progress emails"
DailySessionEmailer:
cron: "0 6 * * *"
# cron: "0 6 * * *"
class: "JamRuby::DailySessionEmailer"
description: "Sends daily scheduled session emails"
@ -41,7 +41,7 @@ ScheduledMusicSessionCleaner:
description: "Purges old, forgotten sessions that have not been started for >4 weeks"
NewMusicianEmailer:
cron: "0 1 * * 1"
# cron: "0 1 * * 1"
class: "JamRuby::NewMusicianEmailer"
description: "Sends weekly emails of new users with good latency"