Merge branch 'mconf-live0.6.4' into base-for-0.6.4

Conflicts:
	bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala
	bigbluebutton-client/src/org/bigbluebutton/main/views/CameraDisplaySettings.mxml
	bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml
	bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMap.mxml
	bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMapDelegate.as
	bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserGraphicHolder.mxml
This commit is contained in:
Felipe Cecagno 2015-06-22 17:18:26 -03:00
commit c79be37591
436 changed files with 22230 additions and 3369 deletions

View File

@ -122,6 +122,27 @@ public String createMeeting(String meetingID, String welcome, String moderatorPa
.trim();
}
//
// getJoinMeetingURL() -- get join meeting URL for both viewer and moderator as guest
//
public String getJoinMeetingURL(String username, String meetingID, String password, String clientURL, Boolean guest) {
String base_url_join = BigBlueButtonURL + "api/join?";
String clientURL_param = "";
if ((clientURL != null) && !clientURL.equals("")) {
clientURL_param = "&redirectClient=true&clientURL=" + urlEncode( clientURL );
}
String join_parameters = "meetingID=" + urlEncode(meetingID)
+ "&fullName=" + urlEncode(username) + "&password="
+ urlEncode(password) + "&guest=" + urlEncode(guest.toString()) + clientURL_param;
return base_url_join + join_parameters + "&checksum="
+ checksum("join" + join_parameters + salt);
}
//
// getJoinMeetingURL() -- get join meeting URL for both viewer and moderator
//

View File

@ -0,0 +1,689 @@
article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
audio:not([controls]){display:none;}
html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
a:hover,a:active{outline:0;}
sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;}
sup{top:-0.5em;}
sub{bottom:-0.25em;}
img{height:auto;border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;}
button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;}
button,input{*overflow:visible;line-height:normal;}
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;}
button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;}
input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;}
textarea{overflow:auto;vertical-align:top;}
.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";}
.clearfix:after{clear:both;}
.hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;}
.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;}
body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;}
a{color:#367380;text-decoration:none;}
a:hover{color:#1f434a;text-decoration:underline;}
.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";}
.row:after{clear:both;}
[class*="span"]{float:left;margin-left:20px;}
.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
.span12{width:940px;}
.span11{width:860px;}
.span10{width:780px;}
.span9{width:700px;}
.span8{width:620px;}
.span7{width:540px;}
.span6{width:460px;}
.span5{width:380px;}
.span4{width:300px;}
.span3{width:220px;}
.span2{width:140px;}
.span1{width:60px;}
.offset12{margin-left:980px;}
.offset11{margin-left:900px;}
.offset10{margin-left:820px;}
.offset9{margin-left:740px;}
.offset8{margin-left:660px;}
.offset7{margin-left:580px;}
.offset6{margin-left:500px;}
.offset5{margin-left:420px;}
.offset4{margin-left:340px;}
.offset3{margin-left:260px;}
.offset2{margin-left:180px;}
.offset1{margin-left:100px;}
.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";}
.row-fluid:after{clear:both;}
.row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;}
.row-fluid>[class*="span"]:first-child{margin-left:0;}
.row-fluid > .span12{width:99.99999998999999%;}
.row-fluid > .span11{width:91.489361693%;}
.row-fluid > .span10{width:82.97872339599999%;}
.row-fluid > .span9{width:74.468085099%;}
.row-fluid > .span8{width:65.95744680199999%;}
.row-fluid > .span7{width:57.446808505%;}
.row-fluid > .span6{width:48.93617020799999%;}
.row-fluid > .span5{width:40.425531911%;}
.row-fluid > .span4{width:31.914893614%;}
.row-fluid > .span3{width:23.404255317%;}
.row-fluid > .span2{width:14.89361702%;}
.row-fluid > .span1{width:6.382978723%;}
.container{margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";}
.container:after{clear:both;}
.container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";}
.container-fluid:after{clear:both;}
p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;}
.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;}
h1,h2,h3,h4,h5,h6{margin:0;font-family:inherit;font-weight:bold;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;}
h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;}
h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;}
h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;}
h4,h5,h6{line-height:18px;}
h4{font-size:14px;}h4 small{font-size:12px;}
h5{font-size:12px;}
h6{font-size:11px;color:#999999;text-transform:uppercase;}
.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;}
.page-header h1{line-height:1;}
ul,ol{padding:0;margin:0 0 9px 25px;}
ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
ul{list-style:disc;}
ol{list-style:decimal;}
li{line-height:18px;}
ul.unstyled,ol.unstyled{margin-left:0;list-style:none;}
dl{margin-bottom:18px;}
dt,dd{line-height:18px;}
dt{font-weight:bold;line-height:17px;}
dd{margin-left:9px;}
.dl-horizontal dt{float:left;clear:left;width:120px;text-align:right;}
.dl-horizontal dd{margin-left:130px;}
hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;}
strong{font-weight:bold;}
em{font-style:italic;}
.muted{color:#999999;}
abbr[title]{border-bottom:1px dotted #ddd;cursor:help;}
abbr.initialism{font-size:90%;text-transform:uppercase;}
blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;}
blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';}
blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;}
q:before,q:after,blockquote:before,blockquote:after{content:"";}
address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;}
small{font-size:100%;}
cite{font-style:normal;}
code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12.025px;line-height:18px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;word-wrap:break-word;}pre.prettyprint{margin-bottom:18px;}
pre code{padding:0;color:inherit;background-color:transparent;border:0;}
.pre-scrollable{max-height:340px;overflow-y:scroll;}
form{margin:0 0 18px;}
fieldset{padding:0;margin:0;border:0;}
legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333333;border:0;border-bottom:1px solid #eee;}legend small{font-size:13.5px;color:#999999;}
label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px;}
input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;}
label{display:block;margin-bottom:5px;color:#333333;}
input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555555;border:1px solid #cccccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
.uneditable-textarea{width:auto;height:auto;}
label input,label textarea,label select{display:block;}
input[type="image"],input[type="checkbox"],input[type="radio"]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border:0 \9;}
input[type="image"]{border:0;}
input[type="file"]{width:auto;padding:initial;line-height:initial;border:initial;background-color:#ffffff;background-color:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
input[type="button"],input[type="reset"],input[type="submit"]{width:auto;height:auto;}
select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px;}
input[type="file"]{line-height:18px \9;}
select{width:220px;background-color:#ffffff;}
select[multiple],select[size]{height:auto;}
input[type="image"]{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
textarea{height:auto;}
input[type="hidden"]{display:none;}
.radio,.checkbox{padding-left:18px;}
.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;}
.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;}
.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;}
.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;}
input,textarea{-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;}
input:focus,textarea:focus{border-color:#367380;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px #367380;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px #367380;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px #367380;outline:0;outline:thin dotted \9;}
input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
.input-mini{width:60px;}
.input-small{width:90px;}
.input-medium{width:150px;}
.input-large{width:210px;}
.input-xlarge{width:270px;}
.input-xxlarge{width:530px;}
input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{float:none;margin-left:0;}
input,textarea,.uneditable-input{margin-left:0;}
input.span12, textarea.span12, .uneditable-input.span12{width:930px;}
input.span11, textarea.span11, .uneditable-input.span11{width:850px;}
input.span10, textarea.span10, .uneditable-input.span10{width:770px;}
input.span9, textarea.span9, .uneditable-input.span9{width:690px;}
input.span8, textarea.span8, .uneditable-input.span8{width:610px;}
input.span7, textarea.span7, .uneditable-input.span7{width:530px;}
input.span6, textarea.span6, .uneditable-input.span6{width:450px;}
input.span5, textarea.span5, .uneditable-input.span5{width:370px;}
input.span4, textarea.span4, .uneditable-input.span4{width:290px;}
input.span3, textarea.span3, .uneditable-input.span3{width:210px;}
input.span2, textarea.span2, .uneditable-input.span2{width:130px;}
input.span1, textarea.span1, .uneditable-input.span1{width:50px;}
input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#eeeeee;border-color:#ddd;cursor:not-allowed;}
.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;}
.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853;}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e;}
.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;}
.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;}
.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48;}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392;}
.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;}
.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;}
.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847;}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b;}
.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;}
input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
.form-actions{padding:17px 20px 18px;margin-top:18px;margin-bottom:18px;background-color:#eeeeee;border-top:1px solid #ddd;*zoom:1;}.form-actions:before,.form-actions:after{display:table;content:"";}
.form-actions:after{clear:both;}
.uneditable-input{display:block;background-color:#ffffff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
:-moz-placeholder{color:#999999;}
::-webkit-input-placeholder{color:#999999;}
.help-block,.help-inline{color:#555555;}
.help-block{display:block;margin-bottom:9px;}
.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px;}
.input-prepend,.input-append{margin-bottom:5px;}.input-prepend input,.input-append input,.input-prepend select,.input-append select,.input-prepend .uneditable-input,.input-append .uneditable-input{*margin-left:0;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-prepend input:focus,.input-append input:focus,.input-prepend select:focus,.input-append select:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{position:relative;z-index:2;}
.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc;}
.input-prepend .add-on,.input-append .add-on{display:inline-block;width:auto;min-width:16px;height:18px;padding:4px 5px;font-weight:normal;line-height:18px;text-align:center;text-shadow:0 1px 0 #ffffff;vertical-align:middle;background-color:#eeeeee;border:1px solid #ccc;}
.input-prepend .add-on,.input-append .add-on,.input-prepend .btn,.input-append .btn{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546;}
.input-prepend .add-on,.input-prepend .btn{margin-right:-1px;}
.input-append input,.input-append select .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-append .uneditable-input{border-left-color:#eee;border-right-color:#ccc;}
.input-append .add-on,.input-append .btn{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
.search-query{padding-left:14px;padding-right:14px;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;}
.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;margin-bottom:0;}
.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;}
.form-search label,.form-inline label{display:inline-block;}
.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0;}
.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle;}
.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-left:0;margin-right:3px;}
.control-group{margin-bottom:9px;}
legend+.control-group{margin-top:18px;-webkit-margin-top-collapse:separate;}
.form-horizontal .control-group{margin-bottom:18px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";}
.form-horizontal .control-group:after{clear:both;}
.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right;}
.form-horizontal .controls{margin-left:160px;*display:inline-block;*margin-left:0;*padding-left:20px;}
.form-horizontal .help-block{margin-top:9px;margin-bottom:0;}
.form-horizontal .form-actions{padding-left:160px;}
table{max-width:100%;border-collapse:collapse;border-spacing:0;background-color:transparent;}
.table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;}
.table th{font-weight:bold;}
.table thead th{vertical-align:bottom;}
.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;}
.table tbody+tbody{border-top:2px solid #dddddd;}
.table-condensed th,.table-condensed td{padding:4px 5px;}
.table-bordered{border:1px solid #dddddd;border-left:0;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;}
.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;}
.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;}
.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;}
.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;}
.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;}
.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;}
table .span1{float:none;width:44px;margin-left:0;}
table .span2{float:none;width:124px;margin-left:0;}
table .span3{float:none;width:204px;margin-left:0;}
table .span4{float:none;width:284px;margin-left:0;}
table .span5{float:none;width:364px;margin-left:0;}
table .span6{float:none;width:444px;margin-left:0;}
table .span7{float:none;width:524px;margin-left:0;}
table .span8{float:none;width:604px;margin-left:0;}
table .span9{float:none;width:684px;margin-left:0;}
table .span10{float:none;width:764px;margin-left:0;}
table .span11{float:none;width:844px;margin-left:0;}
table .span12{float:none;width:924px;margin-left:0;}
table .span13{float:none;width:1004px;margin-left:0;}
table .span14{float:none;width:1084px;margin-left:0;}
table .span15{float:none;width:1164px;margin-left:0;}
table .span16{float:none;width:1244px;margin-left:0;}
table .span17{float:none;width:1324px;margin-left:0;}
table .span18{float:none;width:1404px;margin-left:0;}
table .span19{float:none;width:1484px;margin-left:0;}
table .span20{float:none;width:1564px;margin-left:0;}
table .span21{float:none;width:1644px;margin-left:0;}
table .span22{float:none;width:1724px;margin-left:0;}
table .span23{float:none;width:1804px;margin-left:0;}
table .span24{float:none;width:1884px;margin-left:0;}
[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;*margin-right:.3em;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;}
.icon-white{background-image:url("../img/glyphicons-halflings-white.png");}
.icon-glass{background-position:0 0;}
.icon-music{background-position:-24px 0;}
.icon-search{background-position:-48px 0;}
.icon-envelope{background-position:-72px 0;}
.icon-heart{background-position:-96px 0;}
.icon-star{background-position:-120px 0;}
.icon-star-empty{background-position:-144px 0;}
.icon-user{background-position:-168px 0;}
.icon-film{background-position:-192px 0;}
.icon-th-large{background-position:-216px 0;}
.icon-th{background-position:-240px 0;}
.icon-th-list{background-position:-264px 0;}
.icon-ok{background-position:-288px 0;}
.icon-remove{background-position:-312px 0;}
.icon-zoom-in{background-position:-336px 0;}
.icon-zoom-out{background-position:-360px 0;}
.icon-off{background-position:-384px 0;}
.icon-signal{background-position:-408px 0;}
.icon-cog{background-position:-432px 0;}
.icon-trash{background-position:-456px 0;}
.icon-home{background-position:0 -24px;}
.icon-file{background-position:-24px -24px;}
.icon-time{background-position:-48px -24px;}
.icon-road{background-position:-72px -24px;}
.icon-download-alt{background-position:-96px -24px;}
.icon-download{background-position:-120px -24px;}
.icon-upload{background-position:-144px -24px;}
.icon-inbox{background-position:-168px -24px;}
.icon-play-circle{background-position:-192px -24px;}
.icon-repeat{background-position:-216px -24px;}
.icon-refresh{background-position:-240px -24px;}
.icon-list-alt{background-position:-264px -24px;}
.icon-lock{background-position:-287px -24px;}
.icon-flag{background-position:-312px -24px;}
.icon-headphones{background-position:-336px -24px;}
.icon-volume-off{background-position:-360px -24px;}
.icon-volume-down{background-position:-384px -24px;}
.icon-volume-up{background-position:-408px -24px;}
.icon-qrcode{background-position:-432px -24px;}
.icon-barcode{background-position:-456px -24px;}
.icon-tag{background-position:0 -48px;}
.icon-tags{background-position:-25px -48px;}
.icon-book{background-position:-48px -48px;}
.icon-bookmark{background-position:-72px -48px;}
.icon-print{background-position:-96px -48px;}
.icon-camera{background-position:-120px -48px;}
.icon-font{background-position:-144px -48px;}
.icon-bold{background-position:-167px -48px;}
.icon-italic{background-position:-192px -48px;}
.icon-text-height{background-position:-216px -48px;}
.icon-text-width{background-position:-240px -48px;}
.icon-align-left{background-position:-264px -48px;}
.icon-align-center{background-position:-288px -48px;}
.icon-align-right{background-position:-312px -48px;}
.icon-align-justify{background-position:-336px -48px;}
.icon-list{background-position:-360px -48px;}
.icon-indent-left{background-position:-384px -48px;}
.icon-indent-right{background-position:-408px -48px;}
.icon-facetime-video{background-position:-432px -48px;}
.icon-picture{background-position:-456px -48px;}
.icon-pencil{background-position:0 -72px;}
.icon-map-marker{background-position:-24px -72px;}
.icon-adjust{background-position:-48px -72px;}
.icon-tint{background-position:-72px -72px;}
.icon-edit{background-position:-96px -72px;}
.icon-share{background-position:-120px -72px;}
.icon-check{background-position:-144px -72px;}
.icon-move{background-position:-168px -72px;}
.icon-step-backward{background-position:-192px -72px;}
.icon-fast-backward{background-position:-216px -72px;}
.icon-backward{background-position:-240px -72px;}
.icon-play{background-position:-264px -72px;}
.icon-pause{background-position:-288px -72px;}
.icon-stop{background-position:-312px -72px;}
.icon-forward{background-position:-336px -72px;}
.icon-fast-forward{background-position:-360px -72px;}
.icon-step-forward{background-position:-384px -72px;}
.icon-eject{background-position:-408px -72px;}
.icon-chevron-left{background-position:-432px -72px;}
.icon-chevron-right{background-position:-456px -72px;}
.icon-plus-sign{background-position:0 -96px;}
.icon-minus-sign{background-position:-24px -96px;}
.icon-remove-sign{background-position:-48px -96px;}
.icon-ok-sign{background-position:-72px -96px;}
.icon-question-sign{background-position:-96px -96px;}
.icon-info-sign{background-position:-120px -96px;}
.icon-screenshot{background-position:-144px -96px;}
.icon-remove-circle{background-position:-168px -96px;}
.icon-ok-circle{background-position:-192px -96px;}
.icon-ban-circle{background-position:-216px -96px;}
.icon-arrow-left{background-position:-240px -96px;}
.icon-arrow-right{background-position:-264px -96px;}
.icon-arrow-up{background-position:-289px -96px;}
.icon-arrow-down{background-position:-312px -96px;}
.icon-share-alt{background-position:-336px -96px;}
.icon-resize-full{background-position:-360px -96px;}
.icon-resize-small{background-position:-384px -96px;}
.icon-plus{background-position:-408px -96px;}
.icon-minus{background-position:-433px -96px;}
.icon-asterisk{background-position:-456px -96px;}
.icon-exclamation-sign{background-position:0 -120px;}
.icon-gift{background-position:-24px -120px;}
.icon-leaf{background-position:-48px -120px;}
.icon-fire{background-position:-72px -120px;}
.icon-eye-open{background-position:-96px -120px;}
.icon-eye-close{background-position:-120px -120px;}
.icon-warning-sign{background-position:-144px -120px;}
.icon-plane{background-position:-168px -120px;}
.icon-calendar{background-position:-192px -120px;}
.icon-random{background-position:-216px -120px;}
.icon-comment{background-position:-240px -120px;}
.icon-magnet{background-position:-264px -120px;}
.icon-chevron-up{background-position:-288px -120px;}
.icon-chevron-down{background-position:-313px -119px;}
.icon-retweet{background-position:-336px -120px;}
.icon-shopping-cart{background-position:-360px -120px;}
.icon-folder-close{background-position:-384px -120px;}
.icon-folder-open{background-position:-408px -120px;}
.icon-resize-vertical{background-position:-432px -119px;}
.icon-resize-horizontal{background-position:-456px -118px;}
.dropdown{position:relative;}
.dropdown-toggle{*margin-bottom:-3px;}
.dropdown-toggle:active,.open .dropdown-toggle{outline:0;}
.caret{display:inline-block;width:0;height:0;vertical-align:top;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000000;opacity:0.3;filter:alpha(opacity=30);content:"";}
.dropdown .caret{margin-top:8px;margin-left:2px;}
.dropdown:hover .caret,.open.dropdown .caret{opacity:1;filter:alpha(opacity=100);}
.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;float:left;display:none;min-width:160px;padding:4px 0;margin:0;list-style:none;background-color:#ffffff;border-color:#ccc;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:1px;-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;*border-right-width:2px;*border-bottom-width:2px;}.dropdown-menu.pull-right{right:0;left:auto;}
.dropdown-menu .divider{height:1px;margin:8px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:18px;color:#333333;white-space:nowrap;}
.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;background-color:#367380;}
.dropdown.open{*z-index:1000;}.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);}
.dropdown.open .dropdown-menu{display:block;}
.pull-right .dropdown-menu{left:auto;right:0;}
.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000000;content:"\2191";}
.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px;}
.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;}
.collapse{-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;position:relative;overflow:hidden;height:0;}.collapse.in{height:auto;}
.close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;opacity:0.4;filter:alpha(opacity=40);cursor:pointer;}
.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 10px 4px;margin-bottom:0;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);border:1px solid #cccccc;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;*margin-left:.3em;}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;}
.btn:active,.btn.active{background-color:#cccccc \9;}
.btn:first-child{*margin-left:0;}
.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;}
.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
.btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;outline:0;}
.btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
.btn-large [class^="icon-"]{margin-top:1px;}
.btn-small{padding:5px 9px;font-size:11px;line-height:16px;}
.btn-small [class^="icon-"]{margin-top:-1px;}
.btn-mini{padding:2px 6px;font-size:11px;line-height:14px;}
.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;}
.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255, 255, 255, 0.75);}
.btn-primary{background-color:#366c80;background-image:-moz-linear-gradient(top, #367380, #366180);background-image:-ms-linear-gradient(top, #367380, #366180);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#367380), to(#366180));background-image:-webkit-linear-gradient(top, #367380, #366180);background-image:-o-linear-gradient(top, #367380, #366180);background-image:linear-gradient(top, #367380, #366180);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#367380', endColorstr='#366180', GradientType=0);border-color:#366180 #366180 #1f384a;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#366180;}
.btn-primary:active,.btn-primary.active{background-color:#27455c \9;}
.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;}
.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;}
.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;}
.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;}
.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;}
.btn-success:active,.btn-success.active{background-color:#408140 \9;}
.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;}
.btn-info:active,.btn-info.active{background-color:#24748c \9;}
.btn-inverse{background-color:#414141;background-image:-moz-linear-gradient(top, #555555, #222222);background-image:-ms-linear-gradient(top, #555555, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222));background-image:-webkit-linear-gradient(top, #555555, #222222);background-image:-o-linear-gradient(top, #555555, #222222);background-image:linear-gradient(top, #555555, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#222222;}
.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9;}
button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;}
button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px;}
button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px;}
button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px;}
.btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";}
.btn-group:after{clear:both;}
.btn-group:first-child{*margin-left:0;}
.btn-group+.btn-group{margin-left:5px;}
.btn-toolbar{margin-top:9px;margin-bottom:9px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;}
.btn-group .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
.btn-group .btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
.btn-group .btn:last-child,.btn-group .dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;}
.btn-group .btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;}
.btn-group .btn.large:last-child,.btn-group .large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;}
.btn-group .btn:hover,.btn-group .btn:focus,.btn-group .btn:active,.btn-group .btn.active{z-index:2;}
.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;}
.btn-group .dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:3px;*padding-bottom:3px;}
.btn-group .btn-mini.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:1px;*padding-bottom:1px;}
.btn-group .btn-small.dropdown-toggle{*padding-top:4px;*padding-bottom:4px;}
.btn-group .btn-large.dropdown-toggle{padding-left:12px;padding-right:12px;}
.btn-group.open{*z-index:1000;}.btn-group.open .dropdown-menu{display:block;margin-top:1px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);}
.btn .caret{margin-top:7px;margin-left:0;}
.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);}
.btn-mini .caret{margin-top:5px;}
.btn-small .caret{margin-top:6px;}
.btn-large .caret{margin-top:6px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);}
.alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;}
.alert-heading{color:inherit;}
.alert .close{position:relative;top:-2px;right:-21px;line-height:18px;}
.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;}
.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;}
.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;}
.alert-block{padding-top:14px;padding-bottom:14px;}
.alert-block>p,.alert-block>ul{margin-bottom:0;}
.alert-block p+p{margin-top:5px;}
.nav{margin-left:0;margin-bottom:18px;list-style:none;}
.nav>li>a{display:block;}
.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;}
.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;}
.nav li+.nav-header{margin-top:9px;}
.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;}
.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
.nav-list>li>a{padding:3px 15px;}
.nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#367380;}
.nav-list [class^="icon-"]{margin-right:2px;}
.nav-list .divider{height:1px;margin:8px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";}
.nav-tabs:after,.nav-pills:after{clear:both;}
.nav-tabs>li,.nav-pills>li{float:left;}
.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;}
.nav-tabs{border-bottom:1px solid #ddd;}
.nav-tabs>li{margin-bottom:-1px;}
.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:18px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;}
.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#367380;}
.nav-stacked>li{float:none;}
.nav-stacked>li>a{margin-right:0;}
.nav-tabs.nav-stacked{border-bottom:0;}
.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}
.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}
.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;}
.nav-pills.nav-stacked>li>a{margin-bottom:3px;}
.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;}
.nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;}
.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#367380;border-bottom-color:#367380;margin-top:6px;}
.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#1f434a;border-bottom-color:#1f434a;}
.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;border-bottom-color:#333333;}
.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;}
.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;}
.nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);}
.tabs-stacked .open>a:hover{border-color:#999999;}
.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";}
.tabbable:after{clear:both;}
.tab-content{display:table;width:100%;}
.tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;}
.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;}
.tab-content>.active,.pill-content>.active{display:block;}
.tabs-below .nav-tabs{border-top:1px solid #ddd;}
.tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;}
.tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;}
.tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;}
.tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;}
.tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;}
.tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;}
.tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;}
.tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;}
.tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;}
.tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;}
.tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;}
.tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;}
.tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;}
.navbar{*position:relative;*z-index:2;overflow:visible;margin-bottom:18px;}
.navbar-inner{padding-left:20px;padding-right:20px;background-color:#1b454e;background-image:-moz-linear-gradient(top, #27535c, #0a3138);background-image:-ms-linear-gradient(top, #27535c, #0a3138);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#27535c), to(#0a3138));background-image:-webkit-linear-gradient(top, #27535c, #0a3138);background-image:-o-linear-gradient(top, #27535c, #0a3138);background-image:linear-gradient(top, #27535c, #0a3138);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#27535c', endColorstr='#0a3138', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);}
.navbar .container{width:auto;}
.btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#1b454e;background-image:-moz-linear-gradient(top, #27535c, #0a3138);background-image:-ms-linear-gradient(top, #27535c, #0a3138);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#27535c), to(#0a3138));background-image:-webkit-linear-gradient(top, #27535c, #0a3138);background-image:-o-linear-gradient(top, #27535c, #0a3138);background-image:linear-gradient(top, #27535c, #0a3138);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#27535c', endColorstr='#0a3138', GradientType=0);border-color:#0a3138 #0a3138 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#0a3138;}
.btn-navbar:active,.btn-navbar.active{background-color:#020b0d \9;}
.btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);}
.btn-navbar .icon-bar+.icon-bar{margin-top:3px;}
.nav-collapse.collapse{height:auto;}
.navbar{color:#bbbbbb;}.navbar .brand:hover{text-decoration:none;}
.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;}
.navbar .navbar-text{margin-bottom:0;line-height:40px;}
.navbar .btn,.navbar .btn-group{margin-top:5px;}
.navbar .btn-group .btn{margin-top:0;}
.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";}
.navbar-form:after{clear:both;}
.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;}
.navbar-form input,.navbar-form select{display:inline-block;margin-bottom:0;}
.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;}
.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;}
.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;background-color:#1d90a4;border:1px solid #061e22;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query:-moz-placeholder{color:#cccccc;}
.navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;}
.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;}
.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;}
.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
.navbar-fixed-top{top:0;}
.navbar-fixed-bottom{bottom:0;}
.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;}
.navbar .nav.pull-right{float:right;}
.navbar .nav>li{display:block;float:left;}
.navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#bbbbbb;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}
.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;}
.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#0a3138;}
.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#0a3138;border-right:1px solid #27535c;}
.navbar .nav.pull-right{margin-left:10px;margin-right:0;}
.navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;}
.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;}
.navbar-fixed-bottom .dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;}
.navbar-fixed-bottom .dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;}
.navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;}
.navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);}
.navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;}
.navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;}
.navbar .nav.pull-right .dropdown-menu,.navbar .nav .dropdown-menu.pull-right{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before,.navbar .nav .dropdown-menu.pull-right:before{left:auto;right:12px;}
.navbar .nav.pull-right .dropdown-menu:after,.navbar .nav .dropdown-menu.pull-right:after{left:auto;right:13px;}
.breadcrumb{padding:7px 14px;margin:0 0 18px;list-style:none;background-color:#fbfbfb;background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 #ffffff;}
.breadcrumb .divider{padding:0 5px;color:#999999;}
.breadcrumb .active a{color:#333333;}
.pagination{height:36px;margin:18px 0;}
.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
.pagination li{display:inline;}
.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0;}
.pagination a:hover,.pagination .active a{background-color:#f5f5f5;}
.pagination .active a{color:#999999;cursor:default;}
.pagination .disabled span,.pagination .disabled a,.pagination .disabled a:hover{color:#999999;background-color:transparent;cursor:default;}
.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
.pagination-centered{text-align:center;}
.pagination-right{text-align:right;}
.pager{margin-left:0;margin-bottom:18px;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";}
.pager:after{clear:both;}
.pager li{display:inline;}
.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
.pager a:hover{text-decoration:none;background-color:#f5f5f5;}
.pager .next a{float:right;}
.pager .previous a{float:left;}
.pager .disabled a,.pager .disabled a:hover{color:#999999;background-color:#fff;cursor:default;}
.modal-open .dropdown-menu{z-index:2050;}
.modal-open .dropdown.open{*z-index:2050;}
.modal-open .popover{z-index:2060;}
.modal-open .tooltip{z-index:2070;}
.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;}
.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);}
.modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
.modal.fade.in{top:50%;}
.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;}
.modal-body{overflow-y:auto;max-height:400px;padding:15px;}
.modal-form{margin-bottom:0;}
.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";}
.modal-footer:after{clear:both;}
.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;}
.modal-footer .btn-group .btn+.btn{margin-left:-1px;}
.tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);}
.tooltip.top{margin-top:-2px;}
.tooltip.right{margin-left:2px;}
.tooltip.bottom{margin-top:2px;}
.tooltip.left{margin-left:-2px;}
.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.tooltip-arrow{position:absolute;width:0;height:0;}
.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px;}.popover.top{margin-top:-5px;}
.popover.right{margin-left:5px;}
.popover.bottom{margin-top:5px;}
.popover.left{margin-left:-5px;}
.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
.popover .arrow{position:absolute;width:0;height:0;}
.popover-inner{padding:3px;width:280px;overflow:hidden;background:#000000;background:rgba(0, 0, 0, 0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);}
.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;}
.popover-content{padding:14px;background-color:#ffffff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;}
.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";}
.thumbnails:after{clear:both;}
.thumbnails>li{float:left;margin:0 0 18px 20px;}
.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);}
a.thumbnail:hover{border-color:#367380;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}
.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;}
.thumbnail .caption{padding:9px;}
.label{padding:1px 4px 2px;font-size:10.998px;font-weight:bold;line-height:13px;color:#ffffff;vertical-align:middle;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
.label:hover{color:#ffffff;text-decoration:none;}
.label-important{background-color:#b94a48;}
.label-important:hover{background-color:#953b39;}
.label-warning{background-color:#f89406;}
.label-warning:hover{background-color:#c67605;}
.label-success{background-color:#468847;}
.label-success:hover{background-color:#356635;}
.label-info{background-color:#3a87ad;}
.label-info:hover{background-color:#2d6987;}
.label-inverse{background-color:#333333;}
.label-inverse:hover{background-color:#1a1a1a;}
.badge{padding:1px 9px 2px;font-size:12.025px;font-weight:bold;white-space:nowrap;color:#ffffff;background-color:#999999;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;}
.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;}
.badge-error{background-color:#b94a48;}
.badge-error:hover{background-color:#953b39;}
.badge-warning{background-color:#f89406;}
.badge-warning:hover{background-color:#c67605;}
.badge-success{background-color:#468847;}
.badge-success:hover{background-color:#356635;}
.badge-info{background-color:#3a87ad;}
.badge-info:hover{background-color:#2d6987;}
.badge-inverse{background-color:#333333;}
.badge-inverse:hover{background-color:#1a1a1a;}
@-webkit-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}.progress{overflow:hidden;height:18px;margin-bottom:18px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-ms-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(top, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.progress .bar{width:0%;height:18px;color:#ffffff;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-ms-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(top, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-ms-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;}
.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;}
.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;}
.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);}
.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);}
.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);}
.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
.progress-warning .bar{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);}
.progress-warning.progress-striped .bar{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
.accordion{margin-bottom:18px;}
.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.accordion-heading{border-bottom:0;}
.accordion-heading .accordion-toggle{display:block;padding:8px 15px;}
.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;}
.carousel{position:relative;margin-bottom:18px;line-height:1;}
.carousel-inner{overflow:hidden;width:100%;position:relative;}
.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-ms-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;}
.carousel .item>img{display:block;line-height:1;}
.carousel .active,.carousel .next,.carousel .prev{display:block;}
.carousel .active{left:0;}
.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;}
.carousel .next{left:100%;}
.carousel .prev{left:-100%;}
.carousel .next.left,.carousel .prev.right{left:0;}
.carousel .active.left{left:-100%;}
.carousel .active.right{left:100%;}
.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;}
.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);}
.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:10px 15px 5px;background:#333333;background:rgba(0, 0, 0, 0.75);}
.carousel-caption h4,.carousel-caption p{color:#ffffff;}
.hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;}
.hero-unit p{font-size:18px;font-weight:200;line-height:27px;color:inherit;}
.pull-right{float:right;}
.pull-left{float:left;}
.hide{display:none;}
.show{display:block;}
.invisible{visibility:hidden;}

View File

@ -14,7 +14,7 @@ String locIP=request.getLocalAddr();
<running><%= isMeetingRunning(request.getParameter("meetingID")) %></running>
</response>
<% } else if(request.getParameter("command").equals("getRecords")){%>
<%= getRecordings("English 101,English 102,English 103,English 104,English 105,English 106,English 107,English 108,English 109,English 110")%>
<%= getRecordings(request.getParameter("meetingID"))%>
<% } else if(request.getParameter("command").equals("publish")||request.getParameter("command").equals("unpublish")){%>
<%= setPublishRecordings( (request.getParameter("command").equals("publish")) ? true : false , request.getParameter("recordID"))%>
<% } else if(request.getParameter("command").equals("delete")){%>

View File

@ -197,6 +197,16 @@ if (request.getParameterMap().isEmpty()) {
<td>
<input type="password" required name="password" /></td>
</tr>
<tr>
<td>
&nbsp;</td>
<td style="text-align: right; ">
Guest:</td>
<td>
&nbsp;</td>
<td>
<input type="checkbox" name="guest" value="guest" /></td>
</tr>
<tr>
<td>
&nbsp;</td>
@ -273,7 +283,11 @@ Error: createMeeting() failed
// We've got a valid meeting_ID and passoword -- let's join!
//
String joinURL = getJoinMeetingURL(username, meeting_ID, password, null);
String joinURL;
if(request.getParameter("guest") != null)
joinURL = getJoinMeetingURL(username, meeting_ID, password, null, true);
else
joinURL = getJoinMeetingURL(username, meeting_ID, password, null);
%>
<script language="javascript" type="text/javascript">

View File

@ -0,0 +1,445 @@
<!--
BigBlueButton - http://www.bigbluebutton.org
Copyright (c) 2008-2009 by respective authors (see below). All rights reserved.
BigBlueButton is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation; either version 3 of the License, or (at your option) any later
version.
BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with BigBlueButton; if not, If not, see <http://www.gnu.org/licenses/>.
Author: Fred Dixon <ffdixon@bigbluebutton.org>
-->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Mconf-Live Demo</title>
<link rel="stylesheet" href="css/mconf-bootstrap.min.css" type="text/css" />
<link rel="stylesheet" type="text/css" href="css/ui.jqgrid.css" />
<link rel="stylesheet" type="text/css" href="css/redmond/jquery-ui-redmond.css" />
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery-ui.js"></script>
<script type="text/javascript" src="js/jquery.validate.min.js"></script>
<script src="js/grid.locale-en.js" type="text/javascript"></script>
<script src="js/jquery.jqGrid.min.js" type="text/javascript"></script>
<script src="js/jquery.xml2json.js" type="text/javascript"></script>
<style type="text/css">
.ui-jqgrid{
font-size:0.7em;
margin-left: auto;
margin-right: auto;
}
label.error{
float: none;
color: red;
padding-left: .5em;
vertical-align: top;
width:200px;
text-align:left;
}
</style>
</head>
<body>
<%@ include file="bbb_api.jsp"%>
<%
//
// We're going to define some sample courses (meetings) below. This API exampe shows how you can create a login page for a course.
// The password below are not available to users as they are compiled on the server.
//
HashMap<String, HashMap> allMeetings = new HashMap<String, HashMap>();
HashMap<String, String> meeting;
// String welcome = "<br>Welcome to %%CONFNAME%%!<br><br>For help see our <a href=\"event:http://www.bigbluebutton.org/content/videos\"><u>tutorial videos</u></a>.<br><br>To join the voice bridge for this meeting:<br> (1) click the headset icon in the upper-left, or<br> (2) dial xxx-xxx-xxxx (toll free:1-xxx-xxx-xxxx) and enter conference ID: %%CONFNUM%%.<br><br>";
String welcome = "<br>Welcome to <b>%%CONFNAME%%</b>!<br><br>In order to speak, click on the headset icon.";
meeting = new HashMap<String, String>();
allMeetings.put( "Test room 1", meeting ); // The title that will appear in the drop-down menu
meeting.put("welcomeMsg", welcome); // The welcome mesage
meeting.put("moderatorPW", "prof123"); // The password for moderator
meeting.put("viewerPW", "student123"); // The password for viewer
meeting.put("voiceBridge", "72013"); // The extension number for the voice bridge (use if connected to phone system)
meeting.put("logoutURL", "/demo/demo_mconf.jsp"); // The logout URL (use if you want to return to your pages)
meeting = new HashMap<String, String>();
allMeetings.put( "Test room 2", meeting ); // The title that will appear in the drop-down menu
meeting.put("welcomeMsg", welcome); // The welcome mesage
meeting.put("moderatorPW", "prof123"); // The password for moderator
meeting.put("viewerPW", "student123"); // The password for viewer
meeting.put("voiceBridge", "72014"); // The extension number for the voice bridge (use if connected to phone system)
meeting.put("logoutURL", "/demo/demo_mconf.jsp"); // The logout URL (use if you want to return to your pages)
meeting = new HashMap<String, String>();
allMeetings.put( "Test room 3", meeting ); // The title that will appear in the drop-down menu
meeting.put("welcomeMsg", welcome); // The welcome mesage
meeting.put("moderatorPW", "prof123"); // The password for moderator
meeting.put("viewerPW", "student123"); // The password for viewer
meeting.put("voiceBridge", "72015"); // The extension number for the voice bridge (use if connected to phone system)
meeting.put("logoutURL", "/demo/demo_mconf.jsp"); // The logout URL (use if you want to return to your pages)
meeting = new HashMap<String, String>();
allMeetings.put( "Test room 4", meeting ); // The title that will appear in the drop-down menu
meeting.put("welcomeMsg", welcome); // The welcome mesage
meeting.put("moderatorPW", "prof123"); // The password for moderator
meeting.put("viewerPW", "student123"); // The password for viewer
meeting.put("voiceBridge", "72016"); // The extension number for the voice bridge (use if connected to phone system)
meeting.put("logoutURL", "/demo/demo_mconf.jsp"); // The logout URL (use if you want to return to your pages)
meeting = null;
Iterator<String> meetingIterator = new TreeSet<String>(allMeetings.keySet()).iterator();
if (request.getParameterMap().isEmpty()) {
//
// Assume we want to join a course
//
%>
<div style="width: 400px; margin: auto auto; ">
<div style="text-align: center; ">
<img src="images/mconf.png" style="
width: 300px;
height: auto;
display: block; margin-left: auto; margin-right: auto;
">
</div>
<span style="text-align: center; ">
<h3>Join a test room</h3>
</span>
<FORM NAME="form1" METHOD="GET">
<table cellpadding="3" cellspacing="5">
<tbody>
<tr>
<td>
&nbsp;</td>
<td style="text-align: right; ">
Room:</td>
<td>
&nbsp;
</td>
<td style="text-align: left ">
<select name="meetingID" onchange="onChangeMeeting(this.value);">
<%
String key;
while (meetingIterator.hasNext()) {
key = meetingIterator.next();
out.println("<option value=\"" + key + "\">" + key + "</option>");
}
%>
</select><span id="label_meeting_running" hidden><i>&nbsp;Running!</i></span>
</td>
</tr>
<tr>
<td>
&nbsp;</td>
<td style="text-align: right; ">
Full&nbsp;Name:</td>
<td style="width: 5px; ">
&nbsp;</td>
<td style="text-align: left ">
<input type="text" autofocus required name="username" /></td>
</tr>
<tr>
<td>
&nbsp;</td>
<td style="text-align: right; ">
Role:</td>
<td>
&nbsp;</td>
<td>
<input type="radio" name="password" value="prof123" text"Moderator" checked>Moderator</input>
<input type="radio" name="password" value="student123">Viewer</input>
</td>
</tr>
<tr>
<td>
&nbsp;</td>
<td style="text-align: right; ">
Guest:</td>
<td>
&nbsp;</td>
<td>
<input id="check_guest" type="checkbox" name="guest" value="guest" />&nbsp;&nbsp;&nbsp;(authorization required)</td>
</tr>
<tr>
<td>
&nbsp;</td>
<td>
&nbsp;</td>
<td>
&nbsp;</td>
<td>
<input type="submit" value="Join" style="width: 220px; "></td>
</tr>
</tbody>
</table>
<INPUT TYPE=hidden NAME=action VALUE="create">
</FORM>
</div>
<div style="text-align: center; ">
<h3>Recorded Sessions</h3>
<select id="actionscmb" name="actions" onchange="recordedAction(this.value);">
<option value="novalue" selected>Actions...</option>
<option value="publish">Publish</option>
<option value="unpublish">Unpublish</option>
<option value="delete">Delete</option>
</select>
<table id="recordgrid"></table>
<div id="pager"></div>
<p>Note: New recordings will appear in the above list after processing. Refresh your browser to update the list.</p>
<script>
function onChangeMeeting(meetingID){
isRunningMeeting(meetingID);
}
function recordedAction(action){
if(action=="novalue"){
return;
}
var s = jQuery("#recordgrid").jqGrid('getGridParam','selarrrow');
if(s.length==0){
alert("Select at least one row");
$("#actionscmb").val("novalue");
return;
}
var recordid="";
for(var i=0;i<s.length;i++){
var d = jQuery("#recordgrid").jqGrid('getRowData',s[i]);
recordid+=d.id;
if(i!=s.length-1)
recordid+=",";
}
if(action=="delete"){
var answer = confirm ("Are you sure to delete the selected recordings?");
if (answer)
sendRecordingAction(recordid,action);
else{
$("#actionscmb").val("novalue");
return;
}
}else{
sendRecordingAction(recordid,action);
}
$("#actionscmb").val("novalue");
}
function sendRecordingAction(recordID,action){
$.ajax({
type: "GET",
url: 'demo10_helper.jsp',
data: "command="+action+"&recordID="+recordID,
dataType: "xml",
cache: false,
success: function(xml) {
window.location.reload(true);
$("#recordgrid").trigger("reloadGrid");
},
error: function() {
alert("Failed to connect to API.");
}
});
}
function isRunningMeeting(meetingID) {
$.ajax({
type: "GET",
url: 'demo10_helper.jsp',
data: "command=isRunning&meetingID="+meetingID,
dataType: "xml",
cache: false,
success: function(xml) {
response = $.xml2json(xml);
if(response.running=="true"){
$("#check_record").attr("readonly","readonly");
$("#check_record").attr("disabled","disabled");
$("#label_meeting_running").removeAttr("hidden");
$("#meta_description").val("An active session exists for "+meetingID+". This session is being recorded.");
$("#meta_description").attr("readonly","readonly");
$("#meta_description").attr("disabled","disabled");
}else{
$("#check_record").removeAttr("readonly");
$("#check_record").removeAttr("disabled");
$("#label_meeting_running").attr("hidden","");
$("#meta_description").val("");
$("#meta_description").removeAttr("readonly");
$("#meta_description").removeAttr("disabled");
}
},
error: function() {
alert("Failed to connect to API.");
}
});
}
var meetingID="Test room 1,Test room 2,Test room 3,Test room 4";
$(document).ready(function(){
isRunningMeeting("Test room 1");
$("#formcreate").validate();
$("#meetingID option[value='Test room 1']").attr("selected","selected");
jQuery("#recordgrid").jqGrid({
url: "demo10_helper.jsp?command=getRecords&meetingID="+meetingID,
datatype: "xml",
height: 150,
loadonce: true,
sortable: true,
colNames:['Id','Room','Date Recorded', 'Published', 'Playback', 'Length'],
colModel:[
{name:'id',index:'id', width:50, hidden:true, xmlmap: "recordID"},
{name:'course',index:'course', width:150, xmlmap: "name", sortable:true},
{name:'daterecorded',index:'daterecorded', width:200, xmlmap: "startTime", sortable: true, sorttype: "datetime", datefmt: "d-m-y h:i:s"},
{name:'published',index:'published', width:80, xmlmap: "published", sortable:true },
{name:'playback',index:'playback', width:150, xmlmap:"playback", sortable:false},
{name:'length',index:'length', width:80, xmlmap:"length", sortable:true}
],
xmlReader: {
root : "recordings",
row: "recording",
repeatitems:false,
id: "recordID"
},
pager : '#pager',
emptyrecords: "Nothing to display",
multiselect: true,
caption: "Recorded Sessions",
loadComplete: function(){
$("#recordgrid").trigger("reloadGrid");
}
});
});
</script>
</div>
<%
} else if (request.getParameter("action").equals("create")) {
//
// Got an action=create
//
String username = request.getParameter("username");
String meetingID = request.getParameter("meetingID");
String password = request.getParameter("password");
meeting = allMeetings.get( meetingID );
String welcomeMsg = meeting.get( "welcomeMsg" );
String logoutURL = meeting.get( "logoutURL" );
Integer voiceBridge = Integer.parseInt( meeting.get( "voiceBridge" ).trim() );
String viewerPW = meeting.get( "viewerPW" );
String moderatorPW = meeting.get( "moderatorPW" );
Boolean guest = request.getParameter("guest") != null;
Boolean record = request.getParameter("record") != null;
//
// Check if we have a valid password
//
if ( ! password.equals(viewerPW) && ! password.equals(moderatorPW) ) {
%>
Invalid Password, please <a href="javascript:history.go(-1)">try again</a>.
<%
return;
}
// create the meeting
String base_url_create = BigBlueButtonURL + "api/create?";
String base_url_join = BigBlueButtonURL + "api/join?";
String welcome_param = "&welcome=" + urlEncode(welcomeMsg);
String voiceBridge_param = "&voiceBridge=" + voiceBridge;
String moderator_password_param = "&moderatorPW=" + urlEncode(moderatorPW);
String attendee_password_param = "&attendeePW=" + urlEncode(viewerPW);
String logoutURL_param = "&logoutURL=" + urlEncode(logoutURL);
String create_parameters = "name=" + urlEncode(meetingID)
+ "&meetingID=" + urlEncode(meetingID) + welcome_param + voiceBridge_param
+ moderator_password_param + attendee_password_param + logoutURL_param
+ "&record=true";
// Attempt to create a meeting using meetingID
Document doc = null;
try {
String url = base_url_create + create_parameters
+ "&checksum="
+ checksum("create" + create_parameters + salt);
doc = parseXml( postURL( url, "" ) );
} catch (Exception e) {
e.printStackTrace();
}
if (! doc.getElementsByTagName("returncode").item(0).getTextContent()
.trim().equals("SUCCESS")) {
%>
Error: createMeeting() failed
<p /><%=meetingID%>
<%
return;
}
//
// Looks good, now return a URL to join that meeting
//
String join_parameters = "meetingID=" + urlEncode(meetingID)
+ "&fullName=" + urlEncode(username) + "&password=" + urlEncode(password) + "&guest="+ urlEncode(guest.toString());
String joinURL = base_url_join + join_parameters + "&checksum="
+ checksum("join" + join_parameters + salt);
%>
<script language="javascript" type="text/javascript">
// http://stackoverflow.com/a/11381730
mobileAndTabletcheck = function() {
var check = false;
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
return check;
}
processJoinUrl = function(url) {
if (mobileAndTabletcheck()) {
return url.replace("http://", "bigbluebutton://");
} else {
return url;
}
}
window.location.href = processJoinUrl("<%=joinURL%>");
</script>
<%
}
%>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -9,3 +9,5 @@ index.template.html
conf/config.xml
resources/lib/bbb_webrtc_bridge_sip.js
resources/lib/sip.js
resources/lib/bbb_localization.js
resources/lib/jquery-1.5.1.min.js

View File

@ -55,7 +55,7 @@
<mxmlc file="${SRC_DIR}/BBBClientCheck.mxml"
output="check/BBBClientCheck.swf"
debug="false"
locale="en_US"
locale="en_US,pt_BR"
actionscript-file-encoding="UTF-8"
incremental="false">
<static-link-runtime-shared-libraries>false</static-link-runtime-shared-libraries>
@ -106,6 +106,8 @@
<copy todir="resources/lib/" >
<fileset file="../bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js" />
<fileset file="../bigbluebutton-client/resources/prod/lib/sip.js" />
<fileset file="../bigbluebutton-client/resources/prod/lib/bbb_localization.js" />
<fileset file="../bigbluebutton-client/resources/prod/lib/jquery-1.5.1.min.js" />
</copy>
<get src="${TEST_IMAGE_URL}" dest="${html.output}/test_image.jpg" skipexisting="true" />

View File

@ -34,10 +34,12 @@
<script type="text/javascript" src="history/history.js"></script>
<!-- END Browser History required section -->
<script type="text/javascript" src="resources/lib/jquery-1.5.1.min.js"></script>
<script type="text/javascript" src="resources/lib/api-bridge.js"></script>
<script type="text/javascript" src="resources/lib/sip.js"></script>
<script type="text/javascript" src="resources/lib/bbb_webrtc_bridge_sip.js"></script>
<script type="text/javascript" src="resources/lib/deployJava.js"></script>
<script type="text/javascript" src="resources/lib/bbb_localization.js"></script>
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript">
// For version detection, set to min. required Flash Player version, or 0 (or 0.0.0), for no version detection.

View File

@ -38,6 +38,7 @@
<script type="text/javascript" src="resources/lib/sip.js"></script>
<script type="text/javascript" src="resources/lib/bbb_webrtc_bridge_sip.js"></script>
<script type="text/javascript" src="resources/lib/deployJava.js"></script>
<script type="text/javascript" src="resources/lib/bbb_localization.js"></script>
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript">
// For version detection, set to min. required Flash Player version, or 0 (or 0.0.0), for no version detection.

View File

@ -1,4 +1,4 @@
bbbsystemcheck.title = BigBlueButton Client Check
bbbsystemcheck.title = Mconf-Live Client Check
bbbsystemcheck.refresh = Refresh
bbbsystemcheck.mail = Mail
bbbsystemcheck.version = Client Check Version
@ -29,3 +29,12 @@ bbbsystemcheck.test.name.userAgent = User Agent
bbbsystemcheck.test.name.webRTCEcho = WebRTC Echo Test
bbbsystemcheck.test.name.webRTCSocket = WebRTC Socket Test
bbbsystemcheck.test.name.webRTCSupported = WebRTC Supported
bbbsystemcheck.test.name.port9123 = Port 9123
bbbsystemcheck.test.name.rtmpBigBlueButton = RTMP main app
bbbsystemcheck.test.name.rtmpDeskshare = RTMP deskshare app
bbbsystemcheck.test.name.rtmpSip = RTMP sip app
bbbsystemcheck.test.name.rtmpVideo = RTMP video app
bbbsystemcheck.test.name.rtmptBigBlueButton = RTMPT main app
bbbsystemcheck.test.name.rtmptDeskshare = RTMPT deskshare app
bbbsystemcheck.test.name.rtmptSip = RTMPT sip app
bbbsystemcheck.test.name.rtmptVideo = RTMPT video app

View File

@ -0,0 +1,40 @@
bbbsystemcheck.title = Diagnóstico do cliente Mconf-Live
bbbsystemcheck.refresh = Recarregar
bbbsystemcheck.mail = E-mail
bbbsystemcheck.version = Versão deste verificador
bbbsystemcheck.dataGridColumn.item = Item
bbbsystemcheck.dataGridColumn.status = Status
bbbsystemcheck.dataGridColumn.result = Resultado
bbbsystemcheck.copyAllText = Copiar resultados
bbbsystemcheck.result.undefined = Indefinido
bbbsystemcheck.result.javaEnabled.disabled = O Java está desabilitado em seu navegador
bbbsystemcheck.result.javaEnabled.notDetected = Java não detectado
bbbsystemcheck.result.browser.changeBrowser = Recomendamos o uso de Firefox ou Chrome para uma melhor qualidade de áudio
bbbsystemcheck.result.browser.browserOutOfDate = Seu navegador está desatualizado. Recomendamos que você o atualize para uma versão mais nova.
bbbsystemcheck.status.succeeded = Sucesso
bbbsystemcheck.status.warning = Atenção
bbbsystemcheck.status.failed = Falha
bbbsystemcheck.status.loading = Carregando...
bbbsystemcheck.test.name.browser = Navegador
bbbsystemcheck.test.name.cookieEnabled = Cookies habilitados
bbbsystemcheck.test.name.downloadSpeed = Velocidade de download
bbbsystemcheck.test.name.flashVersion = Versão do Adobe Flash Player
bbbsystemcheck.test.name.pepperFlash = Pepper Flash
bbbsystemcheck.test.name.javaEnabled = Java habilitado
bbbsystemcheck.test.name.language = Idioma
bbbsystemcheck.test.name.ping = Ping
bbbsystemcheck.test.name.screenSize = Tamanho da tela
bbbsystemcheck.test.name.uploadSpeed = Velocidade de upload
bbbsystemcheck.test.name.userAgent = User Agent
bbbsystemcheck.test.name.webRTCEcho = Eco WebRTC
bbbsystemcheck.test.name.webRTCSocket = Socket WebRTC
bbbsystemcheck.test.name.webRTCSupported = Suporte a WebRTC
bbbsystemcheck.test.name.port9123 = Porta 9123
bbbsystemcheck.test.name.rtmpBigBlueButton = RTMP main app
bbbsystemcheck.test.name.rtmpDeskshare = RTMP deskshare app
bbbsystemcheck.test.name.rtmpSip = RTMP sip app
bbbsystemcheck.test.name.rtmpVideo = RTMP video app
bbbsystemcheck.test.name.rtmptBigBlueButton = RTMPT main app
bbbsystemcheck.test.name.rtmptDeskshare = RTMPT deskshare app
bbbsystemcheck.test.name.rtmptSip = RTMPT sip app
bbbsystemcheck.test.name.rtmptVideo = RTMPT video app

View File

@ -7,41 +7,41 @@
<downloadFilePath url="test_image.jpg"/>
<ports>
<port>
<name>Port 9123</name>
<name>bbbsystemcheck.test.name.port9123</name>
<number>9123</number>
</port>
</ports>
<rtmpapps>
<app>
<name>RTMP BigBlueButton app</name>
<name>bbbsystemcheck.test.name.rtmpBigBlueButton</name>
<uri>rtmp://HOST/bigbluebutton</uri>
</app>
<app>
<name>RTMP deskShare app</name>
<name>bbbsystemcheck.test.name.rtmpDeskshare</name>
<uri>rtmp://HOST/deskShare</uri>
</app>
<app>
<name>RTMP video app</name>
<name>bbbsystemcheck.test.name.rtmpVideo</name>
<uri>rtmp://HOST/video</uri>
</app>
<app>
<name>RTMP sip app</name>
<name>bbbsystemcheck.test.name.rtmpSip</name>
<uri>rtmp://HOST/sip</uri>
</app>
<app>
<name>RTMPT BigBlueButton app</name>
<name>bbbsystemcheck.test.name.rtmptBigBlueButton</name>
<uri>rtmpt://HOST/bigbluebutton</uri>
</app>
<app>
<name>RTMPT deskShare app</name>
<name>bbbsystemcheck.test.name.rtmptDeskshare</name>
<uri>rtmpt://HOST/deskShare</uri>
</app>
<app>
<name>RTMPT video app</name>
<name>bbbsystemcheck.test.name.rtmptVideo</name>
<uri>rtmpt://HOST/video</uri>
</app>
<app>
<name>RTMPT sip app</name>
<name>bbbsystemcheck.test.name.rtmptSip</name>
<uri>rtmpt://HOST/sip</uri>
</app>
</rtmpapps>

View File

@ -208,12 +208,9 @@
var swfObj = getSwfObj();
swfObj.webRTCEchoTest(success, errorcode);
webrtc_hangup(function() {
console.log("[BBBClientCheck] Handling webRTC hangup callback");
var userAgentTemp = userAgent;
userAgent = null;
userAgentTemp.stop();
});
if (callActive === true) {
leaveWebRTCVoiceConference();
}
}
BBB.getMyUserInfo = function(callback) {
@ -224,7 +221,7 @@
myRole: "undefined",
amIPresenter: "undefined",
dialNumber: "undefined",
voiceBridge: "undefined",
voiceBridge: "00000",
customdata: "undefined"
}
@ -232,67 +229,42 @@
}
// webrtc test callbacks
BBB.webRTCEchoTestFailed = function(errorcode) {
console.log("[BBBClientCheck] Handling webRTCEchoTestFailed");
sendWebRTCEchoTestAnswer(false, errorcode);
}
BBB.webRTCEchoTestEnded = function() {
console.log("[BBBClientCheck] Handling webRTCEchoTestEnded");
}
BBB.webRTCEchoTestStarted = function() {
console.log("[BBBClientCheck] Handling webRTCEchoTestStarted");
sendWebRTCEchoTestAnswer(true, 'Connected');
}
BBB.webRTCEchoTestConnecting = function() {
console.log("[BBBClientCheck] Handling webRTCEchoTestConnecting");
}
BBB.webRTCEchoTestWaitingForICE = function() {
console.log("[BBBClientCheck] Handling webRTCEchoTestWaitingForICE");
}
BBB.webRTCEchoTestWebsocketSucceeded = function() {
console.log("[BBBClientCheck] Handling webRTCEchoTestWebsocketSucceeded");
BBB.webRTCCallSucceeded = function() {
console.log("[BBBClientCheck] Handling webRTCCallSucceeded");
var swfObj = getSwfObj();
swfObj.webRTCSocketTest(true, 'Connected');
}
BBB.webRTCEchoTestWebsocketFailed = function(errorcode) {
console.log("[BBBClientCheck] Handling webRTCEchoTestWebsocketFailed");
BBB.webRTCCallFailed = function(inEchoTest, errorcode, cause) {
console.log("[BBBClientCheck] Handling webRTCCallFailed, errorcode " + errorcode + ", cause: " + cause);
if (errorcode == 1002) {
// failed to connect the websocket
var swfObj = getSwfObj();
swfObj.webRTCSocketTest(false, errorcode);
} else {
sendWebRTCEchoTestAnswer(false, errorcode);
}
}
// webrtc callbacks
BBB.webRTCConferenceCallFailed = function(errorcode) {
console.log("[BBBClientCheck] Handling webRTCConferenceCallFailed");
BBB.webRTCCallEnded = function(inEchoTest) {
console.log("[BBBClientCheck] Handling webRTCCallEnded");
}
BBB.webRTCConferenceCallEnded = function() {
console.log("[BBBClientCheck] Handling webRTCConferenceCallEnded");
BBB.webRTCCallStarted = function(inEchoTest) {
console.log("[BBBClientCheck] Handling webRTCCallStarted");
sendWebRTCEchoTestAnswer(true, 'Connected');
}
BBB.webRTCConferenceCallStarted = function() {
console.log("[BBBClientCheck] Handling webRTCConferenceCallStarted");
BBB.webRTCCallConnecting = function(inEchoTest) {
console.log("[BBBClientCheck] Handling webRTCCallConnecting");
}
BBB.webRTCConferenceCallConnecting = function() {
console.log("[BBBClientCheck] Handling webRTCConferenceCallConnecting");
BBB.webRTCCallWaitingForICE = function(inEchoTest) {
console.log("[BBBClientCheck] Handling webRTCCallWaitingForICE");
}
BBB.webRTCConferenceCallWaitingForICE = function() {
console.log("[BBBClientCheck] Handling webRTCConferenceCallWaitingForICE");
}
BBB.webRTCConferenceCallWebsocketSucceeded = function() {
console.log("[BBBClientCheck] Handling webRTCConferenceCallWebsocketSucceeded");
}
BBB.webRTCConferenceCallWebsocketFailed = function(errorcode) {
console.log("[BBBClientCheck] Handling webRTCConferenceCallWebsocketFailed");
BBB.webRTCCallTransferring = function(inEchoTest) {
console.log("[BBBClientCheck] Handling webRTCCallTransferring");
}
BBB.webRTCMediaRequest = function() {

View File

@ -18,6 +18,8 @@
<![CDATA[
import mx.events.FlexEvent;
import flash.external.ExternalInterface;
import org.bigbluebutton.clientcheck.AppConfig;
import org.bigbluebutton.clientcheck.view.mainview.MainViewConfig;
import org.bigbluebutton.clientcheck.view.mainview.RefreshButtonConfig;
@ -31,12 +33,25 @@
private static var robotlegsContext:IContext;
private static var DEFAULT_LOCALE:String = "en_US";
private var language:String;
protected function preinitializeHandler(event:FlexEvent):void
{
setLanguage();
setupRobotlegsContext();
Security.allowDomain("*");
}
private function setLanguage():void
{
language = ExternalInterface.call("getLanguage");
if (resourceManager.getLocales().indexOf(language) != -1)
{
resourceManager.localeChain = [language, DEFAULT_LOCALE];
}
}
/**
* Setup robotlegs initial configuration
*/

View File

@ -23,6 +23,7 @@ package org.bigbluebutton.clientcheck.command
import flash.utils.getTimer;
import mx.core.FlexGlobals;
import mx.resources.ResourceManager;
import mx.utils.URLUtil;
import org.bigbluebutton.clientcheck.model.ISystemConfiguration;
@ -78,7 +79,8 @@ package org.bigbluebutton.clientcheck.command
for each (var _port:Object in config.getPorts())
{
var port:IPortTest=new PortTest();
port.portName=_port.name;
port.portName=ResourceManager.getInstance().getString('resources', _port.name);
if (port.portName == "undefined") port.portName = _port.name;
port.portNumber=_port.number;
systemConfiguration.ports.push(port);
}
@ -86,7 +88,8 @@ package org.bigbluebutton.clientcheck.command
for each (var _rtmpApp:Object in config.getRTMPApps())
{
var app:IRTMPAppTest=new RTMPAppTest();
app.applicationName=_rtmpApp.name;
app.applicationName=ResourceManager.getInstance().getString('resources', _rtmpApp.name);
if (app.applicationName == "undefined") app.applicationName = _rtmpApp.name;
app.applicationUri=_rtmpApp.uri;
systemConfiguration.rtmpApps.push(app);
}

View File

@ -22,6 +22,9 @@
switch (value.StatusPriority) {
case SUCCEEDED:
colorRect.visible = true;
solid.color = 0x00FF00;
break;
case LOADING:
colorRect.visible = false;
break;

View File

@ -25,6 +25,6 @@ package org.bigbluebutton.clientcheck.view.mainview
public static const FAILED:Object = {StatusMessage: ResourceManager.getInstance().getString('resources', 'bbbsystemcheck.status.failed'), StatusPriority: 1};
public static const WARNING:Object = {StatusMessage: ResourceManager.getInstance().getString('resources', 'bbbsystemcheck.status.warning'), StatusPriority: 2};
public static const LOADING:Object = {StatusMessage: ResourceManager.getInstance().getString('resources', 'bbbsystemcheck.status.loading'), StatusPriority: 3};
public static const SUCCEED:Object = {StatusMessage: "", StatusPriority: 4};
public static const SUCCEED:Object = {StatusMessage: ResourceManager.getInstance().getString('resources', 'bbbsystemcheck.status.succeeded'), StatusPriority: 4};
}
}

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src/main/java"/>
<classpathentry kind="src" path="src/test/java"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="lib" path="lib/com.springsource.slf4j.api-1.6.1.jar"/>
<classpathentry kind="lib" path="lib/com.springsource.slf4j.bridge-1.6.1.jar"/>
<classpathentry kind="lib" path="lib/commons-pool-1.5.6.jar"/>
<classpathentry kind="lib" path="lib/easymock-2.4.jar"/>
<classpathentry kind="lib" path="lib/jcl-over-slf4j-1.6.1.jar"/>
<classpathentry kind="lib" path="lib/jedis-2.0.0.jar"/>
<classpathentry kind="lib" path="lib/jul-to-slf4j-1.6.1.jar"/>
<classpathentry kind="lib" path="lib/log4j-over-slf4j-1.6.1.jar"/>
<classpathentry kind="lib" path="lib/logback-classic-0.9.28.jar"/>
<classpathentry kind="lib" path="lib/logback-core-0.9.28.jar"/>
<classpathentry kind="lib" path="lib/mina-core-2.0.4.jar"/>
<classpathentry kind="lib" path="lib/mina-integration-beans-2.0.4.jar"/>
<classpathentry kind="lib" path="lib/mina-integration-jmx-2.0.4.jar"/>
<classpathentry kind="lib" path="lib/servlet-api-2.5.jar"/>
<classpathentry kind="lib" path="lib/spring-beans-3.0.6.RELEASE.jar"/>
<classpathentry kind="lib" path="lib/spring-context-3.0.6.RELEASE.jar"/>
<classpathentry kind="lib" path="lib/spring-core-3.0.6.RELEASE.jar"/>
<classpathentry kind="lib" path="lib/spring-web-3.0.6.RELEASE.jar"/>
<classpathentry kind="lib" path="lib/testng-5.8.jar"/>
<classpathentry kind="lib" path="lib/red5-1.0r4406.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -2,4 +2,6 @@ bin
build
dist
lib
build
.classpath
.project
.settings

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>bbb-video</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@ -65,6 +65,7 @@ dependencies {
providedCompile 'org/red5:red5-server:1.0.5-RELEASE@jar'
providedCompile 'org.red5:red5-server-common:1.0.5-RELEASE@jar'
providedCompile 'org.red5:red5-io:1.0.5-RELEASE@jar'
providedCompile 'org.red5:red5-client:1.0.5-RELEASE@jar'
// Logging
providedCompile 'ch.qos.logback:logback-core:1.1.2@jar'
@ -90,6 +91,9 @@ dependencies {
compile 'redis.clients:jedis:2.0.0'
providedCompile 'commons-pool:commons-pool:1.5.6'
compile 'com.google.code.gson:gson:1.7.1'
// Needed for StringUtils
providedCompile 'org.apache.commons:commons-lang3:3.1@jar'
}
test {

View File

@ -0,0 +1,59 @@
/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright 2006-2012 by respective authors (see below). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bigbluebutton.app.video;
import org.red5.client.net.rtmp.RTMPClient;
import org.red5.server.service.PendingCall;
import org.red5.server.net.rtmp.event.Ping;
import org.red5.logging.Red5LoggerFactory;
import org.slf4j.Logger;
/**
* Custom RTMP Client
*
* This client behaves like the flash player plugin when requesting to play a stream
*
* @author Mateus Dalepiane (mdalepiane@gmail.com)
*/
public class CustomRTMPClient extends RTMPClient {
private static Logger log = Red5LoggerFactory.getLogger(CustomRTMPClient.class);
public void play(int streamId, String name) {
log.info("play stream "+ streamId + ", name: " + name);
if (conn != null) {
// get the channel
int channel = getChannelForStreamId(streamId);
// send our requested buffer size
ping(Ping.CLIENT_BUFFER, streamId, 2000);
// send our request for a/v
PendingCall receiveAudioCall = new PendingCall("receiveAudio");
conn.invoke(receiveAudioCall, channel);
PendingCall receiveVideoCall = new PendingCall("receiveVideo");
conn.invoke(receiveVideoCall, channel);
// call play
Object[] params = new Object[1];
params[0] = name;
PendingCall pendingCall = new PendingCall("play", params);
conn.invoke(pendingCall, channel);
} else {
log.warn("Trying to play on a null connection");
}
}
}

View File

@ -0,0 +1,347 @@
/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright 2006-2012 by respective authors (see below). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bigbluebutton.app.video;
import java.io.IOException;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.red5.client.net.rtmp.ClientExceptionHandler;
import org.red5.client.net.rtmp.INetStreamEventHandler;
import org.red5.client.net.rtmp.RTMPClient;
import org.red5.io.utils.ObjectMap;
import org.red5.proxy.StreamingProxy;
import org.red5.server.api.event.IEvent;
import org.red5.server.api.event.IEventDispatcher;
import org.red5.server.api.service.IPendingServiceCall;
import org.red5.server.api.service.IPendingServiceCallback;
import org.red5.server.net.rtmp.event.IRTMPEvent;
import org.red5.server.net.rtmp.event.Notify;
import org.red5.server.net.rtmp.status.StatusCodes;
import org.red5.server.stream.message.RTMPMessage;
import org.red5.logging.Red5LoggerFactory;
import org.slf4j.Logger;
/**
* Relay a stream from one location to another via RTMP.
*
* @author Paul Gregoire (mondain@gmail.com)
*/
public class CustomStreamRelay {
private static Logger log = Red5LoggerFactory.getLogger(CustomStreamRelay.class);
// our consumer
private CustomRTMPClient client;
// our publisher
private StreamingProxy proxy;
// task timer
private Timer timer;
private String sourceHost;
private String destHost;
private String sourceApp;
private String destApp;
private int sourcePort;
private int destPort;
private String sourceStreamName;
private String destStreamName;
private String publishMode;
Map<String, Object> defParams;
private boolean isDisconnecting;
/**
* Creates a stream client to consume a stream from an end point and a proxy to relay the stream
* to another end point.
*
* @param args application arguments
*/
public void setSourceHost(String sourceHost) {
this.sourceHost = sourceHost;
}
public void setSourcePort(int sourcePort) {
this.sourcePort = sourcePort;
}
public void setDestinationHost(String destHost) {
this.destHost = destHost;
}
public void setDestinationPort(int destPort) {
this.destPort = destPort;
}
public void setSourceApp(String sourceApp) {
this.sourceApp = sourceApp;
}
public void setDestinationApp(String destApp) {
this.destApp = destApp;
}
public void setSourceStreamName(String sourceStreamName) {
this.sourceStreamName = sourceStreamName;
}
public void setDestinationStreamName(String destStreamName) {
this.destStreamName = destStreamName;
}
public void setPublishMode(String publishMode) {
this.publishMode = publishMode;
}
public void initRelay(String... args) {
if (args == null || args.length < 7) {
log.error("Not enough args supplied. Usage: <source uri> <source app> <source stream name> <destination uri> <destination app> <destination stream name> <publish mode>");
}
else {
sourceHost = args[0];
destHost = args[3];
sourceApp = args[1];
destApp = args[4];
sourcePort = 1935;
destPort = 1935;
sourceStreamName = args[2];
destStreamName = args[5];
publishMode = args[6]; //live, record, or append
int colonIdx = sourceHost.indexOf(':');
if (colonIdx > 0) {
sourcePort = Integer.valueOf(sourceHost.substring(colonIdx + 1));
sourceHost = sourceHost.substring(0, colonIdx);
log.trace("Source host: %s port: %d\n", sourceHost, sourcePort);
}
colonIdx = destHost.indexOf(':');
if (colonIdx > 0) {
destPort = Integer.valueOf(destHost.substring(colonIdx + 1));
destHost = destHost.substring(0, colonIdx);
log.trace("Destination host: %s port: %d\n", destHost, destPort);
}
}
}
public void stopRelay() {
isDisconnecting = true;
client.disconnect();
proxy.stop();
}
public void startRelay() {
isDisconnecting = false;
// create a timer
timer = new Timer();
// create our publisher
proxy = new StreamingProxy();
proxy.setHost(destHost);
proxy.setPort(destPort);
proxy.setApp(destApp);
proxy.init();
proxy.setConnectionClosedHandler(new Runnable() {
public void run() {
log.info("Publish connection has been closed, source will be disconnected");
client.disconnect();
}
});
proxy.setExceptionHandler(new ClientExceptionHandler() {
@Override
public void handleException(Throwable throwable) {
throwable.printStackTrace();
proxy.stop();
}
});
proxy.start(destStreamName, publishMode, new Object[] {});
// wait for the publish state
// Change to use signal or something more cleaner
do {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
} while (!proxy.isPublished());
log.info("Publishing...");
// create the consumer
client = new CustomRTMPClient();
client.setStreamEventDispatcher(new StreamEventDispatcher());
client.setStreamEventHandler(new INetStreamEventHandler() {
public void onStreamEvent(Notify notify) {
ObjectMap<?, ?> map = (ObjectMap<?, ?>) notify.getCall().getArguments()[0];
String code = (String) map.get("code");
if (StatusCodes.NS_PLAY_STREAMNOTFOUND.equals(code)) {
log.info("Requested stream was not found");
isDisconnecting = true;
client.disconnect();
} else if (StatusCodes.NS_PLAY_UNPUBLISHNOTIFY.equals(code) || StatusCodes.NS_PLAY_COMPLETE.equals(code)) {
log.info("Source has stopped publishing or play is complete");
isDisconnecting = true;
client.disconnect();
}
}
});
client.setConnectionClosedHandler(new Runnable() {
public void run() {
log.info("Source connection has been closed");
//System.exit(2);
if(isDisconnecting) {
log.info("Proxy will be stopped");
client.disconnect();
proxy.stop();
} else {
log.info("Reconnecting client...");
client.connect(sourceHost, sourcePort, defParams, new ClientConnectCallback());
}
}
});
client.setExceptionHandler(new ClientExceptionHandler() {
@Override
public void handleException(Throwable throwable) {
throwable.printStackTrace();
//System.exit(1);
client.disconnect();
proxy.stop();
}
});
// connect the consumer
defParams = client.makeDefaultConnectionParams(sourceHost, sourcePort, sourceApp);
// add pageurl and swfurl
defParams.put("pageUrl", "");
defParams.put("swfUrl", "app:/Red5-StreamRelay.swf");
// indicate for the handshake to generate swf verification data
client.setSwfVerification(true);
// connect the client
log.trace("startRelay:: ProxyRelay status is running: " + proxy.isRunning());
client.connect(sourceHost, sourcePort, defParams, new ClientConnectCallback());
}
private final class ClientConnectCallback implements IPendingServiceCallback{
public void resultReceived(IPendingServiceCall call) {
log.trace("connectCallback");
ObjectMap<?, ?> map = (ObjectMap<?, ?>) call.getResult();
String code = (String) map.get("code");
if ("NetConnection.Connect.Rejected".equals(code)) {
log.warn("Rejected: %s\n", map.get("description"));
client.disconnect();
proxy.stop();
} else if ("NetConnection.Connect.Success".equals(code)) {
// 1. Wait for onBWDone
timer.schedule(new BandwidthStatusTask(), 2000L);
} else {
log.warn("Unhandled response code: %s\n", code);
}
}
}
/**
* Dispatches consumer events.
*/
private final class StreamEventDispatcher implements IEventDispatcher {
public void dispatchEvent(IEvent event) {
try {
proxy.pushMessage(null, RTMPMessage.build((IRTMPEvent) event));
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Handles result from subscribe call.
*/
private final class SubscribeStreamCallBack implements IPendingServiceCallback {
public void resultReceived(IPendingServiceCall call) {
log.trace("SubscirbeStreamCallBack::resultReceived: " + call);
}
}
/**
* Creates a "stream" via playback, this is the source stream.
*/
private final class CreateStreamCallback implements IPendingServiceCallback {
public void resultReceived(IPendingServiceCall call) {
log.trace("CreateStreamCallBack::resultReceived: " + call);
int streamId = (Integer) call.getResult();
log.trace("stream id: " + streamId);
// send our buffer size request
if (sourceStreamName.endsWith(".flv") || sourceStreamName.endsWith(".f4v") || sourceStreamName.endsWith(".mp4")) {
log.trace("play stream name " + sourceStreamName + " start 0 lenght -1");
client.play(streamId, sourceStreamName, 0, -1);
} else {
log.trace("play stream name " + sourceStreamName);
client.play(streamId, sourceStreamName);
}
}
}
/**
* Continues to check for onBWDone
*/
private final class BandwidthStatusTask extends TimerTask {
@Override
public void run() {
// check for onBWDone
log.info("Bandwidth check done: " + client.isBandwidthCheckDone());
// cancel this task
this.cancel();
// create a task to wait for subscribed
timer.schedule(new PlayStatusTask(), 1000L);
// 2. send FCSubscribe
client.subscribe(new SubscribeStreamCallBack(), new Object[] { sourceStreamName });
}
}
private final class PlayStatusTask extends TimerTask {
@Override
public void run() {
// checking subscribed
log.info("Subscribed: " + client.isSubscribed());
// cancel this task
this.cancel();
// 3. create stream
client.createStream(new CreateStreamCallback());
}
}
}

View File

@ -21,14 +21,26 @@ package org.bigbluebutton.app.video;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.commons.lang3.StringUtils;
import org.bigbluebutton.app.video.converter.H263Converter;
import org.bigbluebutton.app.video.converter.VideoRotator;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.adapter.MultiThreadedApplicationAdapter;
import org.red5.server.api.IConnection;
import org.red5.server.api.Red5;
import org.red5.server.api.scope.IBasicScope;
import org.red5.server.api.scope.IBroadcastScope;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.scope.ScopeType;
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.IPlayItem;
import org.red5.server.api.stream.IServerStream;
import org.red5.server.api.stream.IStreamListener;
import org.red5.server.api.stream.ISubscriberStream;
import org.red5.server.stream.ClientBroadcastStream;
import org.slf4j.Logger;
import com.google.gson.Gson;
@ -43,12 +55,25 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
private EventRecordingService recordingService;
private final Map<String, IStreamListener> streamListeners = new HashMap<String, IStreamListener>();
private Map<String, CustomStreamRelay> remoteStreams = new ConcurrentHashMap<String, CustomStreamRelay>();
private Map<String, Integer> listenersOnRemoteStream = new ConcurrentHashMap<String, Integer>();
// Proxy disconnection timer
private Timer timer;
// Proxy disconnection timeout
private long relayTimeout;
private final Map<String, H263Converter> h263Converters = new HashMap<String, H263Converter>();
private final Map<String, VideoRotator> videoRotators = new HashMap<String, VideoRotator>();
@Override
public boolean appStart(IScope app) {
super.appStart(app);
log.info("BBB Video appStart");
System.out.println("BBB Video appStart");
appScope = app;
timer = new Timer();
return true;
}
@ -61,6 +86,13 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
@Override
public boolean roomConnect(IConnection conn, Object[] params) {
log.info("BBB Video roomConnect");
if(params.length == 0) {
params = new Object[2];
params[0] = "UNKNOWN-MEETING-ID";
params[1] = "UNKNOWN-USER-ID";
}
String meetingId = ((String) params[0]).toString();
String userId = ((String) params[1]).toString();
@ -162,18 +194,34 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
super.streamPublishStart(stream);
}
public IBroadcastScope getBroadcastScope(IScope scope, String name) {
IBasicScope basicScope = scope.getBasicScope(ScopeType.BROADCAST, name);
if (basicScope instanceof IBroadcastScope) {
return (IBroadcastScope) basicScope;
} else {
return null;
}
}
@Override
public void streamBroadcastStart(IBroadcastStream stream) {
IConnection conn = Red5.getConnectionLocal();
super.streamBroadcastStart(stream);
log.info("streamBroadcastStart " + stream.getPublishedName() + " " + System.currentTimeMillis() + " " + conn.getScope().getName());
String streamName = stream.getPublishedName();
log.info("streamBroadcastStart " + streamName + " " + System.currentTimeMillis() + " " + conn.getScope().getName());
if (recordVideoStream) {
if (streamName.contains("/")) {
if(VideoRotator.getDirection(streamName) != null) {
VideoRotator rotator = new VideoRotator(streamName);
videoRotators.put(streamName, rotator);
}
}
else if (recordVideoStream) {
recordStream(stream);
VideoStreamListener listener = new VideoStreamListener();
listener.setEventRecordingService(recordingService);
stream.addStreamListener(listener);
streamListeners.put(conn.getScope().getName() + "-" + stream.getPublishedName(), listener);
streamListeners.put(conn.getScope().getName() + "-" + streamName, listener);
}
}
@ -181,6 +229,11 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
}
private boolean isH263Stream(ISubscriberStream stream) {
String streamName = stream.getBroadcastStreamPublishName();
return streamName.startsWith(H263Converter.H263PREFIX);
}
@Override
public void streamBroadcastClose(IBroadcastStream stream) {
super.streamBroadcastClose(stream);
@ -209,6 +262,17 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
event.put("eventName", "StopWebcamShareEvent");
recordingService.record(scopeName, event);
}
String streamName = stream.getName();
if(h263Converters.containsKey(streamName)) {
// Stop converter
h263Converters.remove(streamName).stopConverter();
}
if(videoRotators.containsKey(streamName)) {
// Stop rotator
videoRotators.remove(streamName).stop();
}
}
/**
@ -238,4 +302,125 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
recordingService = s;
}
public void setRelayTimeout(long timeout) {
this.relayTimeout = timeout;
}
@Override
public void streamPlayItemPlay(ISubscriberStream stream, IPlayItem item, boolean isLive) {
// log w3c connect event
String streamName = item.getName();
streamName = streamName.replaceAll(H263Converter.H263PREFIX, "");
if(isH263Stream(stream)) {
log.trace("Detected H263 stream request [{}]", streamName);
synchronized (h263Converters) {
// Check if a new stream converter is necessary
if(!h263Converters.containsKey(streamName)) {
H263Converter converter = new H263Converter(streamName);
h263Converters.put(streamName, converter);
}
else {
H263Converter converter = h263Converters.get(streamName);
converter.addListener();
}
}
}
if(streamName.contains("/")) {
synchronized(remoteStreams) {
if(remoteStreams.containsKey(streamName) == false) {
String[] parts = streamName.split("/");
String sourceServer = parts[0];
String sourceStreamName = StringUtils.join(parts, '/', 1, parts.length);
String destinationServer = Red5.getConnectionLocal().getHost();
String destinationStreamName = streamName;
String app = "video/"+Red5.getConnectionLocal().getScope().getName();
log.trace("streamPlayItemPlay:: streamName [" + streamName + "]");
log.trace("streamPlayItemPlay:: sourceServer [" + sourceServer + "]");
log.trace("streamPlayItemPlay:: sourceStreamName [" + sourceStreamName + "]");
log.trace("streamPlayItemPlay:: destinationServer [" + destinationServer + "]");
log.trace("streamPlayItemPlay:: destinationStreamName [" + destinationStreamName + "]");
log.trace("streamPlayItemPlay:: app [" + app + "]");
CustomStreamRelay remoteRelay = new CustomStreamRelay();
remoteRelay.initRelay(new String[]{sourceServer, app, sourceStreamName, destinationServer, app, destinationStreamName, "live"});
remoteRelay.startRelay();
remoteStreams.put(destinationStreamName, remoteRelay);
listenersOnRemoteStream.put(streamName, 1);
}
else {
Integer numberOfListeners = listenersOnRemoteStream.get(streamName) + 1;
listenersOnRemoteStream.put(streamName,numberOfListeners);
}
}
}
log.info("W3C x-category:stream x-event:play c-ip:{} x-sname:{} x-name:{}", new Object[] { Red5.getConnectionLocal().getRemoteAddress(), stream.getName(), item.getName() });
}
@Override
public void streamSubscriberClose(ISubscriberStream stream) {
String streamName = stream.getBroadcastStreamPublishName();
streamName = streamName.replaceAll(H263Converter.H263PREFIX, "");
if(isH263Stream(stream)) {
synchronized (h263Converters) {
// Remove prefix
if(h263Converters.containsKey(streamName)) {
H263Converter converter = h263Converters.get(streamName);
converter.removeListener();
}
else {
log.warn("Converter not found for H263 stream [{}]", streamName);
}
}
}
synchronized(remoteStreams) {
super.streamSubscriberClose(stream);
log.trace("Subscriber close for stream [{}]", streamName);
if(streamName.contains("/")) {
if(remoteStreams.containsKey(streamName)) {
Integer numberOfListeners = listenersOnRemoteStream.get(streamName);
if(numberOfListeners != null) {
numberOfListeners = numberOfListeners - 1;
listenersOnRemoteStream.put(streamName, numberOfListeners);
log.trace("Stream [{}] has {} subscribers left", streamName, numberOfListeners);
if(numberOfListeners < 1) {
log.info("Starting timeout to close proxy for stream: {}", streamName);
timer.schedule(new DisconnectProxyTask(streamName), relayTimeout);
}
}
}
}
}
}
private final class DisconnectProxyTask extends TimerTask {
// Stream name that should be disconnected
private String streamName;
public DisconnectProxyTask(String streamName) {
this.streamName = streamName;
}
@Override
public void run() {
// Cancel this task
this.cancel();
// Check if someone reconnected
synchronized(remoteStreams) {
Integer numberOfListeners = listenersOnRemoteStream.get(streamName);
log.trace("Stream [{}] has {} subscribers", streamName, numberOfListeners);
if(numberOfListeners != null) {
if(numberOfListeners < 1) {
// No one else is connected to this stream, close relay
log.info("Stopping relay for stream [{}]", streamName);
listenersOnRemoteStream.remove(streamName);
CustomStreamRelay remoteRelay = remoteStreams.remove(streamName);
remoteRelay.stopRelay();
}
}
}
}
}
}

View File

@ -0,0 +1,107 @@
package org.bigbluebutton.app.video.converter;
import org.bigbluebutton.app.video.ffmpeg.FFmpegCommand;
import org.bigbluebutton.app.video.ffmpeg.ProcessMonitor;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.IConnection;
import org.red5.server.api.Red5;
import org.slf4j.Logger;
/**
* Represents a stream converter to H263. This class is responsible
* for managing the execution of FFmpeg based on the number of listeners
* connected to the stream. When the first listener is added FFmpef is
* launched, and when the last listener is removed FFmpeg is stopped.
* Converted streams are published in the same scope as the original ones,
* with 'h263/' appended in the beginning.
*/
public class H263Converter {
private static Logger log = Red5LoggerFactory.getLogger(H263Converter.class, "video");
public final static String H263PREFIX = "h263/";
private String origin;
private Integer numListeners = 0;
private FFmpegCommand ffmpeg;
private ProcessMonitor processMonitor;
/**
* Creates a H263Converter from a given streamName. It is assumed
* that one listener is responsible for this creation, therefore
* FFmpeg is launched.
*
* @param origin streamName of the stream that should be converted
*/
public H263Converter(String origin) {
log.info("Spawn FFMpeg to convert H264 to H263 for stream [{}]", origin);
this.origin = origin;
IConnection conn = Red5.getConnectionLocal();
String ip = conn.getHost();
String conf = conn.getScope().getName();
String inputLive = "rtmp://" + ip + "/video/" + conf + "/" + origin + " live=1";
String output = "rtmp://" + ip + "/video/" + conf + "/" + H263PREFIX + origin;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath("/usr/local/bin/ffmpeg");
ffmpeg.setInput(inputLive);
ffmpeg.setCodec("flv1"); // Sorensen H263
ffmpeg.setFormat("flv");
ffmpeg.setOutput(output);
ffmpeg.setLoglevel("warning");
ffmpeg.setAnalyzeDuration("10000"); // 10ms
this.addListener();
}
/**
* Launches the process monitor responsible for FFmpeg.
*/
private void startConverter() {
String[] command = ffmpeg.getFFmpegCommand(true);
processMonitor = new ProcessMonitor(command);
processMonitor.start();
}
/**
* Adds a listener to H263Converter. If there were
* zero listeners, FFmpeg is launched for this stream.
*/
public synchronized void addListener() {
this.numListeners++;
log.trace("Adding listener to [{}] ; [{}] current listeners ", origin, this.numListeners);
if(this.numListeners.equals(1)) {
log.debug("First listener just joined, must start H263Converter for [{}]", origin);
startConverter();
}
}
/**
* Removes a listener from H263Converter. There are
* zero listeners left, FFmpeg is stopped this stream.
*/
public synchronized void removeListener() {
this.numListeners--;
log.trace("Removing listener from [{}] ; [{}] current listeners ", origin, this.numListeners);
if(this.numListeners <= 0) {
log.debug("No more listeners, may close H263Converter for [{}]", origin);
this.stopConverter();
}
}
/**
* Stops FFmpeg for this stream and sets the number of
* listeners to zero.
*/
public synchronized void stopConverter() {
this.numListeners = 0;
if(processMonitor != null) {
processMonitor.destroy();
processMonitor = null;
}
}
}

View File

@ -0,0 +1,112 @@
package org.bigbluebutton.app.video.converter;
import org.apache.commons.lang3.StringUtils;
import org.bigbluebutton.app.video.ffmpeg.FFmpegCommand;
import org.bigbluebutton.app.video.ffmpeg.ProcessMonitor;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.IConnection;
import org.red5.server.api.Red5;
import org.slf4j.Logger;
/**
* Represents a stream rotator. This class is responsible
* for choosing the rotate direction based on the stream name
* and starting FFmpeg to rotate and re-publish the stream.
*/
public class VideoRotator {
private static Logger log = Red5LoggerFactory.getLogger(VideoRotator.class, "video");
public static final String ROTATE_LEFT = "rotate_left";
public static final String ROTATE_RIGHT = "rotate_right";
private String streamName;
private FFmpegCommand.ROTATE direction;
private FFmpegCommand ffmpeg;
private ProcessMonitor processMonitor;
/**
* Create a new video rotator for the specified stream.
* The streamName should be of the form:
* rotate_[left|right]/streamName
* The rotated stream will be published as streamName.
*
* @param origin Name of the stream that will be rotated
*/
public VideoRotator(String origin) {
this.streamName = getStreamName(origin);
this.direction = getDirection(origin);
IConnection conn = Red5.getConnectionLocal();
String ip = conn.getHost();
String conf = conn.getScope().getName();
String inputLive = "rtmp://" + ip + "/video/" + conf + "/" + origin + " live=1";
String output = "rtmp://" + ip + "/video/" + conf + "/" + streamName;
ffmpeg = new FFmpegCommand();
ffmpeg.setFFmpegPath("/usr/local/bin/ffmpeg");
ffmpeg.setInput(inputLive);
ffmpeg.setFormat("flv");
ffmpeg.setOutput(output);
ffmpeg.setLoglevel("warning");
ffmpeg.setRotation(direction);
ffmpeg.setAnalyzeDuration("10000"); // 10ms
start();
}
/**
* Get the stream name from the direction/streamName string
* @param streamName Name of the stream with rotate direction
* @return The stream name used for re-publish
*/
private String getStreamName(String streamName) {
String parts[] = streamName.split("/");
if(parts.length > 1)
return StringUtils.join(parts, '/', 1, parts.length);
return "";
}
/**
* Get the rotate direction from the streamName string.
* @param streamName Name of the stream with rotate direction
* @return FFmpegCommand.ROTATE for the given direction if present, null otherwise
*/
public static FFmpegCommand.ROTATE getDirection(String streamName) {
String parts[] = streamName.split("/");
switch(parts[0]) {
case ROTATE_LEFT:
return FFmpegCommand.ROTATE.LEFT;
case ROTATE_RIGHT:
return FFmpegCommand.ROTATE.RIGHT;
default:
return null;
}
}
/**
* Start FFmpeg process to rotate and re-publish the stream.
*/
public void start() {
log.debug("Spawn FFMpeg to rotate [{}] stream [{}]", direction.name(), streamName);
String[] command = ffmpeg.getFFmpegCommand(true);
if (processMonitor == null) {
processMonitor = new ProcessMonitor(command);
}
processMonitor.start();
}
/**
* Stop FFmpeg process that is rotating and re-publishing the stream.
*/
public void stop() {
log.debug("Stopping FFMpeg from rotate [{}] stream [{}]", direction.name(), streamName);
if(processMonitor != null) {
processMonitor.destroy();
processMonitor = null;
}
}
}

View File

@ -0,0 +1,191 @@
package org.bigbluebutton.app.video.ffmpeg;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
public class FFmpegCommand {
/**
* Indicate the direction to rotate the video
*/
public enum ROTATE { LEFT, RIGHT };
private HashMap args;
private HashMap x264Params;
private String[] command;
private String ffmpegPath;
private String input;
private String output;
/* Analyze duration is a special parameter that MUST come before the input */
private String analyzeDuration;
public FFmpegCommand() {
this.args = new HashMap();
this.x264Params = new HashMap();
/* Prevent quality loss by default */
try {
this.setVideoQualityScale(1);
} catch (InvalidParameterException e) {
// TODO Auto-generated catch block
e.printStackTrace();
};
this.ffmpegPath = null;
}
public String[] getFFmpegCommand(boolean shouldBuild) {
if(shouldBuild)
buildFFmpegCommand();
return this.command;
}
public void buildFFmpegCommand() {
List comm = new ArrayList<String>();
if(this.ffmpegPath == null)
this.ffmpegPath = "/usr/local/bin/ffmpeg";
comm.add(this.ffmpegPath);
/* Analyze duration MUST come before the input */
if(analyzeDuration != null && !analyzeDuration.isEmpty()) {
comm.add("-analyzeduration");
comm.add(analyzeDuration);
}
comm.add("-i");
comm.add(input);
Iterator argsIter = this.args.entrySet().iterator();
while (argsIter.hasNext()) {
Map.Entry pairs = (Map.Entry)argsIter.next();
comm.add(pairs.getKey());
comm.add(pairs.getValue());
}
if(!x264Params.isEmpty()) {
comm.add("-x264-params");
String params = "";
Iterator x264Iter = this.x264Params.entrySet().iterator();
while (x264Iter.hasNext()) {
Map.Entry pairs = (Map.Entry)x264Iter.next();
String argValue = pairs.getKey() + "=" + pairs.getValue();
params += argValue;
// x264-params are separated by ':'
params += ":";
}
// Remove trailing ':'
params.replaceAll(":+$", "");
comm.add(params);
}
comm.add(this.output);
this.command = new String[comm.size()];
comm.toArray(this.command);
}
public void setFFmpegPath(String arg) {
this.ffmpegPath = arg;
}
public void setInput(String arg) {
this.input = arg;
}
public void setOutput(String arg) {
this.output = arg;
}
public void setCodec(String arg) {
this.args.put("-vcodec", arg);
}
public void setLevel(String arg) {
this.args.put("-level", arg);
}
public void setPreset(String arg) {
this.args.put("-preset", arg);
}
public void setProfile(String arg) {
this.args.put("-profile:v", arg);
}
public void setFormat(String arg) {
this.args.put("-f", arg);
}
public void setPayloadType(String arg) {
this.args.put("-payload_type", arg);
}
public void setLoglevel(String arg) {
this.args.put("-loglevel", arg);
}
public void setSliceMaxSize(String arg) {
this.x264Params.put("slice-max-size", arg);
}
public void setMaxKeyFrameInterval(String arg) {
this.x264Params.put("keyint", arg);
}
public void setResolution(String arg) {
this.args.put("-s", arg);
}
/**
* Set the direction to rotate the video
* @param arg Rotate direction
*/
public void setRotation(ROTATE arg) {
switch (arg) {
case LEFT:
this.args.put("-vf", "transpose=2");
break;
case RIGHT:
this.args.put("-vf", "transpose=1");
break;
}
}
/**
* Set how much time FFmpeg should analyze stream
* data to get stream information. Note that this
* affects directly the delay to start the stream.
*
* @param duration Rotate direction
*/
public void setAnalyzeDuration(String duration) {
this.analyzeDuration = duration;
}
/**
* Set video quality scale to a value (1-31).
* 1 is the highest quality and 31 the lowest.
* <p>
* <b> Note: Does NOT apply to h264 encoder. </b>
* </p>
*
* @param scale Scale value (1-31)
* @throws InvalidParameterException
*/
public void setVideoQualityScale(Integer scale) throws InvalidParameterException {
if(scale < 1 || scale > 31)
throw new InvalidParameterException("Scale must be a value in 1-31 range");
this.args.put("-q:v", scale.toString());
}
}

View File

@ -0,0 +1,105 @@
package org.bigbluebutton.app.video.ffmpeg;
import java.io.InputStream;
import org.slf4j.Logger;
import org.red5.logging.Red5LoggerFactory;
import java.io.IOException;
public class ProcessMonitor implements Runnable {
private static Logger log = Red5LoggerFactory.getLogger(ProcessMonitor.class, "video");
private String[] command;
private Process process;
ProcessStream inputStreamMonitor;
ProcessStream errorStreamMonitor;
private Thread thread = null;
public ProcessMonitor(String[] command) {
this.command = command;
this.process = null;
this.inputStreamMonitor = null;
this.errorStreamMonitor = null;
}
public String toString() {
if (this.command == null || this.command.length == 0) {
return "";
}
StringBuffer result = new StringBuffer();
String delim = "";
for (String i : this.command) {
result.append(delim).append(i);
delim = " ";
}
return result.toString();
}
public void run() {
try {
log.debug("Creating thread to execute FFmpeg");
log.debug("Executing: " + this.toString());
this.process = Runtime.getRuntime().exec(this.command);
if(this.process == null) {
log.debug("process is null");
return;
}
InputStream is = this.process.getInputStream();
InputStream es = this.process.getErrorStream();
inputStreamMonitor = new ProcessStream(is);
errorStreamMonitor = new ProcessStream(es);
inputStreamMonitor.start();
errorStreamMonitor.start();
this.process.waitFor();
int ret = this.process.exitValue();
log.debug("Exit value: " + ret);
destroy();
}
catch(SecurityException se) {
log.debug("Security Exception");
}
catch(IOException ioe) {
log.debug("IO Exception");
}
catch(NullPointerException npe) {
log.debug("NullPointer Exception");
}
catch(IllegalArgumentException iae) {
log.debug("IllegalArgument Exception");
}
catch(InterruptedException ie) {
log.debug("Interrupted Excetion");
}
log.debug("Exiting thread that executes FFmpeg");
}
public void start() {
this.thread = new Thread(this);
this.thread.start();
}
public void destroy() {
if(this.inputStreamMonitor != null
&& this.errorStreamMonitor != null) {
this.inputStreamMonitor.close();
this.errorStreamMonitor.close();
}
if(this.process != null) {
log.debug("Closing FFmpeg process");
this.process.destroy();
}
}
}

View File

@ -0,0 +1,59 @@
package org.bigbluebutton.app.video.ffmpeg;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.slf4j.Logger;
import org.red5.logging.Red5LoggerFactory;
import java.io.IOException;
public class ProcessStream implements Runnable {
private static Logger log = Red5LoggerFactory.getLogger(ProcessStream.class, "video");
private InputStream stream;
private Thread thread;
ProcessStream(InputStream stream) {
if(stream != null)
this.stream = stream;
}
public void run() {
try {
log.debug("Creating thread to execute the process stream");
String line;
InputStreamReader isr = new InputStreamReader(this.stream);
BufferedReader ibr = new BufferedReader(isr);
while ((line = ibr.readLine()) != null) {
log.debug(line);
}
close();
}
catch(IOException ioe) {
log.debug("IOException");
close();
}
log.debug("Exiting thread that handles process stream");
}
public void start() {
this.thread = new Thread(this);
this.thread.start();
}
public void close() {
try {
if(this.stream != null) {
log.debug("Closing process stream");
this.stream.close();
this.stream = null;
}
}
catch(IOException ioe) {
log.debug("IOException");
}
}
}

View File

@ -1,2 +1,5 @@
redis.host=127.0.0.1
redis.port=6379
# timeout (ms) to close the relay after the last listener disconnected
relayTimeout=60000

View File

@ -48,6 +48,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<bean id="web.handler" class="org.bigbluebutton.app.video.VideoApplication">
<property name="recordVideoStream" value="true"/>
<property name="eventRecordingService" ref="redisRecorder"/>
<property name="relayTimeout" value="${relayTimeout}"/>
</bean>
<bean id="redisRecorder" class="org.bigbluebutton.app.video.EventRecordingService">

File diff suppressed because it is too large Load Diff

View File

@ -127,6 +127,11 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
}
}
Boolean guest = false;
if (params.length >= 9 && ((Boolean) params[9])) {
guest = true;
}
if (record == true) {
recorderApplication.createRecordSession(room);
}
@ -134,7 +139,7 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter {
String userId = internalUserID;
String sessionId = CONN + userId;
BigBlueButtonSession bbbSession = new BigBlueButtonSession(room, internalUserID, username, role,
voiceBridge, record, externalUserID, muted, sessionId);
voiceBridge, record, externalUserID, muted, sessionId, guest);
connection.setAttribute(Constants.SESSION, bbbSession);
connection.setAttribute("INTERNAL_USER_ID", internalUserID);
connection.setAttribute("USER_SESSION_ID", sessionId);

View File

@ -29,10 +29,11 @@ public class BigBlueButtonSession {
private final String externalUserID;
private final Boolean startAsMuted;
private final String sessionId;
private final Boolean guest;
public BigBlueButtonSession(String room, String internalUserID, String username,
String role, String voiceBridge, Boolean record,
String externalUserID, Boolean startAsMuted, String sessionId){
String externalUserID, Boolean startAsMuted, String sessionId, Boolean guest){
this.internalUserID = internalUserID;
this.username = username;
this.role = role;
@ -42,6 +43,7 @@ public class BigBlueButtonSession {
this.externalUserID = externalUserID;
this.startAsMuted = startAsMuted;
this.sessionId = sessionId;
this.guest = guest;
}
public String getUsername() {
@ -79,4 +81,8 @@ public class BigBlueButtonSession {
public String getSessionId() {
return sessionId;
}
public Boolean isGuest() {
return guest;
}
}

View File

@ -46,8 +46,8 @@ public class MeetingMessageHandler implements MessageHandler {
emm.moderatorPass, emm.viewerPass, emm.createTime, emm.createDate);
} else if (msg instanceof RegisterUserMessage) {
RegisterUserMessage emm = (RegisterUserMessage) msg;
log.info("Received register user request. Meeting id [{}], userid=[{}], token=[{}]", emm.meetingID, emm.internalUserId, emm.authToken);
bbbGW.registerUser(emm.meetingID, emm.internalUserId, emm.fullname, emm.role, emm.externUserID, emm.authToken);
log.info("Received register user request. Meeting id [{}], userid=[{}], token=[{}], guest=[{}]", emm.meetingID, emm.internalUserId, emm.authToken, emm.guest);
bbbGW.registerUser(emm.meetingID, emm.internalUserId, emm.fullname, emm.role, emm.externUserID, emm.authToken, emm.guest);
} else if (msg instanceof DestroyMeetingMessage) {
DestroyMeetingMessage emm = (DestroyMeetingMessage) msg;
log.info("Received destroy meeting request. Meeting id [{}]", emm.meetingId);

View File

@ -93,4 +93,5 @@ public class Constants {
public static final String VIEWER_PASS = "viewer_pass";
public static final String CREATE_TIME = "create_time";
public static final String CREATE_DATE = "create_date";
public static final String GUEST = "guest";
}

View File

@ -51,6 +51,7 @@ public class MessagingConstants {
public static final String USER_LEFT_EVENT = "UserLeftEvent";
public static final String USER_LEFT_VOICE_REQUEST = "user_left_voice_request";
public static final String USER_STATUS_CHANGE_EVENT = "UserStatusChangeEvent";
public static final String USER_ROLE_CHANGE_EVENT = "UserRoleChangeEvent";
public static final String SEND_POLLS_EVENT = "SendPollsEvent";
public static final String RECORD_STATUS_EVENT = "RecordStatusEvent";
public static final String SEND_PUBLIC_CHAT_MESSAGE_REQUEST = "send_public_chat_message_request";

View File

@ -14,14 +14,16 @@ public class RegisterUserMessage implements IMessage {
public final String role;
public final String externUserID;
public final String authToken;
public final Boolean guest;
public RegisterUserMessage(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken) {
public RegisterUserMessage(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken, Boolean guest) {
this.meetingID = meetingID;
this.internalUserId = internalUserId;
this.fullname = fullname;
this.role = role;
this.externUserID = externUserID;
this.authToken = authToken;
this.guest = guest;
}
public String toJson() {
@ -33,6 +35,7 @@ public class RegisterUserMessage implements IMessage {
payload.put(Constants.ROLE, role);
payload.put(Constants.EXT_USER_ID, externUserID);
payload.put(Constants.AUTH_TOKEN, authToken);
payload.put(Constants.GUEST, guest.toString());
java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(REGISTER_USER, VERSION, null);
@ -53,16 +56,18 @@ public class RegisterUserMessage implements IMessage {
&& payload.has(Constants.NAME)
&& payload.has(Constants.ROLE)
&& payload.has(Constants.EXT_USER_ID)
&& payload.has(Constants.AUTH_TOKEN)) {
&& payload.has(Constants.AUTH_TOKEN)
&& payload.has(Constants.GUEST)) {
String meetingID = payload.get(Constants.MEETING_ID).getAsString();
String fullname = payload.get(Constants.NAME).getAsString();
String role = payload.get(Constants.ROLE).getAsString();
String externUserID = payload.get(Constants.EXT_USER_ID).getAsString();
String authToken = payload.get(Constants.AUTH_TOKEN).getAsString();
Boolean guest = payload.get(Constants.GUEST).getAsBoolean();
//use externalUserId twice - once for external, once for internal
return new RegisterUserMessage(meetingID, externUserID, fullname, role, externUserID, authToken);
return new RegisterUserMessage(meetingID, externUserID, fullname, role, externUserID, authToken, guest);
}
}
}

View File

@ -26,14 +26,6 @@ public class ParticipantsApplication {
private static Logger log = Red5LoggerFactory.getLogger( ParticipantsApplication.class, "bigbluebutton" );
private IBigBlueButtonInGW bbbInGW;
public void userRaiseHand(String meetingId, String userId) {
bbbInGW.userRaiseHand(meetingId, userId);
}
public void lowerHand(String meetingId, String userId, String loweredBy) {
bbbInGW.lowerHand(meetingId, userId, loweredBy);
}
public void ejectUserFromMeeting(String meetingId, String userId, String ejectedBy) {
bbbInGW.ejectUserFromMeeting(meetingId, userId, ejectedBy);
}
@ -50,8 +42,8 @@ public class ParticipantsApplication {
bbbInGW.setUserStatus(room, userid, status, value);
}
public boolean registerUser(String roomName, String userid, String username, String role, String externUserID) {
bbbInGW.registerUser(roomName, userid, username, role, externUserID, userid);
public boolean registerUser(String roomName, String userid, String username, String role, String externUserID, Boolean guest) {
bbbInGW.registerUser(roomName, userid, username, role, externUserID, userid, guest);
return true;
}
@ -74,4 +66,20 @@ public class ParticipantsApplication {
public void getRecordingStatus(String meetingId, String userId) {
bbbInGW.getRecordingStatus(meetingId, userId);
}
public void getGuestPolicy(String meetingId, String requesterId) {
bbbInGW.getGuestPolicy(meetingId, requesterId);
}
public void newGuestPolicy(String meetingId, String guestPolicy, String setBy) {
bbbInGW.setGuestPolicy(meetingId, guestPolicy, setBy);
}
public void responseToGuest(String meetingId, String userId, Boolean response, String requesterId) {
bbbInGW.responseToGuest(meetingId, userId, response, requesterId);
}
public void setParticipantRole(String meetingId, String userId, String role) {
bbbInGW.setUserRole(meetingId, userId, role);
}
}

View File

@ -34,9 +34,7 @@ public class ParticipantsListener implements MessageHandler{
String eventName = headerObject.get("name").toString().replace("\"", "");
if(eventName.equalsIgnoreCase("user_leaving_request") ||
eventName.equalsIgnoreCase("user_raised_hand_message") ||
eventName.equalsIgnoreCase("user_lowered_hand_message")){
if(eventName.equalsIgnoreCase("user_leaving_request")){
String roomName = payloadObject.get("meeting_id").toString().replace("\"", "");
String userID = payloadObject.get("userid").toString().replace("\"", "");
@ -49,13 +47,6 @@ public class ParticipantsListener implements MessageHandler{
String sessionId = "tobeimplemented";
bbbInGW.userLeft(roomName, userID, sessionId);
}
else if(eventName.equalsIgnoreCase("user_raised_hand_message")){
bbbInGW.userRaiseHand(roomName, userID);
}
else if(eventName.equalsIgnoreCase("user_lowered_hand_message")){
String requesterID = payloadObject.get("lowered_by").toString().replace("\"", "");
bbbInGW.lowerHand(roomName, userID, requesterID);
}
}
}
}

View File

@ -45,19 +45,6 @@ public class ParticipantsService {
application.getUsers(scope.getName(), getBbbSession().getInternalUserID(), sessionId);
}
public void userRaiseHand() {
IScope scope = Red5.getConnectionLocal().getScope();
String userId = getBbbSession().getInternalUserID();
application.userRaiseHand(scope.getName(), userId);
}
public void lowerHand(Map<String, String> msg) {
String userId = (String) msg.get("userId");
String loweredBy = (String) msg.get("loweredBy");
IScope scope = Red5.getConnectionLocal().getScope();
application.lowerHand(scope.getName(), userId, loweredBy);
}
public void ejectUserFromMeeting(Map<String, String> msg) {
String userId = (String) msg.get("userId");
String ejectedBy = (String) msg.get("ejectedBy");
@ -80,7 +67,15 @@ public class ParticipantsService {
public void setParticipantStatus(Map<String, Object> msg) {
String roomName = Red5.getConnectionLocal().getScope().getName();
application.setParticipantStatus(roomName, (String) msg.get("userID"), (String) msg.get("status"), (Object) msg.get("value"));
String userid = (String) msg.get("userID");
String status = (String) msg.get("status");
Object value = (Object) msg.get("value");
if (status.equals("mood")) {
value = ((String) value) + "," + System.currentTimeMillis();
}
log.debug("Setting participant status " + roomName + " " + userid + " " + status + " " + value);
application.setParticipantStatus(roomName, userid, status, value);
}
public void setParticipantsApplication(ParticipantsApplication a) {
@ -107,4 +102,29 @@ public class ParticipantsService {
return (BigBlueButtonSession) Red5.getConnectionLocal().getAttribute(Constants.SESSION);
}
public void getGuestPolicy() {
String requesterId = getBbbSession().getInternalUserID();
String roomName = Red5.getConnectionLocal().getScope().getName();
application.getGuestPolicy(roomName, requesterId);
}
public void setGuestPolicy(String guestPolicy) {
String requesterId = getBbbSession().getInternalUserID();
String roomName = Red5.getConnectionLocal().getScope().getName();
application.newGuestPolicy(roomName, guestPolicy, requesterId);
}
public void responseToGuest(Map<String, Object> msg) {
String requesterId = getBbbSession().getInternalUserID();
String roomName = Red5.getConnectionLocal().getScope().getName();
application.responseToGuest(roomName, (String) msg.get("userId"), (Boolean) msg.get("response"), requesterId);
}
public void setParticipantRole(Map<String, String> msg) {
String roomName = Red5.getConnectionLocal().getScope().getName();
String userId = (String) msg.get("userId");
String role = (String) msg.get("role");
log.debug("Setting participant role " + roomName + " " + userId + " " + role);
application.setParticipantRole(roomName, userId, role);
}
}

View File

@ -48,9 +48,9 @@ public class ConversionUpdatesProcessor {
public void sendConversionCompleted(String messageKey, String conference,
String code, String presId, Integer numberOfPages, String presName,
String presBaseUrl) {
String presBaseUrl, Boolean presDownloadable) {
presentationApplication.sendConversionCompleted(messageKey, conference,
code, presId, numberOfPages, presName, presBaseUrl);
code, presId, numberOfPages, presName, presBaseUrl, presDownloadable);
}
public void setPresentationApplication(PresentationApplication a) {

View File

@ -57,9 +57,9 @@ public class PresentationApplication {
public void sendConversionCompleted(String messageKey, String meetingId,
String code, String presentation, int numberOfPages,
String presName, String presBaseUrl) {
String presName, String presBaseUrl, Boolean presDownloadable) {
bbbInGW.sendConversionCompleted(messageKey, meetingId,
code, presentation, numberOfPages, presName, presBaseUrl);
code, presentation, numberOfPages, presName, presBaseUrl, presDownloadable);
}
public void removePresentation(String meetingID, String presentationID){

View File

@ -49,10 +49,10 @@ public class PresentationMessageListener implements MessageHandler {
private void sendConversionCompleted(String messageKey, String conference,
String code, String presId, Integer numberOfPages,
String filename, String presBaseUrl) {
String filename, String presBaseUrl, Boolean presDownloadable) {
conversionUpdatesProcessor.sendConversionCompleted(messageKey, conference,
code, presId, numberOfPages, filename, presBaseUrl);
code, presId, numberOfPages, filename, presBaseUrl, presDownloadable);
}
@ -96,8 +96,9 @@ public class PresentationMessageListener implements MessageHandler {
} else if(messageKey.equalsIgnoreCase(CONVERSION_COMPLETED_KEY)){
Integer numberOfPages = new Integer((String) map.get("numberOfPages"));
String presBaseUrl = (String) map.get("presentationBaseUrl");
Boolean presDownloadable = new Boolean((String) map.get("presDownloadable"));
sendConversionCompleted(messageKey, conference, code,
presId, numberOfPages, filename, presBaseUrl);
presId, numberOfPages, filename, presBaseUrl, presDownloadable);
}
}
}

View File

@ -0,0 +1,19 @@
package org.bigbluebutton.conference.service.recorder.participants;
public class GuestAskToEnterRecordEvent extends AbstractParticipantRecordEvent {
public GuestAskToEnterRecordEvent() {
super();
setEvent("GuestAskToEnterEvent");
}
public void setUserId(String userId) {
eventMap.put("userId", userId);
}
public void setName(String name) {
eventMap.put("name", name);
}
}

View File

@ -0,0 +1,14 @@
package org.bigbluebutton.conference.service.recorder.participants;
public class GuestPolicyEvent extends AbstractParticipantRecordEvent {
public GuestPolicyEvent() {
super();
setEvent("GuestPolicyEvent");
}
public void setPolicy(String guestPolicy) {
eventMap.put("guestPolicy", guestPolicy);
}
}

View File

@ -0,0 +1,19 @@
package org.bigbluebutton.conference.service.recorder.participants;
public class ModeratorResponseEvent extends AbstractParticipantRecordEvent {
public ModeratorResponseEvent() {
super();
setEvent("ModeratorResponseEvent");
}
public void setUserId(String userId) {
eventMap.put("userId", userId);
}
public void setResp(Boolean resp) {
eventMap.put("resp", resp.toString());
}
}

View File

@ -0,0 +1,35 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.conference.service.recorder.participants;
public class ParticipantRoleChangeRecordEvent extends AbstractParticipantRecordEvent {
public ParticipantRoleChangeRecordEvent() {
super();
setEvent("ParticipantRoleChangeEvent");
}
public void setUserId(String userId) {
eventMap.put("userId", userId);
}
public void setRole(String role) {
eventMap.put("role", role);
}
}

View File

@ -0,0 +1,17 @@
package org.bigbluebutton.conference.service.recorder.participants;
public class WaitingForModeratorEvent extends AbstractParticipantRecordEvent {
public WaitingForModeratorEvent() {
super();
setEvent("WaitingForModeratorEvent");
}
public void setUserId(String userId) {
eventMap.put("userId", userId);
}
public void setArg(String arg) {
eventMap.put("userId_userName", arg);
}
}

View File

@ -0,0 +1,54 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
* Author: Felipe Cecagno <felipe@mconf.org>
*/
package org.bigbluebutton.conference.service.sharednotes;
import org.bigbluebutton.core.api.IBigBlueButtonInGW;
import org.red5.logging.Red5LoggerFactory;
import org.slf4j.Logger;
public class SharedNotesApplication {
private static Logger log = Red5LoggerFactory.getLogger( SharedNotesApplication.class, "bigbluebutton" );
private IBigBlueButtonInGW bbbInGW;
public void setBigBlueButtonInGW(IBigBlueButtonInGW inGW) {
bbbInGW = inGW;
}
public void patchDocument(String meetingID, String requesterID, String noteID, String patch, Integer beginIndex, Integer endIndex) {
bbbInGW.patchDocument(meetingID, requesterID, noteID, patch, beginIndex, endIndex);
}
public void currentDocument(String meetingID, String requesterID) {
bbbInGW.getCurrentDocument(meetingID, requesterID);
}
public void createAdditionalNotes(String meetingID, String requesterID) {
bbbInGW.createAdditionalNotes(meetingID, requesterID);
}
public void destroyAdditionalNotes(String meetingID, String requesterID, String noteID) {
bbbInGW.destroyAdditionalNotes(meetingID, requesterID, noteID);
}
public void requestAdditionalNotesSet(String meetingID, String requesterID, int additionalNotesSetSize) {
bbbInGW.requestAdditionalNotesSet(meetingID, requesterID, additionalNotesSetSize);
}
}

View File

@ -0,0 +1,110 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.conference.service.sharednotes;
import org.red5.server.adapter.IApplication;
import org.red5.server.api.IClient;
import org.red5.server.api.IConnection;
import org.slf4j.Logger;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.scope.IScope;
import org.bigbluebutton.conference.service.recorder.RecorderApplication;
import org.bigbluebutton.conference.service.sharednotes.SharedNotesApplication;
public class SharedNotesHandler implements IApplication{
private static Logger log = Red5LoggerFactory.getLogger( SharedNotesHandler.class, "bigbluebutton" );
private RecorderApplication recorderApplication;
private SharedNotesApplication sharedNotesApplication;
private static final String APP = "SHARED NOTES";
@Override
public boolean appConnect(IConnection conn, Object[] params) {
log.debug("***** " + APP + " [ " + " appConnect *********");
return true;
}
@Override
public void appDisconnect(IConnection conn) {
log.debug("***** " + APP + " [ " + " appDisconnect *********");
}
@Override
public boolean appJoin(IClient client, IScope scope) {
log.debug("***** " + APP + " [ " + " appJoin [ " + scope.getName() + "] *********");
return true;
}
@Override
public void appLeave(IClient client, IScope scope) {
log.debug("***** " + APP + " [ " + " appLeave [ " + scope.getName() + "] *********");
}
@Override
public boolean appStart(IScope scope) {
log.debug("***** " + APP + " [ " + " appStart [ " + scope.getName() + "] *********");
return true;
}
@Override
public void appStop(IScope scope) {
log.debug("***** " + APP + " [ " + " appStop [ " + scope.getName() + "] *********");
}
@Override
public void roomDisconnect(IConnection connection) {
log.debug("***** " + APP + " [ " + " roomDisconnect [ " + connection.getScope().getName() + "] *********");
}
@Override
public boolean roomJoin(IClient client, IScope scope) {
log.debug("***** " + APP + " [ " + " roomJoin [ " + scope.getName() + "] *********");
return true;
}
@Override
public void roomLeave(IClient client, IScope scope) {
log.debug("***** " + APP + " [ " + " roomLeave [ " + scope.getName() + "] *********");
}
@Override
public boolean roomConnect(IConnection connection, Object[] params) {
log.debug("***** " + APP + " [ " + " roomConnect [ " + connection.getScope().getName() + "] *********");
return true;
}
@Override
public boolean roomStart(IScope scope) {
log.debug("***** " + APP + " [ " + " roomStart [ " + scope.getName() + "] *********");
return true;
}
@Override
public void roomStop(IScope scope) {
log.debug("***** " + APP + " [ " + " roomStop [ " + scope.getName() + "] *********");
}
public void setSharedNotesApplication(SharedNotesApplication a) {
log.debug("Setting shared notes application");
sharedNotesApplication = a;
}
}

View File

@ -0,0 +1,94 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
* Author: Hugo Lazzari <hslazzari@gmail.com>
*/
package org.bigbluebutton.conference.service.sharednotes;
import java.util.Map;
import org.bigbluebutton.conference.BigBlueButtonSession;
import org.bigbluebutton.conference.Constants;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.Red5;
import org.slf4j.Logger;
public class SharedNotesService {
private static Logger log = Red5LoggerFactory.getLogger( SharedNotesService.class, "bigbluebutton" );
private SharedNotesApplication application;
private BigBlueButtonSession getBbbSession() {
return (BigBlueButtonSession) Red5.getConnectionLocal().getAttribute(Constants.SESSION);
}
public void currentDocument() {
log.debug("SharedNotesService.currentDocument");
String meetingID = Red5.getConnectionLocal().getScope().getName();
String requesterID = getBbbSession().getInternalUserID();
application.currentDocument(meetingID, requesterID);
}
public void patchDocument(Map<String, Object> msg) {
log.debug("SharedNotesService.patchDocument");
String noteID = msg.get("noteID").toString();
String patch = msg.get("patch").toString();
Integer beginIndex = (Integer) msg.get("beginIndex");
Integer endIndex = (Integer) msg.get("endIndex");
String meetingID = Red5.getConnectionLocal().getScope().getName();
String requesterID = getBbbSession().getInternalUserID();
application.patchDocument(meetingID, requesterID, noteID, patch, beginIndex, endIndex);
}
public void createAdditionalNotes() {
log.debug("SharedNotesService.createAdditionalNotes");
String meetingID = Red5.getConnectionLocal().getScope().getName();
String requesterID = getBbbSession().getInternalUserID();
application.createAdditionalNotes(meetingID, requesterID);
}
public void destroyAdditionalNotes(Map<String, Object> msg) {
log.debug("SharedNotesService.destroyAdditionalNotes");
String noteID = msg.get("noteID").toString();
String meetingID = Red5.getConnectionLocal().getScope().getName();
String requesterID = getBbbSession().getInternalUserID();
application.destroyAdditionalNotes(meetingID, requesterID, noteID);
}
public void requestAdditionalNotesSet(Map<String, Object> msg) {
log.debug("SharedNotesService.requestAdditionalNotesSet");
Integer additionalNotesSetSize = (Integer) msg.get("additionalNotesSetSize");
String meetingID = Red5.getConnectionLocal().getScope().getName();
String requesterID = getBbbSession().getInternalUserID();
application.requestAdditionalNotesSet(meetingID, requesterID, additionalNotesSetSize);
}
public void setSharedNotesApplication(SharedNotesApplication a) {
log.debug("Setting sharedNotes application");
application = a;
}
}

View File

@ -0,0 +1,52 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.conference.service.video;
import org.bigbluebutton.conference.BigBlueButtonSession;
import org.bigbluebutton.conference.Constants;
import org.bigbluebutton.core.api.IBigBlueButtonInGW;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.Red5;
import org.slf4j.Logger;
public class VideoApplication {
private static Logger log = Red5LoggerFactory.getLogger(VideoService.class, "bigbluebutton");
private IBigBlueButtonInGW bbbInGW;
private String defaultStreampath;
public void getStreamPath(String streamName) {
String meetingId = getBbbSession().getRoom();
String userId = getBbbSession().getInternalUserID();
bbbInGW.getStreamPath(meetingId, userId, streamName, defaultStreampath);
}
private BigBlueButtonSession getBbbSession() {
return (BigBlueButtonSession) Red5.getConnectionLocal().getAttribute(Constants.SESSION);
}
public void setBigBlueButtonInGW(IBigBlueButtonInGW inGW) {
bbbInGW = inGW;
}
public void setDefaultStreamPath(String path) {
this.defaultStreampath = path;
}
}

View File

@ -0,0 +1,106 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.conference.service.video;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.adapter.ApplicationAdapter;
import org.red5.server.adapter.IApplication;
import org.red5.server.api.IClient;
import org.red5.server.api.IConnection;
import org.red5.server.api.scope.IScope;
import org.slf4j.Logger;
public class VideoHandler extends ApplicationAdapter implements IApplication {
private static Logger log = Red5LoggerFactory.getLogger(VideoService.class, "bigbluebutton");
private static final String APP = "VIDEO";
private VideoApplication videoApplication;
@Override
public boolean appConnect(IConnection conn, Object[] params) {
log.debug("***** " + APP + " [ " + " appConnect *********");
return true;
}
@Override
public void appDisconnect(IConnection conn) {
log.debug("***** " + APP + " [ " + " appDisconnect *********");
}
@Override
public boolean appJoin(IClient client, IScope scope) {
log.debug("***** " + APP + " [ " + " appJoin [ " + scope.getName() + "] *********");
return true;
}
@Override
public void appLeave(IClient client, IScope scope) {
log.debug("***** " + APP + " [ " + " appLeave [ " + scope.getName() + "] *********");
}
@Override
public boolean appStart(IScope scope) {
log.debug("***** " + APP + " [ " + " appStart [ " + scope.getName() + "] *********");
return true;
}
@Override
public void appStop(IScope scope) {
log.debug("***** " + APP + " [ " + " appStop [ " + scope.getName() + "] *********");
}
@Override
public void roomDisconnect(IConnection connection) {
log.debug("***** " + APP + " [ " + " roomDisconnect [ " + connection.getScope().getName() + "] *********");
}
@Override
public boolean roomJoin(IClient client, IScope scope) {
log.debug("***** " + APP + " [ " + " roomJoin [ " + scope.getName() + "] *********");
return true;
}
@Override
public void roomLeave(IClient client, IScope scope) {
log.debug("***** " + APP + " [ " + " roomLeave [ " + scope.getName() + "] *********");
}
@Override
public boolean roomConnect(IConnection connection, Object[] params) {
log.debug("***** " + APP + " [ " + " roomConnect [ " + connection.getScope().getName() + "] *********");
return true;
}
@Override
public boolean roomStart(IScope scope) {
log.debug("***** " + APP + " [ " + " roomStart [ " + scope.getName() + "] *********");
return true;
}
@Override
public void roomStop(IScope scope) {
log.debug("***** " + APP + " [ " + " roomStop [ " + scope.getName() + "] *********");
}
public void setVideoApplication(VideoApplication a) {
log.debug("****** Setting video application ********");
videoApplication = a;
}
}

View File

@ -0,0 +1,39 @@
/**
* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
*
* Copyright (c) 2014 BigBlueButton Inc. and by respective authors (see below).
*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 3.0 of the License, or (at your option) any later
* version.
*
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*
*/
package org.bigbluebutton.conference.service.video;
import org.slf4j.Logger;
import org.red5.logging.Red5LoggerFactory;
public class VideoService {
private static Logger log = Red5LoggerFactory.getLogger(VideoService.class, "bigbluebutton");
private VideoApplication videoApplication;
public void getStreamPath(String streamName) {
log.debug("Stream Path requested for [{}]", streamName);
videoApplication.getStreamPath(streamName);
}
public void setVideoApplication(VideoApplication a) {
log.debug("Setting video application");
this.videoApplication = a;
}
}

View File

@ -27,12 +27,11 @@ public interface IBigBlueButtonInGW {
// Users
void validateAuthToken(String meetingId, String userId, String token, String correlationId, String sessionId);
void registerUser(String roomName, String userid, String username, String role, String externUserID, String authToken);
void userRaiseHand(String meetingId, String userId);
void lowerHand(String meetingId, String userId, String loweredBy);
void registerUser(String roomName, String userid, String username, String role, String externUserID, String authToken, Boolean guest);
void shareWebcam(String meetingId, String userId, String stream);
void unshareWebcam(String meetingId, String userId, String stream);
void setUserStatus(String meetingID, String userID, String status, Object value);
void setUserRole(String meetingID, String userID, String role);
void getUsers(String meetingID, String requesterID);
void userLeft(String meetingID, String userID, String sessionId);
void userJoin(String meetingID, String userID, String authToken);
@ -42,6 +41,9 @@ public interface IBigBlueButtonInGW {
void getRecordingStatus(String meetingId, String userId);
void userConnectedToGlobalAudio(String voiceConf, String userid, String name);
void userDisconnectedFromGlobalAudio(String voiceConf, String userid, String name);
void getGuestPolicy(String meetingID, String userID);
void setGuestPolicy(String meetingID, String guestPolicy, String setBy);
void responseToGuest(String meetingID, String userID, Boolean response, String requesterID);
// Voice
void initAudioSettings(String meetingID, String requesterID, Boolean muted);
@ -84,7 +86,7 @@ public interface IBigBlueButtonInGW {
int pagesCompleted, String presName);
void sendConversionCompleted(String messageKey, String meetingId,
String code, String presId, int numPages, String presName, String presBaseUrl);
String code, String presId, int numPages, String presName, String presBaseUrl, boolean presDownloadable);
// Polling
void getPolls(String meetingID, String requesterID);
@ -116,4 +118,17 @@ public interface IBigBlueButtonInGW {
void enableWhiteboard(String meetingID, String requesterID, Boolean enable);
void isWhiteboardEnabled(String meetingID, String requesterID, String replyTo);
// Shared notes
void patchDocument(String meetingID, String requesterID, String noteID,
String patch, int beginIndex, int endIndex);
void getCurrentDocument(String meetingID, String requesterID);
void createAdditionalNotes(String meetingID, String requesterID);
void destroyAdditionalNotes(String meetingID, String requesterID,
String noteID);
void requestAdditionalNotesSet(String meetingID, String requesterID,
int additionalNotesSetSize);
// Video
void getStreamPath(String meetingID, String requesterID, String streamName, String defaultPath);
}

View File

@ -7,6 +7,7 @@ import org.bigbluebutton.core.apps.poll.PollInGateway
import org.bigbluebutton.core.apps.layout.LayoutInGateway
import org.bigbluebutton.core.apps.chat.ChatInGateway
import scala.collection.JavaConversions._
import org.bigbluebutton.core.apps.sharednotes.SharedNotesInGateway
import org.bigbluebutton.core.apps.whiteboard.WhiteboardInGateway
import org.bigbluebutton.core.apps.voice.VoiceInGateway
import java.util.ArrayList
@ -68,9 +69,9 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen
bbbGW.accept(new ValidateAuthToken(meetingId, userId, token, correlationId, sessionId))
}
def registerUser(meetingID: String, userID: String, name: String, role: String, extUserID: String, authToken: String):Unit = {
def registerUser(meetingID: String, userID: String, name: String, role: String, extUserID: String, authToken: String, guest: java.lang.Boolean):Unit = {
val userRole = if (role == "MODERATOR") Role.MODERATOR else Role.VIEWER
bbbGW.accept(new RegisterUser(meetingID, userID, name, userRole, extUserID, authToken))
bbbGW.accept(new RegisterUser(meetingID, userID, name, userRole, extUserID, authToken, guest))
}
def sendLockSettings(meetingID: String, userId: String, settings: java.util.Map[String, java.lang.Boolean]) {
@ -170,6 +171,11 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen
bbbGW.accept(new ChangeUserStatus(meetingID, userID, status, value));
}
def setUserRole(meetingID: String, userID: String, role: String) {
val userRole = if (role == "MODERATOR") Role.MODERATOR else Role.VIEWER
bbbGW.accept(new ChangeUserRole(meetingID, userID, userRole));
}
def getUsers(meetingID: String, requesterID: String):Unit = {
bbbGW.accept(new GetUsers(meetingID, requesterID))
}
@ -202,6 +208,26 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen
bbbGW.accept(new UserDisconnectedFromGlobalAudio(voiceConf, voiceConf, userid, name))
}
// Guest support
def getGuestPolicy(meetingID: String, requesterID: String) {
bbbGW.accept(new GetGuestPolicy(meetingID, requesterID))
}
def setGuestPolicy(meetingID: String, guestPolicy: String, setBy: String) {
val policy = guestPolicy.toUpperCase() match {
case "ALWAYS_ACCEPT" => GuestPolicy.ALWAYS_ACCEPT
case "ALWAYS_DENY" => GuestPolicy.ALWAYS_DENY
case "ASK_MODERATOR" => GuestPolicy.ASK_MODERATOR
//default
case undef => GuestPolicy.ASK_MODERATOR
}
bbbGW.accept(new SetGuestPolicy(meetingID, policy, setBy))
}
def responseToGuest(meetingID: String, userId: String, response: java.lang.Boolean, requesterID: String) {
bbbGW.accept(new RespondToGuest(meetingID, userId, response, requesterID))
}
/**************************************************************************************
* Message Interface for Presentation
**************************************************************************************/
@ -254,11 +280,11 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen
def sendConversionCompleted(messageKey: String, meetingId: String,
code: String, presentationId: String, numPages: Int,
presName: String, presBaseUrl: String) {
presName: String, presBaseUrl: String, presDownloadable: Boolean) {
// println("******************** PRESENTATION CONVERSION COMPLETED MESSAGE ***************************** ")
val pages = generatePresentationPages(presentationId, numPages, presBaseUrl)
val presentation = new Presentation(id=presentationId, name=presName, pages=pages)
val presentation = new Presentation(id=presentationId, name=presName, pages=pages, downloadable=presDownloadable)
bbbGW.accept(new PresentationConversionCompleted(meetingId, messageKey,
code, presentation))
@ -465,4 +491,32 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen
voiceGW.voiceRecording(meetingId, recordingFile,
timestamp, recording)
}
val sharedNotesGW = new SharedNotesInGateway(bbbGW)
def patchDocument(meetingId: String, userId: String, noteId: String,
patch: String, beginIndex: Int, endIndex: Int) {
sharedNotesGW.patchDocument(meetingId, userId, noteId, patch, beginIndex, endIndex)
}
def getCurrentDocument(meetingId: String, userId: String) {
sharedNotesGW.getCurrentDocument(meetingId, userId)
}
def createAdditionalNotes(meetingId: String, userId: String) {
sharedNotesGW.createAdditionalNotes(meetingId, userId)
}
def destroyAdditionalNotes(meetingId: String, userId: String, noteId: String) {
sharedNotesGW.destroyAdditionalNotes(meetingId, userId, noteId)
}
def requestAdditionalNotesSet(meetingId: String, userId: String, additionalNotesSetSize: Int) {
sharedNotesGW.requestAdditionalNotesSet(meetingId, userId, additionalNotesSetSize)
}
/*********************************************************************
* Message Interface for Video
*******************************************************************/
def getStreamPath(meetingId:String, requesterId:String, streamName: String, defaultPath:String) {
bbbGW.accept(new GetStreamPath(meetingId, requesterId, streamName, defaultPath));
}
}

View File

@ -39,11 +39,10 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
case msg: UserJoining => handleUserJoining(msg)
case msg: UserLeaving => handleUserLeaving(msg)
case msg: GetUsers => handleGetUsers(msg)
case msg: UserRaiseHand => handleUserRaiseHand(msg)
case msg: UserLowerHand => handleUserLowerHand(msg)
case msg: UserShareWebcam => handleUserShareWebcam(msg)
case msg: UserUnshareWebcam => handleUserUnshareWebcam(msg)
case msg: ChangeUserStatus => handleChangeUserStatus(msg)
case msg: ChangeUserRole => handleChangeUserRole(msg)
case msg: AssignPresenter => handleAssignPresenter(msg)
case msg: SetRecordingStatus => handleSetRecordingStatus(msg)
case msg: GetChatHistoryRequest => handleGetChatHistoryRequest(msg)
@ -97,6 +96,10 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
case msg: UndoWhiteboardRequest => handleUndoWhiteboardRequest(msg)
case msg: EnableWhiteboardRequest => handleEnableWhiteboardRequest(msg)
case msg: IsWhiteboardEnabledRequest => handleIsWhiteboardEnabledRequest(msg)
case msg: GetStreamPath => handleGetStreamPath(msg)
case msg: GetGuestPolicy => handleGetGuestPolicy(msg)
case msg: SetGuestPolicy => handleSetGuestPolicy(msg)
case msg: RespondToGuest => handleRespondToGuest(msg)
case msg: GetAllMeetingsRequest => handleGetAllMeetingsRequest(msg)
//OUT MESSAGES
@ -121,11 +124,11 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
case msg: GetUsersReply => handleGetUsersReply(msg)
case msg: ValidateAuthTokenReply => handleValidateAuthTokenReply(msg)
case msg: UserJoined => handleUserJoined(msg)
case msg: UserRaisedHand => handleUserRaisedHand(msg)
case msg: UserLoweredHand => handleUserLoweredHand(msg)
case msg: UserListeningOnly => handleUserListeningOnly(msg)
case msg: UserSharedWebcam => handleUserSharedWebcam(msg)
case msg: UserUnsharedWebcam => handleUserUnsharedWebcam(msg)
case msg: UserStatusChange => handleUserStatusChange(msg)
case msg: UserRoleChange => handleUserRoleChange(msg)
case msg: MuteVoiceUser => handleMuteVoiceUser(msg)
case msg: UserVoiceMuted => handleUserVoiceMuted(msg)
case msg: UserVoiceTalking => handleUserVoiceTalking(msg)
@ -175,6 +178,9 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
case msg: UndoWhiteboardEvent => handleUndoWhiteboardEvent(msg)
case msg: WhiteboardEnabledEvent => handleWhiteboardEnabledEvent(msg)
case msg: IsWhiteboardEnabledReply => handleIsWhiteboardEnabledReply(msg)
case msg: GetGuestPolicyReply => handleGetGuestPolicyReply(msg)
case msg: GuestPolicyChanged => handleGuestPolicyChanged(msg)
case msg: GuestAccessDenied => handleGuestAccessDenied(msg)
case msg: GetAllMeetingsReply => handleGetAllMeetingsReply(msg)
case _ => // do nothing
@ -199,12 +205,14 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
wuser.put(Constants.EXT_USER_ID, user.externUserID)
wuser.put(Constants.NAME, user.name)
wuser.put(Constants.ROLE, user.role.toString())
wuser.put(Constants.RAISE_HAND, user.raiseHand:java.lang.Boolean)
wuser.put(Constants.MOOD, user.mood:java.lang.String)
wuser.put(Constants.PRESENTER, user.presenter:java.lang.Boolean)
wuser.put(Constants.HAS_STREAM, user.hasStream:java.lang.Boolean)
wuser.put(Constants.LOCKED, user.locked:java.lang.Boolean)
wuser.put("webcamStream", user.webcamStreams mkString("|"))
wuser.put(Constants.WEBCAM_STREAM, user.webcamStreams)
wuser.put(Constants.PHONE_USER, user.phoneUser:java.lang.Boolean)
wuser.put(Constants.GUEST, user.guest:java.lang.Boolean)
wuser.put(Constants.WAITING_FOR_ACCEPTANCE, user.waitingForAcceptance:java.lang.Boolean)
wuser.put(Constants.VOICE_USER, vuser)
wuser
@ -412,6 +420,7 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
payload.put(Constants.NAME, msg.name)
payload.put(Constants.ROLE, msg.role.toString())
payload.put(Constants.EXT_USER_ID, msg.extUserID)
payload.put(Constants.GUEST, msg.guest.toString())
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.REGISTER_USER)
@ -470,35 +479,6 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
dispatcher.dispatch(buildJson(header, payload))
}
private def handleUserRaiseHand(msg: UserRaiseHand) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.USER_ID, msg.userId)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.RAISE_HAND)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING USER RAISE HAND *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleUserLowerHand(msg: UserLowerHand) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.USER_ID, msg.userId)
payload.put(Constants.LOWERED_BY, msg.loweredBy)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.LOWER_HAND)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING USER LOWER HAND *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleUserShareWebcam(msg: UserShareWebcam) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
@ -544,6 +524,21 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
dispatcher.dispatch(buildJson(header, payload))
}
private def handleChangeUserRole(msg: ChangeUserRole) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.USER_ID, msg.userID)
payload.put(Constants.ROLE, msg.role)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.CHANGE_USER_ROLE)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING CHANGE USER ROLE *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleAssignPresenter(msg: AssignPresenter) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
@ -1586,34 +1581,18 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
dispatcher.dispatch(json)
}
private def handleUserRaisedHand(msg: UserRaisedHand) {
private def handleUserListeningOnly(msg: UserListeningOnly) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.RAISE_HAND, msg.recorded)
payload.put(Constants.USER_ID, msg.userID)
payload.put(Constants.LISTEN_ONLY, msg.listenOnly)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.USER_RAISED_HAND)
header.put(Constants.NAME, MessageNames.USER_LISTEN_ONLY)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING USER RAISED HAND *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleUserLoweredHand(msg: UserLoweredHand) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.RAISE_HAND, msg.recorded)
payload.put(Constants.USER_ID, msg.userID)
payload.put(Constants.LOWERED_BY, msg.loweredBy)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.USER_LOWERED_HAND)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING USER LOWERED HAND *****************")
// println("***** DISPATCHING USER LISTENING ONLY *****************")
dispatcher.dispatch(buildJson(header, payload))
}
@ -1666,6 +1645,22 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
dispatcher.dispatch(buildJson(header, payload))
}
private def handleUserRoleChange(msg: UserRoleChange) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.RECORDED, msg.recorded)
payload.put(Constants.USER_ID, msg.userID)
payload.put(Constants.ROLE, msg.role)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.USER_ROLE_CHANGED)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING USER ROLE CHANGE *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleMuteVoiceUser(msg: MuteVoiceUser) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
@ -2159,6 +2154,109 @@ class CollectorActor(dispatcher: IDispatcher) extends Actor {
val json = WhiteboardMessageToJsonConverter.isWhiteboardEnabledReplyToJson(msg)
dispatcher.dispatch(json)
}
private def handleGetStreamPath(msg: GetStreamPath) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.REQUESTER_ID, msg.requesterID)
payload.put(Constants.STREAM, msg.streamName)
payload.put(Constants.STREAM_PATH_DEFAULT, msg.streamName)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.GET_STREAM_PATH)
println("***** DISPATCHING GET STREAM PATH *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleGetGuestPolicy(msg: GetGuestPolicy) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.REQUESTER_ID, msg.requesterID)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.GET_GUEST_POLICY)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING GET GUEST POLICY *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleSetGuestPolicy(msg: SetGuestPolicy) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.GUEST_POLICY, msg.policy.toString())
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.SET_GUEST_POLICY)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING SET GUEST POLICY *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleRespondToGuest(msg: RespondToGuest) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.USER_ID, msg.userId)
payload.put(Constants.RESPONSE, msg.response.toString())
payload.put(Constants.REQUESTER_ID, msg.requesterID)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.RESPOND_TO_GUEST)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING RESPOND TO GUEST *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleGetGuestPolicyReply(msg: GetGuestPolicyReply) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.REQUESTER_ID, msg.requesterID)
payload.put(Constants.GUEST_POLICY, msg.policy)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.GET_GUEST_POLICY_REPLY)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING GET GUEST POLICY REPLY *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleGuestPolicyChanged(msg: GuestPolicyChanged) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.GUEST_POLICY, msg.policy)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.GUEST_POLICY_CHANGED)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING GUEST POLICY CHANGED *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleGuestAccessDenied(msg: GuestAccessDenied) {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.USER_ID, msg.userId)
val header = new java.util.HashMap[String, Any]()
header.put(Constants.NAME, MessageNames.GUEST_ACCESS_DENIED)
header.put(Constants.TIMESTAMP, TimestampGenerator.generateTimestamp)
header.put(Constants.CURRENT_TIME, TimestampGenerator.getCurrentTime)
// println("***** DISPATCHING RESPONSE TO GUEST *****************")
dispatcher.dispatch(buildJson(header, payload))
}
private def handleGetAllMeetingsReply(msg: GetAllMeetingsReply) {
val json = MeetingMessageToJsonConverter.getAllMeetingsReplyToJson(msg)
println("***** DISPATCHING GET ALL MEETINGS REPLY OUTMSG *****************")

View File

@ -4,12 +4,14 @@ import scala.actors.Actor
import scala.actors.Actor._
import org.bigbluebutton.core.apps.poll.Poll
import org.bigbluebutton.core.apps.poll.PollApp
import org.bigbluebutton.core.apps.sharednotes.SharedNotesApp
import org.bigbluebutton.core.apps.users.UsersApp
import org.bigbluebutton.core.api._
import org.bigbluebutton.core.apps.presentation.PresentationApp
import org.bigbluebutton.core.apps.layout.LayoutApp
import org.bigbluebutton.core.apps.chat.ChatApp
import org.bigbluebutton.core.apps.whiteboard.WhiteboardApp
import org.bigbluebutton.core.apps.video.VideoApp
import scala.actors.TIMEOUT
import java.util.concurrent.TimeUnit
import org.bigbluebutton.core.util._
@ -24,7 +26,7 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
val outGW: MessageOutGateway)
extends Actor with UsersApp with PresentationApp
with PollApp with LayoutApp with ChatApp
with WhiteboardApp with LogHelper {
with WhiteboardApp with LogHelper with SharedNotesApp with VideoApp {
var audioSettingsInited = false
var permissionsInited = false
@ -33,6 +35,9 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
var muted = false;
var meetingEnded = false
var guestPolicy = GuestPolicy.ASK_MODERATOR
var guestPolicySetBy:String = null
def getDuration():Long = {
duration
}
@ -78,9 +83,8 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
case msg: AssignPresenter => handleAssignPresenter(msg)
case msg: GetUsers => handleGetUsers(msg)
case msg: ChangeUserStatus => handleChangeUserStatus(msg)
case msg: ChangeUserRole => handleChangeUserRole(msg)
case msg: EjectUserFromMeeting => handleEjectUserFromMeeting(msg)
case msg: UserRaiseHand => handleUserRaiseHand(msg)
case msg: UserLowerHand => handleUserLowerHand(msg)
case msg: UserShareWebcam => handleUserShareWebcam(msg)
case msg: UserUnshareWebcam => handleUserunshareWebcam(msg)
case msg: MuteMeetingRequest => handleMuteMeetingRequest(msg)
@ -136,6 +140,16 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
case msg: SetRecordingStatus => handleSetRecordingStatus(msg)
case msg: GetRecordingStatus => handleGetRecordingStatus(msg)
case msg: VoiceRecording => handleVoiceRecording(msg)
case msg: GetStreamPath => handleGetStreamPath(msg)
case msg: GetGuestPolicy => handleGetGuestPolicy(msg)
case msg: SetGuestPolicy => handleSetGuestPolicy(msg)
case msg: RespondToGuest => handleRespondToGuest(msg)
case msg: PatchDocumentRequest => handlePatchDocumentRequest(msg)
case msg: GetCurrentDocumentRequest => handleGetCurrentDocumentRequest(msg)
case msg: CreateAdditionalNotesRequest => handleCreateAdditionalNotesRequest(msg)
case msg: DestroyAdditionalNotesRequest => handleDestroyAdditionalNotesRequest(msg)
case msg: RequestAdditionalNotesSetRequest => handleRequestAdditionalNotesSetRequest(msg)
case msg: EndMeeting => handleEndMeeting(msg)
case StopMeetingActor => exit
@ -248,6 +262,16 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee
outGW.send(new GetRecordingStatusReply(meetingID, recorded, msg.userId, recording.booleanValue()))
}
private def handleGetGuestPolicy(msg: GetGuestPolicy) {
outGW.send(new GetGuestPolicyReply(msg.meetingID, recorded, msg.requesterID, guestPolicy.toString()))
}
private def handleSetGuestPolicy(msg: SetGuestPolicy) {
guestPolicy = msg.policy
guestPolicySetBy = msg.setBy
outGW.send(new GuestPolicyChanged(msg.meetingID, recorded, guestPolicy.toString()))
}
def lockLayout(lock: Boolean) {
permissions = permissions.copy(lockedLayout=lock)
}

View File

@ -37,6 +37,7 @@ object Constants {
val FORCE = "force"
val RESPONSE = "response"
val PRESENTATION_ID = "presentation_id"
val DOWNLOADABLE = "downloadable"
val X_OFFSET = "x_offset"
val Y_OFFSET = "y_offset"
val WIDTH_RATIO = "width_ratio"
@ -63,7 +64,7 @@ object Constants {
val ENABLE = "enable"
val PRESENTER = "presenter"
val USERS = "users"
val RAISE_HAND = "raise_hand"
val MOOD = "mood"
val HAS_STREAM = "has_stream"
val WEBCAM_STREAM = "webcam_stream"
val PHONE_USER = "phone_user"
@ -93,4 +94,10 @@ object Constants {
val VIEWER_PASS = "viewer_pass"
val CREATE_TIME = "create_time"
val CREATE_DATE = "create_date"
val STREAM_PATH = "stream_path"
val STREAM_PATH_DEFAULT = "stream_path_default"
val GUEST = "guest"
val WAITING_FOR_ACCEPTANCE = "waiting_for_acceptance"
val GUEST_POLICY = "guest_policy"
val GUESTS_WAITING = "guests_waiting"
}

View File

@ -1,6 +1,7 @@
package org.bigbluebutton.core.api
import org.bigbluebutton.core.api.Role._
import org.bigbluebutton.core.api.GuestPolicy._
import org.bigbluebutton.core.apps.poll._
import org.bigbluebutton.core.apps.whiteboard.vo.AnnotationVO
import org.bigbluebutton.core.apps.presentation.Presentation
@ -85,7 +86,8 @@ case class RegisterUser(
name: String,
role: Role,
extUserID: String,
authToken: String
authToken: String,
guest: Boolean
) extends InMessage
case class UserJoining(
@ -137,6 +139,12 @@ case class ChangeUserStatus(
value: Object
) extends InMessage
case class ChangeUserRole(
meetingID: String,
userID: String,
role: Role
) extends InMessage
case class AssignPresenter(
meetingID: String,
newPresenterID: String,
@ -188,6 +196,25 @@ case class UserDisconnectedFromGlobalAudio(
name: String
) extends InMessage
// Guest support
case class GetGuestPolicy(
meetingID: String,
requesterID: String
) extends InMessage
case class SetGuestPolicy(
meetingID: String,
policy: GuestPolicy,
setBy: String
) extends InMessage
case class RespondToGuest(
meetingID: String,
userId: String,
response: Boolean,
requesterID: String
) extends InMessage
// Layout
case class GetCurrentLayoutRequest(
meetingID: String,
@ -512,3 +539,43 @@ case class IsWhiteboardEnabledRequest(
case class GetAllMeetingsRequest(
meetingID: String /** Not used. Just to satisfy trait **/
) extends InMessage
// Shared notes
case class PatchDocumentRequest(
meetingID: String,
requesterID: String,
noteID: String,
patch: String,
beginIndex: Int,
endIndex: Int
) extends InMessage
case class GetCurrentDocumentRequest(
meetingID: String,
requesterID: String
) extends InMessage
case class CreateAdditionalNotesRequest(
meetingID: String,
requesterID: String
) extends InMessage
case class DestroyAdditionalNotesRequest(
meetingID: String,
requesterID: String,
noteID: String
) extends InMessage
case class RequestAdditionalNotesSetRequest(
meetingID: String,
requesterID: String,
additionalNotesSetSize: Int
) extends InMessage
// Video
case class GetStreamPath(
meetingID: String,
requesterID: String,
streamName: String,
defaultPath: String
) extends InMessage

View File

@ -24,6 +24,7 @@ object MessageNames {
val USER_SHARE_WEBCAM = "user_share_webcam_request"
val USER_UNSHARE_WEBCAM = "user_unshare_webcam_request"
val CHANGE_USER_STATUS = "change_user_status_request"
val CHANGE_USER_ROLE = "change_user_role_request"
val ASSIGN_PRESENTER = "assign_presenter_request"
val SET_RECORDING_STATUS = "set_recording_status_request"
val GET_CHAT_HISTORY = "get_chat_history_request"
@ -79,6 +80,10 @@ object MessageNames {
val UNDO_WHITEBOARD = "undo_whiteboard_request"
val ENABLE_WHITEBOARD = "enable_whiteboard_request"
val IS_WHITEBOARD_ENABLED = "is_whiteboard_enabled_request"
val GET_STREAM_PATH = "get_stream_path_request"
var GET_GUEST_POLICY = "get_guest_policy"
val SET_GUEST_POLICY = "set_guest_policy"
val RESPOND_TO_GUEST = "respond_to_guest"
val GET_ALL_MEETINGS_REQUEST = "get_all_meetings_request"
// OUT MESSAGES
@ -109,6 +114,7 @@ object MessageNames {
val USER_SHARED_WEBCAM = "user_shared_webcam_message"
val USER_UNSHARED_WEBCAM = "user_unshared_webcam_message"
val USER_STATUS_CHANGED = "user_status_changed_message"
val USER_ROLE_CHANGED = "user_role_changed_message"
val MUTE_VOICE_USER = "mute_voice_user_request"
val USER_VOICE_MUTED = "user_voice_muted_message"
val USER_VOICE_TALKING = "user_voice_talking_message"
@ -160,5 +166,9 @@ object MessageNames {
val MEETING_DESTROYED_EVENT = "meeting_destroyed_event"
val KEEP_ALIVE_REPLY = "keep_alive_reply"
val USER_LISTEN_ONLY = "user_listening_only"
val GET_STREAM_PATH_REPLY = "get_stream_path_reply"
var GET_GUEST_POLICY_REPLY = "get_guest_policy_reply"
val GUEST_POLICY_CHANGED = "guest_policy_changed"
val GUEST_ACCESS_DENIED = "guest_access_denied"
val GET_ALL_MEETINGS_REPLY = "get_all_meetings_reply"
}

View File

@ -255,6 +255,14 @@ case class UserStatusChange(
version:String = Versions.V_0_0_1
) extends IOutMessage
case class UserRoleChange(
meetingID: String,
recorded: Boolean,
userID: String,
role: String,
version:String = Versions.V_0_0_1
) extends IOutMessage
case class MuteVoiceUser(
meetingID: String,
recorded: Boolean,
@ -648,15 +656,76 @@ case class IsWhiteboardEnabledReply(
version:String = Versions.V_0_0_1
) extends IOutMessage
case class GetGuestPolicyReply(
meetingID: String,
recorded: Boolean,
requesterID: String,
policy: String
) extends IOutMessage
case class GuestPolicyChanged(
meetingID: String,
recorded: Boolean,
policy: String
) extends IOutMessage
case class GuestAccessDenied(
meetingID: String,
recorded: Boolean,
userId: String
) extends IOutMessage
case class GetAllMeetingsReply(
meetings: Array[MeetingInfo],
version:String = Versions.V_0_0_1
) extends IOutMessage
case class PatchDocumentReply(
meetingID: String,
recorded: Boolean,
requesterID: String,
noteID: String,
patch: String,
beginIndex: Int,
endIndex: Int,
version:String = Versions.V_0_0_1
) extends IOutMessage
case class GetCurrentDocumentReply(
meetingID: String,
recorded: Boolean,
requesterID: String,
notes: Map[String, String],
version:String = Versions.V_0_0_1
) extends IOutMessage
case class CreateAdditionalNotesReply(
meetingID: String,
recorded: Boolean,
requesterID: String,
noteID: String,
version:String = Versions.V_0_0_1
) extends IOutMessage
case class DestroyAdditionalNotesReply(
meetingID: String,
recorded: Boolean,
requesterID: String,
noteID: String,
version:String = Versions.V_0_0_1
) extends IOutMessage
// Value Objects
case class MeetingVO(
id: String,
recorded: Boolean
)
// Video
case class GetStreamPathReply(
meetingID: String,
requesterID: String,
streamName: String,
streamPath: String
) extends IOutMessage

View File

@ -8,6 +8,13 @@ object Role extends Enumeration {
val VIEWER = Value("VIEWER")
}
object GuestPolicy extends Enumeration {
type GuestPolicy = Value
val ALWAYS_ACCEPT = Value("ALWAYS_ACCEPT")
val ALWAYS_DENY = Value("ALWAYS_DENY")
val ASK_MODERATOR = Value("ASK_MODERATOR")
}
case class Presenter(
presenterID: String,
presenterName: String,
@ -48,7 +55,8 @@ case class RegisteredUser (
externId: String,
name: String,
role: Role.Role,
authToken: String
authToken: String,
guest: Boolean
)
case class Voice(
@ -67,7 +75,9 @@ case class UserVO(
externUserID: String,
name: String,
role: Role.Role,
raiseHand: Boolean,
guest: Boolean,
waitingForAcceptance: Boolean,
mood: String,
presenter: Boolean,
hasStream: Boolean,
locked: Boolean,
@ -94,7 +104,8 @@ case class MeetingConfig(name: String,
record: Boolean=false,
duration: MeetingDuration,
defaultAvatarURL: String,
defaultConfigToken: String)
defaultConfigToken: String,
guestPolicy: GuestPolicy.GuestPolicy=GuestPolicy.ASK_MODERATOR)
case class MeetingName(name: String)

View File

@ -12,7 +12,8 @@ trait LayoutApp {
private var setByUser:String = "system";
private var currentLayout = "";
private var layoutLocked = false
private var viewersOnly = true
// this is not being set by the client, and we need to apply the layouts to all users, not just viewers, so will keep the default value of this as false
private var viewersOnly = false
def handleGetCurrentLayoutRequest(msg: GetCurrentLayoutRequest) {
outGW.send(new GetCurrentLayoutReply(msg.meetingID, recorded, msg.requesterID,

View File

@ -1,7 +1,8 @@
package org.bigbluebutton.core.apps.presentation
case class Presentation(id: String, name: String, current: Boolean = false,
pages: scala.collection.immutable.HashMap[String, Page])
pages: scala.collection.immutable.HashMap[String, Page],
downloadable: Boolean)
case class Page(id: String, num: Int,
thumbUri: String = "",

View File

@ -113,6 +113,7 @@ class PresentationClientMessageSender(service: ConnectionInvokerService) extends
presentation.put("id", msg.presentation.id)
presentation.put("name", msg.presentation.name)
presentation.put("current", msg.presentation.current:java.lang.Boolean)
presentation.put("downloadable", msg.presentation.downloadable:java.lang.Boolean)
val pages = new ArrayList[Page]()
@ -169,6 +170,7 @@ class PresentationClientMessageSender(service: ConnectionInvokerService) extends
presentation.put("id", pres.id)
presentation.put("name", pres.name)
presentation.put("current", pres.current:java.lang.Boolean)
presentation.put("downloadable", pres.downloadable:java.lang.Boolean)
// Get the pages for a presentation
val pages = new ArrayList[Page]()
@ -259,6 +261,7 @@ class PresentationClientMessageSender(service: ConnectionInvokerService) extends
presentation.put("id", msg.presentation.id)
presentation.put("name", msg.presentation.name)
presentation.put("current", msg.presentation.current:java.lang.Boolean)
presentation.put("downloadable", msg.presentation.downloadable:java.lang.Boolean)
// Get the pages for a presentation
val pages = new ArrayList[Page]()

View File

@ -64,6 +64,7 @@ object PesentationMessageToJsonConverter {
presentation.put(Constants.ID, pres.id)
presentation.put(Constants.NAME, pres.name)
presentation.put(Constants.CURRENT, pres.current:java.lang.Boolean)
presentation.put(Constants.DOWNLOADABLE, pres.downloadable:java.lang.Boolean)
// Get the pages for a presentation
val pages = new java.util.ArrayList[java.util.Map[String, Any]]()
@ -120,6 +121,7 @@ object PesentationMessageToJsonConverter {
presentation.put(Constants.ID, msg.presentation.id)
presentation.put(Constants.NAME, msg.presentation.name)
presentation.put(Constants.CURRENT, msg.presentation.current:java.lang.Boolean)
presentation.put(Constants.DOWNLOADABLE, msg.presentation.downloadable:java.lang.Boolean)
// Get the pages for a presentation
val pages = new java.util.ArrayList[java.util.Map[String, Any]]()
@ -204,6 +206,7 @@ object PesentationMessageToJsonConverter {
presentation.put(Constants.ID, msg.presentation.id)
presentation.put(Constants.NAME, msg.presentation.name)
presentation.put(Constants.CURRENT, msg.presentation.current:java.lang.Boolean)
presentation.put(Constants.DOWNLOADABLE, msg.presentation.downloadable:java.lang.Boolean)
val pages = new java.util.ArrayList[java.util.Map[String, Any]]()
msg.presentation.pages.values foreach {p =>
@ -225,6 +228,7 @@ object PesentationMessageToJsonConverter {
presentation.put(Constants.ID, msg.presentation.id)
presentation.put(Constants.NAME, msg.presentation.name)
presentation.put(Constants.CURRENT, msg.presentation.current:java.lang.Boolean)
presentation.put(Constants.DOWNLOADABLE, msg.presentation.downloadable:java.lang.Boolean)
val pages = new java.util.ArrayList[java.util.Map[String, Any]]()
msg.presentation.pages.values foreach {p =>
@ -246,6 +250,7 @@ object PesentationMessageToJsonConverter {
presentation.put(Constants.ID, msg.current.id)
presentation.put(Constants.NAME, msg.current.name)
presentation.put(Constants.CURRENT, msg.current.current:java.lang.Boolean)
presentation.put(Constants.DOWNLOADABLE, msg.current.downloadable:java.lang.Boolean)
val pages = new java.util.ArrayList[java.util.Map[String, Any]]()

View File

@ -0,0 +1,78 @@
package org.bigbluebutton.core.apps.sharednotes
import name.fraser.neil.plaintext.diff_match_patch
import name.fraser.neil.plaintext.diff_match_patch._
import org.bigbluebutton.core.api._
import org.bigbluebutton.core.MeetingActor
import scala.collection.JavaConversions._
import scala.collection._
import java.util.Collections
trait SharedNotesApp {
this : MeetingActor =>
val outGW: MessageOutGateway
val notes = new scala.collection.mutable.HashMap[String, String]()
notes += ("MAIN_WINDOW" -> "")
val patcher = new diff_match_patch()
var notesCounter = 0;
var removedNotes : Set[Int] = Set()
def handlePatchDocumentRequest(msg: PatchDocumentRequest) {
// meetingId, userId, noteId, patch, beginIndex, endIndex
notes.synchronized {
val document = notes(msg.noteID)
val patchObjects = patcher.patch_fromText(msg.patch)
val result = patcher.patch_apply(patchObjects, document)
notes(msg.noteID) = result(0).toString()
}
outGW.send(new PatchDocumentReply(meetingID, recorded, msg.requesterID, msg.noteID, msg.patch, msg.beginIndex, msg.endIndex))
}
def handleGetCurrentDocumentRequest(msg: GetCurrentDocumentRequest) {
val copyNotes = notes.toMap
outGW.send(new GetCurrentDocumentReply(meetingID, recorded, msg.requesterID, copyNotes))
}
private def createAdditionalNotesNonSync(requesterID:String) {
var noteID = 0
if (removedNotes.isEmpty()) {
notesCounter += 1
noteID = notesCounter
} else {
noteID = removedNotes.min
removedNotes -= noteID
}
notes += (noteID.toString -> "")
outGW.send(new CreateAdditionalNotesReply(meetingID, recorded, requesterID, noteID.toString))
}
def handleCreateAdditionalNotesRequest(msg: CreateAdditionalNotesRequest) {
notes.synchronized {
createAdditionalNotesNonSync(msg.requesterID)
}
}
def handleDestroyAdditionalNotesRequest(msg: DestroyAdditionalNotesRequest) {
notes.synchronized {
removedNotes += msg.noteID.toInt
notes -= msg.noteID
}
outGW.send(new DestroyAdditionalNotesReply(meetingID, recorded, msg.requesterID, msg.noteID))
}
def handleRequestAdditionalNotesSetRequest(msg: RequestAdditionalNotesSetRequest) {
notes.synchronized {
var num = msg.additionalNotesSetSize - notes.size + 1
for (i <- 1 to num) {
createAdditionalNotesNonSync(msg.requesterID)
}
}
}
}

View File

@ -0,0 +1,28 @@
package org.bigbluebutton.core.apps.sharednotes
import org.bigbluebutton.core.BigBlueButtonGateway
import org.bigbluebutton.core.api._
class SharedNotesInGateway(bbbGW: BigBlueButtonGateway) {
def patchDocument(meetingId: String, userId: String, noteId: String,
patch: String, beginIndex: Int, endIndex: Int) {
bbbGW.accept(new PatchDocumentRequest(meetingId, userId, noteId, patch, beginIndex, endIndex));
}
def getCurrentDocument(meetingId: String, userId: String) {
bbbGW.accept(new GetCurrentDocumentRequest(meetingId, userId));
}
def createAdditionalNotes(meetingId: String, userId: String) {
bbbGW.accept(new CreateAdditionalNotesRequest(meetingId, userId));
}
def destroyAdditionalNotes(meetingId: String, userId: String, noteId: String) {
bbbGW.accept(new DestroyAdditionalNotesRequest(meetingId, userId, noteId));
}
def requestAdditionalNotesSet(meetingId: String, userId: String, additionalNotesSetSize: Int) {
bbbGW.accept(new RequestAdditionalNotesSetRequest(meetingId, userId, additionalNotesSetSize));
}
}

View File

@ -0,0 +1,64 @@
package org.bigbluebutton.core.apps.sharednotes.red5
import org.bigbluebutton.conference.meeting.messaging.red5.ConnectionInvokerService
import org.bigbluebutton.core.api._
import org.bigbluebutton.conference.meeting.messaging.red5.DirectClientMessage
import com.google.gson.Gson
import org.bigbluebutton.conference.meeting.messaging.red5.BroadcastClientMessage
import scala.collection.mutable.HashMap
import collection.JavaConverters._
import scala.collection.JavaConversions._
import java.util.ArrayList
class SharedNotesClientMessageSender(service: ConnectionInvokerService) extends OutMessageListener2 {
def handleMessage(msg: IOutMessage) {
msg match {
case msg: PatchDocumentReply => handlePatchDocumentReply(msg)
case msg: GetCurrentDocumentReply => handleGetCurrentDocumentReply(msg)
case msg: CreateAdditionalNotesReply => handleCreateAdditionalNotesReply(msg)
case msg: DestroyAdditionalNotesReply => handleDestroyAdditionalNotesReply(msg)
case _ => // do nothing
}
}
private def handlePatchDocumentReply(msg: PatchDocumentReply) {
val message = new java.util.HashMap[String, Object]()
message.put("userID", msg.requesterID)
message.put("noteID", msg.noteID)
message.put("patch", msg.patch)
message.put("beginIndex", msg.beginIndex.toString)
message.put("endIndex", msg.endIndex.toString)
val m = new BroadcastClientMessage(msg.meetingID, "PatchDocumentCommand", message);
service.sendMessage(m);
}
private def handleGetCurrentDocumentReply(msg: GetCurrentDocumentReply) {
val gson = new Gson();
val message = new java.util.HashMap[String, Object]()
val jsonMsg = gson.toJson(mapAsJavaMap(msg.notes))
message.put("notes", jsonMsg)
val m = new DirectClientMessage(msg.meetingID, msg.requesterID, "GetCurrentDocumentCommand", message);
service.sendMessage(m);
}
private def handleCreateAdditionalNotesReply(msg: CreateAdditionalNotesReply) {
val message = new java.util.HashMap[String, Object]()
message.put("noteID", msg.noteID)
val m = new BroadcastClientMessage(msg.meetingID, "CreateAdditionalNotesCommand", message);
service.sendMessage(m);
}
private def handleDestroyAdditionalNotesReply(msg: DestroyAdditionalNotesReply) {
val message = new java.util.HashMap[String, Object]()
message.put("noteID", msg.noteID)
val m = new BroadcastClientMessage(msg.meetingID, "DestroyAdditionalNotesCommand", message);
service.sendMessage(m);
}
}

View File

@ -108,7 +108,7 @@ trait UsersApp {
logger.info("Register user failed: reason=[meeting has ended] mid=[" + meetingID + "] uid=[" + msg.userID + "]")
sendMeetingHasEnded(msg.userID)
} else {
val regUser = new RegisteredUser(msg.userID, msg.extUserID, msg.name, msg.role, msg.authToken)
val regUser = new RegisteredUser(msg.userID, msg.extUserID, msg.name, msg.role, msg.authToken, msg.guest)
regUsers += msg.authToken -> regUser
logger.info("Register user success: mid=[" + meetingID + "] uid=[" + msg.userID + "]")
outGW.send(new UserRegistered(meetingID, recorded, regUser))
@ -208,22 +208,6 @@ trait UsersApp {
au.toArray
}
def handleUserRaiseHand(msg: UserRaiseHand) {
users.getUser(msg.userId) foreach {user =>
val uvo = user.copy(raiseHand=true)
users.addUser(uvo)
outGW.send(new UserRaisedHand(meetingID, recorded, uvo.userID))
}
}
def handleUserLowerHand(msg: UserLowerHand) {
users.getUser(msg.userId) foreach {user =>
val uvo = user.copy(raiseHand=false)
users.addUser(uvo)
outGW.send(new UserLoweredHand(meetingID, recorded, uvo.userID, msg.loweredBy))
}
}
def handleEjectUserFromMeeting(msg: EjectUserFromMeeting) {
users.getUser(msg.userId) foreach {user =>
if (user.voiceUser.joined) {
@ -261,11 +245,28 @@ trait UsersApp {
}
def handleChangeUserStatus(msg: ChangeUserStatus):Unit = {
if (users.hasUser(msg.userID)) {
users.getUser(msg.userID) foreach {user =>
val uvo = msg.status match {
case "mood" => user.copy( mood=msg.value.asInstanceOf[String])
case _ => null
}
if (uvo != null) {
logger.info("User changed mood: mid=[" + meetingID + "] uid=[" + uvo.userID + "] mood=[" + msg.value + "]")
users.addUser(uvo)
}
outGW.send(new UserStatusChange(meetingID, recorded, msg.userID, msg.status, msg.value))
}
}
def handleChangeUserRole(msg: ChangeUserRole) {
users.getUser(msg.userID) foreach {user =>
val uvo = user.copy(role=msg.role)
users.addUser(uvo)
val userRole = if(msg.role == Role.MODERATOR) "MODERATOR" else "VIEWER"
outGW.send(new UserRoleChange(meetingID, recorded, msg.userID, userRole))
}
}
def handleGetUsers(msg: GetUsers):Unit = {
outGW.send(new GetUsersReply(msg.meetingID, msg.requesterID, users.getUsers))
}
@ -275,28 +276,35 @@ trait UsersApp {
regUser foreach { ru =>
val vu = new VoiceUser(msg.userID, msg.userID, ru.name, ru.name,
false, false, false, false)
val waitingForAcceptance = ru.guest && guestPolicy == GuestPolicy.ASK_MODERATOR;
val uvo = new UserVO(msg.userID, ru.externId, ru.name,
ru.role, raiseHand=false, presenter=false,
ru.role, ru.guest, waitingForAcceptance=waitingForAcceptance, mood="", presenter=false,
hasStream=false, locked=getInitialLockStatus(ru.role),
webcamStreams=new ListSet[String](), phoneUser=false, vu, listenOnly=false)
users.addUser(uvo)
logger.info("User joined meeting: mid=[" + meetingID + "] uid=[" + uvo.userID + "] role=[" + uvo.role + "] locked=[" + uvo.locked + "] permissions.lockOnJoin=[" + permissions.lockOnJoin + "] permissions.lockOnJoinConfigurable=[" + permissions.lockOnJoinConfigurable + "]")
if (uvo.guest && guestPolicy == GuestPolicy.ALWAYS_DENY) {
outGW.send(new GuestAccessDenied(meetingID, recorded, uvo.userID))
} else {
outGW.send(new UserJoined(meetingID, recorded, uvo))
outGW.send(new MeetingState(meetingID, recorded, uvo.userID, permissions, meetingMuted))
if (!waitingForAcceptance) {
// Become presenter if the only moderator
if (users.numModerators == 1) {
if (ru.role == Role.MODERATOR) {
assignNewPresenter(msg.userID, ru.name, msg.userID)
}
}
}
webUserJoined
startRecordingIfAutoStart()
}
}
}
def handleUserLeft(msg: UserLeaving):Unit = {
if (users.hasUser(msg.userID)) {
@ -344,7 +352,7 @@ trait UsersApp {
val sessionId = "PHONE-" + webUserId;
val uvo = new UserVO(webUserId, webUserId, msg.voiceUser.callerName,
Role.VIEWER, raiseHand=false, presenter=false,
Role.VIEWER, guest=false, waitingForAcceptance=false, mood="", presenter=false,
hasStream=false, locked=getInitialLockStatus(Role.VIEWER), webcamStreams=new ListSet[String](),
phoneUser=true, vu, listenOnly=false)
@ -363,13 +371,15 @@ trait UsersApp {
def handleVoiceUserJoined(msg: VoiceUserJoined) = {
val user = users.getUser(msg.voiceUser.webUserId) match {
case Some(user) => {
// this is used to restore the mute state on reconnect
val previouslyMuted = user.voiceUser.muted
val nu = user.copy(voiceUser=msg.voiceUser)
users.addUser(nu)
logger.info("Received user joined voice for user [" + nu.name + "] userid=[" + msg.voiceUser.webUserId + "]" )
outGW.send(new UserJoinedVoice(meetingID, recorded, voiceBridge, nu))
if (meetingMuted)
outGW.send(new MuteVoiceUser(meetingID, recorded, nu.userID, nu.userID, meetingMuted))
if (meetingMuted || previouslyMuted)
outGW.send(new MuteVoiceUser(meetingID, recorded, nu.userID, nu.userID, true))
}
case None => {
handleUserJoinedVoiceFromPhone(msg)
@ -445,4 +455,32 @@ trait UsersApp {
}
}
private def isModerator(userId: String):Boolean = {
users.getUser(userId) match {
case Some(user) => return user.role == Role.MODERATOR && !user.waitingForAcceptance
case None => return false
}
}
def handleRespondToGuest(msg: RespondToGuest) {
if (isModerator(msg.requesterID)) {
var usersToAnswer:Array[UserVO] = null;
if (msg.userId == null) {
usersToAnswer = users.getUsers.filter(u => u.waitingForAcceptance == true)
} else {
usersToAnswer = users.getUsers.filter(u => u.waitingForAcceptance == true && u.userID == msg.userId)
}
usersToAnswer foreach {user =>
println("UsersApp - handleGuestAccessDenied for user [" + user.userID + "]");
if (msg.response == true) {
val nu = user.copy(waitingForAcceptance=false)
users.addUser(nu)
outGW.send(new UserJoined(meetingID, recorded, nu))
} else {
outGW.send(new GuestAccessDenied(meetingID, recorded, user.userID))
}
}
}
}
}

View File

@ -3,7 +3,6 @@ package org.bigbluebutton.core.apps.users.red5
import org.bigbluebutton.conference.meeting.messaging.red5.ConnectionInvokerService
import org.bigbluebutton.conference.meeting.messaging.red5.SharedObjectClientMessage
import java.util.ArrayList
import java.util.List
import java.util.Map
import java.util.HashMap
import org.bigbluebutton.core.api._
@ -26,8 +25,7 @@ class UsersClientMessageSender(service: ConnectionInvokerService) extends OutMes
case msg: UserJoined => handleUserJoined(msg)
case msg: UserLeft => handleUserLeft(msg)
case msg: UserStatusChange => handleUserStatusChange(msg)
case msg: UserRaisedHand => handleUserRaisedHand(msg)
case msg: UserLoweredHand => handleUserLoweredHand(msg)
case msg: UserRoleChange => handleUserRoleChange(msg)
case msg: UserSharedWebcam => handleUserSharedWebcam(msg)
case msg: UserUnsharedWebcam => handleUserUnshareWebcam(msg)
case msg: GetUsersReply => handleGetUsersReply(msg)
@ -45,6 +43,9 @@ class UsersClientMessageSender(service: ConnectionInvokerService) extends OutMes
case msg: UserLocked => handleUserLocked(msg)
case msg: MeetingMuted => handleMeetingMuted(msg)
case msg: MeetingState => handleMeetingState(msg)
case msg: GetGuestPolicyReply => handleGetGuestPolicyReply(msg)
case msg: GuestPolicyChanged => handleGuestPolicyChanged(msg)
case msg: GuestAccessDenied => handleGuestAccessDenied(msg)
case _ => // println("Unhandled message in UsersClientMessageSender")
}
@ -79,7 +80,9 @@ class UsersClientMessageSender(service: ConnectionInvokerService) extends OutMes
wuser.put("externUserID", user.externUserID)
wuser.put("name", user.name)
wuser.put("role", user.role.toString())
wuser.put("raiseHand", user.raiseHand:java.lang.Boolean)
wuser.put("guest", user.guest:java.lang.Boolean)
wuser.put("waitingForAcceptance", user.waitingForAcceptance:java.lang.Boolean)
wuser.put("mood", user.mood:java.lang.String)
wuser.put("presenter", user.presenter:java.lang.Boolean)
wuser.put("hasStream", user.hasStream:java.lang.Boolean)
wuser.put("locked", user.locked:java.lang.Boolean)
@ -402,35 +405,6 @@ class UsersClientMessageSender(service: ConnectionInvokerService) extends OutMes
service.sendMessage(m);
}
def handleUserRaisedHand(msg: UserRaisedHand) {
var args = new HashMap[String, Object]()
args.put("userId", msg.userID)
val message = new java.util.HashMap[String, Object]()
val gson = new Gson();
message.put("msg", gson.toJson(args))
// println("UsersClientMessageSender - handleUserRaisedHand \n" + message.get("msg") + "\n")
var m = new BroadcastClientMessage(msg.meetingID, "userRaisedHand", message);
service.sendMessage(m);
}
def handleUserLoweredHand(msg: UserLoweredHand) {
var args = new HashMap[String, Object]();
args.put("userId", msg.userID)
args.put("loweredBy", msg.loweredBy)
val message = new java.util.HashMap[String, Object]()
val gson = new Gson();
message.put("msg", gson.toJson(args))
// println("UsersClientMessageSender - handleUserLoweredHand \n" + message.get("msg") + "\n")
var m = new BroadcastClientMessage(msg.meetingID, "userLoweredHand", message);
service.sendMessage(m);
}
def handleUserSharedWebcam(msg: UserSharedWebcam) {
var args = new HashMap[String, Object]()
args.put("userId", msg.userID)
@ -477,6 +451,21 @@ class UsersClientMessageSender(service: ConnectionInvokerService) extends OutMes
service.sendMessage(m);
}
private def handleUserRoleChange(msg: UserRoleChange) {
var args = new HashMap[String, Object]();
args.put("userID", msg.userID);
args.put("role", msg.role);
val message = new java.util.HashMap[String, Object]()
val gson = new Gson();
message.put("msg", gson.toJson(args))
// println("UsersClientMessageSender - handleUserRoleChange \n" + message.get("msg") + "\n")
var m = new BroadcastClientMessage(msg.meetingID, "participantRoleChange", message);
service.sendMessage(m);
}
private def handleUserListeningOnly(msg: UserListeningOnly) {
var args = new HashMap[String, Object]();
args.put("userId", msg.userID);
@ -491,4 +480,46 @@ class UsersClientMessageSender(service: ConnectionInvokerService) extends OutMes
var m = new BroadcastClientMessage(msg.meetingID, "user_listening_only", message);
service.sendMessage(m);
}
private def handleGetGuestPolicyReply(msg: GetGuestPolicyReply) {
var args = new HashMap[String, Object]();
args.put("guestPolicy", msg.policy.toString());
val message = new java.util.HashMap[String, Object]()
val gson = new Gson();
message.put("msg", gson.toJson(args))
// println("UsersClientMessageSender - handleGetGuestPolicyReply \n" + message.get("msg") + "\n")
val m = new DirectClientMessage(msg.meetingID, msg.requesterID,"get_guest_policy_reply", message);
service.sendMessage(m);
}
private def handleGuestPolicyChanged(msg: GuestPolicyChanged) {
var args = new HashMap[String, Object]();
args.put("guestPolicy", msg.policy.toString());
val message = new java.util.HashMap[String, Object]()
val gson = new Gson();
message.put("msg", gson.toJson(args))
// println("UsersClientMessageSender - handleGuestPolicyChanged \n" + message.get("msg") + "\n")
var m = new BroadcastClientMessage(msg.meetingID, "guest_policy_changed", message);
service.sendMessage(m);
}
private def handleGuestAccessDenied(msg: GuestAccessDenied) {
var args = new HashMap[String, Object]();
args.put("userId", msg.userId);
val message = new java.util.HashMap[String, Object]()
val gson = new Gson();
message.put("msg", gson.toJson(args))
// println("UsersClientMessageSender - handleGuestAccessDenied \n" + message.get("msg") + "\n")
val m = new DirectClientMessage(msg.meetingID, msg.userId, "guest_access_denied", message);
service.sendMessage(m);
}
}

View File

@ -23,11 +23,10 @@ class UsersEventRedisPublisher(service: MessageSender) extends OutMessageListene
case msg: GetUsersReply => handleGetUsersReply(msg)
case msg: ValidateAuthTokenReply => handleValidateAuthTokenReply(msg)
case msg: UserJoined => handleUserJoined(msg)
case msg: UserRaisedHand => handleUserRaisedHand(msg)
case msg: UserLoweredHand => handleUserLoweredHand(msg)
case msg: UserSharedWebcam => handleUserSharedWebcam(msg)
case msg: UserUnsharedWebcam => handleUserUnsharedWebcam(msg)
case msg: UserStatusChange => handleUserStatusChange(msg)
case msg: UserRoleChange => handleUserRoleChange(msg)
case msg: UserVoiceMuted => handleUserVoiceMuted(msg)
case msg: UserVoiceTalking => handleUserVoiceTalking(msg)
case msg: MuteVoiceUser => handleMuteVoiceUser(msg)
@ -80,13 +79,8 @@ class UsersEventRedisPublisher(service: MessageSender) extends OutMessageListene
service.send(MessagingConstants.FROM_USERS_CHANNEL, json)
}
private def handleUserRaisedHand(msg: UserRaisedHand) {
val json = UsersMessageToJsonConverter.userRaisedHandToJson(msg)
service.send(MessagingConstants.FROM_USERS_CHANNEL, json)
}
private def handleUserLoweredHand(msg: UserLoweredHand) {
val json = UsersMessageToJsonConverter.userLoweredHandToJson(msg)
private def handleUserRoleChange(msg: UserRoleChange) {
val json = UsersMessageToJsonConverter.userRoleChangeToJson(msg)
service.send(MessagingConstants.FROM_USERS_CHANNEL, json)
}

View File

@ -16,7 +16,9 @@ object UsersMessageToJsonConverter {
wuser += "extern_userid" -> user.externUserID
wuser += "name" -> user.name
wuser += "role" -> user.role.toString()
wuser += "raise_hand" -> user.raiseHand
wuser += "guest" -> user.guest
wuser += "waiting_for_acceptance" -> user.waitingForAcceptance
wuser += "mood" -> user.mood
wuser += "presenter" -> user.presenter
wuser += "has_stream" -> user.hasStream
wuser += "locked" -> user.locked
@ -46,6 +48,7 @@ object UsersMessageToJsonConverter {
wuser += "name" -> user.name
wuser += "role" -> user.role.toString()
wuser += "authToken" -> user.authToken
wuser += "guest" -> user.guest
mapAsJavaMap(wuser)
}
@ -127,28 +130,6 @@ object UsersMessageToJsonConverter {
Util.buildJson(header, payload)
}
def userRaisedHandToJson(msg: UserRaisedHand):String = {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.RAISE_HAND, msg.recorded)
payload.put(Constants.USER_ID, msg.userID)
val header = Util.buildHeader(MessageNames.USER_RAISED_HAND, msg.version, None)
Util.buildJson(header, payload)
}
def userLoweredHandToJson(msg: UserLoweredHand):String = {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.RAISE_HAND, msg.recorded)
payload.put(Constants.USER_ID, msg.userID)
payload.put(Constants.LOWERED_BY, msg.loweredBy)
val header = Util.buildHeader(MessageNames.USER_LOWERED_HAND, msg.version, None)
Util.buildJson(header, payload)
}
def userStatusChangeToJson(msg: UserStatusChange):String = {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
@ -160,6 +141,16 @@ object UsersMessageToJsonConverter {
Util.buildJson(header, payload)
}
def userRoleChangeToJson(msg: UserRoleChange):String = {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)
payload.put(Constants.USER_ID, msg.userID)
payload.put(Constants.ROLE, msg.role)
val header = Util.buildHeader(MessageNames.USER_ROLE_CHANGED, msg.version, None)
Util.buildJson(header, payload)
}
def userSharedWebcamToJson(msg: UserSharedWebcam):String = {
val payload = new java.util.HashMap[String, Any]()
payload.put(Constants.MEETING_ID, msg.meetingID)

View File

@ -0,0 +1,16 @@
package org.bigbluebutton.core.apps.video
import org.bigbluebutton.core.api._
import org.bigbluebutton.core.MeetingActor
trait VideoApp {
this : MeetingActor =>
val outGW: MessageOutGateway
def handleGetStreamPath(msg: GetStreamPath) {
// TODO: Request stream path from bbbWeb here
val streamPath = msg.defaultPath
outGW.send(new GetStreamPathReply(msg.meetingID, msg.requesterID, msg.streamName, streamPath))
}
}

View File

@ -0,0 +1,31 @@
package org.bigbluebutton.core.apps.video.red5
import org.bigbluebutton.conference.meeting.messaging.red5.ConnectionInvokerService
import org.bigbluebutton.core.api._
import org.bigbluebutton.conference.meeting.messaging.red5.DirectClientMessage
import com.google.gson.Gson
import org.bigbluebutton.conference.meeting.messaging.red5.BroadcastClientMessage
class VideoClientMessageSender(service: ConnectionInvokerService) extends OutMessageListener2 {
def handleMessage(msg: IOutMessage) {
msg match {
case msg:GetStreamPathReply => handleGetStreamPathReply(msg)
case _ => // do nothing
}
}
private def handleGetStreamPathReply(msg: GetStreamPathReply) {
// Build JSON
val args = new java.util.HashMap[String, Object]()
args.put("streamName", msg.streamName);
args.put("streamPath", msg.streamPath);
val message = new java.util.HashMap[String, Object]()
val gson = new Gson();
message.put("msg", gson.toJson(args))
var m = new DirectClientMessage(msg.meetingID, msg.requesterID, "getStreamPathReply", message);
service.sendMessage(m);
}
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
This program is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation; either version 3.0 of the License, or (at your option) any later
version.
BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.0.xsd">
<bean id="sharedNotesHandler" class="org.bigbluebutton.conference.service.sharednotes.SharedNotesHandler">
<property name="sharedNotesApplication"> <ref local="sharedNotesApplication"/></property>
</bean>
<bean id="sharedNotesApplication" class="org.bigbluebutton.conference.service.sharednotes.SharedNotesApplication">
<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
</bean>
<bean id="sharednotes.service" class="org.bigbluebutton.conference.service.sharednotes.SharedNotesService">
<property name="sharedNotesApplication"> <ref local="sharedNotesApplication"/></property>
</bean>
</beans>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
This program is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation; either version 3.0 of the License, or (at your option) any later
version.
BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.0.xsd
">
<bean id="videoHandler" class="org.bigbluebutton.conference.service.video.VideoHandler">
<property name="videoApplication"> <ref local="videoApplication"/></property>
</bean>
<bean id="videoApplication" class="org.bigbluebutton.conference.service.video.VideoApplication">
<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
<property name="defaultStreamPath" value="${video.defaultStreamPath}"/>
</bean>
<bean id="video.service" class="org.bigbluebutton.conference.service.video.VideoService">
<property name="videoApplication"> <ref local="videoApplication"/></property>
</bean>
</beans>

View File

@ -36,3 +36,9 @@ icecast.port=8000
icecast.username=source
icecast.password=hackme
icecast.broadcast=true
# This setting enable the use of proxy servers for the video stream
# To use it, set it to the proxy server addresses, ending with the
# conference address
# Example: proxy1_ip/proxy2_ip/conference_ip
video.defaultStreamPath=

View File

@ -53,6 +53,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<set>
<ref bean="participantsHandler" />
<ref bean="whiteboardApplication" />
<ref bean="sharedNotesHandler" />
<ref bean="videoHandler" />
</set>
</property>
<property name="recorderApplication">
@ -120,6 +122,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<ref bean="chatRedisPublisher"/>
<ref bean="fsConfService"/>
<ref bean="whiteboardEventRedisPublisher"/>
<ref bean="sharedNotesRed5ClientSender"/>
<ref bean="videoRed5ClientSender"/>
</set>
</property>
</bean>
@ -200,7 +204,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<property name="bigBlueButtonInGW"> <ref bean="bbbInGW"/></property>
</bean>
<bean id="sharedNotesRed5ClientSender" class="org.bigbluebutton.core.apps.sharednotes.red5.SharedNotesClientMessageSender">
<constructor-arg index="0" ref="connInvokerService"/>
</bean>
<bean id="videoRed5ClientSender" class="org.bigbluebutton.core.apps.video.red5.VideoClientMessageSender">
<constructor-arg index="0" ref="connInvokerService"/>
</bean>
<import resource="bbb-redis-pool.xml"/>
<import resource="bbb-redis-recorder.xml"/>
@ -210,6 +220,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
<import resource="bbb-app-presentation.xml" />
<import resource="bbb-app-whiteboard.xml" />
<import resource="bbb-app-users.xml" />
<import resource="bbb-app-sharednotes.xml" />
<import resource="bbb-app-video.xml" />
<import resource="bbb-voice-app.xml" />
<import resource="bbb-voice-freeswitch.xml" />

View File

@ -99,7 +99,7 @@ ToolTip {
fontFamily: Arial;
}
Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowProfileComboStyle, .cameraDisplaySettingsWindowCameraSelector, .languageSelectorStyle, .testJavaLinkButtonStyle, .recordButtonStyleNormal, .recordButtonStyleStart, .recordButtonStyleStop, .micSettingsWindowHelpButtonStyle {
Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowProfileComboStyle, .cameraDisplaySettingsWindowCameraSelector, .languageSelectorStyle, .testJavaLinkButtonStyle, .recordButtonStyleNormal, .recordButtonStyleStart, .recordButtonStyleStop, .micSettingsWindowHelpButtonStyle, .bandwidthButtonStyle, .settingsButtonStyle, .acceptButtonStyle, .denyButtonStyle {
textIndent: 0;
paddingLeft: 10;
paddingRight: 10;
@ -169,6 +169,18 @@ Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraD
icon: Embed('assets/images/logout.png');
}
.settingsButtonStyle {
icon: Embed('assets/images/ic_settings_16px.png');
}
.acceptButtonStyle {
icon: Embed('assets/images/ic_thumb_up_16px.png');
}
.denyButtonStyle {
icon: Embed('assets/images/ic_thumb_down_16px.png');
}
DataGrid {
backgroundColor: #e1e2e5;
rollOverColor: #f3f3f3;
@ -285,7 +297,7 @@ DataGrid {
.presentationUploadButtonStyle, .presentationBackButtonStyle, .presentationBackButtonDisabledStyle, .presentationForwardButtonStyle, .presentationForwardButtonDisabledStyle,
.presentationFitToWidthButtonStyle, .presentationFitToPageButtonStyle
.presentationFitToWidthButtonStyle, .presentationFitToPageButtonStyle, .presentationDownloadButtonStyle
{
textIndent: 0;
paddingLeft: 10;
@ -305,23 +317,23 @@ DataGrid {
}
.presentationUploadButtonStyle {
icon: Embed('assets/images/upload.png');
icon: Embed('assets/images/ic_file_upload_16px.png');
}
.presentationBackButtonStyle {
icon: Embed('assets/images/left-arrow.png');
icon: Embed('assets/images/ic_arrow_back_24px.png');
}
.presentationBackButtonDisabledStyle {
icon: Embed('assets/images/left-arrow-disabled.png');
icon: Embed('assets/images/ic_arrow_back_grey_24px.png');
}
.presentationForwardButtonStyle {
icon: Embed('assets/images/right-arrow.png');
icon: Embed('assets/images/ic_arrow_forward_24px.png');
}
.presentationForwardButtonDisabledStyle {
icon: Embed('assets/images/right-arrow-disabled.png');
icon: Embed('assets/images/ic_arrow_forward_grey_24px.png');
}
.presentationFitToWidthButtonStyle {
@ -332,6 +344,10 @@ DataGrid {
icon: Embed('assets/images/fit-to-screen.png');
}
.presentationDownloadButtonStyle {
icon: Embed('assets/images/ic_file_download_16px.png');
}
.presentationZoomSliderStyle{
labelOffset: 0;
thumbOffset: 3;
@ -559,6 +575,19 @@ DataGrid {
fontSize: 12;
}
.micSettingsWindowOpenDialogLabelStyle {
fontFamily: Arial;
fontSize: 14;
fontWeight: bold;
color: #e1e2e5;
}
.micSettingsWindowShareMicrophoneLabelStyle {
fontFamily: Arial;
fontSize: 14;
color: #5e5f63;
}
.micSettingsWindowPlaySoundButtonStyle, .micSettingsWindowChangeMicButtonStyle {
fillAlphas: 1, 1, 1, 1;
fillColors: #fefeff, #e1e2e5, #ffffff, #eeeeee;
@ -897,6 +926,13 @@ Alert {
icon: Embed('assets/images/control-record-stop.png');
}
.bandwidthButtonStyle {
paddingTop: 0;
paddingBottom: 0;
height: 22;
icon: Embed('assets/images/ic_swap_vert_16px.png');
}
.statusImageStyle {
successImage: Embed(source='assets/images/status_success.png');
warningImage: Embed(source='assets/images/status_warning.png');
@ -917,3 +953,63 @@ Alert {
fontSize: 12;
paddingTop: 0;
}
.addLayoutButtonStyle {
icon: Embed('assets/images/ic_add_circle_outline_16px.png');
}
.saveLayoutButtonStyle {
icon: Embed('assets/images/ic_file_download_16px.png');
}
.loadLayoutButtonStyle {
icon: Embed('assets/images/ic_file_upload_16px.png');
}
.broadcastLayoutButtonStyle {
icon: Embed('assets/images/ic_send_16px.png');
}
.moodStyle {
icon: Embed('assets/images/ic_mood_black_18dp.png');
}
.moodRaiseHandStyle {
icon: Embed('assets/images/icon-3-high-five.png');
}
.moodAgreedStyle {
icon: Embed('assets/images/icon-6-thumb-up.png');
}
.moodDisagreedStyle {
icon: Embed('assets/images/icon-7-thumb-down.png');
}
.moodSpeakFasterStyle {
icon: Embed('assets/images/ic_fast_forward_black_18dp.png');
}
.moodSpeakSlowerStyle {
icon: Embed('assets/images/ic_fast_rewind_black_18dp.png');
}
.moodSpeakLouderStyle {
icon: Embed('assets/images/ic_volume_up_black_18dp.png');
}
.moodSpeakSofterStyle {
icon: Embed('assets/images/ic_volume_down_black_18dp.png');
}
.moodBeRightBackStyle {
icon: Embed('assets/images/ic_access_time_black_18dp.png');
}
.moodHappyStyle {
icon: Embed('assets/images/icon-6-smiling-face.png');
}
.moodSadStyle {
icon: Embed('assets/images/icon-7-sad-face.png');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Some files were not shown because too many files have changed in this diff Show More