diff --git a/bbb-api-demo/src/main/webapp/bbb_api.jsp b/bbb-api-demo/src/main/webapp/bbb_api.jsp index 94ffcd244652000334393dff9e99769ca7a95806..b048c20e527866641a7d750e0a5af43989ffa24b 100755 --- a/bbb-api-demo/src/main/webapp/bbb_api.jsp +++ b/bbb-api-demo/src/main/webapp/bbb_api.jsp @@ -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 // diff --git a/bbb-api-demo/src/main/webapp/css/mconf-bootstrap.min.css b/bbb-api-demo/src/main/webapp/css/mconf-bootstrap.min.css new file mode 100644 index 0000000000000000000000000000000000000000..b6f2281784d44047cbcb84a1df94c4e3abeae4b5 --- /dev/null +++ b/bbb-api-demo/src/main/webapp/css/mconf-bootstrap.min.css @@ -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;} diff --git a/bbb-api-demo/src/main/webapp/demo10_helper.jsp b/bbb-api-demo/src/main/webapp/demo10_helper.jsp index 38905bc02698405842404d6ee7a53abc20cf14ba..62024f89611941fa102a1ff0859543cb5d655687 100755 --- a/bbb-api-demo/src/main/webapp/demo10_helper.jsp +++ b/bbb-api-demo/src/main/webapp/demo10_helper.jsp @@ -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")){%> diff --git a/bbb-api-demo/src/main/webapp/demo3.jsp b/bbb-api-demo/src/main/webapp/demo3.jsp index 02c3d7b22394ae80f4bfbe2fc31bcb4917c8f91c..13ca4b343cb98a6f7429a9da0cdb3d70527c993f 100755 --- a/bbb-api-demo/src/main/webapp/demo3.jsp +++ b/bbb-api-demo/src/main/webapp/demo3.jsp @@ -197,6 +197,16 @@ if (request.getParameterMap().isEmpty()) { <td> <input type="password" required name="password" /></td> </tr> + <tr> + <td> + </td> + <td style="text-align: right; "> + Guest:</td> + <td> + </td> + <td> + <input type="checkbox" name="guest" value="guest" /></td> + </tr> <tr> <td> </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"> diff --git a/bbb-api-demo/src/main/webapp/demo_mconf.jsp b/bbb-api-demo/src/main/webapp/demo_mconf.jsp new file mode 100644 index 0000000000000000000000000000000000000000..17edbff5ed069e1671e747197649d2a5e33a8536 --- /dev/null +++ b/bbb-api-demo/src/main/webapp/demo_mconf.jsp @@ -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> + </td> + <td style="text-align: right; "> + Room:</td> + <td> + + </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> Running!</i></span> + + </td> + </tr> + <tr> + <td> + </td> + <td style="text-align: right; "> + Full Name:</td> + <td style="width: 5px; "> + </td> + <td style="text-align: left "> + <input type="text" autofocus required name="username" /></td> + </tr> + <tr> + <td> + </td> + <td style="text-align: right; "> + Role:</td> + <td> + </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> + </td> + <td style="text-align: right; "> + Guest:</td> + <td> + </td> + <td> + <input id="check_guest" type="checkbox" name="guest" value="guest" /> (authorization required)</td> + </tr> + <tr> + <td> + </td> + <td> + </td> + <td> + </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> + + diff --git a/bbb-api-demo/src/main/webapp/images/mconf.png b/bbb-api-demo/src/main/webapp/images/mconf.png new file mode 100644 index 0000000000000000000000000000000000000000..82b0093af6659d22ada9e4eb90b982fe3c7e8c17 Binary files /dev/null and b/bbb-api-demo/src/main/webapp/images/mconf.png differ diff --git a/bbb-client-check/.gitignore b/bbb-client-check/.gitignore index 2d4fa21ea832bfa2d306db4c393f234c932250f1..fbdc8da61e3d20cdffb4029c02eedb3437c34c2b 100644 --- a/bbb-client-check/.gitignore +++ b/bbb-client-check/.gitignore @@ -9,3 +9,4 @@ index.template.html conf/config.xml resources/lib/bbb_webrtc_bridge_sip.js resources/lib/sip.js +resources/lib/bbb_localization.js diff --git a/bbb-client-check/build.xml b/bbb-client-check/build.xml index 7f1cab5f98d59c8f53305362560931020622132e..90eca8216b2129515cca374937d38f1dd48b57b5 100755 --- a/bbb-client-check/build.xml +++ b/bbb-client-check/build.xml @@ -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,7 @@ <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" /> </copy> <get src="${TEST_IMAGE_URL}" dest="${html.output}/test_image.jpg" skipexisting="true" /> diff --git a/bbb-client-check/html-template/index.html b/bbb-client-check/html-template/index.html index 1032a8e289a68c1451d18cf652e7b82064185959..c677de2f7faa83092f8a5bc9e8b316b383bbd9eb 100755 --- a/bbb-client-check/html-template/index.html +++ b/bbb-client-check/html-template/index.html @@ -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. diff --git a/bbb-client-check/html-template/index.template.html b/bbb-client-check/html-template/index.template.html index 710134f4ce5ae367386d66ff52f5f42aee03f7eb..f7583f2543529a98ccd9398970fc7bd5762c022b 100755 --- a/bbb-client-check/html-template/index.template.html +++ b/bbb-client-check/html-template/index.template.html @@ -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. diff --git a/bbb-client-check/locale/en_US/resources.properties b/bbb-client-check/locale/en_US/resources.properties index 09e02a687954da3ec747940d92cbca43a9336621..dd908f88fdd5b83dcfbd3b33dbc30d50061f360d 100755 --- a/bbb-client-check/locale/en_US/resources.properties +++ b/bbb-client-check/locale/en_US/resources.properties @@ -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 diff --git a/bbb-client-check/locale/pt_BR/resources.properties b/bbb-client-check/locale/pt_BR/resources.properties new file mode 100644 index 0000000000000000000000000000000000000000..96877f934040433fa7fd1d8b57b03792a989176c --- /dev/null +++ b/bbb-client-check/locale/pt_BR/resources.properties @@ -0,0 +1,31 @@ +# bbbsystemcheck.title = BigBlueButton Client Check +bbbsystemcheck.refresh = Recarregar +bbbsystemcheck.mail = E-mail +bbbsystemcheck.version = Versão do Client Check +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 diff --git a/bbb-client-check/src/BBBClientCheck.mxml b/bbb-client-check/src/BBBClientCheck.mxml index 2587d1ff739b6cc09092ec00bb219863596469a2..e9ebc0bc163a8a2646ba1ae2487074c0f547a8fe 100755 --- a/bbb-client-check/src/BBBClientCheck.mxml +++ b/bbb-client-check/src/BBBClientCheck.mxml @@ -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 */ diff --git a/bbb-video/.classpath b/bbb-video/.classpath deleted file mode 100755 index e076be4ecfda3344697041bd2b623776fefb6cae..0000000000000000000000000000000000000000 --- a/bbb-video/.classpath +++ /dev/null @@ -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> diff --git a/bbb-video/.gitignore b/bbb-video/.gitignore index 27584657a6b6e349e07bb6c58e0c899b75a6404c..51aacf96f5fb751b8fe597ebe5bf980ec244df90 100644 --- a/bbb-video/.gitignore +++ b/bbb-video/.gitignore @@ -2,4 +2,6 @@ bin build dist lib -build +.classpath +.project +.settings diff --git a/bbb-video/.project b/bbb-video/.project deleted file mode 100644 index dff16b260e4d10e4a1cbf017d678614759e9ff60..0000000000000000000000000000000000000000 --- a/bbb-video/.project +++ /dev/null @@ -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> diff --git a/bbb-video/build.gradle b/bbb-video/build.gradle index c883cbbac2f7ec5c357823736c57d4393d3ad5b1..1b2dfae3dca7f520fee3886f5e062c604531bc79 100755 --- a/bbb-video/build.gradle +++ b/bbb-video/build.gradle @@ -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 { diff --git a/bbb-video/src/main/java/org/bigbluebutton/app/video/CustomRTMPClient.java b/bbb-video/src/main/java/org/bigbluebutton/app/video/CustomRTMPClient.java new file mode 100644 index 0000000000000000000000000000000000000000..ef6734ce43e3c273392bfaac4b567a1cdad93f3d --- /dev/null +++ b/bbb-video/src/main/java/org/bigbluebutton/app/video/CustomRTMPClient.java @@ -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"); + } + } +} diff --git a/bbb-video/src/main/java/org/bigbluebutton/app/video/CustomStreamRelay.java b/bbb-video/src/main/java/org/bigbluebutton/app/video/CustomStreamRelay.java new file mode 100644 index 0000000000000000000000000000000000000000..7262dc3b25a4eb2cbc62c503e872e465248fe685 --- /dev/null +++ b/bbb-video/src/main/java/org/bigbluebutton/app/video/CustomStreamRelay.java @@ -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()); + } + + } + +} diff --git a/bbb-video/src/main/java/org/bigbluebutton/app/video/VideoApplication.java b/bbb-video/src/main/java/org/bigbluebutton/app/video/VideoApplication.java index 43d81edb2a1bd94f94a5ff09340ad455aa826063..147e96fc7ac3214e0ceef55ded889b7b8f7eed5e 100755 --- a/bbb-video/src/main/java/org/bigbluebutton/app/video/VideoApplication.java +++ b/bbb-video/src/main/java/org/bigbluebutton/app/video/VideoApplication.java @@ -21,16 +21,27 @@ 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.bigbluebutton.app.video.h263.H263Converter; 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.IScope; +import org.red5.server.api.scope.IBasicScope; +import org.red5.server.api.scope.IBroadcastScope; +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 org.apache.commons.lang3.StringUtils; import com.google.gson.Gson; public class VideoApplication extends MultiThreadedApplicationAdapter { @@ -42,13 +53,24 @@ public class VideoApplication extends MultiThreadedApplicationAdapter { private boolean recordVideoStream = false; 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>(); + @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 +83,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,13 +191,24 @@ 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 null; + } else { + return (IBroadcastScope) basicScope; + } +} + + @Override public void streamBroadcastStart(IBroadcastStream stream) { IConnection conn = Red5.getConnectionLocal(); super.streamBroadcastStart(stream); log.info("streamBroadcastStart " + stream.getPublishedName() + " " + System.currentTimeMillis() + " " + conn.getScope().getName()); - if (recordVideoStream) { + if (recordVideoStream && !stream.getPublishedName().contains("/")) { recordStream(stream); VideoStreamListener listener = new VideoStreamListener(); listener.setEventRecordingService(recordingService); @@ -180,7 +220,15 @@ public class VideoApplication extends MultiThreadedApplicationAdapter { private Long genTimestamp() { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); } - + + private boolean isH263Stream(ISubscriberStream stream) { + String streamName = stream.getBroadcastStreamPublishName(); + if(streamName.startsWith(H263Converter.H263PREFIX)) { + return true; + } + return false; + } + @Override public void streamBroadcastClose(IBroadcastStream stream) { super.streamBroadcastClose(stream); @@ -209,6 +257,11 @@ public class VideoApplication extends MultiThreadedApplicationAdapter { event.put("eventName", "StopWebcamShareEvent"); recordingService.record(scopeName, event); } + + if(h263Converters.containsKey(stream.getName())) { + // Stop converter + h263Converters.remove(stream.getName()).stopConverter(); + } } /** @@ -237,5 +290,126 @@ public class VideoApplication extends MultiThreadedApplicationAdapter { public void setEventRecordingService(EventRecordingService s) { 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(); + } + } + } + } + } } diff --git a/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/FFmpegCommand.java b/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/FFmpegCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..a73ed2f18eff92960f16f9010c57d4dc7004a299 --- /dev/null +++ b/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/FFmpegCommand.java @@ -0,0 +1,124 @@ +package org.bigbluebutton.app.video.h263; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; + +public class FFmpegCommand { + private HashMap args; + private HashMap x264Params; + + private String[] command; + + private String ffmpegPath; + private String input; + private String output; + + public FFmpegCommand() { + this.args = new HashMap(); + this.x264Params = new HashMap(); + + 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); + + 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); + } +} \ No newline at end of file diff --git a/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/H263Converter.java b/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/H263Converter.java new file mode 100644 index 0000000000000000000000000000000000000000..cc8ec15ce56fda015e578139ba9e4b45f4e7b2d5 --- /dev/null +++ b/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/H263Converter.java @@ -0,0 +1,104 @@ +package org.bigbluebutton.app.video.h263; + +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; + + FFmpegCommand ffmpeg; + 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"); + + 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; + } + } +} \ No newline at end of file diff --git a/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/ProcessMonitor.java b/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/ProcessMonitor.java new file mode 100644 index 0000000000000000000000000000000000000000..f885eb8581f25c8194bb33e5490c689b464ea188 --- /dev/null +++ b/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/ProcessMonitor.java @@ -0,0 +1,105 @@ +package org.bigbluebutton.app.video.h263; + +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(); + } + } +} \ No newline at end of file diff --git a/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/ProcessStream.java b/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/ProcessStream.java new file mode 100644 index 0000000000000000000000000000000000000000..58b1d62e5465cc065c8570fbd5e4588c0169bff8 --- /dev/null +++ b/bbb-video/src/main/java/org/bigbluebutton/app/video/h263/ProcessStream.java @@ -0,0 +1,59 @@ +package org.bigbluebutton.app.video.h263; + +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"); + } + } +} \ No newline at end of file diff --git a/bbb-video/src/main/webapp/WEB-INF/bigbluebutton-video.properties b/bbb-video/src/main/webapp/WEB-INF/bigbluebutton-video.properties index c5307457dda037c95c2db8386ff6693d42214715..8249912773f3c1bf909371b4f1ce299e8ce9d5f4 100755 --- a/bbb-video/src/main/webapp/WEB-INF/bigbluebutton-video.properties +++ b/bbb-video/src/main/webapp/WEB-INF/bigbluebutton-video.properties @@ -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 diff --git a/bbb-video/src/main/webapp/WEB-INF/red5-web.xml b/bbb-video/src/main/webapp/WEB-INF/red5-web.xml index 569c06d0f7bde28c4130de9c2afbcc0872e5fc47..6a0dab5018e24a1a0c7d0462ecb9bacb32a2cacc 100755 --- a/bbb-video/src/main/webapp/WEB-INF/red5-web.xml +++ b/bbb-video/src/main/webapp/WEB-INF/red5-web.xml @@ -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"> diff --git a/bigbluebutton-apps/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java b/bigbluebutton-apps/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java new file mode 100644 index 0000000000000000000000000000000000000000..9d6d8ffdc46186cbf75979aa4378f8caa8ef2ce4 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/name/fraser/neil/plaintext/diff_match_patch.java @@ -0,0 +1,2408 @@ +/* + * Diff Match and Patch + * + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-diff-match-patch/ + * + * 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 name.fraser.neil.plaintext; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/* + * Functions for diff, match and patch. + * Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Class containing the diff, match and patch methods. + * Also contains the behaviour settings. + */ +public class diff_match_patch { + + // Defaults. + // Set these on your diff_match_patch instance to override the defaults. + + /** + * Number of seconds to map a diff before giving up (0 for infinity). + */ + public float Diff_Timeout = 1.0f; + /** + * Cost of an empty edit operation in terms of edit characters. + */ + public short Diff_EditCost = 4; + /** + * The size beyond which the double-ended diff activates. + * Double-ending is twice as fast, but less accurate. + */ + public short Diff_DualThreshold = 32; + /** + * At what point is no match declared (0.0 = perfection, 1.0 = very loose). + */ + public float Match_Threshold = 0.5f; + /** + * How far to search for a match (0 = exact location, 1000+ = broad match). + * A match this many characters away from the expected location will add + * 1.0 to the score (0.0 is a perfect match). + */ + public int Match_Distance = 1000; + /** + * When deleting a large block of text (over ~64 characters), how close does + * the contents have to match the expected contents. (0.0 = perfection, + * 1.0 = very loose). Note that Match_Threshold controls how closely the + * end points of a delete need to match. + */ + public float Patch_DeleteThreshold = 0.5f; + /** + * Chunk size for context length. + */ + public short Patch_Margin = 4; + + /** + * The number of bits in an int. + */ + private int Match_MaxBits = 32; + + /** + * Internal class for returning results from diff_linesToChars(). + * Other less paranoid languages just use a three-element array. + */ + protected static class LinesToCharsResult { + protected String chars1; + protected String chars2; + protected List<String> lineArray; + + protected LinesToCharsResult(String chars1, String chars2, + List<String> lineArray) { + this.chars1 = chars1; + this.chars2 = chars2; + this.lineArray = lineArray; + } + } + + + // DIFF FUNCTIONS + + + /** + * The data structure representing a diff is a Linked list of Diff objects: + * {Diff(Operation.DELETE, "Hello"), Diff(Operation.INSERT, "Goodbye"), + * Diff(Operation.EQUAL, " world.")} + * which means: delete "Hello", add "Goodbye" and keep " world." + */ + public enum Operation { + DELETE, INSERT, EQUAL + } + + + /** + * Find the differences between two texts. + * Run a faster slightly less optimal diff + * This method allows the 'checklines' of diff_main() to be optional. + * Most of the time checklines is wanted, so default to true. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return Linked List of Diff objects. + */ + public LinkedList<Diff> diff_main(String text1, String text2) { + return diff_main(text1, text2, true); + } + + /** + * Find the differences between two texts. Simplifies the problem by + * stripping any common prefix or suffix off the texts before diffing. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff + * @return Linked List of Diff objects. + */ + public LinkedList<Diff> diff_main(String text1, String text2, + boolean checklines) { + // Check for equality (speedup) + LinkedList<Diff> diffs; + if (text1.equals(text2)) { + diffs = new LinkedList<Diff>(); + diffs.add(new Diff(Operation.EQUAL, text1)); + return diffs; + } + + // Trim off common prefix (speedup) + int commonlength = diff_commonPrefix(text1, text2); + String commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup) + commonlength = diff_commonSuffix(text1, text2); + String commonsuffix = text1.substring(text1.length() - commonlength); + text1 = text1.substring(0, text1.length() - commonlength); + text2 = text2.substring(0, text2.length() - commonlength); + + // Compute the diff on the middle block + diffs = diff_compute(text1, text2, checklines); + + // Restore the prefix and suffix + if (commonprefix.length() != 0) { + diffs.addFirst(new Diff(Operation.EQUAL, commonprefix)); + } + if (commonsuffix.length() != 0) { + diffs.addLast(new Diff(Operation.EQUAL, commonsuffix)); + } + + diff_cleanupMerge(diffs); + return diffs; + } + + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @param checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster slightly less optimal diff + * @return Linked List of Diff objects. + */ + protected LinkedList<Diff> diff_compute(String text1, String text2, + boolean checklines) { + LinkedList<Diff> diffs = new LinkedList<Diff>(); + + if (text1.length() == 0) { + // Just add some text (speedup) + diffs.add(new Diff(Operation.INSERT, text2)); + return diffs; + } + + if (text2.length() == 0) { + // Just delete some text (speedup) + diffs.add(new Diff(Operation.DELETE, text1)); + return diffs; + } + + String longtext = text1.length() > text2.length() ? text1 : text2; + String shorttext = text1.length() > text2.length() ? text2 : text1; + int i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup) + Operation op = (text1.length() > text2.length()) ? + Operation.DELETE : Operation.INSERT; + diffs.add(new Diff(op, longtext.substring(0, i))); + diffs.add(new Diff(Operation.EQUAL, shorttext)); + diffs.add(new Diff(op, longtext.substring(i + shorttext.length()))); + return diffs; + } + longtext = shorttext = null; // Garbage collect. + + // Check to see if the problem can be split in two. + String[] hm = diff_halfMatch(text1, text2); + if (hm != null) { + // A half-match was found, sort out the return data. + String text1_a = hm[0]; + String text1_b = hm[1]; + String text2_a = hm[2]; + String text2_b = hm[3]; + String mid_common = hm[4]; + // Send both pairs off for separate processing. + LinkedList<Diff> diffs_a = diff_main(text1_a, text2_a, checklines); + LinkedList<Diff> diffs_b = diff_main(text1_b, text2_b, checklines); + // Merge the results. + diffs = diffs_a; + diffs.add(new Diff(Operation.EQUAL, mid_common)); + diffs.addAll(diffs_b); + return diffs; + } + + // Perform a real diff. + if (checklines && (text1.length() < 100 || text2.length() < 100)) { + checklines = false; // Too trivial for the overhead. + } + List<String> linearray = null; + if (checklines) { + // Scan the text on a line-by-line basis first. + LinesToCharsResult b = diff_linesToChars(text1, text2); + text1 = b.chars1; + text2 = b.chars2; + linearray = b.lineArray; + } + + diffs = diff_map(text1, text2); + if (diffs == null) { + // No acceptable result. + diffs = new LinkedList<Diff>(); + diffs.add(new Diff(Operation.DELETE, text1)); + diffs.add(new Diff(Operation.INSERT, text2)); + } + + if (checklines) { + // Convert the diff back to original text. + diff_charsToLines(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.add(new Diff(Operation.EQUAL, "")); + int count_delete = 0; + int count_insert = 0; + String text_delete = ""; + String text_insert = ""; + ListIterator<Diff> pointer = diffs.listIterator(); + Diff thisDiff = pointer.next(); + while (thisDiff != null) { + switch (thisDiff.operation) { + case INSERT: + count_insert++; + text_insert += thisDiff.text; + break; + case DELETE: + count_delete++; + text_delete += thisDiff.text; + break; + case EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + pointer.previous(); + for (int j = 0; j < count_delete + count_insert; j++) { + pointer.previous(); + pointer.remove(); + } + for (Diff newDiff : diff_main(text_delete, text_insert, false)) { + pointer.add(newDiff); + } + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + break; + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + diffs.removeLast(); // Remove the dummy entry at the end. + } + return diffs; + } + + + /** + * Split two texts into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text1 First string. + * @param text2 Second string. + * @return An object containing the encoded text1, the encoded text2 and + * the List of unique strings. The zeroth element of the List of + * unique strings is intentionally blank. + */ + protected LinesToCharsResult diff_linesToChars(String text1, String text2) { + List<String> lineArray = new ArrayList<String>(); + Map<String, Integer> lineHash = new HashMap<String, Integer>(); + // e.g. linearray[4] == "Hello\n" + // e.g. linehash.get("Hello\n") == 4 + + // "\x00" is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray.add(""); + + String chars1 = diff_linesToCharsMunge(text1, lineArray, lineHash); + String chars2 = diff_linesToCharsMunge(text2, lineArray, lineHash); + return new LinesToCharsResult(chars1, chars2, lineArray); + } + + + /** + * Split a text into a list of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param text String to encode. + * @param lineArray List of unique strings. + * @param lineHash Map of strings to indices. + * @return Encoded string. + */ + private String diff_linesToCharsMunge(String text, List<String> lineArray, + Map<String, Integer> lineHash) { + int lineStart = 0; + int lineEnd = -1; + String line; + StringBuilder chars = new StringBuilder(); + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + while (lineEnd < text.length() - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length() - 1; + } + line = text.substring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (lineHash.containsKey(line)) { + chars.append(String.valueOf((char) (int) lineHash.get(line))); + } else { + lineArray.add(line); + lineHash.put(line, lineArray.size() - 1); + chars.append(String.valueOf((char) (lineArray.size() - 1))); + } + } + return chars.toString(); + } + + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param diffs LinkedList of Diff objects. + * @param lineArray List of unique strings. + */ + protected void diff_charsToLines(LinkedList<Diff> diffs, + List<String> lineArray) { + StringBuilder text; + for (Diff diff : diffs) { + text = new StringBuilder(); + for (int y = 0; y < diff.text.length(); y++) { + text.append(lineArray.get(diff.text.charAt(y))); + } + diff.text = text.toString(); + } + } + + + /** + * Explore the intersection points between the two texts. + * @param text1 Old string to be diffed. + * @param text2 New string to be diffed. + * @return LinkedList of Diff objects or null if no diff available. + */ + protected LinkedList<Diff> diff_map(String text1, String text2) { + long ms_end = System.currentTimeMillis() + (long) (Diff_Timeout * 1000); + // Cache the text lengths to prevent multiple calls. + int text1_length = text1.length(); + int text2_length = text2.length(); + int max_d = text1_length + text2_length - 1; + boolean doubleEnd = Diff_DualThreshold * 2 < max_d; + List<Set<Long>> v_map1 = new ArrayList<Set<Long>>(); + List<Set<Long>> v_map2 = new ArrayList<Set<Long>>(); + Map<Integer, Integer> v1 = new HashMap<Integer, Integer>(); + Map<Integer, Integer> v2 = new HashMap<Integer, Integer>(); + v1.put(1, 0); + v2.put(1, 0); + int x, y; + Long footstep = 0L; // Used to track overlapping paths. + Map<Long, Integer> footsteps = new HashMap<Long, Integer>(); + boolean done = false; + // If the total number of characters is odd, then the front path will + // collide with the reverse path. + boolean front = ((text1_length + text2_length) % 2 == 1); + for (int d = 0; d < max_d; d++) { + // Bail out if timeout reached. + if (Diff_Timeout > 0 && System.currentTimeMillis() > ms_end) { + return null; + } + + // Walk the front path one step. + v_map1.add(new HashSet<Long>()); // Adds at index 'd'. + for (int k = -d; k <= d; k += 2) { + if (k == -d || k != d && v1.get(k - 1) < v1.get(k + 1)) { + x = v1.get(k + 1); + } else { + x = v1.get(k - 1) + 1; + } + y = x - k; + if (doubleEnd) { + footstep = diff_footprint(x, y); + if (front && (footsteps.containsKey(footstep))) { + done = true; + } + if (!front) { + footsteps.put(footstep, d); + } + } + while (!done && x < text1_length && y < text2_length + && text1.charAt(x) == text2.charAt(y)) { + x++; + y++; + if (doubleEnd) { + footstep = diff_footprint(x, y); + if (front && (footsteps.containsKey(footstep))) { + done = true; + } + if (!front) { + footsteps.put(footstep, d); + } + } + } + v1.put(k, x); + v_map1.get(d).add(diff_footprint(x, y)); + if (x == text1_length && y == text2_length) { + // Reached the end in single-path mode. + return diff_path1(v_map1, text1, text2); + } else if (done) { + // Front path ran over reverse path. + v_map2 = v_map2.subList(0, footsteps.get(footstep) + 1); + LinkedList<Diff> a = diff_path1(v_map1, text1.substring(0, x), + text2.substring(0, y)); + a.addAll(diff_path2(v_map2, text1.substring(x), text2.substring(y))); + return a; + } + } + + if (doubleEnd) { + // Walk the reverse path one step. + v_map2.add(new HashSet<Long>()); // Adds at index 'd'. + for (int k = -d; k <= d; k += 2) { + if (k == -d || k != d && v2.get(k - 1) < v2.get(k + 1)) { + x = v2.get(k + 1); + } else { + x = v2.get(k - 1) + 1; + } + y = x - k; + footstep = diff_footprint(text1_length - x, text2_length - y); + if (!front && (footsteps.containsKey(footstep))) { + done = true; + } + if (front) { + footsteps.put(footstep, d); + } + while (!done && x < text1_length && y < text2_length + && text1.charAt(text1_length - x - 1) + == text2.charAt(text2_length - y - 1)) { + x++; + y++; + footstep = diff_footprint(text1_length - x, text2_length - y); + if (!front && (footsteps.containsKey(footstep))) { + done = true; + } + if (front) { + footsteps.put(footstep, d); + } + } + v2.put(k, x); + v_map2.get(d).add(diff_footprint(x, y)); + if (done) { + // Reverse path ran over front path. + v_map1 = v_map1.subList(0, footsteps.get(footstep) + 1); + LinkedList<Diff> a + = diff_path1(v_map1, text1.substring(0, text1_length - x), + text2.substring(0, text2_length - y)); + a.addAll(diff_path2(v_map2, text1.substring(text1_length - x), + text2.substring(text2_length - y))); + return a; + } + } + } + } + // Number of diffs equals number of characters, no commonality at all. + return null; + } + + + /** + * Work from the middle back to the start to determine the path. + * @param v_map List of path sets. + * @param text1 Old string fragment to be diffed. + * @param text2 New string fragment to be diffed. + * @return LinkedList of Diff objects. + */ + protected LinkedList<Diff> diff_path1(List<Set<Long>> v_map, + String text1, String text2) { + LinkedList<Diff> path = new LinkedList<Diff>(); + int x = text1.length(); + int y = text2.length(); + Operation last_op = null; + for (int d = v_map.size() - 2; d >= 0; d--) { + while (true) { + if (v_map.get(d).contains(diff_footprint(x - 1, y))) { + x--; + if (last_op == Operation.DELETE) { + path.getFirst().text = text1.charAt(x) + path.getFirst().text; + } else { + path.addFirst(new Diff(Operation.DELETE, + text1.substring(x, x + 1))); + } + last_op = Operation.DELETE; + break; + } else if (v_map.get(d).contains(diff_footprint(x, y - 1))) { + y--; + if (last_op == Operation.INSERT) { + path.getFirst().text = text2.charAt(y) + path.getFirst().text; + } else { + path.addFirst(new Diff(Operation.INSERT, + text2.substring(y, y + 1))); + } + last_op = Operation.INSERT; + break; + } else { + x--; + y--; + assert (text1.charAt(x) == text2.charAt(y)) + : "No diagonal. Can't happen. (diff_path1)"; + if (last_op == Operation.EQUAL) { + path.getFirst().text = text1.charAt(x) + path.getFirst().text; + } else { + path.addFirst(new Diff(Operation.EQUAL, text1.substring(x, x + 1))); + } + last_op = Operation.EQUAL; + } + } + } + return path; + } + + + /** + * Work from the middle back to the end to determine the path. + * @param v_map List of path sets. + * @param text1 Old string fragment to be diffed. + * @param text2 New string fragment to be diffed. + * @return LinkedList of Diff objects. + */ + protected LinkedList<Diff> diff_path2(List<Set<Long>> v_map, + String text1, String text2) { + LinkedList<Diff> path = new LinkedList<Diff>(); + int x = text1.length(); + int y = text2.length(); + Operation last_op = null; + for (int d = v_map.size() - 2; d >= 0; d--) { + while (true) { + if (v_map.get(d).contains(diff_footprint(x - 1, y))) { + x--; + if (last_op == Operation.DELETE) { + path.getLast().text += text1.charAt(text1.length() - x - 1); + } else { + path.addLast(new Diff(Operation.DELETE, + text1.substring(text1.length() - x - 1, text1.length() - x))); + } + last_op = Operation.DELETE; + break; + } else if (v_map.get(d).contains(diff_footprint(x, y - 1))) { + y--; + if (last_op == Operation.INSERT) { + path.getLast().text += text2.charAt(text2.length() - y - 1); + } else { + path.addLast(new Diff(Operation.INSERT, + text2.substring(text2.length() - y - 1, text2.length() - y))); + } + last_op = Operation.INSERT; + break; + } else { + x--; + y--; + assert (text1.charAt(text1.length() - x - 1) + == text2.charAt(text2.length() - y - 1)) + : "No diagonal. Can't happen. (diff_path2)"; + if (last_op == Operation.EQUAL) { + path.getLast().text += text1.charAt(text1.length() - x - 1); + } else { + path.addLast(new Diff(Operation.EQUAL, + text1.substring(text1.length() - x - 1, text1.length() - x))); + } + last_op = Operation.EQUAL; + } + } + } + return path; + } + + + /** + * Compute a good hash of two integers. + * @param x First int. + * @param y Second int. + * @return A long made up of both ints. + */ + protected long diff_footprint(int x, int y) { + // The maximum size for a long is 9,223,372,036,854,775,807 + // The maximum size for an int is 2,147,483,647 + // Two ints fit nicely in one long. + long result = x; + result = result << 32; + result += y; + return result; + } + + + /** + * Determine the common prefix of two strings + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the start of each string. + */ + public int diff_commonPrefix(String text1, String text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + int n = Math.min(text1.length(), text2.length()); + for (int i = 0; i < n; i++) { + if (text1.charAt(i) != text2.charAt(i)) { + return i; + } + } + return n; + } + + + /** + * Determine the common suffix of two strings + * @param text1 First string. + * @param text2 Second string. + * @return The number of characters common to the end of each string. + */ + public int diff_commonSuffix(String text1, String text2) { + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + int text1_length = text1.length(); + int text2_length = text2.length(); + int n = Math.min(text1_length, text2_length); + for (int i = 1; i <= n; i++) { + if (text1.charAt(text1_length - i) != text2.charAt(text2_length - i)) { + return i - 1; + } + } + return n; + } + + + /** + * Do the two texts share a substring which is at least half the length of + * the longer text? + * @param text1 First string. + * @param text2 Second string. + * @return Five element String array, containing the prefix of text1, the + * suffix of text1, the prefix of text2, the suffix of text2 and the + * common middle. Or null if there was no match. + */ + protected String[] diff_halfMatch(String text1, String text2) { + String longtext = text1.length() > text2.length() ? text1 : text2; + String shorttext = text1.length() > text2.length() ? text2 : text1; + if (longtext.length() < 10 || shorttext.length() < 1) { + return null; // Pointless. + } + + // First check if the second quarter is the seed for a half-match. + String[] hm1 = diff_halfMatchI(longtext, shorttext, + (longtext.length() + 3) / 4); + // Check again based on the third quarter. + String[] hm2 = diff_halfMatchI(longtext, shorttext, + (longtext.length() + 1) / 2); + String[] hm; + if (hm1 == null && hm2 == null) { + return null; + } else if (hm2 == null) { + hm = hm1; + } else if (hm1 == null) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length() > hm2[4].length() ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + if (text1.length() > text2.length()) { + return hm; + //return new String[]{hm[0], hm[1], hm[2], hm[3], hm[4]}; + } else { + return new String[]{hm[2], hm[3], hm[0], hm[1], hm[4]}; + } + } + + + /** + * Does a substring of shorttext exist within longtext such that the + * substring is at least half the length of longtext? + * @param longtext Longer string. + * @param shorttext Shorter string. + * @param i Start index of quarter length substring within longtext. + * @return Five element String array, containing the prefix of longtext, the + * suffix of longtext, the prefix of shorttext, the suffix of shorttext + * and the common middle. Or null if there was no match. + */ + private String[] diff_halfMatchI(String longtext, String shorttext, int i) { + // Start with a 1/4 length substring at position i as a seed. + String seed = longtext.substring(i, i + longtext.length() / 4); + int j = -1; + String best_common = ""; + String best_longtext_a = "", best_longtext_b = ""; + String best_shorttext_a = "", best_shorttext_b = ""; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + int prefixLength = diff_commonPrefix(longtext.substring(i), + shorttext.substring(j)); + int suffixLength = diff_commonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length() < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length() >= longtext.length() / 2) { + return new String[]{best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common}; + } else { + return null; + } + } + + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupSemantic(LinkedList<Diff> diffs) { + if (diffs.isEmpty()) { + return; + } + boolean changes = false; + Stack<Diff> equalities = new Stack<Diff>(); // Stack of qualities. + String lastequality = null; // Always equal to equalities.lastElement().text + ListIterator<Diff> pointer = diffs.listIterator(); + // Number of characters that changed prior to the equality. + int length_changes1 = 0; + // Number of characters that changed after the equality. + int length_changes2 = 0; + Diff thisDiff = pointer.next(); + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // equality found + equalities.push(thisDiff); + length_changes1 = length_changes2; + length_changes2 = 0; + lastequality = thisDiff.text; + } else { + // an insertion or deletion + length_changes2 += thisDiff.text.length(); + if (lastequality != null && (lastequality.length() <= length_changes1) + && (lastequality.length() <= length_changes2)) { + //System.out.println("Splitting: '" + lastequality + "'"); + // Walk back to offending equality. + while (thisDiff != equalities.lastElement()) { + thisDiff = pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.set(new Diff(Operation.DELETE, lastequality)); + // Insert a corresponding an insert. + pointer.add(new Diff(Operation.INSERT, lastequality)); + + equalities.pop(); // Throw away the equality we just deleted. + if (!equalities.empty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.empty()) { + // There are no previous equalities, walk back to the start. + while (pointer.hasPrevious()) { + pointer.previous(); + } + } else { + // There is a safe equality we can fall back to. + thisDiff = equalities.lastElement(); + while (thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + } + + length_changes1 = 0; // Reset the counters. + length_changes2 = 0; + lastequality = null; + changes = true; + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + + if (changes) { + diff_cleanupMerge(diffs); + } + diff_cleanupSemanticLossless(diffs); + } + + + /** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupSemanticLossless(LinkedList<Diff> diffs) { + String equality1, edit, equality2; + String commonString; + int commonOffset; + int score, bestScore; + String bestEquality1, bestEdit, bestEquality2; + // Create a new iterator at the start. + ListIterator<Diff> pointer = diffs.listIterator(); + Diff prevDiff = pointer.hasNext() ? pointer.next() : null; + Diff thisDiff = pointer.hasNext() ? pointer.next() : null; + Diff nextDiff = pointer.hasNext() ? pointer.next() : null; + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff.operation == Operation.EQUAL && + nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + equality1 = prevDiff.text; + edit = thisDiff.text; + equality2 = nextDiff.text; + + // First, shift the edit as far left as possible. + commonOffset = diff_commonSuffix(equality1, edit); + if (commonOffset != 0) { + commonString = edit.substring(edit.length() - commonOffset); + equality1 = equality1.substring(0, equality1.length() - commonOffset); + edit = commonString + edit.substring(0, edit.length() - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + bestScore = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + while (edit.length() != 0 && equality2.length() != 0 + && edit.charAt(0) == equality2.charAt(0)) { + equality1 += edit.charAt(0); + edit = edit.substring(1) + equality2.charAt(0); + equality2 = equality2.substring(1); + score = diff_cleanupSemanticScore(equality1, edit) + + diff_cleanupSemanticScore(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (!prevDiff.text.equals(bestEquality1)) { + // We have an improvement, save it back to the diff. + if (bestEquality1.length() != 0) { + prevDiff.text = bestEquality1; + } else { + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + pointer.next(); // Walk past nextDiff. + } + thisDiff.text = bestEdit; + if (bestEquality2.length() != 0) { + nextDiff.text = bestEquality2; + } else { + pointer.remove(); // Delete nextDiff. + nextDiff = thisDiff; + thisDiff = prevDiff; + } + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? pointer.next() : null; + } + } + + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 5 (best) to 0 (worst). + * @param one First string. + * @param two Second string. + * @return The score. + */ + private int diff_cleanupSemanticScore(String one, String two) { + if (one.length() == 0 || two.length() == 0) { + // Edges are the best. + return 5; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + int score = 0; + // One point for non-alphanumeric. + if (!Character.isLetterOrDigit(one.charAt(one.length() - 1)) + || !Character.isLetterOrDigit(two.charAt(0))) { + score++; + // Two points for whitespace. + if (Character.isWhitespace(one.charAt(one.length() - 1)) + || Character.isWhitespace(two.charAt(0))) { + score++; + // Three points for line breaks. + if (Character.getType(one.charAt(one.length() - 1)) == Character.CONTROL + || Character.getType(two.charAt(0)) == Character.CONTROL) { + score++; + // Four points for blank lines. + if (BLANKLINEEND.matcher(one).find() + || BLANKLINESTART.matcher(two).find()) { + score++; + } + } + } + } + return score; + } + + + private Pattern BLANKLINEEND + = Pattern.compile("\\n\\r?\\n\\Z", Pattern.DOTALL); + private Pattern BLANKLINESTART + = Pattern.compile("\\A\\r?\\n\\r?\\n", Pattern.DOTALL); + + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupEfficiency(LinkedList<Diff> diffs) { + if (diffs.isEmpty()) { + return; + } + boolean changes = false; + Stack<Diff> equalities = new Stack<Diff>(); // Stack of equalities. + String lastequality = null; // Always equal to equalities.lastElement().text + ListIterator<Diff> pointer = diffs.listIterator(); + // Is there an insertion operation before the last equality. + boolean pre_ins = false; + // Is there a deletion operation before the last equality. + boolean pre_del = false; + // Is there an insertion operation after the last equality. + boolean post_ins = false; + // Is there a deletion operation after the last equality. + boolean post_del = false; + Diff thisDiff = pointer.next(); + Diff safeDiff = thisDiff; // The last Diff that is known to be unsplitable. + while (thisDiff != null) { + if (thisDiff.operation == Operation.EQUAL) { + // equality found + if (thisDiff.text.length() < Diff_EditCost && (post_ins || post_del)) { + // Candidate found. + equalities.push(thisDiff); + pre_ins = post_ins; + pre_del = post_del; + lastequality = thisDiff.text; + } else { + // Not a candidate, and can never become one. + equalities.clear(); + lastequality = null; + safeDiff = thisDiff; + } + post_ins = post_del = false; + } else { + // an insertion or deletion + if (thisDiff.operation == Operation.DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del> + * <ins>A</ins>X<ins>C</ins><del>D</del> + * <ins>A</ins><del>B</del>X<ins>C</ins> + * <ins>A</del>X<ins>C</ins><del>D</del> + * <ins>A</ins><del>B</del>X<del>C</del> + */ + if (lastequality != null + && ((pre_ins && pre_del && post_ins && post_del) + || ((lastequality.length() < Diff_EditCost / 2) + && ((pre_ins ? 1 : 0) + (pre_del ? 1 : 0) + + (post_ins ? 1 : 0) + (post_del ? 1 : 0)) == 3))) { + //System.out.println("Splitting: '" + lastequality + "'"); + // Walk back to offending equality. + while (thisDiff != equalities.lastElement()) { + thisDiff = pointer.previous(); + } + pointer.next(); + + // Replace equality with a delete. + pointer.set(new Diff(Operation.DELETE, lastequality)); + // Insert a corresponding an insert. + pointer.add(thisDiff = new Diff(Operation.INSERT, lastequality)); + + equalities.pop(); // Throw away the equality we just deleted. + lastequality = null; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalities.clear(); + safeDiff = thisDiff; + } else { + if (!equalities.empty()) { + // Throw away the previous equality (it needs to be reevaluated). + equalities.pop(); + } + if (equalities.empty()) { + // There are no previous questionable equalities, + // walk back to the last known safe diff. + thisDiff = safeDiff; + } else { + // There is an equality we can fall back to. + thisDiff = equalities.lastElement(); + } + while (thisDiff != pointer.previous()) { + // Intentionally empty loop. + } + post_ins = post_del = false; + } + + changes = true; + } + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + + if (changes) { + diff_cleanupMerge(diffs); + } + } + + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param diffs LinkedList of Diff objects. + */ + public void diff_cleanupMerge(LinkedList<Diff> diffs) { + diffs.add(new Diff(Operation.EQUAL, "")); // Add a dummy entry at the end. + ListIterator<Diff> pointer = diffs.listIterator(); + int count_delete = 0; + int count_insert = 0; + String text_delete = ""; + String text_insert = ""; + Diff thisDiff = pointer.next(); + Diff prevEqual = null; + int commonlength; + while (thisDiff != null) { + switch (thisDiff.operation) { + case INSERT: + count_insert++; + text_insert += thisDiff.text; + prevEqual = null; + break; + case DELETE: + count_delete++; + text_delete += thisDiff.text; + prevEqual = null; + break; + case EQUAL: + if (count_delete != 0 || count_insert != 0) { + // Delete the offending records. + pointer.previous(); // Reverse direction. + while (count_delete-- > 0) { + pointer.previous(); + pointer.remove(); + } + while (count_insert-- > 0) { + pointer.previous(); + pointer.remove(); + } + if (count_delete != 0 && count_insert != 0) { + // Factor out any common prefixies. + commonlength = diff_commonPrefix(text_insert, text_delete); + if (commonlength != 0) { + if (pointer.hasPrevious()) { + thisDiff = pointer.previous(); + assert thisDiff.operation == Operation.EQUAL + : "Previous diff should have been an equality."; + thisDiff.text += text_insert.substring(0, commonlength); + pointer.next(); + } else { + pointer.add(new Diff(Operation.EQUAL, + text_insert.substring(0, commonlength))); + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = diff_commonSuffix(text_insert, text_delete); + if (commonlength != 0) { + thisDiff = pointer.next(); + thisDiff.text = text_insert.substring(text_insert.length() + - commonlength) + thisDiff.text; + text_insert = text_insert.substring(0, text_insert.length() + - commonlength); + text_delete = text_delete.substring(0, text_delete.length() + - commonlength); + pointer.previous(); + } + } + // Insert the merged records. + if (text_delete.length() != 0) { + pointer.add(new Diff(Operation.DELETE, text_delete)); + } + if (text_insert.length() != 0) { + pointer.add(new Diff(Operation.INSERT, text_insert)); + } + // Step forward to the equality. + thisDiff = pointer.hasNext() ? pointer.next() : null; + } else if (prevEqual != null) { + // Merge this equality with the previous one. + prevEqual.text += thisDiff.text; + pointer.remove(); + thisDiff = pointer.previous(); + pointer.next(); // Forward direction + } + count_insert = 0; + count_delete = 0; + text_delete = ""; + text_insert = ""; + prevEqual = thisDiff; + break; + } + thisDiff = pointer.hasNext() ? pointer.next() : null; + } + // System.out.println(diff); + if (diffs.getLast().text.length() == 0) { + diffs.removeLast(); // Remove the dummy entry at the end. + } + + /* + * Second pass: look for single edits surrounded on both sides by equalities + * which can be shifted sideways to eliminate an equality. + * e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC + */ + boolean changes = false; + // Create a new iterator at the start. + // (As opposed to walking the current one back.) + pointer = diffs.listIterator(); + Diff prevDiff = pointer.hasNext() ? pointer.next() : null; + thisDiff = pointer.hasNext() ? pointer.next() : null; + Diff nextDiff = pointer.hasNext() ? pointer.next() : null; + // Intentionally ignore the first and last element (don't need checking). + while (nextDiff != null) { + if (prevDiff.operation == Operation.EQUAL && + nextDiff.operation == Operation.EQUAL) { + // This is a single edit surrounded by equalities. + if (thisDiff.text.endsWith(prevDiff.text)) { + // Shift the edit over the previous equality. + thisDiff.text = prevDiff.text + + thisDiff.text.substring(0, thisDiff.text.length() + - prevDiff.text.length()); + nextDiff.text = prevDiff.text + nextDiff.text; + pointer.previous(); // Walk past nextDiff. + pointer.previous(); // Walk past thisDiff. + pointer.previous(); // Walk past prevDiff. + pointer.remove(); // Delete prevDiff. + pointer.next(); // Walk past thisDiff. + thisDiff = pointer.next(); // Walk past nextDiff. + nextDiff = pointer.hasNext() ? pointer.next() : null; + changes = true; + } else if (thisDiff.text.startsWith(nextDiff.text)) { + // Shift the edit over the next equality. + prevDiff.text += nextDiff.text; + thisDiff.text = thisDiff.text.substring(nextDiff.text.length()) + + nextDiff.text; + pointer.remove(); // Delete nextDiff. + nextDiff = pointer.hasNext() ? pointer.next() : null; + changes = true; + } + } + prevDiff = thisDiff; + thisDiff = nextDiff; + nextDiff = pointer.hasNext() ? pointer.next() : null; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + diff_cleanupMerge(diffs); + } + } + + + /** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. "The cat" vs "The big cat", 1->1, 5->8 + * @param diffs LinkedList of Diff objects. + * @param loc Location within text1. + * @return Location within text2. + */ + public int diff_xIndex(LinkedList<Diff> diffs, int loc) { + int chars1 = 0; + int chars2 = 0; + int last_chars1 = 0; + int last_chars2 = 0; + Diff lastDiff = null; + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + // Equality or deletion. + chars1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + // Equality or insertion. + chars2 += aDiff.text.length(); + } + if (chars1 > loc) { + // Overshot the location. + lastDiff = aDiff; + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + if (lastDiff != null && lastDiff.operation == Operation.DELETE) { + // The location was deleted. + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); + } + + + /** + * Convert a Diff list into a pretty HTML report. + * @param diffs LinkedList of Diff objects. + * @return HTML representation. + */ + public String diff_prettyHtml(LinkedList<Diff> diffs) { + StringBuilder html = new StringBuilder(); + int i = 0; + for (Diff aDiff : diffs) { + String text = aDiff.text.replace("&", "&").replace("<", "<") + .replace(">", ">").replace("\n", "¶<BR>"); + switch (aDiff.operation) { + case INSERT: + html.append("<INS STYLE=\"background:#E6FFE6;\" TITLE=\"i=").append(i) + .append("\">").append(text).append("</INS>"); + break; + case DELETE: + html.append("<DEL STYLE=\"background:#FFE6E6;\" TITLE=\"i=").append(i) + .append("\">").append(text).append("</DEL>"); + break; + case EQUAL: + html.append("<SPAN TITLE=\"i=").append(i).append("\">").append(text) + .append("</SPAN>"); + break; + } + if (aDiff.operation != Operation.DELETE) { + i += aDiff.text.length(); + } + } + return html.toString(); + } + + + /** + * Compute and return the source text (all equalities and deletions). + * @param diffs LinkedList of Diff objects. + * @return Source text. + */ + public String diff_text1(LinkedList<Diff> diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.INSERT) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + + /** + * Compute and return the destination text (all equalities and insertions). + * @param diffs LinkedList of Diff objects. + * @return Destination text. + */ + public String diff_text2(LinkedList<Diff> diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + if (aDiff.operation != Operation.DELETE) { + text.append(aDiff.text); + } + } + return text.toString(); + } + + + /** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param diffs LinkedList of Diff objects. + * @return Number of changes. + */ + public int diff_levenshtein(LinkedList<Diff> diffs) { + int levenshtein = 0; + int insertions = 0; + int deletions = 0; + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + insertions += aDiff.text.length(); + break; + case DELETE: + deletions += aDiff.text.length(); + break; + case EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; + } + + + /** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param diffs Array of diff tuples. + * @return Delta text. + */ + public String diff_toDelta(LinkedList<Diff> diffs) { + StringBuilder text = new StringBuilder(); + for (Diff aDiff : diffs) { + switch (aDiff.operation) { + case INSERT: + try { + text.append("+").append(URLEncoder.encode(aDiff.text, "UTF-8") + .replace('+', ' ')).append("\t"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } + break; + case DELETE: + text.append("-").append(aDiff.text.length()).append("\t"); + break; + case EQUAL: + text.append("=").append(aDiff.text.length()).append("\t"); + break; + } + } + String delta = text.toString(); + if (delta.length() != 0) { + // Strip off trailing tab character. + delta = delta.substring(0, delta.length() - 1); + delta = unescapeForEncodeUriCompatability(delta); + } + return delta; + } + + + /** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param text1 Source string for the diff. + * @param delta Delta text. + * @return Array of diff tuples or null if invalid. + * @throws IllegalArgumentException If invalid input. + */ + public LinkedList<Diff> diff_fromDelta(String text1, String delta) + throws IllegalArgumentException { + LinkedList<Diff> diffs = new LinkedList<Diff>(); + int pointer = 0; // Cursor in text1 + String[] tokens = delta.split("\t"); + for (String token : tokens) { + if (token.length() == 0) { + // Blank tokens are ok (from a trailing \t). + continue; + } + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + String param = token.substring(1); + switch (token.charAt(0)) { + case '+': + // decode would change all "+" to " " + param = param.replace("+", "%2B"); + try { + param = URLDecoder.decode(param, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in diff_fromDelta: " + param, e); + } + diffs.add(new Diff(Operation.INSERT, param)); + break; + case '-': + // Fall through. + case '=': + int n; + try { + n = Integer.parseInt(param); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid number in diff_fromDelta: " + param, e); + } + if (n < 0) { + throw new IllegalArgumentException( + "Negative number in diff_fromDelta: " + param); + } + String text; + try { + text = text1.substring(pointer, pointer += n); + } catch (StringIndexOutOfBoundsException e) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") larger than source text length (" + text1.length() + + ").", e); + } + if (token.charAt(0) == '=') { + diffs.add(new Diff(Operation.EQUAL, text)); + } else { + diffs.add(new Diff(Operation.DELETE, text)); + } + break; + default: + // Anything else is an error. + throw new IllegalArgumentException( + "Invalid diff operation in diff_fromDelta: " + token.charAt(0)); + } + } + if (pointer != text1.length()) { + throw new IllegalArgumentException("Delta length (" + pointer + + ") smaller than source text length (" + text1.length() + ")."); + } + return diffs; + } + + + // MATCH FUNCTIONS + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + public int match_main(String text, String pattern, int loc) { + loc = Math.max(0, Math.min(loc, text.length())); + if (text.equals(pattern)) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (text.length() == 0) { + // Nothing to match. + return -1; + } else if (loc + pattern.length() <= text.length() + && text.substring(loc, loc + pattern.length()).equals(pattern)) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return match_bitap(text, pattern, loc); + } + } + + + /** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. Returns -1 if no match found. + * @param text The text to search. + * @param pattern The pattern to search for. + * @param loc The location to search around. + * @return Best match index or -1. + */ + protected int match_bitap(String text, String pattern, int loc) { + assert (Match_MaxBits == 0 || pattern.length() <= Match_MaxBits) + : "Pattern too long for this application."; + + // Initialise the alphabet. + Map<Character, Integer> s = match_alphabet(pattern); + + // Highest score beyond which we give up. + double score_threshold = Match_Threshold; + // Is there a nearby exact match? (speedup) + int best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length()); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore(0, best_loc, loc, pattern), + score_threshold); + } + } + + // Initialise the bit arrays. + int matchmask = 1 << (pattern.length() - 1); + best_loc = -1; + + int bin_min, bin_mid; + int bin_max = pattern.length() + text.length(); + // Empty initialization added to appease Java compiler. + int[] last_rd = new int[0]; + for (int d = 0; d < pattern.length(); d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at + // this error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore(d, loc + bin_mid, loc, pattern) + <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = (bin_max - bin_min) / 2 + bin_min; + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + int start = Math.max(1, loc - bin_mid + 1); + int finish = Math.min(loc + bin_mid, text.length()) + pattern.length(); + + int[] rd = new int[finish + 2]; + rd[finish + 1] = (1 << d) - 1; + for (int j = finish; j >= start; j--) { + int charMatch; + if (text.length() <= j - 1 || !s.containsKey(text.charAt(j - 1))) { + // Out of range. + charMatch = 0; + } else { + charMatch = s.get(text.charAt(j - 1)); + } + if (d == 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch + | (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | last_rd[j + 1]; + } + if ((rd[j] & matchmask) != 0) { + double score = match_bitapScore(d, j - 1, loc, pattern); + // This match will almost certainly be better than any existing + // match. But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + if (match_bitapScore(d + 1, loc, loc, pattern) > score_threshold) { + // No hope for a (better) match at greater error levels. + break; + } + last_rd = rd; + } + return best_loc; + } + + + /** + * Compute and return the score for a match with e errors and x location. + * @param e Number of errors in match. + * @param x Location of match. + * @param loc Expected location of match. + * @param pattern Pattern being sought. + * @return Overall score for match (0.0 = good, 1.0 = bad). + */ + private double match_bitapScore(int e, int x, int loc, String pattern) { + float accuracy = (float) e / pattern.length(); + int proximity = Math.abs(loc - x); + if (Match_Distance == 0) { + // Dodge divide by zero error. + return proximity == 0 ? accuracy : 1.0; + } + return accuracy + (proximity / (float) Match_Distance); + } + + + /** + * Initialise the alphabet for the Bitap algorithm. + * @param pattern The text to encode. + * @return Hash of character locations. + */ + protected Map<Character, Integer> match_alphabet(String pattern) { + Map<Character, Integer> s = new HashMap<Character, Integer>(); + char[] char_pattern = pattern.toCharArray(); + for (char c : char_pattern) { + s.put(c, 0); + } + int i = 0; + for (char c : char_pattern) { + s.put(c, s.get(c) | (1 << (pattern.length() - i - 1))); + i++; + } + return s; + } + + + // PATCH FUNCTIONS + + + /** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param patch The patch to grow. + * @param text Source text. + */ + protected void patch_addContext(Patch patch, String text) { + if (text.length() == 0) { + return; + } + String pattern = text.substring(patch.start2, patch.start2 + patch.length1); + int padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) + && pattern.length() < Match_MaxBits - Patch_Margin - Patch_Margin) { + padding += Patch_Margin; + pattern = text.substring(Math.max(0, patch.start2 - padding), + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + } + // Add one chunk for good luck. + padding += Patch_Margin; + + // Add the prefix. + String prefix = text.substring(Math.max(0, patch.start2 - padding), + patch.start2); + if (prefix.length() != 0) { + patch.diffs.addFirst(new Diff(Operation.EQUAL, prefix)); + } + // Add the suffix. + String suffix = text.substring(patch.start2 + patch.length1, + Math.min(text.length(), patch.start2 + patch.length1 + padding)); + if (suffix.length() != 0) { + patch.diffs.addLast(new Diff(Operation.EQUAL, suffix)); + } + + // Roll back the start points. + patch.start1 -= prefix.length(); + patch.start2 -= prefix.length(); + // Extend the lengths. + patch.length1 += prefix.length() + suffix.length(); + patch.length2 += prefix.length() + suffix.length(); + } + + + /** + * Compute a list of patches to turn text1 into text2. + * A set of diffs will be computed. + * @param text1 Old text. + * @param text2 New text. + * @return LinkedList of Patch objects. + */ + public LinkedList<Patch> patch_make(String text1, String text2) { + // No diffs provided, compute our own. + LinkedList<Diff> diffs = diff_main(text1, text2, true); + if (diffs.size() > 2) { + diff_cleanupSemantic(diffs); + diff_cleanupEfficiency(diffs); + } + return patch_make(text1, diffs); + } + + + /** + * Compute a list of patches to turn text1 into text2. + * text1 will be derived from the provided diffs. + * @param diffs Array of diff tuples for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList<Patch> patch_make(LinkedList<Diff> diffs) { + // No origin string provided, compute our own. + String text1 = diff_text1(diffs); + return patch_make(text1, diffs); + } + + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is ignored, diffs are the delta between text1 and text2. + * @param text1 Old text + * @param text2 Ignored. + * @param diffs Array of diff tuples for text1 to text2. + * @return LinkedList of Patch objects. + * @deprecated Prefer patch_make(String text1, LinkedList<Diff> diffs). + */ + public LinkedList<Patch> patch_make(String text1, String text2, + LinkedList<Diff> diffs) { + return patch_make(text1, diffs); + } + + + /** + * Compute a list of patches to turn text1 into text2. + * text2 is not provided, diffs are the delta between text1 and text2. + * @param text1 Old text. + * @param diffs Array of diff tuples for text1 to text2. + * @return LinkedList of Patch objects. + */ + public LinkedList<Patch> patch_make(String text1, LinkedList<Diff> diffs) { + LinkedList<Patch> patches = new LinkedList<Patch>(); + if (diffs.isEmpty()) { + return patches; // Get rid of the null case. + } + Patch patch = new Patch(); + int char_count1 = 0; // Number of characters into the text1 string. + int char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + String prepatch_text = text1; + String postpatch_text = text1; + for (Diff aDiff : diffs) { + if (patch.diffs.isEmpty() && aDiff.operation != Operation.EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (aDiff.operation) { + case INSERT: + patch.diffs.add(aDiff); + patch.length2 += aDiff.text.length(); + postpatch_text = postpatch_text.substring(0, char_count2) + + aDiff.text + postpatch_text.substring(char_count2); + break; + case DELETE: + patch.length1 += aDiff.text.length(); + patch.diffs.add(aDiff); + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + aDiff.text.length()); + break; + case EQUAL: + if (aDiff.text.length() <= 2 * Patch_Margin + && !patch.diffs.isEmpty() && aDiff != diffs.getLast()) { + // Small equality inside a patch. + patch.diffs.add(aDiff); + patch.length1 += aDiff.text.length(); + patch.length2 += aDiff.text.length(); + } + + if (aDiff.text.length() >= 2 * Patch_Margin) { + // Time for a new patch. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + patch = new Patch(); + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (aDiff.operation != Operation.INSERT) { + char_count1 += aDiff.text.length(); + } + if (aDiff.operation != Operation.DELETE) { + char_count2 += aDiff.text.length(); + } + } + // Pick up the leftover patch if not empty. + if (!patch.diffs.isEmpty()) { + patch_addContext(patch, prepatch_text); + patches.add(patch); + } + + return patches; + } + + + /** + * Given an array of patches, return another array that is identical. + * @param patches Array of patch objects. + * @return Array of patch objects. + */ + public LinkedList<Patch> patch_deepCopy(LinkedList<Patch> patches) { + LinkedList<Patch> patchesCopy = new LinkedList<Patch>(); + for (Patch aPatch : patches) { + Patch patchCopy = new Patch(); + for (Diff aDiff : aPatch.diffs) { + Diff diffCopy = new Diff(aDiff.operation, aDiff.text); + patchCopy.diffs.add(diffCopy); + } + patchCopy.start1 = aPatch.start1; + patchCopy.start2 = aPatch.start2; + patchCopy.length1 = aPatch.length1; + patchCopy.length2 = aPatch.length2; + patchesCopy.add(patchCopy); + } + return patchesCopy; + } + + + /** + * Merge a set of patches onto the text. Return a patched text, as well + * as an array of true/false values indicating which patches were applied. + * @param patches Array of patch objects + * @param text Old text. + * @return Two element Object array, containing the new text and an array of + * boolean values. + */ + public Object[] patch_apply(LinkedList<Patch> patches, String text) { + if (patches.isEmpty()) { + return new Object[]{text, new boolean[0]}; + } + + // Deep copy the patches so that no changes are made to originals. + patches = patch_deepCopy(patches); + + String nullPadding = patch_addPadding(patches); + text = nullPadding + text + nullPadding; + patch_splitMax(patches); + + int x = 0; + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + int delta = 0; + boolean[] results = new boolean[patches.size()]; + for (Patch aPatch : patches) { + int expected_loc = aPatch.start2 + delta; + String text1 = diff_text1(aPatch.diffs); + int start_loc; + int end_loc = -1; + if (text1.length() > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = match_main(text, + text1.substring(0, this.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = match_main(text, + text1.substring(text1.length() - this.Match_MaxBits), + expected_loc + text1.length() - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= aPatch.length2 - aPatch.length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + String text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, + Math.min(start_loc + text1.length(), text.length())); + } else { + text2 = text.substring(start_loc, + Math.min(end_loc + this.Match_MaxBits, text.length())); + } + if (text1.equals(text2)) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + diff_text2(aPatch.diffs) + + text.substring(start_loc + text1.length()); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + LinkedList<Diff> diffs = diff_main(text1, text2, false); + if (text1.length() > this.Match_MaxBits + && diff_levenshtein(diffs) / (float) text1.length() + > this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + diff_cleanupSemanticLossless(diffs); + int index1 = 0; + for (Diff aDiff : aPatch.diffs) { + if (aDiff.operation != Operation.EQUAL) { + int index2 = diff_xIndex(diffs, index1); + if (aDiff.operation == Operation.INSERT) { + // Insertion + text = text.substring(0, start_loc + index2) + aDiff.text + + text.substring(start_loc + index2); + } else if (aDiff.operation == Operation.DELETE) { + // Deletion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + diff_xIndex(diffs, + index1 + aDiff.text.length())); + } + } + if (aDiff.operation != Operation.DELETE) { + index1 += aDiff.text.length(); + } + } + } + } + } + x++; + } + // Strip the padding off. + text = text.substring(nullPadding.length(), text.length() + - nullPadding.length()); + return new Object[]{text, results}; + } + + + /** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param patches Array of patch objects. + * @return The padding string added to each side. + */ + public String patch_addPadding(LinkedList<Patch> patches) { + int paddingLength = this.Patch_Margin; + String nullPadding = ""; + for (int x = 1; x <= paddingLength; x++) { + nullPadding += String.valueOf((char) x); + } + + // Bump all the patches forward. + for (Patch aPatch : patches) { + aPatch.start1 += paddingLength; + aPatch.start2 += paddingLength; + } + + // Add some padding on start of first diff. + Patch patch = patches.getFirst(); + LinkedList<Diff> diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getFirst().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addFirst(new Diff(Operation.EQUAL, nullPadding)); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getFirst().text.length()) { + // Grow first equality. + Diff firstDiff = diffs.getFirst(); + int extraLength = paddingLength - firstDiff.text.length(); + firstDiff.text = nullPadding.substring(firstDiff.text.length()) + + firstDiff.text; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches.getLast(); + diffs = patch.diffs; + if (diffs.isEmpty() || diffs.getLast().operation != Operation.EQUAL) { + // Add nullPadding equality. + diffs.addLast(new Diff(Operation.EQUAL, nullPadding)); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs.getLast().text.length()) { + // Grow last equality. + Diff lastDiff = diffs.getLast(); + int extraLength = paddingLength - lastDiff.text.length(); + lastDiff.text += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; + } + + + /** + * Look through the patches and break up any which are longer than the + * maximum limit of the match algorithm. + * @param patches LinkedList of Patch objects. + */ + public void patch_splitMax(LinkedList<Patch> patches) { + int patch_size; + String precontext, postcontext; + Patch patch; + int start1, start2; + boolean empty; + Operation diff_type; + String diff_text; + ListIterator<Patch> pointer = patches.listIterator(); + Patch bigpatch = pointer.hasNext() ? pointer.next() : null; + while (bigpatch != null) { + if (bigpatch.length1 <= Match_MaxBits) { + bigpatch = pointer.hasNext() ? pointer.next() : null; + continue; + } + // Remove the big old patch. + pointer.remove(); + patch_size = Match_MaxBits; + start1 = bigpatch.start1; + start2 = bigpatch.start2; + precontext = ""; + while (!bigpatch.diffs.isEmpty()) { + // Create one of several smaller patches. + patch = new Patch(); + empty = true; + patch.start1 = start1 - precontext.length(); + patch.start2 = start2 - precontext.length(); + if (precontext.length() != 0) { + patch.length1 = patch.length2 = precontext.length(); + patch.diffs.add(new Diff(Operation.EQUAL, precontext)); + } + while (!bigpatch.diffs.isEmpty() + && patch.length1 < patch_size - Patch_Margin) { + diff_type = bigpatch.diffs.getFirst().operation; + diff_text = bigpatch.diffs.getFirst().text; + if (diff_type == Operation.INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + patch.diffs.addLast(bigpatch.diffs.removeFirst()); + empty = false; + } else if (diff_type == Operation.DELETE && patch.diffs.size() == 1 + && patch.diffs.getFirst().operation == Operation.EQUAL + && diff_text.length() > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + empty = false; + patch.diffs.add(new Diff(diff_type, diff_text)); + bigpatch.diffs.removeFirst(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, Math.min(diff_text.length(), + patch_size - patch.length1 - Patch_Margin)); + patch.length1 += diff_text.length(); + start1 += diff_text.length(); + if (diff_type == Operation.EQUAL) { + patch.length2 += diff_text.length(); + start2 += diff_text.length(); + } else { + empty = false; + } + patch.diffs.add(new Diff(diff_type, diff_text)); + if (diff_text.equals(bigpatch.diffs.getFirst().text)) { + bigpatch.diffs.removeFirst(); + } else { + bigpatch.diffs.getFirst().text = bigpatch.diffs.getFirst().text + .substring(diff_text.length()); + } + } + } + // Compute the head context for the next patch. + precontext = diff_text2(patch.diffs); + precontext = precontext.substring(Math.max(0, precontext.length() + - Patch_Margin)); + // Append the end context for this patch. + if (diff_text1(bigpatch.diffs).length() > Patch_Margin) { + postcontext = diff_text1(bigpatch.diffs).substring(0, Patch_Margin); + } else { + postcontext = diff_text1(bigpatch.diffs); + } + if (postcontext.length() != 0) { + patch.length1 += postcontext.length(); + patch.length2 += postcontext.length(); + if (!patch.diffs.isEmpty() + && patch.diffs.getLast().operation == Operation.EQUAL) { + patch.diffs.getLast().text += postcontext; + } else { + patch.diffs.add(new Diff(Operation.EQUAL, postcontext)); + } + } + if (!empty) { + pointer.add(patch); + } + } + bigpatch = pointer.hasNext() ? pointer.next() : null; + } + } + + + /** + * Take a list of patches and return a textual representation. + * @param patches List of Patch objects. + * @return Text representation of patches. + */ + public String patch_toText(List<Patch> patches) { + StringBuilder text = new StringBuilder(); + for (Patch aPatch : patches) { + text.append(aPatch); + } + return text.toString(); + } + + + /** + * Parse a textual representation of patches and return a List of Patch + * objects. + * @param textline Text representation of patches. + * @return List of Patch objects. + * @throws IllegalArgumentException If invalid input. + */ + public LinkedList<Patch> patch_fromText(String textline) + throws IllegalArgumentException { + LinkedList<Patch> patches = new LinkedList<Patch>(); + if (textline.length() == 0) { + return patches; + } + List<String> textList = Arrays.asList(textline.split("\n")); + LinkedList<String> text = new LinkedList<String>(textList); + Patch patch; + Pattern patchHeader + = Pattern.compile("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$"); + Matcher m; + char sign; + String line; + while (!text.isEmpty()) { + m = patchHeader.matcher(text.getFirst()); + if (!m.matches()) { + throw new IllegalArgumentException( + "Invalid patch string: " + text.getFirst()); + } + patch = new Patch(); + patches.add(patch); + patch.start1 = Integer.parseInt(m.group(1)); + if (m.group(2).length() == 0) { + patch.start1--; + patch.length1 = 1; + } else if (m.group(2).equals("0")) { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = Integer.parseInt(m.group(2)); + } + + patch.start2 = Integer.parseInt(m.group(3)); + if (m.group(4).length() == 0) { + patch.start2--; + patch.length2 = 1; + } else if (m.group(4).equals("0")) { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = Integer.parseInt(m.group(4)); + } + text.removeFirst(); + + while (!text.isEmpty()) { + try { + sign = text.getFirst().charAt(0); + } catch (IndexOutOfBoundsException e) { + // Blank line? Whatever. + text.removeFirst(); + continue; + } + line = text.getFirst().substring(1); + line = line.replace("+", "%2B"); // decode would change all "+" to " " + try { + line = URLDecoder.decode(line, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } catch (IllegalArgumentException e) { + // Malformed URI sequence. + throw new IllegalArgumentException( + "Illegal escape in patch_fromText: " + line, e); + } + if (sign == '-') { + // Deletion. + patch.diffs.add(new Diff(Operation.DELETE, line)); + } else if (sign == '+') { + // Insertion. + patch.diffs.add(new Diff(Operation.INSERT, line)); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.add(new Diff(Operation.EQUAL, line)); + } else if (sign == '@') { + // Start of next patch. + break; + } else { + // WTF? + throw new IllegalArgumentException( + "Invalid patch mode '" + sign + "' in: " + line); + } + text.removeFirst(); + } + } + return patches; + } + + + /** + * Class representing one diff operation. + */ + public static class Diff { + /** + * One of: INSERT, DELETE or EQUAL. + */ + public Operation operation; + /** + * The text associated with this diff operation. + */ + public String text; + + /** + * Constructor. Initializes the diff with the provided values. + * @param operation One of INSERT, DELETE or EQUAL. + * @param text The text being applied. + */ + public Diff(Operation operation, String text) { + // Construct a diff with the specified operation and text. + this.operation = operation; + this.text = text; + } + + + /** + * Display a human-readable version of this Diff. + * @return text version. + */ + public String toString() { + String prettyText = this.text.replace('\n', '\u00b6'); + return "Diff(" + this.operation + ",\"" + prettyText + "\")"; + } + + + /** + * Is this Diff equivalent to another Diff? + * @param d Another Diff to compare against. + * @return true or false. + */ + public boolean equals(Object d) { + try { + return (((Diff) d).operation == this.operation) + && (((Diff) d).text.equals(this.text)); + } catch (ClassCastException e) { + return false; + } + } + } + + + /** + * Class representing one patch operation. + */ + public static class Patch { + public LinkedList<Diff> diffs; + public int start1; + public int start2; + public int length1; + public int length2; + + + /** + * Constructor. Initializes with an empty list of diffs. + */ + public Patch() { + this.diffs = new LinkedList<Diff>(); + } + + + /** + * Emmulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * @return The GNU diff string. + */ + public String toString() { + String coords1, coords2; + if (this.length1 == 0) { + coords1 = this.start1 + ",0"; + } else if (this.length1 == 1) { + coords1 = Integer.toString(this.start1 + 1); + } else { + coords1 = (this.start1 + 1) + "," + this.length1; + } + if (this.length2 == 0) { + coords2 = this.start2 + ",0"; + } else if (this.length2 == 1) { + coords2 = Integer.toString(this.start2 + 1); + } else { + coords2 = (this.start2 + 1) + "," + this.length2; + } + StringBuilder text = new StringBuilder(); + text.append("@@ -").append(coords1).append(" +").append(coords2) + .append(" @@\n"); + // Escape the body of the patch with %xx notation. + for (Diff aDiff : this.diffs) { + switch (aDiff.operation) { + case INSERT: + text.append('+'); + break; + case DELETE: + text.append('-'); + break; + case EQUAL: + text.append(' '); + break; + } + try { + text.append(URLEncoder.encode(aDiff.text, "UTF-8").replace('+', ' ')) + .append("\n"); + } catch (UnsupportedEncodingException e) { + // Not likely on modern system. + throw new Error("This system does not support UTF-8.", e); + } + } + return unescapeForEncodeUriCompatability(text.toString()); + } + } + + + /** + * Unescape selected chars for compatability with JavaScript's encodeURI. + * In speed critical applications this could be dropped since the + * receiving application will certainly decode these fine. + * Note that this function is case-sensitive. Thus "%3f" would not be + * unescaped. But this is ok because it is only called with the output of + * URLEncoder.encode which returns uppercase hex. + * + * Example: "%3F" -> "?", "%24" -> "$", etc. + * + * @param str The string to escape. + * @return The escaped string. + */ + private static String unescapeForEncodeUriCompatability(String str) { + return str.replace("%21", "!").replace("%7E", "~") + .replace("%27", "'").replace("%28", "(").replace("%29", ")") + .replace("%3B", ";").replace("%2F", "/").replace("%3F", "?") + .replace("%3A", ":").replace("%40", "@").replace("%26", "&") + .replace("%3D", "=").replace("%2B", "+").replace("%24", "$") + .replace("%2C", ",").replace("%23", "#"); + } +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/BigBlueButtonApplication.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/BigBlueButtonApplication.java index 2c880d7e71db96fd7104a78902d9db6a17312085..ef1602b6cddc74a32690a647aba53c0d502c747e 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/BigBlueButtonApplication.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/BigBlueButtonApplication.java @@ -91,12 +91,14 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter { @Override public boolean roomStart(IScope room) { + connInvokerService.addScope(room.getName(), room); return super.roomStart(room); } @Override public void roomStop(IScope room) { recorderApplication.destroyRecordSession(room.getName()); + connInvokerService.removeScope(room.getName()); super.roomStop(room); } @@ -126,6 +128,11 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter { lsMap = new HashMap<String, Boolean>(); } } + + Boolean guest = false; + if (params.length >= 9 && ((Boolean) params[9])) { + guest = true; + } if (record == true) { recorderApplication.createRecordSession(room); @@ -134,7 +141,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); @@ -147,6 +154,8 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter { bbbGW.initAudioSettings(room, internalUserID, muted); + boolean success = connInvokerService.addConnection(internalUserID, connection); + String meetingId = bbbSession.getRoom(); String connType = getConnectionType(Red5.getConnectionLocal().getType()); @@ -173,7 +182,7 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter { log.info("User joining bbb-apps: data={}", logStr); - return super.roomConnect(connection, params); + return success && super.roomConnect(connection, params); } @@ -195,6 +204,8 @@ public class BigBlueButtonApplication extends MultiThreadedApplicationAdapter { String clientId = Red5.getConnectionLocal().getClient().getId(); log.info("***** " + APP + "[clientid=" + clientId + "] disconnnected from " + remoteHost + ":" + remotePort + "."); + connInvokerService.removeConnection(getBbbSession().getInternalUserID()); + BigBlueButtonSession bbbSession = (BigBlueButtonSession) Red5.getConnectionLocal().getAttribute(Constants.SESSION); String meetingId = bbbSession.getRoom(); diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/BigBlueButtonSession.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/BigBlueButtonSession.java index 74a1c969e581ee56fc527003a6b6e006c01cd8e1..63550709fa344ad74a7792207d0e9c76a2d5b0dd 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/BigBlueButtonSession.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/BigBlueButtonSession.java @@ -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; + } } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/red5/ConnectionInvokerService.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/red5/ConnectionInvokerService.java index 9988ef9e794f3955fb2725a1639ef42f63455490..2ed65996ae4ba4cb0405949c5dbaa23f8519ff05 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/red5/ConnectionInvokerService.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/red5/ConnectionInvokerService.java @@ -49,17 +49,38 @@ public class ConnectionInvokerService { private BlockingQueue<ClientMessage> messages; + private ConcurrentHashMap<String, IConnection> connections; + private ConcurrentHashMap<String, IScope> scopes; + private volatile boolean sendMessages = false; private IScope bbbAppScope; public ConnectionInvokerService() { messages = new LinkedBlockingQueue<ClientMessage>(); + + connections = new ConcurrentHashMap<String, IConnection>(); + scopes = new ConcurrentHashMap<String, IScope>(); } public void setAppScope(IScope scope) { bbbAppScope = scope; } + public boolean addConnection(String id, IConnection conn) { + if (connections == null) { + System.out.println("Connections is null!!!!"); + return false; + } + if (id == null) { + System.out.println("CONN ID IS NULL!!!"); + + } + if (conn == null) { + System.out.println("CONN IS NULL"); + } + return connections.putIfAbsent(id, conn) == null; + } + public void start() { sendMessages = true; Runnable sender = new Runnable() { @@ -84,6 +105,18 @@ public class ConnectionInvokerService { sendMessages = false; } + public void removeConnection(String id) { + connections.remove(id); + } + + public void addScope(String id, IScope scope) { + scopes.putIfAbsent(id, scope); + } + + public void removeScope(String id) { + scopes.remove(id); + } + public void sendMessage(final ClientMessage message) { messages.offer(message); } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java index c9b530b0ccb3ed5de44d69053a66018c90c0bb78..4b54f3af9cf54e801fec4b357057cc105e723aa9 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/meeting/messaging/redis/MeetingMessageHandler.java @@ -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); diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/Constants.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/Constants.java index 8b324b248cc0f1a6f892d1bb77d3f3e8969c6cf6..a3fca01130ade69bafbbb24832dfee3fbd2401f0 100644 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/Constants.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/Constants.java @@ -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"; } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java index 7e6a0258b97a2f1103c2c9c2dd79d074354b58fa..20b68aa48ba3b2f3c1a58d26e200ab59d0cdef36 100644 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/MessagingConstants.java @@ -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"; diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/RegisterUserMessage.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/RegisterUserMessage.java index 9e7dfe5259048719e9d522f464562a4c9ca2205b..4abbd11d9e683682a894a3405e7b78847e7a99ac 100644 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/RegisterUserMessage.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/messaging/RegisterUserMessage.java @@ -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); } } } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsApplication.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsApplication.java index b926597e124d702b7d8a081516bc696929ac5d8a..5b3aa997b37b8bc14774e03f1984ffae038a90cd 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsApplication.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsApplication.java @@ -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); } @@ -42,16 +34,16 @@ public class ParticipantsApplication { bbbInGW.shareWebcam(meetingId, userId, stream); } - public void unshareWebcam(String meetingId, String userId) { - bbbInGW.unshareWebcam(meetingId, userId); + public void unshareWebcam(String meetingId, String userId, String stream) { + bbbInGW.unshareWebcam(meetingId, userId, stream); } public void setParticipantStatus(String room, String userid, String status, Object value) { 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); + } } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java index d1efd34f65065e307e714e6d2f2846ed7d4d54c2..5c248d73255d81a3fe13c83dd4fffc9ec0e3eb26 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsListener.java @@ -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); - } } } } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsService.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsService.java index 9ed5e49f7a0d63a567528068b4b0c64edc567558..eb7e43ce189b3ff81627d2a418717093a90289ce 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsService.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/participants/ParticipantsService.java @@ -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"); @@ -71,16 +58,24 @@ public class ParticipantsService { application.shareWebcam(scope.getName(), userId, stream); } - public void unshareWebcam() { + public void unshareWebcam(String stream) { IScope scope = Red5.getConnectionLocal().getScope(); String userId = getBbbSession().getInternalUserID(); - application.unshareWebcam(scope.getName(), userId); + application.unshareWebcam(scope.getName(), userId, stream); } 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); + } } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/ConversionUpdatesProcessor.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/ConversionUpdatesProcessor.java index 1cf384454cb40b5bfeea14feeadbc19bd657ff97..78c58fa1023f7249ca43181de434980d7c57f227 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/ConversionUpdatesProcessor.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/ConversionUpdatesProcessor.java @@ -19,7 +19,8 @@ package org.bigbluebutton.conference.service.presentation; import org.slf4j.Logger; -import org.red5.logging.Red5LoggerFactory; +import org.red5.logging.Red5LoggerFactory; + public class ConversionUpdatesProcessor { private static Logger log = Red5LoggerFactory.getLogger(ConversionUpdatesProcessor.class, "bigbluebutton"); @@ -48,9 +49,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) { diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/PresentationApplication.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/PresentationApplication.java index c1562c9abb22201e8bee76f83dca3eac099fc966..93255376a33c087c742c6fefd53d32fef6cc857a 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/PresentationApplication.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/PresentationApplication.java @@ -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){ diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/PresentationMessageListener.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/PresentationMessageListener.java index fd530e56000e44fa606718eed72429be53af4adc..49fc232b4755bdb91b8e23cb6f689efc310a6276 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/PresentationMessageListener.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/presentation/PresentationMessageListener.java @@ -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); } } } diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/GuestAskToEnterRecordEvent.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/GuestAskToEnterRecordEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..fd281079e82e87a2a744b0ea50efd7de7395a048 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/GuestAskToEnterRecordEvent.java @@ -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); + } + + +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/GuestPolicyEvent.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/GuestPolicyEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..404fca774787ca7c3baf814a2630c786976db490 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/GuestPolicyEvent.java @@ -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); + } + +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/ModeratorResponseEvent.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/ModeratorResponseEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..41891923fe04ce3a6924f10a7614f81a8ed6e687 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/ModeratorResponseEvent.java @@ -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()); + } + + +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/ParticipantRoleChangeRecordEvent.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/ParticipantRoleChangeRecordEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..6248b53e1d21f9bc643dd906d90cc20ebe2ea617 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/ParticipantRoleChangeRecordEvent.java @@ -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); + } +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/WaitingForModeratorEvent.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/WaitingForModeratorEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..39917a486bb11f10b9eee402e37f9ed054e3f089 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/recorder/participants/WaitingForModeratorEvent.java @@ -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); + } + +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesApplication.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..e51afbc150dfaf3e60d5e4e55024e834ca487e84 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesApplication.java @@ -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); + } +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesHandler.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..80ef5c2be1a33031508303e26049b8db68028546 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesHandler.java @@ -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; + } +} \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesService.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesService.java new file mode 100644 index 0000000000000000000000000000000000000000..f7acc4e2853b72ca4a64c68ff1a628c867d67cbb --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/sharednotes/SharedNotesService.java @@ -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; + } +} + diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoApplication.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..b26822165ce5e595b43e6e2d5d1dfaba2e747765 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoApplication.java @@ -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; + } +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoHandler.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..e134c67944573ec9d0bbd0998a3ac252f8a3f049 --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoHandler.java @@ -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; + } +} diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoService.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoService.java new file mode 100644 index 0000000000000000000000000000000000000000..f2300a9f43dd7fe49fcbd993b29cb64a751c0c8b --- /dev/null +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/conference/service/video/VideoService.java @@ -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; + } +} \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java b/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java index 6ec2dfd2f62bee2a3e165caa8ab859610033bb5c..de1bc6d806fdec871f58c518ae0bc2e5e566c5f7 100755 --- a/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java +++ b/bigbluebutton-apps/src/main/java/org/bigbluebutton/core/api/IBigBlueButtonInGW.java @@ -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); + 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); + } diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala index e3fd0232c389b5aaa51bafa7bb6d077caa906bea..a360550d2f7a40b186765e0a48086134121146cc 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/BigBlueButtonInGW.scala @@ -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]) { @@ -162,14 +163,19 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen bbbGW.accept(new UserShareWebcam(meetingId, userId, stream)) } - def unshareWebcam(meetingId: String, userId: String) { - bbbGW.accept(new UserUnshareWebcam(meetingId, userId)) + def unshareWebcam(meetingId: String, userId: String, stream:String) { + bbbGW.accept(new UserUnshareWebcam(meetingId, userId, stream)) } def setUserStatus(meetingID: String, userID: String, status: String, value: Object):Unit = { 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)) } @@ -201,6 +207,26 @@ class BigBlueButtonInGW(bbbGW: BigBlueButtonGateway, presUtil: PreuploadedPresen // but it's not used anywhere. That's why we pass voiceConf twice instead 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)); + } } diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala index 56e93c2f7abb76dfa88a95ee4d8db1d092902b3f..764ede98a20e7c2e780fbacc42bafadce3dbce00 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/CollectorActor.scala @@ -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(Constants.WEBCAM_STREAM, user.webcamStream) + 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 *****************") diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala index 139bacded8d32ae25fd6c425a07a9a56589c2b24..96dd63e40b910ab637d829aa32d3dea912fde6da 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/MeetingActor.scala @@ -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 @@ -247,6 +261,16 @@ class MeetingActor(val meetingID: String, val externalMeetingID: String, val mee private def handleGetRecordingStatus(msg: GetRecordingStatus) { 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) diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/Constants.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/Constants.scala index 1457829ac29bae9b22f53d499cc7277af7e3c6d3..6e2a62b12c7540a2f2ac4ce18376ae3336d5e3c6 100644 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/Constants.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/Constants.scala @@ -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" +} diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala index 0e56ac370e74596db05d101320e4e6bf5f87f454..fc3314e0461a28f8b91d850db1dd381a212c33f1 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala @@ -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( @@ -126,7 +128,8 @@ case class UserShareWebcam( case class UserUnshareWebcam( meetingID: String, - userId: String + userId: String, + stream: String ) extends InMessage case class ChangeUserStatus( @@ -136,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, @@ -187,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, @@ -511,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 diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala index e837c32b793842cd6d8b21a8b78b16c19649ab46..0ece2de127a3ef6a740bfc24f5ceb6bceac3b0c1 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/MessageNames.scala @@ -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" } \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala index f36e420de2d74b06a6e6db527468cd8da00779f3..20bf3b27dc630cc098fe65222641432efb3ece04 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/OutMessages.scala @@ -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 diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala index b314bb2532d247f43174eeee877197d95a621298..7e7197c9de15ee5a1655039087a945ed53126568 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/api/ValueObjects.scala @@ -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,11 +75,13 @@ case class UserVO( externUserID: String, name: String, role: Role.Role, - raiseHand: Boolean, + guest: Boolean, + waitingForAcceptance: Boolean, + mood: String, presenter: Boolean, hasStream: Boolean, locked: Boolean, - webcamStream: String, + webcamStreams: Set[String], phoneUser: Boolean, voiceUser: VoiceUser, listenOnly: 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) diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/layout/LayoutApp.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/layout/LayoutApp.scala index 9fbc137593c2d09d0bc13b7e12a1d6aa1cf230ce..40fa2d723961d8b6a957a7c67893955a322e6658 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/layout/LayoutApp.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/layout/LayoutApp.scala @@ -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, diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/layout/red5/LayoutClientMessageSender.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/layout/red5/LayoutClientMessageSender.scala index 9f1820498c362508273e6517d8f8a7e9ff7e52eb..190167883f8ae228613d8c8a3e876a0ce0da9362 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/layout/red5/LayoutClientMessageSender.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/layout/red5/LayoutClientMessageSender.scala @@ -30,10 +30,10 @@ class LayoutClientMessageSender(service: ConnectionInvokerService) extends OutMe private def handleBroadcastLayoutEvent(msg: BroadcastLayoutEvent) { val message = new java.util.HashMap[String, Object]() message.put("locked", msg.locked:java.lang.Boolean); - message.put("setByUserID", msg.setByUserID); + message.put("setByUserID", msg.requesterID); message.put("layout", msg.layoutID); - msg.applyTo foreach {u => + msg.applyTo.filter(_.userID != msg.requesterID) foreach {u => var m = new DirectClientMessage(msg.meetingID, u.userID, "syncLayout", message); service.sendMessage(m); } diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/PresentationModel.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/PresentationModel.scala index 7f96eb4a09e71aa047e01f54aef33efe73a00c43..f8ca4f581c245d014f089b7206237ac4206a8d15 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/PresentationModel.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/PresentationModel.scala @@ -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 = "", diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/red5/PresentationClientMessageSender.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/red5/PresentationClientMessageSender.scala index fc7ec4bb29a58ced3fb2d27826ffd899b74b66e3..907492888e6ea6b960a20ac15ea1248509c7fe57 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/red5/PresentationClientMessageSender.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/red5/PresentationClientMessageSender.scala @@ -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]() diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/redis/PesentationMessageToJsonConverter.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/redis/PesentationMessageToJsonConverter.scala index e6833b11a0cbc13f3cb3270ac6889a459de0f644..dbf11419aba2156ef019ce6167b123d5cb169fd0 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/redis/PesentationMessageToJsonConverter.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/presentation/redis/PesentationMessageToJsonConverter.scala @@ -64,7 +64,8 @@ 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]]() pres.pages.values foreach {p => @@ -120,7 +121,8 @@ 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]]() msg.presentation.pages.values foreach {p => @@ -204,7 +206,8 @@ 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 => pages.add(pageToMap(p)) @@ -225,7 +228,8 @@ 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 => pages.add(pageToMap(p)) @@ -246,7 +250,8 @@ 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]]() msg.current.pages.values foreach {p => diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/SharedNotesApp.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/SharedNotesApp.scala new file mode 100644 index 0000000000000000000000000000000000000000..85d581011693cd5681cfd95369bd7609ffd0fc4f --- /dev/null +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/SharedNotesApp.scala @@ -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) + } + } + } +} \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/SharedNotesInGateway.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/SharedNotesInGateway.scala new file mode 100755 index 0000000000000000000000000000000000000000..475ff0ed6d5774b69bcd6467442af5773ac052c5 --- /dev/null +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/SharedNotesInGateway.scala @@ -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)); + } +} \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/red5/SharedNotesClientMessageSender.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/red5/SharedNotesClientMessageSender.scala new file mode 100755 index 0000000000000000000000000000000000000000..3e102dba64cacd5c1029c574c0ab3b581885922b --- /dev/null +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/sharednotes/red5/SharedNotesClientMessageSender.scala @@ -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); + } +} \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala old mode 100755 new mode 100644 index 00a8b336b2f1a7a9a475f2494755e4f6a52a577f..53334d73a55923b0191fbccff2ed0872d98da4f1 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/UsersApp.scala @@ -6,6 +6,7 @@ import org.bigbluebutton.core.User import java.util.ArrayList import org.bigbluebutton.core.MeetingActor import scala.collection.mutable.ArrayBuffer +import scala.collection.immutable.ListSet trait UsersApp { this : MeetingActor => @@ -96,7 +97,7 @@ trait UsersApp { /** * Send a reply to BigBlueButtonActor to let it know this MeetingActor hasn't hung! * Sometimes, the actor seems to hang and doesn't anymore accept messages. This is a simple - * audit to check whether the actor is still alive. (ralam feb 25, 2015) + * audit to check whether the actor is still alive. (ralam feb 25, 2015) */ reply(new ValidateAuthTokenReply(meetingID, msg.userId, msg.token, false, msg.correlationId)) } @@ -107,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)) @@ -207,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) { @@ -241,29 +226,47 @@ trait UsersApp { def handleUserShareWebcam(msg: UserShareWebcam) { users.getUser(msg.userId) foreach {user => - val uvo = user.copy(hasStream=true, webcamStream=msg.stream) + val streams = user.webcamStreams + msg.stream + val uvo = user.copy(hasStream=true, webcamStreams=streams) users.addUser(uvo) - logger.info("User shared webcam: mid=[" + meetingID + "] uid=[" + uvo.userID + "]") + logger.info("User shared webcam: mid=[" + meetingID + "] uid=[" + uvo.userID + "] sharedStream=[" + msg.stream + "] streams=[" + streams + "]") outGW.send(new UserSharedWebcam(meetingID, recorded, uvo.userID, msg.stream)) } } def handleUserunshareWebcam(msg: UserUnshareWebcam) { users.getUser(msg.userId) foreach {user => - val stream = user.webcamStream - val uvo = user.copy(hasStream=false, webcamStream="") + val streams = user.webcamStreams - msg.stream + val uvo = user.copy(hasStream=(!streams.isEmpty), webcamStreams=streams) users.addUser(uvo) - logger.info("User unshared webcam: mid=[" + meetingID + "] uid=[" + uvo.userID + "]") - outGW.send(new UserUnsharedWebcam(meetingID, recorded, uvo.userID, stream)) + logger.info("User unshared webcam: mid=[" + meetingID + "] uid=[" + uvo.userID + "] unsharedStream=[" + msg.stream + "] streams=[" + streams + "]") + outGW.send(new UserUnsharedWebcam(meetingID, recorded, uvo.userID, msg.stream)) } } def handleChangeUserStatus(msg: ChangeUserStatus):Unit = { - if (users.hasUser(msg.userID)) { - outGW.send(new UserStatusChange(meetingID, recorded, msg.userID, msg.status, msg.value)) - } + 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)) } @@ -273,26 +276,33 @@ 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), - webcamStream="", phoneUser=false, vu, listenOnly=false) + 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 + "]") - outGW.send(new UserJoined(meetingID, recorded, uvo)) - - outGW.send(new MeetingState(meetingID, recorded, uvo.userID, permissions, meetingMuted)) - - // Become presenter if the only moderator - if (users.numModerators == 1) { - if (ru.role == Role.MODERATOR) { - assignNewPresenter(msg.userID, ru.name, msg.userID) - } - } - webUserJoined - startRecordingIfAutoStart() + 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() + } } } @@ -306,7 +316,7 @@ trait UsersApp { if (u.presenter) { /* The current presenter has left the meeting. Find a moderator and make * him presenter. This way, if there is a moderator in the meeting, there - * will always be a presenter. + * will always be a presenter. */ val moderator = users.findAModerator() moderator.foreach { mod => @@ -342,8 +352,8 @@ trait UsersApp { val sessionId = "PHONE-" + webUserId; val uvo = new UserVO(webUserId, webUserId, msg.voiceUser.callerName, - Role.VIEWER, raiseHand=false, presenter=false, - hasStream=false, locked=getInitialLockStatus(Role.VIEWER), webcamStream="", + Role.VIEWER, guest=false, waitingForAcceptance=false, mood="", presenter=false, + hasStream=false, locked=getInitialLockStatus(Role.VIEWER), webcamStreams=new ListSet[String](), phoneUser=true, vu, listenOnly=false) users.addUser(uvo) @@ -443,4 +453,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)) + } + } + } + } } diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/red5/UsersClientMessageSender.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/red5/UsersClientMessageSender.scala index 3bd250ca62382bb07a6a79bcc39549a09df799d3..ce0a8b807bc103754f1adcb10a95f717d4385212 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/red5/UsersClientMessageSender.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/red5/UsersClientMessageSender.scala @@ -25,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) @@ -44,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") } @@ -78,11 +80,13 @@ 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) - wuser.put("webcamStream", user.webcamStream) + wuser.put("webcamStream", user.webcamStreams mkString("|")) wuser.put("phoneUser", user.phoneUser:java.lang.Boolean) wuser.put("voiceUser", vuser) wuser.put("listenOnly", user.listenOnly:java.lang.Boolean) @@ -401,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) @@ -448,6 +423,7 @@ class UsersClientMessageSender(service: ConnectionInvokerService) extends OutMes def handleUserUnshareWebcam(msg: UserUnsharedWebcam) { var args = new HashMap[String, Object]() args.put("userId", msg.userID) + args.put("webcamStream", msg.stream) val message = new java.util.HashMap[String, Object]() val gson = new Gson(); @@ -474,7 +450,22 @@ class UsersClientMessageSender(service: ConnectionInvokerService) extends OutMes var m = new BroadcastClientMessage(msg.meetingID, "participantStatusChange", message); 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); @@ -489,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); + } } \ No newline at end of file diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/redis/UsersEventRedisPublisher.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/redis/UsersEventRedisPublisher.scala index 791ffb2d4ece3ee149e84af67413fcc2c4c86c26..e6f38b4f8e5d9b6d9efd20ce65157463f6d7e14b 100755 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/redis/UsersEventRedisPublisher.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/redis/UsersEventRedisPublisher.scala @@ -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) @@ -79,17 +78,12 @@ class UsersEventRedisPublisher(service: MessageSender) extends OutMessageListene val json = UsersMessageToJsonConverter.userStatusChangeToJson(msg) 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) - service.send(MessagingConstants.FROM_USERS_CHANNEL, json) + + private def handleUserRoleChange(msg: UserRoleChange) { + val json = UsersMessageToJsonConverter.userRoleChangeToJson(msg) + service.send(MessagingConstants.FROM_USERS_CHANNEL, json) } - + private def handleUserSharedWebcam(msg: UserSharedWebcam) { val json = UsersMessageToJsonConverter.userSharedWebcamToJson(msg) service.send(MessagingConstants.FROM_USERS_CHANNEL, json) diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/redis/UsersMessageToJsonConverter.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/redis/UsersMessageToJsonConverter.scala index 7296d709c69cd6f5bd5ddfb2dfa05fdf8b0362e9..ae70ada5d8b37fbde94695a615a4126f0fd8ef23 100644 --- a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/redis/UsersMessageToJsonConverter.scala +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/users/redis/UsersMessageToJsonConverter.scala @@ -16,11 +16,13 @@ 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 - wuser += "webcam_stream" -> user.webcamStream + wuser += "webcam_stream" -> user.webcamStreams wuser += "phone_user" -> user.phoneUser wuser += "listenOnly" -> user.listenOnly @@ -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) @@ -159,6 +140,16 @@ object UsersMessageToJsonConverter { val header = Util.buildHeader(MessageNames.USER_STATUS_CHANGED, msg.version, None) 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]() diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/video/VideoApp.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/video/VideoApp.scala new file mode 100644 index 0000000000000000000000000000000000000000..e781c57b709a2884490debaceac6a0e7962765ce --- /dev/null +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/video/VideoApp.scala @@ -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)) + } +} diff --git a/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/video/red5/VideoClientMessageSender.scala b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/video/red5/VideoClientMessageSender.scala new file mode 100644 index 0000000000000000000000000000000000000000..c2ace68352d4a66b29eca9deefaccc911c72dfd6 --- /dev/null +++ b/bigbluebutton-apps/src/main/scala/org/bigbluebutton/core/apps/video/red5/VideoClientMessageSender.scala @@ -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); + } +} diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-sharednotes.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-sharednotes.xml new file mode 100755 index 0000000000000000000000000000000000000000..93fc5181e8260db12b98eceeff3a272fb6cdadd4 --- /dev/null +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-sharednotes.xml @@ -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> diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-video.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-video.xml new file mode 100644 index 0000000000000000000000000000000000000000..516a1023128dbe6b05ff60f1d88927606abec773 --- /dev/null +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bbb-app-video.xml @@ -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> diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/bigbluebutton.properties b/bigbluebutton-apps/src/main/webapp/WEB-INF/bigbluebutton.properties index 1ed292882314d789f2c9aa595c6868ef5852cdee..b79535b20702b69d0f070cc015bfbe048ea48546 100644 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/bigbluebutton.properties +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/bigbluebutton.properties @@ -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= diff --git a/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml b/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml index b90467c89793e68cac8e3cb60cbd4a528295af84..f946e5c278cfa67bbb693fb51977a6d06f2f3e34 100755 --- a/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml +++ b/bigbluebutton-apps/src/main/webapp/WEB-INF/red5-web.xml @@ -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" /> diff --git a/bigbluebutton-client/branding/default/style/css/BBBBlack.css b/bigbluebutton-client/branding/default/style/css/BBBBlack.css index af1050468f696c7d82e06ae9a9ba8736debd3af6..10d9f8d963f4e2a7fb5acba0b3bad44fc5f0f456 100755 --- a/bigbluebutton-client/branding/default/style/css/BBBBlack.css +++ b/bigbluebutton-client/branding/default/style/css/BBBBlack.css @@ -46,7 +46,7 @@ ToolTip { paddingRight: 3; } -Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle { +Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .bandwidthButtonStyle { textIndent: 0; paddingLeft: 10; paddingRight: 10; @@ -643,4 +643,11 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c fontFamily: Arial; fontSize: 20; fontWeight: bold; -} \ No newline at end of file +} + +.bandwidthButtonStyle { + paddingTop: 0; + paddingBottom: 0; + height: 22; + icon: Embed('assets/images/bandwidth.png'); +} diff --git a/bigbluebutton-client/branding/default/style/css/BBBDefault.css b/bigbluebutton-client/branding/default/style/css/BBBDefault.css index 25eaf44f81477f912b7360fc5a86735817b19e62..9d3ed593b3227d03d46454968a81e2716fb61f72 100755 --- a/bigbluebutton-client/branding/default/style/css/BBBDefault.css +++ b/bigbluebutton-client/branding/default/style/css/BBBDefault.css @@ -64,7 +64,42 @@ ToolTip { color: #e1e2e5; } -Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowChangeResolutionCombo, .languageSelectorStyle, .testJavaLinkButtonStyle, .recordButtonStyleNormal, .recordButtonStyleStart, .recordButtonStyleStop, .micSettingsWindowHelpButtonStyle { +.videoMessageWarningLabelStyle { + fontWeight: bold; + fontSize: 18; + fontFamily: Arial; + color: #ffff26; +} + +.videoMessageErrorLabelStyle { + fontWeight: bold; + fontSize: 18; + fontFamily: Arial; + color: #ff2727; +} + +.videoMessageBackgroundStyle { + backgroundAlpha: 0.6; + backgroundColor: #b9babc; +} + +.videoToolbarBackgroundTalkingStyle { + backgroundColor: #20c600; + backgroundAlpha: 0.6; +} + +.videoToolbarBackgroundNotTalkingStyle { + backgroundColor: #42444c; + backgroundAlpha: 0.6; +} + +.videoToolbarLabelStyle { + textIndent: 0; + color: #ffffff; + fontFamily: Arial; +} + +Button, .logoutButtonStyle, .chatSendButtonStyle, .helpLinkButtonStyle, .cameraDisplaySettingsWindowProfileComboStyle, .cameraDisplaySettingsWindowCameraSelector, .languageSelectorStyle, .testJavaLinkButtonStyle, .recordButtonStyleNormal, .recordButtonStyleStart, .recordButtonStyleStop, .micSettingsWindowHelpButtonStyle, .bandwidthButtonStyle, .settingsButtonStyle, .acceptButtonStyle, .denyButtonStyle { textIndent: 0; paddingLeft: 10; paddingRight: 10; @@ -134,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; @@ -250,7 +297,7 @@ DataGrid { .presentationUploadButtonStyle, .presentationBackButtonStyle, .presentationBackButtonDisabledStyle, .presentationForwardButtonStyle, .presentationForwardButtonDisabledStyle, -.presentationFitToWidthButtonStyle, .presentationFitToPageButtonStyle +.presentationFitToWidthButtonStyle, .presentationFitToPageButtonStyle, .presentationDownloadButtonStyle { textIndent: 0; paddingLeft: 10; @@ -270,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 { @@ -297,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; @@ -776,6 +827,27 @@ MDIWindow { /*None of the following properties are overridden by the MDIWindow c disabledSkin: Embed('assets/images/3_minimizeButton.png'); } +/* +https://www.iconfinder.com/icons/172512/mute_icon#size=128 +*/ +.muteOverlayBtn +{ + upSkin: Embed('assets/images/audio_20_white.png'); + overSkin: Embed('assets/images/audio_20_white.png'); + downSkin: Embed('assets/images/audio_20_white.png'); + disabledSkin: Embed('assets/images/audio_20_white.png'); +} + +/* +https://www.iconfinder.com/icons/172499/low_volume_icon#size=128 +*/ +.unmuteOverlayBtn +{ + upSkin: Embed('assets/images/audio_muted_20_white.png'); + overSkin: Embed('assets/images/audio_muted_20_white.png'); + downSkin: Embed('assets/images/audio_muted_20_white.png'); + disabledSkin: Embed('assets/images/audio_muted_20_white.png'); +} .resizeHndlr { @@ -841,6 +913,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'); @@ -860,4 +939,64 @@ Alert { .statusMessageStyle { fontSize: 12; paddingTop: 0; -} \ No newline at end of file +} + +.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'); +} diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/audio_20_white.png b/bigbluebutton-client/branding/default/style/css/assets/images/audio_20_white.png new file mode 100644 index 0000000000000000000000000000000000000000..b337c3ea5863f8b40caa1df013ec034f9a15d34a Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/audio_20_white.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/audio_20_white.xcf b/bigbluebutton-client/branding/default/style/css/assets/images/audio_20_white.xcf new file mode 100644 index 0000000000000000000000000000000000000000..f4b196793398428aac3cfeb58d3a9466d2900b43 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/audio_20_white.xcf differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/audio_muted_20_white.png b/bigbluebutton-client/branding/default/style/css/assets/images/audio_muted_20_white.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf58b88e71bee8b91f37fd380f2ce127dee668c Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/audio_muted_20_white.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/bandwidth.png b/bigbluebutton-client/branding/default/style/css/assets/images/bandwidth.png new file mode 100644 index 0000000000000000000000000000000000000000..58f33a59d663acb039a7d1b5604438e139ee83af Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/bandwidth.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_access_time_black_18dp.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_access_time_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1fd032f2d667900c0a6e4803e4b22f5bbe66c25c Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_access_time_black_18dp.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_add_circle_outline_16px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_add_circle_outline_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..62c8a130134f8cf9fe65888d0f53bc29271f70c4 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_add_circle_outline_16px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_back_24px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_back_24px.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf8af82f846bd0f88de0247b5a578aaabefb28f Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_back_24px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_back_grey_24px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_back_grey_24px.png new file mode 100644 index 0000000000000000000000000000000000000000..99cf9f7c9960e4e1f38d71ef4c748eed5aaaf257 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_back_grey_24px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_forward_24px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_forward_24px.png new file mode 100644 index 0000000000000000000000000000000000000000..d407e3dcdd5d4f051859711b50928e10ec2cfc59 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_forward_24px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_forward_grey_24px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_forward_grey_24px.png new file mode 100644 index 0000000000000000000000000000000000000000..70df2653996db6071dcf7b66b0e967a95228d867 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_arrow_forward_grey_24px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_clear_black_18dp.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_clear_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..082c9bdfd9ca005efdacb9295c9a281fff1c1cf1 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_clear_black_18dp.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_fast_forward_black_18dp.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_fast_forward_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a1394ab3215683ef21c4c993af1bdbfdad663631 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_fast_forward_black_18dp.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_fast_rewind_black_18dp.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_fast_rewind_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..30fdf6e706b346a87914b3ae3c15172ddd15f75c Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_fast_rewind_black_18dp.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_file_download_16px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_file_download_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..0521c6210b4570b403ee4f51d00b10ecae0c3702 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_file_download_16px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_file_upload_16px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_file_upload_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..b9089febd06bc90f08a7fefdf6c4867f70ba165e Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_file_upload_16px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_mood_black_18dp.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_mood_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca438be9b3df3a1c4556dcd2051150df9db8e90 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_mood_black_18dp.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_send_16px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_send_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..61ac36eb4b0508596b250075ee2606e998311727 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_send_16px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_settings_16px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_settings_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..e269e38c5f2fd86d09ba489d3daa76b9f8b2b8cf Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_settings_16px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_swap_vert_16px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_swap_vert_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..27d7ef68261109b4625a34f6571e33c5a41868d3 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_swap_vert_16px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_thumb_down_16px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_thumb_down_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..5f7a7853aceab7b258520a5cdedf91b6e339b62c Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_thumb_down_16px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_thumb_up_16px.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_thumb_up_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..19ec42fc3afbff6747e89f114256abfe6ed360e4 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_thumb_up_16px.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_volume_down_black_18dp.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_volume_down_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b51479f4cacce573c97f5a38522a3d320defe1f4 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_volume_down_black_18dp.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/ic_volume_up_black_18dp.png b/bigbluebutton-client/branding/default/style/css/assets/images/ic_volume_up_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..58f4e270760ae3b7483a86de06bc21453e05dba4 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/ic_volume_up_black_18dp.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-3-high-five.png b/bigbluebutton-client/branding/default/style/css/assets/images/icon-3-high-five.png new file mode 100644 index 0000000000000000000000000000000000000000..4361fea6534359e9b1150d56718b8296101d5c35 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/icon-3-high-five.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-3-high-five.svg b/bigbluebutton-client/branding/default/style/css/assets/images/icon-3-high-five.svg new file mode 100644 index 0000000000000000000000000000000000000000..4c563540df1f21f23244b33d76e89864ded4cb4a --- /dev/null +++ b/bigbluebutton-client/branding/default/style/css/assets/images/icon-3-high-five.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32px" + height="32px" + viewBox="0 0 32 32" + version="1.1" + id="svg2" + inkscape:version="0.48.4 r9939" + sodipodi:docname="icon-3-high-five.svg" + inkscape:export-filename="/var/lib/lxc/bbb-dev090-14/rootfs/home/ubuntu/dev/bigbluebutton/bigbluebutton-client/branding/default/style/css/assets/images/icon-3-high-five.png" + inkscape:export-xdpi="50" + inkscape:export-ydpi="50"> + <metadata + id="metadata15"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>icon 3 high five</dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="753" + inkscape:window-height="480" + id="namedview13" + showgrid="false" + inkscape:zoom="7.375" + inkscape:cx="39.864407" + inkscape:cy="16" + inkscape:window-x="75" + inkscape:window-y="34" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /> + <!-- Generator: Sketch 3.0.3 (7891) - http://www.bohemiancoding.com/sketch --> + <title + id="title4">icon 3 high five</title> + <desc + id="desc6">Created with Sketch.</desc> + <defs + id="defs8" /> + <g + id="Page-1" + stroke="none" + stroke-width="1" + fill="none" + fill-rule="evenodd" + sketch:type="MSPage" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <g + id="icon-3-high-five" + sketch:type="MSArtboardGroup" + fill="#929292" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <path + d="M28.1244203,21.5 C28.1244203,26.1944206 24.3188409,30 19.6244203,30.0000003 C16.5115051,30.0000003 13.2262274,28.5474856 10.9652407,24.4282229 C7.70175208,18.4825159 3.52827319,14.5832077 5.51553361,12.5959473 C6.9371827,11.1742982 9.16926196,12.5381668 11.1244203,14.3667868 L11.1244203,14.3667868 L11.1244203,6.50840855 C11.1244203,5.11541748 12.2437085,4 13.6244203,4 C14.1892809,4 14.7078132,4.18537107 15.1244253,4.49839144 C15.1271484,3.1148807 16.2453908,2 17.6244203,2 C19.014265,2 20.1236329,3.12004027 20.1244199,4.50198455 C20.5422503,4.18708451 21.0616172,4 21.6244203,4 C23.0147583,4 24.1244203,5.12055197 24.1244203,6.50282288 L24.1244203,7.49767249 C24.5422506,7.18504628 25.0616174,7 25.6244203,7 C27.0147583,7 28.1244203,8.11599243 28.1244203,9.49263886 L28.1244203,21.5 L28.1244203,21.5 Z M19.6244203,29 C15.8647052,28.9995418 13.6344162,26.9488875 11.8717958,23.9830936 C7.95978233,17.4007216 5.15828327,14.3887562 6.24545305,13.2957153 C7.35605012,12.1791206 10.0660207,14.5979243 12.1244203,16.7983451 L12.1244203,6.49309635 C12.1244203,5.66388585 12.7959932,5 13.6244203,5 C14.4586231,5 15.1244203,5.66848201 15.1244203,6.49309635 L15.1244203,16 L16.1244203,16 L16.1244203,4.49089813 C16.1244203,3.67622006 16.7959932,3 17.6244203,3 C18.4586231,3 19.1244203,3.66749783 19.1244203,4.49089813 L19.1244203,15 L20.1244203,15 L20.1244203,6.50370994 C20.1244203,5.66197696 20.7959932,5 21.6244203,5 C22.4586231,5 23.1244203,5.67323387 23.1244203,6.50370994 L23.1244203,16 L24.1244203,16 L24.1244203,9.4912653 C24.1244203,8.66254437 24.7959932,8 25.6244203,8 C26.4586231,8 27.1244203,8.66766222 27.1244203,9.4912653 L27.1244203,17.7543674 L27.1244203,21.5 C27.1244203,25.6421358 23.7665562,29 19.6244203,29 L19.6244203,29 Z" + id="high-five" + sketch:type="MSShapeGroup" + style="fill:#000000;stroke:#000000;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-smiling-face.png b/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-smiling-face.png new file mode 100644 index 0000000000000000000000000000000000000000..860284020f261ccd08003396b182237b7699b10d Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-smiling-face.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-smiling-face.svg b/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-smiling-face.svg new file mode 100644 index 0000000000000000000000000000000000000000..ed5064c6a86b3f2a84c2dc382410d75162a469fc --- /dev/null +++ b/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-smiling-face.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32px" + height="32px" + viewBox="0 0 32 32" + version="1.1" + id="svg2" + inkscape:version="0.48.4 r9939" + sodipodi:docname="icon-6-smiling-face.svg" + inkscape:export-filename="/var/lib/lxc/bbb-dev090-14/rootfs/home/ubuntu/dev/bigbluebutton/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-smiling-face.png" + inkscape:export-xdpi="50" + inkscape:export-ydpi="50"> + <metadata + id="metadata15"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>icon 6 smiling face</dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="753" + inkscape:window-height="480" + id="namedview13" + showgrid="false" + inkscape:zoom="7.375" + inkscape:cx="16" + inkscape:cy="16" + inkscape:window-x="75" + inkscape:window-y="34" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /> + <!-- Generator: Sketch 3.0.3 (7891) - http://www.bohemiancoding.com/sketch --> + <title + id="title4">icon 6 smiling face</title> + <desc + id="desc6">Created with Sketch.</desc> + <defs + id="defs8" /> + <g + id="Page-1" + stroke="none" + stroke-width="1" + fill="none" + fill-rule="evenodd" + sketch:type="MSPage" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <g + id="icon-6-smiling-face" + sketch:type="MSArtboardGroup" + fill="#929292" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <path + d="M16.5,29 C23.4035597,29 29,23.4035597 29,16.5 C29,9.59644029 23.4035597,4 16.5,4 C9.59644029,4 4,9.59644029 4,16.5 C4,23.4035597 9.59644029,29 16.5,29 L16.5,29 Z M16.5,28 C22.8512749,28 28,22.8512749 28,16.5 C28,10.1487251 22.8512749,5 16.5,5 C10.1487251,5 5,10.1487251 5,16.5 C5,22.8512749 10.1487251,28 16.5,28 L16.5,28 Z M12,14 C12.5522848,14 13,13.5522848 13,13 C13,12.4477152 12.5522848,12 12,12 C11.4477152,12 11,12.4477152 11,13 C11,13.5522848 11.4477152,14 12,14 L12,14 Z M21,14 C21.5522848,14 22,13.5522848 22,13 C22,12.4477152 21.5522848,12 21,12 C20.4477152,12 20,12.4477152 20,13 C20,13.5522848 20.4477152,14 21,14 L21,14 Z M16.4813232,22 C13,22 11,20 11,20 L11,21 C11,21 13,23 16.4813232,23 C19.9626465,23 22,21 22,21 L22,20 C22,20 19.9626465,22 16.4813232,22 L16.4813232,22 Z" + id="smiling-face" + sketch:type="MSShapeGroup" + style="fill:#000000;stroke:#000000;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-thumb-up.png b/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-thumb-up.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2b76b33d690d289e8e22f05d913393797e6fd7 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-thumb-up.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-thumb-up.svg b/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-thumb-up.svg new file mode 100644 index 0000000000000000000000000000000000000000..4610328cf75886c6daaffd48c061fd94020f3234 --- /dev/null +++ b/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-thumb-up.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32px" + height="32px" + viewBox="0 0 32 32" + version="1.1" + id="svg2992" + inkscape:version="0.48.4 r9939" + sodipodi:docname="icon-6-thumb-up.svg" + inkscape:export-filename="/var/lib/lxc/bbb-dev090-14/rootfs/home/ubuntu/dev/bigbluebutton/bigbluebutton-client/branding/default/style/css/assets/images/icon-6-thumb-up.png" + inkscape:export-xdpi="50" + inkscape:export-ydpi="50"> + <metadata + id="metadata3005"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>icon 6 thumb up</dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="753" + inkscape:window-height="480" + id="namedview3003" + showgrid="false" + inkscape:zoom="7.375" + inkscape:cx="16" + inkscape:cy="16" + inkscape:window-x="65" + inkscape:window-y="24" + inkscape:window-maximized="0" + inkscape:current-layer="svg2992" /> + <!-- Generator: Sketch 3.0.3 (7891) - http://www.bohemiancoding.com/sketch --> + <title + id="title2994">icon 6 thumb up</title> + <desc + id="desc2996">Created with Sketch.</desc> + <defs + id="defs2998" /> + <g + id="Page-1" + stroke="none" + stroke-width="1" + fill="none" + fill-rule="evenodd" + sketch:type="MSPage" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <g + id="icon-6-thumb-up" + sketch:type="MSArtboardGroup" + fill="#929292" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <path + d="M19.2488133,30 L23.5023733,30 C24.8817744,30 26,28.8903379 26,27.5 C26,26.9371936 25.8126296,26.417824 25.4978577,25.9999924 C26.8803322,25.9966522 28,24.8882411 28,23.5 C28,22.864214 27.763488,22.28386 27.3722702,21.8426992 L27.3722702,21.8426992 C28.3231922,21.4895043 29,20.5793268 29,19.5 C29,18.864214 28.763488,18.28386 28.3722702,17.8426992 C29.3231922,17.4895043 30,16.5793268 30,15.5 C30,14.1192881 28.8845825,13 27.4915915,13 L21.4997555,13 C22.5490723,7.9831543 22.0463867,1.99999928 18,2 C13.9536133,2.00000073 16.066359,6.78764706 14.5,9.76269531 C12.9337003,12.7376303 10.000223,12.9999801 10,13 L3.99508929,13 C2.8932319,13 2,13.8933973 2,14.9918842 L2,26.0081158 C2,27.1082031 2.88670635,28 3.99810135,28 L10,28 L16,30 L19.2488133,30 L19.2488133,30 Z M10.5,14 C10.5000014,13.9999997 13.8047349,13.276317 15.3710938,10.3012695 C16.9374527,7.32622128 15.1291504,3.00000043 18,3 C21.1514893,2.99999957 21.5007324,8.5 20.2999878,14 L20.2999878,14 L27.5069036,14 C28.3361142,14 29,14.6715729 29,15.5 C29,16.3342028 28.331518,17 27.5069036,17 L24,17 L24,18 L26.5069036,18 C27.3361142,18 28,18.6715729 28,19.5 C28,20.3342028 27.331518,21 26.5069036,21 L23,21 L23,21 L23,22 L25.5069036,22 C26.3361142,22 27,22.6715729 27,23.5 C27,24.3342028 26.331518,25 25.5069036,25 L22,25 L22,25 L22,26 L23.4983244,26 C24.3288106,26 25,26.6715729 25,27.5 C25,28.3342028 24.3276769,29 23.4983244,29 L19.7508378,29 L16,29 L10,27 L4.00292933,27 C3.43788135,27 3,26.5529553 3,26.0014977 L3,14.9985023 C3,14.4474894 3.44769743,14 3.9999602,14 L10.5,14 L10.5,14 L10.5,14 Z" + id="thumb-up" + sketch:type="MSShapeGroup" + style="fill:#000000;stroke:#000000;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-sad-face.png b/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-sad-face.png new file mode 100644 index 0000000000000000000000000000000000000000..63a079cddb4ee5a7180f69be8ec6e91bc34ccf97 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-sad-face.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-sad-face.svg b/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-sad-face.svg new file mode 100644 index 0000000000000000000000000000000000000000..584001036e0f441e05f91c04880642ca3ac3e5a5 --- /dev/null +++ b/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-sad-face.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32px" + height="32px" + viewBox="0 0 32 32" + version="1.1" + id="svg2992" + inkscape:version="0.48.4 r9939" + sodipodi:docname="icon-7-sad-face.svg" + inkscape:export-filename="/var/lib/lxc/bbb-dev090-14/rootfs/home/ubuntu/dev/bigbluebutton/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-sad-face.png" + inkscape:export-xdpi="50" + inkscape:export-ydpi="50"> + <metadata + id="metadata3005"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>icon 7 sad face</dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="753" + inkscape:window-height="480" + id="namedview3003" + showgrid="false" + inkscape:zoom="7.375" + inkscape:cx="16" + inkscape:cy="16" + inkscape:window-x="65" + inkscape:window-y="24" + inkscape:window-maximized="0" + inkscape:current-layer="svg2992" /> + <!-- Generator: Sketch 3.0.3 (7891) - http://www.bohemiancoding.com/sketch --> + <title + id="title2994">icon 7 sad face</title> + <desc + id="desc2996">Created with Sketch.</desc> + <defs + id="defs2998" /> + <g + id="Page-1" + stroke="none" + stroke-width="1" + fill="none" + fill-rule="evenodd" + sketch:type="MSPage" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <g + id="icon-7-sad-face" + sketch:type="MSArtboardGroup" + fill="#929292" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <path + d="M16.5,29 C23.4035597,29 29,23.4035597 29,16.5 C29,9.59644029 23.4035597,4 16.5,4 C9.59644029,4 4,9.59644029 4,16.5 C4,23.4035597 9.59644029,29 16.5,29 L16.5,29 Z M16.5,28 C22.8512749,28 28,22.8512749 28,16.5 C28,10.1487251 22.8512749,5 16.5,5 C10.1487251,5 5,10.1487251 5,16.5 C5,22.8512749 10.1487251,28 16.5,28 L16.5,28 Z M12,15 C12.5522848,15 13,14.5522848 13,14 C13,13.4477152 12.5522848,13 12,13 C11.4477152,13 11,13.4477152 11,14 C11,14.5522848 11.4477152,15 12,15 L12,15 Z M21,15 C21.5522848,15 22,14.5522848 22,14 C22,13.4477152 21.5522848,13 21,13 C20.4477152,13 20,13.4477152 20,14 C20,14.5522848 20.4477152,15 21,15 L21,15 Z M16.4813232,21 C13,21 11,23 11,23 L11,22 C11,22 13,20 16.4813232,20 C19.9626465,20 22,22 22,22 L22,23 C22,23 19.9626465,21 16.4813232,21 L16.4813232,21 Z" + id="sad-face" + sketch:type="MSShapeGroup" + style="fill:#000000;stroke:#000000;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-thumb-down.png b/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-thumb-down.png new file mode 100644 index 0000000000000000000000000000000000000000..e9b46550cad5ab24985d53da86c2f2cb88ca3653 Binary files /dev/null and b/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-thumb-down.png differ diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-thumb-down.svg b/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-thumb-down.svg new file mode 100644 index 0000000000000000000000000000000000000000..20a8802c2fa50ae14d980337dd1ff8e4ade52d98 --- /dev/null +++ b/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-thumb-down.svg @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="32px" + height="32px" + viewBox="0 0 32 32" + version="1.1" + id="svg3007" + inkscape:version="0.48.4 r9939" + sodipodi:docname="icon-7-thumb-down.svg" + inkscape:export-filename="/var/lib/lxc/bbb-dev090-14/rootfs/home/ubuntu/dev/bigbluebutton/bigbluebutton-client/branding/default/style/css/assets/images/icon-7-thumb-down.png" + inkscape:export-xdpi="50" + inkscape:export-ydpi="50"> + <metadata + id="metadata3020"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>icon 7 thumb down</dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="753" + inkscape:window-height="480" + id="namedview3018" + showgrid="false" + inkscape:zoom="7.375" + inkscape:cx="16" + inkscape:cy="16" + inkscape:window-x="65" + inkscape:window-y="24" + inkscape:window-maximized="0" + inkscape:current-layer="svg3007" /> + <!-- Generator: Sketch 3.0.3 (7891) - http://www.bohemiancoding.com/sketch --> + <title + id="title3009">icon 7 thumb down</title> + <desc + id="desc3011">Created with Sketch.</desc> + <defs + id="defs3013" /> + <g + id="Page-1" + stroke="none" + stroke-width="1" + fill="none" + fill-rule="evenodd" + sketch:type="MSPage" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <g + id="icon-7-thumb-down" + sketch:type="MSArtboardGroup" + fill="#929292" + style="fill:#000000;stroke:#000000;stroke-opacity:1"> + <path + d="M19.2488133,2 L23.5023733,2 C24.8817744,2 26,3.10966206 26,4.5 C26,5.06280636 25.8126296,5.58217601 25.4978577,6.00000757 C26.8803322,6.00334775 28,7.11175892 28,8.5 C28,9.13578601 27.763488,9.71613998 27.3722702,10.1573008 L27.3722702,10.1573008 C28.3231922,10.5104957 29,11.4206732 29,12.5 C29,13.135786 28.763488,13.71614 28.3722702,14.1573008 C29.3231922,14.5104957 30,15.4206732 30,16.5 C30,17.8807119 28.8845825,19 27.4915915,19 L21.4997555,19 C22.5490723,24.0168457 22.0463867,30.0000007 18,30 C13.9536133,29.9999993 16.066359,25.2123529 14.5,22.2373047 C12.9337003,19.2623697 10.000223,19.0000199 10,19 L3.99508929,19 C2.8932319,19 2,18.1066027 2,17.0081158 L2,5.99188419 C2,4.89179693 2.88670635,4 3.99810135,4 L10,4 L16,2 L19.2488133,2 L19.2488133,2 Z M10.5,18 C10.5000014,18.0000003 13.8047349,18.723683 15.3710938,21.6987305 C16.9374527,24.6737787 15.1291504,28.9999996 18,29 C21.1514893,29.0000004 21.5007324,23.5 20.2999878,18 L20.2999878,18 L27.5069036,18 C28.3361142,18 29,17.3284271 29,16.5 C29,15.6657972 28.331518,15 27.5069036,15 L24,15 L24,14 L26.5069036,14 C27.3361142,14 28,13.3284271 28,12.5 C28,11.6657972 27.331518,11 26.5069036,11 L23,11 L23,11 L23,10 L25.5069036,10 C26.3361142,10 27,9.32842712 27,8.5 C27,7.66579723 26.331518,7 25.5069036,7 L22,7 L22,7 L22,6 L23.4983244,6 C24.3288106,6 25,5.32842712 25,4.5 C25,3.66579723 24.3276769,3 23.4983244,3 L19.7508378,3 L16,3 L10,5 L4.00292933,5 C3.43788135,5 3,5.44704472 3,5.99850233 L3,17.0014977 C3,17.5525106 3.44769743,18 3.9999602,18 L10.5,18 L10.5,18 L10.5,18 Z" + id="thumb-down" + sketch:type="MSShapeGroup" + style="fill:#000000;stroke:#000000;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/bigbluebutton-client/branding/default/style/css/assets/images/icons-license.txt b/bigbluebutton-client/branding/default/style/css/assets/images/icons-license.txt index 9cbdc68e812a3ba89ecd36ece1f6804c2d2cdb2d..014b6c9a6d1bc809650ae5c3aa0819b1e1c972fb 100755 --- a/bigbluebutton-client/branding/default/style/css/assets/images/icons-license.txt +++ b/bigbluebutton-client/branding/default/style/css/assets/images/icons-license.txt @@ -35,4 +35,22 @@ purchase a royalty-free license. I'm unavailable for custom icon design work. But your suggestions are always welcome! <mailto:p@yusukekamiyamane.com> -==================== \ No newline at end of file +==================== +# Hawcons + +Created by *Yannick Lung*, 2014 +* [Web](http://www.hawcons.com) +* [Twitter](http://www.twitter.com/Hawcons) +* [Facebook](http://www.facebook.com/Hawcons) +* [Mail](mailto:support@hawcons.com) +* [Tumblr](http://hawcons.tumblr.com) +* [Google+](http://www.google.com/+) + +## Version 1.0 + +## LICENSE +You are free to use Hawcons for commercial and personal purposes without attribution, however a credit for the work would be appreciated. You may not sell or redistribute the icons themselves as icons. Do not claim creative credit. + +If you would like to support my work you can do this with the donate button on the website. + +If you have any additional questions please contact Hawcons via email or social networks. diff --git a/bigbluebutton-client/build.xml b/bigbluebutton-client/build.xml index 95eda2e7dfff3b0ca6e8eb4ad49ff327ee1ccddd..886ddc633edc97c776cf819b8782ed07a5c5fec6 100755 --- a/bigbluebutton-client/build.xml +++ b/bigbluebutton-client/build.xml @@ -34,10 +34,10 @@ <property name="NOTES" value="NotesModule" /> <property name="VIDEO" value="VideoconfModule" /> <property name="WHITEBOARD" value="WhiteboardModule" /> - <property name="VIDEO_DOCK" value="VideodockModule" /> <property name="POLLING" value="PollingModule" /> <property name="LAYOUT" value="LayoutModule" /> <property name="USERS" value="UsersModule" /> + <property name="SHAREDNOTES" value="SharedNotesModule" /> <xmlproperty file="${SRC_DIR}/conf/locales.xml" collapseAttributes="true"/> @@ -83,6 +83,7 @@ debug="${DEBUG}" mxml.compatibility-version="3.0.0" swf-version="13" + default-background-color="0xFFFFFF" optimize="true"> </mxmlc> </sequential> @@ -176,6 +177,7 @@ <include-resource-bundles>skins</include-resource-bundles> <include-resource-bundles>styles</include-resource-bundles> <source-path path-element="${FLEX_HOME}/frameworks"/> + <default-background-color>0xFFFFFF</default-background-color> </mxmlc> </sequential> </macrodef> @@ -264,10 +266,6 @@ <build-module src="${SRC_DIR}" target="${VIDEO}" /> </target> - <target name="build-videodock" description="Compile Video Dock Module"> - <build-module src="${SRC_DIR}" target="${VIDEO_DOCK}" /> - </target> - <target name="build-whiteboard" description="Compile Whiteboard Module"> <build-module src="${SRC_DIR}" target="${WHITEBOARD}" /> </target> @@ -280,15 +278,27 @@ <build-module src="${SRC_DIR}" target="${USERS}" /> </target> + <target name="build-sharednotes" description="Compile SharedNotes Module"> + <build-module src="${SRC_DIR}" target="${SHAREDNOTES}" /> + + <echo message="Copying assets for SharedNotesModule" /> + <copy todir="${OUTPUT_DIR}/org/bigbluebutton/modules/sharednotes/views/images/" > + <fileset dir="${BASE_DIR}/src/org/bigbluebutton/modules/sharednotes/views/images/" /> + </copy> + <copy file="${BASE_DIR}/src/org/bigbluebutton/modules/sharednotes/util/shared_notes.js" todir="${OUTPUT_DIR}/lib/"> + + </copy> + </target> + <!-- just a grouping of modules to compile --> <target name="build-main-chat-present" - depends="build-bbb-main, build-chat, build-present, build-layout, build-broadcast, build-users" + depends="build-bbb-main, build-chat, build-present, build-layout, build-broadcast, build-users, build-sharednotes" description="Compile main, chat, present modules"> </target> <!-- just a grouping of modules to compile --> <target name="build-deskshare-phone-video-whiteboard-dyn" - depends="build-deskshare, build-phone, build-video, build-videodock, build-whiteboard, build-notes, build-polling" + depends="build-deskshare, build-phone, build-video, build-whiteboard, build-notes, build-polling" description="Compile deskshare, phone, video, whiteboard modules"> </target> @@ -301,6 +311,7 @@ <mxmlc file="@{src}/@{target}.mxml" output="${OUTPUT_DIR}/@{target}.swf" debug="${DEBUG}" mxml.compatibility-version="3.0.0" swf-version="13" optimize="true" link-report="linker-report.xml"> <target-player>11</target-player> <load-config filename="@{flex}/frameworks/flex-config.xml" /> + <default-background-color>0xFFFFFF</default-background-color> <source-path path-element="@{flex}/frameworks" /> <!-- @@ -369,7 +380,7 @@ <load-config filename="@{flex}/frameworks/flex-config.xml" /> <source-path path-element="@{flex}/frameworks" /> <static-link-runtime-shared-libraries>${STATIC_RSL}</static-link-runtime-shared-libraries> - + <default-background-color>0xFFFFFF</default-background-color> <compiler.library-path dir="@{flex}/frameworks" append="true"> <include name="libs" /> <include name="../bundles/{locale}" /> @@ -420,14 +431,17 @@ <fileset dir="${PROD_RESOURCES_DIR}/lib"/> </copy> <copy file="${PROD_RESOURCES_DIR}/BigBlueButtonTest.html" todir="${OUTPUT_DIR}" overwrite="true"/> - <copy file="${PROD_RESOURCES_DIR}/BigBlueButton.html" todir="${OUTPUT_DIR}" overwrite="true"/> + <copy file="${PROD_RESOURCES_DIR}/MconfLive.html" todir="${OUTPUT_DIR}" overwrite="true"/> <copy file="${PROD_RESOURCES_DIR}/DeskshareStandalone.html" todir="${OUTPUT_DIR}" overwrite="true"/> <copy file="${PROD_RESOURCES_DIR}/bbb.gif" todir="${OUTPUT_DIR}" overwrite="true"/> <copy file="${PROD_RESOURCES_DIR}/avatar.png" todir="${OUTPUT_DIR}" overwrite="true"/> + <copy file="${PROD_RESOURCES_DIR}/logo.png" todir="${OUTPUT_DIR}" overwrite="true"/> + <copy file="${PROD_RESOURCES_DIR}/background.jpg" todir="${OUTPUT_DIR}" overwrite="true"/> <copy file="${PROD_RESOURCES_DIR}/locales.xml" todir="${OUTPUT_DIR}/conf" overwrite="true"/> <copy file="${PROD_RESOURCES_DIR}/expressInstall.swf" todir="${OUTPUT_DIR}" overwrite="true"/> <copy file="${PROD_RESOURCES_DIR}/example-info-data.xml" todir="${OUTPUT_DIR}/conf" overwrite="true"/> <copy file="${PROD_RESOURCES_DIR}/layout.xml" todir="${OUTPUT_DIR}/conf" overwrite="true"/> + <copy file="${PROD_RESOURCES_DIR}/profiles.xml" todir="${OUTPUT_DIR}/conf" overwrite="true"/> <if> <equals arg1="${BUILD_ENV}" arg2="DEV"/> <then> @@ -446,8 +460,8 @@ <target name="generate-html-wrapper"> <html-wrapper - title="BigBlueButton" - file="BigBlueButton.html" + title="Mconf-Live" + file="MconfLive.html" height="100%" width="100%" bgcolor="grey" diff --git a/bigbluebutton-client/locale/en_US/bbbResources.properties b/bigbluebutton-client/locale/en_US/bbbResources.properties index 2632484bd224321184ee1f08740cbfd72c28ab72..60a057b06b2fcb321ac95a0674ff50bf843ab58f 100755 --- a/bigbluebutton-client/locale/en_US/bbbResources.properties +++ b/bigbluebutton-client/locale/en_US/bbbResources.properties @@ -9,6 +9,7 @@ bbb.mainshell.invalidAuthToken = Invalid Authentication Token bbb.mainshell.resetLayoutBtn.toolTip = Reset Layout bbb.mainshell.notification.tunnelling = Tunnelling bbb.mainshell.notification.webrtc = WebRTC Audio +bbb.mainshell.fullscreenBtn.toolTip = Toggle full screen bbb.oldlocalewindow.reminder1 = You may have an old language translations of BigBlueButton. bbb.oldlocalewindow.reminder2 = Please clear your browser's cache and try again. bbb.oldlocalewindow.windowTitle = Warning: Old Language Translations @@ -78,6 +79,9 @@ bbb.webrtcWarning.failedError.1009 = Error 1009: Could not fetch STUN/TURN serve bbb.webrtcWarning.failedError.unknown = Error {0}: Unknown error code bbb.webrtcWarning.failedError.mediamissing = Could not get your microphone for a WebRTC call bbb.webrtcWarning.failedError.endedunexpectedly = The WebRTC echo test ended unexpectedly +bbb.webrtcWarning.connection.dropped = WebRTC connection dropped +bbb.webrtcWarning.connection.reconnecting = Attempting to reconnect +bbb.webrtcWarning.connection.reestablished = WebRTC connection re-established bbb.mainToolbar.helpBtn = Help bbb.mainToolbar.logoutBtn = Logout bbb.mainToolbar.logoutBtn.toolTip = Log Out @@ -98,6 +102,20 @@ bbb.mainToolbar.recordBtn..notification.message1 = You can record this meeting. bbb.mainToolbar.recordBtn..notification.message2 = You must click the Start/Stop Recording button in the title bar to begin/end recording. bbb.mainToolbar.recordingLabel.recording = (Recording) bbb.mainToolbar.recordingLabel.notRecording = Not Recording +bbb.waitWindow.waitMessage.message = You are a guest, please wait moderator approval. +bbb.waitWindow.waitMessage.title = Waiting +bbb.guests.title = Guests +bbb.guests.message.singular = {0} user wants to join this meeting +bbb.guests.message.plural = {0} users want to join this meeting +bbb.guests.allowBtn.toolTip = Allow +bbb.guests.allowEveryoneBtn.text = Allow everyone +bbb.guests.denyBtn.toolTip = Deny +bbb.guests.denyEveryoneBtn.text = Deny everyone +bbb.guests.rememberAction.text = Remember choice +bbb.guests.alwaysAccept = Always accept +bbb.guests.alwaysDeny = Always deny +bbb.guests.askModerator = Ask moderator +bbb.guests.Management = Guest management bbb.clientstatus.title = Configuration Notifications bbb.clientstatus.notification = Unread notifications bbb.clientstatus.tunneling.title = Firewall @@ -129,9 +147,19 @@ bbb.users.settings.webcamSettings = Webcam Settings bbb.users.settings.muteAll = Mute All Users bbb.users.settings.muteAllExcept = Mute All Users Except Presenter bbb.users.settings.unmuteAll = Unmute All Users -bbb.users.settings.lowerAllHands = Lower All Hands -bbb.users.raiseHandBtn.toolTip = Raise Hand -bbb.users.raiseHandBtn.toolTip2 = Lower Hand +bbb.users.settings.clearAllStatus = Clear All Status +bbb.users.status.clearStatus = Clear my status +bbb.users.status.raiseHand = Raise Hand +bbb.users.status.agree = I agree +bbb.users.status.disagree = I disagree +bbb.users.status.speakLouder = Could you please speak louder? +bbb.users.status.speakSofter = Could you please speak softer? +bbb.users.status.speakFaster = Could you please speak faster? +bbb.users.status.speakSlower = Could you please speak slower? +bbb.users.status.beRightBack = I'll be right back +bbb.users.status.laughter = :) +bbb.users.status.sad = :( +bbb.users.raiseHandBtn.toolTip = Status bbb.users.roomMuted.text = Viewers Muted bbb.users.roomLocked.text = Viewers Locked bbb.users.pushToTalk.toolTip = Talk @@ -148,8 +176,18 @@ bbb.users.usersGrid.statusItemRenderer = Status bbb.users.usersGrid.statusItemRenderer.changePresenter = Click To Make Presenter bbb.users.usersGrid.statusItemRenderer.presenter = Presenter bbb.users.usersGrid.statusItemRenderer.moderator = Moderator +bbb.users.usersGrid.statusItemRenderer.clearStatus = Clear Status bbb.users.usersGrid.statusItemRenderer.lowerHand = Lower Hand bbb.users.usersGrid.statusItemRenderer.handRaised = Hand Raised +bbb.users.usersGrid.statusItemRenderer.agree = Agree +bbb.users.usersGrid.statusItemRenderer.disagree = Disagree +bbb.users.usersGrid.statusItemRenderer.speakLouder = Speak louder +bbb.users.usersGrid.statusItemRenderer.speakSofter = Speak softer +bbb.users.usersGrid.statusItemRenderer.speakFaster = Speak faster +bbb.users.usersGrid.statusItemRenderer.speakSlower = Speak slower +bbb.users.usersGrid.statusItemRenderer.beRightBack = Be Right Back +bbb.users.usersGrid.statusItemRenderer.laughter = :) +bbb.users.usersGrid.statusItemRenderer.sad = :( bbb.users.usersGrid.statusItemRenderer.viewer = Viewer bbb.users.usersGrid.mediaItemRenderer = Media bbb.users.usersGrid.mediaItemRenderer.talking = Talking @@ -164,12 +202,15 @@ bbb.users.usersGrid.mediaItemRenderer.webcam = Webcam shared bbb.users.usersGrid.mediaItemRenderer.micOff = Microphone off bbb.users.usersGrid.mediaItemRenderer.micOn = Microphone on bbb.users.usersGrid.mediaItemRenderer.noAudio = Not in audio conference +bbb.users.usersGrid.mediaItemRenderer.promoteUser = Promote {0} to moderator +bbb.users.usersGrid.mediaItemRenderer.demoteUser = Demote {0} to viewer bbb.presentation.title = Presentation bbb.presentation.titleWithPres = Presentation: {0} bbb.presentation.quickLink.label = Presentation Window bbb.presentation.fitToWidth.toolTip = Fit Presentation To Width bbb.presentation.fitToPage.toolTip = Fit Presentation To Page bbb.presentation.uploadPresBtn.toolTip = Upload Presentation +bbb.presentation.downloadPresBtn.toolTip = Download Presentations bbb.presentation.backBtn.toolTip = Previous slide bbb.presentation.btnSlideNum.accessibilityName = Slide {0} of {1} bbb.presentation.btnSlideNum.toolTip = Select a slide @@ -213,6 +254,13 @@ bbb.fileupload.okCancelBtn.toolTip = Close the File Upload dialog box bbb.fileupload.genThumbText = Generating thumbnails.. bbb.fileupload.progBarLbl = Progress: bbb.fileupload.fileFormatHint = Upload any office document or Portable Document Format (PDF) file. For best results upload PDF. +bbb.fileupload.letUserDownload = Let the users download the presentation +bbb.fileupload.letUserDownload.tooltip = Check here if you want the other users to download your presentation +bbb.filedownload.title = Download the Presentations +bbb.filedownload.fileLbl = Choose File to Download: +bbb.filedownload.downloadBtn = Download +bbb.filedownload.downloadBtn.toolTip = Download Presentation +bbb.filedownload.thisFileIsDownloadable = File is downloadable bbb.chat.title = Chat bbb.chat.quickLink.label = Chat Window bbb.chat.cmpColorPicker.toolTip = Text Color @@ -220,6 +268,15 @@ bbb.chat.input.accessibilityName = Chat Message Editing Field bbb.chat.sendBtn = Send bbb.chat.sendBtn.toolTip = Send Message bbb.chat.sendBtn.accessibilityName = Send chat message +bbb.chat.saveBtn.toolTip = Save chat +bbb.chat.saveBtn.accessibilityName = Save chat in text file +bbb.chat.saveBtn.label = Save +bbb.chat.save.complete = Chat successfully saved +bbb.chat.save.filename = public-chat +bbb.chat.copyBtn.toolTip = Copy chat +bbb.chat.copyBtn.accessibilityName = Copy chat to clipboard +bbb.chat.copyBtn.label = Copy +bbb.chat.copy.complete = Chat copied to clipboard bbb.chat.contextmenu.copyalltext = Copy All Text bbb.chat.publicChatUsername = Public bbb.chat.optionsTabName = Options @@ -284,6 +341,7 @@ bbb.desktopView.actualSize = Display actual size bbb.desktopView.minimizeBtn.accessibilityName = Minimize the Desktop Sharing View Window bbb.desktopView.maximizeRestoreBtn.accessibilityName = Maximize the Desktop Sharing View Window bbb.desktopView.closeBtn.accessibilityName = Close the Desktop Sharing View Window +bbb.toolbar.sharednotes.toolTip = Open Shared Notes bbb.toolbar.phone.toolTip.start = Share Your Microphone bbb.toolbar.phone.toolTip.stop = Stop Sharing Your Microphone bbb.toolbar.phone.toolTip.mute = Stop listening the conference @@ -306,6 +364,7 @@ bbb.layout.combo.remote = Remote bbb.layout.save.complete = Layouts were successfully saved bbb.layout.load.complete = Layouts were successfully loaded bbb.layout.load.failed = Failed to load the layouts +bbb.layout.sync = Your layout has been sent to all participants bbb.layout.name.defaultlayout = Default Layout bbb.layout.name.videochat = Video Chat bbb.layout.name.webcamsfocus = Webcam Meeting @@ -334,12 +393,17 @@ bbb.logout.appshutdown = The server app has been shut down bbb.logout.asyncerror = An Async Error occured bbb.logout.connectionclosed = The connection to the server has been closed bbb.logout.connectionfailed = The connection to the server has failed -bbb.logout.rejected = The connection to the server has been rejected +bbb.logout.rejected = The connection to the server has been rejected\n\nIt may occur when you try to use the same session in multiple tabs in your browser bbb.logout.invalidapp = The red5 app does not exist bbb.logout.unknown = Your client has lost connection with the server +bbb.logout.guestkickedout = The moderator didn't allow you to join this meeting bbb.logout.usercommand = You have logged out of the conference bbb.logout.refresh.message = If this logout was unexpected click the button below to reconnect. bbb.logout.refresh.label = Reconnect +bbb.settings.title = Settings +bbb.settings.ok = OK +bbb.settings.cancel = Cancel +bbb.settings.btn.toolTip = Open configuration window bbb.logout.confirm.title = Confirm Logout bbb.logout.confirm.message = Are you sure you want to log out? bbb.logout.confirm.yes = Yes @@ -348,6 +412,12 @@ bbb.notes.title = Notes bbb.notes.cmpColorPicker.toolTip = Text Color bbb.notes.saveBtn = Save bbb.notes.saveBtn.toolTip = Save Note +bbb.sharedNotes.title = Shared notes +bbb.sharedNotes.save.toolTip = Save notes to file +bbb.sharedNotes.save.complete = Notes were successfully saved +bbb.sharedNotes.new.toolTip = Create additional shared notes +bbb.sharedNotes.additionalNotes.closeWarning.title = Closing shared notes +bbb.sharedNotes.additionalNotes.closeWarning.message = This action will destroy the notes on this window for everyone, and there's no way to undo. Are you sure you want to close these notes? bbb.settings.deskshare.instructions = Choose Allow on the prompt that pops up to check that desktop sharing is working properly for you bbb.settings.deskshare.start = Check Desktop Sharing bbb.settings.voice.volume = Microphone Activity @@ -364,6 +434,15 @@ bbb.settings.warning.label = Warning bbb.settings.warning.close = Close this Warning bbb.settings.noissues = No outstanding issues have been detected. bbb.settings.instructions = Accept the Flash prompt that asks you for webcam permissions. If the output matches what is expected, your browser has been set up correctly. Other potentials issues are below. Examine them to find a possible solution. +bbb.bwmonitor.title = Network monitor +bbb.bwmonitor.upload = Upload +bbb.bwmonitor.upload.short = Up +bbb.bwmonitor.download = Download +bbb.bwmonitor.download.short = Down +bbb.bwmonitor.total = Total +bbb.bwmonitor.current = Current +bbb.bwmonitor.available = Available +bbb.bwmonitor.latency = Latency ltbcustom.bbb.highlighter.toolbar.triangle = Triangle ltbcustom.bbb.highlighter.toolbar.triangle.accessibilityName = Switch whiteboard cursor to triangle ltbcustom.bbb.highlighter.toolbar.line = Line diff --git a/bigbluebutton-client/locale/pt_BR/bbbResources.properties b/bigbluebutton-client/locale/pt_BR/bbbResources.properties index 988bab940788e736885fb619b28ffb6b5361d122..843602ef1874bcde479d8a166eb91cbbe63bc829 100755 --- a/bigbluebutton-client/locale/pt_BR/bbbResources.properties +++ b/bigbluebutton-client/locale/pt_BR/bbbResources.properties @@ -9,6 +9,7 @@ bbb.mainshell.invalidAuthToken = Token de autenticação inválido bbb.mainshell.resetLayoutBtn.toolTip = Restaurar layout bbb.mainshell.notification.tunnelling = Tunelando bbb.mainshell.notification.webrtc = Ãudio WebRTC +bbb.mainshell.fullscreenBtn.toolTip = Alternar tela cheia bbb.oldlocalewindow.reminder1 = Você deve ter uma versão antiga da tradução do BigBlueButton. bbb.oldlocalewindow.reminder2 = Por favor, apague os arquivos temporários do seu navegador e tente novamente. bbb.oldlocalewindow.windowTitle = Aviso\: versão antiga da tradução @@ -78,6 +79,9 @@ bbb.webrtcWarning.failedError.1009 = Erro 1009\: Não foi possÃvel recuperar as bbb.webrtcWarning.failedError.unknown = Erro {0}\: Código de erro desconhecido bbb.webrtcWarning.failedError.mediamissing = Não foi possÃvel acessar seu microfone para a chamada WebRTC bbb.webrtcWarning.failedError.endedunexpectedly = O teste de eco WebRTC terminou inesperadamente +bbb.webrtcWarning.connection.dropped = Falha na conexão WebRTC +bbb.webrtcWarning.connection.reconnecting = Tentando reconexão +bbb.webrtcWarning.connection.reestablished = Conexão WebRTC re-estabelecida bbb.mainToolbar.helpBtn = Ajuda bbb.mainToolbar.logoutBtn = Sair bbb.mainToolbar.logoutBtn.toolTip = Sair da sessão @@ -98,6 +102,20 @@ bbb.mainToolbar.recordBtn..notification.message1 = Você pode gravar esta sessã bbb.mainToolbar.recordBtn..notification.message2 = Você precisa clicar no botão de Iniciar/Encerrar gravação na barra superior para começar/terminar a gravação. bbb.mainToolbar.recordingLabel.recording = (Gravando) bbb.mainToolbar.recordingLabel.notRecording = Não gravando +bbb.waitWindow.waitMessage.message = Você é um convidado, por favor aguarde a aprovação do moderador da sessão. +bbb.waitWindow.waitMessage.title = Aguardando +bbb.guests.title = Convidados +bbb.guests.message.singular = {0} usuário deseja entrar na sessão +bbb.guests.message.plural = {0} usuários desejam entrar na sessão +bbb.guests.allowBtn.toolTip = Permitir +bbb.guests.allowEveryoneBtn.text = Permitir todos +bbb.guests.denyBtn.toolTip = Rejeitar +bbb.guests.denyEveryoneBtn.text = Rejeitar todos +bbb.guests.rememberAction.text = Lembrar escolha +bbb.guests.alwaysAccept = Sempre permitir +bbb.guests.alwaysDeny = Sempre rejeitar +bbb.guests.askModerator = Perguntar para o moderador +bbb.guests.Management = Gerenciar convidados bbb.clientstatus.title = Configuração de Notificações bbb.clientstatus.notification = Notificações não lidas bbb.clientstatus.tunneling.title = Firewall @@ -129,9 +147,19 @@ bbb.users.settings.webcamSettings = Configurações de webcam bbb.users.settings.muteAll = Silenciar todos bbb.users.settings.muteAllExcept = Silenciar todos exceto o apresentador bbb.users.settings.unmuteAll = Acionar microfone de todos -bbb.users.settings.lowerAllHands = Baixar todas as mãos -bbb.users.raiseHandBtn.toolTip = Levantar a mão -bbb.users.raiseHandBtn.toolTip2 = Abaixar a mão +bbb.users.settings.clearAllStatus = Limpar status de todos +bbb.users.status.clearStatus = Limpar meu status +bbb.users.status.raiseHand = Levantar a mão +bbb.users.status.agree = Concordo +bbb.users.status.disagree = Discordo +bbb.users.status.speakLouder = Você pode falar mais alto? +bbb.users.status.speakSofter = Você pode falar mais baixo? +bbb.users.status.speakFaster = Você pode falar mais rápido? +bbb.users.status.speakSlower = Você pode falar mais devagar? +bbb.users.status.beRightBack = Já volto +bbb.users.status.laughter = :) +bbb.users.status.sad = :( +bbb.users.raiseHandBtn.toolTip = Status bbb.users.roomMuted.text = Participantes silenciados bbb.users.roomLocked.text = Participantes bloqueados bbb.users.pushToTalk.toolTip = Falar @@ -148,8 +176,18 @@ bbb.users.usersGrid.statusItemRenderer = Papel bbb.users.usersGrid.statusItemRenderer.changePresenter = Clique para tornar apresentador bbb.users.usersGrid.statusItemRenderer.presenter = Apresentador bbb.users.usersGrid.statusItemRenderer.moderator = Moderador +bbb.users.usersGrid.statusItemRenderer.clearStatus = Limpar status bbb.users.usersGrid.statusItemRenderer.lowerHand = Abaixar a mão bbb.users.usersGrid.statusItemRenderer.handRaised = Mão levantada +bbb.users.usersGrid.statusItemRenderer.agree = Concorda +bbb.users.usersGrid.statusItemRenderer.disagree = Discorda +bbb.users.usersGrid.statusItemRenderer.speakLouder = Fale mais alto +bbb.users.usersGrid.statusItemRenderer.speakSofter = Fale mais baixo +bbb.users.usersGrid.statusItemRenderer.speakFaster = Fale mais rápido +bbb.users.usersGrid.statusItemRenderer.speakSlower = Fale mais devagar +bbb.users.usersGrid.statusItemRenderer.beRightBack = Já volta +bbb.users.usersGrid.statusItemRenderer.laughter = :) +bbb.users.usersGrid.statusItemRenderer.sad = :( bbb.users.usersGrid.statusItemRenderer.viewer = Participante bbb.users.usersGrid.mediaItemRenderer = MÃdia bbb.users.usersGrid.mediaItemRenderer.talking = Falando @@ -164,12 +202,15 @@ bbb.users.usersGrid.mediaItemRenderer.webcam = Webcam sendo transmitida bbb.users.usersGrid.mediaItemRenderer.micOff = Microfone desativado bbb.users.usersGrid.mediaItemRenderer.micOn = Microfone ativado bbb.users.usersGrid.mediaItemRenderer.noAudio = Não está na conferência de voz +bbb.users.usersGrid.mediaItemRenderer.promoteUser = Promover {0} para moderador +bbb.users.usersGrid.mediaItemRenderer.demoteUser = Despromover {0} para participante bbb.presentation.title = Apresentação bbb.presentation.titleWithPres = Apresentação\: {0} bbb.presentation.quickLink.label = Janela de apresentação bbb.presentation.fitToWidth.toolTip = Ajustar apresentação à largura bbb.presentation.fitToPage.toolTip = Ajustar apresentação à página bbb.presentation.uploadPresBtn.toolTip = Carregar apresentação +bbb.presentation.downloadPresBtn.toolTip = Baixar apresentação bbb.presentation.backBtn.toolTip = Slide anterior bbb.presentation.btnSlideNum.accessibilityName = Slide {0} de {1} bbb.presentation.btnSlideNum.toolTip = Selecionar um slide @@ -213,6 +254,13 @@ bbb.fileupload.okCancelBtn.toolTip = Fechar a caixa de diálogo para envio de ar bbb.fileupload.genThumbText = Gerando miniaturas dos slides... bbb.fileupload.progBarLbl = Progresso\: bbb.fileupload.fileFormatHint = Carregar qualquer documento Office ou arquivo PDF. Para melhores resultados, opte pelo PDF. +bbb.fileupload.letUserDownload = Permitir que os usuários baixem esse arquivo +bbb.fileupload.letUserDownload.tooltip = Clique aqui se você deseja permitir que os usuários baixem sua apresentação +bbb.filedownload.title = Baixar apresentações +bbb.filedownload.fileLbl = Escolha o arquivo que deseja baixar\: +bbb.filedownload.downloadBtn = Baixar +bbb.filedownload.downloadBtn.toolTip = Baixar arquivo +bbb.filedownload.thisFileIsDownloadable = Os usuários podem baixar este arquivo bbb.chat.title = Bate-papo bbb.chat.quickLink.label = Janela de bate-papo bbb.chat.cmpColorPicker.toolTip = Cor do texto @@ -221,6 +269,15 @@ bbb.chat.sendBtn = Enviar bbb.chat.sendBtn.toolTip = Enviar mensagem bbb.chat.sendBtn.accessibilityName = Enviar mensagem de bate-papo bbb.chat.contextmenu.copyalltext = Copiar todo o texto +bbb.chat.saveBtn.toolTip = Salvar bate-papo +bbb.chat.saveBtn.accessibilityName = Salvar bate-papo em arquivo texto +bbb.chat.saveBtn.label = Salvar +bbb.chat.save.complete = Bate-papo salvo com sucesso +bbb.chat.save.filename = bate-papo-publico +bbb.chat.copyBtn.toolTip = Copiar bate-papo +bbb.chat.copyBtn.accessibilityName = Copiar bate-papo para a área de transferência +bbb.chat.copyBtn.label = Copiar +bbb.chat.copy.complete = Bate-papo copiado para a área de transferência bbb.chat.publicChatUsername = Público bbb.chat.optionsTabName = Opções bbb.chat.privateChatSelect = Selecione uma pessoa para um bate-papo privado @@ -283,6 +340,7 @@ bbb.desktopView.actualSize = Exibir tamanho original bbb.desktopView.minimizeBtn.accessibilityName = Minimizar janela do compartilhamento de tela bbb.desktopView.maximizeRestoreBtn.accessibilityName = Maximizar janela do compartilhamento de tela bbb.desktopView.closeBtn.accessibilityName = Fechar janela do compartilhamento de tela +bbb.toolbar.sharednotes.toolTip = Abrir bloco de notas bbb.toolbar.phone.toolTip.start = Transmitir seu microfone bbb.toolbar.phone.toolTip.stop = Interromper transmissão do seu microfone bbb.toolbar.phone.toolTip.mute = Parar de escutar a conferência @@ -305,6 +363,7 @@ bbb.layout.combo.remote = Remoto bbb.layout.save.complete = Layouts salvos com sucesso bbb.layout.load.complete = Layouts carregados com sucesso bbb.layout.load.failed = Falha ao carregar layouts +bbb.layout.sync = Seu layout foi enviado para todos os participantes bbb.layout.name.defaultlayout = Layout padrão bbb.layout.name.videochat = VÃdeo Chamada bbb.layout.name.webcamsfocus = Reunião com câmeras @@ -333,9 +392,14 @@ bbb.logout.appshutdown = A aplicação no servidor foi interrompida bbb.logout.asyncerror = Um erro assÃncrono ocorreu bbb.logout.connectionclosed = A conexão com o servidor foi fechada bbb.logout.connectionfailed = A conexão com o servidor falhou -bbb.logout.rejected = A conexão com o servidor foi rejeitada +bbb.logout.rejected = A conexão com o servidor foi rejeitada\n\nIsso pode ocorrer quando você tenta utilizar a mesma sessão em múltiplas abas no seu navegador bbb.logout.invalidapp = O aplicativo red5 não existe bbb.logout.unknown = Seu cliente perdeu conexão com o servidor +bbb.logout.guestkickedout = O moderador não permitiu sua entrada na sala +bbb.settings.title = Configurações +bbb.settings.ok = OK +bbb.settings.cancel = Cancelar +bbb.settings.btn.toolTip = Abrir janela de configurações bbb.logout.usercommand = Você saiu da conferência bbb.logout.refresh.message = Se você foi desconectado de maneira inesperada, clique no botão baixo para reconectar. bbb.logout.refresh.label = Reconectar @@ -347,6 +411,12 @@ bbb.notes.title = Notas bbb.notes.cmpColorPicker.toolTip = Cor do texto bbb.notes.saveBtn = Salvar bbb.notes.saveBtn.toolTip = Salvar nota +bbb.sharedNotes.title = Notas compartilhadas +bbb.sharedNotes.save.toolTip = Salvar notas em arquivo +bbb.sharedNotes.save.complete = Notas salvas com sucesso +bbb.sharedNotes.new.toolTip = Criar novas notas compartilhadas +bbb.sharedNotes.additionalNotes.closeWarning.title = Fechando notas compartilhadas +bbb.sharedNotes.additionalNotes.closeWarning.message = Esta ação irá destruir as notas desta janela para todos, e não haverá maneira de recuperá-las. Você tem certeza que deseja fechar estas notas? bbb.settings.deskshare.instructions = Clique em Permitir na janela que será aberta para verificar se o compartilhamento de tela está funcionando corretamente bbb.settings.deskshare.start = Verificar compartilhamento de tela bbb.settings.voice.volume = Atividade do microfone @@ -363,6 +433,15 @@ bbb.settings.warning.label = Aviso bbb.settings.warning.close = Fechar esse aviso bbb.settings.noissues = Nenhum problema foi detectado. bbb.settings.instructions = Aceite a notificação do Flash quando ele requisitar permissão para acessar sua câmera. Se você consegue ver e ouvir a si mesmo, seu navegador foi configurado corretamente. Outros erros em potencial estão indicados abaixo. Verifique cada um para encontrar uma possÃvel solução. +bbb.bwmonitor.title = Monitor de rede +bbb.bwmonitor.upload = Upload +bbb.bwmonitor.upload.short = Up +bbb.bwmonitor.download = Download +bbb.bwmonitor.download.short = Down +bbb.bwmonitor.total = Total +bbb.bwmonitor.current = Atual +bbb.bwmonitor.available = DisponÃvel +bbb.bwmonitor.latency = Latência ltbcustom.bbb.highlighter.toolbar.triangle = Triângulo ltbcustom.bbb.highlighter.toolbar.triangle.accessibilityName = Mudar o cursor do quadro branco para triângulo ltbcustom.bbb.highlighter.toolbar.line = Linha diff --git a/bigbluebutton-client/resources/config.xml.template b/bigbluebutton-client/resources/config.xml.template index afb1e92d3b3738d99142dfe40f1d376a66f3585b..597f0e4f61a63ba11ccec90050b5848226abf646 100755 --- a/bigbluebutton-client/resources/config.xml.template +++ b/bigbluebutton-client/resources/config.xml.template @@ -8,12 +8,13 @@ <bwMon server="HOST" application="video/bwTest"/> <application uri="rtmp://HOST/bigbluebutton" host="http://HOST/bigbluebutton/api/enter"/> <language userSelectionEnabled="true" /> - <skinning enabled="true" url="http://HOST/client/branding/css/BBBDefault.css.swf" /> + <skinning enabled="true" url="http://HOST/client/branding/css/BBBDefault.css.swf?v=VERSION" /> + <branding logo="logo.png" copyright="© 2015 <a href="http://www.mconf.org" target="_blank">http://www.mconf.org</a>" background="" /> <shortcutKeys showButton="true" /> <browserVersions chrome="CHROME_VERSION" firefox="FIREFOX_VERSION" flash="FLASH_VERSION" java="1.7.0_51" /> <layout showLogButton="false" defaultLayout="bbb.layout.name.defaultlayout" showToolbar="true" showFooter="true" showMeetingName="true" showHelpButton="true" - showLogoutWindow="true" showLayoutTools="true" confirmLogout="true" + showLogoutWindow="true" showLayoutTools="true" confirmLogout="true" showNetworkMonitor="true" showRecordingNotification="true"/> <meeting muteOnStart="false" /> <lock disableCamForLockedUsers="false" disableMicForLockedUsers="false" disablePrivateChatForLockedUsers="false" @@ -53,12 +54,14 @@ uri="rtmp://HOST/sip" autoJoin="true" listenOnlyMode="true" + forceListenOnly="false" presenterShareOnly="false" skipCheck="false" showButton="true" enabledEchoCancel="true" useWebRTCIfAvailable="true" showPhoneOption="false" + showMicrophoneHint="true" echoTestApp="9196" dependsOn="UsersModule" /> @@ -66,10 +69,8 @@ <module name="VideoconfModule" url="http://HOST/client/VideoconfModule.swf?v=VERSION" uri="rtmp://HOST/video" dependson = "UsersModule" - videoQuality = "100" presenterShareOnly = "false" controlsForPresenter = "false" - resolutions = "320x240,640x480,1280x720" autoStart = "false" skipCamSettingsCheck="false" showButton = "true" @@ -77,22 +78,16 @@ publishWindowVisible = "true" viewerWindowMaxed = "false" viewerWindowLocation = "top" - camKeyFrameInterval = "30" - camModeFps = "10" - camQualityBandwidth = "0" - camQualityPicture = "90" smoothVideo="false" applyConvolutionFilter="false" convolutionFilter="-1, 0, -1, 0, 6, 0, -1, 0, -1" filterBias="0" filterDivisor="4" - enableH264 = "true" - h264Level = "2.1" - h264Profile = "main" displayAvatar = "false" focusTalking = "false" glowColor = "0x4A931D" glowBlurSize = "30.0" + priorityRatio = "0.67" /> <module name="WhiteboardModule" url="http://HOST/client/WhiteboardModule.swf?v=VERSION" @@ -102,14 +97,7 @@ whiteboardAccess="presenter" keepToolbarVisible="false" /> -<!-- - <module name="PollingModule" url="http://HOST/client/PollingModule.swf?v=VERSION" - uri="rtmp://HOST/bigbluebutton" - dependsOn="PresentModule" - /> - ---> <module name="PresentModule" url="http://HOST/client/PresentModule.swf?v=VERSION" uri="rtmp://HOST/bigbluebutton" host="http://HOST" @@ -121,41 +109,18 @@ maxFileSize="30" /> - <module name="VideodockModule" url="http://HOST/client/VideodockModule.swf?v=VERSION" - uri="rtmp://HOST/bigbluebutton" - dependsOn="VideoconfModule, UsersModule" - autoDock="true" - showControls="true" - maximizeWindow="false" - position="bottom-right" - width="172" - height="179" - layout="smart" - oneAlwaysBigger="false" - baseTabIndex="401" - /> - <module name="LayoutModule" url="http://HOST/client/LayoutModule.swf?v=VERSION" uri="rtmp://HOST/bigbluebutton" layoutConfig="http://HOST/client/conf/layout.xml" - enableEdit="false" + enableEdit="true" /> -<!-- - <module name="NotesModule" url="http://HOST/client/NotesModule.swf?v=VERSION" - saveURL="http://HOST" - position="top-left" - /> - - <module name="BroadcastModule" url="http://HOST/client/BroadcastModule.swf?v=VERSION" - uri="rtmp://HOST/bigbluebutton" - streamsUri="http://HOST/streams.xml" - position="top-left" - showStreams="true" - autoPlay="false" - dependsOn="UsersModule" - /> ---> - + <module name="SharedNotesModule" url="SharedNotesModule.swf?v=VERSION" + uri="rtmp://HOST/bigbluebutton" + refreshDelay="500" + enableMultipleNotes="true" + dependsOn="UsersModule" + position="bottom-left" + /> </modules> </config> diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/MconfLive.html similarity index 96% rename from bigbluebutton-client/resources/prod/BigBlueButton.html rename to bigbluebutton-client/resources/prod/MconfLive.html index 87d1161447be28cb8ad059bc0fb773b039e6df8a..d85cdb4ae825191af92e2b635cb3bc39ed7f0740 100755 --- a/bigbluebutton-client/resources/prod/BigBlueButton.html +++ b/bigbluebutton-client/resources/prod/MconfLive.html @@ -32,6 +32,7 @@ params.quality = "high"; params.bgcolor = "#869ca7"; params.allowfullscreen = "true"; + params.allowfullscreeninteractive = "true"; params.wmode = "window"; params.allowscriptaccess = "true"; params.seamlesstabbing = "true"; @@ -74,6 +75,8 @@ <script src="lib/sip.js?v=VERSION" language="javascript"></script> <script src="lib/bbb_webrtc_bridge_sip.js?v=VERSION" language="javascript"></script> <script src="lib/weburl_regex.js?v=VERSION" language="javascript"></script> + <script src="lib/diff_match_patch_uncompressed.js?v=VERSION" language="javascript"></script> + <script src="lib/shared_notes.js?v=VERSION" language="javascript"></script> <script> window.chatLinkClicked = function(url) { window.open(url, '_blank'); diff --git a/bigbluebutton-client/resources/prod/background.jpg b/bigbluebutton-client/resources/prod/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e043c17469332492be05af752cd7dcb5233998f Binary files /dev/null and b/bigbluebutton-client/resources/prod/background.jpg differ diff --git a/bigbluebutton-client/resources/prod/layout.xml b/bigbluebutton-client/resources/prod/layout.xml index cdcc9d0eda9b21c0e5f3b8c5b9f3c13873b8365c..cb9929b8aabc3c754cd03bb2346044642bfbfe3a 100755 --- a/bigbluebutton-client/resources/prod/layout.xml +++ b/bigbluebutton-client/resources/prod/layout.xml @@ -4,11 +4,12 @@ <window name="NotesWindow" hidden="true" width="0.7" height="1" x="0" y="0" draggable="false" resizable="false"/> <window name="BroadcastWindow" hidden="true" draggable="false" resizable="false"/> <window name="PresentationWindow" width="0.5137481910274964" height="0.9946808510638298" x="0.18017366136034732" y="0" /> - <window name="VideoDock" width="0.1772793053545586" height="0.30851063829787234" x="0" y="0.6875" minWidth="280" /> - <window name="ChatWindow" width="0.3031837916063676" height="0.9960106382978723" x="0.6968162083936325" y="0" /> + <window name="VideoDock" width="0.3031837916063676" height="0.41255006675567424" x="0.6968162083936325" y="0.582109479305741" /> + <window name="ChatWindow" width="0.3031837916063676" height="0.5767690253671562" x="0.6968162083936325" y="0" /> <window name="UsersWindow" width="0.1772793053545586" height="0.6795212765957446" x="0" y="0" minWidth="280" /> <window name="ViewersWindow" width="0.1772793053545586" height="0.33643617021276595" x="0" y="0" /> <window name="ListenersWindow" width="0.1772793053545586" height="0.33643617021276595" x="0" y="0.34308510638297873" /> + <window name="SharedNotesWindow" width="0.1772793053545586" height="0.30851063829787234" minWidth="280" x="0" y="0.6875" /> </layout> <layout name="bbb.layout.name.videochat"> <window name="NotesWindow" hidden="true" width="0.7" height="1" x="0" y="0" draggable="false" resizable="false"/> @@ -19,6 +20,7 @@ <window name="UsersWindow" minimized="true" hidden="true" order="2"/> <window name="ViewersWindow" hidden="true" draggable="false" resizable="false" /> <window name="ListenersWindow" hidden="true" draggable="false" resizable="false"/> + <window name="SharedNotesWindow" hidden="true" /> </layout> <layout name="bbb.layout.name.webcamsfocus"> <window name="NotesWindow" hidden="true" width="0.7" height="1" x="0" y="0" draggable="false" resizable="false"/> @@ -26,7 +28,9 @@ <window name="VideoDock" width="0.6570188133140377" height="0.9960106382978723" x="0" y="0" /> <window name="ChatWindow" width="0.3393632416787265" height="0.5305851063829787" x="0.658465991316932" y="0" /> <window name="UsersWindow" hidden="true" /> - <window name="PresentationWindow" width="0.34008683068017365" height="0.4601063829787234" x="0.658465991316932" y="0.535904255319149" /> + <window name="PresentationWindow" width="0.34008683068017365" height="0.4601063829787234" x="0.658465991316932" y="0.535904255319149" order="1" /> + <window name="DesktopViewWindow" width="0.34008683068017365" height="0.4601063829787234" x="0.658465991316932" y="0.535904255319149" order="0" /> + <window name="SharedNotesWindow" hidden="true" /> </layout> <layout name="bbb.layout.name.presentfocus"> <window name="NotesWindow" hidden="true" width="0.7" height="1" x="0" y="0" draggable="false" resizable="false"/> @@ -35,6 +39,7 @@ <window name="VideoDock" width="0.2923611111111111" height="0.4640957446808511" x="0.7048611111111112" y="0.535904255319149" /> <window name="PresentationWindow" width="0.7027777777777777" height="0.9986702127659575" x="0" y="0" /> <window name="ChatWindow" width="0.2923611111111111" height="0.5305851063829787" x="0.7048611111111112" y="0" /> + <window name="SharedNotesWindow" hidden="true" /> </layout> <layout name="bbb.layout.name.lectureassistant"> <window name="NotesWindow" hidden="true" width="0.7" height="1" x="0" y="0" draggable="false" resizable="false"/> @@ -43,6 +48,7 @@ <window name="UsersWindow" width="0.22152777777777777" height="0.9958677685950413" x="0" y="0" minWidth="280" /> <window name="PresentationWindow" width="0.3104166666666667" height="0.5537190082644629" x="0.6895833333333333" y="0" /> <window name="VideoDock" width="0.30972222222222223" height="0.4357198347107438" x="0.6902777777777778" y="0.558870523415978" /> + <window name="SharedNotesWindow" hidden="true" /> </layout> <layout name="bbb.layout.name.lecture"> <window name="NotesWindow" hidden="true" width="0.7" height="1" x="0" y="0" draggable="false" resizable="false"/> @@ -51,6 +57,7 @@ <window name="VideoDock" width="0.2923611111111111" height="0.4640957446808511" x="0.7048611111111112" y="0.535904255319149" /> <window name="PresentationWindow" width="0.7027777777777777" height="0.9986702127659575" x="0" y="0" /> <window name="ChatWindow" width="0.2923611111111111" height="0.5305851063829787" x="0.7048611111111112" y="0" /> + <window name="SharedNotesWindow" hidden="true" /> </layout> <layout name="bbb.layout.name.lecture" role="presenter"> <window name="NotesWindow" hidden="true" width="0.7" height="1" x="0" y="0" draggable="false" resizable="false"/> @@ -59,6 +66,7 @@ <window name="UsersWindow" hidden="true" /> <window name="PresentationWindow" maximized="true" /> <window name="VideoDock" hidden="true" /> + <window name="SharedNotesWindow" hidden="true" /> </layout> <layout name="bbb.layout.name.lecture" role="moderator"> <window name="NotesWindow" hidden="true" width="0.7" height="1" x="0" y="0" draggable="false" resizable="false"/> @@ -67,6 +75,7 @@ <window name="UsersWindow" width="0.22152777777777777" height="0.9944903581267218" x="0" y="0" minWidth="280" /> <window name="PresentationWindow" width="0.3104166666666667" height="0.5537190082644629" x="0.6895833333333333" y="0" /> <window name="VideoDock" width="0.30972222222222223" height="0.4256198347107438" x="0.6902777777777778" y="0.568870523415978" /> + <window name="SharedNotesWindow" hidden="true" /> </layout> <!-- <layout name="Users"> diff --git a/bigbluebutton-client/resources/prod/lib/bbb_blinker.js b/bigbluebutton-client/resources/prod/lib/bbb_blinker.js index 970dfc07053a996ceb010879f2b0744693619086..c2f1db1e9d7d08b5a9a0b5b5d9256d6b5853b93d 100755 --- a/bigbluebutton-client/resources/prod/lib/bbb_blinker.js +++ b/bigbluebutton-client/resources/prod/lib/bbb_blinker.js @@ -1,5 +1,5 @@ function setTitle(title){ - document.title= "BigBlueButton - " + title; + document.title= title; } function clientReady(message){ diff --git a/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js b/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js index 782e3c5f9f01f0c10eed9820e137cc1cec823d8c..0a5eb75c596717292650e718ad33f2be41ba081f 100755 --- a/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js +++ b/bigbluebutton-client/resources/prod/lib/bbb_webrtc_bridge_sip.js @@ -411,6 +411,17 @@ function make_call(username, voiceBridge, server, callback, recall, isListenOnly console.log('bye event already received'); } }); + currentSession.on('cancel', function(request){ + callActive = false; + + if (currentSession) { + console.log('call canceled'); + clearTimeout(callTimeout); + currentSession = null; + } else { + console.log('cancel event already received'); + } + }); currentSession.on('accepted', function(data){ callActive = true; console.log('BigBlueButton call accepted'); @@ -467,7 +478,12 @@ function webrtc_hangup(callback) { if (callback) { currentSession.on('bye', callback); } - currentSession.bye(); + try { + currentSession.bye(); + } catch (err) { + console.log("Forcing to cancel current session"); + currentSession.cancel(); + } } function isWebRTCAvailable() { diff --git a/bigbluebutton-client/resources/prod/lib/deployJava.js b/bigbluebutton-client/resources/prod/lib/deployJava.js index 7d06cb5ca13b4a0b22cd28cbce230d4d5b8bd1b5..8cfb0afcc17cec02a76399ad7b9402745be9cde6 100755 --- a/bigbluebutton-client/resources/prod/lib/deployJava.js +++ b/bigbluebutton-client/resources/prod/lib/deployJava.js @@ -1,2 +1,2 @@ -/* Copy of the file found at http://www.java.com/js/deployJava.js - January 18, 2015 */ +/* Copy of the file found at http://www.java.com/js/deployJava.js - January 18, 2015 */ var deployJava=function(){var l={core:["id","class","title","style"],i18n:["lang","dir"],events:["onclick","ondblclick","onmousedown","onmouseup","onmouseover","onmousemove","onmouseout","onkeypress","onkeydown","onkeyup"],applet:["codebase","code","name","archive","object","width","height","alt","align","hspace","vspace"],object:["classid","codebase","codetype","data","type","archive","declare","standby","height","width","usemap","name","tabindex","align","border","hspace","vspace"]};var b=l.object.concat(l.core,l.i18n,l.events);var m=l.applet.concat(l.core);function g(o){if(!d.debug){return}if(console.log){console.log(o)}else{alert(o)}}function k(p,o){if(p==null||p.length==0){return true}var r=p.charAt(p.length-1);if(r!="+"&&r!="*"&&(p.indexOf("_")!=-1&&r!="_")){p=p+"*";r="*"}p=p.substring(0,p.length-1);if(p.length>0){var q=p.charAt(p.length-1);if(q=="."||q=="_"){p=p.substring(0,p.length-1)}}if(r=="*"){return(o.indexOf(p)==0)}else{if(r=="+"){return p<=o}}return false}function e(){var o="//java.com/js/webstart.png";try{return document.location.protocol.indexOf("http")!=-1?o:"http:"+o}catch(p){return"http:"+o}}function n(p){var o="http://java.com/dt-redirect";if(p==null||p.length==0){return o}if(p.charAt(0)=="&"){p=p.substring(1,p.length)}return o+"?"+p}function j(q,p){var o=q.length;for(var r=0;r<o;r++){if(q[r]===p){return true}}return false}function c(o){return j(m,o.toLowerCase())}function i(o){return j(b,o.toLowerCase())}function a(o){if("MSIE"!=deployJava.browserName){return true}if(deployJava.compareVersionToPattern(deployJava.getPlugin().version,["10","0","0"],false,true)){return true}if(o==null){return false}return !k("1.6.0_33+",o)}var d={debug:null,version:"20120801",firefoxJavaVersion:null,myInterval:null,preInstallJREList:null,returnPage:null,brand:null,locale:null,installType:null,EAInstallEnabled:false,EarlyAccessURL:null,oldMimeType:"application/npruntime-scriptable-plugin;DeploymentToolkit",mimeType:"application/java-deployment-toolkit",launchButtonPNG:e(),browserName:null,browserName2:null,getJREs:function(){var t=new Array();if(this.isPluginInstalled()){var r=this.getPlugin();var o=r.jvms;for(var q=0;q<o.getLength();q++){t[q]=o.get(q).version}}else{var p=this.getBrowser();if(p=="MSIE"){if(this.testUsingActiveX("1.7.0")){t[0]="1.7.0"}else{if(this.testUsingActiveX("1.6.0")){t[0]="1.6.0"}else{if(this.testUsingActiveX("1.5.0")){t[0]="1.5.0"}else{if(this.testUsingActiveX("1.4.2")){t[0]="1.4.2"}else{if(this.testForMSVM()){t[0]="1.1"}}}}}}else{if(p=="Netscape Family"){this.getJPIVersionUsingMimeType();if(this.firefoxJavaVersion!=null){t[0]=this.firefoxJavaVersion}else{if(this.testUsingMimeTypes("1.7")){t[0]="1.7.0"}else{if(this.testUsingMimeTypes("1.6")){t[0]="1.6.0"}else{if(this.testUsingMimeTypes("1.5")){t[0]="1.5.0"}else{if(this.testUsingMimeTypes("1.4.2")){t[0]="1.4.2"}else{if(this.browserName2=="Safari"){if(this.testUsingPluginsArray("1.7.0")){t[0]="1.7.0"}else{if(this.testUsingPluginsArray("1.6")){t[0]="1.6.0"}else{if(this.testUsingPluginsArray("1.5")){t[0]="1.5.0"}else{if(this.testUsingPluginsArray("1.4.2")){t[0]="1.4.2"}}}}}}}}}}}}}if(this.debug){for(var q=0;q<t.length;++q){g("[getJREs()] We claim to have detected Java SE "+t[q])}}return t},installJRE:function(r,p){var o=false;if(this.isPluginInstalled()&&this.isAutoInstallEnabled(r)){var q=false;if(this.isCallbackSupported()){q=this.getPlugin().installJRE(r,p)}else{q=this.getPlugin().installJRE(r)}if(q){this.refresh();if(this.returnPage!=null){document.location=this.returnPage}}return q}else{return this.installLatestJRE()}},isAutoInstallEnabled:function(o){if(!this.isPluginInstalled()){return false}if(typeof o=="undefined"){o=null}return a(o)},isCallbackSupported:function(){return this.isPluginInstalled()&&this.compareVersionToPattern(this.getPlugin().version,["10","2","0"],false,true)},installLatestJRE:function(q){if(this.isPluginInstalled()&&this.isAutoInstallEnabled()){var r=false;if(this.isCallbackSupported()){r=this.getPlugin().installLatestJRE(q)}else{r=this.getPlugin().installLatestJRE()}if(r){this.refresh();if(this.returnPage!=null){document.location=this.returnPage}}return r}else{var p=this.getBrowser();var o=navigator.platform.toLowerCase();if((this.EAInstallEnabled=="true")&&(o.indexOf("win")!=-1)&&(this.EarlyAccessURL!=null)){this.preInstallJREList=this.getJREs();if(this.returnPage!=null){this.myInterval=setInterval("deployJava.poll()",3000)}location.href=this.EarlyAccessURL;return false}else{if(p=="MSIE"){return this.IEInstall()}else{if((p=="Netscape Family")&&(o.indexOf("win32")!=-1)){return this.FFInstall()}else{location.href=n(((this.returnPage!=null)?("&returnPage="+this.returnPage):"")+((this.locale!=null)?("&locale="+this.locale):"")+((this.brand!=null)?("&brand="+this.brand):""))}}return false}}},runApplet:function(p,u,r){if(r=="undefined"||r==null){r="1.1"}var t="^(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(?:_(\\d+))?)?)?$";var o=r.match(t);if(this.returnPage==null){this.returnPage=document.location}if(o!=null){var q=this.getBrowser();if(q!="?"){if(this.versionCheck(r+"+")){this.writeAppletTag(p,u)}else{if(this.installJRE(r+"+")){this.refresh();location.href=document.location;this.writeAppletTag(p,u)}}}else{this.writeAppletTag(p,u)}}else{g("[runApplet()] Invalid minimumVersion argument to runApplet():"+r)}},writeAppletTag:function(r,w){var o="<"+"applet ";var q="";var t="<"+"/"+"applet"+">";var x=true;if(null==w||typeof w!="object"){w=new Object()}for(var p in r){if(!c(p)){w[p]=r[p]}else{o+=(" "+p+'="'+r[p]+'"');if(p=="code"){x=false}}}var v=false;for(var u in w){if(u=="codebase_lookup"){v=true}if(u=="object"||u=="java_object"||u=="java_code"){x=false}q+='<param name="'+u+'" value="'+w[u]+'"/>'}if(!v){q+='<param name="codebase_lookup" value="false"/>'}if(x){o+=(' code="dummy"')}o+=">";document.write(o+"\n"+q+"\n"+t)},versionCheck:function(p){var v=0;var x="^(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(?:_(\\d+))?)?)?(\\*|\\+)?$";var y=p.match(x);if(y!=null){var r=false;var u=false;var q=new Array();for(var t=1;t<y.length;++t){if((typeof y[t]=="string")&&(y[t]!="")){q[v]=y[t];v++}}if(q[q.length-1]=="+"){u=true;r=false;q.length--}else{if(q[q.length-1]=="*"){u=false;r=true;q.length--}else{if(q.length<4){u=false;r=true}}}var w=this.getJREs();for(var t=0;t<w.length;++t){if(this.compareVersionToPattern(w[t],q,r,u)){return true}}return false}else{var o="Invalid versionPattern passed to versionCheck: "+p;g("[versionCheck()] "+o);alert(o);return false}},isWebStartInstalled:function(r){var q=this.getBrowser();if(q=="?"){return true}if(r=="undefined"||r==null){r="1.4.2"}var p=false;var t="^(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(?:_(\\d+))?)?)?$";var o=r.match(t);if(o!=null){p=this.versionCheck(r+"+")}else{g("[isWebStartInstaller()] Invalid minimumVersion argument to isWebStartInstalled(): "+r);p=this.versionCheck("1.4.2+")}return p},getJPIVersionUsingMimeType:function(){for(var p=0;p<navigator.mimeTypes.length;++p){var q=navigator.mimeTypes[p].type;var o=q.match(/^application\/x-java-applet;jpi-version=(.*)$/);if(o!=null){this.firefoxJavaVersion=o[1];if("Opera"!=this.browserName2){break}}}},launchWebStartApplication:function(r){var o=navigator.userAgent.toLowerCase();this.getJPIVersionUsingMimeType();if(this.isWebStartInstalled("1.7.0")==false){if((this.installJRE("1.7.0+")==false)||((this.isWebStartInstalled("1.7.0")==false))){return false}}var u=null;if(document.documentURI){u=document.documentURI}if(u==null){u=document.URL}var p=this.getBrowser();var q;if(p=="MSIE"){q="<"+'object classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" '+'width="0" height="0">'+"<"+'PARAM name="launchjnlp" value="'+r+'"'+">"+"<"+'PARAM name="docbase" value="'+u+'"'+">"+"<"+"/"+"object"+">"}else{if(p=="Netscape Family"){q="<"+'embed type="application/x-java-applet;jpi-version='+this.firefoxJavaVersion+'" '+'width="0" height="0" '+'launchjnlp="'+r+'"'+'docbase="'+u+'"'+" />"}}if(document.body=="undefined"||document.body==null){document.write(q);document.location=u}else{var t=document.createElement("div");t.id="div1";t.style.position="relative";t.style.left="-10000px";t.style.margin="0px auto";t.className="dynamicDiv";t.innerHTML=q;document.body.appendChild(t)}},createWebStartLaunchButtonEx:function(q,p){if(this.returnPage==null){this.returnPage=q}var o="javascript:deployJava.launchWebStartApplication('"+q+"');";document.write("<"+'a href="'+o+"\" onMouseOver=\"window.status=''; "+'return true;"><'+"img "+'src="'+this.launchButtonPNG+'" '+'border="0" /><'+"/"+"a"+">")},createWebStartLaunchButton:function(q,p){if(this.returnPage==null){this.returnPage=q}var o="javascript:"+"if (!deployJava.isWebStartInstalled(""+p+"")) {"+"if (deployJava.installLatestJRE()) {"+"if (deployJava.launch(""+q+"")) {}"+"}"+"} else {"+"if (deployJava.launch(""+q+"")) {}"+"}";document.write("<"+'a href="'+o+"\" onMouseOver=\"window.status=''; "+'return true;"><'+"img "+'src="'+this.launchButtonPNG+'" '+'border="0" /><'+"/"+"a"+">")},launch:function(o){document.location=o;return true},isPluginInstalled:function(){var o=this.getPlugin();if(o&&o.jvms){return true}else{return false}},isAutoUpdateEnabled:function(){if(this.isPluginInstalled()){return this.getPlugin().isAutoUpdateEnabled()}return false},setAutoUpdateEnabled:function(){if(this.isPluginInstalled()){return this.getPlugin().setAutoUpdateEnabled()}return false},setInstallerType:function(o){this.installType=o;if(this.isPluginInstalled()){return this.getPlugin().setInstallerType(o)}return false},setAdditionalPackages:function(o){if(this.isPluginInstalled()){return this.getPlugin().setAdditionalPackages(o)}return false},setEarlyAccess:function(o){this.EAInstallEnabled=o},isPlugin2:function(){if(this.isPluginInstalled()){if(this.versionCheck("1.6.0_10+")){try{return this.getPlugin().isPlugin2()}catch(o){}}}return false},allowPlugin:function(){this.getBrowser();var o=("Safari"!=this.browserName2&&"Opera"!=this.browserName2);return o},getPlugin:function(){this.refresh();var o=null;if(this.allowPlugin()){o=document.getElementById("deployJavaPlugin")}return o},compareVersionToPattern:function(v,p,r,t){if(v==undefined||p==undefined){return false}var w="^(\\d+)(?:\\.(\\d+)(?:\\.(\\d+)(?:_(\\d+))?)?)?$";var x=v.match(w);if(x!=null){var u=0;var y=new Array();for(var q=1;q<x.length;++q){if((typeof x[q]=="string")&&(x[q]!="")){y[u]=x[q];u++}}var o=Math.min(y.length,p.length);if(t){for(var q=0;q<o;++q){if(y[q]<p[q]){return false}else{if(y[q]>p[q]){return true}}}return true}else{for(var q=0;q<o;++q){if(y[q]!=p[q]){return false}}if(r){return true}else{return(y.length==p.length)}}}else{return false}},getBrowser:function(){if(this.browserName==null){var o=navigator.userAgent.toLowerCase();g("[getBrowser()] navigator.userAgent.toLowerCase() -> "+o);if((o.indexOf("msie")!=-1)&&(o.indexOf("opera")==-1)){this.browserName="MSIE";this.browserName2="MSIE"}else{if(o.indexOf("trident")!=-1||o.indexOf("Trident")!=-1){this.browserName="MSIE";this.browserName2="MSIE"}else{if(o.indexOf("iphone")!=-1){this.browserName="Netscape Family";this.browserName2="iPhone"}else{if((o.indexOf("firefox")!=-1)&&(o.indexOf("opera")==-1)){this.browserName="Netscape Family";this.browserName2="Firefox"}else{if(o.indexOf("chrome")!=-1){this.browserName="Netscape Family";this.browserName2="Chrome"}else{if(o.indexOf("safari")!=-1){this.browserName="Netscape Family";this.browserName2="Safari"}else{if((o.indexOf("mozilla")!=-1)&&(o.indexOf("opera")==-1)){this.browserName="Netscape Family";this.browserName2="Other"}else{if(o.indexOf("opera")!=-1){this.browserName="Netscape Family";this.browserName2="Opera"}else{this.browserName="?";this.browserName2="unknown"}}}}}}}}g("[getBrowser()] Detected browser name:"+this.browserName+", "+this.browserName2)}return this.browserName},testUsingActiveX:function(o){var q="JavaWebStart.isInstalled."+o+".0";if(typeof ActiveXObject=="undefined"||!ActiveXObject){g("[testUsingActiveX()] Browser claims to be IE, but no ActiveXObject object?");return false}try{return(new ActiveXObject(q)!=null)}catch(p){return false}},testForMSVM:function(){var p="{08B0E5C0-4FCB-11CF-AAA5-00401C608500}";if(typeof oClientCaps!="undefined"){var o=oClientCaps.getComponentVersion(p,"ComponentID");if((o=="")||(o=="5,0,5000,0")){return false}else{return true}}else{return false}},testUsingMimeTypes:function(p){if(!navigator.mimeTypes){g("[testUsingMimeTypes()] Browser claims to be Netscape family, but no mimeTypes[] array?");return false}for(var q=0;q<navigator.mimeTypes.length;++q){s=navigator.mimeTypes[q].type;var o=s.match(/^application\/x-java-applet\x3Bversion=(1\.8|1\.7|1\.6|1\.5|1\.4\.2)$/);if(o!=null){if(this.compareVersions(o[1],p)){return true}}}return false},testUsingPluginsArray:function(p){if((!navigator.plugins)||(!navigator.plugins.length)){return false}var o=navigator.platform.toLowerCase();for(var q=0;q<navigator.plugins.length;++q){s=navigator.plugins[q].description;if(s.search(/^Java Switchable Plug-in (Cocoa)/)!=-1){if(this.compareVersions("1.5.0",p)){return true}}else{if(s.search(/^Java/)!=-1){if(o.indexOf("win")!=-1){if(this.compareVersions("1.5.0",p)||this.compareVersions("1.6.0",p)){return true}}}}}if(this.compareVersions("1.5.0",p)){return true}return false},IEInstall:function(){location.href=n(((this.returnPage!=null)?("&returnPage="+this.returnPage):"")+((this.locale!=null)?("&locale="+this.locale):"")+((this.brand!=null)?("&brand="+this.brand):""));return false},done:function(p,o){},FFInstall:function(){location.href=n(((this.returnPage!=null)?("&returnPage="+this.returnPage):"")+((this.locale!=null)?("&locale="+this.locale):"")+((this.brand!=null)?("&brand="+this.brand):"")+((this.installType!=null)?("&type="+this.installType):""));return false},compareVersions:function(r,t){var p=r.split(".");var o=t.split(".");for(var q=0;q<p.length;++q){p[q]=Number(p[q])}for(var q=0;q<o.length;++q){o[q]=Number(o[q])}if(p.length==2){p[2]=0}if(p[0]>o[0]){return true}if(p[0]<o[0]){return false}if(p[1]>o[1]){return true}if(p[1]<o[1]){return false}if(p[2]>o[2]){return true}if(p[2]<o[2]){return false}return true},enableAlerts:function(){this.browserName=null;this.debug=true},poll:function(){this.refresh();var o=this.getJREs();if((this.preInstallJREList.length==0)&&(o.length!=0)){clearInterval(this.myInterval);if(this.returnPage!=null){location.href=this.returnPage}}if((this.preInstallJREList.length!=0)&&(o.length!=0)&&(this.preInstallJREList[0]!=o[0])){clearInterval(this.myInterval);if(this.returnPage!=null){location.href=this.returnPage}}},writePluginTag:function(){var o=this.getBrowser();if(o=="MSIE"){document.write("<"+'object classid="clsid:CAFEEFAC-DEC7-0000-0001-ABCDEFFEDCBA" '+'id="deployJavaPlugin" width="0" height="0">'+"<"+"/"+"object"+">")}else{if(o=="Netscape Family"&&this.allowPlugin()){this.writeEmbedTag()}}},refresh:function(){navigator.plugins.refresh(false);var o=this.getBrowser();if(o=="Netscape Family"&&this.allowPlugin()){var p=document.getElementById("deployJavaPlugin");if(p==null){this.writeEmbedTag()}}},writeEmbedTag:function(){var o=false;if(navigator.mimeTypes!=null){for(var p=0;p<navigator.mimeTypes.length;p++){if(navigator.mimeTypes[p].type==this.mimeType){if(navigator.mimeTypes[p].enabledPlugin){document.write("<"+'embed id="deployJavaPlugin" type="'+this.mimeType+'" hidden="true" />');o=true}}}if(!o){for(var p=0;p<navigator.mimeTypes.length;p++){if(navigator.mimeTypes[p].type==this.oldMimeType){if(navigator.mimeTypes[p].enabledPlugin){document.write("<"+'embed id="deployJavaPlugin" type="'+this.oldMimeType+'" hidden="true" />')}}}}}}};d.writePluginTag();if(d.locale==null){var h=null;if(h==null){try{h=navigator.userLanguage}catch(f){}}if(h==null){try{h=navigator.systemLanguage}catch(f){}}if(h==null){try{h=navigator.language}catch(f){}}if(h!=null){h.replace("-","_");d.locale=h}}return d}(); \ No newline at end of file diff --git a/bigbluebutton-client/resources/prod/lib/diff_match_patch_uncompressed.js b/bigbluebutton-client/resources/prod/lib/diff_match_patch_uncompressed.js new file mode 100644 index 0000000000000000000000000000000000000000..bee148ba34f265566d8ca30c6856712f83a35213 --- /dev/null +++ b/bigbluebutton-client/resources/prod/lib/diff_match_patch_uncompressed.js @@ -0,0 +1,2274 @@ +/** + * Diff Match and Patch + * + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-diff-match-patch/ + * + * 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. + */ + +/** + * @fileoverview Computes the difference between two texts to create a patch. + * Applies the patch onto another text, allowing for errors. + * @author fraser@google.com (Neil Fraser) + */ + +/** + * Modified by Islam El-Ashi <ielashi@gmail.com> + * - Added unpatching support + */ + +/** + * Class containing the diff, match and patch methods. + * @constructor + */ +function diff_match_patch() { + + // Defaults. + // Redefine these in your program to override the defaults. + + // Number of seconds to map a diff before giving up (0 for infinity). + this.Diff_Timeout = 1.0; + // Cost of an empty edit operation in terms of edit characters. + this.Diff_EditCost = 4; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose). + this.Match_Threshold = 0.5; + // How far to search for a match (0 = exact location, 1000+ = broad match). + // A match this many characters away from the expected location will add + // 1.0 to the score (0.0 is a perfect match). + this.Match_Distance = 1000; + // When deleting a large block of text (over ~64 characters), how close does + // the contents have to match the expected contents. (0.0 = perfection, + // 1.0 = very loose). Note that Match_Threshold controls how closely the + // end points of a delete need to match. + this.Patch_DeleteThreshold = 0.5; + // Chunk size for context length. + this.Patch_Margin = 4; + + // The number of bits in an int. + this.Match_MaxBits = 32; +} + + +// DIFF FUNCTIONS + + +/** + * The data structure representing a diff is an array of tuples: + * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] + * which means: delete 'Hello', add 'Goodbye' and keep ' world.' + */ +var DIFF_DELETE = -1; +var DIFF_INSERT = 1; +var DIFF_EQUAL = 0; + +/** @typedef {!Array.<number|string>} */ +diff_match_patch.Diff; + + +/** + * Find the differences between two texts. Simplifies the problem by stripping + * any common prefix or suffix off the texts before diffing. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean=} opt_checklines Optional speedup flag. If present and false, + * then don't run a line-level diff first to identify the changed areas. + * Defaults to true, which does a faster, slightly less optimal diff. + * @param {number} opt_deadline Optional time when the diff should be complete + * by. Used internally for recursive calls. Users should set DiffTimeout + * instead. + * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. + */ +diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines, + opt_deadline) { + // Set a deadline by which time the diff must be complete. + if (typeof opt_deadline == 'undefined') { + if (this.Diff_Timeout <= 0) { + opt_deadline = Number.MAX_VALUE; + } else { + opt_deadline = (new Date).getTime() + this.Diff_Timeout * 1000; + } + } + var deadline = opt_deadline; + + // Check for null inputs. + if (text1 == null || text2 == null) { + throw new Error('Null input. (diff_main)'); + } + + // Check for equality (speedup). + if (text1 == text2) { + if (text1) { + return [[DIFF_EQUAL, text1]]; + } + return []; + } + + if (typeof opt_checklines == 'undefined') { + opt_checklines = true; + } + var checklines = opt_checklines; + + // Trim off common prefix (speedup). + var commonlength = this.diff_commonPrefix(text1, text2); + var commonprefix = text1.substring(0, commonlength); + text1 = text1.substring(commonlength); + text2 = text2.substring(commonlength); + + // Trim off common suffix (speedup). + commonlength = this.diff_commonSuffix(text1, text2); + var commonsuffix = text1.substring(text1.length - commonlength); + text1 = text1.substring(0, text1.length - commonlength); + text2 = text2.substring(0, text2.length - commonlength); + + // Compute the diff on the middle block. + var diffs = this.diff_compute_(text1, text2, checklines, deadline); + + // Restore the prefix and suffix. + if (commonprefix) { + diffs.unshift([DIFF_EQUAL, commonprefix]); + } + if (commonsuffix) { + diffs.push([DIFF_EQUAL, commonsuffix]); + } + this.diff_cleanupMerge(diffs); + return diffs; +}; + + +/** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_compute_ = function(text1, text2, checklines, + deadline) { + var diffs; + + if (!text1) { + // Just add some text (speedup). + return [[DIFF_INSERT, text2]]; + } + + if (!text2) { + // Just delete some text (speedup). + return [[DIFF_DELETE, text1]]; + } + + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + var i = longtext.indexOf(shorttext); + if (i != -1) { + // Shorter text is inside the longer text (speedup). + diffs = [[DIFF_INSERT, longtext.substring(0, i)], + [DIFF_EQUAL, shorttext], + [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; + // Swap insertions for deletions if diff is reversed. + if (text1.length > text2.length) { + diffs[0][0] = diffs[2][0] = DIFF_DELETE; + } + return diffs; + } + + if (shorttext.length == 1) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; + } + longtext = shorttext = null; // Garbage collect. + + // Check to see if the problem can be split in two. + var hm = this.diff_halfMatch_(text1, text2); + if (hm) { + // A half-match was found, sort out the return data. + var text1_a = hm[0]; + var text1_b = hm[1]; + var text2_a = hm[2]; + var text2_b = hm[3]; + var mid_common = hm[4]; + // Send both pairs off for separate processing. + var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline); + var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline); + // Merge the results. + return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b); + } + + if (checklines && text1.length > 100 && text2.length > 100) { + return this.diff_lineMode_(text1, text2, deadline); + } + + return this.diff_bisect_(text1, text2, deadline); +}; + + +/** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_lineMode_ = function(text1, text2, deadline) { + // Scan the text on a line-by-line basis first. + var a = this.diff_linesToChars_(text1, text2); + text1 = /** @type {string} */(a[0]); + text2 = /** @type {string} */(a[1]); + var linearray = /** @type {!Array.<string>} */(a[2]); + + var diffs = this.diff_bisect_(text1, text2, deadline); + + // Convert the diff back to original text. + this.diff_charsToLines_(diffs, linearray); + // Eliminate freak matches (e.g. blank lines) + this.diff_cleanupSemantic(diffs); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.push([DIFF_EQUAL, '']); + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + count_insert++; + text_insert += diffs[pointer][1]; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete >= 1 && count_insert >= 1) { + // Delete the offending records and add the merged ones. + var a = this.diff_main(text_delete, text_insert, false, deadline); + diffs.splice(pointer - count_delete - count_insert, + count_delete + count_insert); + pointer = pointer - count_delete - count_insert; + for (var j = a.length - 1; j >= 0; j--) { + diffs.splice(pointer, 0, a[j]); + } + pointer = pointer + a.length; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + pointer++; + } + diffs.pop(); // Remove the dummy entry at the end. + + return diffs; +}; + + +/** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_bisect_ = function(text1, text2, deadline) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + var max_d = Math.ceil((text1_length + text2_length) / 2); + var v_offset = max_d; + var v_length = 2 * max_d; + var v1 = new Array(v_length); + var v2 = new Array(v_length); + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for (var x = 0; x < v_length; x++) { + v1[x] = -1; + v2[x] = -1; + } + v1[v_offset + 1] = 0; + v2[v_offset + 1] = 0; + var delta = text1_length - text2_length; + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + var front = (delta % 2 != 0); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + var k1start = 0; + var k1end = 0; + var k2start = 0; + var k2end = 0; + for (var d = 0; d < max_d; d++) { + // Bail out if deadline is reached. + if ((new Date()).getTime() > deadline) { + break; + } + + // Walk the front path one step. + for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { + var k1_offset = v_offset + k1; + var x1; + if (k1 == -d || k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1]) { + x1 = v1[k1_offset + 1]; + } else { + x1 = v1[k1_offset - 1] + 1; + } + var y1 = x1 - k1; + while (x1 < text1_length && y1 < text2_length && + text1.charAt(x1) == text2.charAt(y1)) { + x1++; + y1++; + } + v1[k1_offset] = x1; + if (x1 > text1_length) { + // Ran off the right of the graph. + k1end += 2; + } else if (y1 > text2_length) { + // Ran off the bottom of the graph. + k1start += 2; + } else if (front) { + var k2_offset = v_offset + delta - k1; + if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) { + // Mirror x2 onto top-left coordinate system. + var x2 = text1_length - v2[k2_offset]; + if (x1 >= x2) { + // Overlap detected. + return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); + } + } + } + } + + // Walk the reverse path one step. + for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { + var k2_offset = v_offset + k2; + var x2; + if (k2 == -d || k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1]) { + x2 = v2[k2_offset + 1]; + } else { + x2 = v2[k2_offset - 1] + 1; + } + var y2 = x2 - k2; + while (x2 < text1_length && y2 < text2_length && + text1.charAt(text1_length - x2 - 1) == + text2.charAt(text2_length - y2 - 1)) { + x2++; + y2++; + } + v2[k2_offset] = x2; + if (x2 > text1_length) { + // Ran off the left of the graph. + k2end += 2; + } else if (y2 > text2_length) { + // Ran off the top of the graph. + k2start += 2; + } else if (!front) { + var k1_offset = v_offset + delta - k2; + if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) { + var x1 = v1[k1_offset]; + var y1 = v_offset + x1 - k1_offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1_length - x2; + if (x1 >= x2) { + // Overlap detected. + return this.diff_bisectSplit_(text1, text2, x1, y1, deadline); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; +}; + + +/** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. + * @private + */ +diff_match_patch.prototype.diff_bisectSplit_ = function(text1, text2, x, y, + deadline) { + var text1a = text1.substring(0, x); + var text2a = text2.substring(0, y); + var text1b = text1.substring(x); + var text2b = text2.substring(y); + + // Compute both diffs serially. + var diffs = this.diff_main(text1a, text2a, false, deadline); + var diffsb = this.diff_main(text1b, text2b, false, deadline); + + return diffs.concat(diffsb); +}; + + +/** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {!Array.<string|!Array.<string>>} Three element Array, containing the + * encoded text1, the encoded text2 and the array of unique strings. The + * zeroth element of the array of unique strings is intentionally blank. + * @private + */ +diff_match_patch.prototype.diff_linesToChars_ = function(text1, text2) { + var lineArray = []; // e.g. lineArray[4] == 'Hello\n' + var lineHash = {}; // e.g. lineHash['Hello\n'] == 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray[0] = ''; + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode. + * @return {string} Encoded string. + * @private + */ + function diff_linesToCharsMunge_(text) { + var chars = ''; + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + var lineStart = 0; + var lineEnd = -1; + // Keeping our own length variable is faster than looking it up. + var lineArrayLength = lineArray.length; + while (lineEnd < text.length - 1) { + lineEnd = text.indexOf('\n', lineStart); + if (lineEnd == -1) { + lineEnd = text.length - 1; + } + var line = text.substring(lineStart, lineEnd + 1); + lineStart = lineEnd + 1; + + if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : + (lineHash[line] !== undefined)) { + chars += String.fromCharCode(lineHash[line]); + } else { + chars += String.fromCharCode(lineArrayLength); + lineHash[line] = lineArrayLength; + lineArray[lineArrayLength++] = line; + } + } + return chars; + } + + var chars1 = diff_linesToCharsMunge_(text1); + var chars2 = diff_linesToCharsMunge_(text2); + return [chars1, chars2, lineArray]; +}; + + +/** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + * @param {!Array.<string>} lineArray Array of unique strings. + * @private + */ +diff_match_patch.prototype.diff_charsToLines_ = function(diffs, lineArray) { + for (var x = 0; x < diffs.length; x++) { + var chars = diffs[x][1]; + var text = []; + for (var y = 0; y < chars.length; y++) { + text[y] = lineArray[chars.charCodeAt(y)]; + } + diffs[x][1] = text.join(''); + } +}; + + +/** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ +diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerstart = 0; + while (pointermin < pointermid) { + if (text1.substring(pointerstart, pointermid) == + text2.substring(pointerstart, pointermid)) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; +}; + + +/** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ +diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) { + // Quick check for common null cases. + if (!text1 || !text2 || + text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + var pointermin = 0; + var pointermax = Math.min(text1.length, text2.length); + var pointermid = pointermax; + var pointerend = 0; + while (pointermin < pointermid) { + if (text1.substring(text1.length - pointermid, text1.length - pointerend) == + text2.substring(text2.length - pointermid, text2.length - pointerend)) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); + } + return pointermid; +}; + + +/** + * Determine if the suffix of one string is the prefix of another. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of the first + * string and the start of the second string. + * @private + */ +diff_match_patch.prototype.diff_commonOverlap_ = function(text1, text2) { + // Cache the text lengths to prevent multiple calls. + var text1_length = text1.length; + var text2_length = text2.length; + // Eliminate the null case. + if (text1_length == 0 || text2_length == 0) { + return 0; + } + // Truncate the longer string. + if (text1_length > text2_length) { + text1 = text1.substring(text1_length - text2_length); + } else if (text1_length < text2_length) { + text2 = text2.substring(0, text1_length); + } + var text_length = Math.min(text1_length, text2_length); + // Quick check for the worst case. + if (text1 == text2) { + return text_length; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + var best = 0; + var length = 1; + while (true) { + var pattern = text1.substring(text_length - length); + var found = text2.indexOf(pattern); + if (found == -1) { + return best; + } + length += found; + if (found == 0 || text1.substring(text_length - length) == + text2.substring(0, length)) { + best = length; + length++; + } + } +}; + + +/** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.<string>} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + * @private + */ +diff_match_patch.prototype.diff_halfMatch_ = function(text1, text2) { + if (this.Diff_Timeout <= 0) { + // Don't risk returning a non-optimal diff if we have unlimited time. + return null; + } + var longtext = text1.length > text2.length ? text1 : text2; + var shorttext = text1.length > text2.length ? text2 : text1; + if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { + return null; // Pointless. + } + var dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.<string>} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diff_halfMatchI_(longtext, shorttext, i) { + // Start with a 1/4 length substring at position i as a seed. + var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); + var j = -1; + var best_common = ''; + var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; + while ((j = shorttext.indexOf(seed, j + 1)) != -1) { + var prefixLength = dmp.diff_commonPrefix(longtext.substring(i), + shorttext.substring(j)); + var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i), + shorttext.substring(0, j)); + if (best_common.length < suffixLength + prefixLength) { + best_common = shorttext.substring(j - suffixLength, j) + + shorttext.substring(j, j + prefixLength); + best_longtext_a = longtext.substring(0, i - suffixLength); + best_longtext_b = longtext.substring(i + prefixLength); + best_shorttext_a = shorttext.substring(0, j - suffixLength); + best_shorttext_b = shorttext.substring(j + prefixLength); + } + } + if (best_common.length * 2 >= longtext.length) { + return [best_longtext_a, best_longtext_b, + best_shorttext_a, best_shorttext_b, best_common]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + var hm1 = diff_halfMatchI_(longtext, shorttext, + Math.ceil(longtext.length / 4)); + // Check again based on the third quarter. + var hm2 = diff_halfMatchI_(longtext, shorttext, + Math.ceil(longtext.length / 2)); + var hm; + if (!hm1 && !hm2) { + return null; + } else if (!hm2) { + hm = hm1; + } else if (!hm1) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[4].length > hm2[4].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + var text1_a, text1_b, text2_a, text2_b; + if (text1.length > text2.length) { + text1_a = hm[0]; + text1_b = hm[1]; + text2_a = hm[2]; + text2_b = hm[3]; + } else { + text2_a = hm[0]; + text2_b = hm[1]; + text1_a = hm[2]; + text1_b = hm[3]; + } + var mid_common = hm[4]; + return [text1_a, text1_b, text2_a, text2_b, mid_common]; +}; + + +/** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + var lastequality = null; // Always equal to equalities[equalitiesLength-1][1] + var pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + var length_insertions1 = 0; + var length_deletions1 = 0; + // Number of characters that changed after the equality. + var length_insertions2 = 0; + var length_deletions2 = 0; + while (pointer < diffs.length) { + if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. + equalities[equalitiesLength++] = pointer; + length_insertions1 = length_insertions2; + length_deletions1 = length_deletions2; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = /** @type {string} */(diffs[pointer][1]); + } else { // An insertion or deletion. + if (diffs[pointer][0] == DIFF_INSERT) { + length_insertions2 += diffs[pointer][1].length; + } else { + length_deletions2 += diffs[pointer][1].length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if (lastequality !== null && (lastequality.length <= + Math.max(length_insertions1, length_deletions1)) && + (lastequality.length <= Math.max(length_insertions2, + length_deletions2))) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, + [DIFF_DELETE, lastequality]); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + // Throw away the equality we just deleted. + equalitiesLength--; + // Throw away the previous equality (it needs to be reevaluated). + equalitiesLength--; + pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; + length_insertions1 = 0; // Reset the counters. + length_deletions1 = 0; + length_insertions2 = 0; + length_deletions2 = 0; + lastequality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if (changes) { + this.diff_cleanupMerge(diffs); + } + this.diff_cleanupSemanticLossless(diffs); + + // Find any overlaps between deletions and insertions. + // e.g: <del>abcxxx</del><ins>xxxdef</ins> + // -> <del>abc</del>xxx<ins>def</ins> + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while (pointer < diffs.length) { + if (diffs[pointer - 1][0] == DIFF_DELETE && + diffs[pointer][0] == DIFF_INSERT) { + var deletion = /** @type {string} */(diffs[pointer - 1][1]); + var insertion = /** @type {string} */(diffs[pointer][1]); + var overlap_length = this.diff_commonOverlap_(deletion, insertion); + if (overlap_length >= deletion.length / 2 || + overlap_length >= insertion.length / 2) { + // Overlap found. Insert an equality and trim the surrounding edits. + diffs.splice(pointer, 0, + [DIFF_EQUAL, insertion.substring(0, overlap_length)]); + diffs[pointer - 1][1] = + deletion.substring(0, deletion.length - overlap_length); + diffs[pointer + 1][1] = insertion.substring(overlap_length); + pointer++; + } + pointer++; + } + pointer++; + } +}; + + +/** + * Look for single edits surrounded on both sides by equalities + * which can be shifted sideways to align the edit to a word boundary. + * e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came. + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) { + // Define some regex patterns for matching boundaries. + var punctuation = /[^a-zA-Z0-9]/; + var whitespace = /\s/; + var linebreak = /[\r\n]/; + var blanklineEnd = /\n\r?\n$/; + var blanklineStart = /^\r?\n\r?\n/; + + /** + * Given two strings, compute a score representing whether the internal + * boundary falls on logical boundaries. + * Scores range from 5 (best) to 0 (worst). + * Closure, makes reference to regex patterns defined above. + * @param {string} one First string. + * @param {string} two Second string. + * @return {number} The score. + * @private + */ + function diff_cleanupSemanticScore_(one, two) { + if (!one || !two) { + // Edges are the best. + return 5; + } + + // Each port of this function behaves slightly differently due to + // subtle differences in each language's definition of things like + // 'whitespace'. Since this function's purpose is largely cosmetic, + // the choice has been made to use each language's native features + // rather than force total conformity. + var score = 0; + // One point for non-alphanumeric. + if (one.charAt(one.length - 1).match(punctuation) || + two.charAt(0).match(punctuation)) { + score++; + // Two points for whitespace. + if (one.charAt(one.length - 1).match(whitespace) || + two.charAt(0).match(whitespace)) { + score++; + // Three points for line breaks. + if (one.charAt(one.length - 1).match(linebreak) || + two.charAt(0).match(linebreak)) { + score++; + // Four points for blank lines. + if (one.match(blanklineEnd) || two.match(blanklineStart)) { + score++; + } + } + } + } + return score; + } + + var pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] == DIFF_EQUAL && + diffs[pointer + 1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + var equality1 = /** @type {string} */(diffs[pointer - 1][1]); + var edit = /** @type {string} */(diffs[pointer][1]); + var equality2 = /** @type {string} */(diffs[pointer + 1][1]); + + // First, shift the edit as far left as possible. + var commonOffset = this.diff_commonSuffix(equality1, edit); + if (commonOffset) { + var commonString = edit.substring(edit.length - commonOffset); + equality1 = equality1.substring(0, equality1.length - commonOffset); + edit = commonString + edit.substring(0, edit.length - commonOffset); + equality2 = commonString + equality2; + } + + // Second, step character by character right, looking for the best fit. + var bestEquality1 = equality1; + var bestEdit = edit; + var bestEquality2 = equality2; + var bestScore = diff_cleanupSemanticScore_(equality1, edit) + + diff_cleanupSemanticScore_(edit, equality2); + while (edit.charAt(0) === equality2.charAt(0)) { + equality1 += edit.charAt(0); + edit = edit.substring(1) + equality2.charAt(0); + equality2 = equality2.substring(1); + var score = diff_cleanupSemanticScore_(equality1, edit) + + diff_cleanupSemanticScore_(edit, equality2); + // The >= encourages trailing rather than leading whitespace on edits. + if (score >= bestScore) { + bestScore = score; + bestEquality1 = equality1; + bestEdit = edit; + bestEquality2 = equality2; + } + } + + if (diffs[pointer - 1][1] != bestEquality1) { + // We have an improvement, save it back to the diff. + if (bestEquality1) { + diffs[pointer - 1][1] = bestEquality1; + } else { + diffs.splice(pointer - 1, 1); + pointer--; + } + diffs[pointer][1] = bestEdit; + if (bestEquality2) { + diffs[pointer + 1][1] = bestEquality2; + } else { + diffs.splice(pointer + 1, 1); + pointer--; + } + } + } + pointer++; + } +}; + + +/** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) { + var changes = false; + var equalities = []; // Stack of indices where equalities are found. + var equalitiesLength = 0; // Keeping our own length var is faster in JS. + var lastequality = ''; // Always equal to equalities[equalitiesLength-1][1] + var pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + var pre_ins = false; + // Is there a deletion operation before the last equality. + var pre_del = false; + // Is there an insertion operation after the last equality. + var post_ins = false; + // Is there a deletion operation after the last equality. + var post_del = false; + while (pointer < diffs.length) { + if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found. + if (diffs[pointer][1].length < this.Diff_EditCost && + (post_ins || post_del)) { + // Candidate found. + equalities[equalitiesLength++] = pointer; + pre_ins = post_ins; + pre_del = post_del; + lastequality = diffs[pointer][1]; + } else { + // Not a candidate, and can never become one. + equalitiesLength = 0; + lastequality = ''; + } + post_ins = post_del = false; + } else { // An insertion or deletion. + if (diffs[pointer][0] == DIFF_DELETE) { + post_del = true; + } else { + post_ins = true; + } + /* + * Five types to be split: + * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del> + * <ins>A</ins>X<ins>C</ins><del>D</del> + * <ins>A</ins><del>B</del>X<ins>C</ins> + * <ins>A</del>X<ins>C</ins><del>D</del> + * <ins>A</ins><del>B</del>X<del>C</del> + */ + if (lastequality && ((pre_ins && pre_del && post_ins && post_del) || + ((lastequality.length < this.Diff_EditCost / 2) && + (pre_ins + pre_del + post_ins + post_del) == 3))) { + // Duplicate record. + diffs.splice(equalities[equalitiesLength - 1], 0, + [DIFF_DELETE, lastequality]); + // Change second copy to insert. + diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; + equalitiesLength--; // Throw away the equality we just deleted; + lastequality = ''; + if (pre_ins && pre_del) { + // No changes made which could affect previous entry, keep going. + post_ins = post_del = true; + equalitiesLength = 0; + } else { + equalitiesLength--; // Throw away the previous equality. + pointer = equalitiesLength > 0 ? + equalities[equalitiesLength - 1] : -1; + post_ins = post_del = false; + } + changes = true; + } + } + pointer++; + } + + if (changes) { + this.diff_cleanupMerge(diffs); + } +}; + + +/** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + */ +diff_match_patch.prototype.diff_cleanupMerge = function(diffs) { + diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. + var pointer = 0; + var count_delete = 0; + var count_insert = 0; + var text_delete = ''; + var text_insert = ''; + var commonlength; + while (pointer < diffs.length) { + switch (diffs[pointer][0]) { + case DIFF_INSERT: + count_insert++; + text_insert += diffs[pointer][1]; + pointer++; + break; + case DIFF_DELETE: + count_delete++; + text_delete += diffs[pointer][1]; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if (count_delete + count_insert > 1) { + if (count_delete !== 0 && count_insert !== 0) { + // Factor out any common prefixies. + commonlength = this.diff_commonPrefix(text_insert, text_delete); + if (commonlength !== 0) { + if ((pointer - count_delete - count_insert) > 0 && + diffs[pointer - count_delete - count_insert - 1][0] == + DIFF_EQUAL) { + diffs[pointer - count_delete - count_insert - 1][1] += + text_insert.substring(0, commonlength); + } else { + diffs.splice(0, 0, [DIFF_EQUAL, + text_insert.substring(0, commonlength)]); + pointer++; + } + text_insert = text_insert.substring(commonlength); + text_delete = text_delete.substring(commonlength); + } + // Factor out any common suffixies. + commonlength = this.diff_commonSuffix(text_insert, text_delete); + if (commonlength !== 0) { + diffs[pointer][1] = text_insert.substring(text_insert.length - + commonlength) + diffs[pointer][1]; + text_insert = text_insert.substring(0, text_insert.length - + commonlength); + text_delete = text_delete.substring(0, text_delete.length - + commonlength); + } + } + // Delete the offending records and add the merged ones. + if (count_delete === 0) { + diffs.splice(pointer - count_delete - count_insert, + count_delete + count_insert, [DIFF_INSERT, text_insert]); + } else if (count_insert === 0) { + diffs.splice(pointer - count_delete - count_insert, + count_delete + count_insert, [DIFF_DELETE, text_delete]); + } else { + diffs.splice(pointer - count_delete - count_insert, + count_delete + count_insert, [DIFF_DELETE, text_delete], + [DIFF_INSERT, text_insert]); + } + pointer = pointer - count_delete - count_insert + + (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1; + } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { + // Merge this equality with the previous one. + diffs[pointer - 1][1] += diffs[pointer][1]; + diffs.splice(pointer, 1); + } else { + pointer++; + } + count_insert = 0; + count_delete = 0; + text_delete = ''; + text_insert = ''; + break; + } + } + if (diffs[diffs.length - 1][1] === '') { + diffs.pop(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC + var changes = false; + pointer = 1; + // Intentionally ignore the first and last element (don't need checking). + while (pointer < diffs.length - 1) { + if (diffs[pointer - 1][0] == DIFF_EQUAL && + diffs[pointer + 1][0] == DIFF_EQUAL) { + // This is a single edit surrounded by equalities. + if (diffs[pointer][1].substring(diffs[pointer][1].length - + diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { + // Shift the edit over the previous equality. + diffs[pointer][1] = diffs[pointer - 1][1] + + diffs[pointer][1].substring(0, diffs[pointer][1].length - + diffs[pointer - 1][1].length); + diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; + diffs.splice(pointer - 1, 1); + changes = true; + } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) == + diffs[pointer + 1][1]) { + // Shift the edit over the next equality. + diffs[pointer - 1][1] += diffs[pointer + 1][1]; + diffs[pointer][1] = + diffs[pointer][1].substring(diffs[pointer + 1][1].length) + + diffs[pointer + 1][1]; + diffs.splice(pointer + 1, 1); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if (changes) { + this.diff_cleanupMerge(diffs); + } +}; + + +/** + * loc is a location in text1, compute and return the equivalent location in + * text2. + * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + * @param {number} loc Location within text1. + * @return {number} Location within text2. + */ +diff_match_patch.prototype.diff_xIndex = function(diffs, loc) { + var chars1 = 0; + var chars2 = 0; + var last_chars1 = 0; + var last_chars2 = 0; + var x; + for (x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion. + chars1 += diffs[x][1].length; + } + if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion. + chars2 += diffs[x][1].length; + } + if (chars1 > loc) { // Overshot the location. + break; + } + last_chars1 = chars1; + last_chars2 = chars2; + } + // Was the location was deleted? + if (diffs.length != x && diffs[x][0] === DIFF_DELETE) { + return last_chars2; + } + // Add the remaining character length. + return last_chars2 + (loc - last_chars1); +}; + + +/** + * Convert a diff array into a pretty HTML report. + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + * @return {string} HTML representation. + */ +diff_match_patch.prototype.diff_prettyHtml = function(diffs) { + var html = []; + var i = 0; + var pattern_amp = /&/g; + var pattern_lt = /</g; + var pattern_gt = />/g; + var pattern_para = /\n/g; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; // Operation (insert, delete, equal) + var data = diffs[x][1]; // Text of change. + var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<') + .replace(pattern_gt, '>').replace(pattern_para, '¶<br>'); + switch (op) { + case DIFF_INSERT: + html[x] = '<ins style="background:#e6ffe6;">' + text + '</ins>'; + break; + case DIFF_DELETE: + html[x] = '<del style="background:#ffe6e6;">' + text + '</del>'; + break; + case DIFF_EQUAL: + html[x] = '<span>' + text + '</span>'; + break; + } + if (op !== DIFF_DELETE) { + i += data.length; + } + } + return html.join(''); +}; + + +/** + * Compute and return the source text (all equalities and deletions). + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + * @return {string} Source text. + */ +diff_match_patch.prototype.diff_text1 = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_INSERT) { + text[x] = diffs[x][1]; + } + } + return text.join(''); +}; + + +/** + * Compute and return the destination text (all equalities and insertions). + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + * @return {string} Destination text. + */ +diff_match_patch.prototype.diff_text2 = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + if (diffs[x][0] !== DIFF_DELETE) { + text[x] = diffs[x][1]; + } + } + return text.join(''); +}; + + +/** + * Compute the Levenshtein distance; the number of inserted, deleted or + * substituted characters. + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + * @return {number} Number of changes. + */ +diff_match_patch.prototype.diff_levenshtein = function(diffs) { + var levenshtein = 0; + var insertions = 0; + var deletions = 0; + for (var x = 0; x < diffs.length; x++) { + var op = diffs[x][0]; + var data = diffs[x][1]; + switch (op) { + case DIFF_INSERT: + insertions += data.length; + break; + case DIFF_DELETE: + deletions += data.length; + break; + case DIFF_EQUAL: + // A deletion and an insertion is one substitution. + levenshtein += Math.max(insertions, deletions); + insertions = 0; + deletions = 0; + break; + } + } + levenshtein += Math.max(insertions, deletions); + return levenshtein; +}; + + +/** + * Crush the diff into an encoded string which describes the operations + * required to transform text1 into text2. + * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. + * Operations are tab-separated. Inserted text is escaped using %xx notation. + * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples. + * @return {string} Delta text. + */ +diff_match_patch.prototype.diff_toDelta = function(diffs) { + var text = []; + for (var x = 0; x < diffs.length; x++) { + switch (diffs[x][0]) { + case DIFF_INSERT: + text[x] = '+' + encodeURI(diffs[x][1]); + break; + case DIFF_DELETE: + text[x] = '-' + diffs[x][1].length; + break; + case DIFF_EQUAL: + text[x] = '=' + diffs[x][1].length; + break; + } + } + return text.join('\t').replace(/%20/g, ' '); +}; + + +/** + * Given the original text1, and an encoded string which describes the + * operations required to transform text1 into text2, compute the full diff. + * @param {string} text1 Source string for the diff. + * @param {string} delta Delta text. + * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples. + * @throws {!Error} If invalid input. + */ +diff_match_patch.prototype.diff_fromDelta = function(text1, delta) { + var diffs = []; + var diffsLength = 0; // Keeping our own length var is faster in JS. + var pointer = 0; // Cursor in text1 + var tokens = delta.split(/\t/g); + for (var x = 0; x < tokens.length; x++) { + // Each token begins with a one character parameter which specifies the + // operation of this token (delete, insert, equality). + var param = tokens[x].substring(1); + switch (tokens[x].charAt(0)) { + case '+': + try { + diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)]; + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in diff_fromDelta: ' + param); + } + break; + case '-': + // Fall through. + case '=': + var n = parseInt(param, 10); + if (isNaN(n) || n < 0) { + throw new Error('Invalid number in diff_fromDelta: ' + param); + } + var text = text1.substring(pointer, pointer += n); + if (tokens[x].charAt(0) == '=') { + diffs[diffsLength++] = [DIFF_EQUAL, text]; + } else { + diffs[diffsLength++] = [DIFF_DELETE, text]; + } + break; + default: + // Blank tokens are ok (from a trailing \t). + // Anything else is an error. + if (tokens[x]) { + throw new Error('Invalid diff operation in diff_fromDelta: ' + + tokens[x]); + } + } + } + if (pointer != text1.length) { + throw new Error('Delta length (' + pointer + + ') does not equal source text length (' + text1.length + ').'); + } + return diffs; +}; + + +// MATCH FUNCTIONS + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc'. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + */ +diff_match_patch.prototype.match_main = function(text, pattern, loc) { + // Check for null inputs. + if (text == null || pattern == null || loc == null) { + throw new Error('Null input. (match_main)'); + } + + loc = Math.max(0, Math.min(loc, text.length)); + if (text == pattern) { + // Shortcut (potentially not guaranteed by the algorithm) + return 0; + } else if (!text.length) { + // Nothing to match. + return -1; + } else if (text.substring(loc, loc + pattern.length) == pattern) { + // Perfect match at the perfect spot! (Includes case of null pattern) + return loc; + } else { + // Do a fuzzy compare. + return this.match_bitap_(text, pattern, loc); + } +}; + + +/** + * Locate the best instance of 'pattern' in 'text' near 'loc' using the + * Bitap algorithm. + * @param {string} text The text to search. + * @param {string} pattern The pattern to search for. + * @param {number} loc The location to search around. + * @return {number} Best match index or -1. + * @private + */ +diff_match_patch.prototype.match_bitap_ = function(text, pattern, loc) { + if (pattern.length > this.Match_MaxBits) { + throw new Error('Pattern too long for this browser.'); + } + + // Initialise the alphabet. + var s = this.match_alphabet_(pattern); + + var dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Compute and return the score for a match with e errors and x location. + * Accesses loc and pattern through being a closure. + * @param {number} e Number of errors in match. + * @param {number} x Location of match. + * @return {number} Overall score for match (0.0 = good, 1.0 = bad). + * @private + */ + function match_bitapScore_(e, x) { + var accuracy = e / pattern.length; + var proximity = Math.abs(loc - x); + if (!dmp.Match_Distance) { + // Dodge divide by zero error. + return proximity ? 1.0 : accuracy; + } + return accuracy + (proximity / dmp.Match_Distance); + } + + // Highest score beyond which we give up. + var score_threshold = this.Match_Threshold; + // Is there a nearby exact match? (speedup) + var best_loc = text.indexOf(pattern, loc); + if (best_loc != -1) { + score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold); + // What about in the other direction? (speedup) + best_loc = text.lastIndexOf(pattern, loc + pattern.length); + if (best_loc != -1) { + score_threshold = + Math.min(match_bitapScore_(0, best_loc), score_threshold); + } + } + + // Initialise the bit arrays. + var matchmask = 1 << (pattern.length - 1); + best_loc = -1; + + var bin_min, bin_mid; + var bin_max = pattern.length + text.length; + var last_rd; + for (var d = 0; d < pattern.length; d++) { + // Scan for the best match; each iteration allows for one more error. + // Run a binary search to determine how far from 'loc' we can stray at this + // error level. + bin_min = 0; + bin_mid = bin_max; + while (bin_min < bin_mid) { + if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) { + bin_min = bin_mid; + } else { + bin_max = bin_mid; + } + bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min); + } + // Use the result from this iteration as the maximum for the next. + bin_max = bin_mid; + var start = Math.max(1, loc - bin_mid + 1); + var finish = Math.min(loc + bin_mid, text.length) + pattern.length; + + var rd = Array(finish + 2); + rd[finish + 1] = (1 << d) - 1; + for (var j = finish; j >= start; j--) { + // The alphabet (s) is a sparse hash, so the following line generates + // warnings. + var charMatch = s[text.charAt(j - 1)]; + if (d === 0) { // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | + (((last_rd[j + 1] | last_rd[j]) << 1) | 1) | + last_rd[j + 1]; + } + if (rd[j] & matchmask) { + var score = match_bitapScore_(d, j - 1); + // This match will almost certainly be better than any existing match. + // But check anyway. + if (score <= score_threshold) { + // Told you so. + score_threshold = score; + best_loc = j - 1; + if (best_loc > loc) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * loc - best_loc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + // No hope for a (better) match at greater error levels. + if (match_bitapScore_(d + 1, loc) > score_threshold) { + break; + } + last_rd = rd; + } + return best_loc; +}; + + +/** + * Initialise the alphabet for the Bitap algorithm. + * @param {string} pattern The text to encode. + * @return {!Object} Hash of character locations. + * @private + */ +diff_match_patch.prototype.match_alphabet_ = function(pattern) { + var s = {}; + for (var i = 0; i < pattern.length; i++) { + s[pattern.charAt(i)] = 0; + } + for (var i = 0; i < pattern.length; i++) { + s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); + } + return s; +}; + + +// PATCH FUNCTIONS + + +/** + * Increase the context until it is unique, + * but don't let the pattern expand beyond Match_MaxBits. + * @param {!patch_obj} patch The patch to grow. + * @param {string} text Source text. + * @private + */ +diff_match_patch.prototype.patch_addContext_ = function(patch, text) { + if (text.length == 0) { + return; + } + var pattern = text.substring(patch.start2, patch.start2 + patch.length1); + var padding = 0; + + // Look for the first and last matches of pattern in text. If two different + // matches are found, increase the pattern length. + while (text.indexOf(pattern) != text.lastIndexOf(pattern) && + pattern.length < this.Match_MaxBits - this.Patch_Margin - + this.Patch_Margin) { + padding += this.Patch_Margin; + pattern = text.substring(patch.start2 - padding, + patch.start2 + patch.length1 + padding); + } + // Add one chunk for good luck. + padding += this.Patch_Margin; + + // Add the prefix. + var prefix = text.substring(patch.start2 - padding, patch.start2); + if (prefix) { + patch.diffs.unshift([DIFF_EQUAL, prefix]); + } + // Add the suffix. + var suffix = text.substring(patch.start2 + patch.length1, + patch.start2 + patch.length1 + padding); + if (suffix) { + patch.diffs.push([DIFF_EQUAL, suffix]); + } + + // Roll back the start points. + patch.start1 -= prefix.length; + patch.start2 -= prefix.length; + // Extend the lengths. + patch.length1 += prefix.length + suffix.length; + patch.length2 += prefix.length + suffix.length; +}; + + +/** + * Compute a list of patches to turn text1 into text2. + * Use diffs if provided, otherwise compute it ourselves. + * There are four ways to call this function, depending on what data is + * available to the caller: + * Method 1: + * a = text1, b = text2 + * Method 2: + * a = diffs + * Method 3 (optimal): + * a = text1, b = diffs + * Method 4 (deprecated, use method 3): + * a = text1, b = text2, c = diffs + * + * @param {string|!Array.<!diff_match_patch.Diff>} a text1 (methods 1,3,4) or + * Array of diff tuples for text1 to text2 (method 2). + * @param {string|!Array.<!diff_match_patch.Diff>} opt_b text2 (methods 1,4) or + * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2). + * @param {string|!Array.<!diff_match_patch.Diff>} opt_c Array of diff tuples + * for text1 to text2 (method 4) or undefined (methods 1,2,3). + * @return {!Array.<!patch_obj>} Array of patch objects. + */ +diff_match_patch.prototype.patch_make = function(a, opt_b, opt_c) { + var text1, diffs; + if (typeof a == 'string' && typeof opt_b == 'string' && + typeof opt_c == 'undefined') { + // Method 1: text1, text2 + // Compute diffs from text1 and text2. + text1 = /** @type {string} */(a); + diffs = this.diff_main(text1, /** @type {string} */(opt_b), true); + if (diffs.length > 2) { + this.diff_cleanupSemantic(diffs); + this.diff_cleanupEfficiency(diffs); + } + } else if (a && typeof a == 'object' && typeof opt_b == 'undefined' && + typeof opt_c == 'undefined') { + // Method 2: diffs + // Compute text1 from diffs. + diffs = /** @type {!Array.<!diff_match_patch.Diff>} */(a); + text1 = this.diff_text1(diffs); + } else if (typeof a == 'string' && opt_b && typeof opt_b == 'object' && + typeof opt_c == 'undefined') { + // Method 3: text1, diffs + text1 = /** @type {string} */(a); + diffs = /** @type {!Array.<!diff_match_patch.Diff>} */(opt_b); + } else if (typeof a == 'string' && typeof opt_b == 'string' && + opt_c && typeof opt_c == 'object') { + // Method 4: text1, text2, diffs + // text2 is not used. + text1 = /** @type {string} */(a); + diffs = /** @type {!Array.<!diff_match_patch.Diff>} */(opt_c); + } else { + throw new Error('Unknown call format to patch_make.'); + } + + if (diffs.length === 0) { + return []; // Get rid of the null case. + } + var patches = []; + var patch = new patch_obj(); + var patchDiffLength = 0; // Keeping our own length var is faster in JS. + var char_count1 = 0; // Number of characters into the text1 string. + var char_count2 = 0; // Number of characters into the text2 string. + // Start with text1 (prepatch_text) and apply the diffs until we arrive at + // text2 (postpatch_text). We recreate the patches one by one to determine + // context info. + var prepatch_text = text1; + var postpatch_text = text1; + for (var x = 0; x < diffs.length; x++) { + var diff_type = diffs[x][0]; + var diff_text = diffs[x][1]; + + if (!patchDiffLength && diff_type !== DIFF_EQUAL) { + // A new patch starts here. + patch.start1 = char_count1; + patch.start2 = char_count2; + } + + switch (diff_type) { + case DIFF_INSERT: + patch.diffs[patchDiffLength++] = diffs[x]; + patch.length2 += diff_text.length; + postpatch_text = postpatch_text.substring(0, char_count2) + diff_text + + postpatch_text.substring(char_count2); + break; + case DIFF_DELETE: + patch.length1 += diff_text.length; + patch.diffs[patchDiffLength++] = diffs[x]; + postpatch_text = postpatch_text.substring(0, char_count2) + + postpatch_text.substring(char_count2 + + diff_text.length); + break; + case DIFF_EQUAL: + if (diff_text.length <= 2 * this.Patch_Margin && + patchDiffLength && diffs.length != x + 1) { + // Small equality inside a patch. + patch.diffs[patchDiffLength++] = diffs[x]; + patch.length1 += diff_text.length; + patch.length2 += diff_text.length; + } else if (diff_text.length >= 2 * this.Patch_Margin) { + // Time for a new patch. + if (patchDiffLength) { + this.patch_addContext_(patch, prepatch_text); + patches.push(patch); + patch = new patch_obj(); + patchDiffLength = 0; + // Unlike Unidiff, our patch lists have a rolling context. + // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff + // Update prepatch text & pos to reflect the application of the + // just completed patch. + prepatch_text = postpatch_text; + char_count1 = char_count2; + } + } + break; + } + + // Update the current character count. + if (diff_type !== DIFF_INSERT) { + char_count1 += diff_text.length; + } + if (diff_type !== DIFF_DELETE) { + char_count2 += diff_text.length; + } + } + // Pick up the leftover patch if not empty. + if (patchDiffLength) { + this.patch_addContext_(patch, prepatch_text); + patches.push(patch); + } + + return patches; +}; + + +/** + * Given an array of patches, return another array that is identical. + * @param {!Array.<!patch_obj>} patches Array of patch objects. + * @return {!Array.<!patch_obj>} Array of patch objects. + */ +diff_match_patch.prototype.patch_deepCopy = function(patches) { + // Making deep copies is hard in JavaScript. + var patchesCopy = []; + for (var x = 0; x < patches.length; x++) { + var patch = patches[x]; + var patchCopy = new patch_obj(); + patchCopy.diffs = []; + for (var y = 0; y < patch.diffs.length; y++) { + patchCopy.diffs[y] = patch.diffs[y].slice(); + } + patchCopy.start1 = patch.start1; + patchCopy.start2 = patch.start2; + patchCopy.length1 = patch.length1; + patchCopy.length2 = patch.length2; + patchesCopy[x] = patchCopy; + } + return patchesCopy; +}; + + +/** + * Merge a set of patches onto the text. Return a patched text, as well + * as a list of true/false values indicating which patches were applied. + * @param {!Array.<!patch_obj>} patches Array of patch objects. + * @param {string} text Old text. + * @return {!Array.<string|!Array.<boolean>>} Two element Array, containing the + * new text and an array of boolean values. + */ +diff_match_patch.prototype.patch_apply = function(patches, text) { + if (patches.length == 0) { + return [text, []]; + } + + // Deep copy the patches so that no changes are made to originals. + patches = this.patch_deepCopy(patches); + + var nullPadding = this.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + + this.patch_splitMax(patches); + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + var delta = 0; + var results = []; + for (var x = 0; x < patches.length; x++) { + var expected_loc = patches[x].start2 + delta; + var text1 = this.diff_text1(patches[x].diffs); + var start_loc; + var end_loc = -1; + if (text1.length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits), + expected_loc); + if (start_loc != -1) { + end_loc = this.match_main(text, + text1.substring(text1.length - this.Match_MaxBits), + expected_loc + text1.length - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = this.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= patches[x].length2 - patches[x].length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + var text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, start_loc + text1.length); + } else { + text2 = text.substring(start_loc, end_loc + this.Match_MaxBits); + } + if (text1 == text2) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + + this.diff_text2(patches[x].diffs) + + text.substring(start_loc + text1.length); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + var diffs = this.diff_main(text1, text2, false); + if (text1.length > this.Match_MaxBits && + this.diff_levenshtein(diffs) / text1.length > + this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + this.diff_cleanupSemanticLossless(diffs); + var index1 = 0; + var index2; + for (var y = 0; y < patches[x].diffs.length; y++) { + var mod = patches[x].diffs[y]; + if (mod[0] !== DIFF_EQUAL) { + index2 = this.diff_xIndex(diffs, index1); + } + if (mod[0] === DIFF_INSERT) { // Insertion + text = text.substring(0, start_loc + index2) + mod[1] + + text.substring(start_loc + index2); + } else if (mod[0] === DIFF_DELETE) { // Deletion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + this.diff_xIndex(diffs, + index1 + mod[1].length)); + } + if (mod[0] !== DIFF_DELETE) { + index1 += mod[1].length; + } + } + } + } + } + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length); + return [text, results]; +}; + +/** + * Merge a set of patches onto the text. Return a patched text, as well + * as a list of true/false values indicating which patches were applied. + * @param {Array.<patch_obj>} patches Array of patch objects. + * @param {string} text Old text. + * @return {Array.<string|Array.<boolean>>} Two element Array, containing the + * new text and an array of boolean values. + */ +diff_match_patch.prototype.patch_apply_reverse = function(patches, text) { + if (patches.length == 0) { + return [text, []]; + } + + // Deep copy the patches so that no changes are made to originals. + patches = this.patch_deepCopy(patches); + + var nullPadding = this.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + + this.patch_splitMax(patches); + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + var delta = 0; + var results = []; + for (var x = 0; x < patches.length; x++) { + var expected_loc = patches[x].start2 + delta; + var text1 = this.diff_text1(patches[x].diffs); + var start_loc; + var end_loc = -1; + if (text1.length > this.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits), + expected_loc); + if (start_loc != -1) { + end_loc = this.match_main(text, + text1.substring(text1.length - this.Match_MaxBits), + expected_loc + text1.length - this.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = this.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + results[x] = false; + // Subtract the delta for this failed patch from subsequent patches. + delta -= patches[x].length2 - patches[x].length1; + } else { + // Found a match. :) + results[x] = true; + delta = start_loc - expected_loc; + var text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, start_loc + text1.length); + } else { + text2 = text.substring(start_loc, end_loc + this.Match_MaxBits); + } + if (text1 == text2) { + // Perfect match, just shove the replacement text in. + text = text.substring(0, start_loc) + + this.diff_text2(patches[x].diffs) + + text.substring(start_loc + text1.length); + } else { + // Imperfect match. Run a diff to get a framework of equivalent + // indices. + var diffs = this.diff_main(text1, text2, false); + if (text1.length > this.Match_MaxBits && + this.diff_levenshtein(diffs) / text1.length > + this.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + results[x] = false; + } else { + this.diff_cleanupSemanticLossless(diffs); + var index1 = 0; + var index2; + for (var y = 0; y < patches[x].diffs.length; y++) { + var mod = patches[x].diffs[y]; + if (mod[0] !== DIFF_EQUAL) { + index2 = this.diff_xIndex(diffs, index1); + } + if (mod[0] === DIFF_DELETE) { // Deletion + text = text.substring(0, start_loc + index2) + mod[1] + + text.substring(start_loc + index2); + } else if (mod[0] === DIFF_INSERT) { // Insertion + text = text.substring(0, start_loc + index2) + + text.substring(start_loc + this.diff_xIndex(diffs, + index1 + mod[1].length)); + } + if (mod[0] !== DIFF_INSERT) { + index1 += mod[1].length; + } + } + } + } + } + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length); + return [text, results]; +}; + + +/** + * Add some padding on text start and end so that edges can match something. + * Intended to be called only from within patch_apply. + * @param {!Array.<!patch_obj>} patches Array of patch objects. + * @return {string} The padding string added to each side. + */ +diff_match_patch.prototype.patch_addPadding = function(patches) { + var paddingLength = this.Patch_Margin; + var nullPadding = ''; + for (var x = 1; x <= paddingLength; x++) { + nullPadding += String.fromCharCode(x); + } + + // Bump all the patches forward. + for (var x = 0; x < patches.length; x++) { + patches[x].start1 += paddingLength; + patches[x].start2 += paddingLength; + } + + // Add some padding on start of first diff. + var patch = patches[0]; + var diffs = patch.diffs; + if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.unshift([DIFF_EQUAL, nullPadding]); + patch.start1 -= paddingLength; // Should be 0. + patch.start2 -= paddingLength; // Should be 0. + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[0][1].length) { + // Grow first equality. + var extraLength = paddingLength - diffs[0][1].length; + diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1]; + patch.start1 -= extraLength; + patch.start2 -= extraLength; + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + // Add some padding on end of last diff. + patch = patches[patches.length - 1]; + diffs = patch.diffs; + if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) { + // Add nullPadding equality. + diffs.push([DIFF_EQUAL, nullPadding]); + patch.length1 += paddingLength; + patch.length2 += paddingLength; + } else if (paddingLength > diffs[diffs.length - 1][1].length) { + // Grow last equality. + var extraLength = paddingLength - diffs[diffs.length - 1][1].length; + diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength); + patch.length1 += extraLength; + patch.length2 += extraLength; + } + + return nullPadding; +}; + + +/** + * Look through the patches and break up any which are longer than the maximum + * limit of the match algorithm. + * Intended to be called only from within patch_apply. + * @param {!Array.<!patch_obj>} patches Array of patch objects. + */ +diff_match_patch.prototype.patch_splitMax = function(patches) { + var patch_size = this.Match_MaxBits; + for (var x = 0; x < patches.length; x++) { + if (patches[x].length1 > patch_size) { + var bigpatch = patches[x]; + // Remove the big old patch. + patches.splice(x--, 1); + var start1 = bigpatch.start1; + var start2 = bigpatch.start2; + var precontext = ''; + while (bigpatch.diffs.length !== 0) { + // Create one of several smaller patches. + var patch = new patch_obj(); + var empty = true; + patch.start1 = start1 - precontext.length; + patch.start2 = start2 - precontext.length; + if (precontext !== '') { + patch.length1 = patch.length2 = precontext.length; + patch.diffs.push([DIFF_EQUAL, precontext]); + } + while (bigpatch.diffs.length !== 0 && + patch.length1 < patch_size - this.Patch_Margin) { + var diff_type = bigpatch.diffs[0][0]; + var diff_text = bigpatch.diffs[0][1]; + if (diff_type === DIFF_INSERT) { + // Insertions are harmless. + patch.length2 += diff_text.length; + start2 += diff_text.length; + patch.diffs.push(bigpatch.diffs.shift()); + empty = false; + } else if (diff_type === DIFF_DELETE && patch.diffs.length == 1 && + patch.diffs[0][0] == DIFF_EQUAL && + diff_text.length > 2 * patch_size) { + // This is a large deletion. Let it pass in one chunk. + patch.length1 += diff_text.length; + start1 += diff_text.length; + empty = false; + patch.diffs.push([diff_type, diff_text]); + bigpatch.diffs.shift(); + } else { + // Deletion or equality. Only take as much as we can stomach. + diff_text = diff_text.substring(0, + patch_size - patch.length1 - this.Patch_Margin); + patch.length1 += diff_text.length; + start1 += diff_text.length; + if (diff_type === DIFF_EQUAL) { + patch.length2 += diff_text.length; + start2 += diff_text.length; + } else { + empty = false; + } + patch.diffs.push([diff_type, diff_text]); + if (diff_text == bigpatch.diffs[0][1]) { + bigpatch.diffs.shift(); + } else { + bigpatch.diffs[0][1] = + bigpatch.diffs[0][1].substring(diff_text.length); + } + } + } + // Compute the head context for the next patch. + precontext = this.diff_text2(patch.diffs); + precontext = + precontext.substring(precontext.length - this.Patch_Margin); + // Append the end context for this patch. + var postcontext = this.diff_text1(bigpatch.diffs) + .substring(0, this.Patch_Margin); + if (postcontext !== '') { + patch.length1 += postcontext.length; + patch.length2 += postcontext.length; + if (patch.diffs.length !== 0 && + patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) { + patch.diffs[patch.diffs.length - 1][1] += postcontext; + } else { + patch.diffs.push([DIFF_EQUAL, postcontext]); + } + } + if (!empty) { + patches.splice(++x, 0, patch); + } + } + } + } +}; + + +/** + * Take a list of patches and return a textual representation. + * @param {!Array.<!patch_obj>} patches Array of patch objects. + * @return {string} Text representation of patches. + */ +diff_match_patch.prototype.patch_toText = function(patches) { + var text = []; + for (var x = 0; x < patches.length; x++) { + text[x] = patches[x]; + } + return text.join(''); +}; + + +/** + * Parse a textual representation of patches and return a list of patch objects. + * @param {string} textline Text representation of patches. + * @return {!Array.<!patch_obj>} Array of patch objects. + * @throws {!Error} If invalid input. + */ +diff_match_patch.prototype.patch_fromText = function(textline) { + var patches = []; + if (!textline) { + return patches; + } + var text = textline.split('\n'); + var textPointer = 0; + var patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; + while (textPointer < text.length) { + var m = text[textPointer].match(patchHeader); + if (!m) { + throw new Error('Invalid patch string: ' + text[textPointer]); + } + var patch = new patch_obj(); + patches.push(patch); + patch.start1 = parseInt(m[1], 10); + if (m[2] === '') { + patch.start1--; + patch.length1 = 1; + } else if (m[2] == '0') { + patch.length1 = 0; + } else { + patch.start1--; + patch.length1 = parseInt(m[2], 10); + } + + patch.start2 = parseInt(m[3], 10); + if (m[4] === '') { + patch.start2--; + patch.length2 = 1; + } else if (m[4] == '0') { + patch.length2 = 0; + } else { + patch.start2--; + patch.length2 = parseInt(m[4], 10); + } + textPointer++; + + while (textPointer < text.length) { + var sign = text[textPointer].charAt(0); + try { + var line = decodeURI(text[textPointer].substring(1)); + } catch (ex) { + // Malformed URI sequence. + throw new Error('Illegal escape in patch_fromText: ' + line); + } + if (sign == '-') { + // Deletion. + patch.diffs.push([DIFF_DELETE, line]); + } else if (sign == '+') { + // Insertion. + patch.diffs.push([DIFF_INSERT, line]); + } else if (sign == ' ') { + // Minor equality. + patch.diffs.push([DIFF_EQUAL, line]); + } else if (sign == '@') { + // Start of next patch. + break; + } else if (sign === '') { + // Blank line? Whatever. + } else { + // WTF? + throw new Error('Invalid patch mode "' + sign + '" in: ' + line); + } + textPointer++; + } + } + return patches; +}; + + +/** + * Class representing one patch operation. + * @constructor + */ +function patch_obj() { + /** @type {!Array.<!diff_match_patch.Diff>} */ + this.diffs = []; + /** @type {?number} */ + this.start1 = null; + /** @type {?number} */ + this.start2 = null; + /** @type {number} */ + this.length1 = 0; + /** @type {number} */ + this.length2 = 0; +} + + +/** + * Emmulate GNU diff's format. + * Header: @@ -382,8 +481,9 @@ + * Indicies are printed as 1-based, not 0-based. + * @return {string} The GNU diff string. + */ +patch_obj.prototype.toString = function() { + var coords1, coords2; + if (this.length1 === 0) { + coords1 = this.start1 + ',0'; + } else if (this.length1 == 1) { + coords1 = this.start1 + 1; + } else { + coords1 = (this.start1 + 1) + ',' + this.length1; + } + if (this.length2 === 0) { + coords2 = this.start2 + ',0'; + } else if (this.length2 == 1) { + coords2 = this.start2 + 1; + } else { + coords2 = (this.start2 + 1) + ',' + this.length2; + } + var text = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n']; + var op; + // Escape the body of the patch with %xx notation. + for (var x = 0; x < this.diffs.length; x++) { + switch (this.diffs[x][0]) { + case DIFF_INSERT: + op = '+'; + break; + case DIFF_DELETE: + op = '-'; + break; + case DIFF_EQUAL: + op = ' '; + break; + } + text[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n'; + } + return text.join('').replace(/%20/g, ' '); +}; + + +// Export these global variables so that they survive Google's JS compiler. +// In a browser, 'this' will be 'window'. +// In node.js 'this' will be a global object. +this['diff_match_patch'] = diff_match_patch; +this['patch_obj'] = patch_obj; +this['DIFF_DELETE'] = DIFF_DELETE; +this['DIFF_INSERT'] = DIFF_INSERT; +this['DIFF_EQUAL'] = DIFF_EQUAL; + diff --git a/bigbluebutton-client/resources/prod/lib/shared_notes.js b/bigbluebutton-client/resources/prod/lib/shared_notes.js new file mode 100644 index 0000000000000000000000000000000000000000..dc405664379374f0c80f90308a6b7c97d3fafd5f --- /dev/null +++ b/bigbluebutton-client/resources/prod/lib/shared_notes.js @@ -0,0 +1,304 @@ +/* + This file is part of BBB-Notes. + + Copyright (c) Islam El-Ashi. All rights reserved. + + BBB-Notes is free software: you can redistribute it and/or modify + it under the terms of the Lesser GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + any later version. + + BBB-Notes 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 + Lesser GNU General Public License for more details. + + You should have received a copy of the Lesser GNU General Public License + along with BBB-Notes. If not, see <http://www.gnu.org/licenses/>. + + Author: Islam El-Ashi <ielashi@gmail.com>, <http://www.ielashi.com> +*/ +var dmp = new diff_match_patch(); +var debug = false; + +function diff(text1, text2) { + return dmp.patch_toText(dmp.patch_make(dmp.diff_main(unescape(text1),unescape(text2)))); +} + +function patch(patch, text) { + return dmp.patch_apply(dmp.patch_fromText(patch), unescape(text))[0]; +} + +function unpatch(patch, text) { + return dmp.patch_apply_reverse(dmp.patch_fromText(patch), unescape(text))[0]; +} + + +/** + * Helper Methods + */ + +/** + * MobWrite - Real-time Synchronization and Collaboration Service + * + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-mobwrite/ + * + * 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. + */ + +/** + * Modify the user's plaintext by applying a series of patches against it. + * @param {Array.<patch_obj>} patches Array of Patch objects. + * @param {String} client text + */ +function patchClientText(patches, text, selectionStart, selectionEnd) { + text = unescape(text) + // Set some constants which tweak the matching behaviour. + // Maximum distance to search from expected location. + dmp.Match_Distance = 1000; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose) + dmp.Match_Threshold = 0.6; + + var oldClientText = text + var cursor = captureCursor_(oldClientText, selectionStart, selectionEnd); + // Pack the cursor offsets into an array to be adjusted. + // See http://neil.fraser.name/writing/cursor/ + var offsets = []; + if (cursor) { + offsets[0] = cursor.startOffset; + if ('endOffset' in cursor) { + offsets[1] = cursor.endOffset; + } + } + var newClientText = patch_apply_(patches, oldClientText, offsets); + // Set the new text only if there is a change to be made. + if (oldClientText != newClientText) { + //this.setClientText(newClientText); + if (cursor) { + // Unpack the offset array. + cursor.startOffset = offsets[0]; + if (offsets.length > 1) { + cursor.endOffset = offsets[1]; + if (cursor.startOffset >= cursor.endOffset) { + cursor.collapsed = true; + } + } + return [restoreCursor_(cursor, newClientText), newClientText]; + // this.restoreCursor_(cursor); + } + } + // no change in client text + return [[selectionStart, selectionEnd], newClientText]; +} + +/** + * Merge a set of patches onto the text. Return a patched text. + * @param {Array.<patch_obj>} patches Array of patch objects. + * @param {string} text Old text. + * @param {Array.<number>} offsets Offset indices to adjust. + * @return {string} New text. + */ +function patch_apply_(patchText, text, offsets) { + var patches = dmp.patch_fromText(patchText); + if (patches.length == 0) { + return text; + } + + // Deep copy the patches so that no changes are made to originals. + patches = dmp.patch_deepCopy(patches); + var nullPadding = dmp.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + + dmp.patch_splitMax(patches); + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + var delta = 0; + for (var x = 0; x < patches.length; x++) { + var expected_loc = patches[x].start2 + delta; + var text1 = dmp.diff_text1(patches[x].diffs); + var start_loc; + var end_loc = -1; + if (text1.length > dmp.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = dmp.match_main(text, text1.substring(0, dmp.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = dmp.match_main(text, text1.substring(text1.length - dmp.Match_MaxBits), + expected_loc + text1.length - dmp.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = dmp.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + if (debug) { + window.console.warn('Patch failed: ' + patches[x]); + } + // Subtract the delta for this failed patch from subsequent patches. + delta -= patches[x].length2 - patches[x].length1; + } else { + // Found a match. :) + if (debug) { + window.console.info('Patch OK.'); + } + delta = start_loc - expected_loc; + var text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, start_loc + text1.length); + } else { + text2 = text.substring(start_loc, end_loc + dmp.Match_MaxBits); + } + // Run a diff to get a framework of equivalent indices. + var diffs = dmp.diff_main(text1, text2, false); + if (text1.length > dmp.Match_MaxBits && + dmp.diff_levenshtein(diffs) / text1.length > + dmp.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + if (debug) { + window.console.warn('Patch contents mismatch: ' + patches[x]); + } + } else { + var index1 = 0; + var index2; + for (var y = 0; y < patches[x].diffs.length; y++) { + var mod = patches[x].diffs[y]; + if (mod[0] !== DIFF_EQUAL) { + index2 = dmp.diff_xIndex(diffs, index1); + } + if (mod[0] === DIFF_INSERT) { // Insertion + text = text.substring(0, start_loc + index2) + mod[1] + + text.substring(start_loc + index2); + for (var i = 0; i < offsets.length; i++) { + if (offsets[i] + nullPadding.length > start_loc + index2) { + offsets[i] += mod[1].length; + } + } + } else if (mod[0] === DIFF_DELETE) { // Deletion + var del_start = start_loc + index2; + var del_end = start_loc + dmp.diff_xIndex(diffs, + index1 + mod[1].length); + text = text.substring(0, del_start) + text.substring(del_end); + for (var i = 0; i < offsets.length; i++) { + if (offsets[i] + nullPadding.length > del_start) { + if (offsets[i] + nullPadding.length < del_end) { + offsets[i] = del_start - nullPadding.length; + } else { + offsets[i] -= del_end - del_start; + } + } + } + } + if (mod[0] !== DIFF_DELETE) { + index1 += mod[1].length; + } + } + } + } + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length); + return text; +} + + +/** + * Record information regarding the current cursor. + * @return {Object?} Context information of the cursor. + * @private + */ +function captureCursor_(text, selectionStart, selectionEnd) { + var padLength = dmp.Match_MaxBits / 2; // Normally 16. + var cursor = {}; + + cursor.startPrefix = text.substring(selectionStart - padLength, selectionStart); + cursor.startSuffix = text.substring(selectionStart, selectionStart + padLength); + cursor.startOffset = selectionStart; + cursor.collapsed = (selectionStart == selectionEnd); + if (!cursor.collapsed) { + cursor.endPrefix = text.substring(selectionEnd - padLength, selectionEnd); + cursor.endSuffix = text.substring(selectionEnd, selectionEnd + padLength); + cursor.endOffset = selectionEnd; + } + + return cursor; +} + +/** + * Attempt to restore the cursor's location. + * @param {Object} cursor Context information of the cursor. + * @private + */ +function restoreCursor_(cursor, text) { + // Set some constants which tweak the matching behaviour. + // Maximum distance to search from expected location. + dmp.Match_Distance = 1000; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose) + dmp.Match_Threshold = 0.9; + + var padLength = dmp.Match_MaxBits / 2; // Normally 16. + var newText = text; + + // Find the start of the selection in the new text. + var pattern1 = cursor.startPrefix + cursor.startSuffix; + var pattern2, diff; + var cursorStartPoint = dmp.match_main(newText, pattern1, + cursor.startOffset - padLength); + if (cursorStartPoint !== null) { + pattern2 = newText.substring(cursorStartPoint, + cursorStartPoint + pattern1.length); + //alert(pattern1 + '\nvs\n' + pattern2); + // Run a diff to get a framework of equivalent indicies. + diff = dmp.diff_main(pattern1, pattern2, false); + cursorStartPoint += dmp.diff_xIndex(diff, cursor.startPrefix.length); + } + + var cursorEndPoint = null; + if (!cursor.collapsed) { + // Find the end of the selection in the new text. + pattern1 = cursor.endPrefix + cursor.endSuffix; + cursorEndPoint = dmp.match_main(newText, pattern1, + cursor.endOffset - padLength); + if (cursorEndPoint !== null) { + pattern2 = newText.substring(cursorEndPoint, + cursorEndPoint + pattern1.length); + //alert(pattern1 + '\nvs\n' + pattern2); + // Run a diff to get a framework of equivalent indicies. + diff = dmp.diff_main(pattern1, pattern2, false); + cursorEndPoint += dmp.diff_xIndex(diff, cursor.endPrefix.length); + } + } + + // Deal with loose ends + if (cursorStartPoint === null && cursorEndPoint !== null) { + // Lost the start point of the selection, but we have the end point. + // Collapse to end point. + cursorStartPoint = cursorEndPoint; + } else if (cursorStartPoint === null && cursorEndPoint === null) { + // Lost both start and end points. + // Jump to the offset of start. + cursorStartPoint = cursor.startOffset; + } + if (cursorEndPoint === null) { + // End not known, collapse to start. + cursorEndPoint = cursorStartPoint; + } + + return [cursorStartPoint, cursorEndPoint]; +} diff --git a/bigbluebutton-client/resources/prod/logo.png b/bigbluebutton-client/resources/prod/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ced2cc1c9ecee0cf720c7060833cf2f3962e593f Binary files /dev/null and b/bigbluebutton-client/resources/prod/logo.png differ diff --git a/bigbluebutton-client/resources/prod/profiles.xml b/bigbluebutton-client/resources/prod/profiles.xml new file mode 100644 index 0000000000000000000000000000000000000000..825ed1b99d41bdf2e3031c6eb7edb7da2eabad3b --- /dev/null +++ b/bigbluebutton-client/resources/prod/profiles.xml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<profiles fallbackLocale="en_US"> + <profile id="low"> + <locale> + <en_US>Low quality</en_US> + <pt_BR>Baixa qualidade</pt_BR> + </locale> + <width>160</width> + <height>120</height> + <keyFrameInterval>5</keyFrameInterval> + <modeFps>10</modeFps> + <qualityBandwidth>0</qualityBandwidth> + <qualityPicture>90</qualityPicture> + <enableH264>true</enableH264> + <h264Level>2.1</h264Level> + <h264Profile>main</h264Profile> + </profile> + <profile id="medium" default="true"> + <locale> + <en_US>Medium quality</en_US> + <pt_BR>Média qualidade</pt_BR> + </locale> + <width>320</width> + <height>240</height> + <keyFrameInterval>5</keyFrameInterval> + <modeFps>10</modeFps> + <qualityBandwidth>0</qualityBandwidth> + <qualityPicture>90</qualityPicture> + <enableH264>true</enableH264> + <h264Level>2.1</h264Level> + <h264Profile>main</h264Profile> + </profile> + <profile id="high"> + <locale> + <en_US>High quality</en_US> + <pt_BR>Alta qualidade</pt_BR> + </locale> + <width>640</width> + <height>480</height> + <keyFrameInterval>5</keyFrameInterval> + <modeFps>15</modeFps> + <qualityBandwidth>0</qualityBandwidth> + <qualityPicture>90</qualityPicture> + <enableH264>true</enableH264> + <h264Level>2.1</h264Level> + <h264Profile>main</h264Profile> + </profile> +</profiles> diff --git a/bigbluebutton-client/src/BigBlueButton.mxml b/bigbluebutton-client/src/BigBlueButton.mxml index bd8cbfd3747a2f7bb8dcbb77e91d5b54d2adfc82..fdb50e3e65209b27f079c9fb46d15ae8aaaafdce 100755 --- a/bigbluebutton-client/src/BigBlueButton.mxml +++ b/bigbluebutton-client/src/BigBlueButton.mxml @@ -21,7 +21,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:views="*" - pageTitle="BigBlueButton" + pageTitle="Mconf-Live" layout="absolute" preloader="org.bigbluebutton.main.model.BigBlueButtonPreloader"> diff --git a/bigbluebutton-client/src/BigBlueButtonMainContainer.mxml b/bigbluebutton-client/src/BigBlueButtonMainContainer.mxml index 44ddbb8fef5d8413312640c82699723d6f92bbf5..e4d1d3095e3ee5b27d5d901efd3afd6b7d1817c1 100755 --- a/bigbluebutton-client/src/BigBlueButtonMainContainer.mxml +++ b/bigbluebutton-client/src/BigBlueButtonMainContainer.mxml @@ -26,6 +26,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:coreMap="org.bigbluebutton.core.controllers.maps.*" xmlns:mate="http://mate.asfusion.com/" width="100%" height="100%" + backgroundColor="white" horizontalScrollPolicy="off" verticalScrollPolicy="off" creationComplete="init()" @@ -73,6 +74,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. setupAPI(); EventBroadcaster.getInstance().addEventListener("configLoadedEvent", configLoadedEventHandler); BBB.initConfigManager(); + BBB.initVideoProfileManager(); globalModifier = ExternalInterface.call("determineGlobalModifier"); } diff --git a/bigbluebutton-client/src/SharedNotesModule.mxml b/bigbluebutton-client/src/SharedNotesModule.mxml index 013bbadb9f2541d194340c763c948525d5841e21..559faa163b7125280d1e4c0b54303741410356de 100755 --- a/bigbluebutton-client/src/SharedNotesModule.mxml +++ b/bigbluebutton-client/src/SharedNotesModule.mxml @@ -18,9 +18,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. --> <mx:Module xmlns:mx="http://www.adobe.com/2006/mxml" - layout="absolute" - implements="org.bigbluebutton.common.IBigBlueButtonModule" - creationComplete="onCreationComplete()" > + layout="absolute" + implements="org.bigbluebutton.common.IBigBlueButtonModule" + creationComplete="onCreationComplete()" + xmlns:maps="org.bigbluebutton.modules.sharednotes.maps.*" + xmlns:mate="http://mate.asfusion.com/" > + + <maps:SharedNotesEventMap id="sharedNotesEventMap"/> + <mx:Script> <![CDATA[ import com.asfusion.mate.events.Dispatcher; @@ -30,18 +35,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.common.events.CloseWindowEvent; import org.bigbluebutton.common.events.OpenWindowEvent; import org.bigbluebutton.common.events.ToolbarButtonEvent; - import org.bigbluebutton.modules.sharednotes.SharedNotesWindow; - import org.bigbluebutton.modules.sharednotes.ToolbarButton; - import org.bigbluebutton.modules.sharednotes.infrastructure.Client; - import org.bigbluebutton.modules.sharednotes.infrastructure.HTTPServerConnection; - - private var _moduleName:String = "Notes Module"; + import org.bigbluebutton.modules.sharednotes.events.StartSharedNotesModuleEvent; + + private var _moduleName:String = "Notes Module"; private var _attributes:Object; private var globalDispatcher:Dispatcher = new Dispatcher(); private function onCreationComplete():void { - LogUtil.debug("NotesModule Initialized"); + LogUtil.debug("NotesModule Initialized"); globalDispatcher = new Dispatcher(); } @@ -50,7 +52,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } public function get uri():String { - if (_attributes.mode == "PLAYBACK") { + if (_attributes.mode == "PLAYBACK") { return _attributes.uri + "/" + _attributes.playbackRoom; } @@ -60,9 +62,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. public function get username():String { return _attributes.username; } - + public function get mode():String { - if (_attributes.mode == null) { + if (_attributes.mode == null) { _attributes.mode = "LIVE" LogUtil.debug('Setting NotesModule mode: ' + _attributes.mode); } @@ -71,7 +73,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } public function get userid():Number { - return _attributes.userid as Number; + return _attributes.userid; } public function get role():String { @@ -79,23 +81,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } public function start(attributes:Object):void { - LogUtil.debug("notes attr: " + attributes.username); - _attributes = attributes; - SharedNotesWindow.document = _attributes.room; - // The following line has been removed, as the uri should be in - // the URI property in the config.xml file of the client - HTTPServerConnection.syncURL = (_attributes.uri as String).replace("RTMP", "http") + "/notes/notes.jsp"; - addToolbarButton(); - } - - public function addToolbarButton():void{ - var button:ToolbarButton = new ToolbarButton(); - var event:ToolbarButtonEvent = new ToolbarButtonEvent(ToolbarButtonEvent.ADD); - event.button = button; + var event:StartSharedNotesModuleEvent = new StartSharedNotesModuleEvent(); + event.attributes = attributes; globalDispatcher.dispatchEvent(event); } - public function stop():void {} + public function stop():void { + //var event:LayoutEvent = new LayoutEvent(LayoutEvent.STOP_LAYOUT_MODULE_EVENT); + //_globalDispatcher.dispatchEvent(event); + } ]]> </mx:Script> </mx:Module> diff --git a/bigbluebutton-client/src/VideoconfModule.mxml b/bigbluebutton-client/src/VideoconfModule.mxml index a54d585f2945ee601b4b7bc6be5ab7d8af840d50..dc53062d776bdac72e0a12fa21730f88c7362604 100755 --- a/bigbluebutton-client/src/VideoconfModule.mxml +++ b/bigbluebutton-client/src/VideoconfModule.mxml @@ -70,14 +70,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. return _attributes.userrole as String; } - public function get quality():Number{ - return Number(_attributes.videoQuality); - } - - public function get resolutions():String { - return _attributes.resolutions; - } - public function get presenterShareOnly():Boolean{ if (_attributes.presenterShareOnly == "true") return true; else return false; diff --git a/bigbluebutton-client/src/branding/css/assets/img/mconf-logo.png b/bigbluebutton-client/src/branding/css/assets/img/mconf-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..184cce468d90bf0be4bc4bb9320c0f8ae35e4e29 Binary files /dev/null and b/bigbluebutton-client/src/branding/css/assets/img/mconf-logo.png differ diff --git a/bigbluebutton-client/src/branding/css/mconf.css b/bigbluebutton-client/src/branding/css/mconf.css new file mode 100755 index 0000000000000000000000000000000000000000..10667831b49186528ac1c62c16f3de86c8612aca --- /dev/null +++ b/bigbluebutton-client/src/branding/css/mconf.css @@ -0,0 +1,265 @@ +/* + * TODO: LoggedOutWindow has scrollbars + * + * Other: + * light green: 1f7385 + * darker green: 27535C + * grey-ish light green: 637d7f + * navbar dark: #183338 + * navbar light: #27535C + * bootstrap blue: #0E90D2 + * bootstrap green: #468847 + * bootstrap green light: #DFF0D8 + * bootstrap green medium: #D6E9C6 + * Flex 3 css properties: http://www.loscavio.com/downloads/blog/flex3_css_list/flex3_css_list.htm#ComboBox +*/ + +/* To make something bizarre + backgroundAlpha: 1; + backgroundColor: #800000; + borderStyle: solid; + borderColor: #008000; + borderAlpha: 1; + borderThickness: 5; + cornerRadius: 10; + dropShadowEnabled: false; +*/ + +/* green to blue +#468847 -> #0E90D2 +#d6e9ff -> #d6e9ff +#DFF0D8 -> #d6e9ff +#569556 -> #468ce6 +*/ + + +Application +{ + /* top, bottom */ + backgroundGradientColors: #27535C,#183338; /*#27535C,#27535C; /* #637d7f; /*#444, #666;*/ + /* main color used everywhere: selections, button borders?, ... */ + themeColor: #468847; /*#1f7385;*/ + /* top alpha, bottom alpha */ + backgroundGradientAlphas: 0.8, 0.5; + + fontFamily: Verdana; + fontSize: 10; + color: #111111; + fontWeight: normal; +} + +/*The image for your logo is 200x200 pixels. No other size is currently supported so resize your logo accordingly. The logo will always appear in the lower right corner. */ +BrandingLogo +{ + backgroundImage: Embed(source="assets/img/mconf-logo.png"); + backgroundSize: "100%"; +} + +/* Borders around the application so the windows don't touch the edges. + Will affect MicSettings too :( */ +VBox +{ + paddingBottom: 0; + paddingTop: 0; + paddingRight: 5; + paddingLeft: 5; +} + +Text +{ + borderStyle: solid; + borderColor: #800000; + borderAlpha: 1; + borderThickness: 1; +} + +/* the big area where all internal windows are */ +MDICanvas +{ +} + +Button +{ + /* default color: border, hovers, etc. */ + themeColor: #468847; /*#27535C;*/ + /* normal all, smoothness? */ + highlightAlphas: 1, 0.15;/*1, 0.33;*/ + /* bottom normal?, bottom normal?, hover, hover (border?) */ + fillAlphas: 0.8, 0.6, 0.6, 0.8;/*1, 0.16, 0.18, 1;*/ + /* disabled top and middle normal (weak), bottom all, middle hover (weak), bottom hover. top normal always #fff? */ + fillColors: #dddddd, #ffffff, #dddddd, #cccccc; + + borderStyle: solid; + borderColor: #aaaaaa; + borderAlpha: 1; + borderThickness: 2; +} + +ComboBox, Button, TextArea, TextInput +{ + cornerRadius: 3; +} + +ComboBox /*, LinkButton */ +{ + selectionColor: #D6E9C6; /*#1f7385;*/ + rollOverColor: #dddddd; + textSelectedColor: #000000; /*#ffffff;*/ +} + +DataGrid +{ + selectionColor: #DFF0D8;/*#a3c0c7;*/ + rollOverColor: #dddddd; + /*textSelectedColor: #ffffff; TODO: how? */ +} + +/* Container that holds the entire app but also container that holds the control + buttons in every window title bar */ +LayoutContainer +{ + horizontalGap: 10; /* space between window controls (close, min) */ + verticalGap: 0; +} + +/* Both the top control bar and the control bar in the bottom of PresentModule */ +ApplicationControlBar +{ + backgroundAlpha: 0.01; + backgroundColor: #000000;/*#27535C;*/ + borderStyle: solid; + /*borderColor: #27535C;*/ + borderAlpha: 0; + borderThickness: 0; + cornerRadius: 0; + dropShadowEnabled: false; +} +/* doesn't work :( */ +/*ApplicationControlBar Button +{ + backgroundAlpha: 0.1; + backgroundColor: #000000; + borderThickness: 10; + cornerRadius: 1; + dropShadowEnabled: false; + themeColor: #800000; +}*/ + +/* Space around modal windows and aroung the control bars in the bottom of + almost all modules. Apparently doesn't work if TitleWindow is also + defined. */ +Panel +{ + /* To change the modal windows e.g. audio controls */ + /*borderStyle: solid; + borderColor: #ffffff; + borderAlpha: 0.8; + borderThickness: 2; + cornerRadius: 2; + dropShadowEnabled: true;*/ + + /* this style will match the bottom control bars that exist in almost all + modules, like the place with the chat input text control and btn */ + /*controlBarStyleName: "panelControlBar";*/ +} + +/* Internals of modal windows */ +TitleWindow +{ + backgroundAlpha: 1; + backgroundColor: #eeeeee; + borderStyle: solid; + borderColor: #ffffff; + borderAlpha: 0.9; + borderThickness: 4; + cornerRadius: 2; + dropShadowEnabled: true; + shadowDirection: right; +} + +ProgressBar +{ + borderStyle: solid; + borderColor: #cccccc; + borderAlpha: 0.9; + borderThickness: 0; + cornerRadius: 1; + barColor: #569556; + color: #222; /*ffffff;*/ +} + +/* matches the tab component in ChatModule */ +/*TabNavigator +{ +}*/ + +/* List of participants */ +DataGridColumn +{ + backgroundAlpha: 1; + backgroundColor: #800000; + borderStyle: solid; + borderColor: #008000; + borderAlpha: 1; + borderThickness: 5; + cornerRadius: 10; + dropShadowEnabled: false; +} + +/* Internal windows with focus */ +.mdiWindowFocus +{ + headerHeight: 22; + backgroundAlpha: 1; + backgroundColor: #dddddd; /*#637d7f;*/ + borderStyle: solid; + borderColor: #ffffff; /*#27535C;*/ + borderAlpha: 0.9; + borderThickness: 1; + cornerRadius: 1; + dropShadowEnabled: false; + titleStyleName: "mypanelTitleFocused"; +} +.mypanelTitleFocused +{ + fontFamily: Verdana; + fontSize: 10; + fontWeight: bold; + color: #111111; +} + +/* Internal windows without focus */ +.mdiWindowNoFocus +{ + headerHeight: 22; + backgroundAlpha: 0.95; + backgroundColor: #dddddd; /*#637d7f;*/ + borderStyle: solid; + borderColor: #eeeeee; + borderAlpha: 0.9; + borderThickness: 1; + cornerRadius: 1; + dropShadowEnabled: false; + titleStyleName: "mypanelTitle"; +} +.mypanelTitle +{ + fontFamily: Verdana; + fontSize: 10; + fontWeight: bold; + color: #111111; +} + +HSlider +{ + showTrackHighlight: true; +} + +ToolTip { + cornerRadius: 2; + paddingLeft: 3; + paddingRight: 3; + backgroundColor: #94bbbe; + color: #222222; + textAlign: center; +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/Images.as b/bigbluebutton-client/src/org/bigbluebutton/common/Images.as index b4a34d3575900b8fdfd51c79220f510ed8ab652d..4b2ebb019488db2746ef7a759197fe89f520c906 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/common/Images.as +++ b/bigbluebutton-client/src/org/bigbluebutton/common/Images.as @@ -22,9 +22,6 @@ package org.bigbluebutton.common public class Images { - [Embed(source="assets/images/page_link.png")] - public var page_link:Class; - [Embed(source="assets/images/users_settings.png")] public var users_settings:Class; @@ -78,9 +75,6 @@ package org.bigbluebutton.common [Embed(source="assets/images/vdoc_bg.jpg")] public var video_dock_bg:Class; - - [Embed(source="assets/images/bandwidth.png")] - public var bandwidth:Class; [Embed(source="assets/images/statistics.png")] public var stats:Class; @@ -190,9 +184,12 @@ package org.bigbluebutton.common [Embed(source="assets/images/ellipse.png")] public var circle_icon:Class; - [Embed(source="assets/images/arrow_out.png")] + [Embed(source="assets/images/ic_fullscreen_16px.png")] public var full_screen:Class; + [Embed(source="assets/images/ic_fullscreen_exit_16px.png")] + public var exit_full_screen:Class; + [Embed(source="assets/images/BBBlogo.png")] public var bbb_logo:Class; @@ -223,9 +220,6 @@ package org.bigbluebutton.common [Embed(source="assets/images/magnifier.png")] public var magnifier:Class; - [Embed(source="assets/images/add.png")] - public var add:Class; - [Embed(source="assets/images/bullet_go.png")] public var bulletGo:Class; @@ -270,12 +264,9 @@ package org.bigbluebutton.common [Embed(source="assets/images/poll_icon.png")] public var pollIcon:Class; - [Embed(source="assets/images/disk.png")] - public var disk:Class; + [Embed(source="assets/images/disk_grayscale.png")] + public var disk_grayscale:Class; - [Embed(source="assets/images/folder.png")] - public var folder:Class; - // PLACE CUSTOM IMAGES BELOW [Embed(source="assets/images/line.png")] public var line_icon:Class; @@ -306,5 +297,56 @@ package org.bigbluebutton.common [Embed(source="assets/images/grid_icon.png")] public var grid_icon:Class; + + [Embed(source="assets/images/ic_refresh_16px.png")] + public var refreshSmall:Class; + + [Embed(source="assets/images/moderator_white.png")] + public var moderator_white:Class; + + [Embed(source="assets/images/presenter_white.png")] + public var presenter_white:Class; + + [Embed(source="assets/images/user_add.png")] + public var user_add:Class; + + [Embed(source="assets/images/user_delete.png")] + public var user_delete:Class; + + [Embed(source="assets/images/status/ic_mood_black_18dp.png")] + public var mood:Class; + + [Embed(source="assets/images/status/ic_clear_black_18dp.png")] + public var mood_clear:Class; + + [Embed(source="assets/images/status/icon-3-high-five.png")] + public var mood_raise_hand:Class; + + [Embed(source="assets/images/status/icon-6-thumb-up.png")] + public var mood_agreed:Class; + + [Embed(source="assets/images/status/icon-7-thumb-down.png")] + public var mood_disagreed:Class; + + [Embed(source="assets/images/status/ic_fast_forward_black_18dp.png")] + public var mood_speak_faster:Class; + + [Embed(source="assets/images/status/ic_fast_rewind_black_18dp.png")] + public var mood_speak_slower:Class; + + [Embed(source="assets/images/status/ic_volume_up_black_18dp.png")] + public var mood_speak_louder:Class; + + [Embed(source="assets/images/status/ic_volume_down_black_18dp.png")] + public var mood_speak_softer:Class; + + [Embed(source="assets/images/status/ic_access_time_black_18dp.png")] + public var mood_be_right_back:Class; + + [Embed(source="assets/images/status/icon-6-smiling-face.png")] + public var mood_happy:Class; + + [Embed(source="assets/images/status/icon-7-sad-face.png")] + public var mood_sad:Class; } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/add.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/add.png deleted file mode 100644 index 6332fefea4be19eeadf211b0b202b272e8564898..0000000000000000000000000000000000000000 Binary files a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/add.png and /dev/null differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/arrow_refresh_small.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/arrow_refresh_small.png new file mode 100644 index 0000000000000000000000000000000000000000..d3087dfc920b1705cdebc5beeba8a62047c99b68 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/arrow_refresh_small.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/bandwidth.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/bandwidth.png deleted file mode 100755 index adb979ce5aa24832549312d1291605e4dfcc4cae..0000000000000000000000000000000000000000 Binary files a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/bandwidth.png and /dev/null differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/disk_grayscale.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/disk_grayscale.png new file mode 100644 index 0000000000000000000000000000000000000000..30056e2734a4077a030755f5de3d9db5c6804276 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/disk_grayscale.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/folder.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/folder.png deleted file mode 100644 index 784e8fa48234f4f64b6922a6758f254ee0ca08ec..0000000000000000000000000000000000000000 Binary files a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/folder.png and /dev/null differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_fullscreen_16px.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_fullscreen_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..685700f137f2e62b1c8f727d3fbb122438ec6300 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_fullscreen_16px.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_fullscreen_exit_16px.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_fullscreen_exit_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..e6c25681015ba7367447ec565750bd530359f7a2 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_fullscreen_exit_16px.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_refresh_16px.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_refresh_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..02a2d81f6f291371599e4bec81442a796a4d295b Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/ic_refresh_16px.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/moderator_white.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/moderator_white.png new file mode 100644 index 0000000000000000000000000000000000000000..002f1b8e858db0e3d6977ad4f730986e16560ec2 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/moderator_white.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/page_link.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/page_link.png deleted file mode 100644 index 319e27b1925e4f135f1f82873478452403516db9..0000000000000000000000000000000000000000 Binary files a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/page_link.png and /dev/null differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/presenter_white.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/presenter_white.png new file mode 100644 index 0000000000000000000000000000000000000000..273b2f17075bd8cd474e201de74004239f0978f8 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/presenter_white.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_access_time_black_18dp.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_access_time_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1fd032f2d667900c0a6e4803e4b22f5bbe66c25c Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_access_time_black_18dp.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_clear_black_18dp.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_clear_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..082c9bdfd9ca005efdacb9295c9a281fff1c1cf1 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_clear_black_18dp.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_fast_forward_black_18dp.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_fast_forward_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a1394ab3215683ef21c4c993af1bdbfdad663631 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_fast_forward_black_18dp.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_fast_rewind_black_18dp.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_fast_rewind_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..30fdf6e706b346a87914b3ae3c15172ddd15f75c Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_fast_rewind_black_18dp.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_mood_black_18dp.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_mood_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca438be9b3df3a1c4556dcd2051150df9db8e90 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_mood_black_18dp.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_volume_down_black_18dp.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_volume_down_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b51479f4cacce573c97f5a38522a3d320defe1f4 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_volume_down_black_18dp.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_volume_up_black_18dp.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_volume_up_black_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..58f4e270760ae3b7483a86de06bc21453e05dba4 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/ic_volume_up_black_18dp.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-3-high-five.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-3-high-five.png new file mode 100644 index 0000000000000000000000000000000000000000..4361fea6534359e9b1150d56718b8296101d5c35 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-3-high-five.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-6-smiling-face.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-6-smiling-face.png new file mode 100644 index 0000000000000000000000000000000000000000..860284020f261ccd08003396b182237b7699b10d Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-6-smiling-face.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-6-thumb-up.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-6-thumb-up.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2b76b33d690d289e8e22f05d913393797e6fd7 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-6-thumb-up.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-7-sad-face.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-7-sad-face.png new file mode 100644 index 0000000000000000000000000000000000000000..63a079cddb4ee5a7180f69be8ec6e91bc34ccf97 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-7-sad-face.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-7-thumb-down.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-7-thumb-down.png new file mode 100644 index 0000000000000000000000000000000000000000..e9b46550cad5ab24985d53da86c2f2cb88ca3653 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/status/icon-7-thumb-down.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/user_add.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/user_add.png new file mode 100644 index 0000000000000000000000000000000000000000..deae99bcff9815d8530a920e754d743700ddd5fb Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/user_add.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/user_delete.png b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/user_delete.png new file mode 100644 index 0000000000000000000000000000000000000000..acbb5630e51a12a1cd30ea799d659b309e7041cd Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/user_delete.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/events/SettingsComponentEvent.as b/bigbluebutton-client/src/org/bigbluebutton/common/events/SettingsComponentEvent.as new file mode 100755 index 0000000000000000000000000000000000000000..8318e40ee4d87f966b8af4ce3db71350cf58265e --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/common/events/SettingsComponentEvent.as @@ -0,0 +1,40 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +* +*/ + +package org.bigbluebutton.common.events +{ + import flash.events.Event; + + import mx.core.UIComponent; + + public class SettingsComponentEvent extends Event + { + public static const ADD:String = "Add Component Event"; + public static const REMOVE:String = "Remove Component Event"; + + public var component:UIComponent; + + public function SettingsComponentEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/BBB.as b/bigbluebutton-client/src/org/bigbluebutton/core/BBB.as index aa04a385d7ce29e12e057249d53b84ac450abb2d..729d09ac0608dcbaaa7c348c7e8f595f053cc30d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/BBB.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/BBB.as @@ -22,7 +22,9 @@ package org.bigbluebutton.core import org.bigbluebutton.core.managers.ConnectionManager; import org.bigbluebutton.core.managers.UserConfigManager; import org.bigbluebutton.core.managers.UserManager; + import org.bigbluebutton.core.managers.VideoProfileManager; import org.bigbluebutton.core.model.Session; + import org.bigbluebutton.core.model.VideoProfile; import flash.system.Capabilities; public class BBB { @@ -30,6 +32,7 @@ package org.bigbluebutton.core private static var connectionManager:ConnectionManager = null; private static var session:Session = null; private static var userConfigManager:UserConfigManager = null; + private static var videoProfileManager:VideoProfileManager = null; public static function initUserConfigManager():UserConfigManager { if (userConfigManager == null) { @@ -46,10 +49,34 @@ package org.bigbluebutton.core return configManager; } + public static function initVideoProfileManager():VideoProfileManager { + if (videoProfileManager == null) { + videoProfileManager = new VideoProfileManager(); + videoProfileManager.loadProfiles(); + } + return videoProfileManager; + } + public static function getConfigForModule(module:String):XML { return initConfigManager().config.getConfigFor(module); } + + public static function get videoProfiles():Array { + return initVideoProfileManager().profiles; + } + + public static function getVideoProfileById(id:String):VideoProfile { + return initVideoProfileManager().getVideoProfileById(id); + } + + public static function get defaultVideoProfile():VideoProfile { + return initVideoProfileManager().defaultVideoProfile; + } + public static function get fallbackVideoProfile():VideoProfile { + return initVideoProfileManager().fallbackVideoProfile; + } + public static function initConnectionManager():ConnectionManager { if (connectionManager == null) { connectionManager = new ConnectionManager(); diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/UsersUtil.as b/bigbluebutton-client/src/org/bigbluebutton/core/UsersUtil.as index 2dacffbac9fe93bbeb1cd59fa533713d33b296ec..efd18615b565e3647338afd940e6175dde5fcfcb 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/UsersUtil.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/UsersUtil.as @@ -76,10 +76,10 @@ package org.bigbluebutton.core return false; } - public static function getWebcamStream(userID:String):String { + public static function getWebcamStream(userID:String):Array { var u:BBBUser = getUser(userID); if (u != null && u.hasStream) { - return u.streamName; + return u.streamNames; } return null; @@ -128,6 +128,10 @@ package org.bigbluebutton.core public static function getUser(userID:String):BBBUser { return UserManager.getInstance().getConference().getUser(userID); } + + public static function getMyself():BBBUser { + return UserManager.getInstance().getConference().getMyself(); + } public static function isMe(userID:String):Boolean { return UserManager.getInstance().getConference().amIThisUser(userID); @@ -200,4 +204,4 @@ package org.bigbluebutton.core } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/managers/ConnectionManager.as b/bigbluebutton-client/src/org/bigbluebutton/core/managers/ConnectionManager.as index 1e92d35e78154a48568634baa5f3bd8de2ab1055..550e139b2c4127927e24f917925106971161b06d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/managers/ConnectionManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/managers/ConnectionManager.as @@ -67,6 +67,10 @@ package org.bigbluebutton.core.managers public function forceClose():void { connDelegate.forceClose(); } + + public function guestDisconnect():void { + connDelegate.guestDisconnect(); + } } } \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/managers/VideoProfileManager.as b/bigbluebutton-client/src/org/bigbluebutton/core/managers/VideoProfileManager.as new file mode 100644 index 0000000000000000000000000000000000000000..524f66b8e86e67cb2e6f2f709b62f05251baecf8 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/core/managers/VideoProfileManager.as @@ -0,0 +1,119 @@ +/** + * 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.core.managers +{ + import com.asfusion.mate.events.Dispatcher; + + import flash.events.Event; + import flash.events.EventDispatcher; + import flash.net.URLLoader; + import flash.net.URLRequest; + + import mx.core.FlexGlobals; + import mx.utils.URLUtil; + + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.EventBroadcaster; + import org.bigbluebutton.core.model.VideoProfile; + + public class VideoProfileManager extends EventDispatcher { + + public static const PROFILES_XML:String = "client/conf/profiles.xml"; + public static const DEFAULT_FALLBACK_LOCALE:String = "en_US"; + private var _profiles:Array = new Array(); + + public function loadProfiles():void { + var urlLoader:URLLoader = new URLLoader(); + urlLoader.addEventListener(Event.COMPLETE, handleComplete); + var date:Date = new Date(); + var localeReqURL:String = buildRequestURL() + "?a=" + date.time; + trace("VideoProfileManager::loadProfiles [" + localeReqURL + "]"); + urlLoader.load(new URLRequest(localeReqURL)); + } + + private function buildRequestURL():String { + var swfURL:String = FlexGlobals.topLevelApplication.url; + var protocol:String = URLUtil.getProtocol(swfURL); + var serverName:String = URLUtil.getServerNameWithPort(swfURL); + return protocol + "://" + serverName + "/" + PROFILES_XML; + } + + private function handleComplete(e:Event):void{ + trace("VideoProfileManager::handleComplete [" + new XML(e.target.data) + "]"); + + // first clear the array + _profiles.splice(0); + + var profiles:XML = new XML(e.target.data); + var fallbackLocale:String = profiles.@fallbackLocale != undefined? profiles.@fallbackLocale.toString(): DEFAULT_FALLBACK_LOCALE; + for each (var profile:XML in profiles.children()) { + _profiles.push(new VideoProfile(profile, fallbackLocale)); + } + } + + public function get profiles():Array { + if (_profiles.length > 0) { + return _profiles; + } else { + return [ fallbackVideoProfile ]; + } + } + + public function getVideoProfileById(id:String):VideoProfile { + for each (var profile:VideoProfile in _profiles) { + if (profile.id == id) { + return profile; + } + } + return null; + } + + public function get defaultVideoProfile():VideoProfile { + for each (var profile:VideoProfile in _profiles) { + if (profile.defaultProfile) { + return profile; + } + } + if (_profiles.length > 0) { + return _profiles[0]; + } else { + return null; + } + } + + public function get fallbackVideoProfile():VideoProfile { + return new VideoProfile( + <profile id="4L7344ZoBYGTocbHOIvzXsrGiBGoohFv" default="true"> + <locale> + <en_US>Fallback profile</en_US> + </locale> + <width>320</width> + <height>240</height> + <keyFrameInterval>5</keyFrameInterval> + <modeFps>10</modeFps> + <qualityBandwidth>0</qualityBandwidth> + <qualityPicture>90</qualityPicture> + <enableH264>true</enableH264> + <h264Level>2.1</h264Level> + <h264Profile>main</h264Profile> + </profile> + , DEFAULT_FALLBACK_LOCALE); + } + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/model/Config.as b/bigbluebutton-client/src/org/bigbluebutton/core/model/Config.as index 529ba154e0e8cbebf3afad132a87dc6761ed4ab0..f54d33eea0b8f315b03c9da02ab9470f42093d81 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/model/Config.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/model/Config.as @@ -88,6 +88,14 @@ package org.bigbluebutton.core.model return new XML(config.browserVersions.toXMLString()); } + public function get branding():Object{ + var a:Object = new Object(); + a.copyright = config.branding.@copyright; + a.logo = config.branding.@logo; + a.background = config.branding.@background; + return a + } + public function get layout():XML { return new XML(config.layout.toXMLString()); } diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/model/MeBuilder.as b/bigbluebutton-client/src/org/bigbluebutton/core/model/MeBuilder.as index 50a77f661e4afc87140bb0e23330d5a3d71b6984..b69d15ac8715fb72e3f86e9e14bb441996653f93 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/core/model/MeBuilder.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/model/MeBuilder.as @@ -12,6 +12,7 @@ package org.bigbluebutton.core.model internal var logoutURL:String; internal var dialNumber:String; internal var role:String; + internal var guest:Boolean; internal var customData:Object; public function MeBuilder(id: String, name: String) { @@ -58,6 +59,11 @@ package org.bigbluebutton.core.model role = value; return this; } + + public function withGuest(value: Boolean):MeBuilder { + guest = value; + return this; + } public function withCustomData(value: Object):MeBuilder { customData = value; diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/model/VideoProfile.as b/bigbluebutton-client/src/org/bigbluebutton/core/model/VideoProfile.as new file mode 100644 index 0000000000000000000000000000000000000000..022201573f26015554e886a704dbbe303f600b64 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/core/model/VideoProfile.as @@ -0,0 +1,168 @@ +/** + * 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.core.model +{ + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.util.i18n.ResourceUtil; + import flash.utils.Dictionary; + + public class VideoProfile + { + private var _fallbackLanguage:String; + + private static var _nextId:int = -1; + private var _id:String; + private var _default:Boolean = false; + private var _name:Dictionary = new Dictionary(); + private var _width:int = 320; + private var _height:int = 240; + private var _keyFrameInterval:int = 30; + private var _modeFps:int = 10; + private var _qualityBandwidth:int = 0; + private var _qualityPicture:int = 90; + private var _enableH264:Boolean = true; + private var _h264Level:String = "2.1"; + private var _h264Profile:String = "main"; + + public function VideoProfile(vxml:XML, fallbackLanguage:String) + { + _fallbackLanguage = fallbackLanguage; + + if (vxml.@id != undefined) { + _id = vxml.@id.toString(); + } else { + _id = String(nextId()); + } + if (vxml.@default != undefined) { + _default = (vxml.@default.toString().toUpperCase() == "TRUE") ? true : false; + } + if (vxml.locale != undefined) { + for each (var locale:XML in vxml.locale.children()) { + _name[locale.localName()] = locale.toString(); + } + } + if (vxml.width != undefined) { + _width = vxml.width; + } + if (vxml.height != undefined) { + _height = vxml.height; + } + if (vxml.keyFrameInterval != undefined) { + _keyFrameInterval = vxml.keyFrameInterval; + } + if (vxml.modeFps != undefined) { + _modeFps = vxml.modeFps; + } + if (vxml.qualityBandwidth != undefined) { + _qualityBandwidth = vxml.qualityBandwidth; + } + if (vxml.qualityPicture != undefined) { + _qualityPicture = vxml.qualityPicture; + } + if (vxml.enableH264 != undefined) { + _enableH264 = (vxml.enableH264.toString().toUpperCase() == "TRUE") ? true : false; + } + if (vxml.h264Level != undefined) { + _h264Level = vxml.h264Level.toString(); + } + if (vxml.h264Profile != undefined) { + _h264Profile = vxml.h264Profile.toString(); + } + + trace("This is a new video profile"); + trace(this.toString()); + } + + public function toString():String { + return "VideoProfile [ " + + "id: " + this.id + ", " + + "default: " + this.defaultProfile + ", " + + "name: " + this.name + ", " + + "width: " + this.width + ", " + + "height: " + this.height + ", " + + "keyFrameInterval: " + this.keyFrameInterval + ", " + + "modeFps: " + this.modeFps + ", " + + "qualityBandwidth: " + this.qualityBandwidth + ", " + + "qualityPicture: " + this.qualityPicture + ", " + + "enableH264: " + this.enableH264 + ", " + + "h264Level: " + this.h264Level + ", " + + "h264Profile: " + this.h264Profile + " ]"; + } + + private static function nextId():int { + _nextId++; + return _nextId; + } + + public function get id():String { + return _id; + } + + public function get defaultProfile():Boolean { + return _default; + } + + public function get name():String { + var locale:String = ResourceUtil.getInstance().getCurrentLanguageCode(); + if (_name.hasOwnProperty(locale)) { + return _name[locale]; + } else if (_name.hasOwnProperty(_fallbackLanguage)) { + return _name[_fallbackLanguage]; + } else { + return ""; + } + } + + public function get width():int { + return _width; + } + + public function get height():int { + return _height; + } + + public function get keyFrameInterval():int { + return _keyFrameInterval; + } + + public function get modeFps():int { + return _modeFps; + } + + public function get qualityBandwidth():int { + return _qualityBandwidth; + } + + public function get qualityPicture():int { + return _qualityPicture; + } + + public function get enableH264():Boolean { + return _enableH264; + } + + public function get h264Level():String { + return _h264Level; + } + + public function get h264Profile():String { + return _h264Profile; + } + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/model/users/UserBuilder.as b/bigbluebutton-client/src/org/bigbluebutton/core/model/users/UserBuilder.as index 5bb1c9ed70c6c2175952d9eaa01cb430a8f7fc93..41a5db2f8bbe4601842503872476e126b2ab9833 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/model/users/UserBuilder.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/model/users/UserBuilder.as @@ -43,6 +43,10 @@ package org.bigbluebutton.core.model.users public function withRole(value: String):UserBuilder { return this; } + + public function withGuest(value: Boolean):UserBuilder { + return this; + } public function withCustomData(value: String):UserBuilder { return this; diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/services/BandwidthMonitor.as b/bigbluebutton-client/src/org/bigbluebutton/core/services/BandwidthMonitor.as index 77a1054a99b2fbf276cd6c08c8d99b9d2c72500d..8465081e70f15ef91114017f1318ac4327988915 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/services/BandwidthMonitor.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/services/BandwidthMonitor.as @@ -19,6 +19,7 @@ package org.bigbluebutton.core.services { import flash.events.AsyncErrorEvent; + import flash.events.IOErrorEvent; import flash.events.NetStatusEvent; import flash.events.TimerEvent; import flash.net.NetConnection; @@ -34,43 +35,137 @@ package org.bigbluebutton.core.services import org.red5.flash.bwcheck.events.BandwidthDetectEvent; public class BandwidthMonitor { + public static const INTERVAL_BETWEEN_CHECKS:int = 30000; // in ms + + private static var _instance:BandwidthMonitor = null; private var _serverURL:String = "localhost"; private var _serverApplication:String = "video"; private var _clientServerService:String = "checkBandwidthUp"; private var _serverClientService:String = "checkBandwidth"; - private var nc:NetConnection; + private var _pendingClientToServer:Boolean; + private var _pendingServerToClient:Boolean; + private var _lastClientToServerCheck:Date; + private var _lastServerToClientCheck:Date; + private var _runningMeasurement:Boolean; + private var _connecting:Boolean; + private var _nc:NetConnection; - private var bwTestTimer:Timer; + /** + * This class is a singleton. Please initialize it using the getInstance() method. + */ + public function BandwidthMonitor(enforcer:SingletonEnforcer) { + if (enforcer == null) { + throw new Error("There can only be one instance of this class"); + } + initialize(); + } - public function BandwidthMonitor() { - + private function initialize():void { + _pendingClientToServer = false; + _pendingServerToClient = false; + _runningMeasurement = false; + _connecting = false; + _lastClientToServerCheck = null; + _lastServerToClientCheck = null; + + _nc = new NetConnection(); + _nc.objectEncoding = flash.net.ObjectEncoding.AMF0; + _nc.client = this; + _nc.addEventListener(NetStatusEvent.NET_STATUS, onStatus); + _nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); + _nc.addEventListener(IOErrorEvent.IO_ERROR, onIOError); } + /** + * Return the single instance of this class + */ + public static function getInstance():BandwidthMonitor { + if (_instance == null) { + _instance = new BandwidthMonitor(new SingletonEnforcer()); + } + return _instance; + } + public function set serverURL(url:String):void { - _serverURL = url; + if (_nc.connected) + _nc.close(); + _serverURL = url; } public function set serverApplication(app:String):void { - _serverApplication = app; + if (_nc.connected) + _nc.close(); + _serverApplication = app; } - public function start():void { - connect(); - } - private function connect():void { - nc = new NetConnection(); - nc.objectEncoding = flash.net.ObjectEncoding.AMF0; - nc.proxyType = "best"; - nc.client = this; - nc.addEventListener(NetStatusEvent.NET_STATUS, onStatus); - nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); - nc.connect("rtmp://" + _serverURL + "/" + _serverApplication); + if (!_nc.connected && !_connecting) { + _nc.connect("rtmp://" + _serverURL + "/" + _serverApplication); + _connecting = true; + } } - private function onAsyncError(event:AsyncErrorEvent):void - { - LogUtil.debug(event.error.toString()); + public function checkClientToServer():void { + if (_lastClientToServerCheck != null && _lastClientToServerCheck.getTime() + INTERVAL_BETWEEN_CHECKS > new Date().getTime()) + return; + + if (!_nc.connected) { + _pendingClientToServer = true; + connect(); + } if (_runningMeasurement) { + _pendingClientToServer = true; + } else { + _pendingClientToServer = false; + _runningMeasurement = true; + _lastClientToServerCheck = new Date(); + + LogUtil.debug("Start client-server bandwidth detection"); + var clientServer:ClientServerBandwidth = new ClientServerBandwidth(); + clientServer.connection = _nc; + clientServer.service = _clientServerService; + clientServer.addEventListener(BandwidthDetectEvent.DETECT_COMPLETE,onClientServerComplete); + clientServer.addEventListener(BandwidthDetectEvent.DETECT_STATUS,onClientServerStatus); + clientServer.addEventListener(BandwidthDetectEvent.DETECT_FAILED,onDetectFailed); + clientServer.start(); + } + } + + public function checkServerToClient():void { + if (_lastServerToClientCheck != null && _lastServerToClientCheck.getTime() + INTERVAL_BETWEEN_CHECKS > new Date().getTime()) + return; + + if (!_nc.connected) { + _pendingServerToClient = true; + connect(); + } if (_runningMeasurement) { + _pendingServerToClient = true; + } else { + _pendingServerToClient = false; + _runningMeasurement = true; + _lastServerToClientCheck = new Date(); + + LogUtil.debug("Start server-client bandwidth detection"); + var serverClient:ServerClientBandwidth = new ServerClientBandwidth(); + serverClient.connection = _nc; + serverClient.service = _serverClientService; + serverClient.addEventListener(BandwidthDetectEvent.DETECT_COMPLETE,onServerClientComplete); + serverClient.addEventListener(BandwidthDetectEvent.DETECT_STATUS,onServerClientStatus); + serverClient.addEventListener(BandwidthDetectEvent.DETECT_FAILED,onDetectFailed); + serverClient.start(); + } + } + + private function checkPendingOperations():void { + if (_pendingClientToServer) checkClientToServer(); + if (_pendingServerToClient) checkServerToClient(); + } + + private function onAsyncError(event:AsyncErrorEvent):void { + LogUtil.debug(event.error.toString()); + } + + private function onIOError(event:IOErrorEvent):void { + LogUtil.debug(event.text); } private function onStatus(event:NetStatusEvent):void @@ -79,86 +174,47 @@ package org.bigbluebutton.core.services { case "NetConnection.Connect.Success": LogUtil.debug("Starting to monitor bandwidth between client and server"); - // monitor(); break; default: LogUtil.debug("Cannot establish the connection to measure bandwidth"); break; - } - } - - private function monitor():void { - LogUtil.debug("Starting to monitor bandwidth"); - bwTestTimer = new Timer(30000); - bwTestTimer.addEventListener(TimerEvent.TIMER, rtmptRetryTimerHandler); - bwTestTimer.start(); - } - - private function rtmptRetryTimerHandler(event:TimerEvent):void { - LogUtil.debug("Starting to detect bandwidth from server to client"); - ServerClient(); - } - - public function ClientServer():void - { - var clientServer:ClientServerBandwidth = new ClientServerBandwidth(); - //connect(); - clientServer.connection = nc; - clientServer.service = _clientServerService; - clientServer.addEventListener(BandwidthDetectEvent.DETECT_COMPLETE,onClientServerComplete); - clientServer.addEventListener(BandwidthDetectEvent.DETECT_STATUS,onClientServerStatus); - clientServer.addEventListener(BandwidthDetectEvent.DETECT_FAILED,onDetectFailed); - clientServer.start(); - } - - public function ServerClient():void - { - var serverClient:ServerClientBandwidth = new ServerClientBandwidth(); - //connect(); - serverClient.connection = nc; - serverClient.service = _serverClientService; - serverClient.addEventListener(BandwidthDetectEvent.DETECT_COMPLETE,onServerClientComplete); - serverClient.addEventListener(BandwidthDetectEvent.DETECT_STATUS,onServerClientStatus); - serverClient.addEventListener(BandwidthDetectEvent.DETECT_FAILED,onDetectFailed); - serverClient.start(); + } + _connecting = false; + checkPendingOperations(); } - public function onDetectFailed(event:BandwidthDetectEvent):void - { + public function onDetectFailed(event:BandwidthDetectEvent):void { LogUtil.debug("Detection failed with error: " + event.info.application + " " + event.info.description); + _runningMeasurement = false; } - public function onClientServerComplete(event:BandwidthDetectEvent):void - { -// LogUtil.debug("Client-slient bandwidth detect complete"); - + public function onClientServerComplete(event:BandwidthDetectEvent):void { + LogUtil.debug("Client-server bandwidth detection complete"); // LogUtil.debug(ObjectUtil.toString(event.info)); NetworkStatsData.getInstance().setUploadMeasuredBW(event.info); + _runningMeasurement = false; + checkPendingOperations(); } - public function onClientServerStatus(event:BandwidthDetectEvent):void - { - if (event.info) { + public function onClientServerStatus(event:BandwidthDetectEvent):void { +// if (event.info) { // LogUtil.debug("\n count: "+event.info.count+ " sent: "+event.info.sent+" timePassed: "+event.info.timePassed+" latency: "+event.info.latency+" overhead: "+event.info.overhead+" packet interval: " + event.info.pakInterval + " cumLatency: " + event.info.cumLatency); - } +// } } - public function onServerClientComplete(event:BandwidthDetectEvent):void - { -// LogUtil.debug("Server-client bandwidth detect complete"); - + public function onServerClientComplete(event:BandwidthDetectEvent):void { + LogUtil.debug("Server-client bandwidth detection complete"); // LogUtil.debug(ObjectUtil.toString(event.info)); NetworkStatsData.getInstance().setDownloadMeasuredBW(event.info); - -// LogUtil.debug("Detecting Client Server Bandwidth"); - ClientServer(); + _runningMeasurement = false; + checkPendingOperations(); } - public function onServerClientStatus(event:BandwidthDetectEvent):void - { - if (event.info) { + public function onServerClientStatus(event:BandwidthDetectEvent):void { +// if (event.info) { // LogUtil.debug("\n count: "+event.info.count+ " sent: "+event.info.sent+" timePassed: "+event.info.timePassed+" latency: "+event.info.latency+" cumLatency: " + event.info.cumLatency); - } +// } } } } +class SingletonEnforcer{} diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/services/StreamMonitor.as b/bigbluebutton-client/src/org/bigbluebutton/core/services/StreamMonitor.as index 0bcccf89cca99787cf3ccf52c9b093c310fa492f..c7d305ce9b2e219877e165be363e5dcdc1d238bb 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/services/StreamMonitor.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/services/StreamMonitor.as @@ -130,6 +130,9 @@ package org.bigbluebutton.core.services var download:Dictionary = new Dictionary(); var upload:Dictionary = new Dictionary(); + + download["byteCount"] = upload["byteCount"] + = download["currentBytesPerSecond"] = upload["currentBytesPerSecond"] = 0; for (var i:int = 0; i < streams.length; i++) { if (streams[i] == null || streams[i].info == null) { @@ -137,13 +140,13 @@ package org.bigbluebutton.core.services continue; } -// log("Heartbeat on " + streams[i].info); + //log("Heartbeat on " + streams[i].info); var remote:Boolean = isRemoteStream(streams[i]); var ref:Dictionary = (remote? download: upload); if (streams[i].info.uri == null) { - log("Stream URI is null, returning"); + //log("Stream URI is null, returning"); continue; } var uri:String = streams[i].info.uri.toLowerCase(); @@ -174,7 +177,7 @@ package org.bigbluebutton.core.services var property:String = s.@name; var num:Number = 0; if (ref.hasOwnProperty(property)) - num = ref[property] as Number; + num = (ref[property] as Number); num += (streams[i].info[property] as Number); ref[property] = num; } @@ -206,11 +209,6 @@ package org.bigbluebutton.core.services //log(value.streamName + ": " + value.byteCount); } -// var netstatsEvent:NetworkStatsEvent = new NetworkStatsEvent(); -// netstatsEvent.downloadStats = download; -// netstatsEvent.uploadStats = upload; -// _globalDispatcher.dispatchEvent(netstatsEvent); - NetworkStatsData.getInstance().updateConsumedBW(download["currentBytesPerSecond"], upload["currentBytesPerSecond"], download["byteCount"], @@ -224,8 +222,7 @@ package org.bigbluebutton.core.services } private function log(s:String):void { - //LogUtil.debug("[StreamMonitor] " + s); - trace("[StreamMonitor] " + s); + LogUtil.debug("[StreamMonitor] " + s); } } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/vo/CameraSettingsVO.as b/bigbluebutton-client/src/org/bigbluebutton/core/vo/CameraSettingsVO.as index dfc431aa461b778669bdc344be1fe885f77b465e..2664268106e626210d3ea870a0daaec1ef515568 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/vo/CameraSettingsVO.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/vo/CameraSettingsVO.as @@ -18,11 +18,12 @@ */ package org.bigbluebutton.core.vo { + import org.bigbluebutton.core.model.VideoProfile; + public class CameraSettingsVO { public var camIndex:int = 0; - public var camWidth:int = 0; - public var camHeight:int = 0; + public var videoProfile:VideoProfile = null; public var isPublishing:Boolean = false; } diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/vo/Config.as b/bigbluebutton-client/src/org/bigbluebutton/core/vo/Config.as index d005b462285c4cb1d8b7f492b3fe9ee2e5eebdfb..9e5d239d1d178aa3e022e10471a4579c8c0009d3 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/vo/Config.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/vo/Config.as @@ -30,6 +30,9 @@ package org.bigbluebutton.core.vo { private var _shortcutKeysShowButton:Boolean; private var _skinning:String = ""; private var _showDebug:Boolean = false; + private var _copyright:String; + private var _logo:String; + private var _background:String; public function Config(builder:ConfigBuilder) { _version = builder.version; @@ -44,6 +47,9 @@ package org.bigbluebutton.core.vo { _shortcutKeysShowButton = builder.shortcutKeysShowButton; _skinning = builder.skinning; _showDebug = builder.showDebug; + _copyright = builder.copyright; + _logo = builder.logo; + _background = builder.background; } public function get version():String { @@ -93,5 +99,17 @@ package org.bigbluebutton.core.vo { public function get showDebug():Boolean { return _showDebug; } + + public function get copyright():String { + return _copyright; + } + + public function get logo():String { + return _logo; + } + + public function get background():String { + return _background; + } } } \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/core/vo/ConfigBuilder.as b/bigbluebutton-client/src/org/bigbluebutton/core/vo/ConfigBuilder.as index 6e8f0ed2ed121efb0a3c7552090e7155ddb50d0a..a96d0093862e56396ff575539c396a4d88325ebd 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/core/vo/ConfigBuilder.as +++ b/bigbluebutton-client/src/org/bigbluebutton/core/vo/ConfigBuilder.as @@ -30,6 +30,9 @@ package org.bigbluebutton.core.vo { internal var shortcutKeysShowButton:Boolean; internal var skinning:String = ""; internal var showDebug:Boolean = false; + internal var copyright:String; + internal var logo:String; + internal var background:String; public function ConfigBuilder(version:String, localVersion:String){ this.version = version; @@ -85,6 +88,21 @@ package org.bigbluebutton.core.vo { this.showDebug = showDebug; return this; } + + public function withCopyright(copyright:String):ConfigBuilder { + this.copyright = copyright; + return this; + } + + public function withLogo(logo:String):ConfigBuilder { + this.logo = logo; + return this; + } + + public function withBackground(background:String):ConfigBuilder { + this.background = background; + return this; + } public function build():Config { return new Config(this); diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCallbacks.as b/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCallbacks.as index e4b814c3fc8e8bc5684f363ee9cf961f21806612..edea88ba32a7fa33391b4f93540bdd7ee585a2d7 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCallbacks.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCallbacks.as @@ -34,8 +34,8 @@ package org.bigbluebutton.main.api import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.core.vo.CameraSettingsVO; import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.model.users.events.ChangeStatusEvent; import org.bigbluebutton.main.model.users.events.KickUserEvent; - import org.bigbluebutton.main.model.users.events.RaiseHandEvent; import org.bigbluebutton.main.model.users.events.RoleChangeEvent; import org.bigbluebutton.modules.deskshare.events.DeskshareAppletLaunchedEvent; import org.bigbluebutton.modules.deskshare.utils.JavaCheck; @@ -141,9 +141,9 @@ package org.bigbluebutton.main.api } private function handleRaiseHandRequest(handRaised:Boolean):void { - trace(LOG + "Received raise hand request from JS API [" + handRaised + "]"); - var e:RaiseHandEvent = new RaiseHandEvent(RaiseHandEvent.RAISE_HAND); - e.raised = handRaised; + trace("Received raise hand request from JS API [" + handRaised + "]"); + var userID:String = UserManager.getInstance().getConference().getMyUserId(); + var e:ChangeStatusEvent = new ChangeStatusEvent(userID, handRaised? ChangeStatusEvent.RAISE_HAND: ChangeStatusEvent.CLEAR_STATUS); _dispatcher.dispatchEvent(e); } @@ -156,8 +156,8 @@ package org.bigbluebutton.main.api var obj:Object = new Object(); var isUserPublishing:Boolean = false; - var streamName:String = UsersUtil.getWebcamStream(UsersUtil.externalUserIDToInternalUserID(userID)); - if (streamName != null) { + var streamNames:Array = UsersUtil.getWebcamStream(UsersUtil.externalUserIDToInternalUserID(userID)); + if (streamNames && streamNames.length > 0) { isUserPublishing = true; } @@ -165,7 +165,7 @@ package org.bigbluebutton.main.api obj.uri = vidConf.uri + "/" + UsersUtil.getInternalMeetingID(); obj.userID = userID; obj.isUserPublishing = isUserPublishing; - obj.streamName = streamName; + obj.streamNames = streamNames; obj.avatarURL = UsersUtil.getAvatarURL(); return obj; @@ -195,15 +195,12 @@ package org.bigbluebutton.main.api var camSettings:CameraSettingsVO = UsersUtil.amIPublishing(); obj.isPublishing = camSettings.isPublishing; obj.camIndex = camSettings.camIndex; - obj.camWidth = camSettings.camWidth; - obj.camHeight = camSettings.camHeight; - - var vidConf:VideoConfOptions = new VideoConfOptions(); - - obj.camKeyFrameInterval = vidConf.camKeyFrameInterval; - obj.camModeFps = vidConf.camModeFps; - obj.camQualityBandwidth = vidConf.camQualityBandwidth; - obj.camQualityPicture = vidConf.camQualityPicture; + obj.camWidth = camSettings.videoProfile.width; + obj.camHeight = camSettings.videoProfile.height; + obj.camKeyFrameInterval = camSettings.videoProfile.keyFrameInterval; + obj.camModeFps = camSettings.videoProfile.modeFps; + obj.camQualityBandwidth = camSettings.videoProfile.qualityBandwidth; + obj.camQualityPicture = camSettings.videoProfile.qualityPicture; obj.avatarURL = UsersUtil.getAvatarURL(); return obj; diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCalls.as b/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCalls.as index 91158c6e8ba60887c3dbe496ef8de222750b62d2..2b79e1c25253eb7034bcd8e92dc92201d0bc5851 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCalls.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/api/ExternalApiCalls.as @@ -77,8 +77,8 @@ package org.bigbluebutton.main.api var payload:Object = new Object(); var isUserPublishing:Boolean = false; - var streamName:String = UsersUtil.getWebcamStream(event.userID); - if (streamName != null) { + var streamNames:Array = UsersUtil.getWebcamStream(event.userID); + if (streamNames && streamNames.length > 0) { isUserPublishing = true; } @@ -89,7 +89,7 @@ package org.bigbluebutton.main.api var vidConf:VideoConfOptions = new VideoConfOptions(); payload.uri = vidConf.uri + "/" + UsersUtil.getInternalMeetingID(); payload.avatarURL = UsersUtil.getAvatarURL(); - payload.streamName = streamName; + payload.streamNames = streamNames; broadcastEvent(payload); } @@ -141,20 +141,18 @@ package org.bigbluebutton.main.api } public function handleBroadcastStartedEvent(event:BroadcastStartedEvent):void { - var vidConf:VideoConfOptions = new VideoConfOptions(); - var payload:Object = new Object(); payload.eventName = EventConstants.BROADCASTING_CAM_STARTED; payload.isPresenter = UsersUtil.amIPresenter(); payload.streamName = event.stream; payload.isPublishing = event.camSettings.isPublishing; payload.camIndex = event.camSettings.camIndex; - payload.camWidth = event.camSettings.camWidth; - payload.camHeight = event.camSettings.camHeight; - payload.camKeyFrameInterval = vidConf.camKeyFrameInterval; - payload.camModeFps = vidConf.camModeFps; - payload.camQualityBandwidth = vidConf.camQualityBandwidth; - payload.camQualityPicture = vidConf.camQualityPicture; + payload.camWidth = event.camSettings.videoProfile.width; + payload.camHeight = event.camSettings.videoProfile.height; + payload.camKeyFrameInterval = event.camSettings.videoProfile.keyFrameInterval; + payload.camModeFps = event.camSettings.videoProfile.modeFps; + payload.camQualityBandwidth = event.camSettings.videoProfile.qualityBandwidth; + payload.camQualityPicture = event.camSettings.videoProfile.qualityPicture; payload.avatarURL = UsersUtil.getAvatarURL(); broadcastEvent(payload); @@ -162,18 +160,17 @@ package org.bigbluebutton.main.api public function handleAmISharingCamQueryEvent(event:AmISharingWebcamQueryEvent):void { var camSettings:CameraSettingsVO = UsersUtil.amIPublishing(); - var vidConf:VideoConfOptions = new VideoConfOptions(); var payload:Object = new Object(); payload.eventName = EventConstants.AM_I_SHARING_CAM_RESP; payload.isPublishing = camSettings.isPublishing; payload.camIndex = camSettings.camIndex; - payload.camWidth = camSettings.camWidth; - payload.camHeight = camSettings.camHeight; - payload.camKeyFrameInterval = vidConf.camKeyFrameInterval; - payload.camModeFps = vidConf.camModeFps; - payload.camQualityBandwidth = vidConf.camQualityBandwidth; - payload.camQualityPicture = vidConf.camQualityPicture; + payload.camWidth = camSettings.videoProfile.width; + payload.camHeight = camSettings.videoProfile.height; + payload.camKeyFrameInterval = camSettings.videoProfile.keyFrameInterval; + payload.camModeFps = camSettings.videoProfile.modeFps; + payload.camQualityBandwidth = camSettings.videoProfile.qualityBandwidth; + payload.camQualityPicture = camSettings.videoProfile.qualityPicture; payload.avatarURL = UsersUtil.getAvatarURL(); broadcastEvent(payload); @@ -413,4 +410,4 @@ package org.bigbluebutton.main.api } } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/BBBEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/BBBEvent.as index abb5689d61a4b542d38d603e5f2797c1601444e4..87a1f846780a9248d3fbbc8383a6fec5bde8ac5a 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/events/BBBEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/BBBEvent.as @@ -42,6 +42,18 @@ package org.bigbluebutton.main.events { public static const CAM_SETTINGS_CLOSED:String = "CAM_SETTINGS_CLOSED"; public static const JOIN_VOICE_FOCUS_HEAD:String = "JOIN_VOICE_FOCUS_HEAD"; public static const CHANGE_RECORDING_STATUS:String = "CHANGE_RECORDING_STATUS"; + + public static const SETTINGS_CONFIRMED:String = "BBB_SETTINGS_CONFIRMED"; + public static const SETTINGS_CANCELLED:String = "BBB_SETTINGS_CANCELLED"; + + public static const ACCEPT_ALL_WAITING_GUESTS:String = "BBB_ACCEPT_ALL_WAITING_GUESTS"; + public static const DENY_ALL_WAITING_GUESTS:String = "BBB_DENY_ALL_WAITING_GUESTS"; + public static const BROADCAST_GUEST_POLICY:String = "BBB_BROADCAST_GUEST_POLICY"; + public static const RETRIEVE_GUEST_POLICY:String = "BBB_RETRIEVE_GUEST_POLICY"; + public static const MODERATOR_ALLOWED_ME_TO_JOIN:String = "MODERATOR_ALLOWED_ME_TO_JOIN"; + public static const WAITING_FOR_MODERATOR_ACCEPTANCE:String = "WAITING_FOR_MODERATOR_ACCEPTANCE"; + public static const ADD_GUEST_TO_LIST:String = "ADD_GUEST_TO_LIST"; + public static const REMOVE_GUEST_FROM_LIST:String = "REMOVE_GUEST_FROM_LIST"; public var message:String; public var payload:Object = new Object(); diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/LogoutEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/LogoutEvent.as index fb99b50209129e567e291dc70ea8958af25bea80..a8504044c4c92bee56524e0728c86c4ad1243b55 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/events/LogoutEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/LogoutEvent.as @@ -25,6 +25,7 @@ package org.bigbluebutton.main.events public static const USER_LOGGED_OUT:String = "USER_LOGGED_OUT"; public static const DISCONNECT_TEST:String = "disconnect_test"; public static const USER_KICKED_OUT:String = "USER_KICKED_OUT"; + public static const MODERATOR_DENIED_ME:String = "MODERATOR_DENIED_ME"; public static const CONFIRM_LOGOUT:String = "CONFIRM_LOGOUT"; public static const REFOCUS_CONFIRM:String = "REFOCUS_CONFIRM"; diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/NetworkStatsEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/NetworkStatsEvent.as index 665a6b5a37f682052c9c66f9afad6b5170e045d7..fcfe9adeda6056dfb1b650237ac74ae52ba5be4a 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/events/NetworkStatsEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/NetworkStatsEvent.as @@ -20,18 +20,19 @@ package org.bigbluebutton.main.events { import flash.events.Event; import flash.utils.Dictionary; - + public class NetworkStatsEvent extends Event { public static const NETWORK_STATS_EVENTS:String = "NETWORK_STATS_EVENTS"; + public static const OPEN_NETSTATS_WIN:String = "OPEN_NETWORK_WIN"; public var downloadStats:Dictionary; public var uploadStats:Dictionary; - public function NetworkStatsEvent(bubbles:Boolean=true, cancelable:Boolean=false) + public function NetworkStatsEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) { - super(NETWORK_STATS_EVENTS, bubbles, cancelable); + super(type, bubbles, cancelable); } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/RefreshGuestEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/RefreshGuestEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..fc552ad934d1bfecd4a63fe153ce2329b11a5ac1 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/RefreshGuestEvent.as @@ -0,0 +1,34 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +*/ +package org.bigbluebutton.main.events +{ + import flash.events.Event; + + public class RefreshGuestEvent extends Event + { + public static const REFRESH_GUEST_VIEW:String = "RefreshGuestView"; + + public var listOfGuests:Object; + + public function RefreshGuestEvent(type:String = REFRESH_GUEST_VIEW) + { + super(type, true, false); + } + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/RemoveGuestEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/RemoveGuestEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..82a0e03268aad36d19442b264fb3c014eb32c848 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/RemoveGuestEvent.as @@ -0,0 +1,35 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +*/ +package org.bigbluebutton.main.events +{ + import flash.events.Event; + + public class RemoveGuestEvent extends Event + { + public static const REMOVE_GUEST:String = "RemoveGuest"; + public static const REMOVE_ALL:String = "RemoveAllGuests"; + + public var userid:String; + + public function RemoveGuestEvent(type:String = REMOVE_GUEST) + { + super(type, true, false); + } + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/RemoveGuestFromViewEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/RemoveGuestFromViewEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..30d60968a0d7898d51ac19521d7c5026ecc6f206 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/RemoveGuestFromViewEvent.as @@ -0,0 +1,34 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +*/ +package org.bigbluebutton.main.events +{ + import flash.events.Event; + + public class RemoveGuestFromViewEvent extends Event + { + public static const REMOVE_GUEST:String = "RemoveGuest"; + + public var userid:String; + + public function RemoveGuestFromViewEvent(type:String = REMOVE_GUEST) + { + super(type, true, false); + } + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/ResponseModeratorEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/ResponseModeratorEvent.as new file mode 100755 index 0000000000000000000000000000000000000000..a4cf04ef9294bda227c34a6ccc0063a18a0b0668 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/ResponseModeratorEvent.as @@ -0,0 +1,41 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +*/ +package org.bigbluebutton.main.events +{ + import flash.events.Event; + + import org.bigbluebutton.main.model.ConferenceParameters; + import org.bigbluebutton.main.model.users.BBBUser; + import org.bigbluebutton.main.model.ConferenceParameters; + + public class ResponseModeratorEvent extends Event + { + public static const RESPONSE:String = "Response"; + public static const RESPONSE_ALL:String = "RESPONSE_ALL"; + + + public var userid:String; + public var resp:Boolean; + + public function ResponseModeratorEvent(type:String) + { + super(type, true, false); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/events/StoppedViewingWebcamEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/events/StoppedViewingWebcamEvent.as index 9721f3d9f83a46c94eb8f87fb55ebb4b707ee504..582a45d17c7e3023776bf548c55abf2de80e58b2 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/events/StoppedViewingWebcamEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/events/StoppedViewingWebcamEvent.as @@ -27,9 +27,12 @@ package org.bigbluebutton.main.events // The userID of the webcam being viewed. public var webcamUserID:String; + // The streamName of the user + public var streamName:String; + public function StoppedViewingWebcamEvent(bubbles:Boolean=true, cancelable:Boolean=false) { super(STOPPED_VIEWING_WEBCAM, bubbles, cancelable); } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/maps/ApplicationEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/maps/ApplicationEventMap.mxml index 917a07f9131497cb58d776300bffeaf2815ca688..e28be22b287a127dc73dc79123e80eb80c46c703 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/main/maps/ApplicationEventMap.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/maps/ApplicationEventMap.mxml @@ -30,10 +30,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. --> <ObjectBuilder generator="{ModulesProxy}" cache="global" /> <ObjectBuilder generator="{ConfigManager}" cache="global" /> - <!-- - Disabling temporarily the stream monitor - --> - <!--ObjectBuilder generator="{StreamMonitor}" cache="global" /--> + <ObjectBuilder generator="{StreamMonitor}" cache="global" /> </EventHandlers> <EventHandlers type="{PortTestEvent.TEST_RTMP}" > @@ -69,6 +66,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <MethodInvoker generator="{ModulesProxy}" method="startAllModules" /> </EventHandlers> + <EventHandlers type="{LogoutEvent.MODERATOR_DENIED_ME}" > + <MethodInvoker generator="{ModulesProxy}" method="handleLogout" /> + </EventHandlers> <mx:Script> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/ConferenceParameters.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/ConferenceParameters.as index 6c120f8058e709ae07879e37c4dc808b70a6eebc..70d3fdb1669800f82d44abf4c74e2033375d850f 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/ConferenceParameters.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/ConferenceParameters.as @@ -60,6 +60,11 @@ package org.bigbluebutton.main.model * Voice conference bridge that external SIP clients use. Usually the same as webvoiceconf */ public var voicebridge:String; + + /** + * Flag used to enter as a guest + */ + public var guest:Boolean; /** * The welcome string, as passed in through the API /create call. diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/ConfigParameters.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/ConfigParameters.as index 5bb5100e4fdda85603cf53dede8a30bc73c4d0fb..d02e312b99bd48100d1668f077bcd7eb2c46805d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/ConfigParameters.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/ConfigParameters.as @@ -55,6 +55,9 @@ package org.bigbluebutton.main.model public var shortcutKeysShowButton:Boolean; public var skinning:String = ""; public var showDebug:Boolean = false; + public var copyright:String = ""; + public var logo:String = ""; + public var background:String = ""; private var loadedListener:Function; private var dispatcher:Dispatcher = new Dispatcher(); @@ -111,6 +114,10 @@ package org.bigbluebutton.main.model if (xml.skinning.@enabled == "true") skinning = xml.skinning.@url; if (xml.debug.@showDebugWindow == "true") showDebug = true; + + copyright = xml.branding.@copyright; + logo = xml.branding.@logo; + background = xml.branding.@background; } public function getModulesXML():XMLList{ diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/Guest.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/Guest.as new file mode 100644 index 0000000000000000000000000000000000000000..a0484cda092e90468e252709ddef4161ce486491 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/Guest.as @@ -0,0 +1,57 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2013 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: +*/ +package org.bigbluebutton.main.model +{ + + public class Guest + { + private var listOfGuests:Object = new Object(); + private var numberOfGuests:Number = 0; + + public function hasGuest():Boolean { + return numberOfGuests > 0; + } + + public function getNumberOfGuests():Number { + return numberOfGuests; + } + + public function addGuest(userid:String, username:String):void { + listOfGuests[userid] = username; + numberOfGuests++; + } + + public function getGuests():Object { + return this.listOfGuests; + } + + public function removeAllGuests():void { + listOfGuests = new Object(); + numberOfGuests = 0; + } + + public function remove(userid:String):void { + if (listOfGuests[userid] != null) { + numberOfGuests--; + delete listOfGuests[userid]; + } + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/GuestManager.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/GuestManager.as new file mode 100644 index 0000000000000000000000000000000000000000..f3b366e766cfb97da3e513b3e35139ffb04a6f3c --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/GuestManager.as @@ -0,0 +1,64 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2013 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: +*/ +package org.bigbluebutton.main.model +{ + + import com.asfusion.mate.events.Dispatcher; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.events.RefreshGuestEvent; + import org.bigbluebutton.main.events.RemoveGuestFromViewEvent; + + public class GuestManager + { + private var guest:Guest; + private var dispatcher:Dispatcher; + + function GuestManager() { + this.dispatcher = new Dispatcher(); + this.guest = new Guest(); + } + + public function addGuest(evt:BBBEvent):void { + guest.addGuest(evt.payload.userId, evt.payload.name); + refreshGuestView(); + } + + private function refreshGuestView():void { + var refreshGuestEvent:RefreshGuestEvent = new RefreshGuestEvent(); + refreshGuestEvent.listOfGuests = guest.getGuests(); + dispatcher.dispatchEvent(refreshGuestEvent); + } + + public function removeAllGuests():void { + guest.removeAllGuests(); + } + + private function removeGuestFromView(userid:String):void { + var removeGuestFromViewEvent:RemoveGuestFromViewEvent = new RemoveGuestFromViewEvent(); + removeGuestFromViewEvent.userid = userid; + dispatcher.dispatchEvent(removeGuestFromViewEvent); + } + + public function removeGuest(userid:String):void { + guest.remove(userid); + removeGuestFromView(userid); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/LayoutOptions.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/LayoutOptions.as old mode 100755 new mode 100644 index 4c3357e7b2b1feb881bcbb7cb2825fb8ac4113b4..7b433ff37e417ca22a69c1127ead0c0cc9f63a53 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/LayoutOptions.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/LayoutOptions.as @@ -30,6 +30,7 @@ package org.bigbluebutton.main.model [Bindable] public var showHelpButton:Boolean = true; [Bindable] public var showLogoutWindow:Boolean = true; [Bindable] public var showLayoutTools:Boolean = true; + [Bindable] public var showNetworkMonitor:Boolean = true; [Bindable] public var confirmLogout:Boolean = true; [Bindable] public var showRecordingNotification:Boolean = true; @@ -79,6 +80,10 @@ package org.bigbluebutton.main.model showLayoutTools = (vxml.@showLayoutTools.toString().toUpperCase() == "TRUE") ? true : false; } + if(vxml.@showNetworkMonitor != undefined){ + showNetworkMonitor = (vxml.@showNetworkMonitor.toString().toUpperCase() == "TRUE") ? true : false; + } + if(vxml.@showRecordingNotification != undefined){ showRecordingNotification = (vxml.@showRecordingNotification.toString().toUpperCase() == "TRUE") ? true : false; } diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/NetworkStatsData.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/NetworkStatsData.as index c72d5e72f465e46172074da2f8e8f5fb0a3cb22f..eaae1c71af84f9529c99442fc24526c157fb752c 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/NetworkStatsData.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/NetworkStatsData.as @@ -21,6 +21,8 @@ package org.bigbluebutton.main.model import com.asfusion.mate.events.Dispatcher; + import mx.formatters.NumberFormatter; + import org.bigbluebutton.common.LogUtil; import org.bigbluebutton.main.events.NetworkStatsEvent; @@ -31,12 +33,15 @@ package org.bigbluebutton.main.model private static var _instance:NetworkStatsData = null; private var _currentConsumedDownBW:Number = 0; // Kb private var _currentConsumedUpBW:Number = 0; // Kb - private var _totalConsumedDownBW:Number = 0; // MB - private var _totalConsumedUpBW:Number = 0; // MB - private var _measuredDownBW:int = 0; // Mb + private var _totalConsumedDownBW:Number = 0; // KB + private var _totalConsumedUpBW:Number = 0; // KB + private var _measuredDownBWCheck:Boolean = false; + private var _measuredDownBW:Number = 0; // Kb private var _measuredDownLatency:int = 0; // ms - private var _measuredUpBW:int = 0; // Mb + private var _measuredUpBWCheck:Boolean = false; + private var _measuredUpBW:Number = 0; // Kb private var _measuredUpLatency:int = 0; // ms + private var _numberFormatter:NumberFormatter = new NumberFormatter(); /** * This class is a singleton. Please initialize it using the getInstance() method. @@ -49,6 +54,8 @@ package org.bigbluebutton.main.model } private function initialize():void { + _numberFormatter.precision = 1; + _numberFormatter.useThousandsSeparator = true; } /** @@ -63,10 +70,10 @@ package org.bigbluebutton.main.model // all the numbers are in bytes public function updateConsumedBW(down:Number, up:Number, downTotal:Number, upTotal:Number):void { - _currentConsumedDownBW = (down * 8)/1024; - _currentConsumedUpBW = (up * 8)/1024; - _totalConsumedDownBW = downTotal / 1048576; - _totalConsumedUpBW = upTotal / 1048576; + _currentConsumedDownBW = (down * 8) / 1024; + _currentConsumedUpBW = (up * 8) / 1024; + _totalConsumedDownBW = downTotal / 1024; + _totalConsumedUpBW = upTotal / 1024; } /* @@ -77,7 +84,8 @@ package org.bigbluebutton.main.model [latency] 10 */ public function setDownloadMeasuredBW(info:Object):void { - _measuredDownBW = info["kbitDown"] / 1000; + _measuredDownBWCheck = true; + _measuredDownBW = info["kbitDown"]; _measuredDownLatency = info["latency"]; } @@ -90,40 +98,84 @@ package org.bigbluebutton.main.model latency = 11 */ public function setUploadMeasuredBW(info:Object):void { - _measuredUpBW = info.kbitUp / 1000; + _measuredUpBWCheck = true; + _measuredUpBW = info.kbitUp; _measuredUpLatency = info.latency; } - public function get currentConsumedDownBW():Number { - return _currentConsumedDownBW; + private function format_KB(n:Number):String { + var unit:String = "KB"; + if (n >= 1073741824) { + unit = "TB"; + n /= 1073741824; + } else if (n >= 1048576) { + unit = "GB"; + n /= 1048576; + } else if (n >= 1024) { + unit = "MB"; + n /= 1024; + } + return _numberFormatter.format(n) + " " + unit; + } + + private function format_Kbps(n:Number):String { + var unit:String = "Kbps"; + if (n >= 1000000000) { + unit = "Tbps"; + n /= 1000000000; + } else if (n >= 1000000) { + unit = "Gbps"; + n /= 1000000; + } else if (n >= 1000) { + unit = "Mbps"; + n /= 1000; + } + return _numberFormatter.format(n) + " " + unit; + } + + + public function get formattedCurrentConsumedDownBW():String { + return format_Kbps(_currentConsumedDownBW); } - public function get currentConsumedUpBW():Number { - return _currentConsumedUpBW; + public function get formattedCurrentConsumedUpBW():String { + return format_Kbps(_currentConsumedUpBW); } - public function get totalConsumedDownBW():Number { - return _totalConsumedDownBW; + public function get formattedTotalConsumedDownBW():String { + return format_KB(_totalConsumedDownBW); } - public function get totalConsumedUpBW():Number { - return _totalConsumedUpBW; + public function get formattedTotalConsumedUpBW():String { + return format_KB(_totalConsumedUpBW); } - public function get measuredDownBW():int { - return _measuredDownBW; + public function get formattedMeasuredDownBW():String { + if (_measuredDownBWCheck) + return format_Kbps(_measuredDownBW); + else + return "-"; } - public function get measuredDownLatency():int { - return _measuredDownLatency; + public function get formattedMeasuredDownLatency():String { + if (_measuredDownBWCheck) + return _measuredDownLatency + " ms"; + else + return "-"; } - public function get measuredUpBW():int { - return _measuredUpBW; + public function get formattedMeasuredUpBW():String { + if (_measuredUpBWCheck) + return format_Kbps(_measuredUpBW); + else + return "-"; } - public function get measuredUpLatency():int { - return _measuredUpLatency; + public function get formattedMeasuredUpLatency():String { + if (_measuredUpBWCheck) + return _measuredUpLatency + " ms"; + else + return "-"; } } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/User.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/User.as index b6f5679f620b2bdc5f4f3d2c5fb4948ad7dd231e..b7f6d7f67cd347b322effe77915c348b48b294a6 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/User.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/User.as @@ -28,5 +28,6 @@ package org.bigbluebutton.main.model public var role:String; public var isPresenter:Boolean; public var authToken:String; + public var guest:Boolean; } } \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/modules/ModulesDispatcher.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/modules/ModulesDispatcher.as index 93033d81ba6093e7c0f69237d26a5d0d950ded63..a682d081050d66db7b2cae10951bd2c63acf7900 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/modules/ModulesDispatcher.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/modules/ModulesDispatcher.as @@ -1,195 +1,198 @@ -/** - * 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.main.model.modules -{ - import com.asfusion.mate.events.Dispatcher; - import flash.events.TimerEvent; - import flash.utils.Timer; - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.core.BBB; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.vo.Config; - import org.bigbluebutton.core.vo.ConfigBuilder; - import org.bigbluebutton.main.api.JSLog; - import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.main.events.ConfigEvent; - import org.bigbluebutton.main.events.ModuleLoadEvent; - import org.bigbluebutton.main.events.PortTestEvent; - import org.bigbluebutton.main.events.UserServicesEvent; - import org.bigbluebutton.main.model.ConfigParameters; - import org.bigbluebutton.main.model.modules.EnterApiService; - - public class ModulesDispatcher - { - private static const LOG:String = "Main::ModulesDispatcher - "; - private var dispatcher:Dispatcher; - private var enterApiService: EnterApiService; - private var meetingInfo:Object = new Object(); - private var enterApiUrl:String; - - public function ModulesDispatcher() - { - dispatcher = new Dispatcher(); - - } - - public function sendLoadProgressEvent(moduleName:String, loadProgress:Number):void{ - var loadEvent:ModuleLoadEvent = new ModuleLoadEvent(ModuleLoadEvent.MODULE_LOAD_PROGRESS); - loadEvent.moduleName = moduleName; - loadEvent.progress = loadProgress; - dispatcher.dispatchEvent(loadEvent); - } - - public function sendModuleLoadReadyEvent(moduleName:String):void{ - var loadReadyEvent:ModuleLoadEvent = new ModuleLoadEvent(ModuleLoadEvent.MODULE_LOAD_READY); - loadReadyEvent.moduleName = moduleName; - dispatcher.dispatchEvent(loadReadyEvent); - } - - public function sendAllModulesLoadedEvent():void{ - dispatcher.dispatchEvent(new ModuleLoadEvent(ModuleLoadEvent.ALL_MODULES_LOADED)); - - var loginEvent:BBBEvent = new BBBEvent(BBBEvent.LOGIN_EVENT); - dispatcher.dispatchEvent(loginEvent); - } - - public function sendStartUserServicesEvent(application:String, host:String, isTunnelling:Boolean):void{ - var e:UserServicesEvent = new UserServicesEvent(UserServicesEvent.START_USER_SERVICES); - e.applicationURI = application; - e.hostURI = host; - e.isTunnelling = isTunnelling; - dispatcher.dispatchEvent(e); - } - - public function sendPortTestEvent():void { - getMeetingAndUserInfo(); - } - - private function getMeetingAndUserInfo():void { - enterApiService = new EnterApiService(); - enterApiService.addResultListener(resultListener); - enterApiService.load(enterApiUrl); - } - - private function resultListener(success:Boolean, result:Object):void { - if (success) { - trace(LOG + "Saving meeting and user info " + JSON.stringify(result)); - - meetingInfo.username = result.username; - meetingInfo.userId = result.userId; - meetingInfo.meetingName = result.meetingName; - meetingInfo.meetingId = result.meetingId; - - doPortTesting(); - } else { - var logData:Object = new Object(); - JSLog.critical("Failed to get meeting and user info from Enter API", logData); - - dispatcher.dispatchEvent(new PortTestEvent(PortTestEvent.TUNNELING_FAILED)); - } - } - - private function doPortTesting():void { - trace(LOG + "Sending TEST_RTMP Event"); - var e:PortTestEvent = new PortTestEvent(PortTestEvent.TEST_RTMP); - dispatcher.dispatchEvent(e); - } - - private function timerHandler(e:TimerEvent):void{ - trace(LOG + "Sending PORT_TEST_UPDATE Event"); - var evt:PortTestEvent = new PortTestEvent(PortTestEvent.PORT_TEST_UPDATE); - dispatcher.dispatchEvent(evt); - } - - public function sendTunnelingFailedEvent(server: String, app: String):void{ - trace(LOG + "Sending TunnelingFailed Event"); - var logData:Object = new Object(); - logData.server = server; - logData.app = app; - logData.userId = meetingInfo.userId; - logData.username = meetingInfo.username; - logData.meetingName = meetingInfo.meetingName; - logData.meetingId = meetingInfo.meetingId; - trace(LOG + "Cannot connect to Red5 using RTMP and RTMPT", JSON.stringify(logData)); - JSLog.critical("Cannot connect to Red5 using RTMP and RTMPT", logData); - - dispatcher.dispatchEvent(new PortTestEvent(PortTestEvent.TUNNELING_FAILED)); - } - - public function sendPortTestSuccessEvent(port:String, host:String, protocol:String, app:String):void{ - trace(LOG + "Sending PORT_TEST_SUCCESS Event"); - var logData:Object = new Object(); - logData.port = port; - logData.server = host; - logData.protocol = protocol; - logData.app = app; - logData.userId = meetingInfo.userId; - logData.username = meetingInfo.username; - logData.meetingName = meetingInfo.meetingName; - logData.meetingId = meetingInfo.meetingId; - JSLog.debug("Successfully connected on test connection.", logData); - - var portEvent:PortTestEvent = new PortTestEvent(PortTestEvent.PORT_TEST_SUCCESS); - portEvent.port = port; - portEvent.hostname = host; - portEvent.protocol = protocol; - portEvent.app = app; - dispatcher.dispatchEvent(portEvent); - - } - - public function sendPortTestFailedEvent(port:String, host:String, protocol:String, app:String):void{ - trace(LOG + "Sending PORT_TEST_FAILED Event"); - var portFailEvent:PortTestEvent = new PortTestEvent(PortTestEvent.PORT_TEST_FAILED); - portFailEvent.port = port; - portFailEvent.hostname = host; - portFailEvent.protocol = protocol; - portFailEvent.app = app; - dispatcher.dispatchEvent(portFailEvent); - - } - - public function sendModuleLoadingStartedEvent(modules:XMLList):void{ - var event:ModuleLoadEvent = new ModuleLoadEvent(ModuleLoadEvent.MODULE_LOADING_STARTED); - event.modules = modules; - dispatcher.dispatchEvent(event); - } - - public function sendConfigParameters(c:ConfigParameters):void{ - enterApiUrl = c.host; - - var event:ConfigEvent = new ConfigEvent(ConfigEvent.CONFIG_EVENT); - var config:Config; - config = new ConfigBuilder(c.version, c.localeVersion) - .withApplication(c.application) - .withHelpUrl(c.helpURL) - .withHost(c.host) - .withLanguageEnabled(c.languageEnabled) - .withShortcutKeysShowButton(c.shortcutKeysShowButton) - .withNumModule(c.numModules) - .withPortTestApplication(c.portTestApplication) - .withPortTestHost(c.portTestHost) - .withShowDebug(c.showDebug) - .withSkinning(c.skinning) - .build() - event.config = config; - dispatcher.dispatchEvent(event); - } - } +/** + * 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.main.model.modules +{ + import com.asfusion.mate.events.Dispatcher; + import flash.events.TimerEvent; + import flash.utils.Timer; + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.vo.Config; + import org.bigbluebutton.core.vo.ConfigBuilder; + import org.bigbluebutton.main.api.JSLog; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.events.ConfigEvent; + import org.bigbluebutton.main.events.ModuleLoadEvent; + import org.bigbluebutton.main.events.PortTestEvent; + import org.bigbluebutton.main.events.UserServicesEvent; + import org.bigbluebutton.main.model.ConfigParameters; + import org.bigbluebutton.main.model.modules.EnterApiService; + + public class ModulesDispatcher + { + private static const LOG:String = "Main::ModulesDispatcher - "; + private var dispatcher:Dispatcher; + private var enterApiService: EnterApiService; + private var meetingInfo:Object = new Object(); + private var enterApiUrl:String; + + public function ModulesDispatcher() + { + dispatcher = new Dispatcher(); + + } + + public function sendLoadProgressEvent(moduleName:String, loadProgress:Number):void{ + var loadEvent:ModuleLoadEvent = new ModuleLoadEvent(ModuleLoadEvent.MODULE_LOAD_PROGRESS); + loadEvent.moduleName = moduleName; + loadEvent.progress = loadProgress; + dispatcher.dispatchEvent(loadEvent); + } + + public function sendModuleLoadReadyEvent(moduleName:String):void{ + var loadReadyEvent:ModuleLoadEvent = new ModuleLoadEvent(ModuleLoadEvent.MODULE_LOAD_READY); + loadReadyEvent.moduleName = moduleName; + dispatcher.dispatchEvent(loadReadyEvent); + } + + public function sendAllModulesLoadedEvent():void{ + dispatcher.dispatchEvent(new ModuleLoadEvent(ModuleLoadEvent.ALL_MODULES_LOADED)); + + var loginEvent:BBBEvent = new BBBEvent(BBBEvent.LOGIN_EVENT); + dispatcher.dispatchEvent(loginEvent); + } + + public function sendStartUserServicesEvent(application:String, host:String, isTunnelling:Boolean):void{ + var e:UserServicesEvent = new UserServicesEvent(UserServicesEvent.START_USER_SERVICES); + e.applicationURI = application; + e.hostURI = host; + e.isTunnelling = isTunnelling; + dispatcher.dispatchEvent(e); + } + + public function sendPortTestEvent():void { + getMeetingAndUserInfo(); + } + + private function getMeetingAndUserInfo():void { + enterApiService = new EnterApiService(); + enterApiService.addResultListener(resultListener); + enterApiService.load(enterApiUrl); + } + + private function resultListener(success:Boolean, result:Object):void { + if (success) { + trace(LOG + "Saving meeting and user info " + JSON.stringify(result)); + + meetingInfo.username = result.username; + meetingInfo.userId = result.userId; + meetingInfo.meetingName = result.meetingName; + meetingInfo.meetingId = result.meetingId; + + doPortTesting(); + } else { + var logData:Object = new Object(); + JSLog.critical("Failed to get meeting and user info from Enter API", logData); + + dispatcher.dispatchEvent(new PortTestEvent(PortTestEvent.TUNNELING_FAILED)); + } + } + + private function doPortTesting():void { + trace(LOG + "Sending TEST_RTMP Event"); + var e:PortTestEvent = new PortTestEvent(PortTestEvent.TEST_RTMP); + dispatcher.dispatchEvent(e); + } + + private function timerHandler(e:TimerEvent):void{ + trace(LOG + "Sending PORT_TEST_UPDATE Event"); + var evt:PortTestEvent = new PortTestEvent(PortTestEvent.PORT_TEST_UPDATE); + dispatcher.dispatchEvent(evt); + } + + public function sendTunnelingFailedEvent(server: String, app: String):void{ + trace(LOG + "Sending TunnelingFailed Event"); + var logData:Object = new Object(); + logData.server = server; + logData.app = app; + logData.userId = meetingInfo.userId; + logData.username = meetingInfo.username; + logData.meetingName = meetingInfo.meetingName; + logData.meetingId = meetingInfo.meetingId; + trace(LOG + "Cannot connect to Red5 using RTMP and RTMPT", JSON.stringify(logData)); + JSLog.critical("Cannot connect to Red5 using RTMP and RTMPT", logData); + + dispatcher.dispatchEvent(new PortTestEvent(PortTestEvent.TUNNELING_FAILED)); + } + + public function sendPortTestSuccessEvent(port:String, host:String, protocol:String, app:String):void{ + trace(LOG + "Sending PORT_TEST_SUCCESS Event"); + var logData:Object = new Object(); + logData.port = port; + logData.server = host; + logData.protocol = protocol; + logData.app = app; + logData.userId = meetingInfo.userId; + logData.username = meetingInfo.username; + logData.meetingName = meetingInfo.meetingName; + logData.meetingId = meetingInfo.meetingId; + JSLog.debug("Successfully connected on test connection.", logData); + + var portEvent:PortTestEvent = new PortTestEvent(PortTestEvent.PORT_TEST_SUCCESS); + portEvent.port = port; + portEvent.hostname = host; + portEvent.protocol = protocol; + portEvent.app = app; + dispatcher.dispatchEvent(portEvent); + + } + + public function sendPortTestFailedEvent(port:String, host:String, protocol:String, app:String):void{ + trace(LOG + "Sending PORT_TEST_FAILED Event"); + var portFailEvent:PortTestEvent = new PortTestEvent(PortTestEvent.PORT_TEST_FAILED); + portFailEvent.port = port; + portFailEvent.hostname = host; + portFailEvent.protocol = protocol; + portFailEvent.app = app; + dispatcher.dispatchEvent(portFailEvent); + + } + + public function sendModuleLoadingStartedEvent(modules:XMLList):void{ + var event:ModuleLoadEvent = new ModuleLoadEvent(ModuleLoadEvent.MODULE_LOADING_STARTED); + event.modules = modules; + dispatcher.dispatchEvent(event); + } + + public function sendConfigParameters(c:ConfigParameters):void{ + enterApiUrl = c.host; + + var event:ConfigEvent = new ConfigEvent(ConfigEvent.CONFIG_EVENT); + var config:Config; + config = new ConfigBuilder(c.version, c.localeVersion) + .withApplication(c.application) + .withHelpUrl(c.helpURL) + .withHost(c.host) + .withLanguageEnabled(c.languageEnabled) + .withShortcutKeysShowButton(c.shortcutKeysShowButton) + .withNumModule(c.numModules) + .withPortTestApplication(c.portTestApplication) + .withPortTestHost(c.portTestHost) + .withShowDebug(c.showDebug) + .withSkinning(c.skinning) + .withCopyright(c.copyright) + .withLogo(c.logo) + .withBackground(c.background) + .build() + event.config = config; + dispatcher.dispatchEvent(event); + } + } } \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/AutoReconnect.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/AutoReconnect.as new file mode 100644 index 0000000000000000000000000000000000000000..d5286e4e88b41ea139d1ebc917a9e2d7cd5b07da --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/AutoReconnect.as @@ -0,0 +1,68 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2015 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.main.model.users +{ + import flash.events.TimerEvent; + import flash.utils.Timer; + + public class AutoReconnect + { + public static const LOG:String = "AutoReconnect - "; + + private var _backoff:Number; + private var _initialBackoff:Number; + private var _reconnectCallback:Function; + private var _reconnectParameters:Array; + private var _retries: Number; + + public function AutoReconnect(initialBackoff:Number = 1000) { + _initialBackoff = initialBackoff; + _backoff = initialBackoff; + } + + public function onDisconnect(callback:Function, ...parameters):void { + trace(LOG + "onDisconnect, parameters=" + parameters.toString()); + _reconnectCallback = callback; + _reconnectParameters = parameters; + attemptReconnect(_initialBackoff); + _retries = 1; + } + + public function onConnectionAttemptFailed():void { + trace(LOG + "onConnectionAttemptFailed"); + attemptReconnect(_backoff); + _retries = _retries + 1; + } + + private function attemptReconnect(backoff:Number):void { + trace(LOG + "attemptReconnect backoff=" + backoff); + var retryTimer:Timer = new Timer(backoff, 1); + retryTimer.addEventListener(TimerEvent.TIMER, function():void { + trace(LOG + "Reconnecting"); + _reconnectCallback.apply(null, _reconnectParameters); + }); + retryTimer.start(); + if (_backoff < 16000) _backoff = backoff *2; + } + + public function get Retries():Number { + return _retries; + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/BBBUser.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/BBBUser.as index 7ad867ae8d4420699e19336bbee0af707029e003..840cd1ffcc90cd48a0089d5848567a047de07f81 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/BBBUser.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/BBBUser.as @@ -25,10 +25,12 @@ package org.bigbluebutton.main.model.users import org.bigbluebutton.core.events.VoiceConfEvent; import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.core.vo.LockSettingsVO; + import org.bigbluebutton.main.model.users.events.ChangeStatusEvent; import org.bigbluebutton.main.model.users.events.StreamStartedEvent; + import org.bigbluebutton.main.model.users.events.StreamStoppedEvent; import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; - import org.bigbluebutton.util.i18n.ResourceUtil; - + import org.bigbluebutton.util.i18n.ResourceUtil; + public class BBBUser { public static const MODERATOR:String = "MODERATOR"; @@ -52,20 +54,79 @@ package org.bigbluebutton.main.model.users [Bindable] public var disableMyPublicChat:Boolean = false; [Bindable] public var lockedLayout:Boolean = false; - private var _hasStream:Boolean = false; + [Bindable] public var guest:Boolean = false; + [Bindable] public var waitingForAcceptance:Boolean = false; + [Bindable] public function get hasStream():Boolean { - return _hasStream; + return streamNames.length > 0; } public function set hasStream(s:Boolean):void { - _hasStream = s; - verifyMedia(); + throw new Error("hasStream cannot be set. It is derived directly from streamName"); } - - [Bindable] public var viewingStream:Boolean = false; - - [Bindable] public var streamName:String = ""; - + + [Bindable] private var _viewingStream:Array = new Array(); + + [Bindable] + public function get viewingStream():Array { + return _viewingStream; + } + public function set viewingStream(v:Array):void { + throw new Error("Please use the helpers addViewingStream or removeViewingStream to handle viewingStream"); + } + public function addViewingStream(streamName:String):Boolean { + trace("Before adding the stream " + streamName + ": " + _viewingStream); + if (isViewingStream(streamName)) { + return false; + } + + _viewingStream.push(streamName); + trace("After adding the stream " + streamName + ": " + _viewingStream); + return true; + } + public function removeViewingStream(streamName:String):Boolean { + trace("Before removing the stream " + streamName + ": " + _viewingStream); + if (!isViewingStream(streamName)) { + return false; + } + + _viewingStream = _viewingStream.filter(function(item:*, index:int, array:Array):Boolean { return item != streamName; }); + trace("After removing the stream " + streamName + ": " + _viewingStream); + return true; + } + private function isViewingStream(streamName:String):Boolean { + return _viewingStream.some(function(item:*, index:int, array:Array):Boolean { return item == streamName; }); + } + public function isViewingAllStreams():Boolean { + return _viewingStream.length == streamNames.length; + } + + [Bindable] public var streamNames:Array = new Array(); + + [Bindable] + public function get streamName():String { + var streams:String = ""; + for each(var stream:String in streamNames) { + streams = streams + stream + "|"; + } + //Remove last | + streams = streams.slice(0, streams.length-1); + return streams; + } + + private function hasThisStream(streamName:String):Boolean { + return streamNames.some(function(item:*, index:int, array:Array):Boolean { return item == streamName; }); + } + + public function set streamName(streamNames:String):void { + if(streamNames) { + var streamNamesList:Array = streamNames.split("|"); + for each(var streamName:String in streamNamesList) { + sharedWebcam(streamName); + } + } + } + private var _presenter:Boolean = false; [Bindable] public function get presenter():Boolean { @@ -76,16 +137,33 @@ package org.bigbluebutton.main.model.users verifyUserStatus(); } - public var raiseHandTime:Date; - private var _raiseHand:Boolean = false; + private var _mood:String = ChangeStatusEvent.CLEAR_STATUS; + public function get hasMood():Boolean { + return _mood != ChangeStatusEvent.CLEAR_STATUS; + } + [Bindable] + public function get mood():String { + return _mood; + } + public function set mood(m:String):void { + _mood = m; + verifyUserStatus(); + } [Bindable] public function get raiseHand():Boolean { - return _raiseHand; + return _mood == ChangeStatusEvent.RAISE_HAND; } public function set raiseHand(r:Boolean):void { - _raiseHand = r; - raiseHandTime = (r ? new Date() : null); - verifyUserStatus(); + mood = (r? ChangeStatusEvent.RAISE_HAND: ChangeStatusEvent.CLEAR_STATUS); + } + + private var _moodTimestamp:Number = 0; + [Bindable] + public function get moodTimestamp():Number { + return _moodTimestamp; + } + public function set moodTimestamp(t:Number):void { + _moodTimestamp = t; } private var _role:String = Role.VIEWER; @@ -95,9 +173,15 @@ package org.bigbluebutton.main.model.users } public function set role(r:String):void { _role = r; + moderator = _role == MODERATOR; verifyUserStatus(); + if (me) { + applyLockSettings(); + } } - + + [Bindable] public var moderator:Boolean = false; + [Bindable] public var room:String = ""; [Bindable] public var authToken:String = ""; [Bindable] public var selected:Boolean = false; @@ -143,10 +227,51 @@ package org.bigbluebutton.main.model.users _userStatus = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.presenter'); else if (role == Role.MODERATOR) _userStatus = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.moderator'); - else if (raiseHand) - _userStatus = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.handRaised'); - else + else if (hasMood) { + _userStatus = moodStatus; + } else { _userStatus = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.viewer'); + } + } + private function get moodStatus():String { + var s:String = ""; + switch(mood) { + case ChangeStatusEvent.RAISE_HAND: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.handRaised'); + break; + case ChangeStatusEvent.AGREE: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.agree'); + break; + case ChangeStatusEvent.DISAGREE: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.disagree'); + break; + case ChangeStatusEvent.SPEAK_LOUDER: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.speakLouder'); + break; + case ChangeStatusEvent.SPEAK_LOWER: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.speakSofter'); + break; + case ChangeStatusEvent.SPEAK_FASTER: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.speakFaster'); + break; + case ChangeStatusEvent.SPEAK_SLOWER: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.speakSlower'); + break; + case ChangeStatusEvent.BE_RIGHT_BACK: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.beRightBack'); + break; + case ChangeStatusEvent.LAUGHTER: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.laughter'); + break; + case ChangeStatusEvent.SAD: + s = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.sad'); + break; + } + return s; + } + + public function amIGuest():Boolean { + return guest; } /* @@ -178,8 +303,8 @@ package org.bigbluebutton.main.model.users showingWebcam = ResourceUtil.getInstance().getString('bbb.viewers.viewersGrid.statusItemRenderer.streamIcon.toolTip'); if (presenter) isPresenter = ResourceUtil.getInstance().getString('bbb.viewers.viewersGrid.statusItemRenderer.presIcon.toolTip'); - if (raiseHand) - handRaised = ResourceUtil.getInstance().getString('bbb.viewers.viewersGrid.statusItemRenderer.raiseHand.toolTip'); + if (hasMood) + handRaised = moodStatus; status = showingWebcam + isPresenter + handRaised; } @@ -188,25 +313,20 @@ package org.bigbluebutton.main.model.users _status.addStatus(status); } - public function userRaiseHand(raised: Boolean):void { - raiseHand = raised; - if (me) { - UserManager.getInstance().getConference().isMyHandRaised = raised; - } - buildStatus(); - } - public function sharedWebcam(stream: String):void { - hasStream = true; - streamName = stream; - if (hasStream) sendStreamStartedEvent(); + if(stream && stream != "" && !hasThisStream(stream)) { + streamNames.push(stream); + sendStreamStartedEvent(stream); + } buildStatus(); + verifyMedia(); } - public function unsharedWebcam():void { - hasStream = false; - streamName = ""; + public function unsharedWebcam(stream: String):void { + streamNames = streamNames.filter(function(item:*, index:int, array:Array):Boolean { return item != stream }); + sendStreamStoppedEvent(stream); buildStatus(); + verifyMedia(); } public function presenterStatusChanged(presenter: Boolean):void { @@ -235,29 +355,30 @@ package org.bigbluebutton.main.model.users presenter = status.value; break; case "hasStream": - var streamInfo:Array = String(status.value).split(/,/); + var streamInfo:Array = String(status.value).split(/,/); /** * Cannot use this statement as new Boolean(expression) * return true if the expression is a non-empty string not * when the string equals "true". See Boolean class def. - * + * * hasStream = new Boolean(String(streamInfo[0])); - */ - if (String(streamInfo[0]).toUpperCase() == "TRUE") { - hasStream = true; - } else { - hasStream = false; - } - + */ var streamNameInfo:Array = String(streamInfo[1]).split(/=/); - streamName = streamNameInfo[1]; - if (hasStream) sendStreamStartedEvent(); + streamName = streamNameInfo[1]; break; - case "raiseHand": - raiseHand = status.value as Boolean; - if (me) { - UserManager.getInstance().getConference().isMyHandRaised = status.value; - } + case "mood": + trace("New mood received: " + status.value); + var moodValue:String = String(status.value); + if (moodValue == "") { + trace("Empty mood, assuming CLEAR_STATUS"); + moodValue = ChangeStatusEvent.CLEAR_STATUS; + moodTimestamp = 0; + } else { + var valueSplit:Array = moodValue.split(","); + moodValue = valueSplit[0]; + moodTimestamp = Number(valueSplit[1]); + } + mood = moodValue; break; } buildStatus(); @@ -278,12 +399,12 @@ package org.bigbluebutton.main.model.users n.userID = user.userID; n.externUserID = user.externUserID; n.name = user.name; - n.hasStream = user.hasStream; - n.viewingStream = user.viewingStream; - n.streamName = user.streamName; + n._viewingStream = user._viewingStream; + n.streamNames = user.streamNames; n.presenter = user.presenter; - n.raiseHand = user.raiseHand; - n.role = user.role; + n.mood = user.mood; + n.moodTimestamp = user.moodTimestamp; + n._role = user._role; n.room = user.room; n.customdata = user.customdata; n.media = user.media; @@ -297,14 +418,21 @@ package org.bigbluebutton.main.model.users n.disableMyMic = user.disableMyMic; n.disableMyPrivateChat = user.disableMyPrivateChat; n.disableMyPublicChat = user.disableMyPublicChat; + n.guest = user.guest; + return n; } - - private function sendStreamStartedEvent():void{ + + private function sendStreamStartedEvent(stream: String):void{ var dispatcher:Dispatcher = new Dispatcher(); - dispatcher.dispatchEvent(new StreamStartedEvent(userID, name, streamName)); + dispatcher.dispatchEvent(new StreamStartedEvent(userID, name, stream)); } - + + private function sendStreamStoppedEvent(stream:String):void{ + var dispatcher:Dispatcher = new Dispatcher(); + dispatcher.dispatchEvent(new StreamStoppedEvent(userID, name, stream)); + } + public function applyLockSettings():void { var lockSettings:LockSettingsVO = UserManager.getInstance().getConference().getLockSettings(); var lockAppliesToMe:Boolean = me && role != MODERATOR && !presenter && userLocked; @@ -333,4 +461,4 @@ package org.bigbluebutton.main.model.users } } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/Conference.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/Conference.as index c5ca40445579f68a8c2337337eac9e77be1b908b..3f211d9a9c34afe2c4f11fbf423e7b36444e3bcc 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/Conference.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/Conference.as @@ -26,6 +26,7 @@ package org.bigbluebutton.main.model.users { import org.bigbluebutton.core.model.Config; import org.bigbluebutton.core.vo.CameraSettingsVO; import org.bigbluebutton.core.vo.LockSettingsVO; + import org.bigbluebutton.main.model.users.events.ChangeStatusEvent; public class Conference { public var meetingName:String; @@ -36,6 +37,7 @@ package org.bigbluebutton.main.model.users { public var voiceBridge:String; public var dialNumber:String; [Bindable] public var record:Boolean; + [Bindable] public var numAdditionalSharedNotes:Number = 0; private static const LOG:String = "main.model.users::Conference - "; @@ -60,40 +62,35 @@ package org.bigbluebutton.main.model.users { // Custom sort function for the users ArrayCollection. Need to put dial-in users at the very bottom. private function sortFunction(a:Object, b:Object, array:Array = null):int { + if (a.raiseHand && b.raiseHand) { + if (a.moodTimestamp == b.moodTimestamp) { + // do nothing go check moderators + } else if (a.moodTimestamp < b.moodTimestamp) + return -1; + else if (b.moodTimestamp < a.moodTimestamp) + return 1; + } else if (a.raiseHand) + return -1; + else if (b.raiseHand) + return 1; + /*if (a.presenter) return -1; else if (b.presenter) return 1;*/ if (a.role == Role.MODERATOR && b.role == Role.MODERATOR) { - if (a.raiseHand && b.raiseHand) { - if (a.raiseHandTime < b.raiseHandTime) - return -1; - else - return 1; - } else if (a.raiseHand) - return -1; - else if (b.raiseHand) - return 1; + // do nothing, go check names } else if (a.role == Role.MODERATOR) return -1; else if (b.role == Role.MODERATOR) return 1; - else if (a.raiseHand && b.raiseHand) { - if (a.raiseHandTime < b.raiseHandTime) - return -1; - else - return 1; - } else if (a.raiseHand) + else if (a.phoneUser && b.phoneUser) { + // do nothing, go check names + } else if (a.phoneUser) return -1; - else if (b.raiseHand) + else if (b.phoneUser) return 1; - else if (!a.phoneUser && !b.phoneUser) { - - } else if (!a.phoneUser) - return -1; - else if (!b.phoneUser) - return 1; - + /* * Check name (case-insensitive) in the event of a tie up above. If the name * is the same then use userID which should be unique making the order the same @@ -113,15 +110,13 @@ package org.bigbluebutton.main.model.users { public function addUser(newuser:BBBUser):void { trace("Adding new user [" + newuser.userID + "]"); - if (! hasUser(newuser.userID)) { - trace("Am I this new user [" + newuser.userID + ", " + me.userID + "]"); - if (newuser.userID == me.userID) { - newuser.me = true; - } - - users.addItem(newuser); - users.refresh(); - } + removeUserHelper(newuser.userID); + if (newuser.userID == me.userID) { + newuser.me = true; + } + + users.addItem(newuser); + users.refresh(); } public function setCamPublishing(publishing:Boolean):void { @@ -222,15 +217,23 @@ package org.bigbluebutton.main.model.users { var a:BBBUser = user.participant as BBBUser; return a.presenter; } - - public function removeUser(userID:String):void { + + private function removeUserHelper(userID:String):BBBUser { var p:Object = getUserIndex(userID); if (p != null) { - trace("removing user[" + p.participant.name + "," + p.participant.userID + "]"); users.removeItemAt(p.index); - //sort(); + return p.participant as BBBUser; + } else { + return null; + } + } + + public function removeUser(userID:String):void { + var p:Object = removeUserHelper(userID); + if (p != null) { + trace("removing user[" + p.name + "," + p.userID + "]"); users.refresh(); - } + } } /** @@ -276,6 +279,10 @@ package org.bigbluebutton.main.model.users { me.raiseHand = raiseHand; } + public function getMyMood():String { + return me.mood; + } + public function amIThisUser(userID:String):Boolean { return me.userID == userID; } @@ -339,6 +346,10 @@ package org.bigbluebutton.main.model.users { public function getMyUserId():String { return me.userID; } + + public function getMyself():BBBUser { + return me; + } public function setMyUserid(userID:String):void { me.userID = userID; @@ -363,6 +374,14 @@ package org.bigbluebutton.main.model.users { public function setMyRole(role:String):void { me.role = role; } + + public function amIGuest():Boolean { + return me.guest; + } + + public function setGuest(guest:Boolean):void { + me.guest = guest; + } public function setMyRoom(room:String):void { me.room = room; @@ -376,15 +395,6 @@ package org.bigbluebutton.main.model.users { users.removeAll(); } - public function raiseHand(userId: String, raised: Boolean):void { - var aUser:BBBUser = getUser(userId); - if (aUser != null) { - aUser.userRaiseHand(raised) - } - - users.refresh(); - } - public function sharedWebcam(userId: String, stream: String):void { var aUser:BBBUser = getUser(userId); if (aUser != null) { @@ -394,10 +404,10 @@ package org.bigbluebutton.main.model.users { users.refresh(); } - public function unsharedWebcam(userId: String):void { - var aUser:BBBUser = getUser(userId); + public function unsharedWebcam(userId: String, stream:String):void { + var aUser:BBBUser = getUser(userId); if (aUser != null) { - aUser.unsharedWebcam() + aUser.unsharedWebcam(stream); } users.refresh(); @@ -421,7 +431,16 @@ package org.bigbluebutton.main.model.users { users.refresh(); } - + + public function newUserRole(userID:String, role:String):void { + var aUser:BBBUser = getUser(userID); + if (aUser != null) { + aUser.role = role; + } + + users.refresh(); + } + public function getUserIDs():ArrayCollection { var uids:ArrayCollection = new ArrayCollection(); for (var i:int = 0; i < users.length; i++) { @@ -529,4 +548,4 @@ package org.bigbluebutton.main.model.users { myUser.applyLockSettings(); } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/JoinService.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/JoinService.as index 99a484dc7faff46ea8aa9c1b0bd753e85f451483..6a99e655d14ad0ffb00d99d0dc4235bf35f90487 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/JoinService.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/JoinService.as @@ -101,6 +101,7 @@ package org.bigbluebutton.main.model.users response.externUserID = result.response.externUserID; response.internalUserId = result.response.internalUserID; response.role = result.response.role; + response.guest = result.response.guest; response.room = result.response.room; response.authToken = result.response.authToken; response.record = result.response.record; @@ -138,7 +139,8 @@ package org.bigbluebutton.main.model.users .withExternalId(response.externUserID).withToken(response.authToken) .withLayout(response.defaultLayout).withWelcome(response.welcome) .withDialNumber(response.dialnumber).withRole(response.role) - .withCustomData(response.customData).build(); + .withCustomData(response.customData) + .withGuest(response.guest.toUpperCase() == "TRUE").build(); MeetingModel.getInstance().meeting = new MeetingBuilder(response.conference, response.conferenceName) .withDefaultLayout(response.defaultLayout).withVoiceConf(response.voiceBridge) diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/NetConnectionDelegate.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/NetConnectionDelegate.as index 79cfb5114a7c9fec33de471a0f72ea38f769c90a..80cefc3ef989b4e47b26258fce789119ef14eb70 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/NetConnectionDelegate.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/NetConnectionDelegate.as @@ -1,401 +1,412 @@ -/** -* 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.main.model.users -{ - import com.adobe.protocols.dict.events.ErrorEvent; - import com.asfusion.mate.events.Dispatcher; - - import flash.events.*; - import flash.net.NetConnection; - import flash.net.Responder; - import flash.utils.Timer; - - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.services.BandwidthMonitor; - import org.bigbluebutton.main.api.JSLog; - import org.bigbluebutton.main.events.InvalidAuthTokenEvent; - import org.bigbluebutton.main.model.ConferenceParameters; - import org.bigbluebutton.main.model.users.events.ConnectionFailedEvent; +/** +* 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.main.model.users +{ + import com.adobe.protocols.dict.events.ErrorEvent; + import com.asfusion.mate.events.Dispatcher; + + import flash.events.*; + import flash.net.NetConnection; + import flash.net.Responder; + import flash.utils.Timer; + + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.services.BandwidthMonitor; + import org.bigbluebutton.main.api.JSLog; + import org.bigbluebutton.main.events.InvalidAuthTokenEvent; + import org.bigbluebutton.main.model.ConferenceParameters; + import org.bigbluebutton.main.model.users.events.ConnectionFailedEvent; import org.bigbluebutton.main.model.users.events.UsersConnectionEvent; - - public class NetConnectionDelegate - { - public static const LOG:String = "NetConnectionDelegate - "; - - private var _netConnection:NetConnection; - private var connectionId:Number; - private var connected:Boolean = false; - - private var _userid:Number = -1; - private var _role:String = "unknown"; - private var _applicationURI:String; - private var _conferenceParameters:ConferenceParameters; - - // These two are just placeholders. We'll get this from the server later and - // then pass to other modules. - private var _authToken:String = "AUTHORIZED"; - private var _room:String; - private var tried_tunneling:Boolean = false; - private var logoutOnUserCommand:Boolean = false; - private var backoff:Number = 2000; - - private var dispatcher:Dispatcher; - private var _messageListeners:Array = new Array(); - - private var authenticated: Boolean = false; - - public function NetConnectionDelegate():void - { - dispatcher = new Dispatcher(); - - _netConnection = new NetConnection(); - _netConnection.proxyType = "best"; - _netConnection.client = this; - _netConnection.addEventListener( NetStatusEvent.NET_STATUS, netStatus ); - _netConnection.addEventListener( AsyncErrorEvent.ASYNC_ERROR, netASyncError ); - _netConnection.addEventListener( SecurityErrorEvent.SECURITY_ERROR, netSecurityError ); - _netConnection.addEventListener( IOErrorEvent.IO_ERROR, netIOError ); - } - - public function setUri(uri:String):void { - _applicationURI = uri; - } - - - public function get connection():NetConnection { - return _netConnection; - } - - public function addMessageListener(listener:IMessageListener):void { - _messageListeners.push(listener); - } - - public function removeMessageListener(listener:IMessageListener):void { - for (var ob:int=0; ob<_messageListeners.length; ob++) { - if (_messageListeners[ob] == listener) { - _messageListeners.splice (ob,1); - break; - } - } - } - - private function notifyListeners(messageName:String, message:Object):void { - if (messageName != null && messageName != "") { - for (var notify:String in _messageListeners) { - _messageListeners[notify].onMessage(messageName, message); - } - } else { - LogUtil.debug("Message name is undefined"); - } - } - - public function onMessageFromServer(messageName:String, msg:Object):void { - trace(LOG + "Got message from server [" + messageName + "]"); - if (!authenticated && (messageName == "validateAuthTokenReply")) { - handleValidateAuthTokenReply(msg) - } else if (messageName == "validateAuthTokenTimedOut") { - handleValidateAuthTokenTimedOut(msg) - } else if (authenticated) { - notifyListeners(messageName, msg); - } else { - trace(LOG + "Ignoring message=[" + messageName + "] as our token hasn't been validated yet."); - } - } - - private function validateToken():void { - var message:Object = new Object(); - message["userId"] = _conferenceParameters.internalUserID; - message["authToken"] = _conferenceParameters.authToken; - - sendMessage( - "validateToken",// Remote function name - // result - On successful result - function(result:Object):void { - trace(LOG + "validating token for [" + _conferenceParameters.internalUserID + "]"); - }, - // status - On error occurred - function(status:Object):void { - LogUtil.error("Error occurred:"); - for (var x:Object in status) { - LogUtil.error(x + " : " + status[x]); - } - }, - message - ); //_netConnection.call - } - - private function handleValidateAuthTokenTimedOut(msg: Object):void { - trace(LOG + "*** handleValidateAuthTokenTimedOut " + msg.msg + " **** \n"); - var map:Object = JSON.parse(msg.msg); - var tokenValid: Boolean = map.valid as Boolean; - var userId: String = map.userId as String; - - var logData:Object = new Object(); - logData.user = UsersUtil.getUserData(); - JSLog.critical("Validate auth token timed out.", logData); - - if (tokenValid) { - authenticated = true; - trace(LOG + "*** handleValidateAuthTokenTimedOut. valid=[ " + tokenValid + "] **** \n"); - } else { - trace(LOG + "*** handleValidateAuthTokenTimedOut. valid=[ " + tokenValid + "] **** \n"); - dispatcher.dispatchEvent(new InvalidAuthTokenEvent()); - } - } - - private function handleValidateAuthTokenReply(msg: Object):void { - trace(LOG + "*** handleValidateAuthTokenReply " + msg.msg + " **** \n"); - var map:Object = JSON.parse(msg.msg); - var tokenValid: Boolean = map.valid as Boolean; - var userId: String = map.userId as String; - - if (tokenValid) { - authenticated = true; - trace(LOG + "*** handleValidateAuthTokenReply. valid=[ " + tokenValid + "] **** \n"); - } else { - trace(LOG + "*** handleValidateAuthTokenReply. valid=[ " + tokenValid + "] **** \n"); - dispatcher.dispatchEvent(new InvalidAuthTokenEvent()); - } - } - - private function sendConnectionSuccessEvent(userid:String):void{ - var e:UsersConnectionEvent = new UsersConnectionEvent(UsersConnectionEvent.CONNECTION_SUCCESS); - e.userid = userid; - dispatcher.dispatchEvent(e); - - } - - public function sendMessage(service:String, onSuccess:Function, onFailure:Function, message:Object=null):void { - trace(LOG + "SENDING [" + service + "]"); - var responder:Responder = new Responder( - function(result:Object):void { // On successful result - onSuccess("Successfully sent [" + service + "]."); - }, - function(status:Object):void { // status - On error occurred - var errorReason:String = "Failed to send [" + service + "]:\n"; - for (var x:Object in status) { - errorReason += "\t" + x + " : " + status[x]; - } - } - ); - - if (message == null) { - _netConnection.call(service, responder); - } else { - _netConnection.call(service, responder, message); - } - } - - /** - * Connect to the server. - * uri: The uri to the conference application. - * username: Fullname of the participant. - * role: MODERATOR/VIEWER - * conference: The conference room - * mode: LIVE/PLAYBACK - Live:when used to collaborate, Playback:when being used to playback a recorded conference. - * room: Need the room number when playing back a recorded conference. When LIVE, the room is taken from the URI. - */ - public function connect(params:ConferenceParameters, tunnel:Boolean = false):void { - _conferenceParameters = params; - - tried_tunneling = tunnel; - - try { - var uri:String = _applicationURI + "/" + _conferenceParameters.room; - - trace(LOG + "::Connecting to " + uri + " [" + _conferenceParameters.username + "," + _conferenceParameters.role + "," + - _conferenceParameters.conference + "," + _conferenceParameters.record + "," + _conferenceParameters.room + ", " + _conferenceParameters.lockSettings.lockOnJoin + "]"); - _netConnection.connect(uri, _conferenceParameters.username, _conferenceParameters.role, - _conferenceParameters.room, _conferenceParameters.voicebridge, - _conferenceParameters.record, _conferenceParameters.externUserID, - _conferenceParameters.internalUserID, _conferenceParameters.muteOnStart, _conferenceParameters.lockSettings); - } catch(e:ArgumentError) { - // Invalid parameters. - switch (e.errorID) { - case 2004 : - LogUtil.debug("Error! Invalid server location: " + uri); - break; - default : - LogUtil.debug("UNKNOWN Error! Invalid server location: " + uri); - break; - } - } - } - - public function disconnect(logoutOnUserCommand:Boolean):void { - this.logoutOnUserCommand = logoutOnUserCommand; - _netConnection.close(); - } - - - public function forceClose():void { - _netConnection.close(); - } - - protected function netStatus(event:NetStatusEvent):void { - handleResult( event ); - } - - private var _bwMon:BandwidthMonitor = new BandwidthMonitor(); - - private function startMonitoringBandwidth():void { - trace("Start monitoring bandwidth."); - var pattern:RegExp = /(?P<protocol>.+):\/\/(?P<server>.+)\/(?P<app>.+)/; - var result:Array = pattern.exec(_applicationURI); - _bwMon.serverURL = result.server; - _bwMon.serverApplication = "video"; - _bwMon.start(); - } - - private var autoReconnectTimer:Timer = new Timer(1000, 1); - - public function handleResult(event:Object):void { - var info : Object = event.info; - var statusCode : String = info.code; - - var logData:Object = new Object(); - logData.user = UsersUtil.getUserData(); - - switch (statusCode) { - case "NetConnection.Connect.Success": - trace(LOG + ":Connection to viewers application succeeded."); - JSLog.debug("Successfully connected to BBB App.", logData); - - validateToken(); - - break; - - case "NetConnection.Connect.Failed": - if (tried_tunneling) { - trace(LOG + ":Connection to viewers application failed...even when tunneling"); - sendConnectionFailedEvent(ConnectionFailedEvent.CONNECTION_FAILED); - } else { - disconnect(false); - trace(LOG + ":Connection to viewers application failed...try tunneling"); - var rtmptRetryTimer:Timer = new Timer(1000, 1); - rtmptRetryTimer.addEventListener("timer", rtmptRetryTimerHandler); - rtmptRetryTimer.start(); - } - break; - - case "NetConnection.Connect.Closed": - trace(LOG + "Connection to viewers application closed"); - sendConnectionFailedEvent(ConnectionFailedEvent.CONNECTION_CLOSED); - - break; - - case "NetConnection.Connect.InvalidApp": - trace(LOG + ":viewers application not found on server"); - sendConnectionFailedEvent(ConnectionFailedEvent.INVALID_APP); - break; - - case "NetConnection.Connect.AppShutDown": - trace(LOG + ":viewers application has been shutdown"); - sendConnectionFailedEvent(ConnectionFailedEvent.APP_SHUTDOWN); - break; - - case "NetConnection.Connect.Rejected": - trace(LOG + ":Connection to the server rejected. Uri: " + _applicationURI + ". Check if the red5 specified in the uri exists and is running" ); - sendConnectionFailedEvent(ConnectionFailedEvent.CONNECTION_REJECTED); - break; - - case "NetConnection.Connect.NetworkChange": - JSLog.warn("Detected network change to BBB App", logData); - trace(LOG + "Detected network change. User might be on a wireless and temporarily dropped connection. Doing nothing. Just making a note."); - break; - - default : - trace(LOG + ":Default status to the viewers application" ); - sendConnectionFailedEvent(ConnectionFailedEvent.UNKNOWN_REASON); - break; - } - } - - private function autoReconnectTimerHandler(event:TimerEvent):void { - trace(LOG + "autoReconnectTimerHandler: " + event); - connect(_conferenceParameters, tried_tunneling); - } - - private function rtmptRetryTimerHandler(event:TimerEvent):void { - trace(LOG + "rtmptRetryTimerHandler: " + event); - connect(_conferenceParameters, true); - } - - protected function netSecurityError(event: SecurityErrorEvent):void { - trace(LOG + "Security error - " + event.text); - sendConnectionFailedEvent(ConnectionFailedEvent.UNKNOWN_REASON); - } - - protected function netIOError(event: IOErrorEvent):void { - trace(LOG + "Input/output error - " + event.text); - sendConnectionFailedEvent(ConnectionFailedEvent.UNKNOWN_REASON); - } - - protected function netASyncError(event: AsyncErrorEvent):void { - trace(LOG + "Asynchronous code error - " + event.toString() ); - - LogUtil.debug("Asynchronous code error - " + event.toString() ); - sendConnectionFailedEvent(ConnectionFailedEvent.UNKNOWN_REASON); - } - - private function sendConnectionFailedEvent(reason:String):void{ - var logData:Object = new Object(); - - if (this.logoutOnUserCommand) { - logData.reason = "User requested."; - logData.user = UsersUtil.getUserData(); - JSLog.debug("User logged out from BBB App.", logData); - sendUserLoggedOutEvent(); - } else { - logData.reason = reason; - logData.user = UsersUtil.getUserData(); - JSLog.warn("User disconnected from BBB App.", logData); - var e:ConnectionFailedEvent = new ConnectionFailedEvent(reason); - dispatcher.dispatchEvent(e); - } - } - - private function sendUserLoggedOutEvent():void{ - var e:ConnectionFailedEvent = new ConnectionFailedEvent(ConnectionFailedEvent.USER_LOGGED_OUT); - dispatcher.dispatchEvent(e); - } - - private function attemptReconnect(backoff:Number):void{ - var retryTimer:Timer = new Timer(backoff, 1); - retryTimer.addEventListener(TimerEvent.TIMER, function():void{ - connect(_conferenceParameters, tried_tunneling); - }); - retryTimer.start(); - if (this.backoff < 16000) this.backoff = backoff *2; - } - - public function onBWCheck(... rest):Number { - return 0; - } - - public function onBWDone(... rest):void { - var p_bw:Number; - if (rest.length > 0) p_bw = rest[0]; - // your application should do something here - // when the bandwidth check is complete - trace("bandwidth = " + p_bw + " Kbps."); - } - } -} + + public class NetConnectionDelegate + { + public static const LOG:String = "NetConnectionDelegate - "; + + private var _netConnection:NetConnection; + private var connectionId:Number; + private var connected:Boolean = false; + + private var _userid:Number = -1; + private var _role:String = "unknown"; + private var _applicationURI:String; + private var _conferenceParameters:ConferenceParameters; + + // These two are just placeholders. We'll get this from the server later and + // then pass to other modules. + private var _authToken:String = "AUTHORIZED"; + private var _room:String; + private var tried_tunneling:Boolean = false; + private var logoutOnUserCommand:Boolean = false; + private var guestKickedOutCommand:Boolean = false; + private var backoff:Number = 2000; + + private var dispatcher:Dispatcher; + private var _messageListeners:Array = new Array(); + + private var authenticated: Boolean = false; + + public function NetConnectionDelegate():void + { + dispatcher = new Dispatcher(); + + _netConnection = new NetConnection(); + _netConnection.proxyType = "best"; + _netConnection.client = this; + _netConnection.addEventListener( NetStatusEvent.NET_STATUS, netStatus ); + _netConnection.addEventListener( AsyncErrorEvent.ASYNC_ERROR, netASyncError ); + _netConnection.addEventListener( SecurityErrorEvent.SECURITY_ERROR, netSecurityError ); + _netConnection.addEventListener( IOErrorEvent.IO_ERROR, netIOError ); + } + + public function setUri(uri:String):void { + _applicationURI = uri; + + var pattern:RegExp = /(?P<protocol>.+):\/\/(?P<server>.+)\/(?P<app>.+)/; + var result:Array = pattern.exec(uri); + BandwidthMonitor.getInstance().serverURL = result.server; + } + + + public function get connection():NetConnection { + return _netConnection; + } + + public function addMessageListener(listener:IMessageListener):void { + _messageListeners.push(listener); + } + + public function removeMessageListener(listener:IMessageListener):void { + for (var ob:int=0; ob<_messageListeners.length; ob++) { + if (_messageListeners[ob] == listener) { + _messageListeners.splice (ob,1); + break; + } + } + } + + private function notifyListeners(messageName:String, message:Object):void { + if (messageName != null && messageName != "") { + for (var notify:String in _messageListeners) { + _messageListeners[notify].onMessage(messageName, message); + } + } else { + LogUtil.debug("Message name is undefined"); + } + } + + public function onMessageFromServer(messageName:String, msg:Object):void { + trace(LOG + "Got message from server [" + messageName + "]"); + if (!authenticated && (messageName == "validateAuthTokenReply")) { + handleValidateAuthTokenReply(msg) + } else if (messageName == "validateAuthTokenTimedOut") { + handleValidateAuthTokenTimedOut(msg) + } else if (authenticated) { + notifyListeners(messageName, msg); + } else { + trace(LOG + "Ignoring message=[" + messageName + "] as our token hasn't been validated yet."); + } + } + + private function validateToken():void { + var message:Object = new Object(); + message["userId"] = _conferenceParameters.internalUserID; + message["authToken"] = _conferenceParameters.authToken; + + sendMessage( + "validateToken",// Remote function name + // result - On successful result + function(result:Object):void { + trace(LOG + "validating token for [" + _conferenceParameters.internalUserID + "]"); + }, + // status - On error occurred + function(status:Object):void { + LogUtil.error("Error occurred:"); + for (var x:Object in status) { + LogUtil.error(x + " : " + status[x]); + } + }, + message + ); //_netConnection.call + } + + private function handleValidateAuthTokenTimedOut(msg: Object):void { + trace(LOG + "*** handleValidateAuthTokenTimedOut " + msg.msg + " **** \n"); + var map:Object = JSON.parse(msg.msg); + var tokenValid: Boolean = map.valid as Boolean; + var userId: String = map.userId as String; + + var logData:Object = new Object(); + logData.user = UsersUtil.getUserData(); + JSLog.critical("Validate auth token timed out.", logData); + + if (tokenValid) { + authenticated = true; + trace(LOG + "*** handleValidateAuthTokenTimedOut. valid=[ " + tokenValid + "] **** \n"); + } else { + trace(LOG + "*** handleValidateAuthTokenTimedOut. valid=[ " + tokenValid + "] **** \n"); + dispatcher.dispatchEvent(new InvalidAuthTokenEvent()); + } + } + + private function handleValidateAuthTokenReply(msg: Object):void { + trace(LOG + "*** handleValidateAuthTokenReply " + msg.msg + " **** \n"); + var map:Object = JSON.parse(msg.msg); + var tokenValid: Boolean = map.valid as Boolean; + var userId: String = map.userId as String; + + if (tokenValid) { + authenticated = true; + trace(LOG + "*** handleValidateAuthTokenReply. valid=[ " + tokenValid + "] **** \n"); + } else { + trace(LOG + "*** handleValidateAuthTokenReply. valid=[ " + tokenValid + "] **** \n"); + dispatcher.dispatchEvent(new InvalidAuthTokenEvent()); + } + } + + private function sendConnectionSuccessEvent(userid:String):void{ + var e:UsersConnectionEvent = new UsersConnectionEvent(UsersConnectionEvent.CONNECTION_SUCCESS); + e.userid = userid; + dispatcher.dispatchEvent(e); + + } + + public function sendMessage(service:String, onSuccess:Function, onFailure:Function, message:Object=null):void { + trace(LOG + "SENDING [" + service + "]"); + var responder:Responder = new Responder( + function(result:Object):void { // On successful result + onSuccess("Successfully sent [" + service + "]."); + }, + function(status:Object):void { // status - On error occurred + var errorReason:String = "Failed to send [" + service + "]:\n"; + for (var x:Object in status) { + errorReason += "\t" + x + " : " + status[x]; + } + } + ); + + if (message == null) { + _netConnection.call(service, responder); + } else { + _netConnection.call(service, responder, message); + } + } + + /** + * Connect to the server. + * uri: The uri to the conference application. + * username: Fullname of the participant. + * role: MODERATOR/VIEWER + * conference: The conference room + * mode: LIVE/PLAYBACK - Live:when used to collaborate, Playback:when being used to playback a recorded conference. + * room: Need the room number when playing back a recorded conference. When LIVE, the room is taken from the URI. + */ + public function connect(params:ConferenceParameters, tunnel:Boolean = false):void { + _conferenceParameters = params; + + tried_tunneling = tunnel; + + try { + var uri:String = _applicationURI + "/" + _conferenceParameters.room; + + trace(LOG + "::Connecting to " + uri + " [" + _conferenceParameters.username + "," + _conferenceParameters.role + "," + + _conferenceParameters.conference + "," + _conferenceParameters.record + "," + _conferenceParameters.room + ", " + _conferenceParameters.lockSettings.lockOnJoin + "]"); + _netConnection.connect(uri, _conferenceParameters.username, _conferenceParameters.role, + _conferenceParameters.room, _conferenceParameters.voicebridge, + _conferenceParameters.record, _conferenceParameters.externUserID, + _conferenceParameters.internalUserID, _conferenceParameters.muteOnStart, _conferenceParameters.lockSettings, + _conferenceParameters.guest); + } catch(e:ArgumentError) { + // Invalid parameters. + switch (e.errorID) { + case 2004 : + LogUtil.debug("Error! Invalid server location: " + uri); + break; + default : + LogUtil.debug("UNKNOWN Error! Invalid server location: " + uri); + break; + } + } + } + + public function disconnect(logoutOnUserCommand:Boolean):void { + this.logoutOnUserCommand = logoutOnUserCommand; + _netConnection.close(); + } + + public function guestDisconnect() : void + { + this.guestKickedOutCommand = true; + _netConnection.close(); + } + + + public function forceClose():void { + _netConnection.close(); + } + + protected function netStatus(event:NetStatusEvent):void { + handleResult( event ); + } + + private var autoReconnectTimer:Timer = new Timer(1000, 1); + + public function handleResult(event:Object):void { + var info : Object = event.info; + var statusCode : String = info.code; + + var logData:Object = new Object(); + logData.user = UsersUtil.getUserData(); + + switch (statusCode) { + case "NetConnection.Connect.Success": + trace(LOG + ":Connection to viewers application succeeded."); + JSLog.debug("Successfully connected to BBB App.", logData); + + validateToken(); + + break; + + case "NetConnection.Connect.Failed": + if (tried_tunneling) { + trace(LOG + ":Connection to viewers application failed...even when tunneling"); + sendConnectionFailedEvent(ConnectionFailedEvent.CONNECTION_FAILED); + } else { + disconnect(false); + trace(LOG + ":Connection to viewers application failed...try tunneling"); + var rtmptRetryTimer:Timer = new Timer(1000, 1); + rtmptRetryTimer.addEventListener("timer", rtmptRetryTimerHandler); + rtmptRetryTimer.start(); + } + break; + + case "NetConnection.Connect.Closed": + trace(LOG + "Connection to viewers application closed"); + sendConnectionFailedEvent(ConnectionFailedEvent.CONNECTION_CLOSED); + + break; + + case "NetConnection.Connect.InvalidApp": + trace(LOG + ":viewers application not found on server"); + sendConnectionFailedEvent(ConnectionFailedEvent.INVALID_APP); + break; + + case "NetConnection.Connect.AppShutDown": + trace(LOG + ":viewers application has been shutdown"); + sendConnectionFailedEvent(ConnectionFailedEvent.APP_SHUTDOWN); + break; + + case "NetConnection.Connect.Rejected": + trace(LOG + ":Connection to the server rejected. Uri: " + _applicationURI + ". Check if the red5 specified in the uri exists and is running" ); + sendConnectionFailedEvent(ConnectionFailedEvent.CONNECTION_REJECTED); + break; + + case "NetConnection.Connect.NetworkChange": + JSLog.warn("Detected network change to BBB App", logData); + trace(LOG + "Detected network change. User might be on a wireless and temporarily dropped connection. Doing nothing. Just making a note."); + break; + + default : + trace(LOG + ":Default status to the viewers application" ); + sendConnectionFailedEvent(ConnectionFailedEvent.UNKNOWN_REASON); + break; + } + } + + private function autoReconnectTimerHandler(event:TimerEvent):void { + trace(LOG + "autoReconnectTimerHandler: " + event); + connect(_conferenceParameters, tried_tunneling); + } + + private function rtmptRetryTimerHandler(event:TimerEvent):void { + trace(LOG + "rtmptRetryTimerHandler: " + event); + connect(_conferenceParameters, true); + } + + protected function netSecurityError(event: SecurityErrorEvent):void { + trace(LOG + "Security error - " + event.text); + sendConnectionFailedEvent(ConnectionFailedEvent.UNKNOWN_REASON); + } + + protected function netIOError(event: IOErrorEvent):void { + trace(LOG + "Input/output error - " + event.text); + sendConnectionFailedEvent(ConnectionFailedEvent.UNKNOWN_REASON); + } + + protected function netASyncError(event: AsyncErrorEvent):void { + trace(LOG + "Asynchronous code error - " + event.toString() ); + + LogUtil.debug("Asynchronous code error - " + event.toString() ); + sendConnectionFailedEvent(ConnectionFailedEvent.UNKNOWN_REASON); + } + + private function sendConnectionFailedEvent(reason:String):void{ + var logData:Object = new Object(); + + if (this.guestKickedOutCommand) { + logData.reason = "Guest kicked out"; + logData.user = UsersUtil.getUserData(); + JSLog.warn("User disconnected from BBB App.", logData); + sendGuestUserKickedOutEvent(); + } else if (this.logoutOnUserCommand) { + logData.reason = "User requested."; + logData.user = UsersUtil.getUserData(); + JSLog.debug("User logged out from BBB App.", logData); + sendUserLoggedOutEvent(); + } else { + logData.reason = reason; + logData.user = UsersUtil.getUserData(); + JSLog.warn("User disconnected from BBB App.", logData); + var e:ConnectionFailedEvent = new ConnectionFailedEvent(reason); + dispatcher.dispatchEvent(e); + } + } + + private function sendUserLoggedOutEvent():void{ + var e:ConnectionFailedEvent = new ConnectionFailedEvent(ConnectionFailedEvent.USER_LOGGED_OUT); + dispatcher.dispatchEvent(e); + } + + private function sendGuestUserKickedOutEvent():void { + var e:ConnectionFailedEvent = new ConnectionFailedEvent(ConnectionFailedEvent.MODERATOR_DENIED_ME); + dispatcher.dispatchEvent(e); + } + + private function attemptReconnect(backoff:Number):void{ + var retryTimer:Timer = new Timer(backoff, 1); + retryTimer.addEventListener(TimerEvent.TIMER, function():void{ + connect(_conferenceParameters, tried_tunneling); + }); + retryTimer.start(); + if (this.backoff < 16000) this.backoff = backoff *2; + } + + public function onBWCheck(... rest):Number { + return 0; + } + + public function onBWDone(... rest):void { + var p_bw:Number; + if (rest.length > 0) p_bw = rest[0]; + // your application should do something here + // when the bandwidth check is complete + trace("bandwidth = " + p_bw + " Kbps."); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/UserService.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/UserService.as index 4665f7bebf4fbefad0ff6a492025e88fb7a14b17..1b3fe4eea4005423b708c280abbdd065ed8ab909 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/UserService.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/UserService.as @@ -37,17 +37,19 @@ package org.bigbluebutton.main.model.users import org.bigbluebutton.core.managers.UserConfigManager; import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.core.model.Config; - import org.bigbluebutton.core.model.MeetingModel; + import org.bigbluebutton.common.Role; import org.bigbluebutton.main.events.BBBEvent; import org.bigbluebutton.main.events.SuccessfulLoginEvent; import org.bigbluebutton.main.events.UserServicesEvent; + import org.bigbluebutton.main.events.ResponseModeratorEvent; + import org.bigbluebutton.main.events.LogoutEvent; import org.bigbluebutton.main.model.ConferenceParameters; import org.bigbluebutton.main.model.users.events.BroadcastStartedEvent; import org.bigbluebutton.main.model.users.events.BroadcastStoppedEvent; + import org.bigbluebutton.main.model.users.events.ChangeRoleEvent; import org.bigbluebutton.main.model.users.events.ConferenceCreatedEvent; import org.bigbluebutton.main.model.users.events.KickUserEvent; - import org.bigbluebutton.main.model.users.events.LowerHandEvent; - import org.bigbluebutton.main.model.users.events.RaiseHandEvent; + import org.bigbluebutton.main.model.users.events.ChangeStatusEvent import org.bigbluebutton.main.model.users.events.RoleChangeEvent; import org.bigbluebutton.main.model.users.events.UsersConnectionEvent; import org.bigbluebutton.modules.users.services.MessageReceiver; @@ -69,6 +71,15 @@ package org.bigbluebutton.main.model.users public function UserService() { dispatcher = new Dispatcher(); + msgReceiver.onAllowedToJoin = function():void { + sender.queryForParticipants(); + sender.queryForRecordingStatus(); + sender.queryForGuestPolicy(); + + var loadCommand:SuccessfulLoginEvent = new SuccessfulLoginEvent(SuccessfulLoginEvent.USER_LOGGED_IN); + loadCommand.conferenceParameters = _conferenceParameters; + dispatcher.dispatchEvent(loadCommand); + } } public function startService(e:UserServicesEvent):void { @@ -88,6 +99,7 @@ package org.bigbluebutton.main.model.users UserManager.getInstance().getConference().setMyName(result.username); UserManager.getInstance().getConference().setMyRole(result.role); UserManager.getInstance().getConference().setMyRoom(result.room); + UserManager.getInstance().getConference().setGuest(result.guest == "true"); UserManager.getInstance().getConference().setMyAuthToken(result.authToken); UserManager.getInstance().getConference().setMyCustomData(result.customdata); UserManager.getInstance().getConference().setDefaultLayout(result.defaultLayout); @@ -110,6 +122,7 @@ package org.bigbluebutton.main.model.users _conferenceParameters.externMeetingID = result.externMeetingID; _conferenceParameters.conference = result.conference; _conferenceParameters.username = result.username; + _conferenceParameters.guest = (result.guest.toUpperCase() == "TRUE"); _conferenceParameters.role = result.role; _conferenceParameters.room = result.room; _conferenceParameters.authToken = result.authToken; @@ -119,7 +132,7 @@ package org.bigbluebutton.main.model.users _conferenceParameters.meetingID = result.meetingID; _conferenceParameters.externUserID = result.externUserID; _conferenceParameters.internalUserID = result.internalUserId; - _conferenceParameters.logoutUrl = result.logoutUrl; + _conferenceParameters.logoutUrl = processLogoutUrl(result); _conferenceParameters.record = (result.record != "false"); var muteOnStart:Boolean; @@ -150,6 +163,22 @@ package org.bigbluebutton.main.model.users connect(); } } + + private function processLogoutUrl(confInfo:Object):String { + var logoutUrl:String = confInfo.logoutUrl; + var rules:Object = { + "%%FULLNAME%%": confInfo.username, + "%%CONFNAME%%": confInfo.conferenceName, + "%%DIALNUM%%": confInfo.dialnumber, + "%%CONFNUM%%": confInfo.voicebridge + } + + for (var attr:String in rules) { + logoutUrl = logoutUrl.replace(new RegExp(attr, "g"), rules[attr]); + } + + return logoutUrl; + } private function connect():void{ _connectionManager = BBB.initConnectionManager(); @@ -181,13 +210,18 @@ package org.bigbluebutton.main.model.users trace(LOG + "userLoggedIn - Setting my userid to [" + e.userid + "]"); UserManager.getInstance().getConference().setMyUserid(e.userid); _conferenceParameters.userid = e.userid; - - sender.queryForParticipants(); - sender.queryForRecordingStatus(); - - var loadCommand:SuccessfulLoginEvent = new SuccessfulLoginEvent(SuccessfulLoginEvent.USER_LOGGED_IN); - loadCommand.conferenceParameters = _conferenceParameters; - dispatcher.dispatchEvent(loadCommand); + } + + public function denyGuest():void { + dispatcher.dispatchEvent(new LogoutEvent(LogoutEvent.MODERATOR_DENIED_ME)); + } + + public function setGuestPolicy(event:BBBEvent):void { + sender.setGuestPolicy(event.payload['guestPolicy']); + } + + public function guestDisconnect():void { + _connectionManager.guestDisconnect(); } public function isModerator():Boolean { @@ -206,17 +240,21 @@ package org.bigbluebutton.main.model.users sender.removeStream(e.userid, e.stream); } - public function raiseHand(e:RaiseHandEvent):void { - sender.raiseHand(UserManager.getInstance().getConference().getMyUserId(), e.raised); + public function changeStatus(e:ChangeStatusEvent):void { + sender.changeStatus(e.userId, e.getStatusName()); } - - public function lowerHand(e:LowerHandEvent):void { - if (this.isModerator()) sender.raiseHand(e.userid, false); + + public function responseToGuest(e:ResponseModeratorEvent):void { + sender.responseToGuest(e.userid, e.resp); } public function kickUser(e:KickUserEvent):void{ if (this.isModerator()) sender.kickUser(e.userid); } + + public function changeRole(e:ChangeRoleEvent):void { + if (this.isModerator()) sender.changeRole(e.userid, e.role); + } /** * Assign a new presenter diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/LowerHandEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeMyRole.as old mode 100755 new mode 100644 similarity index 78% rename from bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/LowerHandEvent.as rename to bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeMyRole.as index f7a17ca5bbddcb0a1d8a557ba4163d8242ff3935..950c1e61047e304a6fdbe9b14a38af820c56879e --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/LowerHandEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeMyRole.as @@ -20,17 +20,16 @@ package org.bigbluebutton.main.model.users.events { import flash.events.Event; - public class LowerHandEvent extends Event + public class ChangeMyRole extends Event { - public static const LOWER_HAND_EVENT:String = "LOWER_HAND_EVENT"; + public static const CHANGE_MY_ROLE_EVENT:String = "CHANGE_MY_ROLE_EVENT"; - public var userid:String; - - public function LowerHandEvent(userid:String) + public var role:String; + + public function ChangeMyRole(role:String) { - super(LOWER_HAND_EVENT,true); - this.userid = userid; + this.role = role; + super(CHANGE_MY_ROLE_EVENT, true, false); } - } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/RaiseHandEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeRoleEvent.as similarity index 72% rename from bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/RaiseHandEvent.as rename to bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeRoleEvent.as index 0882c9f7f7cfe52a6c663234c6873f755695e196..2aa2df3621b69860ea1ad414f7f0baf771079a31 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/RaiseHandEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeRoleEvent.as @@ -20,15 +20,18 @@ package org.bigbluebutton.main.model.users.events { import flash.events.Event; - public class RaiseHandEvent extends Event + public class ChangeRoleEvent extends Event { - public static const RAISE_HAND:String = "RAISE_HAND_EVENT"; + public static const CHANGE_ROLE_EVENT:String = "CHANGE_ROLE_EVENT"; - public var raised:Boolean; + public var userid:String; + public var role:String; - public function RaiseHandEvent(type:String) + public function ChangeRoleEvent(userid:String, role:String) { - super(type, true, false); + this.userid = userid; + this.role = role; + super(CHANGE_ROLE_EVENT, true, false); } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeStatusBtnEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeStatusBtnEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..5e34d7177e9a71a18c5b534ca058a81e481955b4 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeStatusBtnEvent.as @@ -0,0 +1,26 @@ +package org.bigbluebutton.main.model.users.events +{ + import flash.events.Event; + + public class ChangeStatusBtnEvent extends Event + { + public static const CHANGE_BTN_STATUS:String = "CHANGE_BTN_STATUS"; + + private var status:String; + public var userId:String; + + public function ChangeStatusBtnEvent(id:String,status:String) + { + userId = id; + this.status = status; + super(CHANGE_BTN_STATUS, true, false); + } + + public function getStatusName():String + { + return status; + } + + } +} + diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeStatusEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeStatusEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..63d60a35a435749a9eefdff831ded234a803a1cb --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ChangeStatusEvent.as @@ -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 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.main.model.users.events +{ + import flash.events.Event; + + public class ChangeStatusEvent extends Event + { + public static const CHANGE_STATUS:String = "CHANGE_STATUS_EVENT"; + + public static const CLEAR_STATUS:String = "CLEAR_STATUS"; + public static const RAISE_HAND:String = "RAISE_HAND"; + public static const AGREE:String = "AGREE"; + public static const DISAGREE:String = "DISAGREE"; + public static const SPEAK_LOUDER:String = "SPEAK_LOUDER"; + public static const SPEAK_LOWER:String = "SPEAK_LOWER"; + public static const SPEAK_FASTER:String = "SPEAK_FASTER"; + public static const SPEAK_SLOWER:String = "SPEAK_SLOWER"; + public static const BE_RIGHT_BACK:String = "BE_RIGHT_BACK"; + public static const LAUGHTER:String = "LAUGHTER"; + public static const SAD:String = "SAD"; + + private var status:String; + public var userId:String; + + public function ChangeStatusEvent(id:String,status:String) + { + userId = id; + this.status = status; + super(CHANGE_STATUS, true, false); + } + + public function getStatusName():String + { + return status; + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ConnectionFailedEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ConnectionFailedEvent.as index 11f4672ed5daaf2fb77d565db0838260648dffa0..ba9a68a101eb9444eba742114b93092e34859995 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ConnectionFailedEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/ConnectionFailedEvent.as @@ -30,6 +30,7 @@ package org.bigbluebutton.main.model.users.events public static const CONNECTION_REJECTED:String = "connectionRejected"; public static const ASYNC_ERROR:String = "asyncError"; public static const USER_LOGGED_OUT:String = "userHasLoggedOut"; + public static const MODERATOR_DENIED_ME:String = "moderatorDeniedMe"; public function ConnectionFailedEvent(type:String) { diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/GuestConnectionEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/GuestConnectionEvent.as new file mode 100755 index 0000000000000000000000000000000000000000..047ba2e47071d03c19973644766a51885768738b --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/GuestConnectionEvent.as @@ -0,0 +1,36 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +*/ +package org.bigbluebutton.main.model.users.events +{ + import flash.events.Event; + import flash.net.NetConnection; + + public class GuestConnectionEvent extends Event + { + public static const GUEST_CONNECTION:String = "guestConnection"; + + public var connection:NetConnection; + public var userid:Number; + + public function UsersConnectionEvent(type:String) + { + super(type, true, false); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/StreamStoppedEvent.as b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/StreamStoppedEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..c34195be09ea53abb74b89581f2c830a6405dafc --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/model/users/events/StreamStoppedEvent.as @@ -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.main.model.users.events +{ + import flash.events.Event; + + public class StreamStoppedEvent extends Event + { + public static const STREAM_STOPPED:String = "STREAM_STOPPED"; + + public var user:String; + public var stream:String; + public var userID:String + + public function StreamStoppedEvent(userID:String, user:String, stream:String) + { + this.userID = userID; + this.user = user; + this.stream = stream; + super(STREAM_STOPPED, true, false); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/BBBSettings.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/BBBSettings.mxml new file mode 100644 index 0000000000000000000000000000000000000000..75868aeeacaa60da64a29cd19b10fc0246787960 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/BBBSettings.mxml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" + showCloseButton="false" + xmlns:mate="http://mate.asfusion.com/" + minWidth="250" + creationComplete="init()" + title="{ResourceUtil.getInstance().getString('bbb.settings.title')}" > + + <mx:Script> + <![CDATA[ + import mx.events.ItemClickEvent; + import mx.managers.PopUpManager; + import mx.core.UIComponent; + + import org.bigbluebutton.common.Images; + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.util.i18n.ResourceUtil; + import org.bigbluebutton.common.events.SettingsComponentEvent; + + private function init():void { + this.x = (this.parent.width - this.width) / 2; + this.y = (this.parent.height - this.height) / 2; + } + + public function pushComponents(components:Array):void { + for (var i:int = 0; i < components.length; ++i) { + addedComponents.addChildAt(components[i] as UIComponent, 0); + } + } + + private function onCancelClicked():void { + var event:BBBEvent = new BBBEvent(BBBEvent.SETTINGS_CANCELLED); + dispatchEvent(event); + close(); + } + + private function onOkClicked():void { + var event:BBBEvent = new BBBEvent(BBBEvent.SETTINGS_CONFIRMED); + dispatchEvent(event); + close(); + } + + private function close():void { + PopUpManager.removePopUp(this); + } + + ]]> + </mx:Script> + <mx:VBox id="addedComponents" height="100%" /> + + <mx:ControlBar width="100%" horizontalAlign="center"> + <mx:Button id="okBtn" label="{ResourceUtil.getInstance().getString('bbb.settings.ok')}" width="100" click="onOkClicked()"/> + <mx:Spacer width="10"/> + <mx:Button id="cancelBtn" label="{ResourceUtil.getInstance().getString('bbb.settings.cancel')}" width="100" click="onCancelClicked()"/> + </mx:ControlBar> +</mx:TitleWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/CameraDisplaySettings.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/CameraDisplaySettings.mxml index 64407f1cd4652d3bd057945fff362da0fb823bc1..43d49cfb02c8651be668ba927e8c6a45008be673 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/CameraDisplaySettings.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/CameraDisplaySettings.mxml @@ -30,15 +30,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import flash.ui.Keyboard; + import mx.collections.ArrayCollection; + import mx.collections.ArrayList; import mx.events.CloseEvent; import mx.events.ItemClickEvent; import mx.managers.PopUpManager; import org.bigbluebutton.common.Images; - import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.BBB; import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.model.VideoProfile; import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; import org.bigbluebutton.util.i18n.ResourceUtil; static public var PADDING_HORIZONTAL:Number = 6; @@ -50,22 +52,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private var cancelIcon:Class = images.control_play; [Bindable] - public var resolutions:Array; + public var _videoProfiles:ArrayCollection = new ArrayCollection(); + public var selectedVideoProfile:VideoProfile; public var publishInClient:Boolean; - public var camWidth:Number = 320; - public var camHeight:Number = 240; + public var defaultCamera:String = null; + public var camerasArray:Object; + [Bindable] private var camerasAvailable:ArrayList = new ArrayList(); public var chromePermissionDenied:Boolean = false; - private var _camera:Camera = null; - - // Timer used to enable the start publishing button, only after get any activity on the camera. - // It avoids the problem of publishing a blank video - private var _activationTimer:Timer = null; - private var _waitingForActivation:Boolean = false; - - static private var _cameraAccessDenied:Boolean = false; - - private var _video:Video; + public const OFF_STATE:Number = 0; + public const ON_STATE:Number = 1; + + private var selectedCam:int; private var aspectRatio:Number = 1; [Bindable]private var baseIndex:int; @@ -77,9 +75,38 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function onCreationComplete():void { tabIndex = 51; - changeDefaultCamForMac(); + if(defaultCamera != null) { + var indexDefault:int = 0; + for (var i:int = 0; i < Camera.names.length; i++){ + if(camerasArray[i].status == OFF_STATE) { + var myObj:Object = {} + myObj.label = camerasArray[i].label; + myObj.index = String(i); + camerasAvailable.addItem(myObj); + if(myObj.index == defaultCamera) + indexDefault = camerasAvailable.length-1; + } + } + cmbCameraSelector.selectedIndex = indexDefault; + defaultCamera = null; + } else { + cmbCameraSelector.selectedIndex = 0; + } - if (resolutions.length > 1) { + var idx:int = 0; + var defaultProfile:VideoProfile = BBB.defaultVideoProfile; + for each (var value:VideoProfile in BBB.videoProfiles) { + var item:Object = {index:idx, label:value.name, profile:value}; + _videoProfiles.addItem(item); + + if (value.id == defaultProfile.id) { + cmbVideoProfile.selectedIndex = idx; + } + + idx++; + } + + if (_videoProfiles.length > 1) { showResControls(true); } @@ -90,15 +117,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. updateCamera(); } - private function changeDefaultCamForMac():void { - for (var i:int = 0; i < Camera.names.length; i++){ - if (Camera.names[i] == "USB Video Class Video") { - /** Set as default for Macs */ - _camera = Camera.getCamera("USB Video Class Video"); - } - } - } - private function showVideoControls(show:Boolean):void { if (show) { this.visible = true; @@ -112,84 +130,23 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function updateCamera():void { - _camera = null; - - _camera = Camera.getCamera(); - - if (_camera == null) { - _videoHolder.showWarning('bbb.video.publish.hint.cantOpenCamera'); - return; - } - - _camera.addEventListener(ActivityEvent.ACTIVITY, onActivityEvent); - _camera.addEventListener(StatusEvent.STATUS, onStatusEvent); - - if (_camera.muted) { - if (_cameraAccessDenied && !chromePermissionDenied) { - //onCameraAccessDisallowed(); - //return; - Security.showSettings(SecurityPanel.PRIVACY) - } else if (chromePermissionDenied) { - _videoHolder.showWarning('bbb.video.publish.hint.cameraDenied'); - return; - }else { - _videoHolder.showWarning('bbb.video.publish.hint.waitingApproval'); - } + selectedVideoProfile = cmbVideoProfile.selectedItem.profile as VideoProfile; + if (camerasAvailable.length > cmbCameraSelector.selectedIndex) { + selectedCam = camerasAvailable.getItemAt(cmbCameraSelector.selectedIndex).index; } else { - // if the camera isn't muted, that is because the user has - // previously allowed the camera capture on the flash privacy box - onCameraAccessAllowed(); + selectedCam = -1; } - - displayVideoPreview(); - } - - private function displayVideoPreview():void { - setComboResolution(); - - var videoOptions:VideoConfOptions = new VideoConfOptions(); - _camera.setMotionLevel(5, 1000); - _camera.setKeyFrameInterval(videoOptions.camKeyFrameInterval); - _camera.setMode(camWidth, camHeight, videoOptions.camModeFps); - _camera.setQuality(videoOptions.camQualityBandwidth, videoOptions.camQualityPicture); - - if (_camera.width != camWidth || _camera.height != camHeight) { - LogUtil.debug("Resolution " + camWidth + "x" + camHeight + " is not supported, using " + _camera.width + "x" + _camera.height + " instead"); - setResolution(_camera.width, _camera.height); - } - - if (_video != null) { - _videoHolder.remove(_video); + setAspectRatio(selectedVideoProfile.width,selectedVideoProfile.height); + _video.successCallback = function():void { + btnStartPublish.enabled = true; } - - _video = new Video(); - _video.attachCamera(_camera); - - //aspectRatio = (_video.width / _video.height); - - if (aspectRatio > _videoHolder.width / _videoHolder.height) { - _video.width = _videoHolder.width; - _video.height = _videoHolder.width / aspectRatio; - _video.x = 0; - _video.y = (_videoHolder.height - _video.height) / 2; - } else { - _video.width = _videoHolder.height * aspectRatio; - _video.height = _videoHolder.height; - _video.x = (_videoHolder.width - _video.width) / 2; - _video.y = 0; - } - - _videoHolder.add(_video); + _video.chromePermissionDenied = chromePermissionDenied; + _video.updateCamera(selectedCam,selectedVideoProfile,_canvas.width, _canvas.height,true); } private function showResControls(show:Boolean):void { - if (show) cmbResolution.visible = true; - else cmbResolution.visible = false; - } - - private function setComboResolution():void { - var res:Array = cmbResolution.selectedLabel.split( "x" ); - setResolution(Number(res[0]), Number(res[1])); + if (show) cmbVideoProfile.visible = true; + else cmbVideoProfile.visible = false; } private function setAspectRatio(width:int, height:int):void { @@ -197,25 +154,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. this.minHeight = Math.floor((this.minWidth - PADDING_HORIZONTAL) / aspectRatio) + PADDING_VERTICAL; } - private function setResolution(width:int, height:int):void { - camWidth = width; - camHeight = height; - setAspectRatio(camWidth, camHeight); - } - private function startPublishing():void { updateCamera(); - // Save the index of the camera. Need it to send the message. - var camIndex:int = _camera.index; - disableCamera(); var globalDispatcher:Dispatcher = new Dispatcher(); var camEvent:BBBEvent = new BBBEvent(BBBEvent.CAMERA_SETTING); - camEvent.payload.cameraIndex = camIndex; - camEvent.payload.cameraWidth = camWidth; - camEvent.payload.cameraHeight = camHeight; + camEvent.payload.cameraIndex = selectedCam; + camEvent.payload.videoProfile = selectedVideoProfile; camEvent.payload.publishInClient = publishInClient; globalDispatcher.dispatchEvent(camEvent); @@ -228,13 +175,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function disableCamera():void { - _camera = null; if(_video != null){ - _video.attachCamera(null); - _video.clear(); - _videoHolder.remove(_video); - _video = null; - } + _video.disableCamera(); + } } private function handleKeyDown(event:KeyboardEvent):void { @@ -252,56 +195,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. PopUpManager.removePopUp(this); } - private function onActivityEvent(e:ActivityEvent):void { - if (_waitingForActivation && e.activating) { - _activationTimer.stop(); - _videoHolder.showWarning('bbb.video.publish.hint.videoPreview', false, "0xFFFF00"); - // controls.btnStartPublish.enabled = true; - _waitingForActivation = false; - btnStartPublish.enabled = true; - _videoHolder.imgChromeHelp.visible = false; - } - } - - private function onStatusEvent(e:StatusEvent):void { - if (e.code == "Camera.Unmuted") { - onCameraAccessAllowed(); - // this is just to overwrite the message of waiting for approval - _videoHolder.showWarning('bbb.video.publish.hint.openingCamera'); - } else {//if (e.code == "Camera.Muted") { - onCameraAccessDisallowed(); - } - } - - private function onCameraAccessAllowed():void { - _cameraAccessDenied = false; - - // set timer to ensure that the camera activates. If not, it might be in use by another application - _waitingForActivation = true; - if (_activationTimer != null) { - _activationTimer.stop(); - } - - _activationTimer = new Timer(10000, 1); - _activationTimer.addEventListener(TimerEvent.TIMER, activationTimeout); - _activationTimer.start(); - } - - private function onCameraAccessDisallowed():void { - _videoHolder.showWarning('bbb.video.publish.hint.cameraDenied'); - _cameraAccessDenied = true; - } - - private function activationTimeout(e:TimerEvent):void { - _videoHolder.showWarning('bbb.video.publish.hint.cameraIsBeingUsed'); - - //check for Chrome and show image - var browserType:Array = ExternalInterface.call('determineBrowser'); - if (browserType[0] == "Chrome") { - _videoHolder.imgChromeHelp.visible = true; - } - } - private function showCameraSettings():void { Security.showSettings(SecurityPanel.CAMERA); } @@ -309,21 +202,23 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. ]]> </mx:Script> - <mx:VBox id="webcamDisplay" width="100%" height="100%" paddingBottom="5" paddingLeft="5" paddingRight="5" paddingTop="5" styleName="cameraDisplaySettingsWindowBackground"> - <mx:TextArea width="100%" borderSkin="{null}" editable="false" text="{ResourceUtil.getInstance().getString('bbb.users.settings.webcamSettings')}" + <mx:VBox id="webcamDisplay" width="100%" height="100%" paddingBottom="5" paddingLeft="5" paddingRight="5" paddingTop="5" styleName="cameraDisplaySettingsWindowBackground"> + <mx:HBox width="100%" horizontalAlign="left"> + <mx:TextArea width="100%" wordWrap="false" borderSkin="{null}" editable="false" text="{ResourceUtil.getInstance().getString('bbb.users.settings.webcamSettings')}" styleName="webcamSettingsWindowTitleStyle" tabIndex="{baseIndex}"/> + </mx:HBox> + <mx:HRule width="100%"/> <mx:Spacer height="1"/> - <view:VideoHolder id="_videoHolder" width="100%" height="75%" /> + <mx:Box id="_canvas" width="100%" height="75%" horizontalAlign="center" verticalAlign="middle"> + <view:VideoWithWarnings id="_video"/> + </mx:Box> <mx:HBox width="100%" height="10%" horizontalAlign="center" horizontalGap="13" paddingRight="5"> - <mx:Button id="changeCamera" styleName="cameraDisplaySettingsWindowChangeCamBtn" - label="{ResourceUtil.getInstance().getString('bbb.publishVideo.changeCameraBtn.labelText')}" - toolTip="{ResourceUtil.getInstance().getString('bbb.publishVideo.changeCameraBtn.toolTip')}" - click="showCameraSettings()" tabIndex="{baseIndex+1}"/> - <mx:ComboBox id="cmbResolution" styleName="cameraDisplaySettingsWindowChangeResolutionCombo" - dataProvider="{resolutions}" visible="false" change="updateCamera()" tabIndex="{baseIndex+2}" + <mx:ComboBox id="cmbCameraSelector" styleName="cameraDisplaySettingsWindowCameraSelector" dataProvider="{camerasAvailable}" width="150" visible="true" labelField="label" change="updateCamera()" tabIndex="{baseIndex+1}" height="30"/> + <mx:ComboBox id="cmbVideoProfile" styleName="cameraDisplaySettingsWindowProfileComboStyle" + dataProvider="{_videoProfiles}" visible="false" change="updateCamera()" tabIndex="{baseIndex+2}" toolTip="{ResourceUtil.getInstance().getString('bbb.publishVideo.cmbResolution.tooltip')}" height="30" /> </mx:HBox> @@ -339,5 +234,5 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. label="{ResourceUtil.getInstance().getString('bbb.video.publish.closeBtn.label')}" accessibilityName="{ResourceUtil.getInstance().getString('bbb.video.publish.closeBtn.accessName')}"/> </mx:HBox> - </mx:VBox> + </mx:VBox> </mx:TitleWindow> \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestItem.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestItem.mxml new file mode 100644 index 0000000000000000000000000000000000000000..67f85a8d14f9a9466521d5e78c50f480edca8d14 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestItem.mxml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + BigBlueButton open source conferencing system - http://www.bigbluebutton.org + + Copyright (c) 2010 BigBlueButton Inc. and by respective authors (see below). + + 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 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/>. + + $Id: $ +--> + +<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" + verticalAlign="middle" width="100%" > + + <mx:Script> + <![CDATA[ + import mx.core.FlexGlobals; + import mx.managers.PopUpManager; + import com.asfusion.mate.events.Dispatcher; + import org.bigbluebutton.common.Images; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.managers.UserManager; + import org.bigbluebutton.main.events.ResponseModeratorEvent; + import org.bigbluebutton.main.events.ModuleLoadEvent; + import org.bigbluebutton.util.i18n.ResourceUtil; + import mx.containers.HBox; + import mx.controls.Button; + import mx.controls.Spacer; + import org.bigbluebutton.main.events.BBBEvent; + + private var dispatcher:Dispatcher = new Dispatcher(); + + [Bindable] private var username:String = ""; + private var userid:String; + + public function setUser(username:String, userid:String):void { + this.username = username; + this.userid = userid; + } + + private function onRejectUserBtnClick():void { + sendResponseToServer(false); + } + + private function onAcceptUserBtnClick():void { + sendResponseToServer(true); + } + + private function sendResponseToServer(accept:Boolean):void { + var respCommand:ResponseModeratorEvent = new ResponseModeratorEvent(ResponseModeratorEvent.RESPONSE); + respCommand.userid = userid; + respCommand.resp = accept; + dispatcher.dispatchEvent(respCommand); + } + + ]]> + </mx:Script> + + <mx:Label text="{username}" maxWidth="150" /> + <mx:Spacer width="100%" /> + <mx:Button styleName="denyButtonStyle" toolTip="{ResourceUtil.getInstance().getString('bbb.guests.denyBtn.toolTip')}" click="onRejectUserBtnClick()" /> + <mx:Button styleName="acceptButtonStyle" toolTip="{ResourceUtil.getInstance().getString('bbb.guests.allowBtn.toolTip')}" click="onAcceptUserBtnClick()" /> + +</mx:HBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestManagement.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestManagement.mxml new file mode 100644 index 0000000000000000000000000000000000000000..de1b3fc08cc0ed1d33b115e39aedf299ad7b4c13 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestManagement.mxml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="utf-8"?> +<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" + xmlns:mate="http://mate.asfusion.com/" + width="400" > + + + <mate:Listener type="{BBBEvent.RETRIEVE_GUEST_POLICY}" method="refreshGuestPolicy"/> + <mate:Listener type="{BBBEvent.SETTINGS_CONFIRMED}" method="onOkClicked"/> + <mate:Listener type="{BBBEvent.SETTINGS_CANCELLED}" method="onCancelClicked"/> + + <mx:Script> + <![CDATA[ + import mx.events.ItemClickEvent; + import mx.managers.PopUpManager; + + import org.bigbluebutton.common.Images; + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.util.i18n.ResourceUtil; + import org.bigbluebutton.common.events.SettingsComponentEvent; + import com.asfusion.mate.events.Dispatcher; + + private var option:Number = 0; + private var guestPolicy:String; + + private function refreshGuestPolicy(event:BBBEvent):void { + setGuestPolicy(event.payload.guestPolicy); + } + + public function setGuestPolicy(guestPolicy:String):void { + this.guestPolicy = guestPolicy; + + if(guestPolicy == "ASK_MODERATOR") { + guestManagement.selectedValue = 0; + option = 0; + } else if(guestPolicy == "ALWAYS_ACCEPT") { + guestManagement.selectedValue = 1; + option = 1; + } else { + guestManagement.selectedValue = 2; + option = 2; + } + } + + public function addToSettings():void { + var addGuestManagement:SettingsComponentEvent = new SettingsComponentEvent(SettingsComponentEvent.ADD); + addGuestManagement.component = this; + var dispatcher:Dispatcher = new Dispatcher(); + dispatcher.dispatchEvent(addGuestManagement); + } + + private function onCancelClicked(e:Event = null):void { + setGuestPolicy(guestPolicy); + } + + private function handleGuestClick(event:ItemClickEvent):void { + option = event.currentTarget.selectedValue; + } + + private function onOkClicked(e:Event = null):void { + var event:BBBEvent = new BBBEvent(BBBEvent.BROADCAST_GUEST_POLICY); + if(option == 0) { + event.payload['guestPolicy'] = "ASK_MODERATOR"; + dispatchEvent(event); + } + else if(option == 1) { + event.payload['guestPolicy'] = "ALWAYS_ACCEPT"; + dispatchEvent(event); + dispatchEvent(new BBBEvent(BBBEvent.ACCEPT_ALL_WAITING_GUESTS)); + } + else { + event.payload['guestPolicy'] = "ALWAYS_DENY"; + dispatchEvent(event); + dispatchEvent(new BBBEvent(BBBEvent.DENY_ALL_WAITING_GUESTS)); + } + LogUtil.debug("SENDING TO SERVER POLICY"); + } + + + ]]> + </mx:Script> + <mx:VBox width="30%"> + <mx:Label text="{ResourceUtil.getInstance().getString('bbb.guests.Management')}"/> + </mx:VBox> + <mx:Spacer width="30" /> + <mx:VBox width="70%"> + <mx:RadioButtonGroup id="guestManagement" itemClick="handleGuestClick(event);"/> + <mx:RadioButton groupName="guestManagement" + id="askModerator" + label="{ResourceUtil.getInstance().getString('bbb.guests.askModerator')}" + value="0" + /> + <mx:RadioButton groupName="guestManagement" + id="alwaysAccept" + label="{ResourceUtil.getInstance().getString('bbb.guests.alwaysAccept')}" + value="1" + /> + <mx:RadioButton groupName="guestManagement" + id="AlwaysDeny" + label="{ResourceUtil.getInstance().getString('bbb.guests.alwaysDeny')}" + value="2" + /> + </mx:VBox> +</mx:HBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestWindow.mxml new file mode 100755 index 0000000000000000000000000000000000000000..34b73aa891025ba9e2b9a0481656bc6bf277cabc --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/GuestWindow.mxml @@ -0,0 +1,144 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + BigBlueButton open source conferencing system - http://www.bigbluebutton.org + + Copyright (c) 2010 BigBlueButton Inc. and by respective authors (see below). + + 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 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/>. + + $Id: $ +--> + +<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" + title="{ResourceUtil.getInstance().getString('bbb.guests.title')}" showCloseButton="false" creationComplete="init()" + x="0" y="0" layout="vertical" width="320" horizontalAlign="center" + xmlns:mate="http://mate.asfusion.com/" > + + <mate:Listener type="{BBBEvent.ACCEPT_ALL_WAITING_GUESTS}" method="acceptAllWaitingGuests" /> + <mate:Listener type="{BBBEvent.DENY_ALL_WAITING_GUESTS}" method="denyAllWaitingGuests" /> + <mate:Listener type="{RemoveGuestFromViewEvent.REMOVE_GUEST}" receive="{remove(event.userid)}" /> + + <mx:Script> + <![CDATA[ + import mx.core.FlexGlobals; + import mx.managers.PopUpManager; + import com.asfusion.mate.events.Dispatcher; + import org.bigbluebutton.common.Images; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.managers.UserManager; + import org.bigbluebutton.main.events.ModuleLoadEvent; + import org.bigbluebutton.main.events.RemoveGuestEvent; + import org.bigbluebutton.main.events.ResponseModeratorEvent; + import org.bigbluebutton.main.events.RemoveGuestFromViewEvent; + import org.bigbluebutton.util.i18n.ResourceUtil; + import mx.containers.HBox; + import mx.controls.Button; + import mx.controls.Spacer; + import mx.events.CloseEvent; + import org.bigbluebutton.main.events.BBBEvent; + + private var guestButtons:Object = new Object(); + [Bindable] private var numberOfGuests:Number = 0; + private var dispatcher:Dispatcher = new Dispatcher(); + + public function init():void { + //Uncomment this line to make titlewindow undraggable + //this.isPopUp = false; + } + + public function refreshGuestView(listOfGuests:Object):void { + for (var userid:String in listOfGuests) { + if(guestButtons[userid] == null) { + var guestItem:GuestItem = new GuestItem(); + guestItem.setUser(listOfGuests[userid], userid); + guestListBox.addChild(guestItem); + guestButtons[userid] = guestItem; + numberOfGuests++; + } + } + this.visible = true; + } + + public function sendResponseToAllGuests(resp:Boolean):void { + removeAllGuests(); + var respCommand:ResponseModeratorEvent = new ResponseModeratorEvent(ResponseModeratorEvent.RESPONSE_ALL); + respCommand.resp = resp; + dispatcher.dispatchEvent(respCommand); + } + + public function sendResponseToAllGuestsCheckBox(resp:Boolean):void { + if(rememberCheckBox.selected) { + var event:BBBEvent = new BBBEvent(BBBEvent.BROADCAST_GUEST_POLICY); + if (resp) { + event.payload['guestPolicy'] = "ALWAYS_ACCEPT"; + } else { + event.payload['guestPolicy'] = "ALWAYS_DENY"; + } + dispatcher.dispatchEvent(event); + } + sendResponseToAllGuests(resp); + } + + public function acceptAllWaitingGuests(event:BBBEvent):void { + sendResponseToAllGuests(true); + } + + public function denyAllWaitingGuests(event:BBBEvent):void { + sendResponseToAllGuests(false); + } + + public function removeAllGuests():void { + var removeGuestEvent:RemoveGuestEvent = new RemoveGuestEvent(RemoveGuestEvent.REMOVE_ALL); + dispatcher.dispatchEvent(removeGuestEvent); + + closeWindow(); + } + + public function remove(userid:String):void { + if (guestButtons[userid] != null) { + numberOfGuests = numberOfGuests - 1; + guestListBox.removeChild(guestButtons[userid]); + delete guestButtons[userid]; + + var removeGuestEvent:RemoveGuestEvent = new RemoveGuestEvent(); + removeGuestEvent.userid = userid; + dispatcher.dispatchEvent(removeGuestEvent); + + if (!hasGuest()) { + closeWindow(); + } + } + } + + public function hasGuest():Boolean { + return numberOfGuests > 0; + } + + public function closeWindow():void { + this.visible = false; + PopUpManager.removePopUp(this); + dispatchEvent(new CloseEvent(CloseEvent.CLOSE)); + } + + ]]> + </mx:Script> + <mx:Label text="{numberOfGuests > 1? ResourceUtil.getInstance().getString('bbb.guests.message.plural', [String(numberOfGuests)]): ResourceUtil.getInstance().getString('bbb.guests.message.singular', [String(numberOfGuests)])}"/> + <mx:HRule width="100%"/> + <mx:Button id="allowEveryoneBtn" label="{ResourceUtil.getInstance().getString('bbb.guests.allowEveryoneBtn.text')}" width="70%" click="sendResponseToAllGuestsCheckBox(true)" toolTip="{allowEveryoneBtn.label}"/> + <mx:Button id="denyEveryoneBtn" label="{ResourceUtil.getInstance().getString('bbb.guests.denyEveryoneBtn.text')}" width="70%" click="sendResponseToAllGuestsCheckBox(false)" toolTip="{denyEveryoneBtn.label}"/> + <mx:CheckBox id="rememberCheckBox" label="{ResourceUtil.getInstance().getString('bbb.guests.rememberAction.text')}"/> + <mx:HRule width="100%"/> + <mx:VBox id="guestListBox" width="100%" height="100%" maxHeight="200" paddingLeft="10" paddingRight="10" paddingBottom="2" /> + +</mx:TitleWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LoadingBar.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LoadingBar.mxml index e67b6ca6add55d0702d50a0f92b4b4bcbc059d0b..7f81d31c85038723a9c1321a163e7544eb685366 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LoadingBar.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LoadingBar.mxml @@ -20,7 +20,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. --> -<mx:ProgressBar xmlns:mate="http://mate.asfusion.com/" xmlns:mx="http://www.adobe.com/2006/mxml" labelPlacement="bottom" minimum="0" maximum="100" mode="manual" > +<mx:ProgressBar xmlns:mate="http://mate.asfusion.com/" xmlns:mx="http://www.adobe.com/2006/mxml" labelPlacement="bottom" minimum="0" maximum="100" mode="manual" barColor="black" > <mate:Listener type="{ModuleLoadEvent.MODULE_LOADING_STARTED}" method="moduleLoadingStarted" /> <mate:Listener type="{ModuleLoadEvent.MODULE_LOAD_PROGRESS}" method="moduleLoadProgress" /> @@ -55,6 +55,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } this.label = ResourceUtil.getInstance().getString('bbb.mainshell.statusProgress.loading', [numModules]); + this.visible = true; } private function moduleLoadProgress(e:ModuleLoadEvent):void{ diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/LoggedOutWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/LoggedOutWindow.mxml index aa83dcdc0f0d94a1b237e4ca543cbea2c6dc6ab5..d74ceea141091c5e83f9c9c5edfc8af868eea407 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/LoggedOutWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/LoggedOutWindow.mxml @@ -27,6 +27,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mx:Script> <![CDATA[ import mx.core.FlexGlobals; + import mx.events.FlexEvent; import mx.managers.PopUpManager; import org.bigbluebutton.common.LogUtil; import org.bigbluebutton.core.BBB; @@ -34,6 +35,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.util.i18n.ResourceUtil; private static const LOG:String = "Main::LoggedOutWindow - "; + + [Bindable] private var DISPLAY_MESSAGE_ONLY_STATE:String = "displayMessageOnlyState"; + [Bindable] private var DISPLAY_RECONNECT_BUTTON_STATE:String = "displayReconnectButton"; [Bindable] private var message:String = "You have logged out of the conference"; private var urlLoader:URLLoader; @@ -70,7 +74,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. var request:URLRequest = new URLRequest(getLogoutUrl()); trace(LOG + "Logging out to: " + getLogoutUrl()); - navigateToURL(request, '_self'); + navigateToURL(request, '_parent'); PopUpManager.removePopUp(this); } @@ -83,29 +87,41 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. switch(reason){ case ConnectionFailedEvent.APP_SHUTDOWN: message = ResourceUtil.getInstance().getString('bbb.logout.appshutdown'); + setCurrentState(DISPLAY_MESSAGE_ONLY_STATE); break; case ConnectionFailedEvent.ASYNC_ERROR: message = ResourceUtil.getInstance().getString('bbb.logout.asyncerror'); + setCurrentState(DISPLAY_RECONNECT_BUTTON_STATE); break; case ConnectionFailedEvent.CONNECTION_CLOSED: message = ResourceUtil.getInstance().getString('bbb.logout.connectionclosed'); + setCurrentState(DISPLAY_RECONNECT_BUTTON_STATE); break; case ConnectionFailedEvent.CONNECTION_FAILED: message = ResourceUtil.getInstance().getString('bbb.logout.connectionfailed'); + setCurrentState(DISPLAY_RECONNECT_BUTTON_STATE); break; case ConnectionFailedEvent.CONNECTION_REJECTED: message = ResourceUtil.getInstance().getString('bbb.logout.rejected'); + setCurrentState(DISPLAY_MESSAGE_ONLY_STATE); break; case ConnectionFailedEvent.INVALID_APP: message = ResourceUtil.getInstance().getString('bbb.logout.invalidapp'); + setCurrentState(DISPLAY_MESSAGE_ONLY_STATE); break; case ConnectionFailedEvent.UNKNOWN_REASON: message = ResourceUtil.getInstance().getString('bbb.logout.unknown'); + setCurrentState(DISPLAY_RECONNECT_BUTTON_STATE); break; case ConnectionFailedEvent.USER_LOGGED_OUT: message = ResourceUtil.getInstance().getString('bbb.logout.usercommand'); + setCurrentState(DISPLAY_MESSAGE_ONLY_STATE); redirect(); // we know that the disconnect was requested so automatically redirect break; + case ConnectionFailedEvent.MODERATOR_DENIED_ME: + message = ResourceUtil.getInstance().getString('bbb.logout.guestkickedout'); + setCurrentState(DISPLAY_MESSAGE_ONLY_STATE); + break; } } @@ -114,11 +130,22 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } ]]> </mx:Script> - <mx:VBox width="100%" height="100%" horizontalAlign="center"> - <mx:Text text="{message}"/> + <mx:states> + <mx:State name="{DISPLAY_MESSAGE_ONLY_STATE}"> + <mx:SetProperty target="{this}" name="height" value="100%"/> + </mx:State> + <mx:State name="{DISPLAY_RECONNECT_BUTTON_STATE}"> + <mx:AddChild relativeTo="okBtn" position="after"> + <mx:VBox id="reconnectDialog" width="100%" height="100%" horizontalAlign="center"> + <mx:HRule width="100%"/> + <mx:Text width="380" textAlign="center" text="{ResourceUtil.getInstance().getString('bbb.logout.refresh.message')}" /> + <mx:Button id="reconnectBtn" label="{ResourceUtil.getInstance().getString('bbb.logout.refresh.label')}" click="reconnect()" /> + </mx:VBox> + </mx:AddChild> + </mx:State> + </mx:states> + <mx:VBox id="mainContainer" width="100%" height="100%" horizontalAlign="center"> + <mx:Text text="{message}" textAlign="center" width="100%"/> <mx:Button id="okBtn" label="{ResourceUtil.getInstance().getString('bbb.logout.button.label')}" click="redirect()"/> - <mx:HRule width="100%" /> - <mx:Text width="380" textAlign="center" text="{ResourceUtil.getInstance().getString('bbb.logout.refresh.message')}" /> - <mx:Button id="reconnectBtn" label="{ResourceUtil.getInstance().getString('bbb.logout.refresh.label')}" click="reconnect()" /> </mx:VBox> -</mx:TitleWindow> \ No newline at end of file +</mx:TitleWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml index 79ad1846c4d65fe58d47d3c40739a2cc621986ba..a557479b6e8e13a87e7837a22b41611e9e982ee3 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainApplicationShell.mxml @@ -28,8 +28,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:maps="org.bigbluebutton.main.maps.*" xmlns:api="org.bigbluebutton.main.api.*" width="100%" height="100%" + backgroundColor="white" styleName="mainVBox" creationComplete="initializeShell()" - xmlns:common="org.bigbluebutton.common.*"> + xmlns:common="org.bigbluebutton.common.*" + verticalGap="0"> <mate:Listener type="{OpenWindowEvent.OPEN_WINDOW_EVENT}" method="handleOpenWindowEvent" /> <mate:Listener type="{CloseWindowEvent.CLOSE_WINDOW_EVENT}" method="handleCloseWindowEvent"/> @@ -57,9 +59,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mate:Listener type="{BBBEvent.OPEN_WEBCAM_PREVIEW}" method="openVideoPreviewWindow" /> <mate:Listener type="{LockControlEvent.OPEN_LOCK_SETTINGS}" method="openLockSettingsWindow" /> <mate:Listener type="{InvalidAuthTokenEvent.INVALID_AUTH_TOKEN}" method="handleInvalidAuthToken" /> + + <mate:Listener type="{NetworkStatsEvent.OPEN_NETSTATS_WIN}" method="openNetworkStatsWindow" /> + <mate:Listener type="{BBBEvent.RETRIEVE_GUEST_POLICY}" method="setGuestPolicy"/> + <mate:Listener type="{ConnectionFailedEvent.MODERATOR_DENIED_ME}" method="handleLogout" /> + <mate:Listener type="{BBBEvent.MODERATOR_ALLOWED_ME_TO_JOIN}" method="guestAllowed" /> + <mate:Listener type="{RefreshGuestEvent.REFRESH_GUEST_VIEW}" method="refreshGuestView" /> + <mate:Listener type="{BBBEvent.REMOVE_GUEST_FROM_LIST}" method="removeGuestWindow" /> + <mate:Listener type="{BBBEvent.WAITING_FOR_MODERATOR_ACCEPTANCE}" method="openWaitWindow" /> + <mx:Script> - <![CDATA[ + <![CDATA[ import com.asfusion.mate.events.Dispatcher; import flash.events.MouseEvent; @@ -85,6 +96,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.common.events.OpenWindowEvent; import org.bigbluebutton.common.events.ToolbarButtonEvent; import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.UsersUtil; import org.bigbluebutton.core.events.LockControlEvent; import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.core.vo.LockSettingsVO; @@ -94,11 +106,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.main.events.ConfigEvent; import org.bigbluebutton.main.events.InvalidAuthTokenEvent; import org.bigbluebutton.main.events.LogoutEvent; + import org.bigbluebutton.main.events.NetworkStatsEvent; import org.bigbluebutton.main.events.MeetingNotFoundEvent; import org.bigbluebutton.main.events.ModuleLoadEvent; import org.bigbluebutton.main.events.PortTestEvent; + import org.bigbluebutton.main.events.RefreshGuestEvent; import org.bigbluebutton.main.events.ShortcutEvent; import org.bigbluebutton.main.events.SuccessfulLoginEvent; + import org.bigbluebutton.main.model.Guest; import org.bigbluebutton.main.model.LayoutOptions; import org.bigbluebutton.main.model.users.Conference; import org.bigbluebutton.main.model.users.events.ConnectionFailedEvent; @@ -109,18 +124,20 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.phone.events.WebRTCCallEvent; import org.bigbluebutton.modules.phone.events.WebRTCEchoTestEvent; import org.bigbluebutton.modules.phone.events.WebRTCMediaEvent; + import org.bigbluebutton.modules.phone.PhoneOptions; import org.bigbluebutton.modules.videoconf.events.ShareCameraRequestEvent; import org.bigbluebutton.util.i18n.ResourceUtil; - import org.bigbluebutton.util.logging.Logger; + import org.bigbluebutton.util.logging.Logger; private static const LOG:String = "Main::MainApplicationShell - "; private var globalDispatcher:Dispatcher; - private var dispState:String; //full-screen? private var images:Images = new Images(); private var stoppedModules:ArrayCollection; private var logs:Logger = new Logger(); private var logWindow:LogWindow; + private var waitWindow:WaitingWindow = null; + private var guestWindow:GuestWindow = null; private var scWindow:ShortcutHelpWindow; private var logoutWindow:LoggedOutWindow; private var connectionLostWindow:ConnectionLostWindow; @@ -136,10 +153,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. [Bindable] private var fullscreen_icon:Class = images.full_screen; [Bindable] private var logs_icon:Class = images.table; [Bindable] private var reset_layout_icon:Class = images.layout; - [Bindable] private var statsIcon:Class = images.bandwidth; private var receivedConfigLocaleVer:Boolean = false; private var receivedResourceLocaleVer:Boolean = false; + + [Bindable] private var copyrightText:String; + + private var _hideToolbarsTimer:Timer; public function get mode():String { return _mode; @@ -148,18 +168,31 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. [Bindable] private var layoutOptions:LayoutOptions; [Bindable] private var showToolbarOpt:Boolean = true; - [Bindable] private var toolbarHeight:Number = 32; - [Bindable] private var toolbarPaddingTop:Number = 4; + [Bindable] private var _showToolbar:Boolean = true; + private const DEFAULT_TOOLBAR_HEIGHT:Number = 32; + [Bindable] private var toolbarHeight:Number = DEFAULT_TOOLBAR_HEIGHT; + [Bindable] private var toolbarPaddingTop:Number = 2; [Bindable] private var showFooterOpt:Boolean = true; + [Bindable] private var _showFooter:Boolean = true; [Bindable] private var footerHeight:Number = 24; - [Bindable] private var globalHeight:Number = 0; [Bindable] private var isTunneling:Boolean = false; - + + private var guestManagement:GuestManagement = null; + private var guest:Guest = new Guest(); + private var guestPolicy:String = "ASK_MODERATOR"; + private var confirmingLogout:Boolean = false; private var chromeBrowser:ChromeWebcamPermissionImage = null; + private const THRESHOLD_AREA_SHOW_TOOLBARS:int = 5; + private const HIDE_TOOLBARS_EFFECT_DURATION:int = 500; + private const HIDE_TOOLBARS_DELAY:int = 1000; + public function initOptions(e:Event):void { + updateCopyrightText(); + loadBackground(); + UserManager.getInstance().getConference().configLockSettings(); layoutOptions = new LayoutOptions(); layoutOptions.parseOptions(); @@ -176,19 +209,134 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. langSelector.visible = locale.userSelectionEnabled if (!showFooterOpt) { - footerHeight = 0; - controlBar.visible = false; + controlBar.visible = controlBar.includeInLayout = false; } } + + private function updateCopyrightText():void { + if (BBB.initConfigManager().config.branding.copyright == undefined) { + copyrightText = ResourceUtil.getInstance().getString('bbb.mainshell.copyrightLabel2',[appVersion]); + } else { + copyrightText = String(BBB.initConfigManager().config.branding.copyright).replace("{0}", appVersion); + } + } protected function initializeShell():void { globalDispatcher = new Dispatcher(); + _hideToolbarsTimer = new Timer(HIDE_TOOLBARS_DELAY, 1); + _hideToolbarsTimer.addEventListener(TimerEvent.TIMER, onHideToolbarsTimerComplete); + + if (stage == null) { + addEventListener(Event.ADDED_TO_STAGE, initFullScreen); + } else { + initFullScreen(); + } + systemManager.addEventListener(Event.RESIZE, updateCopyrightLabelDimensions); copyrightLabel2.addEventListener(FlexEvent.UPDATE_COMPLETE, updateCopyrightLabelDimensions); updateCopyrightLabelDimensions(); } + private function setGuestPolicy(event:BBBEvent):void { + guestPolicy = event.payload.guestPolicy; + if(guestManagement == null) { + LogUtil.debug("ADD Guest Event"); + guestManagement = new GuestManagement(); + guestManagement.setGuestPolicy(guestPolicy); + guestManagement.addToSettings(); + } + } + + private function isFullScreen():Boolean { + return stage.displayState == StageDisplayState.FULL_SCREEN || + stage.displayState == StageDisplayState.FULL_SCREEN_INTERACTIVE; + } + + private function mouseMoveHandler(e:MouseEvent):void { + if (isFullScreen()) { + if (e.stageY <= THRESHOLD_AREA_SHOW_TOOLBARS || e.stageY > this.height - THRESHOLD_AREA_SHOW_TOOLBARS) { + showToolbars(); + } else if (e.stageY >= toolbar.height && e.stageY <= this.height - controlBar.height) { + hideToolbars(); + } + } else { + showToolbars(); + } + } + + private function set showToolbar(visible:Boolean):void { + if (!showToolbarOpt) return; + + if (visible) { + if (enlargeToolbar.isPlaying) return; + if (shrinkToolbar.isPlaying) shrinkToolbar.end(); + + enlargeToolbar.heightFrom = toolbar.height; + if (enlargeToolbar.heightFrom != enlargeToolbar.heightTo) { + _showToolbar = true; + enlargeToolbar.play([toolbar]); + } + } else { + if (shrinkToolbar.isPlaying) return; + if (enlargeToolbar.isPlaying) enlargeToolbar.end(); + + shrinkToolbar.heightFrom = toolbar.height; + if (shrinkToolbar.heightFrom != shrinkToolbar.heightTo) { + _showToolbar = true; + shrinkToolbar.play([toolbar]); + } + } + } + + + private function onToolbarEffectEnd():void { + _showToolbar = showToolbarOpt && (toolbar.height > 0); + } + + private function set showFooter(visible:Boolean):void { + if (!showFooterOpt) return; + + if (visible) { + if (enlargeControlBar.isPlaying) return; + if (shrinkControlBar.isPlaying) shrinkControlBar.end(); + + enlargeControlBar.heightFrom = controlBar.height; + if (enlargeControlBar.heightFrom != enlargeControlBar.heightTo) { + _showFooter = true; + enlargeControlBar.play([controlBar]); + } + } else { + if (shrinkControlBar.isPlaying) return; + if (enlargeControlBar.isPlaying) enlargeControlBar.end(); + + shrinkControlBar.heightFrom = controlBar.height; + if (shrinkControlBar.heightFrom != shrinkControlBar.heightTo) { + _showFooter = true; + shrinkControlBar.play([controlBar]); + + } + } + } + + private function onControlBarEffectEnd():void { + _showFooter = showFooterOpt && (controlBar.height > 0); + } + + private function onHideToolbarsTimerComplete(event:TimerEvent):void { + showToolbar = showFooter = false; + } + + private function hideToolbars():void { + _hideToolbarsTimer.reset(); + _hideToolbarsTimer.start(); + } + + private function showToolbars():void { + _hideToolbarsTimer.reset(); + showToolbar = showFooter = true; + } + private function updateCopyrightLabelDimensions(e:Event = null):void { var screenRect:Rectangle = systemManager.screen; @@ -197,15 +345,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } else { copyrightLabel2.width += Math.min(spacer.width, copyrightLabel2.measuredWidth - copyrightLabel2.width); } - - globalHeight = screenRect.height; } - protected function initFullScreen():void { - /* Set up full screen handler. */ + protected function initFullScreen(e:Event = null):void { stage.addEventListener(FullScreenEvent.FULL_SCREEN, fullScreenHandler); - dispState = stage.displayState; - } + stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler); + } private var sendStartModulesEvent:Boolean = true; @@ -214,10 +359,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. receivedConfigLocaleVer = true; appVersion = event.appVersion; localeVersion = event.localeVersion; - trace("Received locale version fron config.xml"); + trace("Received locale version from config.xml"); } else { receivedResourceLocaleVer = true; - trace("Received locale version fron locale file."); + trace("Received locale version from locale file."); } if (receivedConfigLocaleVer && receivedResourceLocaleVer) { @@ -237,17 +382,66 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. dispatcher.dispatchEvent(new ModuleLoadEvent(ModuleLoadEvent.START_ALL_MODULES)); } + public function guestAllowed(evt:BBBEvent):void { + progressBar.visible = true; + if (waitWindow != null) { + PopUpManager.removePopUp(waitWindow); + waitWindow = null; + } + } + private function fullScreenHandler(evt:FullScreenEvent):void { - dispState = stage.displayState + " (fullScreen=" + evt.fullScreen.toString() + ")"; if (evt.fullScreen) { LogUtil.debug("Switching to full screen"); - /* Do something specific here if we switched to full screen mode. */ - + fullscreen_icon = images.exit_full_screen; + hideToolbars(); } else { LogUtil.debug("Switching to normal screen"); - /* Do something specific here if we switched to normal mode. */ + fullscreen_icon = images.full_screen; + showToolbars(); } } + + private function closeGuestWindow(e:Event):void { + if(guestWindow != null) { + guestWindow.closeWindow(); + guestWindow = null; + } + } + + private function refreshGuestView(evt:RefreshGuestEvent):void { + // do not show the guest window if the user isn't moderator or if he's moderator but is still waiting for acceptance + if ( ! ( + UsersUtil.amIModerator() + && UserManager.getInstance().getConference().getMyUser() != null + && !UserManager.getInstance().getConference().getMyUser().waitingForAcceptance) ) { + return; + } + + if (guestWindow == null) { + guestWindow = PopUpManager.createPopUp( mdiCanvas, GuestWindow, false) as GuestWindow; + guestWindow.addEventListener(Event.CLOSE, closeGuestWindow); + + guestWindow.x = systemManager.screen.width - guestWindow.width - 20; + guestWindow.y = 20; + } + guestWindow.refreshGuestView(evt.listOfGuests); + } + + public function removeGuestWindow(evt:BBBEvent):void { + if (guestWindow != null) { + guestWindow.remove(evt.payload.userId); + } + } + + private function openWaitWindow(evt:BBBEvent):void { + progressBar.visible = false; + waitWindow = PopUpManager.createPopUp( mdiCanvas, WaitingWindow, false) as WaitingWindow; + + // Calculate position of TitleWindow in Application's coordinates. + waitWindow.x = (systemManager.screen.width - waitWindow.width) / 2; + waitWindow.y = (systemManager.screen.height - waitWindow.height) / 2; + } private function openLogWindow():void { if (logWindow == null){ @@ -280,18 +474,19 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function toggleFullScreen():void{ - LogUtil.debug("Toggling fullscreen"); - try { + LogUtil.debug("Toggling fullscreen, current mode: " + stage.displayState); + try { switch (stage.displayState) { case StageDisplayState.FULL_SCREEN: - LogUtil.debug("full screen mode"); + case StageDisplayState.FULL_SCREEN_INTERACTIVE: + LogUtil.debug("Full screen mode, switching to normal screen mode"); // If already in full screen mode, switch to normal mode. stage.displayState = StageDisplayState.NORMAL; break; default: - LogUtil.debug("Normal screen mode"); + LogUtil.debug("Normal screen mode, switching to full screen mode"); // If not in full screen mode, switch to full screen mode. - stage.displayState = StageDisplayState.FULL_SCREEN; + stage.displayState = StageDisplayState.FULL_SCREEN_INTERACTIVE; break; } } catch (err:SecurityError) { @@ -401,7 +596,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function openVideoPreviewWindow(event:BBBEvent):void { var camSettings:CameraDisplaySettings = CameraDisplaySettings(PopUpManager.createPopUp(mdiCanvas, CameraDisplaySettings, true)); - camSettings.resolutions = (event.payload.resolutions as String).split(","); + camSettings.defaultCamera = event.payload.defaultCamera; + camSettings.camerasArray = event.payload.camerasArray; camSettings.publishInClient = event.payload.publishInClient; camSettings.chromePermissionDenied = event.payload.chromePermissionDenied; @@ -457,6 +653,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function handleWebRTCMediaRequestEvent(event:WebRTCMediaEvent):void { + var options:PhoneOptions = new PhoneOptions(); + if (!options.showMicrophoneHint) { + return; + } + var browser:String = ExternalInterface.call("determineBrowser")[0]; if (browser == "Firefox") { var ffBrowser:FirefoxMicPermissionImage = PopUpManager.createPopUp(mdiCanvas, FirefoxMicPermissionImage, true) as FirefoxMicPermissionImage; @@ -546,13 +747,23 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. return logoutUrl; } - private function handleLogout(e:ConnectionFailedEvent):void { + private function handleLogout(e:ConnectionFailedEvent):void { + if(progressBar != null) + progressBar.visible = false; + + if(waitWindow != null) + waitWindow.removeWindow(); + + if(guestWindow != null) { + guestWindow.closeWindow(); + } + if (e is ConnectionFailedEvent) { showlogoutWindow((e as ConnectionFailedEvent).type); } else showlogoutWindow("You have logged out of the conference"); - } - + } + private function redirectToLogoutUrl ():void { var logoutURL:String = getLogoutUrl(); var request:URLRequest = new URLRequest(logoutURL); @@ -606,24 +817,66 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. addedBtns.removeChild(event.button as UIComponent); } - private var networkStatsWindow:NetworkStatsWindow = new NetworkStatsWindow(); + private var networkStatsWindow:NetworkStatsWindow = null; private function openNetworkStatsWindow(e:Event = null):void { - /*var btnPosition:Point = new Point(btnNetwork.x, btnNetwork.y); - var btnPositionOnGlobal:Point = btnNetwork.localToGlobal(btnPosition); - var windowPosition:Point = networkStatsWindow.globalToLocal(btnPositionOnGlobal); - - windowPosition.x += btnNetwork.width + 10; - windowPosition.y -= networkStatsWindow.height - 10; + if (networkStatsWindow == null) { + networkStatsWindow = new NetworkStatsWindow(); + } - networkStatsWindow.x = windowPosition.x; - networkStatsWindow.y = windowPosition.y;*/ + mdiCanvas.windowManager.add(networkStatsWindow); + mdiCanvas.windowManager.absPos(networkStatsWindow, mdiCanvas.width - networkStatsWindow.width, 0); + mdiCanvas.windowManager.bringToFront(networkStatsWindow); + } - networkStatsWindow.appear(); + private function updateToolbarHeight():void { + if (toolbarHeight != 0) { + toolbarHeight = Math.max(DEFAULT_TOOLBAR_HEIGHT, toolbar.logo.height + 10); + } } - - private function closeNetworkStatsWindow(e:Event = null):void { - networkStatsWindow.disappear(); + + private function onCanvasResize():void { + var canvasAspectRatio:Number = mdiCanvas.width / mdiCanvas.height; + + if (canvasAspectRatio >= canvasBackgroundAspectRatio) { + canvasBackground.width = Math.max(mdiCanvas.width, canvasBackgroundOriginalWidth); + canvasBackground.height = Math.ceil(canvasBackground.width / canvasBackgroundAspectRatio); + } else { + canvasBackground.height = Math.max(mdiCanvas.height, canvasBackgroundOriginalHeight); + canvasBackground.width = Math.ceil(canvasBackground.height * canvasBackgroundAspectRatio); + } + } + + private var canvasBackgroundAspectRatio:Number = NaN; + private var canvasBackgroundOriginalWidth:Number; + private var canvasBackgroundOriginalHeight:Number; + + private function onCanvasBackgroundLoaded():void { + if (isNaN(canvasBackgroundAspectRatio)) { + var candidate:Number = canvasBackground.width / canvasBackground.height; + + if (isNaN(candidate)) + return; + + canvasBackgroundAspectRatio = candidate; + canvasBackgroundOriginalWidth = canvasBackground.width; + canvasBackgroundOriginalHeight = canvasBackground.height; + + onCanvasResize(); + } + } + + private function loadBackground():void { + var backgroundCandidate:String = BBB.initConfigManager().config.branding.background; + if (backgroundCandidate == "") { + hideBackground(); + } else { + canvasBackground.source = backgroundCandidate; + } + } + + private function hideBackground():void { + canvasBackground.visible = canvasBackground.includeInLayout = false; } private function openLockSettingsWindow(event:LockControlEvent):void { @@ -656,28 +909,38 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. ]]> </mx:Script> + <mx:Resize id="shrinkControlBar" duration="{HIDE_TOOLBARS_EFFECT_DURATION}" heightFrom="{footerHeight}" heightTo="0" effectEnd="onControlBarEffectEnd()" /> + <mx:Resize id="enlargeControlBar" duration="{HIDE_TOOLBARS_EFFECT_DURATION}" heightFrom="0" heightTo="{footerHeight}" effectEnd="onControlBarEffectEnd()" /> + <mx:Resize id="shrinkToolbar" target="{toolbar}" duration="{HIDE_TOOLBARS_EFFECT_DURATION}" heightFrom="{toolbarHeight}" heightTo="0" effectEnd="onToolbarEffectEnd()" /> + <mx:Resize id="enlargeToolbar" duration="{HIDE_TOOLBARS_EFFECT_DURATION}" heightFrom="0" heightTo="{toolbarHeight}" effectEnd="onToolbarEffectEnd()" /> + <views:MainToolbar id="toolbar" dock="true" width="100%" height="{toolbarHeight}" - visible="{showToolbarOpt}" + visible="{_showToolbar}" + includeInLayout="{showToolbarOpt}" verticalAlign="middle" toolbarOptions="{layoutOptions}" - paddingTop="{toolbarPaddingTop}"/> + paddingTop="{toolbarPaddingTop}" + updateComplete="updateToolbarHeight()" + paddingBottom="0" /> <views:MainCanvas id="mdiCanvas" horizontalScrollPolicy="off" verticalScrollPolicy="off" effectsLib="{flexlib.mdi.effects.effectsLib.MDIVistaEffects}" width="100%" - height="{globalHeight - footerHeight - toolbarHeight - 12}"> + height="100%" + resize="onCanvasResize()"> + <mx:Image id="canvasBackground" updateComplete="onCanvasBackgroundLoaded()" ioError="hideBackground()" /> <views:LoadingBar id="progressBar" horizontalCenter="0" verticalCenter="0" width="50%" /> <views:BrandingLogo x="{this.width - 300}" y="{this.height - 300}" /> </views:MainCanvas> - <mx:ControlBar width="100%" height="{footerHeight}" paddingTop="0" id="controlBar"> + <mx:ControlBar width="100%" height="{footerHeight}" id="controlBar" paddingTop="2" paddingBottom="2" verticalAlign="middle" visible="{_showFooter}" includeInLayout="{showFooterOpt}" > <mx:Label - htmlText="{ResourceUtil.getInstance().getString('bbb.mainshell.copyrightLabel2',[appVersion])}" + htmlText="{copyrightText}" link="onFooterLinkClicked(event)" id="copyrightLabel2" truncateToFit="true" selectable="true" paddingRight="10"/> @@ -713,5 +976,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. click="openLogWindow()" tabIndex="{baseIndex+1}"/> <mx:HBox id="addedBtns" /> + <mx:Button width="20" height="20" toolTip="{ResourceUtil.getInstance().getString('bbb.mainshell.fullscreenBtn.toolTip')}" id="fullScreen" icon="{fullscreen_icon}" click="toggleFullScreen()" /> </mx:ControlBar> </mx:VBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml index 88f03f535171eb9bc6ba519a97fae25b481b5c5c..399a8cb4053cb88e444d0f3776c6223f9952be6b 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MainToolbar.mxml @@ -37,29 +37,44 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mate:Listener type="{ShortcutEvent.FOCUS_SHORTCUT_BUTTON}" method="focusShortcutButton" /> <mate:Listener type="{ShortcutEvent.FOCUS_LOGOUT_BUTTON}" method="focusLogoutButton" /> <mate:Listener type="{ConferenceCreatedEvent.CONFERENCE_CREATED_EVENT}" method="retrieveMeetingName" /> + <mate:Listener type="{ConnectionFailedEvent.MODERATOR_DENIED_ME}" method="hideToolbar" /> + <mate:Listener type="{SettingsComponentEvent.ADD}" method="addSettingsComponent" /> + <mate:Listener type="{SettingsComponentEvent.REMOVE}" method="removeSettingsComponent"/> <mate:Listener type="{BBBEvent.CHANGE_RECORDING_STATUS}" method="onRecordingStatusChanged" /> + <mate:Listener type="{SuccessfulLoginEvent.USER_LOGGED_IN}" method="refreshModeratorButtonsVisibility" /> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="refreshRole" /> <mx:Script> <![CDATA[ import com.asfusion.mate.events.Dispatcher; import mx.accessibility.AlertAccImpl; + import mx.binding.utils.BindingUtils; import mx.controls.Alert; + import mx.core.IToolTip; import mx.core.UIComponent; - import mx.events.CloseEvent; + import mx.events.CloseEvent; + import mx.events.ToolTipEvent; + import mx.managers.PopUpManager; import org.bigbluebutton.common.IBbbToolbarComponent; import org.bigbluebutton.common.LogUtil; import org.bigbluebutton.common.events.CloseWindowEvent; import org.bigbluebutton.common.events.OpenWindowEvent; import org.bigbluebutton.common.events.ToolbarButtonEvent; + import org.bigbluebutton.common.events.SettingsComponentEvent; import org.bigbluebutton.core.BBB; import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.core.services.BandwidthMonitor; + import org.bigbluebutton.core.UsersUtil; import org.bigbluebutton.main.events.BBBEvent; import org.bigbluebutton.main.events.ConfigEvent; import org.bigbluebutton.main.events.LogoutEvent; + import org.bigbluebutton.main.events.NetworkStatsEvent; import org.bigbluebutton.main.events.SettingsEvent; import org.bigbluebutton.main.events.ShortcutEvent; + import org.bigbluebutton.main.events.SuccessfulLoginEvent; import org.bigbluebutton.main.model.LayoutOptions; + import org.bigbluebutton.main.model.NetworkStatsData; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; import org.bigbluebutton.main.model.users.events.ConferenceCreatedEvent; import org.bigbluebutton.main.model.users.events.ConnectionFailedEvent; import org.bigbluebutton.util.i18n.ResourceUtil; @@ -71,10 +86,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. [Bindable] private var showHelpBtn:Boolean = false; [Bindable] private var showToolbar:Boolean = false; + [Bindable] private var showGuestSettingsButton:Boolean = false; + [Bindable] private var showRecordButton:Boolean = false; [Bindable] public var toolbarOptions:LayoutOptions = new LayoutOptions(); [Bindable] private var baseIndex:int; [Bindable] private var numButtons:int; + [Bindable] private var _bandwidthConsumedUp:String = "-"; + [Bindable] private var _bandwidthConsumedDown:String = "-"; + private var _updateBandwidthTimer:Timer = new Timer(1000); + private var _bandwidthToolTip:IToolTip; + /* * Because of the de-centralized way buttons are added to the toolbar, there is a large gap between the tab indexes of the main buttons * on the left and the tab indexes of the "other" items on the right (shortcut glossary, language slector, etc). This will make it more @@ -84,6 +106,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. */ private var xml:XML; + private var settingsComponents:Array = new Array(); + private var settingsPopup:BBBSettings = null; private function init():void{ if (Capabilities.hasAccessibility) { @@ -98,6 +122,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. timer.addEventListener(TimerEvent.TIMER, checkAccessiblity); timer.start(); + BindingUtils.bindSetter(refreshModeratorButtonsVisibility, UserManager.getInstance().getConference(), "record"); } private function onCreationComplete():void { @@ -151,6 +176,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } else { showHelpBtn = false; } + if(toolbarOptions.showNetworkMonitor) { + initBandwidthToolTip(); + } } private function retrieveMeetingName(e:ConferenceCreatedEvent):void { @@ -161,6 +189,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } } } + + private function refreshModeratorButtonsVisibility(e:*):void { + showGuestSettingsButton = UsersUtil.amIModerator() + && UserManager.getInstance().getConference().getMyUser() != null + && !UserManager.getInstance().getConference().getMyUser().waitingForAcceptance; + + showRecordButton = showGuestSettingsButton && UserManager.getInstance().getConference().record; + } public function addButton(name:String):Button{ var btn:Button = new Button(); @@ -277,6 +313,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function gotConfigParameters(e:ConfigEvent):void{ shortcutKeysBtn.includeInLayout = shortcutKeysBtn.visible = e.config.shortcutKeysShowButton; DEFAULT_HELP_URL = e.config.helpURL; + + if (e.config.logo == "") { + hideLogo(); + } else { + logo.source = e.config.logo; + } } private function onDisconnectTest():void{ @@ -334,9 +376,66 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } } + private function onNetStatsButtonClick(e:Event = null):void { + var d:Dispatcher = new Dispatcher(); + d.dispatchEvent(new NetworkStatsEvent(NetworkStatsEvent.OPEN_NETSTATS_WIN)); + } + + private function initBandwidthToolTip():void { + _updateBandwidthTimer.addEventListener(TimerEvent.TIMER, updateBandwidthTimerHandler); + _updateBandwidthTimer.start(); + btnNetwork.addEventListener(ToolTipEvent.TOOL_TIP_SHOW, bwToolTipShowHandler); + btnNetwork.addEventListener(ToolTipEvent.TOOL_TIP_END, bwToolTipEndHandler); + } + + private function bwToolTipShowHandler(e:ToolTipEvent):void { + // The ToolTip must be stored so it's text can be updated + _bandwidthToolTip = e.toolTip; + updateBwToolTip(); + } + + private function bwToolTipEndHandler(e:ToolTipEvent):void { + _bandwidthToolTip = null; + } + + private function updateBandwidthTimerHandler(e:TimerEvent):void { + _bandwidthConsumedDown = NetworkStatsData.getInstance().formattedCurrentConsumedDownBW; + _bandwidthConsumedUp = NetworkStatsData.getInstance().formattedCurrentConsumedUpBW; + updateBwToolTip(); + } + + private function updateBwToolTip():void { + if(_bandwidthToolTip) { + _bandwidthToolTip.text = ResourceUtil.getInstance().getString('bbb.bwmonitor.upload.short') + ": " + _bandwidthConsumedUp + + " | " + ResourceUtil.getInstance().getString('bbb.bwmonitor.download.short')+": "+_bandwidthConsumedDown; + } + } + + private function hideLogo():void { + logoHolder.visible = logoHolder.includeInLayout = false; + } + + private function addSettingsComponent(e:SettingsComponentEvent = null):void { + settingsComponents.push(e.component); + } + + private function removeSettingsComponent(e:SettingsComponentEvent = null):void { + throw("Not implemented"); + } + + private function onSettingsButtonClick():void { + settingsPopup = BBBSettings(PopUpManager.createPopUp(this.parent, BBBSettings, true)); + settingsPopup.pushComponents(settingsComponents); + } + + private function refreshRole(e:ChangeMyRole):void { + refreshModeratorButtonsVisibility(null); + } ]]> </mx:Script> - + <mx:HBox id="logoHolder" paddingRight="12" > + <mx:Image id="logo" ioError="hideLogo()" /> + </mx:HBox> <mx:HBox id="quickLinks" width="1" includeInLayout="false"> <mx:LinkButton id="usersLinkBtn" click="onQuickLinkClicked('users')" label="{ResourceUtil.getInstance().getString('bbb.users.quickLink.label')}" accessibilityDescription="{usersLinkBtn.label}" toolTip="{usersLinkBtn.label}" @@ -352,16 +451,31 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. tabIndex="{baseIndex+4}" height="22" styleName="quickWindowLinkStyle" /> </mx:HBox> <mx:HBox id="addedBtns"/> - <views:RecordButton id="recordBtn" tabIndex="{baseIndex+numButtons+10}" /> + <views:RecordButton id="recordBtn" visible="{showRecordButton}" includeInLayout="{showRecordButton}" tabIndex="{baseIndex+numButtons+10}" /> <mx:VRule strokeWidth="2" height="100%" visible="{muteMeBtn.visible}" includeInLayout="{muteMeBtn.includeInLayout}"/> <views:MuteMeButton id="muteMeBtn" height="20" tabIndex="{baseIndex+numButtons+11}"/> <mx:Spacer width="50%"/> <mx:Label id="meetingNameLbl" styleName="meetingNameLabelStyle" /> <mx:Spacer width="50%"/> + <mx:Button + id="bbbSettings" + visible="{showGuestSettingsButton}" + includeInLayout="{showGuestSettingsButton}" + toolTip="{ResourceUtil.getInstance().getString('bbb.settings.btn.toolTip')}" + click="onSettingsButtonClick()" + styleName="settingsButtonStyle" + height="22" + /> <!-- <mx:Button label="DISCONNECT!" click="BBB.initConnectionManager().forceClose()" height="22" toolTip="Click to simulate disconnection" /> --> + <mx:Button + id="btnNetwork" + styleName="bandwidthButtonStyle" + toolTip="dummy text" + click="onNetStatsButtonClick()" + visible="{toolbarOptions.showNetworkMonitor}" /> <mx:Button id="shortcutKeysBtn" label="{ResourceUtil.getInstance().getString('bbb.mainToolbar.shortcutBtn')}" styleName="shortcutButtonStyle" click="onShortcutButtonClick()" height="22" toolTip="{ResourceUtil.getInstance().getString('bbb.mainToolbar.shortcutBtn.toolTip')}" diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/MuteMeButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/MuteMeButton.mxml index 944f038226e4c0f88cc75b3ab95efd14e112e0ca..e39d9788dc23444a7ee08cd57c7789e44261f0af 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/MuteMeButton.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/MuteMeButton.mxml @@ -38,6 +38,7 @@ $Id: $ <mate:Listener type="{ShortcutEvent.MUTE_ME_EVENT}" method="toggleMuteMeState" /> <mate:Listener type="{EventConstants.USER_TALKING}" method="handleUserTalking" /> <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="lockSettingsChanged" /> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="refreshRole" /> <mate:Listener type="{BBBEvent.USER_VOICE_JOINED}" method="handleJoinedVoiceConferenceEvent" /> <mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleLeftVoiceConferenceEvent" /> <mate:Listener type="{BBBEvent.USER_VOICE_MUTED}" method="handleVoiceMutedEvent" /> @@ -59,6 +60,7 @@ $Id: $ import org.bigbluebutton.main.events.ShortcutEvent; import org.bigbluebutton.main.model.users.BBBUser; import org.bigbluebutton.main.model.users.Conference; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; import org.bigbluebutton.util.i18n.ResourceUtil; private static const LOG:String = "Main::View::MuteMeButton - "; @@ -85,6 +87,10 @@ $Id: $ private function localeChanged(e:Event):void { updateMuteMeBtn(); } + + private function refreshRole(e:ChangeMyRole):void { + updateMuteMeBtn(); + } private function lockSettingsChanged(e:Event):void { if (UsersUtil.amIModerator() || UsersUtil.amIPresenter()){ @@ -115,7 +121,7 @@ $Id: $ var conference:Conference = userManager.getConference(); var me:BBBUser = conference.getMyUser(); // trace(LOG + "allowMuteUnmute [ voiceJoined=[" + conference.voiceJoined + "], disableMic=[" + me.disableMyMic + "]"); - return (conference.voiceJoined && !me.disableMyMic); + return (conference.voiceJoined && (!me.disableMyMic || me.moderator)); } private function updateMuteMeBtn(placeholder:Boolean = false):void { diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/NetworkStatsWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/NetworkStatsWindow.mxml index 8eeb5600c88b558d5cb8f281a2212dc562968d5e..4972a9e09f8f9fd3bf195f353cc724d5569b35a8 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/NetworkStatsWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/NetworkStatsWindow.mxml @@ -23,14 +23,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <CustomMdiWindow xmlns="org.bigbluebutton.common.*" xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:mate="http://mate.asfusion.com/" - title="Network monitor" + title="{ResourceUtil.getInstance().getString('bbb.bwmonitor.title')}" creationComplete="onCreationComplete()" resizable="false" - showCloseButton="false" + showCloseButton="true" implements="org.bigbluebutton.common.IBbbModuleWindow" - width="210" height="261" minHeight="0" minWidth="0" + width="210" minWidth="0" resize="onResize()"> - <mx:Script> <![CDATA[ @@ -40,41 +39,44 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.common.events.CloseWindowEvent; import org.bigbluebutton.common.events.OpenWindowEvent; + import org.bigbluebutton.common.Images; import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.services.BandwidthMonitor; import org.bigbluebutton.main.model.NetworkStatsData; + import org.bigbluebutton.util.i18n.ResourceUtil; + import mx.core.IFlexDisplayObject; import mx.effects.Fade; import mx.events.EffectEvent; - import mx.formatters.NumberFormatter; + import mx.managers.PopUpManager; private var _globalDispatcher:Dispatcher = new Dispatcher(); private var _updateTimer:Timer = new Timer(1000); - private var _numberFormatter:NumberFormatter = new NumberFormatter(); + + private var _images:Images = new Images(); + [Bindable] private var _refreshIcon:Class = _images.refreshSmall; private function onCreationComplete():void { this.windowControls.maximizeRestoreBtn.visible = false; this.windowControls.minimizeBtn.visible = false; - this.x = parent.width - this.width; - this.y = parent.height - this.height; - - _numberFormatter.precision = 2; - _numberFormatter.useThousandsSeparator = true; - _updateTimer.addEventListener(TimerEvent.TIMER, timerHandler); _updateTimer.start(); + + height = panel.measuredHeight + borderMetrics.top + borderMetrics.bottom; + runBandwidthMeasurement(); } private function timerHandler(e:TimerEvent):void { - labelCurrentDownload.text = _numberFormatter.format(NetworkStatsData.getInstance().currentConsumedDownBW); - labelTotalDownload.text = _numberFormatter.format(NetworkStatsData.getInstance().totalConsumedDownBW); - labelAvailableDownload.text = _numberFormatter.format(NetworkStatsData.getInstance().measuredDownBW); - labelDownloadLatency.text = String(NetworkStatsData.getInstance().measuredDownLatency); + labelCurrentDownload.text = NetworkStatsData.getInstance().formattedCurrentConsumedDownBW; + labelTotalDownload.text = NetworkStatsData.getInstance().formattedTotalConsumedDownBW; + labelAvailableDownload.text = NetworkStatsData.getInstance().formattedMeasuredDownBW; + labelDownloadLatency.text = NetworkStatsData.getInstance().formattedMeasuredDownLatency; - labelCurrentUpload.text = _numberFormatter.format(NetworkStatsData.getInstance().currentConsumedUpBW); - labelTotalUpload.text = _numberFormatter.format(NetworkStatsData.getInstance().totalConsumedUpBW); - labelAvailableUpload.text = _numberFormatter.format(NetworkStatsData.getInstance().measuredUpBW); - labelUploadLatency.text = String(NetworkStatsData.getInstance().measuredUpLatency); + labelCurrentUpload.text = NetworkStatsData.getInstance().formattedCurrentConsumedUpBW; + labelTotalUpload.text = NetworkStatsData.getInstance().formattedTotalConsumedUpBW; + labelAvailableUpload.text = NetworkStatsData.getInstance().formattedMeasuredUpBW; + labelUploadLatency.text = NetworkStatsData.getInstance().formattedMeasuredUpLatency; } public function getPrefferedPosition():String { @@ -82,57 +84,32 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function onResize():void { - LogUtil.debug("width=" + width + " height=" + height); - } - - public function appear():void { - var fader:Fade = new Fade(); - fader.alphaFrom = 0; - fader.alphaTo = 1; - fader.duration = 500; - fader.target = this; -// fader.addEventListener(EffectEvent.EFFECT_START, function(e:EffectEvent):void { -// var windowEvent:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); -// windowEvent.window = e.currentTarget as IBbbModuleWindow; -// _globalDispatcher.dispatchEvent(windowEvent); -// }); - fader.play(); - var windowEvent:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); - windowEvent.window = this; - _globalDispatcher.dispatchEvent(windowEvent); - this.windowManager.bringToFront(this); + //LogUtil.debug("width=" + width + " height=" + height); } - public function disappear():void { - var fader:Fade = new Fade(); - fader.alphaFrom = 1; - fader.alphaTo = 0; - fader.duration = 500; - fader.target = this; - fader.addEventListener(EffectEvent.EFFECT_END, function(e:EffectEvent):void { - var windowEvent:CloseWindowEvent = new CloseWindowEvent(CloseWindowEvent.CLOSE_WINDOW_EVENT); - windowEvent.window = e.target as IBbbModuleWindow; - _globalDispatcher.dispatchEvent(windowEvent); - }); - fader.play(); + private function runBandwidthMeasurement():void { + BandwidthMonitor.getInstance().checkClientToServer(); + BandwidthMonitor.getInstance().checkServerToClient(); } + ]]> </mx:Script> - <mx:Panel width="100%" height="100%" + <mx:Panel id="panel" width="100%" paddingTop="10" paddingLeft="10" paddingRight="10" paddingBottom="10" headerHeight="10"> <mx:VBox verticalGap="0" width="100%" height="100%"> - <mx:HBox backgroundColor="haloOrange" width="100%" horizontalAlign="center"><mx:Label fontWeight="bold" text="Upload"/></mx:HBox> - <mx:HBox horizontalGap="0"><mx:Label text="Total: "/><mx:Label id="labelTotalUpload" fontWeight="bold" text="-"/><mx:Label text="MB"/></mx:HBox> - <mx:HBox horizontalGap="0"><mx:Label text="Current: "/><mx:Label id="labelCurrentUpload" fontWeight="bold" text="-"/><mx:Label text="Kb/s"/></mx:HBox> - <mx:HBox horizontalGap="0"><mx:Label text="Available: "/><mx:Label id="labelAvailableUpload" fontWeight="bold" text="-"/><mx:Label text="Mb/s"/></mx:HBox> - <mx:HBox horizontalGap="0"><mx:Label text="Latency: "/><mx:Label id="labelUploadLatency" fontWeight="bold" text="-"/><mx:Label text="ms"/></mx:HBox> - <mx:HBox backgroundColor="haloOrange" width="100%" horizontalAlign="center"><mx:Label fontWeight="bold" text="Download"/></mx:HBox> - <mx:HBox horizontalGap="0"><mx:Label text="Total: "/><mx:Label id="labelTotalDownload" fontWeight="bold" text="-"/><mx:Label text="MB"/></mx:HBox> - <mx:HBox horizontalGap="0"><mx:Label text="Current: "/><mx:Label id="labelCurrentDownload" fontWeight="bold" text="-"/><mx:Label text="Kb/s"/></mx:HBox> - <mx:HBox horizontalGap="0"><mx:Label text="Available: "/><mx:Label id="labelAvailableDownload" fontWeight="bold" text="-"/><mx:Label text="Mb/s"/></mx:HBox> - <mx:HBox horizontalGap="0"><mx:Label text="Latency: "/><mx:Label id="labelDownloadLatency" fontWeight="bold" text="-"/><mx:Label text="ms"/></mx:HBox> + <mx:HBox backgroundColor="haloOrange" width="100%" horizontalAlign="center"><mx:Label fontWeight="bold" text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.upload')}"/></mx:HBox> + <mx:HBox visible="false" includeInLayout="false" horizontalGap="0"><mx:Label text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.total')}: "/><mx:Label id="labelTotalUpload" fontWeight="bold" text="-"/></mx:HBox> + <mx:HBox horizontalGap="0"><mx:Label text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.current')}: "/><mx:Label id="labelCurrentUpload" fontWeight="bold" text="-"/></mx:HBox> + <mx:HBox visible="false" includeInLayout="false" horizontalGap="0"><mx:Label text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.available')}: "/><mx:Label id="labelAvailableUpload" fontWeight="bold" text="-"/></mx:HBox> + <mx:HBox horizontalGap="0"><mx:Label text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.latency')}: "/><mx:Label id="labelUploadLatency" fontWeight="bold" text="-"/></mx:HBox> + <mx:HBox backgroundColor="haloOrange" width="100%" horizontalAlign="center"><mx:Label fontWeight="bold" text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.download')}"/></mx:HBox> + <mx:HBox visible="false" includeInLayout="false" horizontalGap="0"><mx:Label text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.total')}: "/><mx:Label id="labelTotalDownload" fontWeight="bold" text="-"/></mx:HBox> + <mx:HBox horizontalGap="0"><mx:Label text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.current')}: "/><mx:Label id="labelCurrentDownload" fontWeight="bold" text="-"/></mx:HBox> + <mx:HBox visible="false" includeInLayout="false" horizontalGap="0"><mx:Label text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.available')}: "/><mx:Label id="labelAvailableDownload" fontWeight="bold" text="-"/></mx:HBox> + <mx:HBox horizontalGap="0"><mx:Label text="{ResourceUtil.getInstance().getString('bbb.bwmonitor.latency')}: "/><mx:Label id="labelDownloadLatency" fontWeight="bold" text="-"/></mx:HBox> + <mx:HBox horizontalGap="0" width="100%" ><mx:Spacer width="100%"/><mx:Button id="labelRefresh" icon="{_refreshIcon}" width="16" height="16" click="runBandwidthMeasurement()" /></mx:HBox> </mx:VBox> </mx:Panel> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/RecordButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/RecordButton.mxml index c1b5a6f0834b26d10db270214d585b30db82ab03..ca51d3d2d62d1d3c3dd41c6c819addfc0d5c65b5 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/RecordButton.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/RecordButton.mxml @@ -31,14 +31,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. : ResourceUtil.getInstance().getString('bbb.mainToolbar.recordBtn.toolTip.notRecording')}" enabled="false" creationComplete="onCreationComplete()" - visible="{UserManager.getInstance().getConference().record}" - includeInLayout="{UserManager.getInstance().getConference().record}" mouseOver="onRecordButtonMouseOver(event)" mouseOut="onRecordButtonMouseOut(event)" > <mate:Listener type="{BBBEvent.CHANGE_RECORDING_STATUS}" method="onRecordingStatusChanged" /> <mate:Listener type="{FlashJoinedVoiceConferenceEvent.JOINED_VOICE_CONFERENCE}" method="handleFlashJoinedVoiceConference" /> <mate:Listener type="{WebRTCCallEvent.WEBRTC_CALL_STARTED}" method="handleWebRTCCallStarted" /> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="refreshRole" /> <mx:Script> <![CDATA[ import com.asfusion.mate.events.Dispatcher; @@ -50,6 +49,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.core.model.MeetingModel; import org.bigbluebutton.main.events.BBBEvent; import org.bigbluebutton.main.model.LayoutOptions; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; import org.bigbluebutton.modules.phone.events.FlashJoinedVoiceConferenceEvent; import org.bigbluebutton.modules.phone.events.WebRTCCallEvent; import org.bigbluebutton.util.i18n.ResourceUtil; @@ -110,15 +110,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. trace("RecordButton:doChangeRecordingStatus changing record status to " + event.payload.recording); } - private function onRecordingStatusChanged(event:BBBEvent):void { - if (event.payload.remote) { - this.selected = event.payload.recording; + private function updateButton(recording:Boolean):void { + this.selected = recording; - resourcesChanged(); + resourcesChanged(); - if (UserManager.getInstance().getConference().amIModerator() && MeetingModel.getInstance().meeting.allowStartStopRecording) { - this.enabled = true; - } + this.enabled = UserManager.getInstance().getConference().amIModerator() && MeetingModel.getInstance().meeting.allowStartStopRecording; + } + + private function onRecordingStatusChanged(event:BBBEvent):void { + if (event.payload.remote) { + updateButton(event.payload.recording); trace("RecordButton:onRecordingStatusChanged changing record status to " + event.payload.recording); } @@ -190,6 +192,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function localeChanged(e:Event):void{ resourcesChanged(); } + + private function refreshRole(e:ChangeMyRole):void { + updateButton(this.selected); + } ]]> </mx:Script> </mx:Button> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoHolder.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoHolder.mxml deleted file mode 100755 index 7acf021b1fa94ea6a778fb5e78a08e63b29480a8..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoHolder.mxml +++ /dev/null @@ -1,79 +0,0 @@ -<?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/>. - ---> -<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml"> - <mx:Script> - <![CDATA[ - import mx.core.UIComponent; - - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.util.i18n.ResourceUtil; - private var hideWarningTimer:Timer = null; - - private function onCreationComplete():void { - this.addChild(_videoHolder); - } - - public function remove(video:Video):void { - _videoHolder.removeChild(video); - } - - public function add(video:Video):void { - _videoHolder.addChild(video); - } - - public function showWarning(resourceName:String, autoHide:Boolean=false, color:String="0xFF0000"):void { - const text:String = ResourceUtil.getInstance().getString(resourceName); - - if (hideWarningTimer != null) { - hideWarningTimer.stop(); - } - - if (autoHide) { - hideWarningTimer = new Timer(3000, 1); - hideWarningTimer.addEventListener(TimerEvent.TIMER, hideWarning); - hideWarningTimer.start(); - } - - // bring the label to front - setChildIndex(lblWarning, getChildren().length - 1); - lblWarning.text = text; - lblWarning.setStyle("color", color); - lblWarning.visible = true; - LogUtil.debug("Showing warning: " + text); - } - - private function hideWarning(e:TimerEvent):void { - lblWarning.visible = false; - } - - ]]> - </mx:Script> - - <mx:Fade id="dissolveOut" duration="1000" alphaFrom="1.0" alphaTo="0.0"/> - <mx:Fade id="dissolveIn" duration="1000" alphaFrom="0.0" alphaTo="1.0"/> - - <mx:UIComponent id="_videoHolder"/> - <mx:Image id="imgChromeHelp" y="60" x="{this.width/2 - imgChromeHelp.width/2}" visible="false" - source="@Embed('assets/chrome-allow-mic-access.png')"/> - <mx:Text id="lblWarning" width="100%" textAlign="center" fontSize="14" fontWeight="bold" - y="{this.height - lblWarning.height - 30}" - visible="true" selectable="false" hideEffect="{dissolveOut}" showEffect="{dissolveIn}"/> -</mx:Canvas> \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoWithWarnings.as b/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoWithWarnings.as new file mode 100644 index 0000000000000000000000000000000000000000..048f8d0bbe698b740be157a8f5df4a53a58888a5 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoWithWarnings.as @@ -0,0 +1,255 @@ +package org.bigbluebutton.main.views +{ + + import com.asfusion.mate.events.Dispatcher; + import flash.events.StatusEvent; + import flash.events.TimerEvent; + import flash.events.ActivityEvent; + import flash.media.Camera; + import flash.media.Video + import flash.net.NetStream; + import flash.system.Security; + import flash.system.SecurityPanel; + import flash.utils.Timer; + import mx.containers.Canvas; + import mx.controls.Text; + import mx.core.UIComponent; + import mx.events.FlexEvent; + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.model.VideoProfile; + import org.bigbluebutton.util.i18n.ResourceUtil; + + public class VideoWithWarnings extends VideoWithWarningsBase { + + private var hideWarningTimer:Timer = null; + private var _camera:Camera = null; + private var _activationTimer:Timer = null; + private var _waitingForActivation:Boolean = false; + private var _cameraAccessDenied:Boolean = false; + private var _chromePermissionDenied:Boolean = false; + private var _videoProfile:VideoProfile = null; + private var _showPreviewMsg:Boolean = false; + private var _video:Video = new Video(); + private var _creationCompleted:Boolean = false; + + private var _successCallback:Function = null; + private var _failCallback:Function = null; + + public function VideoWithWarnings() { + super(); + + this.addEventListener(FlexEvent.CREATION_COMPLETE , creationCompleteHandler); + } + + private function creationCompleteHandler(e:FlexEvent):void { + _video.smoothing = true; + _videoHolder.addChild(_video); + + _creationCompleted = true; + } + + public function cameraState():Boolean { + return _camera != null; + } + + public function getCamera():Camera { + return _camera; + } + + public function videoFilters(f:Array):void { + _video.filters = f; + } + + private function hideWarning(e:TimerEvent):void { + _text.visible = false; + } + + private function showMessageHelper(resourceName:String, autoHide:Boolean, styleName:String):void { + const text:String = ResourceUtil.getInstance().getString(resourceName); + + if (hideWarningTimer != null) { + hideWarningTimer.stop() + hideWarningTimer = null; + } + + if (autoHide) { + hideWarningTimer = new Timer(3000, 1); + hideWarningTimer.addEventListener(TimerEvent.TIMER, hideWarning); + hideWarningTimer.start(); + } + + _text.text = text; + // _text.text = "The quick brown fox jumps over the lazy dog"; + _text.setStyle("styleName", styleName); + _text.visible = true; + trace("Showing warning: " + text); + } + + private function showError(resourceName:String, autoHide:Boolean=false):void { + showMessageHelper(resourceName, autoHide, "videoMessageErrorLabelStyle"); + } + + private function showWarning(resourceName:String, autoHide:Boolean=false):void { + showMessageHelper(resourceName, autoHide, "videoMessageWarningLabelStyle"); + } + + public function set successCallback(f:Function):void { + _successCallback = f; + } + + public function set failCallback(f:Function):void { + _failCallback = f; + } + + private function onSuccessCallback():void { + if (_showPreviewMsg) { + showWarning('bbb.video.publish.hint.videoPreview'); + } else { + _text.visible = false; + _text.text = " "; + } + + if (_successCallback != null) { + _successCallback(); + } + } + + private function onFailCallback(resourceName:String):void { + showError(resourceName); + if (_failCallback != null) { + _failCallback(); + } + } + + public function updateCamera(camIndex:int, vp:VideoProfile, containerWidth:int, containerHeight:int, showPreviewMsg:Boolean=false):void { + disableCamera(); + + _videoProfile = vp; + _showPreviewMsg = showPreviewMsg; + + _camera = Camera.getCamera(camIndex.toString()); + if (camIndex == -1) { + onFailCallback('bbb.video.publish.hint.noCamera'); + } else if (_camera == null) { + onFailCallback('bbb.video.publish.hint.cantOpenCamera'); + } else { + _camera.addEventListener(ActivityEvent.ACTIVITY, onActivityEvent); + _camera.addEventListener(StatusEvent.STATUS, onStatusEvent); + + if (_camera.muted) { + if (_cameraAccessDenied && !_chromePermissionDenied) { + Security.showSettings(SecurityPanel.PRIVACY) + } else if (_chromePermissionDenied) { + showWarning('bbb.video.publish.hint.cameraDenied'); + } else { + showWarning('bbb.video.publish.hint.waitingApproval'); + } + } else { + onCameraAccessAllowed(); + } + + displayVideoPreview(); + } + + this.width = containerWidth; + this.height = containerHeight; + invalidateDisplayList(); + } + + private function displayVideoPreview():void { + trace("Using this video profile:: " + _videoProfile.toString()); + _camera.setMotionLevel(5, 1000); + _camera.setKeyFrameInterval(_videoProfile.keyFrameInterval); + _camera.setMode(_videoProfile.width, _videoProfile.height, _videoProfile.modeFps); + _camera.setQuality(_videoProfile.qualityBandwidth, _videoProfile.qualityPicture); + + if (_camera.width != _videoProfile.width || _camera.height != _videoProfile.height) + trace("Resolution " + _videoProfile.width + "x" + _videoProfile.height + " is not supported, using " + _camera.width + "x" + _camera.height + " instead"); + + _video.attachCamera(_camera); + } + + override protected function updateDisplayList(w:Number, h:Number):void { + super.updateDisplayList(w, h); + + var videoWidth:int; + var videoHeight:int; + + if (_creationCompleted && _videoProfile != null) { + var ar:Number = _videoProfile.width / _videoProfile.height; + if (w / h > ar) { + videoWidth = Math.ceil(h * ar); + videoHeight = h; + } else { + videoWidth = w; + videoHeight = Math.ceil(w / ar); + } + videoCanvas.width = _video.width = videoWidth; + videoCanvas.height = _video.height = videoHeight; + } + } + + public function attachNetStream(ns:NetStream, vp:VideoProfile, containerWidth:int, containerHeight:int):void { + disableCamera(); + _videoProfile = vp; + _video.attachNetStream(ns); + + this.width = containerWidth; + this.height = containerHeight; + invalidateDisplayList(); + } + + public function disableCamera():void { + _video.clear(); + _video.attachCamera(null); + _camera = null; + } + + private function onActivityEvent(e:ActivityEvent):void { + if (_waitingForActivation && e.activating) { + _activationTimer.stop(); + _waitingForActivation = false; + + onSuccessCallback(); + } + } + + private function onStatusEvent(e:StatusEvent):void { + if (e.code == "Camera.Unmuted") { + onCameraAccessAllowed(); + } else { + onCameraAccessDisallowed(); + } + } + + private function onCameraAccessAllowed():void { + // this is just to overwrite the message of waiting for approval + showWarning('bbb.video.publish.hint.openingCamera'); + + _cameraAccessDenied = false; + + // set timer to ensure that the camera activates. If not, it might be in use by another application + _waitingForActivation = true; + if (_activationTimer != null) { + _activationTimer.stop(); + } + + _activationTimer = new Timer(10000, 1); + _activationTimer.addEventListener(TimerEvent.TIMER, activationTimeout); + _activationTimer.start(); + } + + private function onCameraAccessDisallowed():void { + onFailCallback('bbb.video.publish.hint.cameraDenied'); + _cameraAccessDenied = true; + } + + private function activationTimeout(e:TimerEvent):void { + onFailCallback('bbb.video.publish.hint.cameraIsBeingUsed'); + } + + public function set chromePermissionDenied(value:Boolean):void { + _chromePermissionDenied = value; + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoWithWarningsBase.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoWithWarningsBase.mxml new file mode 100644 index 0000000000000000000000000000000000000000..2ab3d7928eccfacaee3ab42ca89fa53cd3b87d56 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/VideoWithWarningsBase.mxml @@ -0,0 +1,56 @@ +<?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/>. + +--> +<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" + xmlns:mate="http://mate.asfusion.com/" width="100%" height="100%" + horizontalAlign="center" > + + <mx:Fade id="dissolveOut" duration="1000" alphaFrom="1.0" alphaTo="0.0"/> + <mx:Fade id="dissolveIn" duration="1000" alphaFrom="0.0" alphaTo="1.0"/> + + <mx:Canvas + id="videoCanvas" + width="100%" + height="100%" + horizontalScrollPolicy="off" verticalScrollPolicy="off" > + <mx:UIComponent + id="_videoHolder" + width="100%" + height="100%" /> + <mx:VBox + width="100%" + height="100%" + verticalAlign="bottom" + visible="{_text.visible}" + includeInLayout="{_text.visible}" + hideEffect="{dissolveOut}" showEffect="{dissolveIn}" > + <mx:Box + width="100%" + styleName="videoMessageBackgroundStyle" > + <mx:Text + id="_text" + selectable="false" + textAlign="center" + visible="false" + width="100%" /> + </mx:Box> + </mx:VBox> + </mx:Canvas> +</mx:VBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/WaitingWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/WaitingWindow.mxml new file mode 100755 index 0000000000000000000000000000000000000000..660472502e1f018b64a5528689b1b2b46df9e03f --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/WaitingWindow.mxml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + BigBlueButton open source conferencing system - http://www.bigbluebutton.org + + Copyright (c) 2010 BigBlueButton Inc. and by respective authors (see below). + + 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 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/>. + + $Id: $ +--> + +<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" + title="{ResourceUtil.getInstance().getString('bbb.waitWindow.waitMessage.title')}" showCloseButton="false" + layout="vertical" width="350" horizontalAlign="center"> + + <mx:Script> + <![CDATA[ + import mx.managers.PopUpManager; + + import org.bigbluebutton.util.i18n.ResourceUtil; + + public function removeWindow():void { + PopUpManager.removePopUp(this); + } + ]]> + </mx:Script> + <mx:Text text="{ResourceUtil.getInstance().getString('bbb.waitWindow.waitMessage.message')}" width="100%" textAlign="center" /> +</mx:TitleWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml b/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml index 08d6f34215125133fa5869728807c123081ff110..23cbe2cd1438b5d1bc66a580be5f45956e38d300 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/main/views/WebRTCEchoTest.mxml @@ -57,8 +57,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private static const LOG:String = "Phone::WebRTCEchoTest - "; private static var DEFAULT_HELP_URL:String = "http://www.bigbluebutton.org/content/videos"; + + private static const TIMEOUT:Number = 60; + private static const CANCEL_BUTTON:Number = 55; private var dotTimer:Timer; + + private var cancelTimer:Timer; + private var countdown:Number; + + [Bindable] + private var cancelButtonLabel:String = ResourceUtil.getInstance().getString('bbb.micSettings.cancel'); + private var userClosed:Boolean = false; override public function move(x:Number, y:Number):void { @@ -66,8 +76,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function onCancelClicked():void { - - if (dotTimer) dotTimer.stop(); + stopTimers(); PopUpManager.removePopUp(this); } @@ -80,12 +89,50 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. lblConnectMessage.text = lblConnectMessageMock.text = ResourceUtil.getInstance().getString('bbb.micSettings.webrtc.connecting'); dotTimer = new Timer(200, 0); dotTimer.addEventListener(TimerEvent.TIMER, dotAnimate); - dotTimer.start(); - - var testState:String = PhoneModel.getInstance().webRTCModel.state; - if (testState == Constants.DO_ECHO_TEST) { - webRTCEchoTestStarted(); - } + + cancelTimer = new Timer(1000, 0); + cancelTimer.addEventListener(TimerEvent.TIMER, timeout); + + startTimers(); + + cancelButton.width = cancelButton.measureText(genCancelButtonLabel(TIMEOUT)).width + + cancelButton.getStyle("paddingRight") + + cancelButton.getStyle("paddingLeft") + + 8; // 8 is magic number + + var testState:String = PhoneModel.getInstance().webRTCModel.state; + if (testState == Constants.DO_ECHO_TEST) { + webRTCEchoTestStarted(); + } + } + + private function startTimers():void { + cancelButton.visible = false; + if (!dotTimer.running) dotTimer.start(); + if (!cancelTimer.running) { + countdown = TIMEOUT; + cancelTimer.start(); + } + } + + private function stopTimers():void { + if (dotTimer.running) dotTimer.stop(); + if (cancelTimer.running) cancelTimer.stop(); + } + + private function genCancelButtonLabel(countdown:Number):String { + return cancelButtonLabel + " (" + countdown.toString() + ")"; + } + + private function timeout(e:TimerEvent):void { + if (countdown > 0) { + if (!cancelButton.visible && countdown < CANCEL_BUTTON) + cancelButton.visible = true; + cancelButton.label = genCancelButtonLabel(countdown); + countdown--; + } else { + noButtonClicked(); + } } private function dotAnimate(e:TimerEvent):void { @@ -132,7 +179,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function webRTCEchoTestStarted():void { setCurrentState("started"); - dotTimer.stop(); + stopTimers(); } private function handleWebRTCEchoTestEndedEvent(e:WebRTCEchoTestEvent):void { @@ -142,7 +189,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function webRTCEchoTestEnded():void { setCurrentState("connecting"); lblConnectMessage.text = lblConnectMessageMock.text = ResourceUtil.getInstance().getString('bbb.micSettings.webrtc.endedecho'); - if (!dotTimer.running) dotTimer.start(); + startTimers(); if (!userClosed) { onCancelClicked(); @@ -158,19 +205,19 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function handleWebRTCEchoTestWaitingForICEEvent(e:WebRTCEchoTestEvent):void { setCurrentState("connecting"); lblConnectMessage.text = lblConnectMessageMock.text = ResourceUtil.getInstance().getString('bbb.micSettings.webrtc.waitingforice'); - if (!dotTimer.running) dotTimer.start(); + startTimers(); } private function handleWebRTCEchoTestTransferringEvent(e:WebRTCEchoTestEvent):void { setCurrentState("connecting"); lblConnectMessage.text = lblConnectMessageMock.text = ResourceUtil.getInstance().getString('bbb.micSettings.webrtc.transferring'); - if (!dotTimer.running) dotTimer.start(); + startTimers(); } private function handleWebRTCCallConnectingEvent(e:WebRTCCallEvent):void { setCurrentState("connecting"); lblConnectMessage.text = lblConnectMessageMock.text = ResourceUtil.getInstance().getString('bbb.micSettings.webrtc.connecting'); - if (!dotTimer.running) dotTimer.start(); + startTimers(); } private function handleWebRTCCallFailedEvent(e:WebRTCCallEvent):void { @@ -180,7 +227,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function handleWebRTCCallWaitingForICEEvent(e:WebRTCCallEvent):void { setCurrentState("connecting"); lblConnectMessage.text = lblConnectMessageMock.text = ResourceUtil.getInstance().getString('bbb.micSettings.webrtc.waitingforice'); - if (!dotTimer.running) dotTimer.start(); + startTimers(); } private function webRTCCallStarted():void { @@ -226,13 +273,22 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mx:states> <mx:State name="connecting"> <mx:AddChild relativeTo="cnvTitle" position="after"> - <mx:HBox width="100%" height="100%" verticalAlign="middle" horizontalAlign="center"> - <mx:TextArea id="lblConnectMessage" editable="false" textAlign="right" borderSkin="{null}" - width="{lblConnectMessageMock.width + 4}" height="{lblConnectDots.height}" - styleName="micSettingsWindowSpeakIntoMicLabelStyle" /> - <mx:Text id="lblConnectMessageMock" visible="false" includeInLayout="false" /> - <mx:Label id="lblConnectDots" width="20" textAlign="left" styleName="micSettingsWindowSpeakIntoMicLabelStyle" text="" /> - </mx:HBox> + <mx:VBox width="100%" height="100%" verticalAlign="middle"> + <mx:HBox width="100%" height="100%" verticalAlign="middle" horizontalAlign="center"> + <mx:TextArea id="lblConnectMessage" editable="false" textAlign="right" borderSkin="{null}" + width="{lblConnectMessageMock.width + 4}" height="{lblConnectDots.height}" + styleName="micSettingsWindowSpeakIntoMicLabelStyle" /> + <mx:Text id="lblConnectMessageMock" visible="false" includeInLayout="false" /> + <mx:Label id="lblConnectDots" width="20" textAlign="left" styleName="micSettingsWindowSpeakIntoMicLabelStyle" text="" /> + </mx:HBox> + <mx:HBox width="100%" verticalAlign="bottom" horizontalAlign="right"> + <mx:Button id="cancelButton" + label="{cancelButtonLabel}" + styleName="micSettingsWindowPlaySoundButtonStyle" + click="noButtonClicked()" + toolTip="" /> + </mx:HBox> + </mx:VBox> </mx:AddChild> </mx:State> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatCopyEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatCopyEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..a0f5e832c857bd40fb96f0131fe2cfaca2d555a2 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatCopyEvent.as @@ -0,0 +1,20 @@ +package org.bigbluebutton.modules.chat.events +{ + import flash.events.Event; + import org.bigbluebutton.modules.chat.model.ChatConversation; + + public class ChatCopyEvent extends Event + { + public static const COPY_CHAT_EVENT:String = 'COPY_CHAT_EVENT'; + + public var chatMessages:ChatConversation; + + public function ChatCopyEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} + + diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatEvent.as index 5f6787c914370b3c7ac1a6f1a2d4232cf1f9d7a5..580ed3ebff62d0c68525f26a2ef638ba506532bd 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatEvent.as @@ -26,6 +26,7 @@ package org.bigbluebutton.modules.chat.events public static const CHAT_EVENT:String = 'CHAT_EVENT'; public static const NEW_CHAT_MESSAGE_EVENT:String = 'NEW_CHAT_MESSAGE_EVENT'; public static const PRIVATE_CHAT_MESSAGE_EVENT:String = 'PRIVATE_CHAT_MESSAGE_EVENT'; + public static const RESIZE_CHAT_TOOLBAR:String = 'RESIZE_CHAT_TOOLBAR'; public function ChatEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) { diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatSaveEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatSaveEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..4deebf0159a89cd8dda5805253c0ed6bd714abb6 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatSaveEvent.as @@ -0,0 +1,21 @@ +package org.bigbluebutton.modules.chat.events +{ + import flash.events.Event; + import org.bigbluebutton.modules.chat.model.ChatConversation; + + public class ChatSaveEvent extends Event + { + public static const SAVE_CHAT_EVENT:String = 'SAVE_CHAT_EVENT'; + + public var chatMessages:ChatConversation; + public var filename:String; + + public function ChatSaveEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} + + diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatToolbarButtonEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatToolbarButtonEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..928085d4cce75b0932f2fbd6b00d1cf7ecae7227 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/events/ChatToolbarButtonEvent.as @@ -0,0 +1,16 @@ +package org.bigbluebutton.modules.chat.events +{ + import flash.events.Event; + + public class ChatToolbarButtonEvent extends Event + { + public static const SAVE_CHAT_TOOLBAR_EVENT:String = "SAVE_CHAT_TOOLBAR_EVENT"; + public static const COPY_CHAT_TOOLBAR_EVENT:String = "COPY_CHAT_TOOLBAR_EVENT"; + + public function ChatToolbarButtonEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml index efcbf5fe4ef8d2ca4c94cc5a3f96c5c6ec6ad3c9..4fec5fa0264c62da36642644adf7d8bb4c1450f6 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/maps/ChatEventMap.mxml @@ -1,97 +1,109 @@ -<?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/>. - ---> - -<EventMap xmlns="http://mate.asfusion.com/" - xmlns:mx="http://www.adobe.com/2006/mxml"> - <mx:Script> - <![CDATA[ - import com.asfusion.mate.events.Dispatcher; - import mx.events.FlexEvent; - import org.bigbluebutton.common.events.OpenWindowEvent; - import org.bigbluebutton.core.EventConstants; - import org.bigbluebutton.main.events.ModuleStartedEvent; - import org.bigbluebutton.modules.chat.events.ChatEvent; - import org.bigbluebutton.modules.chat.events.SendPrivateChatMessageEvent; - import org.bigbluebutton.modules.chat.events.SendPublicChatMessageEvent; - import org.bigbluebutton.modules.chat.events.StartChatModuleEvent; - import org.bigbluebutton.modules.chat.events.StopChatModuleEvent; - import org.bigbluebutton.modules.chat.events.TranscriptEvent; - import org.bigbluebutton.modules.chat.services.ChatMessageService; - import org.bigbluebutton.modules.chat.services.MessageReceiver; - import org.bigbluebutton.modules.chat.services.MessageSender; - import org.bigbluebutton.modules.chat.views.ChatView; - import org.bigbluebutton.modules.chat.views.ChatWindow; - ]]> - </mx:Script> - - <EventHandlers type="{FlexEvent.PREINITIALIZE}"> - <ObjectBuilder generator="{ChatEventMapDelegate}" constructorArguments="{scope.dispatcher}"/> - </EventHandlers> - - <EventHandlers type="{StartChatModuleEvent.START_CHAT_MODULE_EVENT}"> - <MethodInvoker generator="{ChatEventMapDelegate}" method="openChatWindow" /> - <ObjectBuilder generator="{ChatMessageService}"/> - </EventHandlers> - - <EventHandlers type="{StopChatModuleEvent.STOP_CHAT_MODULE_EVENT}"> - <MethodInvoker generator="{ChatEventMapDelegate}" method="closeChatWindow" /> - </EventHandlers> - - <EventHandlers type="{EventConstants.SEND_PUBLIC_CHAT_REQ}"> - <MethodInvoker generator="{ChatMessageService}" method="sendPublicMessageFromApi" arguments="{event.message}"/> - </EventHandlers> - - <EventHandlers type="{EventConstants.SEND_PRIVATE_CHAT_REQ}"> - <MethodInvoker generator="{ChatMessageService}" method="sendPrivateMessageFromApi" arguments="{event.message}"/> - </EventHandlers> - - <EventHandlers type="{SendPublicChatMessageEvent.SEND_PUBLIC_CHAT_MESSAGE_EVENT}"> - <MethodInvoker generator="{ChatMessageService}" method="sendPublicMessage" arguments="{event.chatMessage}"/> - </EventHandlers> - - <EventHandlers type="{SendPrivateChatMessageEvent.SEND_PRIVATE_CHAT_MESSAGE_EVENT}"> - <MethodInvoker generator="{ChatMessageService}" method="sendPrivateMessage" arguments="{event.chatMessage}"/> - </EventHandlers> - - <EventHandlers type="{TranscriptEvent.LOAD_TRANSCRIPT}" > - <MethodInvoker generator="{ChatMessageService}" method="getPublicChatMessages"/> - </EventHandlers> - - <EventHandlers type="{TranscriptEvent.TRANSCRIPT_EVENT}" > - <MethodInvoker generator="{ChatMessageService}" method="sendWelcomeMessage"/> - </EventHandlers> - - <Injectors target="{ChatMessageService}"> - <PropertyInjector targetKey="dispatcher" source="{scope.dispatcher}"/> - <PropertyInjector targetKey="receiver" source="{MessageReceiver}"/> - <PropertyInjector targetKey="sender" source="{MessageSender}"/> - </Injectors> - - <Injectors target="{MessageReceiver}"> - <PropertyInjector targetKey="dispatcher" source="{scope.dispatcher}"/> - </Injectors> - - <Injectors target="{MessageSender}"> - <PropertyInjector targetKey="dispatcher" source="{scope.dispatcher}"/> - </Injectors> - -</EventMap> +<?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/>. + +--> + +<EventMap xmlns="http://mate.asfusion.com/" + xmlns:mx="http://www.adobe.com/2006/mxml"> + <mx:Script> + <![CDATA[ + import com.asfusion.mate.events.Dispatcher; + import mx.events.FlexEvent; + import org.bigbluebutton.common.events.OpenWindowEvent; + import org.bigbluebutton.core.EventConstants; + import org.bigbluebutton.main.events.ModuleStartedEvent; + import org.bigbluebutton.modules.chat.events.ChatCopyEvent; + import org.bigbluebutton.modules.chat.events.ChatEvent; + import org.bigbluebutton.modules.chat.events.ChatSaveEvent; + import org.bigbluebutton.modules.chat.events.SendPrivateChatMessageEvent; + import org.bigbluebutton.modules.chat.events.SendPublicChatMessageEvent; + import org.bigbluebutton.modules.chat.events.StartChatModuleEvent; + import org.bigbluebutton.modules.chat.events.StopChatModuleEvent; + import org.bigbluebutton.modules.chat.events.TranscriptEvent; + import org.bigbluebutton.modules.chat.services.ChatMessageService; + import org.bigbluebutton.modules.chat.services.MessageReceiver; + import org.bigbluebutton.modules.chat.services.MessageSender; + import org.bigbluebutton.modules.chat.services.ChatCopy; + import org.bigbluebutton.modules.chat.services.ChatSaver; + import org.bigbluebutton.modules.chat.views.ChatView; + import org.bigbluebutton.modules.chat.views.ChatWindow; + ]]> + </mx:Script> + + <EventHandlers type="{FlexEvent.PREINITIALIZE}"> + <ObjectBuilder generator="{ChatEventMapDelegate}" constructorArguments="{scope.dispatcher}"/> + </EventHandlers> + + <EventHandlers type="{StartChatModuleEvent.START_CHAT_MODULE_EVENT}"> + <MethodInvoker generator="{ChatEventMapDelegate}" method="openChatWindow" /> + <ObjectBuilder generator="{ChatMessageService}"/> + </EventHandlers> + + <EventHandlers type="{StopChatModuleEvent.STOP_CHAT_MODULE_EVENT}"> + <MethodInvoker generator="{ChatEventMapDelegate}" method="closeChatWindow" /> + </EventHandlers> + + <EventHandlers type="{EventConstants.SEND_PUBLIC_CHAT_REQ}"> + <MethodInvoker generator="{ChatMessageService}" method="sendPublicMessageFromApi" arguments="{event.message}"/> + </EventHandlers> + + <EventHandlers type="{EventConstants.SEND_PRIVATE_CHAT_REQ}"> + <MethodInvoker generator="{ChatMessageService}" method="sendPrivateMessageFromApi" arguments="{event.message}"/> + </EventHandlers> + + <EventHandlers type="{SendPublicChatMessageEvent.SEND_PUBLIC_CHAT_MESSAGE_EVENT}"> + <MethodInvoker generator="{ChatMessageService}" method="sendPublicMessage" arguments="{event.chatMessage}"/> + </EventHandlers> + + <EventHandlers type="{SendPrivateChatMessageEvent.SEND_PRIVATE_CHAT_MESSAGE_EVENT}"> + <MethodInvoker generator="{ChatMessageService}" method="sendPrivateMessage" arguments="{event.chatMessage}"/> + </EventHandlers> + + <EventHandlers type="{TranscriptEvent.LOAD_TRANSCRIPT}" > + <MethodInvoker generator="{ChatMessageService}" method="getPublicChatMessages"/> + </EventHandlers> + + <EventHandlers type="{TranscriptEvent.TRANSCRIPT_EVENT}" > + <MethodInvoker generator="{ChatMessageService}" method="sendWelcomeMessage"/> + </EventHandlers> + + <EventHandlers type="{ChatSaveEvent.SAVE_CHAT_EVENT}"> + <MethodInvoker generator="{ChatSaver}" method="saveChatToFile" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{ChatCopyEvent.COPY_CHAT_EVENT}"> + <MethodInvoker generator="{ChatCopy}" method="copyAllText" arguments="{event}"/> + </EventHandlers> + + <Injectors target="{ChatMessageService}"> + <PropertyInjector targetKey="dispatcher" source="{scope.dispatcher}"/> + <PropertyInjector targetKey="receiver" source="{MessageReceiver}"/> + <PropertyInjector targetKey="sender" source="{MessageSender}"/> + </Injectors> + + <Injectors target="{MessageReceiver}"> + <PropertyInjector targetKey="dispatcher" source="{scope.dispatcher}"/> + </Injectors> + + <Injectors target="{MessageSender}"> + <PropertyInjector targetKey="dispatcher" source="{scope.dispatcher}"/> + </Injectors> + +</EventMap> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/model/ChatConversation.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/model/ChatConversation.as index 1fc9c8422e2fd01c1bfb1d162ca73aa08939c072..dd3eb24c0af81d83d6f5e7e401db1fddf6b818c7 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/model/ChatConversation.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/model/ChatConversation.as @@ -18,6 +18,8 @@ */ package org.bigbluebutton.modules.chat.model { + import com.adobe.utils.StringUtil; + import flash.system.Capabilities; import mx.collections.ArrayCollection; @@ -64,10 +66,14 @@ package org.bigbluebutton.modules.chat.model public function getAllMessageAsString():String{ var allText:String = ""; - var returnStr:String = (Capabilities.os.indexOf("Windows") >= 0 ? "\r\n" : "\r"); + var returnStr:String = (Capabilities.os.indexOf("Windows") >= 0 ? "\r\n" : "\n"); for (var i:int = 0; i < messages.length; i++){ var item:ChatMessage = messages.getItemAt(i) as ChatMessage; - allText += item.name + " - " + item.time + " : " + item.text + returnStr; + allText += "[" + item.time + "] "; + if (StringUtil.trim(item.name) != "") { + allText += item.name + ": "; + } + allText += item.text + returnStr; } return allText; } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatCopy.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatCopy.as new file mode 100644 index 0000000000000000000000000000000000000000..b2b964b1de44daa154747175f4f43da0880522fb --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatCopy.as @@ -0,0 +1,19 @@ +package org.bigbluebutton.modules.chat.services +{ + import flash.system.System; + + import mx.controls.Alert; + + import org.bigbluebutton.modules.chat.model.ChatConversation; + import org.bigbluebutton.modules.chat.events.ChatCopyEvent; + import org.bigbluebutton.util.i18n.ResourceUtil; + + public class ChatCopy + { + public function copyAllText(e:ChatCopyEvent):void { + var chat:ChatConversation = e.chatMessages; + System.setClipboard(chat.getAllMessageAsString()); + Alert.show(ResourceUtil.getInstance().getString('bbb.chat.copy.complete'), "", Alert.OK); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatSaver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatSaver.as new file mode 100644 index 0000000000000000000000000000000000000000..bbb38e2b782560e7ccf2e09fdbbdbec19e162d31 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/services/ChatSaver.as @@ -0,0 +1,39 @@ +package org.bigbluebutton.modules.chat.services +{ + import flash.events.Event; + import mx.controls.Alert; + import flash.net.FileReference; + import org.bigbluebutton.modules.chat.model.ChatConversation; + import org.bigbluebutton.modules.chat.events.ChatSaveEvent; + import org.bigbluebutton.util.i18n.ResourceUtil; + + public class ChatSaver + { + public function ChatSaver(){ + } + + public function saveChatToFile(e:ChatSaveEvent):void{ + var chat:ChatConversation = e.chatMessages; + var filename:String = e.filename; + + var textToExport:String = chat.getAllMessageAsString(); + var fileRef:FileReference = new FileReference(); + + fileRef.addEventListener(Event.COMPLETE, function(evt:Event):void { + Alert.show(ResourceUtil.getInstance().getString('bbb.chat.save.complete'), "", Alert.OK); + }); + + var cr:String = String.fromCharCode(13); + var lf:String = String.fromCharCode(10); + var crlf:String = String.fromCharCode(13, 10); + + textToExport = textToExport.replace(new RegExp(crlf, "g"), '\n'); + textToExport = textToExport.replace(new RegExp(cr, "g"), '\n'); + textToExport = textToExport.replace(new RegExp(lf, "g"), '\n'); + textToExport = textToExport.replace(new RegExp('\n', "g"), crlf); + + fileRef.save(textToExport, filename + ".txt"); + } + } +} + diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/AddChatTabBox.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/AddChatTabBox.mxml index 068ffa266a598883ed9104bd1abab51452a52c1e..21353a36eba7c081470da3b560dc845d5c8a8d70 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/AddChatTabBox.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/AddChatTabBox.mxml @@ -26,7 +26,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. creationComplete="onCreationComplete()"> <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="lockSettingsChanged" /> - + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="refreshRole" /> + <mx:Script> <![CDATA[ import com.asfusion.mate.events.Dispatcher; @@ -42,6 +43,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.main.model.users.BBBUser; import org.bigbluebutton.main.model.users.Conference; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; import org.bigbluebutton.modules.chat.events.ChatOptionsEvent; import org.bigbluebutton.modules.chat.model.ChatOptions; import org.bigbluebutton.util.i18n.ResourceUtil; @@ -106,16 +108,20 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function lockSettingsChanged(e:Event):void { - - if (UsersUtil.amIModerator() || UsersUtil.amIPresenter()) return; // Settings only affect viewers. - - var userManager:UserManager = UserManager.getInstance(); - var conference:Conference = userManager.getConference(); - var me:BBBUser = conference.getMyUser(); - - usersList.enabled = !me.disableMyPrivateChat; + refreshListStatus(); + } + + private function refreshRole(e:ChangeMyRole):void { + refreshListStatus(); + } + + private function refreshListStatus():void { + var userManager:UserManager = UserManager.getInstance(); + var conference:Conference = userManager.getConference(); + var me:BBBUser = conference.getMyUser(); + + usersList.enabled = !me.disableMyPrivateChat || me.moderator || me.presenter; } - ]]> </mx:Script> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/AdvancedList.as b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/AdvancedList.as index 93f73a8748017a6491f9dd206bcbea83d8ff7be0..3c1e6a546050e4597a96923090a6d7e558330d68 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/AdvancedList.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/AdvancedList.as @@ -19,6 +19,7 @@ package org.bigbluebutton.modules.chat.views { import mx.controls.List; + import org.bigbluebutton.modules.chat.events.ChatEvent; public class AdvancedList extends List { @@ -34,6 +35,8 @@ package org.bigbluebutton.modules.chat.views super.measure(); //sovled on forum by Flex HarUI measuredHeight = measureHeightOfItems() + viewMetrics.top + viewMetrics.bottom; + + dispatchEvent(new ChatEvent(ChatEvent.RESIZE_CHAT_TOOLBAR)); } public function scrollToBottom():void { diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml index 3affc31ea42cbbdd0c51c9deee3618747783b818..1d1e4e5375c9e879b6364a32d82712952f7bb9c9 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatBox.mxml @@ -64,6 +64,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mate:Listener type="{ShortcutEvent.CHAT_DEBUG}" method="chatDebugInfo" /> <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="lockSettingsChanged" /> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="refreshRole" /> <mx:Script> <![CDATA[ @@ -87,8 +88,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.main.events.UserLeftEvent; import org.bigbluebutton.main.model.users.BBBUser; import org.bigbluebutton.main.model.users.Conference; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; import org.bigbluebutton.modules.chat.ChatConstants; import org.bigbluebutton.modules.chat.ChatUtil; + import org.bigbluebutton.modules.chat.events.ChatEvent; import org.bigbluebutton.modules.chat.events.ChatOptionsEvent; import org.bigbluebutton.modules.chat.events.PrivateChatMessageEvent; import org.bigbluebutton.modules.chat.events.PublicChatMessageEvent; @@ -148,6 +151,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. [Bindable] private var chatListHeight:Number = 100; + + [Bindable] + private var chatToolbarHeight:Number = 50; [Bindable] public var chatOptions:ChatOptions = new ChatOptions(); @@ -183,6 +189,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. trace(LOG + " onCreationComplete. Apply lock settings"); applyLockSettings(); + + chatToolbar.registerListeners(chatMessagesList); + + chatMessagesList.addEventListener(ChatEvent.RESIZE_CHAT_TOOLBAR, adjustToolbarWidthAccordingToScrollBar); } private function focusChatBox(e:ShortcutEvent):void{ @@ -289,6 +299,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } } } + + private function adjustToolbarWidthAccordingToScrollBar(e:ChatEvent):void{ + invalidateDisplayList(); + validateNow(); + } private function handlePrivateChatMessageEvent(event:PrivateChatMessageEvent):void { var message:ChatMessageVO = event.message; @@ -346,6 +361,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function changeFontSize(e:ChatOptionsEvent):void { this.setStyle("fontSize", e.fontSize); } + + private function copyAllText():void{ + System.setClipboard(chatMessages.getAllMessageAsString()); + } + + public function getChatMessages():ChatConversation { + return chatMessages; + } private function addContextMenuItems():void { var contextMenu:ContextMenu = new ContextMenu(); @@ -579,7 +602,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. cm.fromTime = now.valueOf(); cm.fromTimezoneOffset = now.getTimezoneOffset(); - //cm.message = ChatUtil.parseURLs(ChatUtil.cleanup(message)); cm.message = ExternalInterface.call('parseURLs', (ChatUtil.cleanup(message))); publicEvent.chatMessage = cm; @@ -601,7 +623,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. cm.fromTime = now.valueOf(); cm.fromTimezoneOffset = now.getTimezoneOffset(); - //cm.message = ChatUtil.parseURLs(ChatUtil.cleanup(message)); cm.message = ExternalInterface.call('parseURLs', (ChatUtil.cleanup(message))); cm.toUserID = chatWithUserID; cm.toUsername = chatWithUsername; @@ -665,16 +686,41 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. txtMsgArea.enabled = sendBtn.enabled = !me.disableMyPrivateChat; } } + + private function refreshRole(e:ChangeMyRole):void { + applyLockSettings(); + } + + override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { + super.updateDisplayList(unscaledWidth, unscaledHeight); + + // Force validation before evaluation of toolbar width + validateNow(); + + const paddingHeight:int = 5; + const paddingWidth:int = 5; + + chatToolbar.width = chatMessagesCanvas.width - paddingWidth * 2; + chatToolbar.x = (chatMessagesCanvas.width - chatToolbar.width) / 2; + chatToolbar.y = chatMessagesCanvas.height - chatToolbar.height - paddingHeight; + + if(chatMessagesList.mx_internal::scroll_verticalScrollBar != null && chatMessagesList.mx_internal::scroll_verticalScrollBar.visible){ + chatToolbar.width -= chatMessagesList.mx_internal::scroll_verticalScrollBar.width; + } + } ]]> </mx:Script> <mx:HBox width="100%" height="{chatListHeight}" verticalScrollPolicy="off"> - <chat:AdvancedList width="100%" height="{chatListHeight}" id="chatMessagesList" selectable="false" variableRowHeight="true" - itemRenderer="org.bigbluebutton.modules.chat.views.ChatMessageRenderer" verticalScrollPolicy="on" horizontalScrollPolicy="off" wordWrap="true" - dataProvider="{chatMessages.messages}" - tabIndex="{baseIndex}" - accessibilityName="{ResourceUtil.getInstance().getString('bbb.chat.messageList')}" /> + <mx:Canvas id="chatMessagesCanvas" width="100%" height="{chatListHeight}" horizontalScrollPolicy="off" verticalScrollPolicy="off" > + <chat:AdvancedList width="100%" height="{chatListHeight}" id="chatMessagesList" selectable="false" variableRowHeight="true" paddingTop="0" paddingBottom="0" + itemRenderer="org.bigbluebutton.modules.chat.views.ChatMessageRenderer" verticalScrollPolicy="on" horizontalScrollPolicy="off" wordWrap="true" + dataProvider="{chatMessages.messages}" + tabIndex="{baseIndex}" + accessibilityName="{ResourceUtil.getInstance().getString('bbb.chat.messageList')}" /> + <chat:ChatToolbar id="chatToolbar" /> + </mx:Canvas> </mx:HBox> <mx:HBox id="chatCtrlBar" width="100%" height="60" styleName="chatControlBarStyle" verticalScrollPolicy="off" diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatMessageRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatMessageRenderer.mxml index 0182c53f617c8d78ce8a482d27080c57a9c81374..fd2f545b9822910d37c482baa8d092fe7715da78 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatMessageRenderer.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatMessageRenderer.mxml @@ -61,9 +61,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. //remove the header if not needed to save space hbHeader.includeInLayout = hbHeader.visible = lblName.visible || lblTime.visible; - // adjust the name width so that "..." are added if it's too long - lblName.width = this.width - lblTime.width - 22; - // If you remove this some of the chat messages will fail to render validateNow(); } @@ -78,7 +75,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. </mx:Script> <mx:Canvas width="100%" id="hbHeader" verticalScrollPolicy="off" horizontalScrollPolicy="off"> - <mx:Label id="lblName" text="{data.name}" visible="true" color="gray" textAlign="left" left="0"/> + <mx:Label id="lblName" text="{data.name}" visible="true" color="gray" textAlign="left" left="0" width="{this.width - lblTime.width - 22}"/> <mx:Text id="lblTime" htmlText="{data.time}" textAlign="right" visible="true" color="gray" right="0" /> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatToolbar.mxml new file mode 100644 index 0000000000000000000000000000000000000000..e79f65054313c692cb6ac2a9b76e5be2bdaf153d --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatToolbar.mxml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8"?> + +<mx:HBox xmlns="flexlib.containers.*" + initialize="init()" + xmlns:mx="http://www.adobe.com/2006/mxml" + xmlns:mate="http://mate.asfusion.com/" + creationComplete="onCreationComplete()" + visible="{toolbarVisible}" + styleName="whiteboardToolbarStyle" + horizontalAlign="center" + hideEffect="{fadeOut}" showEffect="{fadeIn}" > + + <mx:Script> + <![CDATA[ + import mx.core.UIComponent; + import com.asfusion.mate.events.Dispatcher; + + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.modules.chat.events.ChatToolbarButtonEvent; + import org.bigbluebutton.modules.chat.model.ChatOptions; + import org.bigbluebutton.util.i18n.ResourceUtil; + + [Bindable] public var chatOptions:ChatOptions; + [Bindable] private var baseIndex:int; + [Bindable] private var toolbarVisible:Boolean = false; + + private var mousedOver:Boolean = false; + private var globalDispatcher:Dispatcher; + private var _toolbarHideTimer:Timer; + + public function init():void{ + chatOptions = new ChatOptions(); + baseIndex = chatOptions.baseTabIndex; + + _toolbarHideTimer = new Timer(500, 1); + _toolbarHideTimer.addEventListener(TimerEvent.TIMER, closeToolbar); + } + + private function onCreationComplete():void { + globalDispatcher = new Dispatcher(); + this.addEventListener(MouseEvent.ROLL_OVER, handleMouseIn); + this.addEventListener(MouseEvent.ROLL_OUT, handleMouseOut); + } + + private function checkVisibility():void { + toolbarVisible = (toolbarAllowed() && mousedOver); + } + + public function closeToolbar(e:TimerEvent = null):void { + mousedOver = false; + checkVisibility(); + //parent.removeChild(this); + } + + private function handleMouseIn(e:MouseEvent = null):void { + _toolbarHideTimer.reset(); + mousedOver = true; + checkVisibility(); + } + + private function handleMouseOut(e:MouseEvent = null):void { + _toolbarHideTimer.reset(); + _toolbarHideTimer.start(); + } + + private function toolbarAllowed():Boolean { + // Created to make possible to create rules to allow or not the toolbar + return true; + } + + public function sendSaveEvent():void{ + var saveEvent:ChatToolbarButtonEvent = new ChatToolbarButtonEvent(ChatToolbarButtonEvent.SAVE_CHAT_TOOLBAR_EVENT); + globalDispatcher.dispatchEvent(saveEvent); + } + + public function sendCopyEvent():void{ + var copyEvent:ChatToolbarButtonEvent = new ChatToolbarButtonEvent(ChatToolbarButtonEvent.COPY_CHAT_TOOLBAR_EVENT); + globalDispatcher.dispatchEvent(copyEvent); + } + + public function registerListeners(component:UIComponent):void { + component.addEventListener(MouseEvent.ROLL_OVER, handleMouseIn); + component.addEventListener(MouseEvent.ROLL_OUT, handleMouseOut); + } + ]]> + </mx:Script> + + <mx:Fade id="fadeOut" duration="200" alphaFrom="1.0" alphaTo="0.0" /> + <mx:Fade id="fadeIn" duration="200" alphaFrom="0.0" alphaTo="1.0" /> + + <mx:Button id="saveBtn" label="{ResourceUtil.getInstance().getString('bbb.chat.saveBtn.label')}" + styleName="chatToolbarSaveButtonStyle" + toolTip="{ResourceUtil.getInstance().getString('bbb.chat.saveBtn.toolTip')}" + click="sendSaveEvent()" + tabIndex="{baseIndex+1}" + accessibilityName="{ResourceUtil.getInstance().getString('bbb.chat.copyBtn.accessibilityName')}"/> + + <mx:Button id="copyBtn" label="{ResourceUtil.getInstance().getString('bbb.chat.copyBtn.label')}" + styleName="chatToolbarCopyButtonStyle" + toolTip="{ResourceUtil.getInstance().getString('bbb.chat.copyBtn.toolTip')}" + click="sendCopyEvent()" + tabIndex="{baseIndex+2}" + accessibilityName="{ResourceUtil.getInstance().getString('bbb.chat.copyBtn.accessibilityName')}"/> + +</mx:HBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatView.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatView.mxml index f71d7bb55034d254408fe5411da029a659900306..87bc8ec6a5c2c8c638241b888ae564dfea6aca6b 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatView.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/chat/views/ChatView.mxml @@ -27,11 +27,13 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:flexlib="http://code.google.com/p/flexlib/" width="100%" height="100%" xmlns:containers="flexlib.containers.*" verticalScrollPolicy="off"> - + <mate:Listener type="{PrivateChatMessageEvent.PRIVATE_CHAT_MESSAGE_EVENT}" method="handlePrivateChatMessageEvent"/> <mate:Listener type="{PublicChatMessageEvent.PUBLIC_CHAT_MESSAGE_EVENT}" method="handlePublicChatMessageEvent"/> <mate:Listener type="{EventConstants.START_PRIVATE_CHAT}" method="handleStartPrivateChatMessageEvent"/> <mate:Listener type="{ShortcutEvent.FOCUS_CHAT_TABS}" method="focusChatTabs" /> + <mate:Listener type="{ChatToolbarButtonEvent.SAVE_CHAT_TOOLBAR_EVENT}" method="dispatchSaveChatEvent" /> + <mate:Listener type="{ChatToolbarButtonEvent.COPY_CHAT_TOOLBAR_EVENT}" method="dispatchCopyChatEvent" /> <mx:Script> <![CDATA[ @@ -61,7 +63,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.main.events.ShortcutEvent; import org.bigbluebutton.main.model.users.BBBUser; import org.bigbluebutton.modules.chat.ChatUtil; + import org.bigbluebutton.modules.chat.events.ChatCopyEvent; import org.bigbluebutton.modules.chat.events.ChatOptionsEvent; + import org.bigbluebutton.modules.chat.events.ChatSaveEvent; import org.bigbluebutton.modules.chat.events.PrivateChatMessageEvent; import org.bigbluebutton.modules.chat.events.PublicChatMessageEvent; import org.bigbluebutton.modules.chat.events.SendPrivateChatMessageEvent; @@ -70,6 +74,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.chat.model.business.UserVO; import org.bigbluebutton.modules.chat.vo.ChatMessageVO; import org.bigbluebutton.util.i18n.ResourceUtil; + import org.bigbluebutton.modules.chat.events.ChatToolbarButtonEvent; private static const PUBLIC_CHAT_USERID:String = 'public_chat_userid'; private var PUBLIC_CHAT_USERNAME:String = ResourceUtil.getInstance().getString("bbb.chat.publicChatUsername"); @@ -85,7 +90,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private static const PUBLIC_TAB_NEW:String = ResourceUtil.getInstance().getString("bbb.accessibility.chat.chatView.publicTabNew"); private var publicWaiting:Boolean = false; - private var publicFocus:Boolean = false; + private var publicFocus:Boolean = false; private var noticeLabel:String; [Embed(source="../sounds/notice.mp3")] @@ -93,7 +98,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private var noticeSound:Sound = new noticeSoundClass() as Sound; // All author and license information for the use of this sound can be found in: // src/org/bigbluebutton/modules/chat/sounds/license.txt - + // Initialization private function init():void { chatOptions = new ChatOptions(); @@ -102,7 +107,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. baseIndex = chatOptions.getBaseIndex() + 4; } - + private function onCreationComplete():void{ openChatBoxFor(PUBLIC_CHAT_USERID, true); makePublicChatUncloseable(); @@ -120,6 +125,33 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. systemManager.stage.addEventListener(Event.ACTIVATE, activate); systemManager.stage.addEventListener(Event.DEACTIVATE, deactivate); } + + private function getVisibleChatBox():ChatBox { + var chatBox:ChatBox = chatTabs.getChildAt(chatTabs.selectedIndex) as ChatBox; + return chatBox; + } + + private function dispatchSaveChatEvent(e:Event):void { + var chatBox:ChatBox = getVisibleChatBox(); + var saveEvent:ChatSaveEvent = new ChatSaveEvent(ChatSaveEvent.SAVE_CHAT_EVENT); + + if (chatBox.chatWithUsername == null || chatBox.chatWithUsername == "") { + saveEvent.filename = ResourceUtil.getInstance().getString('bbb.chat.save.filename'); + } else { + saveEvent.filename = chatBox.chatWithUsername; + } + + saveEvent.chatMessages = chatBox.getChatMessages(); + globalDispatcher.dispatchEvent(saveEvent); + } + + private function dispatchCopyChatEvent(e:Event):void { + var chatBox:ChatBox = getVisibleChatBox(); + var copyEvent:ChatCopyEvent = new ChatCopyEvent(ChatCopyEvent.COPY_CHAT_EVENT); + + copyEvent.chatMessages = chatBox.getChatMessages(); + globalDispatcher.dispatchEvent(copyEvent); + } private function focusChatTabs(e:ShortcutEvent):void{ focusManager.setFocus(chatTabs); @@ -309,7 +341,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. // Activates an audio alert for screen-reader users on public message reception - private function publicNotification():void { + private function publicNotification():void { publicWaiting = true; if (Accessibility.active){ noticeSound.play(); @@ -327,7 +359,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } } - public function publicChatFocus(event:FocusEvent):void{ + public function publicChatFocus(event:FocusEvent):void{ publicFocus = true; publicWaiting = false; } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/managers/SmartWindowResizer.as b/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/managers/SmartWindowResizer.as new file mode 100644 index 0000000000000000000000000000000000000000..a180561d00890498c914afea0dee524fd19e9e3a --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/managers/SmartWindowResizer.as @@ -0,0 +1,78 @@ +package org.bigbluebutton.modules.deskshare.managers { + public class SmartWindowResizer { + + static private var RESIZING_DIRECTION_UNKNOWN:int = 0; + static private var RESIZING_DIRECTION_VERTICAL:int = 1; + static private var RESIZING_DIRECTION_HORIZONTAL:int = 2; + static private var RESIZING_DIRECTION_BOTH:int = 3; + private var _resizeDirection:int; + + public function SmartWindowResizer() { + + } + + public function onResizeStart():void { + /** + * when the window is resized by the user, the application doesn't know + * about the resize direction + */ + _resizeDirection = RESIZING_DIRECTION_UNKNOWN; + } + + public function onResizeEnd():void { + /** + * after the resize ends, the direction is set to BOTH because of the + * non-user resize actions - like when the window is docked, and so on + */ + _resizeDirection = RESIZING_DIRECTION_BOTH; + } + + public function onResize(externalWidth:int, externalHeight:int, maximized:Boolean, internalWidth:int, internalHeight:int, internalAspectRatio:Number, keepInternalAspectRatio:Boolean, callback:Function):void { + var internalWidthCandidate:int = externalWidth; + var internalHeightCandidate:int = externalHeight; + + // try to discover in which direction the user is resizing the window + if (_resizeDirection != RESIZING_DIRECTION_BOTH) { + if (internalWidthCandidate == internalWidth && internalHeightCandidate != internalHeight) { + _resizeDirection = (_resizeDirection == RESIZING_DIRECTION_VERTICAL || _resizeDirection == RESIZING_DIRECTION_UNKNOWN? RESIZING_DIRECTION_VERTICAL: RESIZING_DIRECTION_BOTH); + } else if (internalWidthCandidate != internalWidth && internalHeightCandidate == internalHeight) { + _resizeDirection = (_resizeDirection == RESIZING_DIRECTION_HORIZONTAL || _resizeDirection == RESIZING_DIRECTION_UNKNOWN? RESIZING_DIRECTION_HORIZONTAL: RESIZING_DIRECTION_BOTH); + } else { + _resizeDirection = RESIZING_DIRECTION_BOTH; + } + } + + // depending on the direction, the tmp size is different + switch (_resizeDirection) { + case RESIZING_DIRECTION_VERTICAL: + internalWidthCandidate = Math.floor(internalHeightCandidate * internalAspectRatio); + break; + case RESIZING_DIRECTION_HORIZONTAL: + internalHeightCandidate = Math.floor(internalWidthCandidate / internalAspectRatio); + break; + case RESIZING_DIRECTION_BOTH: + // this direction is used also for non-user window resize actions + internalWidthCandidate = Math.min (internalWidthCandidate, Math.floor(internalHeightCandidate * internalAspectRatio)); + internalHeightCandidate = Math.min (internalHeightCandidate, Math.floor(internalWidthCandidate / internalAspectRatio)); + break; + } + + var internalOffsetX:int; + var internalOffsetY:int; + + if (!keepInternalAspectRatio || maximized) { + // center the video in the window + internalOffsetX = Math.floor ((externalWidth - internalWidthCandidate) / 2); + internalOffsetY = Math.floor ((externalHeight - internalHeightCandidate) / 2); + } else { + // fit window dimensions on video + internalOffsetX = 0; + internalOffsetY = 0; + externalWidth = internalWidthCandidate; + externalHeight = internalHeightCandidate; + } + + callback(externalWidth, externalHeight, internalWidthCandidate, internalHeightCandidate, internalOffsetX, internalOffsetY); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopPublishWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopPublishWindow.mxml index 033bd34bf6c01d8839685e4d4bb3368b0ea6f18a..acd363cc2c74554b4923cff2be2e5c6a046ad672 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopPublishWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopPublishWindow.mxml @@ -71,9 +71,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.deskshare.utils.JavaCheck; import org.bigbluebutton.util.i18n.ResourceUtil; - public static const SCALE:Number = 5; - private static const VID_HEIGHT_PAD:Number = 73; - private static const VID_WIDTH_PAD:Number = 6; + private const DEFAULT_PREVIEW_WIDTH:int = 320; + private const DEFAULT_PREVIEW_HEIGHT:int = 180; // 16x9 aspect ratio private var images:Images = new Images(); [Bindable] public var bbbLogo:Class = images.bbb_logo; @@ -231,15 +230,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. // as it results in a flickering and jerky mouse pointer (ralam jun 10, 2010). cursor.x = video.x + ((event.x/captureWidth)) * video.width; cursor.y = video.y + ((event.y/captureHeight)) * video.height; - cursorImg.visible = true; // Do not display cursor if they are outside the capture area. - if (cursor.x < video.x) cursor.x = video.x; - if (cursor.y < video.y) cursor.y = video.y; - if (cursor.x > video.x + video.width) cursor.x = video.x + video.width; - if (cursor.y > video.y + video.height) cursor.y = video.y + video.height; - cursorImg.x = cursor.x; - cursorImg.y = cursor.y; + cursorImg.visible = !(cursor.x < video.x + || cursor.y < video.y + || cursor.x > video.x + video.width + || cursor.y > video.y + video.height); + cursorImg.x = cursor.x; + cursorImg.y = cursor.y; } private function onAppletStart(event:AppletStartedEvent):void{ @@ -257,35 +255,29 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. // Store capture dimensions so we can position cursor properly. captureWidth = capWidth; captureHeight = capHeight; + var captureAspectRatio:Number = captureWidth / captureHeight; + var videoHolderAspectRatio:Number = DEFAULT_PREVIEW_WIDTH / DEFAULT_PREVIEW_HEIGHT; videoHolder = new UIComponent(); - var vidW:Number = captureWidth; - var vidH:Number = captureHeight; - - // Don't scale if capture dimension is smaller than window. - if((captureWidth > this.width - VID_WIDTH_PAD) && (captureHeight < this.height - VID_HEIGHT_PAD)){ - vidW = this.width - VID_WIDTH_PAD; - vidH = (captureHeight / captureWidth) * vidW; - } - else if( ((captureWidth < this.width - VID_WIDTH_PAD) && (captureHeight > this.height - VID_HEIGHT_PAD)) - || ((captureWidth > this.width - VID_WIDTH_PAD) && (captureHeight > this.height - VID_HEIGHT_PAD)) ){ - vidH = this.height - VID_HEIGHT_PAD; - vidW = (captureWidth / captureHeight) * vidH; - } - else{ - vidW = captureWidth; - vidH = captureHeight; + var vidW:Number; + var vidH:Number; + if (captureAspectRatio > videoHolderAspectRatio) { + vidW = DEFAULT_PREVIEW_WIDTH; + vidH = Math.floor(DEFAULT_PREVIEW_WIDTH / captureAspectRatio); + } else { + vidH = DEFAULT_PREVIEW_HEIGHT; + vidW = Math.floor(DEFAULT_PREVIEW_HEIGHT * captureAspectRatio); } - LogUtil.debug("deskshare preview[" + captureWidth + "," + captureHeight + "][" + vidW + "," + vidH + "]"); + trace("deskshare preview[" + captureWidth + "," + captureHeight + "][" + vidW + "," + vidH + "]"); video = new Video(vidW, vidH); video.width = vidW; video.height = vidH; - videoHolder.width = vidW; - videoHolder.height = vidH; - video.x = videoHolder.x = (this.width - VID_WIDTH_PAD - vidW) / 2; - video.y = videoHolder.y = (this.height - VID_HEIGHT_PAD - vidH) / 2; + videoHolder.width = DEFAULT_PREVIEW_WIDTH; + videoHolder.height = DEFAULT_PREVIEW_HEIGHT; + video.x = Math.floor((DEFAULT_PREVIEW_WIDTH - video.width) / 2); + video.y = Math.floor((DEFAULT_PREVIEW_HEIGHT - video.height) / 2); videoHolder.addChild(video); diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopViewToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopViewToolbar.mxml new file mode 100644 index 0000000000000000000000000000000000000000..0ce614017abd5281dc97873a01a8b429a5d691b5 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopViewToolbar.mxml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> + +<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" + xmlns:mate="http://mate.asfusion.com/" + initialize="init()" + creationComplete="onCreationComplete()" + visible="{toolbarVisible}" + horizontalAlign="center" + verticalAlign="middle" + paddingTop="0" paddingBottom="0" + hideEffect="{fadeOut}" showEffect="{fadeIn}" > + <mx:Script> + <![CDATA[ + import mx.core.UIComponent; + import com.asfusion.mate.events.Dispatcher; + + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.util.i18n.ResourceUtil; + + [Bindable] private var toolbarVisible:Boolean = false; + + private var globalDispatcher:Dispatcher; + private var _toolbarHideTimer:Timer; + + public function init():void{ + _toolbarHideTimer = new Timer(1000, 1); + _toolbarHideTimer.addEventListener(TimerEvent.TIMER, onHideTimerComplete); + } + + private function onCreationComplete():void { + globalDispatcher = new Dispatcher(); + } + + private function handleMouseOver(e:MouseEvent = null):void { + showToolbar(); + } + + private function handleMouseOut(e:MouseEvent = null):void { + hideToolbar(); + } + + private function onHideTimerComplete(event:TimerEvent):void { + toolbarVisible = false; + } + + private function showToolbar():void { + _toolbarHideTimer.reset(); + toolbarVisible = toolbarAllowed; + } + + private function hideToolbar():void { + _toolbarHideTimer.reset(); + _toolbarHideTimer.start(); + } + + private function get toolbarAllowed():Boolean { + // Created to make possible to create rules to allow or not the toolbar + return true; + } + + public function registerListeners(component:UIComponent):void { + component.addEventListener(MouseEvent.MOUSE_OVER, handleMouseOver); + component.addEventListener(MouseEvent.MOUSE_OUT, handleMouseOut); + } + ]]> + </mx:Script> + + <mx:Fade id="fadeOut" duration="200" alphaFrom="1.0" alphaTo="0.0" /> + <mx:Fade id="fadeIn" duration="200" alphaFrom="0.0" alphaTo="1.0" /> +</mx:HBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopViewWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopViewWindow.mxml index d17a955645fdb5687407bd6a21a50f80ae0f2dd6..a1378129fec187516f59252604c25ad886d21f78 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopViewWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/deskshare/view/components/DesktopViewWindow.mxml @@ -22,6 +22,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <CustomMdiWindow xmlns="org.bigbluebutton.common.*" xmlns:mx="http://www.adobe.com/2006/mxml" + xmlns:views="org.bigbluebutton.modules.deskshare.view.components.*" width="600" height="400" initialize="init()" creationComplete="onCreationComplete()" @@ -29,7 +30,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:mate="http://mate.asfusion.com/" title="{ResourceUtil.getInstance().getString('bbb.desktopView.title')}" showCloseButton="false" - resize="fitToWindow()" > + resize="onResizeEvent()" + layout="absolute" > <mate:Listener type="{ViewStreamEvent.STOP}" method="onStopViewStreamEvent" /> <mate:Listener type="{CursorEvent.UPDATE_CURSOR_LOC_EVENT}" method="onUpdateCursorEvent" /> @@ -51,14 +53,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.deskshare.events.ViewStreamEvent; import org.bigbluebutton.modules.deskshare.events.ViewWindowEvent; import org.bigbluebutton.modules.deskshare.model.DeskshareOptions; + import org.bigbluebutton.modules.deskshare.managers.SmartWindowResizer; import org.bigbluebutton.util.i18n.ResourceUtil; - private var screenHeight:Number = Capabilities.screenResolutionY; - private var screenWidth:Number = Capabilities.screenResolutionX; - private var images:Images = new Images(); - [Bindable] public var fitToWidthIcon:Class = images.magnifier; - [Bindable] public var fitToActualSizeIcon:Class = images.mag_reset; private var cursor:Shape; @@ -69,8 +67,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private var videoHeight:Number; private var videoWidth:Number; - private static const VIDEO_WIDTH_PADDING:int = 7; - private static const VIDEO_HEIGHT_PADDING:int = 65; + [Bindable] private var actualSize:Boolean = false; // The following code block is to deal with a bug in FLexLib // with MDI windows not responding well to being maximized @@ -79,6 +76,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private var savedX:Number; private var savedY:Number; private var isMaximized:Boolean = false; + private var resizer:SmartWindowResizer = new SmartWindowResizer(); [Bindable] private var baseIndex:int; [Bindable] private var dsOptions:DeskshareOptions; @@ -89,19 +87,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. } private function onCreationComplete():void{ - videoHolder.addChild(video); - this.addChild(videoHolder); + videoHolderCanvas.addChild(videoHolder); videoHolder.percentWidth = 100; videoHolder.percentHeight = 100; - addEventListener(MDIWindowEvent.RESIZE_END, onResizeEndEvent); - fitToActualSize(); + addEventListener(MDIWindowEvent.RESIZE_START, onResizeStartEvent); + addEventListener(MDIWindowEvent.RESIZE_END, onResizeEndEvent); cursor = new Shape(); cursor.graphics.lineStyle(6, 0xFF0000, 0.6); cursor.graphics.drawCircle(0,0,3); videoHolder.addChild(cursor); videoHolder.addChild(cursorImg); cursor.visible = false; - maximize(); resourcesChanged(); @@ -109,27 +105,67 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. minimizeBtn.tabIndex = baseIndex+1; maximizeRestoreBtn.tabIndex = baseIndex+2; closeBtn.tabIndex = baseIndex+3; + + bottomBar.registerListeners(this); + onResizeEvent(); } + private function onResizeStartEvent(event:MDIWindowEvent):void { + if (event.window == this) { + resizer.onResizeStart(); + } + } + private function onResizeEndEvent(event:MDIWindowEvent):void { if (event.window == this) { - fitToWindow(); + resizer.onResizeEnd(); + } + } + + private function onResizeEvent():void { + if (this.minimized) { + return; + } + + if (actualSize) { + onResizeCallback(this.width - horizontalBorder, this.height - verticalBorder, videoWidth, videoHeight, Math.max((this.width - horizontalBorder - videoWidth) / 2, 0), Math.max((this.height - verticalBorder - videoHeight) / 2, 0)); + } else { + resizer.onResize(this.width - horizontalBorder, this.height - verticalBorder, this.maximized, video.width, video.height, videoWidth / videoHeight, false, onResizeCallback); } } + + private function onResizeCallback(externalWidth:int, externalHeight:int, internalWidth:int, internalHeight:int, internalOffsetX:int, internalOffsetY:int):void { + this.width = externalWidth + horizontalBorder; + this.height = externalHeight + verticalBorder; + + /* Reposition video within window */ + videoHolder.x = internalOffsetX; + videoHolder.y = internalOffsetY; + + videoHolder.width = video.width = internalWidth; + videoHolder.height = video.height = internalHeight; + } + + protected function get verticalBorder():Number { + return this.borderMetrics.top + this.borderMetrics.bottom; + } + protected function get horizontalBorder():Number { + return this.borderMetrics.left + this.borderMetrics.right; + } + private function onUpdateCursorEvent(event:CursorEvent):void { - cursor.x = ((event.x/videoWidth)) * videoHolder.width; - cursor.y = ((event.y/videoHeight)) * videoHolder.height; + cursor.x = ((event.x/videoWidth)) * video.width; + cursor.y = ((event.y/videoHeight)) * video.height; cursorImg.visible = true; // DO NOT compute the x and y coordinate and assign directly to the cursorImg // as it results in a flickering and jerky mouse pointer (ralam jun 10, 2010). - if (cursor.x < videoHolder.x) cursor.x = videoHolder.x; - if (cursor.y < videoHolder.y) cursor.y = videoHolder.y; - if (cursor.x > videoHolder.x + videoHolder.width) cursor.x = videoHolder.x + videoHolder.width; - if (cursor.y > videoHolder.y + videoHolder.height) cursor.y = videoHolder.y + videoHolder.height; - cursorImg.x = cursor.x; - cursorImg.y = cursor.y; + if (cursor.x < video.x) cursor.x = video.x; + if (cursor.y < video.y) cursor.y = video.y; + if (cursor.x > video.x + video.width) cursor.x = video.x + video.width; + if (cursor.y > video.y + video.height) cursor.y = video.y + video.height; + cursorImg.move(cursor.x, cursor.y); } public function startVideo(connection:NetConnection, stream:String, width:Number, height:Number):void{ @@ -153,21 +189,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. this.stream = stream; this.title = "Viewing Remote Desktop"; + videoHolder.addChild(video); + onResizeEvent(); } - protected function updateButtonsPosition():void { - if (this.width < bottomBar.width) { - bottomBar.visible = false; - } - - if (bottomBar.visible == false) { - bottomBar.y = bottomBar.x = 0; - } else { - bottomBar.y = (this.height - bottomBar.height) / 2; - bottomBar.x = (this.width - bottomBar.width) / 2; - } - } - public function stopViewing():void { ns.close(); closeWindow(); @@ -201,85 +226,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. public function getPrefferedPosition():String{ return MainCanvas.DESKTOP_SHARING_VIEW; } - - /** - * resizes the desktop sharing video to fit to this window - */ - private function fitToWindow():void{ - if (videoIsSmallerThanWindow()) { - fitWindowToVideo(); - } - - // Ignore if we are displaying the actual size of the video - if (! btnActualSize.selected) { - fitVideoToWindow(); - } - } - - private function fitVideoToWindow():void { - if (this.width < this.height) { - fitToWidthAndAdjustHeightToMaintainAspectRatio(); - } else { - fitToHeightAndAdjustWidthToMaintainAspectRatio(); - } - } - - private function fitWindowToVideo():void { - video.width = videoWidth; - videoHolder.width = videoWidth; - video.height = videoHeight; - videoHolder.height = videoHeight; - this.height = videoHeight + VIDEO_HEIGHT_PADDING; - this.width = videoWidth + VIDEO_WIDTH_PADDING; - } - - private function videoIsSmallerThanWindow():Boolean { - return (videoHeight < this.height) && (videoWidth < this.width); - } - - - private function fitToWidthAndAdjustHeightToMaintainAspectRatio():void { - video.width = this.width - VIDEO_WIDTH_PADDING; - videoHolder.width = video.width; - // Maintain aspect-ratio - video.height = (videoHeight * video.width) / videoWidth; - videoHolder.height = video.height; - this.height = video.height + VIDEO_HEIGHT_PADDING; - } - - private function fitToHeightAndAdjustWidthToMaintainAspectRatio():void { - video.height = this.height - VIDEO_HEIGHT_PADDING; - videoHolder.height = video.height; - // Maintain aspect-ratio - video.width = (videoWidth * video.height) / videoHeight; - videoHolder.width = video.width; - this.width = video.width + VIDEO_WIDTH_PADDING; - } - - /** - * resizes the desktop sharing video to actual video resolution - */ - private function fitToActualSize():void{ - if (videoIsSmallerThanWindow()) { - fitWindowToVideo(); - } else { - video.width = videoWidth; - videoHolder.width = videoWidth; - video.height = videoHeight; - videoHolder.height = videoHeight; - } - } - private function determineHowToDisplayVideo():void { - if (btnActualSize.selected) { - fitToActualSize(); - btnActualSize.toolTip = ResourceUtil.getInstance().getString('bbb.desktopView.fitToWindow'); - btnActualSize.label = ResourceUtil.getInstance().getString('bbb.desktopView.fitToWindow'); - } else { - fitToWindow(); - btnActualSize.toolTip = ResourceUtil.getInstance().getString('bbb.desktopView.actualSize'); - btnActualSize.label = ResourceUtil.getInstance().getString('bbb.desktopView.actualSize'); - } + private function toggleActualSize():void { + actualSize = !actualSize; + onResizeEvent(); } private function closeWindow():void { @@ -308,17 +258,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. ]]> </mx:Script> - <mx:Move id="cursorMove" target="{cursorImg}"/> <mx:Image id="cursorImg" visible="false" source="@Embed('../../assets/images/cursor4.png')"/> - - <mx:HBox id="bottomBar" visible="true" height="30" horizontalAlign="center" paddingTop="0" paddingBottom="0" width="100%" > + + <mx:Canvas width="100%" height="100%" id="videoHolderCanvas" /> + + <views:DesktopViewToolbar x="0" y="{videoHolderCanvas.height - bottomBar.height - 16}" id="bottomBar" height="28" width="100%" > <mx:Button id="btnActualSize" paddingTop="0" paddingBottom="0" styleName="deskshareControlButtonStyle" - toggle="true" - click="determineHowToDisplayVideo()" - selected="false" - height="90%" - label="{btnActualSize.selected ? ResourceUtil.getInstance().getString('bbb.desktopView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.desktopView.actualSize')}" - toolTip="{btnActualSize.selected ? ResourceUtil.getInstance().getString('bbb.desktopView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.desktopView.actualSize')}" + click="toggleActualSize()" + height="28" + label="{actualSize ? ResourceUtil.getInstance().getString('bbb.desktopView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.desktopView.actualSize')}" + toolTip="{actualSize ? ResourceUtil.getInstance().getString('bbb.desktopView.fitToWindow') : ResourceUtil.getInstance().getString('bbb.desktopView.actualSize')}" tabIndex="{baseIndex+4}"/> - </mx:HBox> + </views:DesktopViewToolbar> </CustomMdiWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/managers/LayoutManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/managers/LayoutManager.as index c8de8cfb70486a600a555da682d3b49e59f006d3..bf902510746f2db914222f287ac210606a62b6ff 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/managers/LayoutManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/managers/LayoutManager.as @@ -125,10 +125,27 @@ package org.bigbluebutton.modules.layout.managers }); _fileRef.save(_layoutModel.toString(), "layouts.xml"); } - + + public function loadLayoutsFromFile():void { + var loader:LayoutLoader = new LayoutLoader(); + loader.addEventListener(LayoutsLoadedEvent.LAYOUTS_LOADED_EVENT, function(e:LayoutsLoadedEvent):void { + if (e.success) { + _layoutModel.addLayouts(e.layouts); + applyLayout(_layoutModel.getDefaultLayout()); + broadcastLayouts(); + Alert.show(ResourceUtil.getInstance().getString('bbb.layout.load.complete'), "", Alert.OK, _canvas); + } else + Alert.show(ResourceUtil.getInstance().getString('bbb.layout.load.failed'), "", Alert.OK, _canvas); + }); + loader.loadFromLocalFile(); + } + public function addCurrentLayoutToList():void { + _currentLayout.name += " " + (++_customLayoutsCount); _layoutModel.addLayout(_currentLayout); - + updateCurrentLayout(_currentLayout); + broadcastLayouts(); + var redefineLayout:LayoutFromRemoteEvent = new LayoutFromRemoteEvent(); redefineLayout.layout = _currentLayout; // this is to force LayoutCombo to update the current label @@ -213,6 +230,8 @@ package org.bigbluebutton.modules.layout.managers //trace(LOG + " layout changed by me. Sync others to this new layout."); var e:SyncLayoutEvent = new SyncLayoutEvent(_currentLayout); _globalDispatcher.dispatchEvent(e); + + Alert.show(ResourceUtil.getInstance().getString('bbb.layout.sync'), "", Alert.OK, _canvas); } private function sendLayoutUpdate(layout:LayoutDefinition):void { @@ -228,6 +247,7 @@ package org.bigbluebutton.modules.layout.managers if (layout != null) { layout.applyToCanvas(_canvas); dispatchSwitchedLayoutEvent(layout.name); + UserManager.getInstance().getConference().numAdditionalSharedNotes = layout.numAdditionalSharedNotes; } //trace(LOG + " applyLayout layout [" + layout.name + "]"); updateCurrentLayout(layout); @@ -319,6 +339,7 @@ package org.bigbluebutton.modules.layout.managers //trace(LOG + "updateCurrentLayout - currentLayout = [" + layout.name + "]"); layout.currentLayout = true; } else { + _globalDispatcher.dispatchEvent(new LayoutEvent(LayoutEvent.INVALIDATE_LAYOUT_EVENT)); _currentLayout = LayoutDefinition.getLayout(_canvas, ResourceUtil.getInstance().getString('bbb.layout.combo.customName')); //trace(LOG + "updateCurrentLayout - layout is NULL! Setting currentLayout = [" + _currentLayout.name + "]"); } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/maps/LayoutEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/maps/LayoutEventMap.mxml index 877b027688c6870fc7e5219023a578c3d9107bb4..c36a54feff572f6e3d9a1638270eaf3eaa053b40 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/maps/LayoutEventMap.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/maps/LayoutEventMap.mxml @@ -24,6 +24,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import mx.events.FlexEvent; import org.bigbluebutton.core.EventConstants; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; import org.bigbluebutton.main.events.MadePresenterEvent; import org.bigbluebutton.modules.layout.events.ChangeLayoutEvent; import org.bigbluebutton.modules.layout.events.ComboBoxCreatedEvent; @@ -62,7 +63,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <EventHandlers type="{LayoutEvent.STOP_LAYOUT_MODULE_EVENT}"> <MethodInvoker generator="{LayoutEventMapDelegate}" method="stopModule" /> </EventHandlers> - + + <EventHandlers type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}"> + <MethodInvoker generator="{LayoutEventMapDelegate}" method="refreshRole" arguments="{event}"/> + </EventHandlers> + <EventHandlers type="{LayoutEvent.APPLY_DEFAULT_LAYOUT_EVENT}"> <MethodInvoker generator="{LayoutManager}" method="applyDefaultLayout" /> </EventHandlers> @@ -100,6 +105,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <MethodInvoker generator="{LayoutManager}" method="saveLayoutsToFile" /> </EventHandlers> + <EventHandlers type="{LayoutEvent.LOAD_LAYOUTS_EVENT}"> + <MethodInvoker generator="{LayoutManager}" method="loadLayoutsFromFile" /> + </EventHandlers> + <EventHandlers type="{LayoutEvent.ADD_CURRENT_LAYOUT_EVENT}"> <MethodInvoker generator="{LayoutManager}" method="addCurrentLayoutToList" /> </EventHandlers> @@ -121,4 +130,4 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <PropertyInjector targetKey="sender" source="{MessageSender}"/> </Injectors> -</EventMap> \ No newline at end of file +</EventMap> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/maps/LayoutEventMapDelegate.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/maps/LayoutEventMapDelegate.mxml index 293ceda75ffa4e71a017ca7dd7b74b14d15dd5e4..a3331bdc926344dc3f7e2c4ae5ea33ee23177346 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/maps/LayoutEventMapDelegate.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/maps/LayoutEventMapDelegate.mxml @@ -24,13 +24,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import com.asfusion.mate.events.Dispatcher; import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.common.Role; import org.bigbluebutton.common.events.ToolbarButtonEvent; import org.bigbluebutton.main.model.LayoutOptions; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; import org.bigbluebutton.modules.layout.views.ToolbarComponent; private var _attributes:Object; private var _globalDispatcher:Dispatcher = new Dispatcher(); private var options:LayoutOptions = new LayoutOptions(); + private var layoutComponent:ToolbarComponent; public function stopModule():void { } @@ -39,7 +42,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. _attributes = attributes; options.parseOptions(); - var layoutComponent:ToolbarComponent = new ToolbarComponent(); + layoutComponent = new ToolbarComponent(); // using the negation will keep it enabled if the property is not there layoutComponent.enableEdit = !(_attributes.enableEdit == "false"); @@ -50,6 +53,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. _globalDispatcher.dispatchEvent(event); } + + public function refreshRole(e:ChangeMyRole):void { + layoutComponent.enableEdit = !(_attributes.enableEdit == "false"); + layoutComponent.refreshRole(e.role == Role.MODERATOR); + } ]]> </mx:Script> </EventMap> \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/model/LayoutDefinition.as b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/model/LayoutDefinition.as index 8319197cd88a3cab566019ac85ba270e3a8abccf..d8e637016bf67ea3f2cb411aa0f567bb34a2960a 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/model/LayoutDefinition.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/model/LayoutDefinition.as @@ -43,7 +43,7 @@ package org.bigbluebutton.modules.layout.model { static private var _ignoredWindows:Array = new Array("AvatarWindow", "PublishWindow", "VideoWindow", "DesktopPublishWindow", "DesktopViewWindow", - "LogWindow"); + "LogWindow", "NetworkStatsWindow"); static private var _roles:Array = new Array(Role.VIEWER, Role.MODERATOR, Role.PRESENTER); private function loadLayout(vxml:XML):void { @@ -313,5 +313,15 @@ package org.bigbluebutton.modules.layout.model { } return layoutDefinition; } + + public function get numAdditionalSharedNotes():Number { + var sharedNotesCounter:int = 0; + for each (var window:WindowLayout in _layoutsPerRole[Role.VIEWER]) { + if (window.name.indexOf("AdditionalSharedNotesWindow") != -1) { + sharedNotesCounter++; + } + } + return sharedNotesCounter; + } } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/model/WindowLayout.as b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/model/WindowLayout.as index 9f521083714c846e448954868b148dcca89b07d6..5108dcfc3c890c2b7fee083e9f1cf80cf2b7e430 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/model/WindowLayout.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/model/WindowLayout.as @@ -252,12 +252,16 @@ package org.bigbluebutton.modules.layout.model { } static public function getType(obj:Object):String { - var qualifiedClass:String = String(getQualifiedClassName(obj)); - var pattern:RegExp = /(\w+)::(\w+)/g; - if (qualifiedClass.match(pattern)) { - return qualifiedClass.split("::")[1]; - } else { - return String(Object).substr(String(Object).lastIndexOf(".") + 1).match(/[a-zA-Z]+/).join(); + if (obj.hasOwnProperty("windowName")) { + return obj.windowName; + } else { + var qualifiedClass:String = String(getQualifiedClassName(obj)); + var pattern:RegExp = /(\w+)::(\w+)/g; + if (qualifiedClass.match(pattern)) { + return qualifiedClass.split("::")[1]; + } else { + return String(Object).substr(String(Object).lastIndexOf(".") + 1).match(/[a-zA-Z]+/).join(); + } } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/services/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/services/MessageReceiver.as index dfdcac3adbd36ea49e1ef557e85d40b9c7e41f35..bfda4775b726e066e85d299519d63e3e8775e9f0 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/services/MessageReceiver.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/services/MessageReceiver.as @@ -64,7 +64,12 @@ package org.bigbluebutton.modules.layout.services lockLayout(message.locked, message.setById); - _dispatcher.dispatchEvent(new LayoutEvent(LayoutEvent.APPLY_DEFAULT_LAYOUT_EVENT)); + if (message.layout == "") { + _dispatcher.dispatchEvent(new LayoutEvent(LayoutEvent.APPLY_DEFAULT_LAYOUT_EVENT)); + } else { + // it means that the moderator has used the broadcast layout method before the user joins + handleSyncLayout(message); + } _dispatcher.dispatchEvent(new ModuleLoadEvent(ModuleLoadEvent.LAYOUT_MODULE_STARTED)); } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/AddButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/AddButton.mxml index 5492a9a2ac8317c4295ea359b5d73351c518bf38..ae591a6d0f15e7f14359098943370bf2b558e630 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/AddButton.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/AddButton.mxml @@ -23,7 +23,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:mate="http://mate.asfusion.com/" xmlns:views="org.bigbluebutton.modules.layout.views.*" toolTip="{ResourceUtil.getInstance().getString('bbb.layout.addButton.toolTip')}" - icon="{icon_add}" + styleName="addLayoutButtonStyle" click="onClick(event)" enabled="{UserManager.getInstance().getConference().amIModerator()}"> @@ -40,8 +40,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.layout.events.LayoutEvent; private var _dispatcher:Dispatcher = new Dispatcher(); - private var _images:Images = new Images(); - [Bindable] private var icon_add:Class = _images.add; private function init():void { } @@ -50,6 +48,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. _dispatcher.dispatchEvent(new LayoutEvent(LayoutEvent.ADD_CURRENT_LAYOUT_EVENT)); } + public function refreshRole(amIModerator:Boolean):void { + this.enabled = amIModerator; + } ]]> </mx:Script> </views:LayoutButton> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/BroadcastButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/BroadcastButton.mxml index 76d7a59b400a8665e277254720725c825038c0d8..2c7015b25e96a9f7c403a03cb06119823bcff265 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/BroadcastButton.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/BroadcastButton.mxml @@ -23,9 +23,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:mate="http://mate.asfusion.com/" xmlns:views="org.bigbluebutton.modules.layout.views.*" toolTip="{ResourceUtil.getInstance().getString('bbb.layout.broadcastButton.toolTip')}" - icon="{_icon}" - click="onClick(event)" - enabled="{UserManager.getInstance().getConference().amIModerator()}"> + styleName="broadcastLayoutButtonStyle" + click="onClick(event)"> <mx:Script> <![CDATA[ @@ -41,18 +40,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import flash.events.FocusEvent; private var _dispatcher:Dispatcher = new Dispatcher(); - private var _images:Images = new Images(); - [Bindable] private var _icon:Class = _images.page_link; private function init():void { - if (!UserManager.getInstance().getConference().amIModerator()) { - this.visible = false; - } + refreshRole(UserManager.getInstance().getConference().amIModerator()); } private function onClick(e:Event):void { _dispatcher.dispatchEvent(new LayoutEvent(LayoutEvent.BROADCAST_LAYOUT_EVENT)); } + + public function refreshRole(amIModerator:Boolean):void { + this.visible = this.includeInLayout = this.enabled = amIModerator; + } ]]> </mx:Script> </views:LayoutButton> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LayoutsCombo.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LayoutsCombo.mxml index ee5e171ba0b1445c7f764dd0debc1032ab3af89d..2cdc1a2acd599ef6fe060e82d386df549f06d059 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LayoutsCombo.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LayoutsCombo.mxml @@ -30,6 +30,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mate:Listener type="{LayoutsReadyEvent.LAYOUTS_READY}" method="populateLayoutsList"/> <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="lockSettingsChanged" /> <mate:Listener type="{LocaleChangeEvent.LOCALE_CHANGED}" method="localeChanged" /> + <mate:Listener type="{LayoutEvent.INVALIDATE_LAYOUT_EVENT}" method="invalidadeLayout" /> <mx:Script> <![CDATA[ @@ -88,12 +89,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. dataProvider = layoutNames; populateComboBox(); this.addEventListener(DropdownEvent.OPEN, openDropdownHandler); + refreshRole(UsersUtil.amIModerator()); } private function lockSettingsChanged(e:LockControlEvent):void { - var conference:Conference = UserManager.getInstance().getConference(); - var thisUser:BBBUser = conference.getMyUser(); - this.enabled = ! thisUser.lockedLayout; + refreshRole(UsersUtil.amIModerator()); } private function populateLayoutsList(e:LayoutsReadyEvent):void { @@ -157,6 +157,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. _dispatcher.dispatchEvent(new ChangeLayoutEvent(e.currentTarget.selectedItem.localeKey)); } + private function invalidadeLayout(e:Event):void { + selectedIndex = -1; + prompt = ResourceUtil.getInstance().getString('bbb.layout.combo.custom'); + } + + public function refreshRole(amIModerator:Boolean):void { + var conference:Conference = UserManager.getInstance().getConference(); + var thisUser:BBBUser = conference.getMyUser(); + this.enabled = ! thisUser.lockedLayout || amIModerator; + } ]]> </mx:Script> </mx:ComboBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LoadButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LoadButton.mxml index 92918e09876805dd4390a6cace1a913a035e5ce1..a2a65cc1a45ba540973ce8cf740bfc8559371fb6 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LoadButton.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LoadButton.mxml @@ -23,7 +23,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:mate="http://mate.asfusion.com/" xmlns:views="org.bigbluebutton.modules.layout.views.*" toolTip="{ResourceUtil.getInstance().getString('bbb.layout.loadButton.toolTip')}" - icon="{icon_load}" + styleName="loadLayoutButtonStyle" click="onClick(event)" enabled="{UserManager.getInstance().getConference().amIModerator()}"> @@ -40,8 +40,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.layout.events.LayoutEvent; private var _dispatcher:Dispatcher = new Dispatcher(); - private var _images:Images = new Images(); - [Bindable] private var icon_load:Class = _images.folder; private function init():void { } @@ -50,6 +48,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. _dispatcher.dispatchEvent(new LayoutEvent(LayoutEvent.LOAD_LAYOUTS_EVENT)); } + public function refreshRole(amIModerator:Boolean):void { + this.enabled = amIModerator; + } ]]> </mx:Script> </views:LayoutButton> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LockButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LockButton.mxml index 0095c7ceb38f3d76cc4e7fcff0a3ae24ec22b78c..bbae8a82b445704a1ea7d64765290376270f7777 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LockButton.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/LockButton.mxml @@ -49,9 +49,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. [Bindable] private var icon_unlocked:Class = _images.unlocked; private function init():void { - if (!UserManager.getInstance().getConference().amIModerator()) { - this.visible = false; - } + refreshRole(UserManager.getInstance().getConference().amIModerator()); } private function onClick(e:Event):void { @@ -73,6 +71,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. this.selected = false; this.setStyle("icon", icon_unlocked); } + + public function refreshRole(amIModerator:Boolean):void { + this.enabled = amIModerator; + if (amIModerator) { + this.visible = true; + } else { + this.visible = this.selected; + } + } ]]> </mx:Script> </views:LayoutButton> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/SaveButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/SaveButton.mxml index 3eed08904519fde39e46d42cd2d36525cacf3eba..b83087daf5216ed2bd74be9d01634da8e244917d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/SaveButton.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/SaveButton.mxml @@ -23,7 +23,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:mate="http://mate.asfusion.com/" xmlns:views="org.bigbluebutton.modules.layout.views.*" toolTip="{ResourceUtil.getInstance().getString('bbb.layout.saveButton.toolTip')}" - icon="{icon_save}" + styleName="saveLayoutButtonStyle" click="onClick(event)" enabled="{UserManager.getInstance().getConference().amIModerator()}"> @@ -40,8 +40,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.layout.events.LayoutEvent; private var _dispatcher:Dispatcher = new Dispatcher(); - private var _images:Images = new Images(); - [Bindable] private var icon_save:Class = _images.disk; private function init():void { } @@ -50,6 +48,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. _dispatcher.dispatchEvent(new LayoutEvent(LayoutEvent.SAVE_LAYOUTS_EVENT)); } + public function refreshRole(amIModerator:Boolean):void { + this.enabled = amIModerator; + } ]]> </mx:Script> </views:LayoutButton> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/ToolbarComponent.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/ToolbarComponent.mxml index 346e8537af15628c0ea83055820b292987e8921b..36b1b6be38e596fad00d9af0075c73b44fc3ec31 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/ToolbarComponent.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/layout/views/ToolbarComponent.mxml @@ -48,6 +48,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. public function set enableEdit(arg:Boolean):void { _enableEdit = arg && UserManager.getInstance().getConference().amIModerator(); } + + public function refreshRole(amIModerator:Boolean):void { + comboBox.refreshRole(amIModerator); + //lockButton.refreshRole(amIModerator); + addButton.refreshRole(amIModerator); + saveButton.refreshRole(amIModerator); + loadButton.refreshRole(amIModerator); + broadcastButton.refreshRole(amIModerator); + } + public function set visibleTools(arg:Boolean):void { _visibleTools = arg; @@ -91,10 +101,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. visible="{_enableEdit}" tabIndex="{baseIndex+3}"/> <views:BroadcastButton id="broadcastButton" - tabIndex="{baseIndex+4}" - enabled="{!lockButton.selected}"/> - <views:LockButton id="lockButton" + tabIndex="{baseIndex+4}"/> +<!-- <views:LockButton id="lockButton" tabIndex="{baseIndex+5}" visible="false" includeInLayout="false"/> + --> </mx:HBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as index 6cc32b3ec809ab74d9b05594eca684fa3cb4cd71..954c072c46dde2cd9f1763a14180ccf496b30d8d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/PhoneOptions.as @@ -21,6 +21,8 @@ package org.bigbluebutton.modules.phone import org.bigbluebutton.core.BBB; public class PhoneOptions { + static public var firstAudioJoin:Boolean = true; + public var uri:String = "unknown"; [Bindable] @@ -49,6 +51,11 @@ package org.bigbluebutton.modules.phone [Bindable] public var showPhoneOption:Boolean = false; + + [Bindable] + public var showMicrophoneHint:Boolean = true; + + public var forceListenOnly:Boolean = false; public function PhoneOptions() { parseOptions(); @@ -81,12 +88,18 @@ package org.bigbluebutton.modules.phone if (vxml.@listenOnlyMode != undefined) { listenOnlyMode = (vxml.@listenOnlyMode.toString().toUpperCase() == "TRUE"); } + if (vxml.@forceListenOnly != undefined) { + forceListenOnly = (vxml.@forceListenOnly.toString().toUpperCase() == "TRUE"); + } if (vxml.@presenterShareOnly != undefined) { presenterShareOnly = (vxml.@presenterShareOnly.toString().toUpperCase() == "TRUE"); } if (vxml.@showPhoneOption != undefined) { showPhoneOption = (vxml.@showPhoneOption.toString().toUpperCase() == "TRUE"); } + if (vxml.@showMicrophoneHint != undefined) { + showMicrophoneHint = (vxml.@showMicrophoneHint.toString().toUpperCase() == "TRUE"); + } } } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/FlashCallManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/FlashCallManager.as index 76dcb7b0fa1cdf7de49430d8d12a946336830dbe..48fc3bc60825df2f3cab75ecff672914d835a84c 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/FlashCallManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/FlashCallManager.as @@ -97,7 +97,7 @@ * after. (richard mar 28, 2014) */ if (mic) { - if (options.skipCheck) { + if (options.skipCheck && PhoneOptions.firstAudioJoin) { trace(LOG + "Calling into voice conference. skipCheck=[" + options.skipCheck + "] echoTestDone=[" + echoTestDone + "]"); streamManager.useDefaultMic(); @@ -197,6 +197,17 @@ } public function initialize():void { + trace(LOG + "Initializing FlashCallManager, current state: " + state); + switch (state) { + case STOP_ECHO_THEN_JOIN_CONF: + // if we initialize usingFlash here, we won't be able to hang up from + // the flash connection + trace(LOG + "Invalid state for initialize, aborting..."); + return; + default: + break; + } + printMics(); options = new PhoneOptions(); if (options.useWebRTCIfAvailable && isWebRTCSupported()) { diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/WebRTCCallManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/WebRTCCallManager.as index 1a01fb3eed9f6e8d56e390be9dda475cbce5e3fc..383ab77834244e3a7306905bfb8f25ae9b45c6c3 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/WebRTCCallManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/managers/WebRTCCallManager.as @@ -14,6 +14,7 @@ package org.bigbluebutton.modules.phone.managers import org.bigbluebutton.core.UsersUtil; import org.bigbluebutton.main.api.JSAPI; import org.bigbluebutton.main.events.ClientStatusEvent; + import org.bigbluebutton.main.model.users.AutoReconnect; import org.bigbluebutton.modules.phone.PhoneModel; import org.bigbluebutton.modules.phone.PhoneOptions; import org.bigbluebutton.modules.phone.events.AudioSelectionWindowEvent; @@ -44,6 +45,11 @@ package org.bigbluebutton.modules.phone.managers private var options:PhoneOptions; private var model:WebRTCModel = PhoneModel.getInstance().webRTCModel; + + private var INITIAL_BACKOFF:Number = 100; + private var MAX_RETRIES:Number = 3; + private var reconnect:AutoReconnect = new AutoReconnect(INITIAL_BACKOFF); + private var reconnecting:Boolean = false; public function WebRTCCallManager() { var browserInfo:Array = JSAPI.getInstance().getBrowserInfo(); @@ -59,8 +65,6 @@ package org.bigbluebutton.modules.phone.managers ResourceUtil.getInstance().getString("bbb.clientstatus.webrtc.title"), ResourceUtil.getInstance().getString("bbb.clientstatus.webrtc.message"))); } - - usingWebRTC = checkIfToUseWebRTC(); } private function isWebRTCSupported():Boolean { @@ -131,7 +135,12 @@ package org.bigbluebutton.modules.phone.managers trace(LOG + "setting state to IN_CONFERENCE"); model.state = Constants.IN_CONFERENCE; dispatcher.dispatchEvent(new WebRTCJoinedVoiceConferenceEvent()); - + if(reconnecting) { + dispatcher.dispatchEvent(new ClientStatusEvent(ClientStatusEvent.SUCCESS_MESSAGE_EVENT, + ResourceUtil.getInstance().getString("bbb.webrtcWarning.connection.reestablished"), + ResourceUtil.getInstance().getString("bbb.webrtcWarning.connection.reestablished"))); + reconnecting = false; + } } public function handleWebRTCCallEndedEvent():void { @@ -148,9 +157,11 @@ package org.bigbluebutton.modules.phone.managers public function handleJoinVoiceConferenceCommand(event:JoinVoiceConferenceCommand):void { trace(LOG + "handleJoinVoiceConferenceCommand - usingWebRTC: " + usingWebRTC + ", event.mic: " + event.mic); + usingWebRTC = checkIfToUseWebRTC(); + if (!usingWebRTC || !event.mic) return; - if (options.skipCheck || echoTestDone) { + if ((options.skipCheck && PhoneOptions.firstAudioJoin) || echoTestDone) { joinVoiceConference(); } else { startWebRTCEchoTest(); @@ -209,16 +220,36 @@ package org.bigbluebutton.modules.phone.managers var errorString:String; model.state = Constants.INITED; - if (event.errorCode == 1004) { - errorString = ResourceUtil.getInstance().getString("bbb.webrtcWarning.failedError." + event.errorCode, [event.cause]); - } else { - errorString = ResourceUtil.getInstance().getString("bbb.webrtcWarning.failedError." + event.errorCode); + if(!reconnecting) { + trace(LOG + "WebRTC call failed, attempting reconnection"); + reconnecting = true; + dispatcher.dispatchEvent(new ClientStatusEvent(ClientStatusEvent.WARNING_MESSAGE_EVENT, + ResourceUtil.getInstance().getString("bbb.webrtcWarning.connection.dropped"), + ResourceUtil.getInstance().getString("bbb.webrtcWarning.connection.reconnecting"))); + reconnect.onDisconnect(joinVoiceConference); } - - if (!errorString) { - errorString = ResourceUtil.getInstance().getString("bbb.webrtcWarning.failedError.unknown", [event.errorCode]); + else { + trace(LOG + "WebRTC call reconnection failed"); + if( reconnect.Retries < MAX_RETRIES ) { + trace(LOG + "Retring... " + reconnect.Retries); + reconnect.onConnectionAttemptFailed(); + } + else { + trace(LOG + "Giving up"); + reconnecting = false; + + if (event.errorCode == 1004) { + errorString = ResourceUtil.getInstance().getString("bbb.webrtcWarning.failedError." + event.errorCode, [event.cause]); + } else { + errorString = ResourceUtil.getInstance().getString("bbb.webrtcWarning.failedError." + event.errorCode); + } + + if (!errorString) { + errorString = ResourceUtil.getInstance().getString("bbb.webrtcWarning.failedError.unknown", [event.errorCode]); + } + sendWebRTCAlert(ResourceUtil.getInstance().getString("bbb.webrtcWarning.title"), ResourceUtil.getInstance().getString("bbb.webrtcWarning.message", [errorString]), errorString); + } } - sendWebRTCAlert(ResourceUtil.getInstance().getString("bbb.webrtcWarning.title"), ResourceUtil.getInstance().getString("bbb.webrtcWarning.message", [errorString]), errorString); } public function handleWebRTCMediaFailedEvent():void { diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/maps/FlashCallEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/maps/FlashCallEventMap.mxml index ec5d98ef561bf20f36e02cbd38c4a4641e4b5f61..3106fb7b358833cb918bdfc8c580fa8928a901a7 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/maps/FlashCallEventMap.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/maps/FlashCallEventMap.mxml @@ -100,6 +100,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. </EventHandlers> <EventHandlers type="{JoinVoiceConferenceCommand.JOIN_VOICE_CONF}"> + <MethodInvoker generator="{FlashCallManager}" method="initialize"/> <MethodInvoker generator="{FlashCallManager}" method="handleJoinVoiceConferenceCommand" arguments="{event}"/> </EventHandlers> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/views/components/ToolbarButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/views/components/ToolbarButton.mxml index 84415f5a4ccd8444b736a54abbc429a1d09ea517..1cf69137414fa2fc8f97a28725ae7282413ac9ae 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/phone/views/components/ToolbarButton.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/phone/views/components/ToolbarButton.mxml @@ -32,6 +32,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mate:Listener type="{FlashLeftVoiceConferenceEvent.LEFT_VOICE_CONFERENCE}" method="handleFlashLeftVoiceConferenceEvent" /> <mate:Listener type="{FlashJoinedVoiceConferenceEvent.JOINED_VOICE_CONFERENCE}" method="handleFlashJoinedVoiceConferenceEvent" /> <mate:Listener type="{FlashJoinedListenOnlyVoiceConferenceEvent.JOINED_LISTEN_ONLY_VOICE_CONFERENCE}" method="handleFlashJoinedListenOnlyConferenceEvent" /> + <mate:Listener type="{FlashEchoTestStoppedEvent.ECHO_TEST_STOPPED}" method="handleStopEchoTestEvent" /> <mate:Listener type="{WebRTCCallEvent.WEBRTC_CALL_STARTED}" method="handleWebRTCCallStartedEvent" /> <mate:Listener type="{WebRTCCallEvent.WEBRTC_CALL_ENDED}" method="handleWebRTCCallEndedEvent" /> <mate:Listener type="{AudioSelectionWindowEvent.CLOSED_AUDIO_SELECTION}" method="handleClosedAudioSelectionWindowEvent" /> @@ -49,6 +50,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.main.views.MainToolbar; import org.bigbluebutton.modules.phone.PhoneOptions; import org.bigbluebutton.modules.phone.events.AudioSelectionWindowEvent; + import org.bigbluebutton.modules.phone.events.FlashEchoTestStoppedEvent; import org.bigbluebutton.modules.phone.events.FlashJoinedListenOnlyVoiceConferenceEvent; import org.bigbluebutton.modules.phone.events.FlashJoinedVoiceConferenceEvent; import org.bigbluebutton.modules.phone.events.FlashLeftVoiceConferenceEvent; @@ -78,7 +80,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. this.enabled = false; trace(LOG + "startPhone 2 enabled=[" + enabled + "] selected=[" + selected + "]"); if (this.selected) { - if(thisUser.disableMyMic){ + if(thisUser.disableMyMic || defaultListenOnlyMode){ var command:JoinVoiceConferenceCommand = new JoinVoiceConferenceCommand(); command.mic = false; dispatcher.dispatchEvent(command); @@ -93,6 +95,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. dispatcher.dispatchEvent(new LeaveVoiceConferenceCommand()); } } + + private function get defaultListenOnlyMode():Boolean { + return (phoneOptions.listenOnlyMode && phoneOptions.forceListenOnly); + } public function remoteClick(event:ShortcutEvent):void{ this.selected = true; @@ -120,11 +126,11 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. // when the button is added to the stage display the audio selection window if auto join is true if (phoneOptions.autoJoin) { - if (phoneOptions.skipCheck || thisUser.disableMyMic) { + if (phoneOptions.skipCheck || thisUser.disableMyMic || defaultListenOnlyMode) { var command:JoinVoiceConferenceCommand = new JoinVoiceConferenceCommand(); if ( (phoneOptions.presenterShareOnly && !UsersUtil.amIPresenter() && !UsersUtil.amIModerator()) - || thisUser.disableMyMic + || thisUser.disableMyMic || defaultListenOnlyMode ) { command.mic = false; } else { @@ -136,10 +142,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. trace(LOG + "Sending Show Audio Selection command"); dispatcher.dispatchEvent(new AudioSelectionWindowEvent(AudioSelectionWindowEvent.SHOW_AUDIO_SELECTION)); } + } else { + joinDefaultListenOnlyMode(); } } private function onUserJoinedConference():void { + PhoneOptions.firstAudioJoin = false; + this.selected = true; this.enabled = true; @@ -149,6 +159,12 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. this.toolTip = ResourceUtil.getInstance().getString('bbb.toolbar.phone.toolTip.stop'); } + private function onUserJoinedListenOnlyConference():void { + trace(LOG + "onUserJoinedListenOnlyConference enabled=[" + enabled + "] selected=[" + selected + "]"); + + resetButtonState(); + } + private function onUserLeftConference():void { this.selected = false; this.enabled = true; @@ -158,6 +174,14 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. this.toolTip = ResourceUtil.getInstance().getString('bbb.toolbar.phone.toolTip.start'); } + private function joinDefaultListenOnlyMode(micLeft:Boolean = true):void { + if (defaultListenOnlyMode && micLeft) { + var command:JoinVoiceConferenceCommand = new JoinVoiceConferenceCommand(); + command.mic = false; + dispatcher.dispatchEvent(command); + } + } + private function handleFlashJoinedVoiceConferenceEvent(event:FlashJoinedVoiceConferenceEvent):void { trace(LOG + "User has joined the conference using flash"); onUserJoinedConference(); @@ -165,12 +189,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function handleFlashJoinedListenOnlyConferenceEvent(event:FlashJoinedListenOnlyVoiceConferenceEvent):void { trace(LOG + "User has joined the listen only conference using flash"); - onUserJoinedConference(); + if (defaultListenOnlyMode) { + onUserJoinedListenOnlyConference(); + } else { + onUserJoinedConference(); + } } private function handleFlashLeftVoiceConferenceEvent(event:FlashLeftVoiceConferenceEvent):void { trace(LOG + "User has left the conference using flash"); + var micLeft:Boolean = (_currentState == ACTIVE_STATE); onUserLeftConference(); + joinDefaultListenOnlyMode(micLeft); } private function handleWebRTCCallStartedEvent(event: WebRTCCallEvent):void { @@ -181,6 +211,20 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function handleWebRTCCallEndedEvent(event:WebRTCCallEvent):void { trace(LOG + "User has left the conference using webrtc"); onUserLeftConference(); + joinDefaultListenOnlyMode(); + } + + private function handleStopEchoTestEvent(event:Event):void { + resetButtonState(); + joinDefaultListenOnlyMode(); + } + + private function resetButtonState():void { + this.selected = false; + this.enabled = true; + _currentState = DEFAULT_STATE; + this.styleName = "voiceConfDefaultButtonStyle"; + this.toolTip = ResourceUtil.getInstance().getString('bbb.toolbar.phone.toolTip.start'); } private function handleClosedAudioSelectionWindowEvent(event:AudioSelectionWindowEvent):void { @@ -190,6 +234,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. _currentState = DEFAULT_STATE; this.styleName = "voiceConfDefaultButtonStyle"; this.toolTip = ResourceUtil.getInstance().getString('bbb.toolbar.phone.toolTip.start'); + joinDefaultListenOnlyMode(); } //For whatever reason the tooltip does not update when localization is changed dynamically. Overrideing it here diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/business/FileUploadService.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/business/FileUploadService.as index 63a64f599b02d93a8b1a71777048b86daa6c6bdc..02caa6d2a6bfbd706f42396105f51b27c7a0f209 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/business/FileUploadService.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/business/FileUploadService.as @@ -66,8 +66,9 @@ package org.bigbluebutton.modules.present.business * @param file - The FileReference class of the file we wish to send * */ - public function upload(presentationName:String, file:FileReference):void { + public function upload(presentationName:String, file:FileReference, downloadable:Boolean):void { sendVars.presentation_name = presentationName; + sendVars.is_downloadable = downloadable; var fileToUpload : FileReference = new FileReference(); fileToUpload = file; diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/business/PresentProxy.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/business/PresentProxy.as index 6d006946b619d015d539dc9500f253190ed156c4..5ba503d042644a8775c1cd54eac3ef501ac1178b 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/business/PresentProxy.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/business/PresentProxy.as @@ -23,6 +23,7 @@ package org.bigbluebutton.modules.present.business import flash.events.TimerEvent; import flash.net.NetConnection; import flash.utils.Timer; + import flash.net.navigateToURL; import mx.collections.ArrayCollection; @@ -43,6 +44,7 @@ package org.bigbluebutton.modules.present.business import org.bigbluebutton.modules.present.events.PresenterCommands; import org.bigbluebutton.modules.present.events.RemovePresentationEvent; import org.bigbluebutton.modules.present.events.UploadEvent; + import org.bigbluebutton.modules.present.events.DownloadEvent; import org.bigbluebutton.modules.present.managers.PresentationSlides; import org.bigbluebutton.modules.present.model.Page; import org.bigbluebutton.modules.present.model.Presentation; @@ -51,6 +53,11 @@ package org.bigbluebutton.modules.present.business import org.bigbluebutton.modules.present.services.messaging.MessageReceiver; import org.bigbluebutton.modules.present.services.messaging.MessageSender; + import flash.events.*; + import flash.net.FileReference; + import flash.net.URLRequest; + import flash.errors.*; + public class PresentProxy { private static const LOG:String = "Present::PresentProxy - "; @@ -150,9 +157,22 @@ package org.bigbluebutton.modules.present.business if (uploadService == null) { uploadService = new FileUploadService(host + "/bigbluebutton/presentation/upload", conference, room); } - uploadService.upload(e.filename, e.file); + uploadService.upload(e.filename, e.file, e.isDownloadable); } + /** + * Start downloading the selected file + * @param e + * + */ + public function startDownload(e:DownloadEvent):void { + var presentationName:String = e.fileNameToDownload; + var downloadUri:String = host + "/bigbluebutton/presentation/" + conference + "/" + room + "/" + presentationName + "/download"; + LogUtil.debug("PresentationApplication::downloadPresentation()... " + downloadUri); + var req:URLRequest = new URLRequest(downloadUri); + navigateToURL(req,"_blank"); + } + /** * To to the specified slide * @param e - The event which holds the slide number diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/commands/UploadFileCommand.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/commands/UploadFileCommand.as index 2d2c05c960ef18b64b77258b930d654e569ad399..55b3771f71216faacfd77e5f32b384a0dc5319f8 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/commands/UploadFileCommand.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/commands/UploadFileCommand.as @@ -9,6 +9,7 @@ package org.bigbluebutton.modules.present.commands public var filename:String; public var file:FileReference; + public var isDownloadable:Boolean; public function UploadFileCommand() { diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/events/DownloadEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/events/DownloadEvent.as new file mode 100755 index 0000000000000000000000000000000000000000..486cfc4057c78b5027b3c1de1bc3b4dee3a47094 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/events/DownloadEvent.as @@ -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.modules.present.events +{ + import flash.events.Event; + + public class DownloadEvent extends Event { + public static const OPEN_DOWNLOAD_WINDOW:String = "OPEN_DOWNLOAD_WINDOW"; + public static const CLOSE_DOWNLOAD_WINDOW:String = "CLOSE_DOWNLOAD_WINDOW"; + public static const DOWNLOAD_PRESENTATION:String = "DOWNLOAD_PRESENTATION"; + + public var fileNameToDownload:String; + + public function DownloadEvent(type:String) { + super(type, true, false); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/events/UploadEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/events/UploadEvent.as old mode 100755 new mode 100644 diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/managers/PresentManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/managers/PresentManager.as index 1bf466e60f0c7e33ea2bd2a8042b9d9fea44a335..2af363e4fe7c9269754dbb097fb981b89675950a 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/managers/PresentManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/managers/PresentManager.as @@ -35,14 +35,17 @@ package org.bigbluebutton.modules.present.managers import org.bigbluebutton.modules.present.events.PresentModuleEvent; import org.bigbluebutton.modules.present.events.RemovePresentationEvent; import org.bigbluebutton.modules.present.events.UploadEvent; + import org.bigbluebutton.modules.present.events.DownloadEvent; import org.bigbluebutton.modules.present.model.PresentationModel; import org.bigbluebutton.modules.present.ui.views.FileUploadWindow; + import org.bigbluebutton.modules.present.ui.views.FileDownloadWindow; import org.bigbluebutton.modules.present.ui.views.PresentationWindow; public class PresentManager { private var globalDispatcher:Dispatcher; private var uploadWindow:FileUploadWindow; + private var downloadWindow:FileDownloadWindow; private var presentWindow:PresentationWindow; public function PresentManager() { @@ -65,8 +68,8 @@ package org.bigbluebutton.modules.present.managers private function openWindow(window:IBbbModuleWindow):void{ var event:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); - event.window = window; - globalDispatcher.dispatchEvent(event); + event.window = window; + globalDispatcher.dispatchEvent(event); } public function handleOpenUploadWindow(e:UploadEvent):void{ @@ -87,5 +90,23 @@ package org.bigbluebutton.modules.present.managers PopUpManager.removePopUp(uploadWindow); uploadWindow = null; } + + public function handleOpenDownloadWindow():void { + if (downloadWindow != null) return; + + downloadWindow = FileDownloadWindow(PopUpManager.createPopUp(FlexGlobals.topLevelApplication as DisplayObject, FileDownloadWindow, true)); + + var point1:Point = new Point(); + point1.x = FlexGlobals.topLevelApplication.width / 2; + point1.y = FlexGlobals.topLevelApplication.height / 2; + + downloadWindow.x = point1.x - (downloadWindow.width/2); + downloadWindow.y = point1.y - (downloadWindow.height/2); + } + + public function handleCloseDownloadWindow():void { + PopUpManager.removePopUp(downloadWindow); + downloadWindow = null; + } } } \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/maps/PresentEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/maps/PresentEventMap.mxml index a23051553e047488f6e6019bc716b122b69a844b..9f5cdcb034dbf1371880540c54d7d962d9cfab8d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/maps/PresentEventMap.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/maps/PresentEventMap.mxml @@ -40,6 +40,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.present.events.PresentationEvent; import org.bigbluebutton.modules.present.events.PresenterCommands; import org.bigbluebutton.modules.present.events.RemovePresentationEvent; + import org.bigbluebutton.modules.present.events.DownloadEvent; import org.bigbluebutton.modules.present.events.UploadEvent; import org.bigbluebutton.modules.present.managers.PresentManager; import org.bigbluebutton.modules.present.services.PageLoaderService; @@ -70,6 +71,18 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <EventHandlers type="{PresentModuleEvent.STOP_MODULE}" > <MethodInvoker generator="{PresentManager}" method="handleStopModuleEvent" /> </EventHandlers> + + <EventHandlers type="{DownloadEvent.OPEN_DOWNLOAD_WINDOW}" > + <MethodInvoker generator="{PresentManager}" method="handleOpenDownloadWindow" /> + </EventHandlers> + + <EventHandlers type="{DownloadEvent.CLOSE_DOWNLOAD_WINDOW}" > + <MethodInvoker generator="{PresentManager}" method="handleCloseDownloadWindow" /> + </EventHandlers> + + <EventHandlers type="{DownloadEvent.DOWNLOAD_PRESENTATION}" > + <MethodInvoker generator="{PresentProxy}" method="startDownload" arguments="{event}" /> + </EventHandlers> <EventHandlers type="{UploadEvent.OPEN_UPLOAD_WINDOW}" > <MethodInvoker generator="{PresentManager}" method="handleOpenUploadWindow" arguments="{event}" /> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/model/Presentation.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/model/Presentation.as index cfbf8af31aec114bd8c036e4ada9b869e34cff5c..99f5e479e9201718ed0e077207fdbeaaed3d2cd7 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/model/Presentation.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/model/Presentation.as @@ -10,12 +10,14 @@ package org.bigbluebutton.modules.present.model private var _pages:ArrayCollection; private var _current:Boolean = false; + private var _downloadable:Boolean = false; - public function Presentation(id: String, name: String, current: Boolean, pages: ArrayCollection) { + public function Presentation(id: String, name: String, current: Boolean, pages: ArrayCollection, downloadable: Boolean) { _id = id; _name = name; _current = current; _pages = pages + _downloadable = downloadable; } public function get id():String { @@ -66,5 +68,9 @@ package org.bigbluebutton.modules.present.model return pages; } + + public function get downloadable():Boolean { + return _downloadable; + } } } \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/model/PresentationModel.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/model/PresentationModel.as index ba19ece2ce6a831037275a8eeaeb8ab8dd365f6e..fb28f19929f97efcf55f1cf89d60d6e9d5fd159a 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/model/PresentationModel.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/model/PresentationModel.as @@ -183,6 +183,19 @@ package org.bigbluebutton.modules.present.model trace(LOG + "Could not find presentation [" + presId + "]."); return null; } + + public function getDownloadablePresentations():ArrayCollection { + var presos:ArrayCollection = new ArrayCollection(); + + for (var i:int = 0; i < _presentations.length; i++) { + var pres: Presentation = _presentations.getItemAt(i) as Presentation; + if (pres.downloadable) { + presos.addItem(pres); + } + } + + return presos; + } } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/PresentationService.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/PresentationService.as index c013c219d562f8e9c022ace8e4bdbcf397392cc5..98ed27cfaeffb2acb0511e41ee680a91f6f38495 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/PresentationService.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/PresentationService.as @@ -120,7 +120,7 @@ package org.bigbluebutton.modules.present.services presoPages.addItem(pg); } - var presentation: Presentation = new Presentation(presVO.id, presVO.name, presVO.isCurrent(), presoPages); + var presentation: Presentation = new Presentation(presVO.id, presVO.name, presVO.isCurrent(), presoPages, presVO.downloadable); return presentation; } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/messages/PresentationVO.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/messages/PresentationVO.as index 26eb7f54a0df917e6769d3c102dd92bdaf613958..d1dfd866659bf7640a8ecd36d1b19ab56a987537 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/messages/PresentationVO.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/messages/PresentationVO.as @@ -7,12 +7,14 @@ package org.bigbluebutton.modules.present.services.messages private var _name:String; private var _current:Boolean = false; private var _pages:ArrayCollection; + private var _downloadable:Boolean = false; - public function PresentationVO(id: String, name: String, current: Boolean, pages: ArrayCollection) { + public function PresentationVO(id: String, name: String, current: Boolean, pages: ArrayCollection, downloadable: Boolean) { _id = id; _name = name; _current = current; _pages = pages + _downloadable = downloadable; } public function get id():String { @@ -36,5 +38,9 @@ package org.bigbluebutton.modules.present.services.messages return pages; } + + public function get downloadable():Boolean { + return _downloadable; + } } } \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/messaging/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/messaging/MessageReceiver.as index 999d4e254eecf6ff2450c90cbdf61e52a69f4013..990c71a614e17214310943964d8126281c6cdba7 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/messaging/MessageReceiver.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/services/messaging/MessageReceiver.as @@ -228,7 +228,7 @@ package org.bigbluebutton.modules.present.services.messaging } var preso:PresentationVO = new PresentationVO(presentation.id, presentation.name, - presentation.current, presoPages); + presentation.current, presoPages, presentation.downloadable); return preso; } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/DownloadPresentationRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/DownloadPresentationRenderer.mxml new file mode 100755 index 0000000000000000000000000000000000000000..249173edc9faf0e1f17973735777cc9a4f77d48d --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/DownloadPresentationRenderer.mxml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="90%" verticalAlign="middle"> + <mx:Script> + <![CDATA[ + import org.bigbluebutton.common.LogUtil; + import com.asfusion.mate.events.Dispatcher; + import org.bigbluebutton.util.i18n.ResourceUtil; + import org.bigbluebutton.modules.present.events.DownloadEvent; + + private var globalDispatch:Dispatcher = new Dispatcher(); + + private function downloadPresentation():void { + LogUtil.debug("FileDownloadWindow::downloadPresentation() " + data); + var downloadEvent:DownloadEvent = new DownloadEvent(DownloadEvent.DOWNLOAD_PRESENTATION); + downloadEvent.fileNameToDownload = data.id as String; + globalDispatch.dispatchEvent(downloadEvent); + } + + ]]> + </mx:Script> + <mx:Label id="presentationNameLabel" text="{data.name as String}" styleName="presentationNameLabelStyle" width="80%"/> + <mx:Button id="downloadBtn" label="{ResourceUtil.getInstance().getString('bbb.filedownload.downloadBtn')}" + toolTip="{ResourceUtil.getInstance().getString('bbb.filedownload.downloadBtn')}" + styleName="presentationUploadShowButtonStyle" + click="downloadPresentation()" enabled="{data.downloadable as Boolean}"/> + +</mx:HBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileDownloadWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileDownloadWindow.mxml new file mode 100755 index 0000000000000000000000000000000000000000..04a3e288983aee0b301e04a0578fffe6f66678c8 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileDownloadWindow.mxml @@ -0,0 +1,77 @@ +<?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/>. + +--> + +<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" + xmlns:mate="http://mate.asfusion.com/" + layout="absolute" width="580" styleName="presentationFileUploadWindowStyle" + initialize="initData();"> + + <mate:Dispatcher id="globalDispatch" /> + + <mx:Script> + <![CDATA[ + import org.bigbluebutton.common.LogUtil; + import com.asfusion.mate.events.Dispatcher; + import mx.collections.*; + import mx.events.FlexEvent; + import mx.events.ValidationResultEvent; + import mx.managers.PopUpManager; + import mx.utils.*; + import mx.validators.*; + import org.bigbluebutton.common.Images; + import org.bigbluebutton.modules.present.events.RemovePresentationEvent; + import org.bigbluebutton.modules.present.events.DownloadEvent; + import org.bigbluebutton.modules.present.model.PresentationModel; + import org.bigbluebutton.util.i18n.ResourceUtil; + + [Bindable] private var downloadablePresentations:ArrayCollection; + + override public function move(x:Number, y:Number):void + { + return; + } + + private function initData():void { + downloadablePresentations = PresentationModel.getInstance().getDownloadablePresentations(); + } + + ]]> + + </mx:Script> + + <mx:VBox width="100%" height="100%"> + <mx:Label text="{ResourceUtil.getInstance().getString('bbb.filedownload.title')}" styleName="presentationUploadTitleStyle" paddingBottom="0"/> + <mx:Canvas width="100%" height="205" verticalScrollPolicy="off"> + <mx:List width="100%" height="202" left="5" top="5" right="5" bottom="5" id="presentationNamesList" alternatingItemColors="[#EFEFEF, #FEFEFE]" allowMultipleSelection="false" + itemRenderer="org.bigbluebutton.modules.present.ui.views.DownloadPresentationRenderer" + dragEnabled="false" dataProvider="{downloadablePresentations}"> + </mx:List> + </mx:Canvas> + <mx:Canvas width="100%" height="48"> + <mx:Button id="okCancelBtn" label="{ResourceUtil.getInstance().getString('bbb.fileupload.okCancelBtn')}" + styleName="presentationUploadCancelButtonStyle" right="5" bottom="15" + click="globalDispatch.dispatchEvent(new DownloadEvent(DownloadEvent.CLOSE_DOWNLOAD_WINDOW))" + toolTip="{ResourceUtil.getInstance().getString('bbb.fileupload.okCancelBtn.toolTip')}"/> + </mx:Canvas> + </mx:VBox> + +</mx:TitleWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileUploadWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileUploadWindow.mxml index 11623be3dbb32dc83dc98632125674813c8bb940..a610f972b5b2f2376ef6be7cdf2ec7b6b1fdaa70 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileUploadWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/FileUploadWindow.mxml @@ -78,7 +78,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. [BIndable] public var maxFileSize:Number; private var images:Images = new Images(); - [Bindable] private var addIcon : Class = images.add; [Bindable] private var bulletGoIcon : Class = images.bulletGo; [Bindable] private var deleteIcon : Class = images.cancel; @@ -172,12 +171,15 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. progBarLbl.visible = true; lblFileName.enabled = false; + + var isDownloadable:Boolean = new Boolean(letUserDownload.selected); var uploadCmd:UploadFileCommand = new UploadFileCommand(); uploadCmd.filename = presentationName; uploadCmd.file = fileToUpload; + uploadCmd.isDownloadable = isDownloadable; globalDispatch.dispatchEvent(uploadCmd); - + letUserDownload.visible = false; } } @@ -258,6 +260,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. selectBtn.enabled = true; uploadBtn.enabled = true; lblFileName.enabled = true; + letUserDownload.visible = true; } private function handleConvertUpdate(e:ConversionUpdateEvent):void{ @@ -297,7 +300,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. text="{ResourceUtil.getInstance().getString('bbb.fileupload.title')}" editable="false" styleName="presentationUploadTitleStyle" width="400" left="0"/> - <mx:HBox id="fileUploadBox" width="100%" paddingLeft="5" paddingTop="0" verticalAlign="middle"> + <mx:HBox id="fileUploadBox" width="100%" paddingLeft="5" paddingTop="0" verticalAlign="middle" paddingBottom="3"> <mx:Label id="lblFileName" width="{fileUploadBox.width-selectBtn.width-uploadBtn.width-30}" selectable="false" click="selectFile()" text="{ResourceUtil.getInstance().getString('bbb.fileupload.lblFileName.defaultText')}" /> <mx:Button id="selectBtn" label="{ResourceUtil.getInstance().getString('bbb.fileupload.selectBtn.label')}" toolTip="{ResourceUtil.getInstance().getString('bbb.fileupload.selectBtn.toolTip')}" @@ -306,6 +309,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. toolTip="{ResourceUtil.getInstance().getString('bbb.fileupload.uploadBtn.toolTip')}" click="startUpload()" enabled="false" icon="{bulletGoIcon}"/> </mx:HBox> + <mx:HBox> + <mx:CheckBox id="letUserDownload" label="{ResourceUtil.getInstance().getString('bbb.fileupload.letUserDownload')}" selected="true" toolTip="{ResourceUtil.getInstance().getString('bbb.fileupload.letUserDownload.tooltip')}"/> + </mx:HBox> <mx:HBox id="progressReportBox" width="100%" paddingLeft="10" paddingTop="5" paddingBottom="10" includeInLayout="true" visible="false"> <mx:Label id="progBarLbl" text="{ResourceUtil.getInstance().getString('bbb.fileupload.progBarLbl')}" styleName="presentationUploadProgressBarLabelStyle" visible="false"/> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml index 1207f993e89035af916758c1f28ce4627b09fd77..825e52595aa3b74a11235760da06aafeb82bbc5c 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/PresentationWindow.mxml @@ -92,6 +92,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.modules.present.events.PresentationEvent; import org.bigbluebutton.modules.present.events.PresenterCommands; import org.bigbluebutton.modules.present.events.UploadEvent; + import org.bigbluebutton.modules.present.events.DownloadEvent; import org.bigbluebutton.modules.present.events.WindowResizedEvent; import org.bigbluebutton.modules.present.managers.Slide; import org.bigbluebutton.modules.present.model.Page; @@ -113,7 +114,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. [Bindable] private var DEFAULT_X_POSITION:Number = 237; [Bindable] private var DEFAULT_Y_POSITION:Number = 0; - [Bindable] private var CONTROL_BAR_HEIGHT:Number; + [Bindable] private var CONTROL_BAR_HEIGHT:Number = 45; private static const TOP_WINDOW_BORDER:Number = 30; private static const WIDTH_PADDING:Number = 6; @@ -153,7 +154,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. minimizeBtn.tabIndex = baseIndex+1; maximizeRestoreBtn.tabIndex = baseIndex+2; closeBtn.tabIndex = baseIndex+3; - + slideView.slideLoader.tabIndex = baseIndex+4; hotkeyCapture(); @@ -351,6 +352,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function setupPresenter(isPresenter:Boolean):void { uploadPres.visible = isPresenter; + downloadPres.visible = true; var page:Page = PresentationModel.getInstance().getCurrentPage(); if (page != null) { displaySlideNumber(page.num); @@ -387,11 +389,6 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. btnFitToWidth.visible = showButtons; btnFitToPage.visible = showButtons; - if(isPresenter) { - CONTROL_BAR_HEIGHT = 45; - } else { - CONTROL_BAR_HEIGHT = 0; - } fitSlideToWindowMaintainingAspectRatio(); } @@ -576,6 +573,16 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. onFitToPage(true); } } + + private function onDownloadButtonClicked():void { + openDownloadWindow(); + + } + + private function openDownloadWindow():void { + var event:DownloadEvent = new DownloadEvent(DownloadEvent.OPEN_DOWNLOAD_WINDOW); + dispatchEvent(event); + } private function onUploadButtonClicked():void { openUploadWindow(); @@ -603,7 +610,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <views:SlideView id="slideView" width="100%" height="100%" visible="false" mouseDown="mouseDown = true" mouseUp="mouseDown = false" verticalScrollPolicy="off" horizontalScrollPolicy="off" tabIndex="{baseIndex+4}"/> <mx:ControlBar id="presCtrlBar" name="presCtrlBar" width="100%" height="{CONTROL_BAR_HEIGHT}" styleName="presentationWindowControlsStyle" > - <mx:Button id="uploadPres" visible="false" height="30" styleName="presentationUploadButtonStyle" + <mx:Button id="downloadPres" visible="true" height="30" width="30" styleName="presentationDownloadButtonStyle" + toolTip="{ResourceUtil.getInstance().getString('bbb.presentation.downloadPresBtn.toolTip')}" + click="onDownloadButtonClicked()" tabIndex="{baseIndex+5}"/> + <mx:Button id="uploadPres" visible="false" height="30" width="30" styleName="presentationUploadButtonStyle" toolTip="{ResourceUtil.getInstance().getString('bbb.presentation.uploadPresBtn.toolTip')}" click="onUploadButtonClicked()" tabIndex="{baseIndex+5}"/> <mx:Spacer width="100%" id="spacer1"/> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/UploadedPresentationRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/UploadedPresentationRenderer.mxml index 2493680071f52cdb1bbf2c4470845fe40a1842fa..47acfe4ca2dbd96ab14d109889db981c75155e54 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/UploadedPresentationRenderer.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/present/ui/views/UploadedPresentationRenderer.mxml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="90%" + creationComplete="onCreationComplete()" verticalScrollPolicy="off" horizontalScrollPolicy="off" toolTip="{data as String}" @@ -13,9 +14,16 @@ import org.bigbluebutton.modules.present.events.RemovePresentationEvent; import org.bigbluebutton.modules.present.events.UploadEvent; import org.bigbluebutton.util.i18n.ResourceUtil; + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.common.Images; private var globalDispatch:Dispatcher = new Dispatcher(); + private function onCreationComplete():void { + var images:Images = new Images(); + isDownloadable.source = images.disk_grayscale; + } + private function showPresentation():void { trace("FileUploadWindow::showPresentation() " + data.id); var changePresCommand:ChangePresentationCommand = new ChangePresentationCommand(data.id); @@ -33,7 +41,10 @@ } ]]> </mx:Script> - <mx:Label id="presentationNameLabel" width="{this.width-showBtn.width-deleteBtn.width-30}" text="{data.name as String}" styleName="presentationNameLabelStyle"/> + <mx:Label id="presentationNameLabel" width="{this.width-isDownloadable.width-showBtn.width-deleteBtn.width-30}" text="{data.name as String}" styleName="presentationNameLabelStyle"/> + <mx:Image id="isDownloadable" visible="{data.downloadable as Boolean}" + toolTip="{ResourceUtil.getInstance().getString('bbb.filedownload.thisFileIsDownloadable')}" + verticalAlign="middle" /> <mx:Button id="showBtn" label="{ResourceUtil.getInstance().getString('bbb.fileupload.showBtn')}" toolTip="{ResourceUtil.getInstance().getString('bbb.fileupload.showBtn.toolTip')}" styleName="presentationUploadShowButtonStyle" height="26" diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/SharedNotesOptions.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/SharedNotesOptions.as new file mode 100755 index 0000000000000000000000000000000000000000..5c547d29f026dc339da3caa3e69f576c08bc519a --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/SharedNotesOptions.as @@ -0,0 +1,45 @@ +package org.bigbluebutton.modules.sharednotes +{ + import org.bigbluebutton.core.BBB; + + public class SharedNotesOptions + { + [Bindable] + public var refreshDelay:int = 500; + + [Bindable] + public var position:String = "bottom-left"; + + [Bindable] + public var autoStart:Boolean = false; + + [Bindable] + public var showButton:Boolean = false; + + [Bindable] + public var enableMultipleNotes:Boolean = false; + + + public function SharedNotesOptions() + { + var vxml:XML = BBB.getConfigForModule("SharedNotesModule"); + if (vxml != null) { + if (vxml.@refreshDelay != undefined) { + refreshDelay = Number(vxml.@refreshDelay); + } + if (vxml.@position != undefined) { + position = vxml.@position.toString(); + } + if (vxml.@autoStart != undefined) { + autoStart = (vxml.@autoStart.toString().toUpperCase() == "TRUE") ? true : false; + } + if (vxml.@showButton != undefined) { + showButton = (vxml.@showButton.toString().toUpperCase() == "TRUE") ? true : false; + } + if (vxml.@enableMultipleNotes != undefined) { + enableMultipleNotes = (vxml.@enableMultipleNotes.toString().toUpperCase() == "TRUE") ? true : false; + } + } + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/SharedNotesWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/SharedNotesWindow.mxml deleted file mode 100755 index 81499a413fb0bf884c18d180e185a6397cf68c7b..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/SharedNotesWindow.mxml +++ /dev/null @@ -1,219 +0,0 @@ -<!-- - -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/>. - ---> -<containers:CustomMdiWindow xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:containers="org.bigbluebutton.common.*" - layout="absolute" width="508" height="494" implements="org.bigbluebutton.common.IBbbModuleWindow" - title="Notes" creationComplete="init()" xmlns:components="org.bigbluebutton.modules.sharednotes.components.*"> - <mx:Script> - <![CDATA[ - import com.asfusion.mate.events.Dispatcher; - - import flash.utils.getTimer; - - import flexlib.mdi.events.MDIWindowEvent; - - import mx.events.ResizeEvent; - import mx.events.SliderEvent; - - import org.bigbluebutton.common.IBbbModuleWindow; - import org.bigbluebutton.main.views.MainCanvas; - import org.bigbluebutton.modules.sharednotes.infrastructure.Client; - import org.bigbluebutton.modules.sharednotes.infrastructure.ServerConnection; - - private var _xPosition:int, _yPosition:int; - private var _defaultWidth:int = 400; - private var _defaultHeight:int = 300; - private var client:org.bigbluebutton.modules.sharednotes.infrastructure.Client; - - private static var _room:String; - - public static const SHARED_NOTES_CLOSED:String = "SHARED_NOTES_CLOSED"; - - private var resizeTimer:Timer = new Timer(500); - - public function init():void { - textArea.addEventListener(Event.CHANGE, onTextChange); - addEventListener(ServerConnection.SYNCED_EVENT, onSynced); - addEventListener(ServerConnection.SYNCING_EVENT, onSyncing); - addEventListener(MDIWindowEvent.RESIZE, onResize); - addEventListener(MDIWindowEvent.MAXIMIZE, function(e:Event):void { resizeTimer.start(); }); - addEventListener(MDIWindowEvent.RESTORE, function(e:Event):void { resizeTimer.start(); }); - addEventListener(MDIWindowEvent.CLOSE, function(e:Event):void { if (client) client.shutdown(); }); - resizeTimer.addEventListener(TimerEvent.TIMER, onResizeTimer); - - client = new Client(textArea, this); - } - - private function onResizeTimer(e:Event):void { - onResize(); - resizeTimer.stop(); - } - - private function addBlur():void { - var bf:BlurFilter = new BlurFilter(0,0,0); - - var myFilters:Array = new Array(); - - myFilters.push(bf); - - textArea.filters = myFilters; - } - - private function onResize(e:Event = null):void { - const RIGHT_PADDING:int = textArea.x * 2, TOP_PADDING:int = 35, BOTTOM_PADDING:int = 60; - - textArea.width = canvas.width - RIGHT_PADDING; - textArea.height = canvas.height - BOTTOM_PADDING; - txtPlayback.width = canvas.width - RIGHT_PADDING; - txtPlayback.height = canvas.height - BOTTOM_PADDING - 20; - btnPlayback.y = canvas.height - TOP_PADDING; - btnBackPlayback.y = canvas.height - TOP_PADDING; - icoSynced.y = canvas.height - TOP_PADDING; - icoSyncing.y = canvas.height - TOP_PADDING; - icoSynced.x = canvas.width - RIGHT_PADDING - 10; - icoSyncing.x = canvas.width - RIGHT_PADDING - 10; - playbackSlider.width = canvas.width - playbackSlider.x - RIGHT_PADDING; - playbackSlider.y = canvas.height - BOTTOM_PADDING - 10; - lblPlaybackVersion.y = playbackSlider.y + 3; - } - - private function onSynced(e:Event):void { - if (client.version > 0 && !btnBackPlayback.visible) { btnPlayback.visible = true; } - icoSynced.visible = true; - icoSyncing.visible = false; - lblConnecting.visible = false; - } - - private function onSyncing(e:Event):void { - icoSyncing.visible = true; - icoSynced.visible = false; - } - - private function onTextChange(e:Event):void { - onSyncing(e); - } - - protected function btnPlay_clickHandler(event:MouseEvent):void - { - if (client.isTypingTestRunning()) - { - client.stopTyping(); - btnPlay.label = "Start"; - } - else - { - client.startTyping(); - btnPlay.label = "Stop"; - } - } - - private var previousSliderValue:int = 0; - protected function playbackSlider_changeHandler(event:SliderEvent):void - { - txtPlayback.text = client.getSnapshotAtVersion(previousSliderValue, event.value, txtPlayback.text); - lblPlaybackVersion.text = "Version " + event.value + ":"; - } - - protected function btnPlayback_clickHandler(event:MouseEvent):void - { - playbackSlider.maximum = client.version; - playbackSlider.value = 0; - textArea.visible = false; - btnBackPlayback.visible = true; - btnPlayback.visible = false; - previousSliderValue = 0; - txtPlayback.text = client.getSnapshotAtVersion(0, 0, ""); - } - - protected function btnBackPlayback_clickHandler(event:MouseEvent):void - { - textArea.visible = true; - btnBackPlayback.visible = false; - btnPlayback.visible = true; - } - - public function get xPosition():int { - return _xPosition; - } - - public function get yPosition():int { - return _yPosition; - } - - public function set xPosition(x:int):void { - _xPosition = x; - } - - public function set yPosition(y:int):void { - _yPosition = y; - } - - public function get defaultWidth():int{ - return _defaultWidth; - } - - public function get defaultHeight():int{ - return _defaultHeight; - } - - public function set defaultHeight(height:int):void{ - this._defaultHeight = height; - } - - public function set defaultWidth(width:int):void{ - this._defaultWidth = width; - } - - public function resetWidthAndHeight():void{ - this.width = _defaultWidth; - this.height = _defaultHeight; - } - - public function getPrefferedPosition():String{ - return MainCanvas.MIDDLE; - } - - public static function set document(document:String):void { - Client.documentName = document; - } - - override public function close(event:MouseEvent=null):void { - new Dispatcher().dispatchEvent(new Event(SHARED_NOTES_CLOSED, true)); - super.close(event); - } - ]]> - </mx:Script> - <mx:Fade id="dissolveOut" duration="1000" alphaFrom="1.0" alphaTo="0.0"/> - <mx:Fade id="dissolveIn" duration="1000" alphaFrom="0.0" alphaTo="1.0"/> - - <mx:Canvas id="canvas" x="0" y="0" width="100%" height="100%"> - - <mx:Button x="10" y="425" label="Playback" id="btnPlayback" click="btnPlayback_clickHandler(event)" icon="@Embed(source='images/play-icon.png')" visible="false"/> - <mx:HSlider x="79" y="395" width="413" id="playbackSlider" liveDragging="true" snapInterval="1" change="playbackSlider_changeHandler(event)" maximum="0"/> - <mx:TextArea x="10" y="10" width="482" height="387" id="txtPlayback"/> - <mx:Image x="476" y="431" source="@Embed(source='images/tick.png')" id="icoSynced" visible="false" toolTip="Document up to date."/> - <mx:Image x="476" y="431" source="@Embed(source='images/action_refresh.gif')" id="icoSyncing" visible="false" toolTip="Updating document..."/> - <mx:Button x="10" y="425" icon="@Embed(source='images/arrow_left.png')" id="btnBackPlayback" click="btnBackPlayback_clickHandler(event)" visible="false"/> - <mx:Label id="lblPlaybackVersion" x="10" y="399" text="Version 0:"/> - <components:PatchableTextArea x="10" y="10" id="textArea" hideEffect="{dissolveOut}" showEffect="{dissolveIn}" creationComplete="addBlur()" width="482" height="407"/> - <mx:Button x="114" y="425" label="Start" id="btnPlay" click="btnPlay_clickHandler(event)" visible="false"/> - <mx:Label x="211.5" y="186" text="Connecting..." id="lblConnecting"/> - - </mx:Canvas> -</containers:CustomMdiWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/ToolbarButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/ToolbarButton.mxml deleted file mode 100755 index 4c29691f6c1cbc4cd8fb3cb43a4eea68ca563818..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/ToolbarButton.mxml +++ /dev/null @@ -1,74 +0,0 @@ -<?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/>. - ---> - -<mx:Button xmlns:mx="http://www.adobe.com/2006/mxml" icon="{notesIcon}" - click="openPublishWindow()" creationComplete="init()" - toolTip="{ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip')}" xmlns:mate="http://mate.asfusion.com/" - implements="org.bigbluebutton.common.IBbbToolbarComponent"> - - <mate:Listener type="{SharedNotesWindow.SHARED_NOTES_CLOSED}" method="closeEventHandler" /> - <mx:Script> - <![CDATA[ - import com.asfusion.mate.events.Dispatcher; - - import org.bigbluebutton.common.IBbbModuleWindow; - import org.bigbluebutton.common.Images; - import org.bigbluebutton.common.events.OpenWindowEvent; - import org.bigbluebutton.main.views.MainToolbar; - import org.bigbluebutton.util.i18n.ResourceUtil; - - [Embed(source="images/note_edit.png")] - public var notes:Class; - - - [Bindable] public var notesIcon:Class = notes; - - private var dispatcher:Dispatcher; - - private function init():void{ - dispatcher = new Dispatcher(); - this.addEventListener(SharedNotesWindow.SHARED_NOTES_CLOSED, closeEventHandler); - } - - private function openPublishWindow():void{ - var window:IBbbModuleWindow = new SharedNotesWindow(); - - var event:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); - event.window = window; - dispatcher.dispatchEvent(event); - this.enabled = false; - } - - public function show():void{ - this.enabled = true; - } - - private function closeEventHandler(e:Event):void { - show(); - } - - public function getAlignment():String{ - return MainToolbar.ALIGN_LEFT; - } - ]]> - </mx:Script> -</mx:Button> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/components/PatchableTextArea.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/components/PatchableTextArea.as deleted file mode 100755 index 566209502274cd3d435b668594d330cc844495bc..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/components/PatchableTextArea.as +++ /dev/null @@ -1,87 +0,0 @@ -/** - * 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.modules.sharednotes.components -{ - import mx.controls.TextArea; - - import org.bigbluebutton.modules.sharednotes.util.DiffPatch; - - public class PatchableTextArea extends TextArea - { - private var _tackOnText : String = ""; - private var _tackOnTextChanged : Boolean = false; - - private var _patch : String = ""; - private var _patchChanged : Boolean = false; - - public function set tackOnText(value:String):void - { - _tackOnText = value; - _tackOnTextChanged = true; - invalidateProperties(); - } - - public function get tackOnText():String - { - return _tackOnText; - } - - public function set patch(value:String):void - { - _patch = value; - _patchChanged = true; - invalidateProperties(); - } - - public function get patch():String - { - return _patch; - } - - override protected function commitProperties():void - { - super.commitProperties(); - - if (_patchChanged) { - patchClientText(); - patch = ""; - _patchChanged = false; - } - - if(_tackOnTextChanged) { - this.textField.text += tackOnText; - tackOnText = ""; - _tackOnTextChanged = false; - } - } - - public function get textFieldText():String { - return this.textField.text; - } - - private function patchClientText():void { - var results:Array = DiffPatch.patchClientText(patch, textField.text, selectionBeginIndex, selectionEndIndex); - - textField.text = results[1]; - - var cursorSelection:Array = results[0]; - textField.setSelection(cursorSelection[0], cursorSelection[1]); - } - } -} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/AddNoteEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/AddNoteEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..ec5d5ccdcb52500308dba15a3dd5f74cc05747c0 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/AddNoteEvent.as @@ -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 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/>. + * + */ +package org.bigbluebutton.modules.sharednotes.events +{ + import flash.events.Event; + + public class AddNoteEvent extends Event + { + public static const ADD_NOTE:String = 'ADD_NOTE'; + public var document:String; + public var userid:String; + + public function AddNoteEvent(type:String = CURRENT_DOCUMENT, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/CurrentDocumentEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/CurrentDocumentEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..5cf63d710e5eaf211ade765966d8da197e300c28 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/CurrentDocumentEvent.as @@ -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 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class CurrentDocumentEvent extends Event + { + public static const CURRENT_DOCUMENT:String = 'CURRENT_DOCUMENT'; + public var document:Object; + + public function CurrentDocumentEvent(type:String = CURRENT_DOCUMENT, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Patch.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/GetCurrentDocumentEvent.as old mode 100755 new mode 100644 similarity index 61% rename from bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Patch.as rename to bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/GetCurrentDocumentEvent.as index 6b69894cf15c0843b230716558e03585325d250f..7fe58b655b29b0fdebf587efd968c2691ae92708 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Patch.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/GetCurrentDocumentEvent.as @@ -1,31 +1,34 @@ -/** - * 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.modules.sharednotes.infrastructure -{ - public class Patch - { - public var version:int, data:String; - - public function Patch(version:int, data:String) - { - this.version = version; - this.data = data; - } - } -} \ No newline at end of file +/** + * 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class GetCurrentDocumentEvent extends Event + { + public static const GET_CURRENT_DOCUMENT:String = 'GET_CURRENT_DOCUMENT'; + + public function GetCurrentDocumentEvent(type:String = GET_CURRENT_DOCUMENT, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/JoinSharedNotesEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/JoinSharedNotesEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..904994346ed014264d06104df9de0406e375d647 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/JoinSharedNotesEvent.as @@ -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 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class JoinSharedNotesEvent extends Event + { + public static const JOIN_SHARED_NOTES_EVENT:String = 'JOIN_SHARED_NOTES_EVENT'; + public var patch:String; + + public function JoinSharedNotesEvent(type:String = JOIN_SHARED_NOTES_EVENT, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/ReceivePatchEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/ReceivePatchEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..5ce614c98d1e3c7be25f6edf9473a5cca05b22c1 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/ReceivePatchEvent.as @@ -0,0 +1,38 @@ +/** + * 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class ReceivePatchEvent extends Event + { + public static const RECEIVE_PATCH_EVENT:String = 'RECEIVE_PATCH_EVENT'; + public var patch:String; + public var beginIndex:Number; + public var endIndex:Number; + public var noteId:String; + + public function ReceivePatchEvent(type:String = RECEIVE_PATCH_EVENT, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/RequestNoteIdEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/RequestNoteIdEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..5285fb6f27c22347d97bd77dbb7b164d522d43c7 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/RequestNoteIdEvent.as @@ -0,0 +1,36 @@ +/** + * 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class ToolbarButtonWindowEvent extends Event + { + public static const SHOW_WINDOW:String = 'SHOW_WINDOW'; + public static const HIDE_WINDOW:String = 'HIDE_WINDOW'; + public static const ADD_WINDOW:String = 'ADD_WINDOW'; + + public function ToolbarButtonWindowEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/SendPatchEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/SendPatchEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..5bcbd53befcffdb0f4b94d56ef2cad202bc83c0c --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/SendPatchEvent.as @@ -0,0 +1,38 @@ +/** + * 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class SendPatchEvent extends Event + { + public static const SEND_PATCH_EVENT:String = 'SEND_PATCH_EVENT'; + public var patch:String; + public var beginIndex:Number; + public var endIndex:Number; + public var noteId:String; + + public function SendPatchEvent(type:String = SEND_PATCH_EVENT, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/SharedNotesEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/SharedNotesEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..526bd0a79615db1d6b45a78ad30da9617c8d5888 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/SharedNotesEvent.as @@ -0,0 +1,45 @@ +/** + * 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.modules.sharednotes.events +{ + import org.bigbluebutton.main.events.BBBEvent; + + public class SharedNotesEvent extends BBBEvent + { + public static const CREATE_ADDITIONAL_NOTES_REQUEST_EVENT:String = 'SHARED_NOTES_CREATE_ADDITIONAL_NOTES_REQUEST'; + public static const CREATE_ADDITIONAL_NOTES_REPLY_EVENT:String = 'SHARED_NOTES_CREATE_ADDITIONAL_NOTES_REPLY'; + public static const DESTROY_ADDITIONAL_NOTES_REQUEST_EVENT:String = 'SHARED_NOTES_DESTROY_ADDITIONAL_NOTES_REQUEST'; + public static const DESTROY_ADDITIONAL_NOTES_REPLY_EVENT:String = 'SHARED_NOTES_DESTROY_ADDITIONAL_NOTES_REPLY'; + + public static const REQUEST_ADDITIONAL_NOTES_SET_EVENT:String = 'SHARED_NOTES_ADDITIONAL_NOTES_SET_REQUEST'; + + public static const CURRENT_DOCUMENT_REQUEST_EVENT:String = 'SHARED_NOTES_CURRENT_DOCUMENT_REQUEST'; + public static const CURRENT_DOCUMENT_REPLY_EVENT:String = 'SHARED_NOTES_CURRENT_DOCUMENT_REPLY'; + public static const CONNECT_EVENT:String = 'SHARED_NOTES_CONNECT'; + public static const SEND_PATCH_EVENT:String = 'SHARED_NOTES_SEND_PATCH'; + public static const RECEIVE_PATCH_EVENT:String = 'SHARED_NOTES_RECEIVE_PATCH'; + + public function SharedNotesEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, null, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/StartSharedNotesModuleEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/StartSharedNotesModuleEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..f4a49d43bc172c4a85c2b1827e6a40f467ce755d --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/StartSharedNotesModuleEvent.as @@ -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 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class StartSharedNotesModuleEvent extends Event + { + public static const START_SHAREDNOTES_MODULE_EVENT:String = 'START_SHAREDNOTES_MODULE_EVENT'; + public var attributes:Object; + + public function StartSharedNotesModuleEvent(type:String=START_SHAREDNOTES_MODULE_EVENT, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/StopSharedNotesModuleEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/StopSharedNotesModuleEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..f96fd73a4a27ebbb0f7fd217e8c66c6f30586ec1 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/StopSharedNotesModuleEvent.as @@ -0,0 +1,34 @@ +/** + * 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class StopSharedNotesModuleEvent extends Event + { + public static const STOP_SHAREDNOTES_MODULE_EVENT:String = 'STOP_SHAREDNOTES_MODULE_EVENT'; + + public function StopSharedNotesModuleEvent(type:String=STOP_SHAREDNOTES_MODULE_EVENT, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/ToolbarButtonWindowEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/ToolbarButtonWindowEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..5ff9ec3f1434c2356beb276781b165993f14f444 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/events/ToolbarButtonWindowEvent.as @@ -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 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.modules.sharednotes.events +{ + import flash.events.Event; + + public class ToolbarButtonWindowEvent extends Event + { + public static const SHOW_WINDOW:String = 'SHOW_WINDOW'; + public static const HIDE_WINDOW:String = 'HIDE_WINDOW'; + + public function ToolbarButtonWindowEvent(type:String, bubbles:Boolean=true, cancelable:Boolean=false) + { + super(type, bubbles, cancelable); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Client.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Client.as deleted file mode 100755 index fa3666e4adc94ac55d6df4852374fa89bc713727..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Client.as +++ /dev/null @@ -1,205 +0,0 @@ -/** - * 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.modules.sharednotes.infrastructure -{ - import com.adobe.crypto.SHA1; - import com.adobe.serialization.json.JSON; - - import flash.events.Event; - import flash.events.IEventDispatcher; - import flash.events.TimerEvent; - import flash.utils.Timer; - - import org.bigbluebutton.modules.sharednotes.components.PatchableTextArea; - import org.bigbluebutton.modules.sharednotes.util.DiffPatch; - - - public class Client - { - private var _id:int; - - private var textArea:PatchableTextArea; // the text component to display the document - private var documentShadow:String = ""; // the shadow of the current document - private var initialDocument:String; // for storing the initial document - - private var logPrefix:String; // used for logging - - private var patchHistory:Array = new Array(); // a history of the patches - - private var server:ServerConnection; - - private var testCharacterTimer:Timer; - private var documentCheckTimer:Timer; // timer to check for changes in the document - private var timeoutTimer:Timer = new Timer(5000); // setting the timeout for server requests to 5 seconds - - public static var documentName:String = ""; - - - public function Client(textComponent:PatchableTextArea, dispatcher:IEventDispatcher) { - textArea = textComponent; - server = new HTTPServerConnection(this, dispatcher); - } - - public function initClient(id:int, serverConnection:ServerConnection, document:String = ""):void { - _id = id; - documentShadow = new String(document); - textArea.text = new String(document); - initialDocument = new String(document); - server = serverConnection; - - logPrefix = "[Client " + id + "] "; - initDocumentCheckTimer(); - - timeoutTimer.addEventListener(TimerEvent.TIMER, function(e:Event):void { - timeoutTimer.stop(); - sendMessage(); - }); - - // used for testing - testCharacterTimer = new Timer(10); - testCharacterTimer.addEventListener(TimerEvent.TIMER, playSentence); - } - - private function initDocumentCheckTimer():void { - documentCheckTimer = new Timer(500); - documentCheckTimer.addEventListener(TimerEvent.TIMER, documentCheckEventHandler); - documentCheckTimer.start(); - } - - private function documentCheckEventHandler(e:TimerEvent):void { - if (!server.pendingResponse) { - sendMessage(); - } - } - - public function sendMessage():void { - var messageToSend:Message = new Message(id, documentName, ServerConnection.connectionType); - - if (documentShadow != textArea.textFieldText) { - trace("****** SENDING MESSAGE *******"); - - textArea.editable = false; - var clientText:String = new String(textArea.textFieldText); // a snapshot of the client text - - messageToSend.patchData = DiffPatch.diff(documentShadow, clientText); - - patchHistory.push(messageToSend.patchData); - - documentShadow = clientText; - - messageToSend.checksum = SHA1.hash(documentShadow); - - textArea.editable = true; - - trace(logPrefix + "sending " + messageToSend); - } - - //server.send("m, " + JSON.encode(messageToSend)); - - timeoutTimer.start(); - } - - public function receiveMessage(serverMessage:Message): void { - timeoutTimer.stop(); // we received a response - cancel the time out - - trace(logPrefix + "received message.\nMessage: " + serverMessage); - - if (serverMessage.patchData != "") { - var result:String = DiffPatch.patch(serverMessage.patchData, documentShadow); - - if (SHA1.hash(result) == serverMessage.checksum) { - documentShadow = result; - textArea.patch = serverMessage.patchData; - patchHistory.push(serverMessage.patchData); - } - else { - throw new Error("Checksum mismatch"); - } - } - - server.pendingResponse = false; - } - - public function getSnapshotAtVersion(initialVersion:int, finalVersion:int, documentSnapshot:String = ""):String { - if (initialVersion == 0) documentSnapshot = initialDocument; - - if (initialVersion < finalVersion) { - for (var i:int = initialVersion; i < finalVersion; i++) { - documentSnapshot = DiffPatch.patch(patchHistory[i], documentSnapshot); - } - } - else { - for (i = finalVersion; i < initialVersion;i++) { - documentSnapshot = DiffPatch.unpatch(patchHistory[i], documentSnapshot); - } - } - - return documentSnapshot; - } - - public function startTyping():void { - testCharacterTimer.start(); - } - - public function stopTyping():void { - testCharacterTimer.stop(); - } - - private var testSentence:String = "The quick brown fox jumps over the lazy dog."; - - private var testCounter:int = 0; - - private function playSentence(event:TimerEvent):void { - if (textArea.editable) { - if (testCounter == testSentence.length) { - testCounter = 0; - textArea.tackOnText += "\n"; - } - else { - textArea.tackOnText += testSentence.charAt(testCounter++); - } - textArea.editable = true; - } - } - - public function isTypingTestRunning():Boolean { - return testCharacterTimer.running; - } - - private var pendingServerMessage:Message; - private function startReceive(e:TimerEvent):void { - receiveMessage(pendingServerMessage); - } - - private function timeoutHandler(event:TimerEvent):void { - sendMessage(); - } - - public function get version():int { return patchHistory.length; } - - public function get id():int { return _id; } - - public function shutdown():void { - server.shutdown(); - if (testCharacterTimer) testCharacterTimer.stop(); - if (documentCheckTimer) documentCheckTimer.stop(); - if (timeoutTimer) timeoutTimer.stop(); - } - } -} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/HTTPServerConnection.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/HTTPServerConnection.as deleted file mode 100755 index a9c8a50b5c7906bc045d5d052c9d9de9a726f677..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/HTTPServerConnection.as +++ /dev/null @@ -1,62 +0,0 @@ -/** - * 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.modules.sharednotes.infrastructure -{ - import flash.events.Event; - import flash.events.IEventDispatcher; - import flash.net.URLLoader; - import flash.net.URLRequest; - import flash.net.URLRequestMethod; - import flash.net.URLVariables; - - public class HTTPServerConnection extends ServerConnection - { - public static var syncURL:String = ""; - private var loader:URLLoader = new URLLoader(); // used for connecting to the server - - public function HTTPServerConnection(client:Client, dispatcher:IEventDispatcher) - { - super(client, dispatcher); - ServerConnection.connectionType = "http"; - loader.addEventListener(Event.COMPLETE, completeHandler); - sendConnectRequest(); - } - - private function completeHandler(event:Event):void { - var loader:URLLoader = URLLoader(event.target); - loader.close(); - - receive(loader.data); - } - - public override function send(message:String):void { - var params : URLVariables = new URLVariables(); - params.message = message; - var request:URLRequest = new URLRequest(syncURL); - request.data = params; - request.method = URLRequestMethod.POST; - try { - loader.load(request); - pendingResponse = true; - } catch (error:Error) { - trace("Unable to load requested document."); - } - } - } -} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Message.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Message.as deleted file mode 100755 index ef6b54f259e107f08c661cbec0b43b06665350d6..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/Message.as +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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.modules.sharednotes.infrastructure -{ - public class Message - { - public var senderId:int; - public var patchData:String; - public var checksum:String; - public var documentName:String; - public var conType:String; - - public function Message(senderId:int, documentName:String, conType:String, patchData:String = "" , checksum:String = ""){ - this.senderId = senderId; - this.documentName = documentName; - this.patchData = patchData; - this.checksum = checksum; - this.conType = conType; - } - - public static function deserialize(o:Object):Message { - var patchData:String = "", checksum:String = ""; - - if (o.checksum) checksum = o.checksum; - if (o.patchData) patchData = o.patchData; - - return new Message(o.senderId, o.documentName, o.conType, patchData, checksum); - } - - public function toString():String { - return "Message: " + ", senderId " + senderId + ", patchData: " + patchData + ", checksum " + checksum - } - - } -} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/ServerConnection.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/ServerConnection.as deleted file mode 100755 index 39c1dc4787c9655d84a64050de30bafe33e38715..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/ServerConnection.as +++ /dev/null @@ -1,95 +0,0 @@ -/** - * 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.modules.sharednotes.infrastructure -{ - import com.adobe.serialization.json.JSON; - - import flash.events.Event; - import flash.events.IEventDispatcher; - import flash.events.TimerEvent; - import flash.utils.Timer; - - public class ServerConnection - { - public static const SYNCING_EVENT : String = "SN_SYNCING_EVENT"; - public static const SYNCED_EVENT : String = "SN_SYNCED_EVENT"; - - private var client:Client; - protected var dispatcher:IEventDispatcher; - - private var _pendingResponse:Boolean = false; // flag indicating if a response has been received from the server - private var connectionTimeout:Timer = new Timer(5000); //TODO: change the timeout - - public static var connectionType:String = ""; // determines the type of server connection (whether socket or http) - - public function ServerConnection(client:Client, dispatcher:IEventDispatcher) { - this.client = client; - this.dispatcher = dispatcher; - connectionTimeout.addEventListener(TimerEvent.TIMER, function(e:Event):void { - connectionTimeout.stop(); - sendConnectRequest(); - }); - } - - protected function sendConnectRequest():void { - var request:Object = new Object(); - request.documentName = Client.documentName; - request.connectionType = ServerConnection.connectionType; - //send("c, " + JSON.encode(request)); - connectionTimeout.start(); - } - - public function send(message:String):void { } // to be overridden - - protected function receive(data:String):void { - if (data.indexOf("c,") == 0) { - trace("Received connection data: " + data); - //var clientData:Object = JSON.decode(data.substring(2)); - //client.initClient(clientData.id, this, clientData.initialDocument); - connectionTimeout.stop(); - pendingResponse = false; - } - else if (data.indexOf("m,") == 0) { - //var message:Message = Message.deserialize(JSON.decode(data.substring(2))); - //client.receiveMessage(message); - } - else { - trace("unrecognized data: " + data); - } - } - - public function get pendingResponse():Boolean { - return _pendingResponse; - } - - public function set pendingResponse(value : Boolean):void { - _pendingResponse = value; - - if (_pendingResponse) { - dispatcher.dispatchEvent(new Event(SYNCING_EVENT)); - } else { - dispatcher.dispatchEvent(new Event(SYNCED_EVENT)); - } - } - - public function shutdown():void { - connectionTimeout.stop(); - } - } -} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/XMLServerConnection.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/XMLServerConnection.as deleted file mode 100755 index 17b321eebef7d80ea59a9b2db9b7a80706a971e3..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/infrastructure/XMLServerConnection.as +++ /dev/null @@ -1,78 +0,0 @@ -/** - * 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.modules.sharednotes.infrastructure -{ - import flash.events.DataEvent; - import flash.events.Event; - import flash.events.IEventDispatcher; - import flash.events.TimerEvent; - import flash.net.XMLSocket; - import flash.utils.Timer; - - public class XMLServerConnection extends ServerConnection - { - public static var serverURL:String = "192.168.0.104"; - public static const XML_CONNECTION_FAILED:String = "XML_CONNECTION_FAILED"; - private static const MAXIMUM_CONNECTION_ATTEMPTS:int = 5; - - private var _socket: XMLSocket; - private var timeoutTimer: Timer = new Timer(5000); - private var connectionAttempts:int = MAXIMUM_CONNECTION_ATTEMPTS; // attempt to connect five times before dispatching a failure - - private function get socket():XMLSocket { - return _socket; - } - - public function XMLServerConnection(client:Client, dispatcher:IEventDispatcher) - { - super(client, dispatcher); - ServerConnection.connectionType = "xmlsocket"; - - timeoutTimer.addEventListener(TimerEvent.TIMER, function(e:Event):void { timeoutTimer.stop(); connect(); }); - connect(); - } - - public function connect():void { - _socket = new XMLSocket(); - _socket.addEventListener(Event.CONNECT, function(e:Event):void { - connectionAttempts = MAXIMUM_CONNECTION_ATTEMPTS; - - sendConnectRequest(); - }); - _socket.addEventListener(DataEvent.DATA, function(e:DataEvent):void { receive(e.data); }); - socket.connect(serverURL, 8095); - } - - public override function send(message:String):void { - try { - pendingResponse = true; - socket.send(message); - } catch(e:Error) { - //socket.close(); - if (connectionAttempts > 0) { - trace(e.message); - connectionAttempts--; - timeoutTimer.start(); - } else { - dispatcher.dispatchEvent(new Event(XML_CONNECTION_FAILED)); - } - } - } - } -} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/managers/SharedNotesManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/managers/SharedNotesManager.as new file mode 100755 index 0000000000000000000000000000000000000000..97ccc2c2a5fe6317aa8fa8a352ef2719120999e5 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/managers/SharedNotesManager.as @@ -0,0 +1,65 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +*/ + +package org.bigbluebutton.modules.sharednotes.managers { + import com.asfusion.mate.events.Dispatcher; + + import org.bigbluebutton.modules.sharednotes.events.SendPatchEvent; + import org.bigbluebutton.modules.sharednotes.events.SharedNotesEvent; + import org.bigbluebutton.modules.sharednotes.services.MessageReceiver; + import org.bigbluebutton.modules.sharednotes.services.MessageSender; + import org.bigbluebutton.core.managers.UserManager; + + public class SharedNotesManager { + public var sender:MessageSender; + public var receiver:MessageReceiver; + private var attributes:Object; + + public function SharedNotesManager() { + + } + + public function setModuleAttributes(attributes:Object):void { + this.attributes = attributes; + } + + public function patchDocument(e:SendPatchEvent):void { + sender.patchDocument(e.noteId, UserManager.getInstance().getConference().getMyUserId(), e.patch, e.beginIndex, e.endIndex); + } + public function getCurrentDocument():void { + sender.currentDocument(); + } + + public function createAdditionalNotes(e:SharedNotesEvent):void { + sender.createAdditionalNotes(); + } + + public function destroyAdditionalNotes(notesId:String):void { + trace("SharedNotesManager: destroying notes " + notesId); + sender.destroyAdditionalNotes(notesId); + } + + public function requestAdditionalNotesSet(e:SharedNotesEvent):void { + var notesSet:Number = e.payload.numAdditionalSharedNotes; + trace("SharedNotesManager: requested to open a new notes set"); + trace("SharedNotesManager: set size: " + notesSet); + sender.requestAdditionalNotesSet(notesSet); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/maps/SharedNotesEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/maps/SharedNotesEventMap.mxml new file mode 100755 index 0000000000000000000000000000000000000000..c2180c58916705e0fbce9a81672f1757ed202df7 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/maps/SharedNotesEventMap.mxml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + BigBlueButton open source conferencing system - http://www.bigbluebutton.org + + Copyright (c) 2010 BigBlueButton Inc. and by respective authors (see below). + + 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 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/>. + + +--> + +<EventMap xmlns="http://mate.asfusion.com/" + xmlns:mx="http://www.adobe.com/2006/mxml"> + <mx:Script> + <![CDATA[ + import org.bigbluebutton.modules.sharednotes.events.StartSharedNotesModuleEvent; + import org.bigbluebutton.modules.sharednotes.events.StopSharedNotesModuleEvent; + import org.bigbluebutton.modules.sharednotes.managers.SharedNotesManager; + import org.bigbluebutton.modules.sharednotes.services.MessageReceiver; + import org.bigbluebutton.modules.sharednotes.services.MessageSender; + import org.bigbluebutton.modules.sharednotes.events.ToolbarButtonWindowEvent; + import org.bigbluebutton.modules.sharednotes.events.GetCurrentDocumentEvent; + import org.bigbluebutton.modules.sharednotes.events.CurrentDocumentEvent; + import org.bigbluebutton.modules.sharednotes.events.SendPatchEvent; + import org.bigbluebutton.modules.sharednotes.events.JoinSharedNotesEvent; + import org.bigbluebutton.modules.sharednotes.events.SharedNotesEvent; + import mx.events.FlexEvent; + + ]]> + </mx:Script> + + <!-- + This is the main event map for the chat module, think of it as the application controller. + --> + <EventHandlers type="{FlexEvent.PREINITIALIZE}"> + <!-- + The FlexEvent.PREINITIALIZE event is a good place for creating and initializing managers. + --> + <ObjectBuilder generator="{SharedNotesEventMapDelegate}" /> + <ObjectBuilder generator="{SharedNotesManager}" /> + </EventHandlers> + + <EventHandlers type="{StartSharedNotesModuleEvent.START_SHAREDNOTES_MODULE_EVENT}"> + <MethodInvoker generator="{SharedNotesManager}" method="setModuleAttributes" arguments="{event.attributes}" /> + <MethodInvoker generator="{SharedNotesEventMapDelegate}" method="addMainWindow"/> + <MethodInvoker generator="{SharedNotesManager}" method="getCurrentDocument"/> + </EventHandlers> + + <EventHandlers type="{StopSharedNotesModuleEvent.STOP_SHAREDNOTES_MODULE_EVENT}"> + <MethodInvoker generator="{SharedNotesEventMapDelegate}" method="removeToolbarButton"/> + <MethodInvoker generator="{SharedNotesManager}" method="disconnectFromSharedNotes"/> + </EventHandlers> + + <EventHandlers type="{GetCurrentDocumentEvent.GET_CURRENT_DOCUMENT}"> + <MethodInvoker generator="{SharedNotesManager}" method="getCurrentDocument"/> + </EventHandlers> + + <EventHandlers type="{CurrentDocumentEvent.CURRENT_DOCUMENT}"> + <MethodInvoker generator="{SharedNotesEventMapDelegate}" method="addRemoteDocuments" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{SendPatchEvent.SEND_PATCH_EVENT}"> + <MethodInvoker generator="{SharedNotesManager}" method="patchDocument" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{SharedNotesEvent.CREATE_ADDITIONAL_NOTES_REQUEST_EVENT}"> + <MethodInvoker generator="{SharedNotesManager}" method="createAdditionalNotes" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{SharedNotesEvent.CREATE_ADDITIONAL_NOTES_REPLY_EVENT}"> + <MethodInvoker generator="{SharedNotesEventMapDelegate}" method="createAdditionalNotes" arguments="{event.payload.notesId}"/> + </EventHandlers> + + <EventHandlers type="{SharedNotesEvent. DESTROY_ADDITIONAL_NOTES_REQUEST_EVENT}"> + <MethodInvoker generator="{SharedNotesManager}" method="destroyAdditionalNotes" arguments="{event.payload.notesId}"/> + </EventHandlers> + + <EventHandlers type="{SharedNotesEvent.DESTROY_ADDITIONAL_NOTES_REPLY_EVENT}"> + <MethodInvoker generator="{SharedNotesEventMapDelegate}" method="destroyAdditionalNotes" arguments="{event.payload.notesId}"/> + </EventHandlers> + + <EventHandlers type="{SharedNotesEvent.REQUEST_ADDITIONAL_NOTES_SET_EVENT}"> + <MethodInvoker generator="{SharedNotesManager}" method="requestAdditionalNotesSet" arguments="{event}"/> + </EventHandlers> + + <Injectors target="{SharedNotesManager}"> + <PropertyInjector targetKey="receiver" source="{MessageReceiver}"/> + <PropertyInjector targetKey="sender" source="{MessageSender}"/> + </Injectors> + + <Injectors target="{MessageReceiver}"> + <PropertyInjector targetKey="dispatcher" source="{scope.dispatcher}"/> + </Injectors> + + <Injectors target="{MessageSender}"> + <PropertyInjector targetKey="dispatcher" source="{scope.dispatcher}"/> + </Injectors> +</EventMap> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/maps/SharedNotesEventMapDelegate.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/maps/SharedNotesEventMapDelegate.as new file mode 100755 index 0000000000000000000000000000000000000000..a6e5a829419ed21c95bd7fbe528d07d9641860a1 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/maps/SharedNotesEventMapDelegate.as @@ -0,0 +1,117 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +*/ + + +package org.bigbluebutton.modules.sharednotes.maps +{ + import com.asfusion.mate.events.Dispatcher; + + import mx.binding.utils.BindingUtils; + import mx.utils.ObjectUtil; + + import org.bigbluebutton.common.events.ToolbarButtonEvent; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.managers.UserManager; + import org.bigbluebutton.modules.sharednotes.views.SharedNotesWindow; + import org.bigbluebutton.modules.sharednotes.views.AdditionalSharedNotesWindow; + import org.bigbluebutton.common.events.CloseWindowEvent; + import org.bigbluebutton.common.events.OpenWindowEvent; + import org.bigbluebutton.modules.sharednotes.SharedNotesOptions; + import org.bigbluebutton.modules.sharednotes.events.JoinSharedNotesEvent; + import org.bigbluebutton.modules.sharednotes.events.CurrentDocumentEvent; + import org.bigbluebutton.modules.sharednotes.events.SharedNotesEvent; + import org.bigbluebutton.common.LogUtil; + + public class SharedNotesEventMapDelegate { + public static const NAME:String = "SharedNotesController"; + + private var globalDispatcher:Dispatcher; + + private var windows:Array = []; + private var window:SharedNotesWindow; + + private var options:SharedNotesOptions = new SharedNotesOptions(); + + public function SharedNotesEventMapDelegate() { + globalDispatcher = new Dispatcher(); + window = new SharedNotesWindow(); + } + + public function addRemoteDocuments(e:CurrentDocumentEvent):void{ + window.addRemoteDocument(e.document); + for(var id:String in e.document){ + LogUtil.debug("NoteId:" + id +":"+e.document[id] + ":" + e.type); + if(id != window.noteId && !windows.hasOwnProperty(id)){ + createAdditionalNotes(id); + windows[id].addRemoteDocument(e.document); + } + } + + BindingUtils.bindSetter(openAdditionalNotesSet, UserManager.getInstance().getConference(), "numAdditionalSharedNotes"); + } + + private function openAdditionalNotesSet(numAdditionalSharedNotes:Number):void { + var e:SharedNotesEvent = new SharedNotesEvent(SharedNotesEvent.REQUEST_ADDITIONAL_NOTES_SET_EVENT); + e.payload.numAdditionalSharedNotes = numAdditionalSharedNotes; + globalDispatcher.dispatchEvent(e); + } + + public function addMainWindow():void { + var openEvent:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); + openEvent.window = window; + globalDispatcher.dispatchEvent(openEvent); + } + + private function get windowsAsString():String { + return ObjectUtil.toString(windows).split("\n").filter(function(element:*, index:int, arr:Array):Boolean { + return element.substring(0, 4) != " "; + }).join("\n"); + } + + public function createAdditionalNotes(notesId:String):void { + trace(NAME + ": creating additional notes " + notesId); + + if(!windows.hasOwnProperty(notesId)) { + var newWindow:AdditionalSharedNotesWindow = new AdditionalSharedNotesWindow(notesId); + windows[notesId] = newWindow; + + var openEvent:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); + openEvent.window = newWindow; + globalDispatcher.dispatchEvent(openEvent); + } + } + + public function destroyAdditionalNotes(notesId:String):void { + trace(NAME + ": destroying additional notes, notesId: " + notesId); + + var destroyWindow:AdditionalSharedNotesWindow = windows[notesId]; + if (destroyWindow != null) { + trace(NAME + ": notes found, removing window"); + + var closeEvent:CloseWindowEvent = new CloseWindowEvent(CloseWindowEvent.CLOSE_WINDOW_EVENT); + closeEvent.window = destroyWindow; + globalDispatcher.dispatchEvent(closeEvent); + + trace(NAME + ": removing from windows list"); + delete windows[notesId]; + } + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/services/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/services/MessageReceiver.as new file mode 100644 index 0000000000000000000000000000000000000000..421a474c3a9fd55798fdf812a475eeef8686ed75 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/services/MessageReceiver.as @@ -0,0 +1,100 @@ +/** + * 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.modules.sharednotes.services +{ + import flash.events.IEventDispatcher; + + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.main.model.users.IMessageListener; + import org.bigbluebutton.modules.sharednotes.events.CurrentDocumentEvent; + import org.bigbluebutton.modules.sharednotes.events.SharedNotesEvent; + import org.bigbluebutton.modules.sharednotes.events.ReceivePatchEvent; + + public class MessageReceiver implements IMessageListener { + private static const LOG:String = "SharedNotes::MessageReceiver - "; + + public var dispatcher:IEventDispatcher; + + public function MessageReceiver() + { + BBB.initConnectionManager().addMessageListener(this); + } + + public function onMessage(messageName:String, message:Object):void + { + switch (messageName) { + case "PatchDocumentCommand": + handlePatchDocumentCommand(message); + break; + case "GetCurrentDocumentCommand": + handleGetCurrentDocumentCommand(message); + break; + case "CreateAdditionalNotesCommand": + handleCreateAdditionalNotesCommand(message); + break; + case "DestroyAdditionalNotesCommand": + handleDestroyAdditionalNotesCommand(message); + break; + default: + // LogUtil.warn("Cannot handle message [" + messageName + "]"); + } + } + + private function handlePatchDocumentCommand(message:Object):void { + if (message.userID == UsersUtil.getMyUserID()) { + return; + } + + trace(LOG + "Handling patch document message [" + message + "]"); + + var receivePatchEvent:ReceivePatchEvent = new ReceivePatchEvent(); + receivePatchEvent.noteId = message.noteID; + receivePatchEvent.patch = message.patch; + receivePatchEvent.beginIndex = message.beginIndex; + receivePatchEvent.endIndex = message.endIndex; + dispatcher.dispatchEvent(receivePatchEvent); + } + + private function handleGetCurrentDocumentCommand(message:Object):void { + trace(LOG + "Handling get current document message [" + message + "]"); + var notes:Object = JSON.parse(message.notes); + + var currentDocumentEvent:CurrentDocumentEvent = new CurrentDocumentEvent(); + currentDocumentEvent.document = notes; + dispatcher.dispatchEvent(currentDocumentEvent); + } + + private function handleCreateAdditionalNotesCommand(message:Object):void { + trace(LOG + "Handling create additional notes message [" + message + "]"); + + var e:SharedNotesEvent = new SharedNotesEvent(SharedNotesEvent.CREATE_ADDITIONAL_NOTES_REPLY_EVENT); + e.payload.notesId = message.noteID; + dispatcher.dispatchEvent(e); + } + + private function handleDestroyAdditionalNotesCommand(message:Object):void { + trace(LOG + "Handling destroy additional notes message [" + message + "]"); + + var e:SharedNotesEvent = new SharedNotesEvent(SharedNotesEvent.DESTROY_ADDITIONAL_NOTES_REPLY_EVENT); + e.payload.notesId = message.noteID; + dispatcher.dispatchEvent(e); + } + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/services/MessageSender.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/services/MessageSender.as new file mode 100644 index 0000000000000000000000000000000000000000..8cdfb62b7ac7dce52ba066c0a29590c3d0666ef0 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/services/MessageSender.as @@ -0,0 +1,108 @@ +/** + * 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.modules.sharednotes.services +{ + import flash.events.IEventDispatcher; + import flash.net.Responder; + + import mx.utils.ObjectUtil; + + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.managers.ConnectionManager; + + public class MessageSender + { + private static const LOG:String = "SharedNotes::MessageSender - "; + + public var dispatcher:IEventDispatcher; + + private var onSuccessDebugger:Function = function(result:String):void { + trace(result); + }; + private var onErrorDebugger:Function = function(result:String):void { + trace(result); + }; + + public function currentDocument():void { + trace(LOG + "Sending [sharednotes.currentDocument] to server."); + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage( + "sharednotes.currentDocument", + onSuccessDebugger, + onErrorDebugger + ); + } + + public function createAdditionalNotes():void { + trace(LOG + "Sending [sharednotes.createAdditionalNotes] to server."); + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage( + "sharednotes.createAdditionalNotes", + onSuccessDebugger, + onErrorDebugger + ); + } + + public function destroyAdditionalNotes(notesId:String):void { + trace(LOG + "Sending [sharednotes.destroyAdditionalNotes] to server."); + var message:Object = new Object(); + message["noteID"] = notesId; + + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage( + "sharednotes.destroyAdditionalNotes", + onSuccessDebugger, + onErrorDebugger, + message + ); + } + + public function patchDocument(noteId:String, userid:String, patch:String, beginIndex:Number, endIndex:Number):void { + trace(LOG + "Sending [sharednotes.patchDocument] to server."); + var message:Object = new Object(); + message["noteID"] = noteId; + message["patch"] = patch; + message["beginIndex"] = beginIndex; + message["endIndex"] = endIndex; + + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage( + "sharednotes.patchDocument", + onSuccessDebugger, + onErrorDebugger, + message + ); + } + + public function requestAdditionalNotesSet(additionalNotesSetSize:Number):void { + trace(LOG + "Sending [sharednotes.requestAdditionalNotesSet] to server."); + var message:Object = new Object(); + message["additionalNotesSetSize"] = additionalNotesSetSize; + + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage( + "sharednotes.requestAdditionalNotesSet", + onSuccessDebugger, + onErrorDebugger, + message + ); + } + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/util/shared_notes.js b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/util/shared_notes.js new file mode 100644 index 0000000000000000000000000000000000000000..67b7d09a6a36d3202ac878783bfff9a3675edc77 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/util/shared_notes.js @@ -0,0 +1,313 @@ +/* + This file is part of BBB-Notes. + + Copyright (c) Islam El-Ashi. All rights reserved. + + BBB-Notes is free software: you can redistribute it and/or modify + it under the terms of the Lesser GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + any later version. + + BBB-Notes 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 + Lesser GNU General Public License for more details. + + You should have received a copy of the Lesser GNU General Public License + along with BBB-Notes. If not, see <http://www.gnu.org/licenses/>. + + Author: Islam El-Ashi <ielashi@gmail.com>, <http://www.ielashi.com> +*/ +var dmp = new diff_match_patch(); +var debug = false; + +function diff(text1, text2) { + return dmp.patch_toText(dmp.patch_make(dmp.diff_main(unescape(text1),unescape(text2)))); +} + +function patch(patch, text) { + return dmp.patch_apply(dmp.patch_fromText(patch), unescape(text))[0]; +} + +function unpatch(patch, text) { + return dmp.patch_apply_reverse(dmp.patch_fromText(patch), unescape(text))[0]; +} + + +/** + * Helper Methods + */ + +/** + * MobWrite - Real-time Synchronization and Collaboration Service + * + * Copyright 2006 Google Inc. + * http://code.google.com/p/google-mobwrite/ + * + * 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. + */ + +/** + * Modify the user's plaintext by applying a series of patches against it. + * @param {Array.<patch_obj>} patches Array of Patch objects. + * @param {String} client text + */ +function patchClientText(patches, text, selectionStart, selectionEnd) { + text = unescape(text) + // Set some constants which tweak the matching behaviour. + // Maximum distance to search from expected location. + dmp.Match_Distance = 1000; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose) + dmp.Match_Threshold = 0.6; + + var oldClientText = text + oldClientText = oldClientText + "``````````````````````"; + var cursor = captureCursor_(oldClientText, selectionStart, selectionEnd); + // Pack the cursor offsets into an array to be adjusted. + // See http://neil.fraser.name/writing/cursor/ + var offsets = []; + if (cursor) { + offsets[0] = cursor.startOffset; + if ('endOffset' in cursor) { + offsets[1] = cursor.endOffset; + } + } + var newClientText = patch_apply_(patches, oldClientText, offsets); + // Set the new text only if there is a change to be made. + if (oldClientText != newClientText) { + //this.setClientText(newClientText); + if (cursor) { + // Unpack the offset array. + cursor.startOffset = offsets[0]; + if (offsets.length > 1) { + cursor.endOffset = offsets[1]; + if (cursor.startOffset >= cursor.endOffset) { + cursor.collapsed = true; + } + } + //document.write(oldClientText+"<br>"); + + //document.write(newClientText+"<br>"); + //document.write(newClientText.substring(0,newClientText.length-22)+"<br>"); + return [restoreCursor_(cursor, newClientText), newClientText.substring(0,newClientText.length-22)]; + // this.restoreCursor_(cursor); + } + } + // no change in client text + + return [[selectionStart, selectionEnd], newClientText.substring(0,newClientText.length-22)]; +} + +/** + * Merge a set of patches onto the text. Return a patched text. + * @param {Array.<patch_obj>} patches Array of patch objects. + * @param {string} text Old text. + * @param {Array.<number>} offsets Offset indices to adjust. + * @return {string} New text. + */ +function patch_apply_(patchText, text, offsets) { + var patches = dmp.patch_fromText(patchText); + if (patches.length == 0) { + return text; + } + + // Deep copy the patches so that no changes are made to originals. + patches = dmp.patch_deepCopy(patches); + var nullPadding = dmp.patch_addPadding(patches); + text = nullPadding + text + nullPadding; + + dmp.patch_splitMax(patches); + // delta keeps track of the offset between the expected and actual location + // of the previous patch. If there are patches expected at positions 10 and + // 20, but the first patch was found at 12, delta is 2 and the second patch + // has an effective expected position of 22. + var delta = 0; + for (var x = 0; x < patches.length; x++) { + var expected_loc = patches[x].start2 + delta; + var text1 = dmp.diff_text1(patches[x].diffs); + var start_loc; + var end_loc = -1; + if (text1.length > dmp.Match_MaxBits) { + // patch_splitMax will only provide an oversized pattern in the case of + // a monster delete. + start_loc = dmp.match_main(text, text1.substring(0, dmp.Match_MaxBits), expected_loc); + if (start_loc != -1) { + end_loc = dmp.match_main(text, text1.substring(text1.length - dmp.Match_MaxBits), + expected_loc + text1.length - dmp.Match_MaxBits); + if (end_loc == -1 || start_loc >= end_loc) { + // Can't find valid trailing context. Drop this patch. + start_loc = -1; + } + } + } else { + start_loc = dmp.match_main(text, text1, expected_loc); + } + if (start_loc == -1) { + // No match found. :( + if (debug) { + window.console.warn('Patch failed: ' + patches[x]); + } + // Subtract the delta for this failed patch from subsequent patches. + delta -= patches[x].length2 - patches[x].length1; + } else { + // Found a match. :) + if (debug) { + window.console.info('Patch OK.'); + } + delta = start_loc - expected_loc; + var text2; + if (end_loc == -1) { + text2 = text.substring(start_loc, start_loc + text1.length); + } else { + text2 = text.substring(start_loc, end_loc + dmp.Match_MaxBits); + } + // Run a diff to get a framework of equivalent indices. + var diffs = dmp.diff_main(text1, text2, false); + if (text1.length > dmp.Match_MaxBits && + dmp.diff_levenshtein(diffs) / text1.length > + dmp.Patch_DeleteThreshold) { + // The end points match, but the content is unacceptably bad. + if (debug) { + window.console.warn('Patch contents mismatch: ' + patches[x]); + } + } else { + var index1 = 0; + var index2; + for (var y = 0; y < patches[x].diffs.length; y++) { + var mod = patches[x].diffs[y]; + if (mod[0] !== DIFF_EQUAL) { + index2 = dmp.diff_xIndex(diffs, index1); + } + if (mod[0] === DIFF_INSERT) { // Insertion + text = text.substring(0, start_loc + index2) + mod[1] + + text.substring(start_loc + index2); + for (var i = 0; i < offsets.length; i++) { + if (offsets[i] + nullPadding.length > start_loc + index2) { + offsets[i] += mod[1].length; + } + } + } else if (mod[0] === DIFF_DELETE) { // Deletion + var del_start = start_loc + index2; + var del_end = start_loc + dmp.diff_xIndex(diffs, + index1 + mod[1].length); + text = text.substring(0, del_start) + text.substring(del_end); + for (var i = 0; i < offsets.length; i++) { + if (offsets[i] + nullPadding.length > del_start) { + if (offsets[i] + nullPadding.length < del_end) { + offsets[i] = del_start - nullPadding.length; + } else { + offsets[i] -= del_end - del_start; + } + } + } + } + if (mod[0] !== DIFF_DELETE) { + index1 += mod[1].length; + } + } + } + } + } + // Strip the padding off. + text = text.substring(nullPadding.length, text.length - nullPadding.length); + return text; +} + + +/** + * Record information regarding the current cursor. + * @return {Object?} Context information of the cursor. + * @private + */ +function captureCursor_(text, selectionStart, selectionEnd) { + var padLength = dmp.Match_MaxBits / 2; // Normally 16. + var cursor = {}; + + cursor.startPrefix = text.substring(selectionStart - padLength, selectionStart); + cursor.startSuffix = text.substring(selectionStart, selectionStart + padLength); + cursor.startOffset = selectionStart; + cursor.collapsed = (selectionStart == selectionEnd); + if (!cursor.collapsed) { + cursor.endPrefix = text.substring(selectionEnd - padLength, selectionEnd); + cursor.endSuffix = text.substring(selectionEnd, selectionEnd + padLength); + cursor.endOffset = selectionEnd; + } + + return cursor; +} + +/** + * Attempt to restore the cursor's location. + * @param {Object} cursor Context information of the cursor. + * @private + */ +function restoreCursor_(cursor, text) { + // Set some constants which tweak the matching behaviour. + // Maximum distance to search from expected location. + dmp.Match_Distance = 1000; + // At what point is no match declared (0.0 = perfection, 1.0 = very loose) + dmp.Match_Threshold = 0.9; + + var padLength = dmp.Match_MaxBits / 2; // Normally 16. + var newText = text; + // Find the start of the selection in the new text. + var pattern1 = cursor.startPrefix + cursor.startSuffix; + var pattern2, diff; + var cursorStartPoint = dmp.match_main(newText, pattern1, + cursor.startOffset - padLength); + + + if (cursorStartPoint !== null) { + pattern2 = newText.substring(cursorStartPoint, + cursorStartPoint + pattern1.length); + //alert(pattern1 + '\nvs\n' + pattern2); + // Run a diff to get a framework of equivalent indicies. + diff = dmp.diff_main(pattern1, pattern2, false); + + cursorStartPoint += dmp.diff_xIndex(diff, cursor.startPrefix.length); + } + + var cursorEndPoint = null; + if (!cursor.collapsed) { + // Find the end of the selection in the new text. + pattern1 = cursor.endPrefix + cursor.endSuffix; + cursorEndPoint = dmp.match_main(newText, pattern1, + cursor.endOffset - padLength); + if (cursorEndPoint !== null) { + pattern2 = newText.substring(cursorEndPoint, + cursorEndPoint + pattern1.length); + //alert(pattern1 + '\nvs\n' + pattern2); + // Run a diff to get a framework of equivalent indicies. + diff = dmp.diff_main(pattern1, pattern2, false); + cursorEndPoint += dmp.diff_xIndex(diff, cursor.endPrefix.length); + } + } + + // Deal with loose ends + if (cursorStartPoint === null && cursorEndPoint !== null) { + // Lost the start point of the selection, but we have the end point. + // Collapse to end point. + cursorStartPoint = cursorEndPoint; + } else if (cursorStartPoint === null && cursorEndPoint === null) { + // Lost both start and end points. + // Jump to the offset of start. + cursorStartPoint = cursor.startOffset; + } + if (cursorEndPoint === null) { + // End not known, collapse to start. + cursorEndPoint = cursorStartPoint; + } + + return [cursorStartPoint, cursorEndPoint]; +} + diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/AdditionalSharedNotesWindow.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/AdditionalSharedNotesWindow.as new file mode 100644 index 0000000000000000000000000000000000000000..f6535999b02471682b2ec5bccdf36e63346077ac --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/AdditionalSharedNotesWindow.as @@ -0,0 +1,71 @@ +package org.bigbluebutton.modules.sharednotes.views +{ + import flash.display.Sprite; + import flash.events.MouseEvent; + + import mx.controls.Alert; + import mx.events.CloseEvent; + + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.main.views.MainCanvas; + import org.bigbluebutton.modules.sharednotes.events.SharedNotesEvent; + import org.bigbluebutton.util.i18n.ResourceUtil; + + public class AdditionalSharedNotesWindow extends SharedNotesWindow + { + private var _windowName:String; + + public function AdditionalSharedNotesWindow(n:String) { + super(); + + trace("AdditionalSharedNotesWindow: in-constructor additional notes " + n); + _notesId = n; + _windowName = "AdditionalSharedNotesWindow_" + _notesId; + + showCloseButton = UsersUtil.amIModerator(); + width = 240; + height = 240; + + closeBtn.addEventListener(MouseEvent.CLICK, onCloseBtnClick); + } + + public function get windowName():String { + return this._windowName; + } + + override public function onCreationComplete():void { + super.onCreationComplete(); + + trace("AdditionalSharedNotesWindow: [2] in-constructor additional notes " + noteId); + + btnNew.visible = btnNew.includeInLayout = false; + } + + private function onCloseBtnClick(e:MouseEvent):void { + var alert:Alert = Alert.show( + ResourceUtil.getInstance().getString('bbb.sharedNotes.additionalNotes.closeWarning.message'), + ResourceUtil.getInstance().getString('bbb.sharedNotes.additionalNotes.closeWarning.title'), + Alert.YES | Alert.NO, parent as Sprite, alertClose, null, Alert.YES); + e.stopPropagation(); + } + + private function alertClose(e:CloseEvent):void { + if (e.detail == Alert.YES) { + showCloseButton = false; + + trace("AdditionalSharedNotesWindow: requesting to destroy notes " + noteId); + var destroyNotesEvent:SharedNotesEvent = new SharedNotesEvent(SharedNotesEvent.DESTROY_ADDITIONAL_NOTES_REQUEST_EVENT); + destroyNotesEvent.payload.notesId = _notesId; + _dispatcher.dispatchEvent(destroyNotesEvent); + } + } + + override public function getPrefferedPosition():String { + return MainCanvas.POPUP; + } + + override protected function updateTitle():void { + title = ResourceUtil.getInstance().getString('bbb.sharedNotes.title') + " " + noteId; + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/SharedNotesWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/SharedNotesWindow.mxml new file mode 100644 index 0000000000000000000000000000000000000000..f30f3c8e656b675052cfd82f877bb70002108d10 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/SharedNotesWindow.mxml @@ -0,0 +1,211 @@ +<!-- + This file is part of BBB-Notes. + + Copyright (c) Islam El-Ashi. All rights reserved. + + BBB-Notes is free software: you can redistribute it and/or modify + it under the terms of the Lesser GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + any later version. + + BBB-Notes 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 + Lesser GNU General Public License for more details. + + You should have received a copy of the Lesser GNU General Public License + along with BBB-Notes. If not, see <http://www.gnu.org/licenses/>. + + Author: Islam El-Ashi <ielashi@gmail.com>, <http://www.ielashi.com> +--> +<containers:CustomMdiWindow xmlns:mx="http://www.adobe.com/2006/mxml" + xmlns:containers="org.bigbluebutton.common.*" + xmlns:mate="http://mate.asfusion.com/" + layout="absolute" + minWidth="160" + minHeight="160" + implements="org.bigbluebutton.common.IBbbModuleWindow" + creationComplete="onCreationComplete()" + xmlns:components="org.bigbluebutton.modules.sharednotes.views.components.*" + showCloseButton="false"> + + + <mate:Listener type="{ReceivePatchEvent.RECEIVE_PATCH_EVENT}" method="receivePatch" /> + + <mx:Script> + <![CDATA[ + import com.asfusion.mate.events.Dispatcher; + + import flash.utils.getTimer; + + import mx.accessibility.AlertAccImpl; + import mx.binding.utils.BindingUtils; + import mx.controls.Alert; + import mx.events.ResizeEvent; + import mx.events.SliderEvent; + import mx.utils.ObjectUtil; + import flexlib.mdi.containers.MDICanvas; + + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.common.IBbbModuleWindow; + import org.bigbluebutton.common.Role; + import org.bigbluebutton.main.views.MainCanvas; + import org.bigbluebutton.modules.sharednotes.views.components.PatchableTextArea; + import org.bigbluebutton.modules.sharednotes.SharedNotesOptions; + import org.bigbluebutton.modules.sharednotes.util.DiffPatch; + import org.bigbluebutton.modules.sharednotes.events.GetCurrentDocumentEvent; + import org.bigbluebutton.modules.sharednotes.events.CurrentDocumentEvent; + import org.bigbluebutton.modules.sharednotes.events.SendPatchEvent; + import org.bigbluebutton.modules.sharednotes.events.SharedNotesEvent; + import org.bigbluebutton.modules.sharednotes.events.ReceivePatchEvent; + import org.bigbluebutton.modules.sharednotes.util.DiffPatch; + import org.bigbluebutton.util.i18n.ResourceUtil; + import flash.utils.getQualifiedClassName; + import mx.collections.ArrayCollection; + + private var _room:String; + private var _uri:String; + private var _host:String; + private var _connection:NetConnection; + protected var _dispatcher:Dispatcher = new Dispatcher(); + private var _document:String = ""; + protected var _notesId:String="MAIN_WINDOW"; + + private var options:SharedNotesOptions = new SharedNotesOptions(); + + private var sendUpdateTimer:Timer; + + public function onCreationComplete():void { + sendUpdateTimer = new Timer(options.refreshDelay, 1); + + textArea.addEventListener("SHARED_NOTES_SAVED", function(e:Event):void { + Alert.show(ResourceUtil.getInstance().getString('bbb.sharedNotes.save.complete'), "", Alert.OK); + }); + + textArea.addEventListener(Event.CHANGE, function(e:Event):void { + if (!sendUpdateTimer.running) { + sendUpdateTimer.reset(); + sendUpdateTimer.start(); + } + }); + // the following code was used to decide which event to listen for updates in the text area + //textArea.addEventListener(flash.events.Event.CHANGE, onTextAreaEvent); + //textArea.addEventListener(KeyboardEvent.KEY_DOWN, onTextAreaEvent); + //textArea.addEventListener(TextEvent.TEXT_INPUT, onTextAreaEvent); + //textArea.addEventListener(mx.events.FlexEvent.DATA_CHANGE, onTextAreaEvent); + //textArea.addEventListener(mx.events.FlexEvent.UPDATE_COMPLETE, onTextAreaEvent); + + sendUpdateTimer.addEventListener(TimerEvent.TIMER, function(e:Event):void { + sendPatch(); + }); + + BindingUtils.bindSetter(updateRoleDependentProperties, UsersUtil.getMyself(), "role"); + + updateTitle(); + } + + private function onTextAreaEvent(e:Event):void { + trace("Text area event " + e.type + " on user " + UsersUtil.getMyUserID()); + } + + private function updateRoleDependentProperties(role:String):void { + if(noteId == "MAIN_WINDOW"){ + btnNew.visible = btnNew.includeInLayout = options.enableMultipleNotes && role == Role.MODERATOR; + } else { + showCloseButton = role == Role.MODERATOR; + } + } + + public function get noteId():String{ + return _notesId; + } + + public function addRemoteDocument(notes:Object):void{ + _document = notes[noteId]; + textArea.text = _document; + } + + private function sendPatch():void { + var clientText:String = new String(textArea.textFieldText); // a snapshot of the client text + LogUtil.debug("SEND PATCH" + clientText + "::" + _document); + if (_document != clientText) { + textArea.editable = false; + onSyncing(); +// trace("ADD LOCAL"); + var sendPatchEvent:SendPatchEvent = new SendPatchEvent(); + sendPatchEvent.noteId = noteId; + sendPatchEvent.patch = DiffPatch.diff(_document, clientText); + sendPatchEvent.beginIndex = textArea.selectionBeginIndex; + sendPatchEvent.endIndex = textArea.selectionEndIndex; + //patchHistory.push(messageToSend.patchData); + _dispatcher.dispatchEvent(sendPatchEvent); + _document = clientText; + textArea.editable = true; + onSynced(); + } + } + + private function receivePatch(e:ReceivePatchEvent):void { +// trace("SharedNotesWindow: patch received"); +// trace("=====\n" + e.patch + "\n====="); + if (e.patch != "" && e.noteId==noteId) { + var result:String = DiffPatch.patch(e.patch, _document); +// trace("SharedNotesWindow: before the patch\n" + _document); +// trace("SharedNotesWindow: after the patch\n" + result); + _document = result; + textArea.patchClientText(e.patch, e.beginIndex, e.endIndex); +// trace("SharedNotes: patching local with server modifications"); + } + } + + private function onSynced(e:Event = null):void { + } + + private function onSyncing(e:Event = null):void { + } + + private function onTextChange(e:Event):void { + onSyncing(e); + } + + protected function btnSave_clickHandler(event:MouseEvent):void + { + textArea.saveNotesToFile(title); + } + + protected function btnNew_clickHandler(event:MouseEvent):void + { + _dispatcher.dispatchEvent(new SharedNotesEvent(SharedNotesEvent.CREATE_ADDITIONAL_NOTES_REQUEST_EVENT)); + } + + public function getPrefferedPosition():String{ + return options.position; + } + + override public function close(event:MouseEvent=null):void { + super.close(event); + } + + override protected function resourcesChanged():void { + super.resourcesChanged(); + + updateTitle(); + } + + protected function updateTitle():void { + title = ResourceUtil.getInstance().getString('bbb.sharedNotes.title'); + } + + ]]> + </mx:Script> + + <mx:VBox width="100%" height="100%"> + <components:PatchableTextArea width="100%" height="100%" paddingBottom="0" id="textArea" editable="true" verticalScrollPolicy="auto"/> + + <mx:HBox width="100%" horizontalAlign="right" paddingTop="0"> + <mx:Button id="btnNew" width="26" height="26" click="btnNew_clickHandler(event)" icon="@Embed(source='images/ic_note_add_16px.png')" toolTip="{ResourceUtil.getInstance().getString('bbb.sharedNotes.new.toolTip')}"/> + <mx:Button width="26" height="26" click="btnSave_clickHandler(event)" icon="@Embed(source='images/ic_save_16px.png')" toolTip="{ResourceUtil.getInstance().getString('bbb.sharedNotes.save.toolTip')}"/> + </mx:HBox> + </mx:VBox> +</containers:CustomMdiWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/components/PatchableTextArea.as b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/components/PatchableTextArea.as new file mode 100644 index 0000000000000000000000000000000000000000..5b1deac905fc906f85051a8fbe5083887710a565 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/components/PatchableTextArea.as @@ -0,0 +1,170 @@ +/* + This file is part of BBB-Notes. + + Copyright (c) Islam El-Ashi. All rights reserved. + + BBB-Notes is free software: you can redistribute it and/or modify + it under the terms of the Lesser GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + any later version. + + BBB-Notes 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 + Lesser GNU General Public License for more details. + + You should have received a copy of the Lesser GNU General Public License + along with BBB-Notes. If not, see <http://www.gnu.org/licenses/>. + + Author: Islam El-Ashi <ielashi@gmail.com>, <http://www.ielashi.com> +*/ +package org.bigbluebutton.modules.sharednotes.views.components { + import com.asfusion.mate.events.Dispatcher; + + import mx.controls.TextArea; + + import flash.events.Event; + + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.modules.sharednotes.util.DiffPatch; + import flash.net.FileReference; + import org.bigbluebutton.util.i18n.ResourceUtil; + import flash.geom.*; + import flash.text.*; + import flash.events.MouseEvent; + import flash.events.FocusEvent; + import flash.events.KeyboardEvent + import flash.display.InteractiveObject; + + + public class PatchableTextArea extends TextArea { + private var _patch : String = ""; + private var _patchChanged : Boolean = false; + private var lastBegin:int = 0; + private var lastEnd:int = 0; + + public function PatchableTextArea() { + } + + public function init():void { + this.textField.alwaysShowSelection = true; + } + + public function set patch(value:String):void { + _patch = value; + _patchChanged = true; + invalidateProperties(); + } + + override protected function commitProperties():void + { + super.commitProperties(); + } + + public function get textFieldText():String { + return textField.text; + } + + public function saveNotesToFile(title:String):void { + var filename:String = title.replace(/\s+/g, '-').toLowerCase(); + var _fileRef:FileReference = new FileReference(); + _fileRef.addEventListener(Event.COMPLETE, function(e:Event):void { + dispatchEvent(new Event("SHARED_NOTES_SAVED")); + }); + + var cr:String = String.fromCharCode(13); + var lf:String = String.fromCharCode(10); + var crlf:String = String.fromCharCode(13, 10); + + var textToExport:String = this.textFieldText; + textToExport = textToExport.replace(new RegExp(crlf, "g"), '\n'); + textToExport = textToExport.replace(new RegExp(cr, "g"), '\n'); + textToExport = textToExport.replace(new RegExp(lf, "g"), '\n'); + textToExport = textToExport.replace(new RegExp('\n', "g"), crlf); + + _fileRef.save(textToExport, filename+".txt"); + + //Future code to add zoom and font format + //var format:TextFormat = new TextFormat(); + //format.color = 0x0000FF; + //this.textField.setTextFormat( format, selectionBeginIndex, selectionBeginIndex+1 ) ; + //restorePositon(200); + } + + public function getOldPosition():Number { + var oldPosition:Number = 0; + if(selectionEndIndex == 0 && this.text.length == 0) { + oldPosition = 0; + } + else if(selectionEndIndex == this.text.length) { + oldPosition = this.textField.getLineIndexOfChar(selectionEndIndex-1); + } + else + oldPosition = this.textField.getLineIndexOfChar(selectionEndIndex); + + oldPosition-=this.verticalScrollPosition; + return oldPosition; + } + + public function restoreCursor(endIndex:Number, oldPosition:Number, oldVerticalPosition:Number):void { + var cursorLine:Number = 0; + if(endIndex == 0 && this.text.length == 0) { + cursorLine = 0; + } + else if(endIndex == this.text.length) { + cursorLine = this.textField.getLineIndexOfChar(endIndex-1); + } + else + cursorLine = this.textField.getLineIndexOfChar(endIndex); + + var relativePositon:Number = cursorLine - this.verticalScrollPosition; + + var desloc:Number = relativePositon - oldPosition; + this.verticalScrollPosition+=desloc; + + trace("relative: " + relativePositon); + trace("old: " + oldPosition); + trace("vertical: " + this.verticalScrollPosition); + } + + + public function patchClientText(patch:String, beginIndex:Number, endIndex:Number):void { + var results:Array; + + lastBegin = selectionBeginIndex; + lastEnd = selectionEndIndex; + var oldPosition:Number = getOldPosition(); + var oldVerticalPosition:Number = this.verticalScrollPosition; + + trace("Initial Position: " + lastBegin + " " + lastEnd); + results = DiffPatch.patchClientText(patch, textField.text, selectionBeginIndex, selectionEndIndex); + + if(results[0][0] == lastBegin && results[0][1] > lastEnd) { + var str1:String = this.text.substring(lastBegin,lastEnd); + var str2:String = results[1].substring(lastBegin,lastEnd); + trace("STRING 1: " + str1); + trace("STRING 2: " + str2); + + if(str1 != str2) { + lastEnd = results[0][1]; + } + + } else { + lastBegin = results[0][0]; + lastEnd = results[0][1]; + } + this.text = results[1]; + this.validateNow(); + + trace("Final Position: " + results[0][0] + " " + results[0][1]); + + trace("Length: " + this.text.length); + restoreCursor(lastEnd, oldPosition, oldVerticalPosition); + this.validateNow(); + textField.selectable = true; + // textField.stage.focus = InteractiveObject(textField); + textField.setSelection(lastBegin, lastEnd); + this.validateNow(); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/action_refresh.gif b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/action_refresh.gif similarity index 100% rename from bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/action_refresh.gif rename to bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/action_refresh.gif diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/arrow_left.png b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/arrow_left.png similarity index 100% rename from bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/arrow_left.png rename to bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/arrow_left.png diff --git a/bigbluebutton-client/src/org/bigbluebutton/common/assets/images/disk.png b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/disk.png similarity index 100% rename from bigbluebutton-client/src/org/bigbluebutton/common/assets/images/disk.png rename to bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/disk.png diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/ic_note_add_16px.png b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/ic_note_add_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..efe50b413b11446ae64ba09642e4d7592b842e3f Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/ic_note_add_16px.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/ic_save_16px.png b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/ic_save_16px.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ef221d2d969ac54ebc43bd1f889d93608e6540 Binary files /dev/null and b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/ic_save_16px.png differ diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/note_edit.png b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/note_edit.png similarity index 100% rename from bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/note_edit.png rename to bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/note_edit.png diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/play-icon.png b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/play-icon.png similarity index 100% rename from bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/play-icon.png rename to bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/play-icon.png diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/tick.png b/bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/tick.png similarity index 100% rename from bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/images/tick.png rename to bigbluebutton-client/src/org/bigbluebutton/modules/sharednotes/views/images/tick.png diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/maps/UsersMainEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/users/maps/UsersMainEventMap.mxml index 5205fcd806f87465756e3f5e23c2178adf78ec8b..c5e983becf105e9281fa17b738481ab965a51457 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/maps/UsersMainEventMap.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/maps/UsersMainEventMap.mxml @@ -28,13 +28,17 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.core.events.VoiceConfEvent; import org.bigbluebutton.main.events.BBBEvent; import org.bigbluebutton.main.events.LogoutEvent; + <!--TODO: Move guest events to user events? --> + import org.bigbluebutton.main.events.ResponseModeratorEvent; + import org.bigbluebutton.main.events.SuccessfulLoginEvent; import org.bigbluebutton.main.events.UserServicesEvent; + import org.bigbluebutton.main.model.GuestManager; import org.bigbluebutton.main.model.users.UserService; import org.bigbluebutton.main.model.users.events.BroadcastStartedEvent; import org.bigbluebutton.main.model.users.events.BroadcastStoppedEvent; + import org.bigbluebutton.main.model.users.events.ChangeRoleEvent; + import org.bigbluebutton.main.model.users.events.ChangeStatusEvent; import org.bigbluebutton.main.model.users.events.KickUserEvent; - import org.bigbluebutton.main.model.users.events.LowerHandEvent; - import org.bigbluebutton.main.model.users.events.RaiseHandEvent; import org.bigbluebutton.main.model.users.events.RoleChangeEvent; import org.bigbluebutton.main.model.users.events.UsersConnectionEvent; ]]> @@ -43,6 +47,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <EventHandlers type="{FlexEvent.APPLICATION_COMPLETE}" > <ObjectBuilder generator="{UserService}" cache="global" /> + <ObjectBuilder generator="{GuestManager}" cache="global" /> </EventHandlers> <EventHandlers type="{LogoutEvent.USER_LOGGED_OUT}" > @@ -62,12 +67,8 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. </EventHandlers> <!-- Viewers Stuff --> - <EventHandlers type="{RaiseHandEvent.RAISE_HAND}" > - <MethodInvoker generator="{UserService}" method="raiseHand" arguments="{event}" /> - </EventHandlers> - - <EventHandlers type="{LowerHandEvent.LOWER_HAND_EVENT}" > - <MethodInvoker generator="{UserService}" method="lowerHand" arguments="{event}" /> + <EventHandlers type="{ChangeStatusEvent.CHANGE_STATUS}" > + <MethodInvoker generator="{UserService}" method="changeStatus" arguments="{event}" /> </EventHandlers> <EventHandlers type="{BroadcastStartedEvent.BROADCAST_STARTED_EVENT}" > @@ -91,6 +92,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <MethodInvoker generator="{UserService}" method="kickUser" arguments="{event}" /> </EventHandlers> + <EventHandlers type="{ChangeRoleEvent.CHANGE_ROLE_EVENT}" > + <MethodInvoker generator="{UserService}" method="changeRole" arguments="{event}" /> + </EventHandlers> <EventHandlers type="{VoiceConfEvent.EJECT_USER}" > <MethodInvoker generator="{UserService}" method="ejectUser" arguments="{event}" /> @@ -140,4 +144,33 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <EventHandlers type="{RoleChangeEvent.ASSIGN_PRESENTER}"> <MethodInvoker generator="{UserService}" method="assignPresenter" arguments="{event}" /> </EventHandlers> + <!-- End Lock Events --> + + <!-- Guest Events --> + <EventHandlers type="{BBBEvent.ADD_GUEST_TO_LIST}" > + <MethodInvoker generator="{GuestManager}" method="addGuest" arguments="{event}" /> + </EventHandlers> + + <EventHandlers type="{BBBEvent.REMOVE_GUEST_FROM_LIST}" > + <MethodInvoker generator="{GuestManager}" method="removeGuest" arguments="{event.payload.userId}" /> + </EventHandlers> + + <EventHandlers type="{LogoutEvent.MODERATOR_DENIED_ME}" > + <MethodInvoker generator="{UserService}" method="guestDisconnect" /> + <MethodInvoker generator="{UserService}" method="logoutUser" /> + </EventHandlers> + + <EventHandlers type="{ResponseModeratorEvent.RESPONSE}" > + <MethodInvoker generator="{UserService}" method="responseToGuest" arguments="{event}" /> + <!-- MethodInvoker generator="{GuestManager}" method="removeGuest" arguments="{event.userid}" /--> + </EventHandlers> + + <EventHandlers type="{ResponseModeratorEvent.RESPONSE_ALL}" > + <MethodInvoker generator="{UserService}" method="responseToGuest" arguments="{event}" /> + </EventHandlers> + + <EventHandlers type="{BBBEvent.BROADCAST_GUEST_POLICY}" > + <MethodInvoker generator="{UserService}" method="setGuestPolicy" arguments="{event}" /> + </EventHandlers> + <!-- End Guest Events --> </EventMap> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as index 6981b9fc4b33efed194b90184036223cdc8de76c..99f8b2c97e128a162b0920db6b4c496bcf5387a3 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as @@ -33,6 +33,7 @@ package org.bigbluebutton.modules.users.services import org.bigbluebutton.core.vo.LockSettings; import org.bigbluebutton.core.vo.LockSettingsVO; import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.events.LogoutEvent; import org.bigbluebutton.main.events.MadePresenterEvent; import org.bigbluebutton.main.events.PresenterStatusEvent; import org.bigbluebutton.main.events.SwitchedPresenterEvent; @@ -41,6 +42,8 @@ package org.bigbluebutton.modules.users.services import org.bigbluebutton.main.model.users.BBBUser; import org.bigbluebutton.main.model.users.Conference; import org.bigbluebutton.main.model.users.IMessageListener; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; + import org.bigbluebutton.main.model.users.events.ChangeStatusBtnEvent; import org.bigbluebutton.main.model.users.events.RoleChangeEvent; import org.bigbluebutton.main.model.users.events.UsersConnectionEvent; import org.bigbluebutton.modules.present.events.CursorEvent; @@ -55,6 +58,7 @@ package org.bigbluebutton.modules.users.services private var dispatcher:Dispatcher; private var _conference:Conference; + public var onAllowedToJoin:Function = null; private static var globalDispatcher:Dispatcher = new Dispatcher(); public function MessageReceiver() { @@ -94,6 +98,9 @@ package org.bigbluebutton.modules.users.services case "participantStatusChange": handleParticipantStatusChange(message); break; + case "participantRoleChange": + handleParticipantRoleChange(message); + break; case "userJoinedVoice": handleUserJoinedVoice(message); break; @@ -106,12 +113,6 @@ package org.bigbluebutton.modules.users.services case "voiceUserTalking": handleVoiceUserTalking(message); break; - case "userRaisedHand": - handleUserRaisedHand(message); - break; - case "userLoweredHand": - handleUserLoweredHand(message); - break; case "userSharedWebcam": handleUserSharedWebcam(message); break; @@ -136,6 +137,15 @@ package org.bigbluebutton.modules.users.services case "userLocked": handleUserLocked(message); break; + case "get_guest_policy_reply": + handleGetGuestPolicyReply(message); + break; + case "guest_policy_changed": + handleGuestPolicyChanged(message); + break; + case "guest_access_denied": + handleGuestAccessDenied(message); + break; } } @@ -376,19 +386,27 @@ package org.bigbluebutton.modules.users.services UsersService.getInstance().userLeft(webUser); + if(webUser.waitingForAcceptance) { + var removeGuest:BBBEvent = new BBBEvent(BBBEvent.REMOVE_GUEST_FROM_LIST); + removeGuest.payload.userId = webUser.userId; + dispatcher.dispatchEvent(removeGuest); + } + var user:BBBUser = UserManager.getInstance().getConference().getUser(webUserId); - - trace(LOG + "Notify others that user [" + user.userID + ", " + user.name + "] is leaving!!!!"); - - // Flag that the user is leaving the meeting so that apps (such as avatar) doesn't hang - // around when the user already left. - user.isLeavingFlag = true; - - var joinEvent:UserLeftEvent = new UserLeftEvent(UserLeftEvent.LEFT); - joinEvent.userID = user.userID; - dispatcher.dispatchEvent(joinEvent); - - UserManager.getInstance().getConference().removeUser(webUserId); + // If the user is null, it was a rejected guest + if(user != null) { + trace(LOG + "Notify others that user [" + user.userID + ", " + user.name + "] is leaving!!!!"); + + // Flag that the user is leaving the meeting so that apps (such as avatar) doesn't hang + // around when the user already left. + user.isLeavingFlag = true; + + var joinEvent:UserLeftEvent = new UserLeftEvent(UserLeftEvent.LEFT); + joinEvent.userID = user.userID; + dispatcher.dispatchEvent(joinEvent); + + UserManager.getInstance().getConference().removeUser(webUserId); + } } public function handleParticipantJoined(msg:Object):void { @@ -495,18 +513,6 @@ package org.bigbluebutton.modules.users.services dispatcher.dispatchEvent(roleEvent); } - private function handleUserRaisedHand(msg: Object): void { - trace(LOG + "*** handleUserRaisedHand " + msg.msg + " **** \n"); - var map:Object = JSON.parse(msg.msg); - UserManager.getInstance().getConference().raiseHand(map.userId, true); - } - - private function handleUserLoweredHand(msg: Object):void { - trace(LOG + "*** handleUserLoweredHand " + msg.msg + " **** \n"); - var map:Object = JSON.parse(msg.msg); - UserManager.getInstance().getConference().raiseHand(map.userId, false); - } - private function handleUserSharedWebcam(msg: Object):void { trace(LOG + "*** handleUserSharedWebcam " + msg.msg + " **** \n"); var map:Object = JSON.parse(msg.msg); @@ -516,7 +522,7 @@ package org.bigbluebutton.modules.users.services private function handleUserUnsharedWebcam(msg: Object):void { trace(LOG + "*** handleUserUnsharedWebcam " + msg.msg + " **** \n"); var map:Object = JSON.parse(msg.msg); - UserManager.getInstance().getConference().unsharedWebcam(map.userId); + UserManager.getInstance().getConference().unsharedWebcam(map.userId, map.webcamStream); } public function participantStatusChange(userID:String, status:String, value:Object):void { @@ -537,10 +543,13 @@ package org.bigbluebutton.modules.users.services user.userID = joinedUser.userId; user.name = joinedUser.name; user.role = joinedUser.role; + user.guest = joinedUser.guest; + user.waitingForAcceptance = joinedUser.waitingForAcceptance; user.externUserID = joinedUser.externUserID; user.isLeavingFlag = false; user.listenOnly = joinedUser.listenOnly; user.userLocked = joinedUser.locked; + user.me = (user.userID == UserManager.getInstance().getConference().getMyUserId()) trace(LOG + "User status: hasStream " + joinedUser.hasStream); @@ -548,18 +557,48 @@ package org.bigbluebutton.modules.users.services UserManager.getInstance().getConference().addUser(user); if (joinedUser.hasStream) { - UserManager.getInstance().getConference().sharedWebcam(user.userID, joinedUser.webcamStream); - } else { - UserManager.getInstance().getConference().unsharedWebcam(user.userID); + var streams:Array = joinedUser.webcamStream.split("|"); + for each(var stream:String in streams) { + UserManager.getInstance().getConference().sharedWebcam(user.userID, stream); + } } UserManager.getInstance().getConference().presenterStatusChanged(user.userID, joinedUser.presenter); - UserManager.getInstance().getConference().raiseHand(user.userID, joinedUser.raiseHand); + UserManager.getInstance().getConference().newUserStatus(user.userID, "mood", joinedUser.mood); var joinEvent:UserJoinedEvent = new UserJoinedEvent(UserJoinedEvent.JOINED); joinEvent.userID = user.userID; dispatcher.dispatchEvent(joinEvent); - + + if (user.guest) { + if (user.waitingForAcceptance) { + if (user.me) { + var waitCommand:BBBEvent = new BBBEvent(BBBEvent.WAITING_FOR_MODERATOR_ACCEPTANCE); + dispatcher.dispatchEvent(waitCommand); + } else { + var e:BBBEvent = new BBBEvent(BBBEvent.ADD_GUEST_TO_LIST); + e.payload.userId = user.userID; + e.payload.name = user.name; + dispatcher.dispatchEvent(e); + } + } else { + if (user.me) { + var allowedCommand:BBBEvent = new BBBEvent(BBBEvent.MODERATOR_ALLOWED_ME_TO_JOIN); + dispatcher.dispatchEvent(allowedCommand); + } else { + var removeGuest:BBBEvent = new BBBEvent(BBBEvent.REMOVE_GUEST_FROM_LIST); + removeGuest.payload.userId = user.userID; + dispatcher.dispatchEvent(removeGuest); + } + } + } + + if (user.me && (!user.guest || !user.waitingForAcceptance)) { + if (onAllowedToJoin != null) { + onAllowedToJoin(); + onAllowedToJoin = null; + } + } } /** @@ -571,6 +610,11 @@ package org.bigbluebutton.modules.users.services trace(LOG + "Received status change [" + map.userID + "," + map.status + "," + map.value + "]") UserManager.getInstance().getConference().newUserStatus(map.userID, map.status, map.value); + var status:String = map.value; + var statusArray:Array = status.split(","); + if(map.status == "mood") { + dispatcher.dispatchEvent(new ChangeStatusBtnEvent(map.userID, statusArray[0])); + } if (msg.status == "presenter"){ var e:PresenterStatusEvent = new PresenterStatusEvent(PresenterStatusEvent.PRESENTER_NAME_CHANGE); @@ -579,5 +623,43 @@ package org.bigbluebutton.modules.users.services dispatcher.dispatchEvent(e); } } + + public function handleParticipantRoleChange(msg:Object):void { + var map:Object = JSON.parse(msg.msg); + trace(LOG + "*** received participant role change [" + map.userID + "," + map.role + "]"); + UserManager.getInstance().getConference().newUserRole(map.userID, map.role); + if(UserManager.getInstance().getConference().amIThisUser(map.userID)) { + UserManager.getInstance().getConference().setMyRole(map.role); + var e:ChangeMyRole = new ChangeMyRole(map.role); + dispatcher.dispatchEvent(e); + } + } + + public function handleGuestPolicyChanged(msg:Object):void { + trace(LOG + "*** handleGuestPolicyChanged " + msg.msg + " **** \n"); + var map:Object = JSON.parse(msg.msg); + + var policy:BBBEvent = new BBBEvent(BBBEvent.RETRIEVE_GUEST_POLICY); + policy.payload['guestPolicy'] = map.guestPolicy; + dispatcher.dispatchEvent(policy); + } + + public function handleGetGuestPolicyReply(msg:Object):void { + trace(LOG + "*** handleGetGuestPolicyReply " + msg.msg + " **** \n"); + var map:Object = JSON.parse(msg.msg); + + var policy:BBBEvent = new BBBEvent(BBBEvent.RETRIEVE_GUEST_POLICY); + policy.payload['guestPolicy'] = map.guestPolicy; + dispatcher.dispatchEvent(policy); + } + + public function handleGuestAccessDenied(msg:Object):void { + trace(LOG + "*** handleGuestAccessDenied " + msg.msg + " ****"); + var map:Object = JSON.parse(msg.msg); + + if (UsersUtil.getMyUserID() == map.userId) { + dispatcher.dispatchEvent(new LogoutEvent(LogoutEvent.MODERATOR_DENIED_ME)); + } + } } } \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageSender.as b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageSender.as index 58998b4cc0947c97a78823e49066b72680e24142..36db2249e185078519a40b46fdd178cfb883cca5 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageSender.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageSender.as @@ -74,32 +74,21 @@ package org.bigbluebutton.modules.users.services ); } - public function raiseHand(userID:String, raise:Boolean):void { + public function changeStatus(userID:String, status:String):void { var _nc:ConnectionManager = BBB.initConnectionManager(); - if (raise) { - _nc.sendMessage("participants.userRaiseHand", - function(result:String):void { // On successful result - LogUtil.debug(result); - }, - function(status:String):void { // status - On error occurred - LogUtil.error(status); - } - ); - } else { - var message:Object = new Object(); - message["userId"] = userID; - message["loweredBy"] = userID; - - _nc.sendMessage("participants.lowerHand", - function(result:String):void { // On successful result - LogUtil.debug(result); - }, - function(status:String):void { // status - On error occurred - LogUtil.error(status); - }, - message - ); - } + var message:Object = new Object(); + message["userID"] = userID; + message["status"] = "mood"; + message["value"] = status; + _nc.sendMessage("participants.setParticipantStatus", + function(result:String):void { // On successful result + LogUtil.debug(result); + }, + function(status:String):void { // status - On error occurred + LogUtil.error(status); + }, + message + ); } public function addStream(userID:String, streamName:String):void { @@ -123,7 +112,8 @@ package org.bigbluebutton.modules.users.services }, function(status:String):void { // status - On error occurred LogUtil.error(status); - } + }, + streamName ); } @@ -371,5 +361,76 @@ package org.bigbluebutton.modules.users.services newLockSettings ); } + + public function changeRole(userID:String, role:String):void { + var _nc:ConnectionManager = BBB.initConnectionManager(); + var message:Object = new Object(); + message["userId"] = userID; + message["role"] = role; + + _nc.sendMessage( + "participants.setParticipantRole",// Remote function name + function(result:String):void { // On successful result + LogUtil.debug(result); + }, + function(status:String):void { // status - On error occurred + LogUtil.error(status); + }, + message + ); + } + + public function queryForGuestPolicy():void { + trace(LOG + "queryForGuestPolicy"); + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage( + "participants.getGuestPolicy", + function(result:String):void { // On successful result + LogUtil.debug(result); + }, + function(status:String):void { // status - On error occurred + LogUtil.error(status); + } + ); + } + + public function setGuestPolicy(policy:String):void { + trace(LOG + "setGuestPolicy - new policy:[" + policy + "]"); + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage( + "participants.setGuestPolicy", + function(result:String):void { // On successful result + LogUtil.debug(result); + }, + function(status:String):void { // status - On error occurred + LogUtil.error(status); + }, + policy + ); + } + + public function responseToGuest(userId:String, response:Boolean):void { + trace(LOG + "responseToGuest - userId:[" + userId + "] response:[" + response + "]"); + + var message:Object = new Object(); + message["userId"] = userId; + message["response"] = response; + + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage( + "participants.responseToGuest", + function(result:String):void { // On successful result + LogUtil.debug(result); + }, + function(status:String):void { // status - On error occurred + LogUtil.error(status); + }, + message + ); + } + + public function responseToAllGuests(response:Boolean):void { + responseToGuest(null, response); + } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml index 9cb31de1a1d83ad796cc743e3873fa9ec5abeec7..0d4bd25c0eb6d683cc745179ce938431eb283565 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/MediaItemRenderer.mxml @@ -28,11 +28,14 @@ <mate:Listener type="{UsersRollEvent.USER_ROLL_OVER}" method="onRollOver" /> <mate:Listener type="{UsersRollEvent.USER_ROLL_OUT}" method="onRollOut" /> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="onChangeMyRole" /> <mx:Script> <![CDATA[ import mx.binding.utils.BindingUtils; + import mx.controls.Menu; import mx.events.FlexEvent; + import mx.events.MenuEvent; import org.bigbluebutton.common.Images; import org.bigbluebutton.common.Role; @@ -41,6 +44,8 @@ import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.core.vo.LockSettingsVO; import org.bigbluebutton.main.model.users.BBBUser; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; + import org.bigbluebutton.main.model.users.events.ChangeRoleEvent; import org.bigbluebutton.main.model.users.events.KickUserEvent; import org.bigbluebutton.modules.users.events.UsersRollEvent; import org.bigbluebutton.modules.users.events.ViewCameraEvent; @@ -62,8 +67,10 @@ private var options:UsersOptions; + private var myMenu:Menu = null; + private function onCreationComplete():void{ - lockBtn.enabled = muteBtn.enabled = kickUserBtn.enabled = moderator = UserManager.getInstance().getConference().amIModerator(); + refreshRole(UserManager.getInstance().getConference().amIModerator()); this.addEventListener(FlexEvent.DATA_CHANGE, dataChangeHandler); @@ -98,6 +105,19 @@ } } + private function onChangeMyRole(e:ChangeMyRole):void { + rolledOver = false; + updateButtons(); + // close the menu if it was opened + if (myMenu) myMenu.hide(); + + refreshRole(e.role == Role.MODERATOR); + } + + private function refreshRole(amIModerator:Boolean):void { + lockBtn.enabled = muteBtn.enabled = settingsBtn.enabled = moderator = amIModerator; + } + private function muteMouseOverHandler():void { rolledOverMute = true; updateButtons(); @@ -153,7 +173,7 @@ var ls:LockSettingsVO = UserManager.getInstance().getConference().getLockSettings(); if (data != null) { - kickUserBtn.visible = !data.me && rolledOver && options.allowKickUser; + settingsBtn.visible = rolledOver && !data.me; if (!data.voiceJoined) { if (data.listenOnly) { @@ -203,7 +223,8 @@ } if (data.hasStream) { - if (data.viewingStream || data.me) { + // if it's myself or if I'm watching all the streams from the given user, then don't activate the button + if (data.me || data.isViewingAllStreams()) { webcamImg.visible = true; webcamImg.includeInLayout = true; webcamBtn.visible = false; @@ -252,6 +273,69 @@ } } + private function promoteUser():void { + changeUserRole(Role.MODERATOR); + } + + private function demoteUser():void { + changeUserRole(Role.VIEWER); + } + + private function changeUserRole(role:String):void { + var changeRoleEvent:ChangeRoleEvent = new ChangeRoleEvent(data.userID, role); + dispatchEvent(changeRoleEvent); + } + + private function openSettings():void { + if (data != null) { + var myMenuData:Array = []; + + if (data.role == Role.MODERATOR) { + myMenuData.push({ + label: ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.demoteUser',[data.name]), + icon: images.user_delete, + callback: demoteUser + }); + } else { + myMenuData.push({ + label: ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.promoteUser',[data.name]), + icon: images.user_add, + callback: promoteUser + }); + } + + if (options.allowKickUser) { + myMenuData.push({ + label: ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.kickUser',[data.name]), + icon: images.eject_user_new, + callback: kickUser + }); + } + + // make sure the previous menu is closed before opening a new one + // This could be improved to include a flag that tells if the menu is open, + // but it would require an extra listener for the MenuCloseEvent. + if (myMenu) myMenu.hide(); + + myMenu = Menu.createMenu(null, myMenuData, true); + myMenu.variableRowHeight = true; + + var settingsBtnPos:Point = settingsBtn.localToGlobal(new Point(0,0)); + + var myMenuPos:Point = new Point(); + myMenuPos.x = settingsBtnPos.x + settingsBtn.width; + myMenuPos.y = settingsBtnPos.y; + + myMenu.addEventListener(MenuEvent.ITEM_CLICK, menuClickHandler); + myMenu.show(myMenuPos.x, myMenuPos.y); + myMenu.setFocus(); + } + } + + private function menuClickHandler(e:MenuEvent):void { + e.item.callback(); + } + ]]> </mx:Script> @@ -269,16 +353,16 @@ mouseOver="muteMouseOverHandler()" mouseOut="muteMouseOutHandler()" toolTip="{data.voiceMuted ? ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.pushToTalk',[data.name]) : ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.pushToMute',[data.name])}" /> - <mx:Button id="kickUserBtn" icon="{images.eject_user_new}" - width="20" height="20" visible="false" - toolTip="{ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.kickUser',[data.name])}" - click="kickUser()"/> <mx:Image id="lockImg" visible="false" includeInLayout="false" width="20" height="20" /> <mx:Button id="lockBtn" visible="false" includeInLayout="false" enabled="false" width="20" height="20" click="toggleLockState()" mouseOver="lockMouseOverHandler()" mouseOut="lockMouseOutHandler()" toolTip="{data.userLocked ? ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.pushToUnlock',[data.name]) : ResourceUtil.getInstance().getString('bbb.users.usersGrid.mediaItemRenderer.pushToLock',[data.name])}" /> + <mx:Button id="settingsBtn" visible="false" + width="20" height="20" click="openSettings()" + icon="{images.users_settings}" + toolTip="{ResourceUtil.getInstance().getString('bbb.users.settings.buttonTooltip')}" /> <!-- Helper objects because using BindingUtil with data break when the itemRenderer is recycled --> <mx:Image id="muteInd" includeInLayout="false" visible="{data.voiceMuted}" /> <mx:Image id="voiceJoinedInd" includeInLayout="false" visible="{data.voiceJoined}" /> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/StatusItemRenderer.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/StatusItemRenderer.mxml index 66e6b25a5157c0f61bc29b4b71a8ab68ce2eb2e7..19da4366b7a53063c7b76b48d12752f3c7633627 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/StatusItemRenderer.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/StatusItemRenderer.mxml @@ -30,6 +30,7 @@ <mate:Listener type="{UsersRollEvent.USER_ROLL_OVER}" method="onRollOver" /> <mate:Listener type="{UsersRollEvent.USER_ROLL_OUT}" method="onRollOut" /> <mate:Listener type="{LocaleChangeEvent.LOCALE_CHANGED}" method="localeChanged" /> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="onChangeMyRole" /> <mx:Script> <![CDATA[ @@ -41,7 +42,8 @@ import org.bigbluebutton.common.Images; import org.bigbluebutton.common.Role; import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.main.model.users.events.LowerHandEvent; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; + import org.bigbluebutton.main.model.users.events.ChangeStatusEvent; import org.bigbluebutton.main.model.users.events.RoleChangeEvent; import org.bigbluebutton.modules.users.events.UsersRollEvent; import org.bigbluebutton.util.i18n.ResourceUtil; @@ -51,7 +53,7 @@ private var moderator:Boolean = false; private function onCreationComplete():void { - moderator = UserManager.getInstance().getConference().amIModerator(); + refreshRole(UserManager.getInstance().getConference().amIModerator()); /* I was trying to the binds through actionscript, but every time the itemrenderer was recycled * the binds would stop functioning. I think it might have been because I was using strong @@ -69,7 +71,7 @@ */ BindingUtils.bindSetter(updateButton, presenterInd, "includeInLayout"); BindingUtils.bindSetter(updateButton, moderatorInd, "includeInLayout"); - BindingUtils.bindSetter(updateButton, raiseHandInd, "includeInLayout"); + BindingUtils.bindSetter(updateButton, statusInd, "includeInLayout"); // The next two lines should be listening for the same data to change and be updating at the same // time, but the FlexEvent.DATA_CHANGE wasn't working consistently. @@ -88,47 +90,107 @@ } } - private function updateButton(unneeded:Object = null):void { - if (data != null) { - if (rolledOver && data.raiseHand) { - roleBtn.setStyle("icon", images.hand_new); - roleBtn.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.lowerHand') + " - " + data.raiseHandTime.hours + ":" + data.raiseHandTime.minutes + ":" + data.raiseHandTime.seconds; - roleImg.visible = false; - roleBtn.visible = true; - roleBtn.enabled = true; - } else if (rolledOver && !data.presenter && !data.phoneUser) { - roleBtn.setStyle("icon", images.presenter_new); - roleBtn.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.changePresenter'); - roleImg.visible = false; - roleBtn.visible = true; - roleBtn.enabled = true; - } else if (data.raiseHand) { - roleImg.source = images.hand_new; - roleImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.handRaised') + " - " + data.raiseHandTime.hours + ":" + data.raiseHandTime.minutes + ":" + data.raiseHandTime.seconds; - roleImg.visible = true; - roleBtn.visible = false; - roleBtn.enabled = false; - } else if (data.presenter) { - roleImg.source = images.presenter_new; - roleImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.presenter'); + private function updateButton(unneeded:Object = null):void { + if (data != null) { + if (rolledOver) { //if i'm moderator and I rolled the mouse + if (data.hasMood) { + statusBtn.setStyle("icon", images.mood_clear); + if (data.raiseHand) { + statusBtn.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.lowerHand'); + } else { + statusBtn.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.clearStatus'); + } + + statusImg.visible = false; + statusBtn.visible = true; + statusBtn.enabled = true; + } + if (!data.presenter) { + roleBtn.setStyle("icon", images.presenter_new); + roleBtn.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.changePresenter'); + + roleImg.visible = false; + roleBtn.visible = true; + roleBtn.enabled = true; + } + } else { + //updating which role to show: presenter or moderator + if (data.presenter) { + roleImg.source = images.presenter_new; + roleImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.presenter') + } else if(data.role == Role.MODERATOR) { + roleImg.source = images.moderator; + roleImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.moderator'); + } else { + roleImg.source = null; + roleImg.toolTip = ""; + } + + //updating which status to show + switch(data.mood) { + case ChangeStatusEvent.RAISE_HAND: + statusImg.source = images.mood_raise_hand; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.handRaised'); + break; + + case ChangeStatusEvent.AGREE: + statusImg.source = images.mood_agreed; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.agree'); + break; + + case ChangeStatusEvent.DISAGREE: + statusImg.source = images.mood_disagreed; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.disagree'); + break; + + case ChangeStatusEvent.SPEAK_LOUDER: + statusImg.source = images.mood_speak_louder; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.speakLouder'); + break; + + case ChangeStatusEvent.SPEAK_LOWER: + statusImg.source = images.mood_speak_softer; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.speakSofter'); + break; + + case ChangeStatusEvent.SPEAK_FASTER: + statusImg.source = images.mood_speak_faster; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.speakFaster'); + break; + + case ChangeStatusEvent.SPEAK_SLOWER: + statusImg.source = images.mood_speak_slower; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.speakSlower'); + break; + + case ChangeStatusEvent.BE_RIGHT_BACK: + statusImg.source = images.mood_be_right_back; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.beRightBack'); + break; + + case ChangeStatusEvent.LAUGHTER: + statusImg.source = images.mood_happy; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.laughter'); + break; + + case ChangeStatusEvent.SAD: + statusImg.source = images.mood_sad; + statusImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.sad'); + break; + + default: //user has no status + statusImg.source = null; + statusImg.toolTip = ""; + } roleImg.visible = true; roleBtn.visible = false; roleBtn.enabled = false; - } else if (data.role == Role.MODERATOR) { - roleImg.source = images.moderator; - roleImg.toolTip = ResourceUtil.getInstance().getString('bbb.users.usersGrid.statusItemRenderer.moderator'); - roleImg.visible = true; - roleBtn.visible = false; - roleBtn.enabled = false; - } else { - roleImg.source = null; - roleImg.toolTip = ""; - roleImg.visible = true; - roleBtn.visible = false; - roleBtn.enabled = false; - } - } - } + statusImg.visible = true; + statusBtn.visible = false; + statusBtn.enabled = false; + } + } + } private function onRollOver(e:UsersRollEvent):void{ if (moderator && (e.userID == data.userID) && !data.phoneUser) { @@ -143,17 +205,32 @@ updateButton(); } } + + private function onChangeMyRole(e:ChangeMyRole):void { + rolledOver = false; + updateButton(); + + refreshRole(e.role == Role.MODERATOR); + } + + private function refreshRole(amIModerator:Boolean):void { + moderator = amIModerator; + } - private function roleBtnClicked():void { - if (data.raiseHand) { - dispatchEvent(new LowerHandEvent(data.userID)); - } else if (!data.presenter) { - var e:RoleChangeEvent = new RoleChangeEvent(RoleChangeEvent.ASSIGN_PRESENTER); - e.userid = data.userID; - e.username = data.name; - dispatchEvent(e); - } - } + private function roleBtnClicked():void { + if (!data.presenter) { + var e:RoleChangeEvent = new RoleChangeEvent(RoleChangeEvent.ASSIGN_PRESENTER); + e.userid = data.userID; + e.username = data.name; + dispatchEvent(e); + } + } + + private function statusBtnClicked():void { + if (data.hasMood) { + dispatchEvent( new ChangeStatusEvent(data.userID, ChangeStatusEvent.CLEAR_STATUS) ); + } + } // Need to refresh the roleBtn toolTip text on locale change private function localeChanged(e:Event):void { @@ -164,8 +241,11 @@ <mx:Image id="roleImg" visible="true" width="16" height="16" includeInLayout="{roleImg.visible}" /> <mx:Button id="roleBtn" visible="false" enabled="false" width="20" height="20" click="roleBtnClicked()" includeInLayout="{roleBtn.visible}" /> + <mx:Image id="statusImg" visible="true" width="18" height="18" includeInLayout="{statusImg.visible}" /> + <mx:Button id="statusBtn" visible="false" enabled="false" width="18" height="18" click="statusBtnClicked()" includeInLayout="{statusBtn.visible}" /> + <!-- Helper objects because direct bindings to data break when the itemRenderer is recycled --> <mx:Image id="presenterInd" includeInLayout="false" visible="{data.presenter}" /> <mx:Image id="moderatorInd" includeInLayout="false" visible="{data.role == Role.MODERATOR}" /> - <mx:Image id="raiseHandInd" includeInLayout="false" visible="{data.raiseHand}" /> + <mx:Image id="statusInd" includeInLayout="false" visible="{data.hasMood}" /> </mx:HBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml index f6e081075b153599ae6ff41234708c0b00d336cb..14068d980c1f45ba465d99fdda980bc68d467a8d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/views/UsersWindow.mxml @@ -35,6 +35,8 @@ <mate:Listener type="{ShortcutEvent.MUTE_ALL_BUT_PRES}" method="remoteMuteAllButPres" /> <mate:Listener type="{MeetingMutedEvent.MEETING_MUTED}" method="handleMeetingMuted" /> <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="handleChangedLockSettingsEvent" /> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="onChangeMyRole" /> + <mate:Listener type="{ChangeStatusBtnEvent.CHANGE_BTN_STATUS}" method="onStatusChanged"/> <mx:Script> <![CDATA[ import com.asfusion.mate.events.Dispatcher; @@ -60,9 +62,11 @@ import org.bigbluebutton.core.vo.LockSettingsVO; import org.bigbluebutton.main.events.ShortcutEvent; import org.bigbluebutton.main.model.users.BBBUser; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; + import org.bigbluebutton.main.model.users.events.ChangeRoleEvent; + import org.bigbluebutton.main.model.users.events.ChangeStatusBtnEvent; + import org.bigbluebutton.main.model.users.events.ChangeStatusEvent; import org.bigbluebutton.main.model.users.events.KickUserEvent; - import org.bigbluebutton.main.model.users.events.LowerHandEvent; - import org.bigbluebutton.main.model.users.events.RaiseHandEvent; import org.bigbluebutton.main.model.users.events.RoleChangeEvent; import org.bigbluebutton.main.views.LockSettings; import org.bigbluebutton.main.views.MainCanvas; @@ -104,15 +108,17 @@ private var muteMeRolled:Boolean = false; - private var myMenuData:Array = []; + private var moodMenuHeight:Number = -1; + + private function onChangeMyRole(e:ChangeMyRole):void { + refreshRole(e.role == Role.MODERATOR); + } private function onCreationComplete():void { dispatcher = new Dispatcher(); users = UserManager.getInstance().getConference().users; - amIModerator = UserManager.getInstance().getConference().amIModerator(); - - settingsBtn.visible = settingsBtn.includeInLayout = partOptions.enableSettingsButton && amIModerator; + refreshRole(UserManager.getInstance().getConference().amIModerator()); BindingUtils.bindSetter(updateNumberofUsers, users, "length"); @@ -139,6 +145,16 @@ addContextMenuItems(); } + private function refreshRole(moderator:Boolean = true):void { + amIModerator = moderator; + + settingsBtn.visible = settingsBtn.includeInLayout = partOptions.enableSettingsButton && amIModerator; + + changeButtons(UserManager.getInstance().getConference().amIPresenter); + + if (myMenu) myMenu.hide(); + } + public function getPrefferedPosition():String{ return MainCanvas.TOP_LEFT; } @@ -224,20 +240,116 @@ } private function raiseHand():void{ - var e:RaiseHandEvent = new RaiseHandEvent(RaiseHandEvent.RAISE_HAND); - if (UserManager.getInstance().getConference().isMyHandRaised) { - e.raised = false; - raiseHandBtn.accessibilityName = ResourceUtil.getInstance().getString('bbb.users.raiseHandBtn.toolTip2'); + var e:ChangeStatusEvent; + var userID:String = UserManager.getInstance().getConference().getMyUserId(); + if (UserManager.getInstance().getConference().getMyMood() == ChangeStatusEvent.RAISE_HAND) { + e = new ChangeStatusEvent(userID, ChangeStatusEvent.CLEAR_STATUS); + raiseHandBtn.accessibilityName = getStringForStatus(ChangeStatusEvent.CLEAR_STATUS); } else { - e.raised = true; - raiseHandBtn.accessibilityName = ResourceUtil.getInstance().getString('bbb.users.raiseHandBtn.toolTip'); + e = new ChangeStatusEvent(userID, ChangeStatusEvent.RAISE_HAND); + raiseHandBtn.accessibilityName = getStringForStatus(ChangeStatusEvent.RAISE_HAND); } dispatchEvent(e); } + + private function clearAllStatus():void { + for (var i:int = 0; i < users.length; i++) { + var p:BBBUser = users.getItemAt(i) as BBBUser; + if (p.hasMood) { + dispatchEvent( new ChangeStatusEvent(p.userID, ChangeStatusEvent.CLEAR_STATUS) ); + } + } + } + + private function getStringForStatus(status:String):String { + switch (status) { + case ChangeStatusEvent.CLEAR_STATUS: + return ResourceUtil.getInstance().getString('bbb.users.status.clearStatus'); + case ChangeStatusEvent.RAISE_HAND: + return ResourceUtil.getInstance().getString('bbb.users.status.raiseHand'); + case ChangeStatusEvent.AGREE: + return ResourceUtil.getInstance().getString('bbb.users.status.agree'); + case ChangeStatusEvent.DISAGREE: + return ResourceUtil.getInstance().getString('bbb.users.status.disagree'); + case ChangeStatusEvent.SPEAK_LOUDER: + return ResourceUtil.getInstance().getString('bbb.users.status.speakLouder'); + case ChangeStatusEvent.SPEAK_LOWER: + return ResourceUtil.getInstance().getString('bbb.users.status.speakSofter'); + case ChangeStatusEvent.SPEAK_FASTER: + return ResourceUtil.getInstance().getString('bbb.users.status.speakFaster'); + case ChangeStatusEvent.SPEAK_SLOWER: + return ResourceUtil.getInstance().getString('bbb.users.status.speakSlower'); + case ChangeStatusEvent.BE_RIGHT_BACK: + return ResourceUtil.getInstance().getString('bbb.users.status.beRightBack'); + case ChangeStatusEvent.LAUGHTER: + return ResourceUtil.getInstance().getString('bbb.users.status.laughter'); + case ChangeStatusEvent.SAD: + return ResourceUtil.getInstance().getString('bbb.users.status.sad'); + default: + return ""; + } + } + + private function openMoodMenu():void { + var myMenuData:Array = []; + if(partOptions.enableRaiseHand) { + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.CLEAR_STATUS), icon: images.mood_clear, handler: function():void { changeStatus(ChangeStatusEvent.CLEAR_STATUS); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.RAISE_HAND), icon: images.mood_raise_hand, handler: function():void { changeStatus(ChangeStatusEvent.RAISE_HAND); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.AGREE), icon: images.mood_agreed, handler: function():void { changeStatus(ChangeStatusEvent.AGREE); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.DISAGREE), icon: images.mood_disagreed, handler: function():void { changeStatus(ChangeStatusEvent.DISAGREE); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.SPEAK_LOUDER), icon: images.mood_speak_louder, handler: function():void { changeStatus(ChangeStatusEvent.SPEAK_LOUDER); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.SPEAK_LOWER), icon: images.mood_speak_softer, handler: function():void { changeStatus(ChangeStatusEvent.SPEAK_LOWER); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.SPEAK_FASTER), icon: images.mood_speak_faster, handler: function():void { changeStatus(ChangeStatusEvent.SPEAK_FASTER); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.SPEAK_SLOWER), icon: images.mood_speak_slower, handler: function():void { changeStatus(ChangeStatusEvent.SPEAK_SLOWER); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.BE_RIGHT_BACK), icon: images.mood_be_right_back, handler: function():void { changeStatus(ChangeStatusEvent.BE_RIGHT_BACK); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.LAUGHTER), icon: images.mood_happy, handler: function():void { changeStatus(ChangeStatusEvent.LAUGHTER); }} ); + myMenuData.push( {label: getStringForStatus(ChangeStatusEvent.SAD), icon: images.mood_sad, handler: function():void { changeStatus(ChangeStatusEvent.SAD); }} ); + } + + // make sure the previous menu is closed before opening a new one + // This could be improved to include a flag that tells if the menu is open, + // but it would require an extra listener for the MenuCloseEvent. + if (myMenu) myMenu.hide(); + + myMenu = Menu.createMenu(null, myMenuData, true); + + if (moodMenuHeight == -1) { + myMenuData.length * myMenu.rowHeight; + } + + var raiseHandBtnPos:Point = raiseHandBtn.localToGlobal(new Point(0,0)); + + var myMenuPos:Point = new Point(); + myMenuPos.x = raiseHandBtnPos.x + 1; + myMenuPos.y = raiseHandBtnPos.y + raiseHandBtn.height + 1; + if (myMenuPos.y + moodMenuHeight > stage.stageHeight) { + myMenuPos.x += raiseHandBtn.width; + myMenuPos.y = stage.stageHeight - moodMenuHeight; + } + + myMenu.addEventListener(MenuEvent.ITEM_CLICK, menuClickHandler); + myMenu.addEventListener(MenuEvent.MENU_SHOW, moodMenuShowHandler); + myMenu.show(myMenuPos.x, myMenuPos.y); + } - private function openSettings():void { - myMenuData = []; - myMenuData.push({label: ResourceUtil.getInstance().getString('bbb.users.settings.lowerAllHands'), icon: images.hand_new, handler:lowerHands}); + private function moodMenuShowHandler(e:MenuEvent):void { + var menu:Menu = e.menu as Menu; + + // TODO it doesn't work because menu.y is always 0 + if (menu.y + menu.height > stage.stageHeight) { + var myMenuPos:Point = new Point(); + myMenuPos.x = menu.x + raiseHandBtn.width; + myMenuPos.y = stage.stageHeight - menu.height; + menu.show(myMenuPos.x, myMenuPos.y); + } + + moodMenuHeight = menu.height; + myMenu.setFocus(); + } + + private function openPresenterSettings():void { + var myMenuData:Array = []; + myMenuData.push({label: ResourceUtil.getInstance().getString('bbb.users.settings.clearAllStatus'), icon: images.mood_clear, handler:clearAllStatus}); if (!roomMuted) { myMenuData.push({label: ResourceUtil.getInstance().getString('bbb.users.settings.muteAll'), icon: images.audio_muted, handler: muteAll}); @@ -259,16 +371,14 @@ myMenu.variableRowHeight = true; myMenu.show(this.x + settingsBtn.x + settingsBtn.width + 2, this.y + this.height); myMenu.addEventListener(MenuEvent.ITEM_CLICK, menuClickHandler); - myMenu.addEventListener(MenuEvent.MENU_SHOW, menuShowHandler); + myMenu.addEventListener(MenuEvent.MENU_SHOW, presenterMenuShowHandler); } private function menuClickHandler(e:MenuEvent):void { - if(myMenuData[e.index] != undefined && myMenuData[e.index].handler != undefined) { - myMenuData[e.index].handler(); - } + e.item.handler(); } - private function menuShowHandler(e:MenuEvent):void { + private function presenterMenuShowHandler(e:MenuEvent):void { myMenu.setFocus(); } @@ -277,6 +387,11 @@ roomLocked = lockSettings.isAnythingLocked() && ( lockSettings.getLockOnJoin() || UsersUtil.isAnyoneLocked()); } + private function changeStatus(status:String):void { + var e:ChangeStatusEvent = new ChangeStatusEvent(UserManager.getInstance().getConference().getMyUserId(), status); + dispatchEvent(e); + } + private function lockSettings():void { LogUtil.traceObject("Action: lockSettings"); @@ -284,47 +399,6 @@ dispatcher.dispatchEvent(event); } - /*private function unlockAll():void { - LogUtil.traceObject("Action: unlockAll"); - if (amIModerator) { - if (roomLocked) { - var unlockCommand:LockControlEvent = new LockControlEvent(LockControlEvent.UNLOCK_ALL); - dispatchEvent(unlockCommand); - roomLocked = false; - } else { - LogUtil.error("Action: unlockAll, but room is not locked"); - } - } - } - - private function lockAll():void { - LogUtil.traceObject("Action: lockAll"); - if (amIModerator) { - if (!roomLocked) { - var lockCommand:LockControlEvent = new LockControlEvent(LockControlEvent.LOCK_ALL); - dispatchEvent(lockCommand); - roomLocked = true; - } else { - LogUtil.error("Action: lockAll, but room is already locked"); - } - } - } - - private function lockAlmostAll():void { - LogUtil.traceObject("Action: lockAlmostAll"); - - if (amIModerator) { - if (!roomLocked) { - var lockCommand:LockControlEvent = new LockControlEvent(LockControlEvent.LOCK_ALMOST_ALL); - dispatchEvent(lockCommand); - roomLocked = true; - } else { - LogUtil.error("Action: lockAlmostAll, but room is already locked"); - } - } - } - */ - private function handleMeetingMuted(e:MeetingMutedEvent):void { roomMuted = MeetingModel.getInstance().meetingMuted; } @@ -353,13 +427,6 @@ } } - private function lowerHands():void { - for (var i:int = 0; i < users.length; i++) { - var p:BBBUser = users.getItemAt(i) as BBBUser; - if (p.raiseHand) dispatchEvent(new LowerHandEvent(p.userID)); - } - } - override protected function resourcesChanged():void{ super.resourcesChanged(); if (users.length > 8) @@ -379,13 +446,7 @@ maximizeRestoreBtn.accessibilityName = ResourceUtil.getInstance().getString("bbb.users.maximizeRestoreBtn.accessibilityName"); } - if (raiseHandBtn) { - if (UserManager.getInstance().getConference().isMyHandRaised) { - raiseHandBtn.accessibilityName = ResourceUtil.getInstance().getString('bbb.users.raiseHandBtn.toolTip2'); - } else { - raiseHandBtn.accessibilityName = ResourceUtil.getInstance().getString('bbb.users.raiseHandBtn.toolTip'); - } - } + raiseHandBtn.accessibilityName = getStringForStatus(UserManager.getInstance().getConference().getMyMood()); addContextMenuItems(); } @@ -499,6 +560,48 @@ private function remoteMuteAllButPres(e:ShortcutEvent):void{ muteAlmostAll(); } + + public function onStatusChanged(e:ChangeStatusBtnEvent):void{ + var icon:String = e.getStatusName(); + + if (e.userId == UserManager.getInstance().getConference().getMyUserId()) { + switch(icon) { + case "RAISE_HAND": + raiseHandBtn.setStyle("icon",images.mood_raise_hand); + break; + case "AGREE": + raiseHandBtn.setStyle("icon",images.mood_agreed); + break; + case "DISAGREE": + raiseHandBtn.setStyle("icon",images.mood_disagreed); + break; + case "SPEAK_LOUDER": + raiseHandBtn.setStyle("icon",images.mood_speak_louder); + break; + case "SPEAK_LOWER": + raiseHandBtn.setStyle("icon",images.mood_speak_softer); + break; + case "SPEAK_FASTER": + raiseHandBtn.setStyle("icon",images.mood_speak_faster); + break; + case "SPEAK_SLOWER": + raiseHandBtn.setStyle("icon",images.mood_speak_slower); + break; + case "BE_RIGHT_BACK": + raiseHandBtn.setStyle("icon",images.mood_be_right_back); + break; + case "LAUGHTER": + raiseHandBtn.setStyle("icon",images.mood_happy); + break; + case "SAD": + raiseHandBtn.setStyle("icon",images.mood_sad); + break; + default: + raiseHandBtn.setStyle("icon",images.mood); + break; + } + } + } ]]> </mx:Script> @@ -520,11 +623,11 @@ </mx:DataGrid> <mx:ControlBar width="100%"> - <mx:Button id="raiseHandBtn" toggle="true" icon="{images.hand_new}" selected="{UserManager.getInstance().getConference().isMyHandRaised}" - width="30" height="30" toolTip="{ResourceUtil.getInstance().getString('bbb.users.raiseHandBtn.toolTip')}" click="raiseHand()" + <mx:Button id="raiseHandBtn" icon="{images.mood}" selected="{UserManager.getInstance().getConference().isMyHandRaised}" + width="30" height="30" toolTip="{ResourceUtil.getInstance().getString('bbb.users.raiseHandBtn.toolTip')}" click="openMoodMenu()" visible="true" tabIndex="{partOptions.baseTabIndex+10}" /> <mx:Button id="settingsBtn" icon="{images.users_settings}" width="30" height="30" - toolTip="{ResourceUtil.getInstance().getString('bbb.users.settings.buttonTooltip')}" click="openSettings()" visible="true" tabIndex="{partOptions.baseTabIndex+15}" /> + toolTip="{ResourceUtil.getInstance().getString('bbb.users.settings.buttonTooltip')}" click="openPresenterSettings()" visible="true" tabIndex="{partOptions.baseTabIndex+15}" /> <mx:VBox> <mx:Label text="{ResourceUtil.getInstance().getString('bbb.users.roomMuted.text')}" visible="{roomMuted}" includeInLayout="{roomMuted}" /> <mx:Label text="{ResourceUtil.getInstance().getString('bbb.users.roomLocked.text')}" visible="{roomLocked}" includeInLayout="{roomLocked}" /> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/business/VideoProxy.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/business/VideoProxy.as index 03efbe148423139dda4ce383656963911f92fd5a..7033929c211aa57b2ef974cabac35925d1d3f1f5 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/business/VideoProxy.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/business/VideoProxy.as @@ -1,199 +1,382 @@ -/** -* 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.modules.videoconf.business -{ - import com.asfusion.mate.events.Dispatcher; - - import flash.events.AsyncErrorEvent; - import flash.events.IOErrorEvent; - import flash.events.NetStatusEvent; - import flash.events.SecurityErrorEvent; - import flash.media.H264Level; - import flash.media.H264Profile; - import flash.media.H264VideoStreamSettings; - import flash.net.NetConnection; - import flash.net.NetStream; - import flash.system.Capabilities; - - import mx.collections.ArrayCollection; - - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.core.BBB; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.main.model.users.BBBUser; - import org.bigbluebutton.main.model.users.events.StreamStartedEvent; - import org.bigbluebutton.modules.videoconf.events.ConnectedEvent; - import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; - - - public class VideoProxy - { - public var videoOptions:VideoConfOptions; - - private var nc:NetConnection; - private var ns:NetStream; - private var _url:String; - - private function parseOptions():void { - videoOptions = new VideoConfOptions(); - videoOptions.parseOptions(); - } - - public function VideoProxy(url:String) - { - _url = url; - parseOptions(); - nc = new NetConnection(); +/** +* 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.modules.videoconf.business +{ + import com.asfusion.mate.events.Dispatcher; + + import flash.events.AsyncErrorEvent; + import flash.events.IOErrorEvent; + import flash.events.NetStatusEvent; + import flash.events.SecurityErrorEvent; + import flash.media.H264Level; + import flash.media.H264Profile; + import flash.media.H264VideoStreamSettings; + import flash.net.NetConnection; + import flash.net.NetStream; + import flash.system.Capabilities; + import flash.utils.Dictionary; + + import mx.collections.ArrayCollection; + + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.managers.UserManager; + import org.bigbluebutton.modules.videoconf.events.ConnectedEvent; + import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; + import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; + import org.bigbluebutton.modules.videoconf.events.PlayConnectionReady; + import org.bigbluebutton.modules.videoconf.services.messaging.MessageSender; + import org.bigbluebutton.modules.videoconf.services.messaging.MessageReceiver; + import org.bigbluebutton.modules.videoconf.events.PlayConnectionClosedEvent; + + + public class VideoProxy + { + public var videoOptions:VideoConfOptions; + + // NetConnection used for stream publishing + private var nc:NetConnection; + // NetStream used for stream publishing + private var ns:NetStream; + private var _url:String; + private var camerasPublishing:Object = new Object(); + private var connected:Boolean = false; + + // Message sender to request stream path + private var msgSender:MessageSender; + // Message receiver to receive the stream path + private var msgReceiver:MessageReceiver; + + // Dictionary<url,NetConnection> used for stream playing + private var playConnectionDict:Dictionary; + // Dictionary<url,Array<streamName>> used to keep track of streams using a URL + private var urlStreamsDict:Dictionary; + // Dictionary<streamName,streamNamePrefix> used for stream playing + private var streamNamePrefixDict:Dictionary; + // Dictionary<streamName,url> + private var streamUrlDict:Dictionary; + + private function parseOptions():void { + videoOptions = new VideoConfOptions(); + videoOptions.parseOptions(); + } + + public function VideoProxy(url:String) + { + _url = url; + parseOptions(); + nc = new NetConnection(); nc.proxyType = "best"; - nc.client = this; - nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); - nc.addEventListener(IOErrorEvent.IO_ERROR, onIOError); - nc.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); - nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); - - } - - public function connect():void { - nc.connect(_url, UsersUtil.getInternalMeetingID(), UsersUtil.getMyUserID()); - } - - private function onAsyncError(event:AsyncErrorEvent):void{ - } - - private function onIOError(event:NetStatusEvent):void{ - } - - private function onConnectedToVideoApp():void{ - var dispatcher:Dispatcher = new Dispatcher(); - dispatcher.dispatchEvent(new ConnectedEvent(ConnectedEvent.VIDEO_CONNECTED)); - } - - private function onNetStatus(event:NetStatusEvent):void{ - switch(event.info.code){ - case "NetConnection.Connect.Success": - ns = new NetStream(nc); - onConnectedToVideoApp(); - break; - default: - LogUtil.debug("[" + event.info.code + "] for [" + _url + "]"); - break; - } - } - - private function onSecurityError(event:NetStatusEvent):void{ - } - - public function get connection():NetConnection{ - return this.nc; - } - - public function startPublishing(e:StartBroadcastEvent):void{ - ns.addEventListener( NetStatusEvent.NET_STATUS, onNetStatus ); - ns.addEventListener( IOErrorEvent.IO_ERROR, onIOError ); - ns.addEventListener( AsyncErrorEvent.ASYNC_ERROR, onAsyncError ); - ns.client = this; - ns.attachCamera(e.camera); -// Uncomment if you want to build support for H264. But you need at least FP 11. (ralam july 23, 2011) -// if (Capabilities.version.search("11,0") != -1) { - if ((BBB.getFlashPlayerVersion() >= 11) && videoOptions.enableH264) { -// if (BBB.getFlashPlayerVersion() >= 11) { - LogUtil.info("Using H264 codec for video."); - var h264:H264VideoStreamSettings = new H264VideoStreamSettings(); - var h264profile:String = H264Profile.MAIN; - if (videoOptions.h264Profile != "main") { - h264profile = H264Profile.BASELINE; - } - var h264Level:String = H264Level.LEVEL_4_1; - if (videoOptions.h264Level == "1") { - h264Level = H264Level.LEVEL_1; - } else if (videoOptions.h264Level == "1.1") { - h264Level = H264Level.LEVEL_1_1; - } else if (videoOptions.h264Level == "1.2") { - h264Level = H264Level.LEVEL_1_2; - } else if (videoOptions.h264Level == "1.3") { - h264Level = H264Level.LEVEL_1_3; - } else if (videoOptions.h264Level == "1b") { - h264Level = H264Level.LEVEL_1B; - } else if (videoOptions.h264Level == "2") { - h264Level = H264Level.LEVEL_2; - } else if (videoOptions.h264Level == "2.1") { - h264Level = H264Level.LEVEL_2_1; - } else if (videoOptions.h264Level == "2.2") { - h264Level = H264Level.LEVEL_2_2; - } else if (videoOptions.h264Level == "3") { - h264Level = H264Level.LEVEL_3; - } else if (videoOptions.h264Level == "3.1") { - h264Level = H264Level.LEVEL_3_1; - } else if (videoOptions.h264Level == "3.2") { - h264Level = H264Level.LEVEL_3_2; - } else if (videoOptions.h264Level == "4") { - h264Level = H264Level.LEVEL_4; - } else if (videoOptions.h264Level == "4.1") { - h264Level = H264Level.LEVEL_4_1; - } else if (videoOptions.h264Level == "4.2") { - h264Level = H264Level.LEVEL_4_2; - } else if (videoOptions.h264Level == "5") { - h264Level = H264Level.LEVEL_5; - } else if (videoOptions.h264Level == "5.1") { - h264Level = H264Level.LEVEL_5_1; - } - - LogUtil.info("Codec used: " + h264Level); - - h264.setProfileLevel(h264profile, h264Level); - ns.videoStreamSettings = h264; - } - - ns.publish(e.stream); - } - - public function stopBroadcasting():void{ - trace("Closing netstream for webcam publishing"); - - if (ns != null) { - ns.attachCamera(null); - ns.close(); - ns = null; - ns = new NetStream(nc); - } - } - - public function disconnect():void { - trace("VideoProxy:: disconnecting from Video application"); - stopBroadcasting(); - if (nc != null) nc.close(); - } - - public function onBWCheck(... rest):Number { - return 0; - } - - public function onBWDone(... rest):void { - var p_bw:Number; - if (rest.length > 0) p_bw = rest[0]; - // your application should do something here - // when the bandwidth check is complete - trace("bandwidth = " + p_bw + " Kbps."); - } - - - } -} + nc.client = this; + nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); + nc.addEventListener(IOErrorEvent.IO_ERROR, onIOError); + nc.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); + nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); + playConnectionDict = new Dictionary(); + urlStreamsDict = new Dictionary(); + streamNamePrefixDict = new Dictionary(); + streamUrlDict = new Dictionary(); + msgReceiver = new MessageReceiver(this); + msgSender = new MessageSender(); + } + + public function connect():void { + nc.connect(_url, UsersUtil.getInternalMeetingID(), UsersUtil.getMyUserID()); + playConnectionDict[_url] = nc; + urlStreamsDict[_url] = new Array(); + } + + private function onAsyncError(event:AsyncErrorEvent):void{ + } + + private function onIOError(event:NetStatusEvent):void{ + } + + private function onConnectedToVideoApp():void{ + var dispatcher:Dispatcher = new Dispatcher(); + dispatcher.dispatchEvent(new ConnectedEvent(ConnectedEvent.VIDEO_CONNECTED)); + } + + private function onNetStatus(event:NetStatusEvent):void{ + switch(event.info.code){ + case "NetConnection.Connect.Success": + connected = true; + //ns = new NetStream(nc); + onConnectedToVideoApp(); + break; + default: + LogUtil.debug("[" + event.info.code + "] for [" + _url + "]"); + connected = false; + break; + } + } + + private function onSecurityError(event:NetStatusEvent):void{ + } + + public function get publishConnection():NetConnection{ + return this.nc; + } + + private function onPlayNetStatus(event:NetStatusEvent):void { + var url:String = event.target.uri; + var streams:Array = urlStreamsDict[url]; + var dispatcher:Dispatcher = new Dispatcher(); + var prefix:String; + var stream:String; + switch(event.info.code){ + case "NetConnection.Connect.Success": + // Notify streams from this connection + var conn:NetConnection = playConnectionDict[url]; + for each (stream in streams) { + prefix = streamNamePrefixDict[stream]; + dispatcher.dispatchEvent(new PlayConnectionReady(stream, conn, prefix)); + } + break; + case "NetConnection.Connect.Failed": + case "NetConnection.Connect.Closed": + trace("[" + event.info.code + "] for a play connection at [" + url + "]"); + trace("Affected streams: ["+streams+"]"); + for each (stream in streams) { + prefix = streamNamePrefixDict[stream]; + delete streamNamePrefixDict[stream]; + delete streamUrlDict[stream]; + dispatcher.dispatchEvent(new PlayConnectionClosedEvent(stream, prefix)); + } + delete playConnectionDict[url]; + delete urlStreamsDict[url]; + break; + default: + LogUtil.debug("[" + event.info.code + "] for a play connection at [" + url + "]"); + break; + } + } + + public function createPlayConnectionFor(streamName:String):void { + LogUtil.debug("VideoProxy::createPlayConnectionFor:: Requesting path for stream [" + streamName + "]"); + // Check if a connection already exists + if(!streamUrlDict[streamName]) { + trace("VideoProxy::createPlayConnectionFor:: Requesting path for stream [" + streamName + "]"); + // Ask red5 the path to stream + msgSender.getStreamPath(streamName); + } + else { + trace("VideoProxy::createPlayConnectionFor:: Found connection for stream [" + streamName + "]"); + } + } + + public function handleStreamPathReceived(streamName:String, connectionPath:String):void { + trace("VideoProxy::handleStreamPathReceived:: Path for stream [" + streamName + "]: [" + connectionPath + "]"); + + var newUrl:String; + var streamPrefix:String; + + // Check whether the is through proxy servers or not + if(connectionPath == "") { + newUrl = _url; + streamPrefix = ""; + } + else { + var ipRegex:RegExp = /([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/; + var serverIp:String = connectionPath.split("/")[0]; + newUrl = _url.replace(ipRegex, serverIp); + streamPrefix = connectionPath.replace(serverIp, ""); + } + + if(streamPrefix != "") + streamPrefix = streamPrefix + "/"; + + // Store URL for this stream + streamUrlDict[streamName] = newUrl; + + // Set current streamPrefix to use the current path + streamNamePrefixDict[streamName] = streamPrefix; + + if(urlStreamsDict[newUrl] == null) { + urlStreamsDict[newUrl] = new Array(); + urlStreamsDict[streamPrefix+streamName] = urlStreamsDict[newUrl]; + } + urlStreamsDict[newUrl].push(streamName); + + // If connection with this URL does not exist + if(!playConnectionDict[newUrl]){ + // Create new NetConnection and store it + var connection:NetConnection = new NetConnection(); + connection.client = this; + connection.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); + connection.addEventListener(IOErrorEvent.IO_ERROR, onIOError); + connection.addEventListener(NetStatusEvent.NET_STATUS, onPlayNetStatus); + connection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); + connection.connect(newUrl, UsersUtil.getInternalMeetingID(), UsersUtil.getMyUserID()); + trace("VideoProxy::handleStreamPathReceived:: Creating NetConnection for [" + newUrl + "]"); + playConnectionDict[newUrl] = connection; + } + else { + if(playConnectionDict[newUrl].connected) { + // Connection is ready, send event + var dispatcher:Dispatcher = new Dispatcher(); + dispatcher.dispatchEvent(new PlayConnectionReady(streamName, playConnectionDict[newUrl], streamPrefix)); + } + trace("VideoProxy::handleStreamPathReceived:: Found NetConnection for [" + newUrl + "]"); + } + } + + public function getConnectionForStream(stream:String):NetConnection { + var url:String = streamUrlDict[stream]; + return playConnectionDict[url]; + } + + public function getPrefixForStream(stream:String):String { + if(streamNamePrefixDict[stream]) + return streamNamePrefixDict[stream]; + else + return ""; + } + + public function closePlayConnectionFor(streamName:String):void { + var temp:Array = streamName.split("/"); + var stream:String = temp[temp.length-1]; + var streamUrl:String = streamUrlDict[stream]; + + // Remove the url entry for this stream + delete streamUrlDict[stream]; + + // Check if the connection should be closed + var streams:Array = urlStreamsDict[streamUrl]; + if(streams != null) { + streams = streams.filter(function(item:*, index:int, array:Array):Boolean { return item != stream }); + urlStreamsDict[streamUrl] = streams; + } + // Do not close publish connection, no matter what + if(playConnectionDict[streamUrl] == nc) + return; + if(streams == null || streams.length <= 0) { + trace("VideoProxy:: closePlayConnectionFor:: Closing connection with: [" + streamUrl + "]"); + // No one else is using this NetConnection + var connection:NetConnection = playConnectionDict[streamUrl]; + if(connection != null) connection.close(); + delete playConnectionDict[streamUrl]; + delete urlStreamsDict[streamUrl]; + } + else { + trace("VideoProxy:: closePlayConnectionFor:: Connection with: [" + streamUrl + "] has [" + streams.length + "] streams"); + } + } + + public function startPublishing(e:StartBroadcastEvent):void{ + var ns:NetStream = new NetStream(nc); + ns.addEventListener( NetStatusEvent.NET_STATUS, onNetStatus ); + ns.addEventListener( IOErrorEvent.IO_ERROR, onIOError ); + ns.addEventListener( AsyncErrorEvent.ASYNC_ERROR, onAsyncError ); + ns.client = this; + ns.attachCamera(e.camera); +// Uncomment if you want to build support for H264. But you need at least FP 11. (ralam july 23, 2011) +// if (Capabilities.version.search("11,0") != -1) { + if ((BBB.getFlashPlayerVersion() >= 11) && e.videoProfile.enableH264) { +// if (BBB.getFlashPlayerVersion() >= 11) { + LogUtil.info("Using H264 codec for video."); + var h264:H264VideoStreamSettings = new H264VideoStreamSettings(); + var h264profile:String = H264Profile.MAIN; + if (e.videoProfile.h264Profile != "main") { + h264profile = H264Profile.BASELINE; + } + var h264Level:String = H264Level.LEVEL_4_1; + switch (e.videoProfile.h264Level) { + case "1": h264Level = H264Level.LEVEL_1; break; + case "1.1": h264Level = H264Level.LEVEL_1_1; break; + case "1.2": h264Level = H264Level.LEVEL_1_2; break; + case "1.3": h264Level = H264Level.LEVEL_1_3; break; + case "1b": h264Level = H264Level.LEVEL_1B; break; + case "2": h264Level = H264Level.LEVEL_2; break; + case "2.1": h264Level = H264Level.LEVEL_2_1; break; + case "2.2": h264Level = H264Level.LEVEL_2_2; break; + case "3": h264Level = H264Level.LEVEL_3; break; + case "3.1": h264Level = H264Level.LEVEL_3_1; break; + case "3.2": h264Level = H264Level.LEVEL_3_2; break; + case "4": h264Level = H264Level.LEVEL_4; break; + case "4.1": h264Level = H264Level.LEVEL_4_1; break; + case "4.2": h264Level = H264Level.LEVEL_4_2; break; + case "5": h264Level = H264Level.LEVEL_5; break; + case "5.1": h264Level = H264Level.LEVEL_5_1; break; + } + + LogUtil.info("Codec used: " + h264Level); + + h264.setProfileLevel(h264profile, h264Level); + ns.videoStreamSettings = h264; + } + + ns.publish(e.stream); + camerasPublishing[e.stream] = ns; + } + + public function stopBroadcasting(stream:String):void{ + trace("Closing netstream for webcam publishing"); + if (camerasPublishing[stream] != null) { + var ns:NetStream = camerasPublishing[stream]; + ns.attachCamera(null); + ns.close(); + ns = null; + delete camerasPublishing[stream]; + } + } + + public function stopAllBroadcasting():void { + for each (var ns:NetStream in camerasPublishing) + { + ns.attachCamera(null); + ns.close(); + ns = null; + } + camerasPublishing = new Object(); + } + + public function disconnect():void { + trace("VideoProxy:: disconnecting from Video application"); + stopAllBroadcasting(); + // Close publish NetConnection + if (nc != null) nc.close(); + // Close play NetConnections + for (var k:Object in playConnectionDict) { + var connection:NetConnection = playConnectionDict[k]; + connection.close(); + } + // Reset dictionaries + playConnectionDict = new Dictionary(); + streamNamePrefixDict = new Dictionary(); + urlStreamsDict = new Dictionary(); + streamUrlDict = new Dictionary(); + } + + public function onBWCheck(... rest):Number { + return 0; + } + + public function onBWDone(... rest):void { + var p_bw:Number; + if (rest.length > 0) p_bw = rest[0]; + // your application should do something here + // when the bandwidth check is complete + trace("bandwidth = " + p_bw + " Kbps."); + } + + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/business/VideoWindowItf.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/business/VideoWindowItf.as deleted file mode 100755 index af6fad2e08d343e4cc2fc5037a7d0859db44ff43..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/business/VideoWindowItf.as +++ /dev/null @@ -1,335 +0,0 @@ -/** -* 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.modules.videoconf.business -{ - import com.asfusion.mate.events.Dispatcher; - - import flash.events.MouseEvent; - import flash.geom.Point; - import flash.media.Video; - - import flexlib.mdi.events.MDIWindowEvent; - - import mx.containers.Panel; - import mx.controls.Button; - import mx.core.UIComponent; - import mx.events.FlexEvent; - - import org.bigbluebutton.common.CustomMdiWindow; - import org.bigbluebutton.common.IBbbModuleWindow; - import org.bigbluebutton.common.Images; - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.common.events.CloseWindowEvent; - import org.bigbluebutton.common.events.DragWindowEvent; - import org.bigbluebutton.core.EventConstants; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.events.CoreEvent; - import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.main.model.users.BBBUser; - import org.bigbluebutton.main.model.users.events.KickUserEvent; - import org.bigbluebutton.main.model.users.events.RoleChangeEvent; - import org.bigbluebutton.main.views.MainCanvas; - import org.bigbluebutton.modules.videoconf.events.UserTalkingEvent; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; - import org.bigbluebutton.modules.videoconf.views.ControlButtons; - import org.bigbluebutton.util.i18n.ResourceUtil; - - public class VideoWindowItf extends CustomMdiWindow implements IBbbModuleWindow - { - protected var _video:Video; - protected var _videoHolder:UIComponent; - // images must be static because it needs to be created *before* the PublishWindow creation - static protected var images:Images = new Images(); - - static public var PADDING_HORIZONTAL:Number = 6; - static public var PADDING_VERTICAL:Number = 29; - protected var _minWidth:int = 160 + PADDING_HORIZONTAL; - protected var _minHeight:int = 120 + PADDING_VERTICAL; - protected var aspectRatio:Number = 1; - protected var keepAspect:Boolean = false; - - protected var mousePositionOnDragStart:Point; - - public var streamName:String; - - private var windowType:String = "VideoWindowItf"; - - public var userID:String = null; - - protected var _controlButtons:ControlButtons = new ControlButtons(); - - [Bindable] public var resolutions:Array; - - protected var videoConfOptions:VideoConfOptions = new VideoConfOptions(); - - public function VideoWindowItf() { - super(); - - this.addEventListener(FlexEvent.CREATION_COMPLETE, onCreationComplete); - } - - private function onCreationComplete(event:FlexEvent):void { - tabFocusEnabled = false; - accessibilityEnabled = false; - titleBarOverlay.accessibilityEnabled = false; - titleBarOverlay.tabFocusEnabled = false; - closeBtn.accessibilityEnabled = false; - closeBtn.tabFocusEnabled = false; - } - - public function getWindowType():String { - return windowType; - } - - protected function updateControlButtons():void { - _controlButtons.updateControlButtons(); - } - - protected function getVideoResolution(stream:String):Array { - var pattern:RegExp = new RegExp("(\\d+x\\d+)-([A-Za-z0-9]+_\\d+)-\\d+", ""); - if (pattern.test(stream)) { - LogUtil.debug("The stream name is well formatted [" + stream + "]"); - var uid:String = UserManager.getInstance().getConference().getMyUserId(); - LogUtil.debug("Stream resolution is [" + pattern.exec(stream)[1] + "]"); - LogUtil.debug("Userid [" + pattern.exec(stream)[2] + "]"); - userID = pattern.exec(stream)[2]; - addControlButtons(); - return pattern.exec(stream)[1].split("x"); - } else { - LogUtil.error("The stream name doesn't follow the pattern <width>x<height>-<userId>-<timestamp>. However, the video resolution will be set to the lowest defined resolution in the config.xml: " + resolutions[0]); - return resolutions[0].split("x"); - } - } - - protected function get paddingVertical():Number { - return this.borderMetrics.top + this.borderMetrics.bottom; - } - - protected function get paddingHorizontal():Number { - return this.borderMetrics.left + this.borderMetrics.right; - } - - static private var RESIZING_DIRECTION_UNKNOWN:int = 0; - static private var RESIZING_DIRECTION_VERTICAL:int = 1; - static private var RESIZING_DIRECTION_HORIZONTAL:int = 2; - static private var RESIZING_DIRECTION_BOTH:int = 3; - private var resizeDirection:int = RESIZING_DIRECTION_BOTH; - - /** - * when the window is resized by the user, the application doesn't know - * about the resize direction - */ - public function onResizeStart(event:MDIWindowEvent = null):void { - resizeDirection = RESIZING_DIRECTION_UNKNOWN; - } - - /** - * after the resize ends, the direction is set to BOTH because of the - * non-user resize actions - like when the window is docked, and so on - */ - public function onResizeEnd(event:MDIWindowEvent = null):void { - resizeDirection = RESIZING_DIRECTION_BOTH; - } - - protected function onResize():void { - if (_video == null || _videoHolder == null || this.minimized) return; - - // limits the window size to the parent size - this.width = (this.parent != null? Math.min(this.width, this.parent.width): this.width); - this.height = (this.parent != null? Math.min(this.height, this.parent.height): this.height); - - var tmpWidth:Number = this.width - PADDING_HORIZONTAL; - var tmpHeight:Number = this.height - PADDING_VERTICAL; - - // try to discover in which direction the user is resizing the window - if (resizeDirection != RESIZING_DIRECTION_BOTH) { - if (tmpWidth == _video.width && tmpHeight != _video.height) - resizeDirection = (resizeDirection == RESIZING_DIRECTION_VERTICAL || resizeDirection == RESIZING_DIRECTION_UNKNOWN? RESIZING_DIRECTION_VERTICAL: RESIZING_DIRECTION_BOTH); - else if (tmpWidth != _video.width && tmpHeight == _video.height) - resizeDirection = (resizeDirection == RESIZING_DIRECTION_HORIZONTAL || resizeDirection == RESIZING_DIRECTION_UNKNOWN? RESIZING_DIRECTION_HORIZONTAL: RESIZING_DIRECTION_BOTH); - else - resizeDirection = RESIZING_DIRECTION_BOTH; - } - - // depending on the direction, the tmp size is different - switch (resizeDirection) { - case RESIZING_DIRECTION_VERTICAL: - tmpWidth = Math.floor(tmpHeight * aspectRatio); - break; - case RESIZING_DIRECTION_HORIZONTAL: - tmpHeight = Math.floor(tmpWidth / aspectRatio); - break; - case RESIZING_DIRECTION_BOTH: - // this direction is used also for non-user window resize actions - tmpWidth = Math.min (tmpWidth, Math.floor(tmpHeight * aspectRatio)); - tmpHeight = Math.min (tmpHeight, Math.floor(tmpWidth / aspectRatio)); - break; - } - - _video.width = _videoHolder.width = tmpWidth; - _video.height = _videoHolder.height = tmpHeight; - - if (!keepAspect || this.maximized) { - // center the video in the window - _video.x = Math.floor ((this.width - PADDING_HORIZONTAL - tmpWidth) / 2); - _video.y = Math.floor ((this.height - PADDING_VERTICAL - tmpHeight) / 2); - } else { - // fit window dimensions on video - _video.x = 0; - _video.y = 0; - this.width = tmpWidth + PADDING_HORIZONTAL; - this.height = tmpHeight + PADDING_VERTICAL; - } - - // reposition the window to fit inside the parent window - if (this.parent != null) { - if (this.x + this.width > this.parent.width) - this.x = this.parent.width - this.width; - if (this.x < 0) - this.x = 0; - if (this.y + this.height > this.parent.height) - this.y = this.parent.height - this.height; - if (this.y < 0) - this.y = 0; - } - - updateButtonsPosition(); - } - - public function updateWidth():void { - this.width = Math.floor((this.height - paddingVertical) * aspectRatio) + paddingHorizontal; - onResize(); - } - - public function updateHeight():void { - this.height = Math.floor((this.width - paddingHorizontal) / aspectRatio) + paddingVertical; - onResize(); - } - - protected function setAspectRatio(width:int,height:int):void { - aspectRatio = (width/height); - this.minHeight = Math.floor((this.minWidth - PADDING_HORIZONTAL) / aspectRatio) + PADDING_VERTICAL; - } - - public function getPrefferedPosition():String{ - if (_controlButtonsEnabled) - return MainCanvas.POPUP; - else - // the window is docked, so it should not be moved on reset layout - return MainCanvas.ABSOLUTE; - } - - override public function close(event:MouseEvent = null):void{ - trace("VideoWIndowItf close window event"); - - var e:CloseWindowEvent = new CloseWindowEvent(); - e.window = this; - dispatchEvent(e); - - super.close(event); - } - - private var _controlButtonsEnabled:Boolean = true; - - private var img_unlock_keep_aspect:Class = images.lock_open; - private var img_lock_keep_aspect:Class = images.lock_close; - private var img_fit_video:Class = images.arrow_in; - private var img_original_size:Class = images.shape_handles; - private var img_mute_icon:Class = images.webcam_mute; - private var signOutIcon:Class = images.webcam_kickuser; - private var adminIcon:Class = images.webcam_make_presenter; - private var chatIcon:Class = images.webcam_private_chat; - - protected function addControlButtons():void { - _controlButtons.sharerUserID = userID; - _controlButtons.visible = true; - this.addChild(_controlButtons); - } - - protected function get controlButtons():ControlButtons { - if (_controlButtons == null) { - _controlButtons.visible = false; - } - return _controlButtons; - } - - protected function createButtons():void { - updateButtonsPosition(); - } - - protected function updateButtonsPosition():void { - if (this.width < controlButtons.width) { - controlButtons.visible = false; - } - - if (controlButtons.visible == false) { - controlButtons.y = controlButtons.x = 0; - } else { - controlButtons.y = this.height - PADDING_VERTICAL - controlButtons.height - controlButtons.padding; - controlButtons.x = this.width - PADDING_HORIZONTAL - controlButtons.width - controlButtons.padding; - } - } - - protected function showButtons(event:MouseEvent = null):void { - if (_controlButtonsEnabled && controlButtons.visible == false && this.width > controlButtons.width) { - controlButtons.visible = true; - updateButtonsPosition(); - } - } - - protected function hideButtons(event:MouseEvent = null):void { - if (_controlButtonsEnabled && controlButtons.visible == true) { - controlButtons.visible = false; - updateButtonsPosition(); - } - } - - protected function onDoubleClick(event:MouseEvent = null):void { - // it occurs when the window is docked, for example - if (!this.maximizeRestoreBtn.visible) return; - - this.maximizeRestore(); - } - - override public function maximizeRestore(event:MouseEvent = null):void { - // if the user is maximizing the window, the control buttons should disappear - buttonsEnabled = this.maximized; - super.maximizeRestore(event); - } - - public function set buttonsEnabled(enabled:Boolean):void { - if (!enabled) - hideButtons(); - _controlButtonsEnabled = enabled; - } - - - protected function userMuted(muted:Boolean):void { - _controlButtons.userMuted(muted); - } - - protected function simulateClick():void { - if (videoConfOptions.focusTalking) { - var talkingEvent:UserTalkingEvent = new UserTalkingEvent(UserTalkingEvent.TALKING); - dispatchEvent(talkingEvent); - } - } - } -} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/CloseAllPublishWindowEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/CloseAllPublishWindowEvent.as new file mode 100755 index 0000000000000000000000000000000000000000..01a7ce9ce0356f91150c468aad3d5e7bf61c5886 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/CloseAllPublishWindowEvent.as @@ -0,0 +1,33 @@ +/** +* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ +* +* Copyright (c) 2010 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/>. +* +*/ +package org.bigbluebutton.modules.videoconf.events +{ + import flash.events.Event; + + public class CloseAllPublishWindowEvent extends Event + { + public static const CLOSE_ALL_PUBLISH_WINDOW:String = "CLOSE_ALL_PUBLISH_WINDOW"; + + public function CloseAllPublishWindowEvent(type:String = CLOSE_ALL_PUBLISH_WINDOW) + { + super(type, true, false); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/PlayConnectionClosedEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/PlayConnectionClosedEvent.as new file mode 100644 index 0000000000000000000000000000000000000000..1ee96f9da7d6ddb22e2476c78370326b5b8ae8e0 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/PlayConnectionClosedEvent.as @@ -0,0 +1,44 @@ +/** + * 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.modules.videoconf.events +{ + import flash.events.Event; + import flash.net.NetConnection; + + public class PlayConnectionClosedEvent extends Event + { + public static const PLAY_CONNECTION_CLOSED_EVENT:String = "a netconnetion was closed"; + + private var _streamName:String; + public function get streamName():String { return _streamName; } + + private var _prefix:String; + public function get prefix():String { return _prefix; } + + public function PlayConnectionClosedEvent(streamName:String, prefix:String) + { + _streamName = streamName; + _prefix = prefix; + var type:String = PLAY_CONNECTION_CLOSED_EVENT; + var bubbles:Boolean = true; + var cancelable:Boolean = false; + super(type, bubbles, cancelable); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/PlayConnectionReady.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/PlayConnectionReady.as new file mode 100644 index 0000000000000000000000000000000000000000..fcd98139d006952f63620642307486236d41fe8a --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/PlayConnectionReady.as @@ -0,0 +1,48 @@ +/** + * 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.modules.videoconf.events +{ + import flash.events.Event; + import flash.net.NetConnection; + + public class PlayConnectionReady extends Event + { + public static const PLAY_CONNECTION_READY:String = "a netconnetion is ready"; + + private var _streamName:String; + public function get streamName():String { return _streamName; } + + private var _connection:NetConnection; + public function get connection():NetConnection { return _connection; } + + private var _prefix:String; + public function get prefix():String { return _prefix; } + + public function PlayConnectionReady(streamName:String, conn:NetConnection, prefix:String) + { + _streamName = streamName; + _connection = conn; + _prefix = prefix; + var type:String = PLAY_CONNECTION_READY; + var bubbles:Boolean = true; + var cancelable:Boolean = false; + super(type, bubbles, cancelable); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/ShareCameraRequestEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/ShareCameraRequestEvent.as index f9809a28c2499b133a70ca928ac53fd5b27bcfec..09bb5f184dbb12e6863e151fc948615dd296f088 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/ShareCameraRequestEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/ShareCameraRequestEvent.as @@ -25,6 +25,9 @@ package org.bigbluebutton.modules.videoconf.events public static const SHARE_CAMERA_REQUEST:String = "ShareCameraRequestEvent"; public var publishInClient:Boolean = true; + + public var defaultCamera:String = "0"; + public var camerasArray:Object = null; public function ShareCameraRequestEvent(type:String = SHARE_CAMERA_REQUEST) { @@ -32,4 +35,4 @@ package org.bigbluebutton.modules.videoconf.events } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StartBroadcastEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StartBroadcastEvent.as index 1a28e2c5bbacee80f1886f4411b550c77e9a6d2e..cffa9de8d8991b48abe7dee428237ff9fc91742d 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StartBroadcastEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StartBroadcastEvent.as @@ -21,12 +21,15 @@ package org.bigbluebutton.modules.videoconf.events import flash.events.Event; import flash.media.Camera; + import org.bigbluebutton.core.model.VideoProfile; + public class StartBroadcastEvent extends Event { public static const START_BROADCAST:String = "startBroadcastEvent"; public var stream:String; public var camera:Camera; + public var videoProfile:VideoProfile; public function StartBroadcastEvent(type:String = START_BROADCAST) { diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StopBroadcastEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StopBroadcastEvent.as index f0857b6d79ae734633b8833ccef99141da6e6607..2fd1324a8dd038eaa0c9ea4a689e0b288c7a1064 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StopBroadcastEvent.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StopBroadcastEvent.as @@ -25,6 +25,7 @@ package org.bigbluebutton.modules.videoconf.events public static const STOP_BROADCASTING:String = "STOP_BROADCASTING"; public var stream:String; + public var camId:int = -1; public function StopBroadcastEvent(type:String = STOP_BROADCASTING) { @@ -32,4 +33,4 @@ package org.bigbluebutton.modules.videoconf.events } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StopShareCameraRequestEvent.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StopShareCameraRequestEvent.as new file mode 100755 index 0000000000000000000000000000000000000000..ce5e16d71e3992f5f2974224db6f6abc4a47d21c --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/events/StopShareCameraRequestEvent.as @@ -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.modules.videoconf.events +{ + import flash.events.Event; + + public class StopShareCameraRequestEvent extends Event + { + public static const STOP_SHARE_CAMERA_REQUEST:String = "StopShareCameraRequestEvent"; + public static const STOP_SHARE_ALL_CAMERA_REQUEST:String = "StopShareAllCameraRequestEvent"; + public var camId:int; + + public function StopShareCameraRequestEvent(type:String = STOP_SHARE_CAMERA_REQUEST) + { + super(type, true, false); + } + + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMap.mxml index 1d01eb5498f1aa29907e05db3935c8d9116da6a6..3e5f8d31eac28d0666c6a0069fb4b4bd5e92bcc7 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMap.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMap.mxml @@ -1,122 +1,144 @@ -<?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/>. - ---> - -<EventMap xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="http://mate.asfusion.com/"> - <mx:Script> - <![CDATA[ - import org.bigbluebutton.common.events.ToolbarButtonEvent; - import org.bigbluebutton.core.events.ConnectAppEvent; - import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.main.events.MadePresenterEvent; - import org.bigbluebutton.main.events.StoppedViewingWebcamEvent; - import org.bigbluebutton.main.events.UserJoinedEvent; - import org.bigbluebutton.main.events.UserLeftEvent; - import org.bigbluebutton.main.model.users.events.StreamStartedEvent; - import org.bigbluebutton.modules.users.events.ViewCameraEvent; - import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; - import org.bigbluebutton.modules.videoconf.events.ConnectedEvent; - import org.bigbluebutton.modules.videoconf.events.ShareCameraRequestEvent; - import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; - import org.bigbluebutton.modules.videoconf.events.StopBroadcastEvent; - import org.bigbluebutton.modules.videoconf.events.VideoModuleStartEvent; - import org.bigbluebutton.modules.videoconf.events.VideoModuleStopEvent; - import org.bigbluebutton.modules.videoconf.events.WebRTCWebcamRequestEvent; - ]]> - </mx:Script> - - <EventHandlers type="{VideoModuleStartEvent.START}"> - <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> - <MethodInvoker generator="{VideoEventMapDelegate}" method="start" arguments="{event.uri}"/> - </EventHandlers> - - <EventHandlers type="{VideoModuleStopEvent.STOP}"> - <MethodInvoker generator="{VideoEventMapDelegate}" method="stopModule"/> - </EventHandlers> - - <EventHandlers type="{BBBEvent.CAMERA_SETTING}" > - <MethodInvoker generator="{VideoEventMapDelegate}" method="handleCameraSetting" arguments="{event}"/> - </EventHandlers> - - <EventHandlers type="{ConnectAppEvent.CONNECT_VIDEO_APP}"> - <MethodInvoker generator="{VideoEventMapDelegate}" method="connectToVideoApp" /> - </EventHandlers> - - <EventHandlers type="{ShareCameraRequestEvent.SHARE_CAMERA_REQUEST}"> - <MethodInvoker generator="{VideoEventMapDelegate}" method="handleShareCameraRequestEvent" arguments="{event}"/> - </EventHandlers> - - <EventHandlers type="{StartBroadcastEvent.START_BROADCAST}" > - <MethodInvoker generator="{VideoEventMapDelegate}" method="startPublishing" arguments="{event}" /> - </EventHandlers> - - <EventHandlers type="{StopBroadcastEvent.STOP_BROADCASTING}" > - <MethodInvoker generator="{VideoEventMapDelegate}" method="stopPublishing" arguments="{event}" /> - </EventHandlers> - - <EventHandlers type="{StreamStartedEvent.STREAM_STARTED}"> - <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> - <MethodInvoker generator="{VideoEventMapDelegate}" method="viewCamera" arguments="{[event.userID, event.stream, event.user]}" /> - </EventHandlers> - - <EventHandlers type="{ViewCameraEvent.VIEW_CAMERA_EVENT}"> - <MethodInvoker generator="{VideoEventMapDelegate}" method="viewCamera" arguments="{[event.userID, event.stream, event.viewedName]}" /> - </EventHandlers> - - <EventHandlers type="{UserJoinedEvent.JOINED}"> - <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> - <MethodInvoker generator="{VideoEventMapDelegate}" method="handleUserJoinedEvent" arguments="{event}" /> - </EventHandlers> - - <EventHandlers type="{UserLeftEvent.LEFT}"> - <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> - <MethodInvoker generator="{VideoEventMapDelegate}" method="handleUserLeftEvent" arguments="{event}" /> - </EventHandlers> - - <EventHandlers type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" > - <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> - <MethodInvoker generator="{VideoEventMapDelegate}" method="switchToPresenter" arguments="{event}"/> - </EventHandlers> - - <EventHandlers type="{MadePresenterEvent.SWITCH_TO_VIEWER_MODE}"> - <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> - <MethodInvoker generator="{VideoEventMapDelegate}" method="switchToViewer" arguments="{event}"/> - </EventHandlers> - - <EventHandlers type="{ConnectedEvent.VIDEO_CONNECTED}"> - <MethodInvoker generator="{VideoEventMapDelegate}" method="connectedToVideoApp" /> - </EventHandlers> - - <EventHandlers type="{ClosePublishWindowEvent.CLOSE_PUBLISH_WINDOW}"> - <MethodInvoker generator="{VideoEventMapDelegate}" method="handleClosePublishWindowEvent" arguments="{event}"/> - </EventHandlers> - - <EventHandlers type="{StoppedViewingWebcamEvent.STOPPED_VIEWING_WEBCAM}"> - <MethodInvoker generator="{VideoEventMapDelegate}" method="handleStoppedViewingWebcamEvent" arguments="{event}"/> - </EventHandlers> - - <EventHandlers type="{BBBEvent.CAM_SETTINGS_CLOSED}"> - <MethodInvoker generator="{VideoEventMapDelegate}" method="handleCamSettingsClosedEvent" arguments="{event}"/> - </EventHandlers> - - <!-- ~~~~~~~~~~~~~~~~~~ INJECTORS ~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> - -</EventMap> +<?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/>. + +--> + +<EventMap xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="http://mate.asfusion.com/"> + <mx:Script> + <![CDATA[ + import org.bigbluebutton.core.events.ConnectAppEvent; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.events.MadePresenterEvent; + import org.bigbluebutton.main.events.StoppedViewingWebcamEvent; + import org.bigbluebutton.main.events.UserJoinedEvent; + import org.bigbluebutton.main.events.UserLeftEvent; + import org.bigbluebutton.main.model.users.events.StreamStartedEvent; + import org.bigbluebutton.main.model.users.events.StreamStoppedEvent; + import org.bigbluebutton.modules.users.events.ViewCameraEvent; + import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; + import org.bigbluebutton.modules.videoconf.events.ConnectedEvent; + import org.bigbluebutton.modules.videoconf.events.ShareCameraRequestEvent; + import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; + import org.bigbluebutton.modules.videoconf.events.StopBroadcastEvent; + import org.bigbluebutton.modules.videoconf.events.StopShareCameraRequestEvent; + import org.bigbluebutton.modules.videoconf.events.VideoModuleStartEvent; + import org.bigbluebutton.modules.videoconf.events.VideoModuleStopEvent; + import org.bigbluebutton.modules.videoconf.events.PlayConnectionReady; + import org.bigbluebutton.modules.videoconf.events.PlayConnectionClosedEvent; + ]]> + </mx:Script> + + <EventHandlers type="{VideoModuleStartEvent.START}"> + <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> + <MethodInvoker generator="{VideoEventMapDelegate}" method="start" arguments="{event.uri}"/> + <EventAnnouncer generator="{ConnectAppEvent}" type="{ConnectAppEvent.CONNECT_VIDEO_APP}" /> + </EventHandlers> + + <EventHandlers type="{VideoModuleStopEvent.STOP}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="stopModule"/> + </EventHandlers> + + <EventHandlers type="{BBBEvent.CAMERA_SETTING}" > + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleCameraSetting" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{ConnectAppEvent.CONNECT_VIDEO_APP}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="connectToVideoApp" /> + </EventHandlers> + + <EventHandlers type="{ShareCameraRequestEvent.SHARE_CAMERA_REQUEST}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleShareCameraRequestEvent" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{StopShareCameraRequestEvent.STOP_SHARE_CAMERA_REQUEST}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleStopShareCameraRequestEvent" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{StopShareCameraRequestEvent.STOP_SHARE_ALL_CAMERA_REQUEST}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleStopAllShareCameraRequestEvent" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{StartBroadcastEvent.START_BROADCAST}" > + <MethodInvoker generator="{VideoEventMapDelegate}" method="startPublishing" arguments="{event}" /> + </EventHandlers> + + <EventHandlers type="{StopBroadcastEvent.STOP_BROADCASTING}" > + <MethodInvoker generator="{VideoEventMapDelegate}" method="stopPublishing" arguments="{event}" /> + </EventHandlers> + + <EventHandlers type="{StreamStartedEvent.STREAM_STARTED}"> + <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> + <MethodInvoker generator="{VideoEventMapDelegate}" method="viewCamera" arguments="{[event.userID, event.stream, event.user]}" /> + </EventHandlers> + + <EventHandlers type="{StreamStoppedEvent.STREAM_STOPPED}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleStreamStoppedEvent" arguments="{[event.userID, event.stream]}" /> + </EventHandlers> + + <EventHandlers type="{ViewCameraEvent.VIEW_CAMERA_EVENT}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="viewCamera" arguments="{[event.userID, event.stream, event.viewedName]}" /> + </EventHandlers> + + <EventHandlers type="{UserJoinedEvent.JOINED}"> + <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleUserJoinedEvent" arguments="{event}" /> + </EventHandlers> + + <EventHandlers type="{UserLeftEvent.LEFT}"> + <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleUserLeftEvent" arguments="{event}" /> + </EventHandlers> + + <EventHandlers type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" > + <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> + <MethodInvoker generator="{VideoEventMapDelegate}" method="switchToPresenter" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{MadePresenterEvent.SWITCH_TO_VIEWER_MODE}"> + <ObjectBuilder generator="{VideoEventMapDelegate}" cache="global" constructorArguments="{scope.dispatcher}"/> + <MethodInvoker generator="{VideoEventMapDelegate}" method="switchToViewer" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{ConnectedEvent.VIDEO_CONNECTED}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="connectedToVideoApp" /> + </EventHandlers> + + <EventHandlers type="{ClosePublishWindowEvent.CLOSE_PUBLISH_WINDOW}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleClosePublishWindowEvent" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{StoppedViewingWebcamEvent.STOPPED_VIEWING_WEBCAM}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleStoppedViewingWebcamEvent" arguments="{[event.webcamUserID, event.streamName]}"/> + </EventHandlers> + + <EventHandlers type="{BBBEvent.CAM_SETTINGS_CLOSED}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handleCamSettingsClosedEvent" arguments="{event}"/> + </EventHandlers> + + <EventHandlers type="{PlayConnectionReady.PLAY_CONNECTION_READY}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handlePlayConnectionReady" arguments="{event}" /> + </EventHandlers> + + <EventHandlers type="{PlayConnectionClosedEvent.PLAY_CONNECTION_CLOSED_EVENT}"> + <MethodInvoker generator="{VideoEventMapDelegate}" method="handlePlayConnectionClosed" arguments="{[event.streamName, event.prefix]}" /> + </EventHandlers> + <!-- ~~~~~~~~~~~~~~~~~~ INJECTORS ~~~~~~~~~~~~~~~~~~~~~~~~~~~ --> + +</EventMap> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMapDelegate.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMapDelegate.as old mode 100755 new mode 100644 index 49e5a071aaed86cb18561d22681094a710190e88..04d766d7743d511f192c9c0cce569d4b85ac3093 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMapDelegate.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/VideoEventMapDelegate.as @@ -1,500 +1,550 @@ -/** - * 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.modules.videoconf.maps -{ - import com.asfusion.mate.utils.debug.Debugger; - import com.asfusion.mate.utils.debug.DebuggerUtil; - - import flash.events.IEventDispatcher; - import flash.external.ExternalInterface; - import flash.media.Camera; - - import mx.collections.ArrayCollection; - - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.common.events.CloseWindowEvent; - import org.bigbluebutton.common.events.OpenWindowEvent; - import org.bigbluebutton.common.events.ToolbarButtonEvent; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.events.ConnectAppEvent; - import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.core.vo.CameraSettingsVO; - import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.main.events.MadePresenterEvent; - import org.bigbluebutton.main.events.StoppedViewingWebcamEvent; - import org.bigbluebutton.main.events.UserJoinedEvent; - import org.bigbluebutton.main.events.UserLeftEvent; - import org.bigbluebutton.main.model.users.BBBUser; - import org.bigbluebutton.main.model.users.events.BroadcastStartedEvent; - import org.bigbluebutton.main.model.users.events.BroadcastStoppedEvent; - import org.bigbluebutton.main.model.users.events.StreamStartedEvent; - import org.bigbluebutton.modules.videoconf.business.VideoProxy; - import org.bigbluebutton.modules.videoconf.business.VideoWindowItf; - import org.bigbluebutton.modules.videoconf.events.CloseAllWindowsEvent; - import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; - import org.bigbluebutton.modules.videoconf.events.ConnectedEvent; - import org.bigbluebutton.modules.videoconf.events.OpenVideoWindowEvent; - import org.bigbluebutton.modules.videoconf.events.ShareCameraRequestEvent; - import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; - import org.bigbluebutton.modules.videoconf.events.StopBroadcastEvent; - import org.bigbluebutton.modules.videoconf.events.WebRTCWebcamRequestEvent; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; - import org.bigbluebutton.modules.videoconf.views.AvatarWindow; - import org.bigbluebutton.modules.videoconf.views.PublishWindow; - import org.bigbluebutton.modules.videoconf.views.ToolbarButton; - import org.bigbluebutton.modules.videoconf.views.VideoWindow; - import org.flexunit.runner.manipulation.filters.IncludeAllFilter; - - public class VideoEventMapDelegate - { - static private var PERMISSION_DENIED_ERROR:String = "PermissionDeniedError"; - - private var options:VideoConfOptions = new VideoConfOptions(); - private var uri:String; - - private var webcamWindows:WindowManager = new WindowManager(); - - private var button:ToolbarButton = new ToolbarButton(); - private var proxy:VideoProxy; - private var streamName:String; - - private var _dispatcher:IEventDispatcher; - private var _ready:Boolean = false; - private var _isPublishing:Boolean = false; - private var _isPreviewWebcamOpen:Boolean = false; - private var _isWaitingActivation:Boolean = false; - private var _chromeWebcamPermissionDenied:Boolean = false; - - public function VideoEventMapDelegate(dispatcher:IEventDispatcher) - { - _dispatcher = dispatcher; - } - - private function get me():String { - return UsersUtil.getMyUsername(); - } - - public function start(uri:String):void { - trace("VideoEventMapDelegate:: [" + me + "] Video Module Started."); - this.uri = uri; - } - - public function viewCamera(userID:String, stream:String, name:String, mock:Boolean = false):void { - trace("VideoEventMapDelegate:: [" + me + "] viewCamera. ready = [" + _ready + "]"); - - if (!_ready) return; - trace("VideoEventMapDelegate:: [" + me + "] Viewing [" + userID + " stream [" + stream + "]"); - if (! UserManager.getInstance().getConference().amIThisUser(userID)) { - openViewWindowFor(userID); - } - } - - public function handleUserLeftEvent(event:UserLeftEvent):void { - trace("VideoEventMapDelegate:: [" + me + "] handleUserLeftEvent. ready = [" + _ready + "]"); - - if (!_ready) return; - - closeWindow(event.userID); - } - - public function handleUserJoinedEvent(event:UserJoinedEvent):void { - trace("VideoEventMapDelegate:: [" + me + "] handleUserJoinedEvent. ready = [" + _ready + "]"); - - if (!_ready) return; - - if (options.displayAvatar) { - openAvatarWindowFor(event.userID); - } - } - - private function displayToolbarButton():void { - button.isPresenter = true; - - if (options.presenterShareOnly) { - if (UsersUtil.amIPresenter()) { - button.isPresenter = true; - } else { - button.isPresenter = false; - } - } - - } - - private function addToolbarButton():void{ - LogUtil.debug("****************** Adding toolbar button. presenter?=[" + UsersUtil.amIPresenter() + "]"); - if (proxy.videoOptions.showButton) { - - displayToolbarButton(); - - var event:ToolbarButtonEvent = new ToolbarButtonEvent(ToolbarButtonEvent.ADD); - event.button = button; - event.module="Webcam"; - _dispatcher.dispatchEvent(event); - } - } - - private function autoStart():void { - if (options.skipCamSettingsCheck) { - skipCameraSettingsCheck(); - } else { - _dispatcher.dispatchEvent(new ShareCameraRequestEvent()); - } - } - - private function changeDefaultCamForMac():Camera { - for (var i:int = 0; i < Camera.names.length; i++){ - if (Camera.names[i] == "USB Video Class Video") { - /** Set as default for Macs */ - return Camera.getCamera("USB Video Class Video"); - } - } - - return null; - } - - private function getDefaultResolution(resolutions:String):Array { - var res:Array = resolutions.split(","); - if (res.length > 0) { - var resStr:Array = (res[0] as String).split("x"); - var resInts:Array = [Number(resStr[0]), Number(resStr[1])]; - return resInts; - } else { - return [Number("320"), Number("240")]; - } - } - - private function skipCameraSettingsCheck():void { - var cam:Camera = changeDefaultCamForMac(); - if (cam == null) { - cam = Camera.getCamera(); - } - - var videoOptions:VideoConfOptions = new VideoConfOptions(); - - var resolutions:Array = getDefaultResolution(videoOptions.resolutions); - var camWidth:Number = resolutions[0]; - var camHeight:Number = resolutions[1]; - trace("Skipping cam check. Using default resolution [" + camWidth + "x" + camHeight + "]"); - cam.setMode(camWidth, camHeight, videoOptions.camModeFps); - cam.setMotionLevel(5, 1000); - cam.setKeyFrameInterval(videoOptions.camKeyFrameInterval); - - cam.setQuality(videoOptions.camQualityBandwidth, videoOptions.camQualityPicture); - initCameraWithSettings(cam.index, cam.width, cam.height); - } - - private function openWebcamWindows():void { - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindows:: ready = [" + _ready + "]"); - - var uids:ArrayCollection = UsersUtil.getUserIDs(); - - for (var i:int = 0; i < uids.length; i++) { - var u:String = uids.getItemAt(i) as String; - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindows:: open window for = [" + u + "]"); - openWebcamWindowFor(u); - } - } - - private function openWebcamWindowFor(userID:String):void { - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: open window for = [" + userID + "]"); - if (! UsersUtil.isMe(userID) && UsersUtil.hasWebcamStream(userID)) { - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: Not ME and user = [" + userID + "] is publishing."); - - if (webcamWindows.hasWindow(userID)) { - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: user = [" + userID + "] has a window open. Close it."); - closeWindow(userID); - } - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: View user's = [" + userID + "] webcam."); - openViewWindowFor(userID); - } else { - if (UsersUtil.isMe(userID) && options.autoStart) { - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: It's ME and AutoStart. Start publishing."); - autoStart(); - } else { - if (options.displayAvatar) { - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: It's NOT ME and NOT AutoStart. Open Avatar for user = [" + userID + "]"); - openAvatarWindowFor(userID); - } else { - trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: Is THERE another option for user = [" + userID + "]"); - } - } - } - } - - private function openAvatarWindowFor(userID:String):void { - if (! UsersUtil.hasUser(userID)) return; - - var window:AvatarWindow = new AvatarWindow(); - window.userID = userID; - window.title = UsersUtil.getUserName(userID); - - trace("VideoEventMapDelegate:: [" + me + "] openAvatarWindowFor:: Closing window for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); - closeWindow(userID); - - webcamWindows.addWindow(window); - - trace("VideoEventMapDelegate:: [" + me + "] openAvatarWindowFor:: Opening AVATAR window for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); - - openWindow(window); - dockWindow(window); - } - - private function openPublishWindowFor(userID:String, camIndex:int, camWidth:int, camHeight:int):void { - var publishWindow:PublishWindow = new PublishWindow(); - publishWindow.userID = userID; - publishWindow.title = UsersUtil.getUserName(userID); - publishWindow.camIndex = camIndex; - publishWindow.setResolution(camWidth, camHeight); - publishWindow.videoOptions = options; - publishWindow.quality = options.videoQuality; - publishWindow.chromePermissionDenied = _chromeWebcamPermissionDenied; - publishWindow.resolutions = options.resolutions.split(","); - - - trace("VideoEventMapDelegate:: [" + me + "] openPublishWindowFor:: Closing window for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); - closeWindow(userID); - - webcamWindows.addWindow(publishWindow); - - trace("VideoEventMapDelegate:: [" + me + "] openPublishWindowFor:: Opening PUBLISH window for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); - - openWindow(publishWindow); - dockWindow(publishWindow); - } - - private function closeWindow(userID:String):void { - if (! webcamWindows.hasWindow(userID)) { - trace("VideoEventMapDelegate:: [" + me + "] closeWindow:: No window for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); - return; - } - - var win:VideoWindowItf = webcamWindows.removeWindow(userID); - if (win != null) { - trace("VideoEventMapDelegate:: [" + me + "] closeWindow:: Closing [" + win.getWindowType() + "] for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); - win.close(); - var cwe:CloseWindowEvent = new CloseWindowEvent(); - cwe.window = win; - _dispatcher.dispatchEvent(cwe); - } else { - trace("VideoEventMapDelegate:: [" + me + "] closeWindow:: Not Closing. No window for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); - } - } - - private function openViewWindowFor(userID:String):void { - trace("VideoEventMapDelegate:: [" + me + "] openViewWindowFor:: Opening VIEW window for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); - - var window:VideoWindow = new VideoWindow(); - window.userID = userID; - window.videoOptions = options; - window.resolutions = options.resolutions.split(","); - window.title = UsersUtil.getUserName(userID); - - closeWindow(userID); - - var bbbUser:BBBUser = UsersUtil.getUser(userID); - window.startVideo(proxy.connection, bbbUser.streamName); - - webcamWindows.addWindow(window); - openWindow(window); - dockWindow(window); - } - - private function openWindow(window:VideoWindowItf):void { - var windowEvent:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); - windowEvent.window = window; - _dispatcher.dispatchEvent(windowEvent); - } - - private function dockWindow(window:VideoWindowItf):void { - // this event will dock the window, if it's enabled - var openVideoEvent:OpenVideoWindowEvent = new OpenVideoWindowEvent(); - openVideoEvent.window = window; - _dispatcher.dispatchEvent(openVideoEvent); - } - - public function connectToVideoApp():void { - proxy = new VideoProxy(uri); - proxy.connect(); - } - - public function startPublishing(e:StartBroadcastEvent):void{ - LogUtil.debug("VideoEventMapDelegate:: [" + me + "] startPublishing:: Publishing stream to: " + proxy.connection.uri + "/" + e.stream); - streamName = e.stream; - proxy.startPublishing(e); - - _isWaitingActivation = false; - _isPublishing = true; - UsersUtil.setIAmPublishing(true); - - var broadcastEvent:BroadcastStartedEvent = new BroadcastStartedEvent(); - broadcastEvent.stream = e.stream; - broadcastEvent.userid = UsersUtil.getMyUserID(); - broadcastEvent.isPresenter = UsersUtil.amIPresenter(); - broadcastEvent.camSettings = UsersUtil.amIPublishing(); - - _dispatcher.dispatchEvent(broadcastEvent); - if (proxy.videoOptions.showButton) { - button.publishingStatus(button.START_PUBLISHING); - } - } - - public function stopPublishing(e:StopBroadcastEvent):void{ - trace("VideoEventMapDelegate:: [" + me + "] Stop publishing. ready = [" + _ready + "]"); - stopBroadcasting(); - } - - private function stopBroadcasting():void { - trace("Stopping broadcast of webcam"); - - proxy.stopBroadcasting(); - - _isPublishing = false; - UsersUtil.setIAmPublishing(false); - var broadcastEvent:BroadcastStoppedEvent = new BroadcastStoppedEvent(); - broadcastEvent.stream = streamName; - broadcastEvent.userid = UsersUtil.getMyUserID(); - broadcastEvent.avatarURL = UsersUtil.getAvatarURL(); - _dispatcher.dispatchEvent(broadcastEvent); - - - - if (proxy.videoOptions.showButton) { - //Make toolbar button enabled again - button.publishingStatus(button.STOP_PUBLISHING); - } - - closeWindow(UsersUtil.getMyUserID()); - - if (options.displayAvatar) { - trace("VideoEventMapDelegate:: [" + me + "] Opening avatar"); - openAvatarWindowFor(UsersUtil.getMyUserID()); - } - } - - public function handleClosePublishWindowEvent(event:ClosePublishWindowEvent):void { - trace("Closing publish window"); - if (_isPublishing || _chromeWebcamPermissionDenied) { - stopBroadcasting(); - } - trace("Resetting flags for publish window."); - // Reset flags to determine if we are publishing or previewing webcam. - _isPublishing = false; - _isWaitingActivation = false; - } - - public function handleShareCameraRequestEvent(event:ShareCameraRequestEvent):void { - if (options.skipCamSettingsCheck) { - skipCameraSettingsCheck(); - } else { - trace("Webcam: "+_isPublishing + " " + _isPreviewWebcamOpen + " " + _isWaitingActivation); - if (!_isPublishing && !_isPreviewWebcamOpen && !_isWaitingActivation) { - openWebcamPreview(event.publishInClient); - } - } - } - - public function handleCamSettingsClosedEvent(event:BBBEvent):void{ - _isPreviewWebcamOpen = false; - } - - private function openWebcamPreview(publishInClient:Boolean):void { - var openEvent:BBBEvent = new BBBEvent(BBBEvent.OPEN_WEBCAM_PREVIEW); - openEvent.payload.publishInClient = publishInClient; - openEvent.payload.resolutions = options.resolutions; - openEvent.payload.chromePermissionDenied = _chromeWebcamPermissionDenied; - - _isPreviewWebcamOpen = true; - - _dispatcher.dispatchEvent(openEvent); - } - - public function stopModule():void { - trace("VideoEventMapDelegate:: stopping video module"); - closeAllWindows(); - proxy.disconnect(); - } - - public function closeAllWindows():void{ - trace("VideoEventMapDelegate:: closing all windows"); - if (_isPublishing) { - stopBroadcasting(); - } - - _dispatcher.dispatchEvent(new CloseAllWindowsEvent()); - } - - public function switchToPresenter(event:MadePresenterEvent):void{ - trace("VideoEventMapDelegate:: [" + me + "] Got Switch to presenter event. ready = [" + _ready + "]"); - - if (options.showButton) { - displayToolbarButton(); - } - } - - public function switchToViewer(event:MadePresenterEvent):void{ - trace("VideoEventMapDelegate:: [" + me + "] Got Switch to viewer event. ready = [" + _ready + "]"); - - if (options.showButton){ - LogUtil.debug("****************** Switching to viewer. Show video button?=[" + UsersUtil.amIPresenter() + "]"); - displayToolbarButton(); - if (_isPublishing && options.presenterShareOnly) { - stopBroadcasting(); - } - } - } - - public function connectedToVideoApp():void{ - trace("VideoEventMapDelegate:: [" + me + "] Connected to video application."); - _ready = true; - addToolbarButton(); - openWebcamWindows(); - } - - public function handleCameraSetting(event:BBBEvent):void { - var cameraIndex:int = event.payload.cameraIndex; - var camWidth:int = event.payload.cameraWidth; - var camHeight:int = event.payload.cameraHeight; - trace("VideoEventMapDelegate::handleCameraSettings [" + cameraIndex + "," + camWidth + "," + camHeight + "]"); - initCameraWithSettings(cameraIndex, camWidth, camHeight); - } - - private function initCameraWithSettings(camIndex:int, camWidth:int, camHeight:int):void { - var camSettings:CameraSettingsVO = new CameraSettingsVO(); - camSettings.camIndex = camIndex; - camSettings.camWidth = camWidth; - camSettings.camHeight = camHeight; - - UsersUtil.setCameraSettings(camSettings); - - _isWaitingActivation = true; - openPublishWindowFor(UsersUtil.getMyUserID(), camIndex, camWidth, camHeight); - } - - public function handleStoppedViewingWebcamEvent(event:StoppedViewingWebcamEvent):void { - trace("VideoEventMapDelegate::handleStoppedViewingWebcamEvent [" + me + "] received StoppedViewingWebcamEvent for user [" + event.webcamUserID + "]"); - - closeWindow(event.webcamUserID); - - if (options.displayAvatar && UsersUtil.hasUser(event.webcamUserID) && ! UsersUtil.isUserLeaving(event.webcamUserID)) { - trace("VideoEventMapDelegate::handleStoppedViewingWebcamEvent [" + me + "] Opening avatar for user [" + event.webcamUserID + "]"); - openAvatarWindowFor(event.webcamUserID); - } - } - } +/** + * 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.modules.videoconf.maps +{ + import com.asfusion.mate.utils.debug.Debugger; + import com.asfusion.mate.utils.debug.DebuggerUtil; + + import flash.events.IEventDispatcher; + import flash.external.ExternalInterface; + import flash.media.Camera; + + import mx.collections.ArrayCollection; + import mx.collections.ArrayList; + + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.common.events.CloseWindowEvent; + import org.bigbluebutton.common.events.OpenWindowEvent; + import org.bigbluebutton.common.events.ToolbarButtonEvent; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.events.ConnectAppEvent; + import org.bigbluebutton.core.managers.UserManager; + import org.bigbluebutton.core.model.VideoProfile; + import org.bigbluebutton.core.vo.CameraSettingsVO; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.events.MadePresenterEvent; + import org.bigbluebutton.main.events.StoppedViewingWebcamEvent; + import org.bigbluebutton.main.events.UserJoinedEvent; + import org.bigbluebutton.main.events.UserLeftEvent; + import org.bigbluebutton.main.model.users.BBBUser; + import org.bigbluebutton.main.model.users.events.BroadcastStartedEvent; + import org.bigbluebutton.main.model.users.events.BroadcastStoppedEvent; + import org.bigbluebutton.main.model.users.events.StreamStartedEvent; + import org.bigbluebutton.modules.videoconf.business.VideoProxy; + import org.bigbluebutton.modules.videoconf.events.CloseAllWindowsEvent; + import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; + import org.bigbluebutton.modules.videoconf.events.ConnectedEvent; + import org.bigbluebutton.modules.videoconf.events.OpenVideoWindowEvent; + import org.bigbluebutton.modules.videoconf.events.PlayConnectionReady; + import org.bigbluebutton.modules.videoconf.events.ShareCameraRequestEvent; + import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; + import org.bigbluebutton.modules.videoconf.events.StopBroadcastEvent; + import org.bigbluebutton.modules.videoconf.events.StopShareCameraRequestEvent; + import org.bigbluebutton.modules.videoconf.events.WebRTCWebcamRequestEvent; + import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; + import org.bigbluebutton.modules.videoconf.views.AvatarWindow; + import org.bigbluebutton.modules.videoconf.views.GraphicsWrapper; + import org.bigbluebutton.modules.videoconf.views.ToolbarPopupButton; + import org.bigbluebutton.modules.videoconf.views.UserAvatar; + import org.bigbluebutton.modules.videoconf.views.UserGraphic; + import org.bigbluebutton.modules.videoconf.views.UserGraphicHolder; + import org.bigbluebutton.modules.videoconf.views.UserVideo; + import org.bigbluebutton.modules.videoconf.views.VideoDock; + import org.flexunit.runner.manipulation.filters.IncludeAllFilter; + + public class VideoEventMapDelegate + { + static private var PERMISSION_DENIED_ERROR:String = "PermissionDeniedError"; + + private var options:VideoConfOptions = new VideoConfOptions(); + private var uri:String; + + private var button:ToolbarPopupButton = new ToolbarPopupButton(); + private var proxy:VideoProxy; + + private var _dispatcher:IEventDispatcher; + private var _ready:Boolean = false; + private var _isPublishing:Boolean = false; + private var _isPreviewWebcamOpen:Boolean = false; + private var _isWaitingActivation:Boolean = false; + private var _chromeWebcamPermissionDenied:Boolean = false; + + private var _videoDock:VideoDock; + private var _graphics:GraphicsWrapper = new GraphicsWrapper(); + private var streamList:ArrayList = new ArrayList(); + private var numberOfWindows:Object = new Object(); + + // Store userID of windows waiting for a NetConnection + private var pendingVideoWindowsList:Object = new Object(); + + public function VideoEventMapDelegate(dispatcher:IEventDispatcher) + { + _dispatcher = dispatcher; + } + + private function get me():String { + return UsersUtil.getMyUsername(); + } + + public function start(uri:String):void { + trace("VideoEventMapDelegate:: [" + me + "] Video Module Started."); + this.uri = uri; + + _videoDock = new VideoDock(); + var windowEvent:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); + windowEvent.window = _videoDock; + _dispatcher.dispatchEvent(windowEvent); + + _videoDock.addChild(_graphics); + } + + public function viewCamera(userID:String, stream:String, name:String, mock:Boolean = false):void { + trace("VideoEventMapDelegate:: [" + me + "] viewCamera. ready = [" + _ready + "]"); + + if (!_ready) return; + trace("VideoEventMapDelegate:: [" + me + "] Viewing [" + userID + " stream [" + stream + "]"); + if (! UserManager.getInstance().getConference().amIThisUser(userID)) { + // Check whether is one stream or all of them + if(stream.indexOf("|") > 0) { + initAllPlayConnectionsFor(userID); + } else { + initPlayConnectionFor(userID, stream); + } + } + } + + public function handleUserLeftEvent(event:UserLeftEvent):void { + trace("VideoEventMapDelegate:: [" + me + "] handleUserLeftEvent. ready = [" + _ready + "]"); + + if (!_ready) return; + + closeWindow(event.userID); + } + + public function handleUserJoinedEvent(event:UserJoinedEvent):void { + trace("VideoEventMapDelegate:: [" + me + "] handleUserJoinedEvent. ready = [" + _ready + "]"); + + if (!_ready) return; + + if (options.displayAvatar) { + openAvatarWindowFor(event.userID); + } + } + + private function displayToolbarButton():void { + button.isPresenter = true; + + if (options.presenterShareOnly) { + if (UsersUtil.amIPresenter()) { + button.isPresenter = true; + } else { + button.isPresenter = false; + } + } + + } + + private function addToolbarButton():void{ + LogUtil.debug("****************** Adding toolbar button. presenter?=[" + UsersUtil.amIPresenter() + "]"); + if (proxy.videoOptions.showButton) { + + displayToolbarButton(); + + var event:ToolbarButtonEvent = new ToolbarButtonEvent(ToolbarButtonEvent.ADD); + event.button = button; + event.module="Webcam"; + _dispatcher.dispatchEvent(event); + } + } + + private function autoStart():void { + if (options.skipCamSettingsCheck) { + skipCameraSettingsCheck(); + } else { + var dp:Object = []; + for(var i:int = 0; i < Camera.names.length; i++) { + dp.push({label: Camera.names[i], status: button.OFF_STATE}); + } + button.enabled = false; + var shareCameraRequestEvent:ShareCameraRequestEvent = new ShareCameraRequestEvent(); + shareCameraRequestEvent.camerasArray = dp; + _dispatcher.dispatchEvent(shareCameraRequestEvent); + } + } + + private function changeDefaultCamForMac():Camera { + for (var i:int = 0; i < Camera.names.length; i++){ + if (Camera.names[i] == "USB Video Class Video") { + /** Set as default for Macs */ + return Camera.getCamera("USB Video Class Video"); + } + } + + return null; + } + + private function skipCameraSettingsCheck(camIndex:int = -1):void { + if (camIndex == -1) { + var cam:Camera = changeDefaultCamForMac(); + if (cam == null) { + cam = Camera.getCamera(); + } + camIndex = cam.index; + } + + var videoProfile:VideoProfile = BBB.defaultVideoProfile; + initCameraWithSettings(camIndex, videoProfile); + } + + private function openWebcamWindows():void { + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindows:: ready = [" + _ready + "]"); + + var uids:ArrayCollection = UsersUtil.getUserIDs(); + + for (var i:int = 0; i < uids.length; i++) { + var u:String = uids.getItemAt(i) as String; + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindows:: open window for = [" + u + "]"); + openWebcamWindowFor(u); + } + } + + private function openWebcamWindowFor(userID:String):void { + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: open window for = [" + userID + "]"); + if (! UsersUtil.isMe(userID) && UsersUtil.hasWebcamStream(userID)) { + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: Not ME and user = [" + userID + "] is publishing."); + + if (hasWindow(userID)) { + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: user = [" + userID + "] has a window open. Close it."); + closeWindow(userID); + } + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: View user's = [" + userID + "] webcam."); + initAllPlayConnectionsFor(userID); + } else { + if (UsersUtil.isMe(userID) && options.autoStart) { + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: It's ME and AutoStart. Start publishing."); + autoStart(); + } else { + if (options.displayAvatar) { + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: It's NOT ME and NOT AutoStart. Open Avatar for user = [" + userID + "]"); + openAvatarWindowFor(userID); + } else { + trace("VideoEventMapDelegate:: [" + me + "] openWebcamWindowFor:: Is THERE another option for user = [" + userID + "]"); + } + } + } + } + + private function openAvatarWindowFor(userID:String):void { + if (! UsersUtil.hasUser(userID)) return; + + closeAllAvatarWindows(userID); + + _graphics.addAvatarFor(userID); + } + + private function closeAllAvatarWindows(userID:String):void { + _graphics.removeAvatarFor(userID); + } + + private function openPublishWindowFor(userID:String, camIndex:int, videoProfile:VideoProfile):void { + closeAllAvatarWindows(userID); + + _graphics.addCameraFor(userID, camIndex, videoProfile); + } + + private function hasWindow(userID:String):Boolean { + return _graphics.hasGraphicsFor(userID); + } + + private function closeWindow(userID:String):void { + _graphics.removeGraphicsFor(userID); + var bbbUser:BBBUser = UsersUtil.getUser(userID); + var streams:Array = bbbUser.streamNames; + for each(var stream:String in streams) { + proxy.closePlayConnectionFor(stream); + } + } + + private function closePublishWindowWithStream(userID:String, stream:String):int { + return _graphics.removeVideoByStreamName(userID, stream); + } + + private function initAllPlayConnectionsFor(userID:String):void { + trace("VideoEventMapDelegate:: initAllPlayConnectionsFor : [" + userID + "]"); + var bbbUser:BBBUser = UsersUtil.getUser(userID); + var streams:Array = bbbUser.streamNames; + for each(var stream:String in streams) { + initPlayConnectionFor(userID, stream); + } + } + + private function initPlayConnectionFor(userID:String, streamName:String):void { + trace("VideoEventMapDelegate:: initPlayConnectionFor : user [" + userID + "] stream [" + streamName + "]"); + // Store the userID + pendingVideoWindowsList[streamName] = userID; + // Request the connection + proxy.createPlayConnectionFor(streamName); + } + + public function handlePlayConnectionReady(e:PlayConnectionReady):void { + var userID:String = pendingVideoWindowsList[e.streamName]; + trace("VideoEventMapDelegate:: handlePlayConnectionReady : stream:[" + e.streamName + "] conn:["+e.connection.uri+"] prefix:["+e.prefix+"] userID:["+userID+"]"); + if(userID) { + openViewWindowFor(userID, e.streamName); + } + } + + private function openViewWindowFor(userID:String, streamName:String):void { + trace("VideoEventMapDelegate:: [" + me + "] openViewWindowFor:: Opening VIEW window for [" + userID + "] [" + UsersUtil.getUserName(userID) + "]"); + + var bbbUser:BBBUser = UsersUtil.getUser(userID); + if (bbbUser.hasStream) { + closeAllAvatarWindows(userID); + } + _graphics.addVideoFor(userID, proxy.getConnectionForStream(streamName), proxy.getPrefixForStream(streamName) + streamName); + } + + public function connectToVideoApp():void { + proxy = new VideoProxy(uri); + proxy.connect(); + } + + public function startPublishing(e:StartBroadcastEvent):void{ + LogUtil.debug("VideoEventMapDelegate:: [" + me + "] startPublishing:: Publishing stream to: " + proxy.publishConnection.uri + "/" + e.stream); + proxy.startPublishing(e); + + _isWaitingActivation = false; + _isPublishing = true; + UsersUtil.setIAmPublishing(true); + + var broadcastEvent:BroadcastStartedEvent = new BroadcastStartedEvent(); + streamList.addItem(e.stream); + broadcastEvent.stream = e.stream; + broadcastEvent.userid = UsersUtil.getMyUserID(); + broadcastEvent.isPresenter = UsersUtil.amIPresenter(); + broadcastEvent.camSettings = UsersUtil.amIPublishing(); + + _dispatcher.dispatchEvent(broadcastEvent); + if (proxy.videoOptions.showButton) { + button.publishingStatus(button.START_PUBLISHING); + } + } + + public function stopPublishing(e:StopBroadcastEvent):void{ + trace("VideoEventMapDelegate:: [" + me + "] Stop publishing. ready = [" + _ready + "]"); + if(streamList.length <= 1) { + setStopLastBroadcasting(); + } else { + UsersUtil.setIAmPublishing(true); + } + streamList.removeItem(e.stream); + stopBroadcasting(e.stream); + button.setCamAsInactive(e.camId); + } + + private function stopAllBroadcasting():void { + trace("[VideoEventMapDelegate:stopAllBroadcasting]"); + setStopLastBroadcasting(); + streamList = new ArrayList(); + proxy.stopAllBroadcasting(); + + var userID:String = UsersUtil.getMyUserID(); + _graphics.removeGraphicsFor(userID); + + var broadcastEvent:BroadcastStoppedEvent = new BroadcastStoppedEvent(); + broadcastEvent.stream = ""; + broadcastEvent.userid = UsersUtil.getMyUserID(); + broadcastEvent.avatarURL = UsersUtil.getAvatarURL(); + _dispatcher.dispatchEvent(broadcastEvent); + + if (proxy.videoOptions.showButton) { + //Make toolbar button enabled again + button.setAllCamAsInactive(); + } + if (options.displayAvatar) { + trace("VideoEventMapDelegate:: [" + me + "] Opening avatar"); + openAvatarWindowFor(UsersUtil.getMyUserID()); + } + } + + private function setStopLastBroadcasting():void { + trace("[VideoEventMapDelegate:setStopLastBroadcasting]"); + _isPublishing = false; + UsersUtil.setIAmPublishing(false); + } + + private function stopBroadcasting(stream:String):void { + trace("Stopping broadcast of stream [" + stream + "]"); + + proxy.stopBroadcasting(stream); + + var broadcastEvent:BroadcastStoppedEvent = new BroadcastStoppedEvent(); + broadcastEvent.stream = stream; + broadcastEvent.userid = UsersUtil.getMyUserID(); + broadcastEvent.avatarURL = UsersUtil.getAvatarURL(); + _dispatcher.dispatchEvent(broadcastEvent); + + var camId:int = closePublishWindowWithStream(UsersUtil.getMyUserID(), stream); + + if (proxy.videoOptions.showButton) { + //Make toolbar button enabled again + button.publishingStatus(button.STOP_PUBLISHING, camId); + } + + if (streamList.length == 0 && options.displayAvatar) { + trace("VideoEventMapDelegate:: [" + me + "] Opening avatar"); + openAvatarWindowFor(UsersUtil.getMyUserID()); + } + } + + public function handleClosePublishWindowEvent(event:ClosePublishWindowEvent):void { + trace("Closing publish window"); + if (_isPublishing || _chromeWebcamPermissionDenied) { + stopAllBroadcasting(); + } + } + + public function handleShareCameraRequestEvent(event:ShareCameraRequestEvent):void { + trace("[VideoEventMapDelegate:handleShareCameraRequestEvent]"); + if (options.skipCamSettingsCheck) { + skipCameraSettingsCheck(int(event.defaultCamera)); + } else { + openWebcamPreview(event.publishInClient, event.defaultCamera, event.camerasArray); + } + } + + public function handleStopAllShareCameraRequestEvent(event:StopShareCameraRequestEvent):void { + trace("[VideoEventMapDelegate:handleStopAllShareCameraRequestEvent]"); + stopAllBroadcasting(); + } + + public function handleStopShareCameraRequestEvent(event:StopShareCameraRequestEvent):void { + trace("[VideoEventMapDelegate:handleStopShareCameraRequestEvent]"); + var userID:String = UsersUtil.getMyUserID(); + var camIndex:int = event.camId; + + _graphics.removeVideoByCamIndex(userID, camIndex); + } + + public function handleCamSettingsClosedEvent(event:BBBEvent):void{ + _isPreviewWebcamOpen = false; + } + + private function openWebcamPreview(publishInClient:Boolean, defaultCamera:String, camerasArray:Object):void { + var openEvent:BBBEvent = new BBBEvent(BBBEvent.OPEN_WEBCAM_PREVIEW); + openEvent.payload.publishInClient = publishInClient; + openEvent.payload.defaultCamera = defaultCamera; + openEvent.payload.camerasArray = camerasArray; + openEvent.payload.chromePermissionDenied = _chromeWebcamPermissionDenied; + + _isPreviewWebcamOpen = true; + + _dispatcher.dispatchEvent(openEvent); + } + + public function stopModule():void { + trace("VideoEventMapDelegate:: stopping video module"); + closeAllWindows(); + proxy.disconnect(); + } + + public function closeAllWindows():void{ + trace("VideoEventMapDelegate:: closing all windows"); + if (_isPublishing) { + stopAllBroadcasting(); + } + + _graphics.shutdown(); + } + + public function switchToPresenter(event:MadePresenterEvent):void{ + trace("VideoEventMapDelegate:: [" + me + "] Got Switch to presenter event. ready = [" + _ready + "]"); + + if (options.showButton) { + displayToolbarButton(); + } + } + + public function switchToViewer(event:MadePresenterEvent):void{ + trace("VideoEventMapDelegate:: [" + me + "] Got Switch to viewer event. ready = [" + _ready + "]"); + + if (options.showButton){ + LogUtil.debug("****************** Switching to viewer. Show video button?=[" + UsersUtil.amIPresenter() + "]"); + displayToolbarButton(); + if (_isPublishing && options.presenterShareOnly) { + stopAllBroadcasting(); + } + } + } + + public function connectedToVideoApp():void{ + trace("VideoEventMapDelegate:: [" + me + "] Connected to video application."); + _ready = true; + addToolbarButton(); + openWebcamWindows(); + } + + public function handleCameraSetting(event:BBBEvent):void { + var cameraIndex:int = event.payload.cameraIndex; + var videoProfile:VideoProfile = event.payload.videoProfile; + trace("VideoEventMapDelegate::handleCameraSettings [" + cameraIndex + "," + videoProfile.id + "]"); + initCameraWithSettings(cameraIndex, videoProfile); + } + + private function initCameraWithSettings(camIndex:int, videoProfile:VideoProfile):void { + var camSettings:CameraSettingsVO = new CameraSettingsVO(); + camSettings.camIndex = camIndex; + camSettings.videoProfile = videoProfile; + + UsersUtil.setCameraSettings(camSettings); + + _isWaitingActivation = true; + button.setCamAsActive(camIndex); + openPublishWindowFor(UsersUtil.getMyUserID(), camIndex, videoProfile); + } + + private function closeViewWindowWithStream(userID:String, stream:String):void { + _graphics.removeVideoByStreamName(userID, stream); + proxy.closePlayConnectionFor(stream); + } + + public function handleStreamStoppedEvent(userID:String, streamName:String):void { + // Since the stream used to create the window includes de prefix, we should include it here too + var stream:String = proxy.getPrefixForStream(streamName) + streamName; + handleStoppedViewingWebcamEvent(userID, stream); + } + + public function handlePlayConnectionClosed(stream:String, prefix:String):void { + var userID:String = pendingVideoWindowsList[stream]; + closeViewWindowWithStream(userID, prefix + stream); + } + + public function handleStoppedViewingWebcamEvent(userID:String, streamName:String):void { + trace("VideoEventMapDelegate::handleStoppedViewingWebcamEvent [" + me + "] received StoppedViewingWebcamEvent for user [" + userID + "]"); + + closeViewWindowWithStream(userID, streamName); + + if (options.displayAvatar && UsersUtil.hasUser(userID) && ! UsersUtil.isUserLeaving(userID)) { + trace("VideoEventMapDelegate::handleStoppedViewingWebcamEvent [" + me + "] Opening avatar for user [" + userID + "]"); + openAvatarWindowFor(userID); + } + } + } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/WindowManager.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/WindowManager.as index 499d1e3d8ca6b2692883d2059453266572ce226f..4eaace66a7fc233427cf39f06af3f5613ee340d4 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/WindowManager.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/maps/WindowManager.as @@ -25,6 +25,7 @@ package org.bigbluebutton.modules.videoconf.maps import org.bigbluebutton.core.UsersUtil; import org.bigbluebutton.modules.videoconf.business.VideoWindowItf; import org.bigbluebutton.modules.videoconf.views.AvatarWindow; + import mx.collections.ArrayList; public class WindowManager { @@ -50,6 +51,17 @@ package org.bigbluebutton.modules.videoconf.maps return null; } + + public function removeWin(window:VideoWindowItf):VideoWindowItf { + for (var i:int = 0; i < webcamWindows.length; i++) { + var win:VideoWindowItf = webcamWindows.getItemAt(i) as VideoWindowItf; + if (win == window) { + return webcamWindows.removeItemAt(i) as VideoWindowItf; + } + } + + return null; + } public function hasWindow(userID:String):Boolean { trace("[" + me + "] hasWindow:: user [" + userID + "] numWindows = [" + webcamWindows.length + "]"); @@ -73,5 +85,16 @@ package org.bigbluebutton.modules.videoconf.maps return null; } + + public function getAllWindow(userID:String):ArrayList { + var windowsList:ArrayList = new ArrayList(); + for (var i:int = 0; i < webcamWindows.length; i++) { + var win:VideoWindowItf = webcamWindows.getItemAt(i) as VideoWindowItf; + trace("[" + me + "] getWindow:: [" + win.userID + " == " + userID + "] equal = [" + (win.userID == userID) + "]"); + if (win.userID == userID) windowsList.addItem(win); + } + + return windowsList; + } } -} \ No newline at end of file +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/model/VideoConfOptions.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/model/VideoConfOptions.as index 15d35e8874704b849eed6aff5d411c115dc428fa..2998091b912b54f91805e763a97b43ffdb1f382f 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/model/VideoConfOptions.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/model/VideoConfOptions.as @@ -24,12 +24,6 @@ package org.bigbluebutton.modules.videoconf.model { public var uri:String = "rtmp://localhost/video"; - [Bindable] - public var videoQuality:Number = 100; - - [Bindable] - public var resolutions:String = "320x240,640x480,1280x720"; - [Bindable] public var autoStart:Boolean = false; @@ -48,15 +42,6 @@ package org.bigbluebutton.modules.videoconf.model [Bindable] public var viewerWindowLocation:String = "middle"; - [Bindable] - public var camKeyFrameInterval:Number = 5; - - [Bindable] - public var camModeFps:Number = 15; - - [Bindable] - public var camQualityBandwidth:Number = 0; - [Bindable] public var smoothVideo:Boolean = false; @@ -72,18 +57,6 @@ package org.bigbluebutton.modules.videoconf.model [Bindable] public var filterDivisor:Number = 4; - [Bindable] - public var enableH264:Boolean = false; - - [Bindable] - public var h264Level:String = "2.1"; - - [Bindable] - public var h264Profile:String = "main"; - - [Bindable] - public var camQualityPicture:Number = 50; - [Bindable] public var baseTabIndex:int; [Bindable] @@ -106,6 +79,8 @@ package org.bigbluebutton.modules.videoconf.model [Bindable] public var glowBlurSize:Number = 30.0; + [Bindable] + public var priorityRatio:Number = 2/3; public function VideoConfOptions() { parseOptions(); @@ -117,12 +92,6 @@ package org.bigbluebutton.modules.videoconf.model if (vxml.@uri != undefined) { uri = vxml.@uri.toString(); } - if (vxml.@videoQuality != undefined) { - videoQuality = Number(vxml.@videoQuality.toString()); - } - if (vxml.@resolutions != undefined) { - resolutions = vxml.@resolutions.toString(); - } if (vxml.@showCloseButton != undefined) { showCloseButton = (vxml.@showCloseButton.toString().toUpperCase() == "TRUE") ? true : false; } @@ -153,18 +122,6 @@ package org.bigbluebutton.modules.videoconf.model if (vxml.@viewerWindowLocation != undefined) { viewerWindowLocation = vxml.@viewerWindowLocation.toString().toUpperCase(); } - if (vxml.@camKeyFrameInterval != undefined) { - camKeyFrameInterval = Number(vxml.@camKeyFrameInterval.toString()); - } - if (vxml.@camModeFps != undefined) { - camModeFps = Number(vxml.@camModeFps.toString()); - } - if (vxml.@camQualityBandwidth != undefined) { - camQualityBandwidth = Number(vxml.@camQualityBandwidth.toString()); - } - if (vxml.@camQualityPicture != undefined) { - camQualityPicture = Number(vxml.@camQualityPicture.toString()); - } if (vxml.@smoothVideo != undefined) { smoothVideo = (vxml.@smoothVideo.toString().toUpperCase() == "TRUE") ? true : false; } @@ -184,16 +141,7 @@ package org.bigbluebutton.modules.videoconf.model if (vxml.@filterDivisor != undefined) { filterDivisor = Number(vxml.@filterDivisor.toString()); } - if (vxml.@enableH264 != undefined) { - enableH264 = (vxml.@enableH264.toString().toUpperCase() == "TRUE") ? true : false; - } - if (vxml.@h264Level != undefined) { - h264Level = vxml.@h264Level.toString(); - } - if (vxml.@h264Profile != undefined) { - h264Profile = vxml.@h264Profile.toString(); - } - + if (vxml.@baseTabIndex != undefined) { baseTabIndex = vxml.@baseTabIndex; } @@ -216,6 +164,9 @@ package org.bigbluebutton.modules.videoconf.model if (vxml.@glowBlurSize != undefined) { glowBlurSize = Number(vxml.@glowBlurSize.toString()); } + if (vxml.@priorityRatio != undefined) { + priorityRatio = Number(vxml.@priorityRatio.toString()); + } } } } diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/services/messaging/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/services/messaging/MessageReceiver.as new file mode 100644 index 0000000000000000000000000000000000000000..80abc73969addcdd439194b9f6c0f24501f6cfdb --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/services/messaging/MessageReceiver.as @@ -0,0 +1,54 @@ +/** + * 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.modules.videoconf.services.messaging +{ + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.main.model.users.IMessageListener; + import org.bigbluebutton.core.managers.ConnectionManager; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.modules.videoconf.business.VideoProxy; + + public class MessageReceiver implements IMessageListener { + private static const LOG:String = "VideoConf::MessageReceiver - "; + + private var proxy:VideoProxy; + + public function MessageReceiver(proxy:VideoProxy) { + BBB.initConnectionManager().addMessageListener(this); + this.proxy = proxy; + } + + public function onMessage(messageName:String, message:Object):void { + switch (messageName) { + case "getStreamPathReply": + handleGetStreamPathReply(message); + break; + } + } + + private function handleGetStreamPathReply(msg:Object):void { + var map:Object = JSON.parse(msg.msg); + var streamName:String = map.streamName; + var streamPath:String = map.streamPath; + + proxy.handleStreamPathReceived(streamName, streamPath); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/services/messaging/MessageSender.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/services/messaging/MessageSender.as new file mode 100644 index 0000000000000000000000000000000000000000..956e3160728ac44882b1b4eb90567f962ad97740 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/services/messaging/MessageSender.as @@ -0,0 +1,43 @@ +/** + * 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.modules.videoconf.services.messaging +{ + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.managers.ConnectionManager; + + public class MessageSender { + private static const LOG:String = "VideoConf::MessageSender - "; + + public function getStreamPath(streamName:String):void { + LogUtil.debug(LOG + "getStreamPath for stream [" + streamName + "]"); + var _nc:ConnectionManager = BBB.initConnectionManager(); + _nc.sendMessage("video.getStreamPath", onSuccess, onFailure, streamName); + } + + private function onSuccess(result:String):void { + LogUtil.debug(result); + } + + private function onFailure(status:String):void { + LogUtil.error(status); + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/AvatarWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/AvatarWindow.mxml index 4214254a5fa35dae9d2b97e9b4d325a022522375..5e547d63b8024b81ad4aaed14c3b2e1b16c173a6 100644 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/AvatarWindow.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/AvatarWindow.mxml @@ -36,37 +36,39 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mate:Listener type="{BBBEvent.USER_VOICE_MUTED}" method="handleUserVoiceMutedEvent" /> <mate:Listener type="{EventConstants.USER_TALKING}" method="handleUserTalkingEvent" /> - <mate:Listener type="{SwitchedPresenterEvent.SWITCHED_PRESENTER}" method="handleSwitchedPresenterEvent" /> - <mate:Listener type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" method="handleMadePresenterEvent" /> - <mate:Listener type="{BBBEvent.USER_VOICE_JOINED}" method="handleNewRoleEvent" /> - <mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleNewRoleEvent" /> + <mate:Listener type="{SwitchedPresenterEvent.SWITCHED_PRESENTER}" method="handleSwitchedPresenterEvent" /> + <mate:Listener type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" method="handleMadePresenterEvent" /> + <mate:Listener type="{BBBEvent.USER_VOICE_JOINED}" method="handleNewRoleEvent" /> + <mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleNewRoleEvent" /> <mate:Listener type="{CloseAllWindowsEvent.CLOSE_ALL_WINDOWS}" method="closeWindow" /> <mx:Script> <![CDATA[ - import flexlib.mdi.events.MDIWindowEvent; + import flexlib.mdi.events.MDIWindowEvent; - import mx.core.UIComponent; - import mx.events.ResizeEvent; - import org.bigbluebutton.common.Images; - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.common.Role; - import org.bigbluebutton.common.events.CloseWindowEvent; - import org.bigbluebutton.common.events.LocaleChangeEvent; - import org.bigbluebutton.core.EventConstants; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.events.CoreEvent; - import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.main.events.MadePresenterEvent; - import org.bigbluebutton.main.events.SwitchedPresenterEvent; - import org.bigbluebutton.main.views.MainCanvas; - import org.bigbluebutton.modules.videoconf.business.TalkingButtonOverlay; - import org.bigbluebutton.modules.videoconf.events.CloseAllWindowsEvent; - import org.bigbluebutton.modules.videoconf.events.OpenVideoWindowEvent; - import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; - import org.bigbluebutton.util.i18n.ResourceUtil; + import mx.core.UIComponent; + import mx.events.ResizeEvent; + + import org.bigbluebutton.common.Images; + import org.bigbluebutton.common.LogUtil; + import org.bigbluebutton.common.Role; + import org.bigbluebutton.common.events.CloseWindowEvent; + import org.bigbluebutton.common.events.LocaleChangeEvent; + import org.bigbluebutton.core.EventConstants; + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.events.CoreEvent; + import org.bigbluebutton.core.events.SwitchedLayoutEvent; + import org.bigbluebutton.core.managers.UserManager; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.events.MadePresenterEvent; + import org.bigbluebutton.main.events.SwitchedPresenterEvent; + import org.bigbluebutton.main.views.MainCanvas; + import org.bigbluebutton.modules.videoconf.business.TalkingButtonOverlay; + import org.bigbluebutton.modules.videoconf.events.CloseAllWindowsEvent; + import org.bigbluebutton.modules.videoconf.events.OpenVideoWindowEvent; + import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; + import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; + import org.bigbluebutton.util.i18n.ResourceUtil; [Bindable] private var defaultWidth:Number = 320; [Bindable] private var defaultHeight:Number = 240; diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ControlButtons.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ControlButtons.mxml deleted file mode 100755 index 45b36d4a6f6b74535f26cd66292723a2744583ab..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ControlButtons.mxml +++ /dev/null @@ -1,239 +0,0 @@ -<?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/>. - ---> -<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" - xmlns:mate="http://mate.asfusion.com/" - creationComplete="onCreationComplete()"> - <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="lockSettingsChanged" /> - <mx:Script> - <![CDATA[ - import com.asfusion.mate.events.Dispatcher; - - import org.bigbluebutton.common.Images; - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.core.BBB; - import org.bigbluebutton.core.EventConstants; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.events.CoreEvent; - import org.bigbluebutton.core.events.LockControlEvent; - import org.bigbluebutton.core.events.VoiceConfEvent; - import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.main.model.users.BBBUser; - import org.bigbluebutton.main.model.users.Conference; - import org.bigbluebutton.main.model.users.events.KickUserEvent; - import org.bigbluebutton.main.model.users.events.RoleChangeEvent; - import org.bigbluebutton.modules.chat.model.ChatOptions; - import org.bigbluebutton.modules.users.model.UsersOptions; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; - import org.bigbluebutton.util.i18n.ResourceUtil; - - public var sharerUserID:String; - [Bindable] - private var BUTTONS_SIZE:int = 31; - private var BUTTONS_PADDING:int = 10; - - [Bindable] - private var showButton:Boolean; - - private function lockSettingsChanged(e:*):void { - showControlButtons(); - } - - private function onCreationComplete():void { - showControlButtons(); - showPrivateChatButton(); - } - - public function get padding():int { - return BUTTONS_PADDING; - } - - public function userMuted(muted:Boolean):void { - if (muted) { - muteUnmuteBtn.styleName = "videoMutedButtonStyle"; - } else { - muteUnmuteBtn.styleName = "videoUnmutedButtonStyle"; - } - } - - public function updateControlButtons():void { - showControlButtons(); - } - - private function onKickUserClicked(event:Event):void { - event.stopImmediatePropagation(); - - var gd:Dispatcher = new Dispatcher(); - gd.dispatchEvent(new KickUserEvent(sharerUserID)); - } - - private function onPrivateChatClicked(event:Event):void { - event.stopImmediatePropagation(); - - var e:CoreEvent = new CoreEvent(EventConstants.START_PRIVATE_CHAT); - e.message.chatWith = sharerUserID; - var gd:Dispatcher = new Dispatcher(); - gd.dispatchEvent(e); - } - - private function onSwitchPresenterClicked(event:Event):void { - event.stopImmediatePropagation(); - - var e:RoleChangeEvent = new RoleChangeEvent(RoleChangeEvent.ASSIGN_PRESENTER); - e.userid = sharerUserID; - e.username = UsersUtil.getUserName(sharerUserID); - var gd:Dispatcher = new Dispatcher(); - gd.dispatchEvent(e); - } - - private function onMuteUnmuteClicked(event:Event):void { - event.stopImmediatePropagation(); - - var bu:BBBUser = UsersUtil.getUser(sharerUserID); - if (bu != null) { - var e:VoiceConfEvent = new VoiceConfEvent(VoiceConfEvent.MUTE_USER); - e.userid = bu.userID; - e.mute = ! bu.voiceMuted; - var gd:Dispatcher = new Dispatcher(); - gd.dispatchEvent(e); - } - } - - private function showControlButtons():void { - var vidOptions:VideoConfOptions = new VideoConfOptions(); - - displaySwitchPresenterButton(vidOptions.controlsForPresenter); - displayMuteButton(vidOptions.controlsForPresenter); - displayEjectButton(vidOptions.controlsForPresenter); - } - - private function displayEjectButton(controlsForPresenter:Boolean):void { - var userOption:UsersOptions = new UsersOptions(); - if (! userOption.allowKickUser) { - trace("Kicking user not allowed"); - // User kicking not enabled. Just return; - ejectUserBtn.visible = false; - return; - } - - trace("Kicking user allowed [" + userOption.allowKickUser + "]"); - - /** - * Display button if: - * 1. If I am moderator and this window is not me. - * 2. If I am the presenter and display controls for presenter and this window is not me. - */ - if (ejectUserBtn != null) { - if (UsersUtil.amIModerator() && ! UsersUtil.isMe(sharerUserID)) { - ejectUserBtn.visible = true; - ejectUserBtn.toolTip = ResourceUtil.getInstance().getString('bbb.video.controls.ejectUserBtn.toolTip', [UsersUtil.getUserName(sharerUserID)]); - return; - } - - if (controlsForPresenter && UsersUtil.amIPresenter() && ! UsersUtil.isMe(sharerUserID)) { /** 2 **/ - ejectUserBtn.visible = true; - ejectUserBtn.toolTip = ResourceUtil.getInstance().getString('bbb.video.controls.ejectUserBtn.toolTip', [UsersUtil.getUserName(sharerUserID)]); - return; - } - - ejectUserBtn.visible = false; - } - } - - private function displaySwitchPresenterButton(controlsForPresenter:Boolean):void { - /** - * Display button if: - * 1. If I am moderator and this user is NOT presenter. - * 2. If I am the presenter and display controls for presenter and this window is not me. - */ - if (switchPresenter != null) { - if (UsersUtil.amIModerator() && (sharerUserID != UsersUtil.getPresenterUserID())) { /** 1. **/ - switchPresenter.visible = true; - switchPresenter.toolTip = ResourceUtil.getInstance().getString('bbb.video.controls.switchPresenter.toolTip', [UsersUtil.getUserName(sharerUserID)]); - return; - } - - if (controlsForPresenter && UsersUtil.amIPresenter() && ! UsersUtil.isMe(sharerUserID)) { /** 2 **/ - switchPresenter.visible = true; - switchPresenter.toolTip = ResourceUtil.getInstance().getString('bbb.video.controls.switchPresenter.toolTip', [UsersUtil.getUserName(sharerUserID)]); - return; - } - - switchPresenter.visible = false; - } - } - - private function displayMuteButton(controlsForPresenter:Boolean):void { - /** - * Display button if user is joined to voice and: - * 1. I am moderator or presenter and display controls for presenter and user is not locked with mic disabled due to lock - * 2. if this window is me and my mic is not disabled due to lock - * */ - if (muteUnmuteBtn != null) { - if (UsersUtil.isUserJoinedToVoice(sharerUserID)) { - var isMe:Boolean = UsersUtil.isMe(sharerUserID); - var userManager:UserManager = UserManager.getInstance(); - var conference:Conference = userManager.getConference(); - var me:BBBUser = conference.getMyUser(); - - var allowMuteUnmuteButton:Boolean = false; - if (isMe && !me.disableMyMic) { - allowMuteUnmuteButton = true; - } else if (UsersUtil.amIModerator() || (controlsForPresenter && UsersUtil.amIPresenter()) ) { - allowMuteUnmuteButton = true; - - var thisUser:BBBUser = UsersUtil.getUser(sharerUserID); - if (thisUser.userLocked && conference.getLockSettings().getDisableMic()) { - allowMuteUnmuteButton = false; - } - } - - if (allowMuteUnmuteButton) { - muteUnmuteBtn.visible = true; - muteUnmuteBtn.toolTip = ResourceUtil.getInstance().getString('bbb.video.controls.muteButton.toolTip', [UsersUtil.getUserName(sharerUserID)]); - } else { - muteUnmuteBtn.visible = false; - } - } - } - } - - private function showPrivateChatButton():void { - var chatOptions:ChatOptions = new ChatOptions(); - // the first check is to see if the chat module is in the config.xml - if (BBB.getConfigForModule("ChatModule") && chatOptions.privateEnabled && ! UsersUtil.isMe(sharerUserID)) { - privateChatBtn.toolTip = ResourceUtil.getInstance().getString('bbb.video.controls.privateChatBtn.toolTip', [UsersUtil.getUserName(sharerUserID)]); - privateChatBtn.visible = true; - } else { - privateChatBtn.visible = false; - privateChatBtn.includeInLayout = false; - } - } - ]]> - </mx:Script> - <mx:Button id="muteUnmuteBtn" visible="false" click="onMuteUnmuteClicked(event)" - width="{BUTTONS_SIZE}" height="{BUTTONS_SIZE}" styleName="videoUnmutedButtonStyle"/> - <mx:Button id="switchPresenter" visible="false" click="onSwitchPresenterClicked(event)" - width="{BUTTONS_SIZE}" height="{BUTTONS_SIZE}" styleName="videoSwitchPresenterButtonStyle" paddingTop="2"/> - <mx:Button id="ejectUserBtn" visible="false" click="onKickUserClicked(event)" - width="{BUTTONS_SIZE}" height="{BUTTONS_SIZE}" styleName="videoEjectUserButtonStyle" paddingTop="2"/> - <mx:Button id="privateChatBtn" click="onPrivateChatClicked(event)" - width="{BUTTONS_SIZE}" height="{BUTTONS_SIZE}" styleName="videoPrivateChatButtonStyle" paddingTop="2"/> -</mx:HBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/GraphicsWrapper.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/GraphicsWrapper.as new file mode 100644 index 0000000000000000000000000000000000000000..cf7b61d1fb07bc2a7a394c66db8b1327abbccb5e --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/GraphicsWrapper.as @@ -0,0 +1,428 @@ +package org.bigbluebutton.modules.videoconf.views +{ + import flash.display.DisplayObject; + import flash.net.NetConnection; + import flash.events.Event; + import flash.events.MouseEvent; + import mx.containers.Canvas; + import mx.core.UIComponent; + import mx.events.FlexEvent; + import mx.utils.ObjectUtil; + + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.model.VideoProfile; + import org.bigbluebutton.main.model.users.BBBUser; + import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; + import org.bigbluebutton.modules.videoconf.views.UserGraphicHolder; + import org.bigbluebutton.common.LogUtil; + + + public class GraphicsWrapper extends Canvas { + + private var _options:VideoConfOptions = new VideoConfOptions(); + private var priorityWeight:Number = _options.priorityRatio; + private var priorityMode:Boolean = false; + private var priorityItem:DisplayObject = null; + private var _minContentAspectRatio:Number=4/3; + + public function GraphicsWrapper() { + percentWidth = percentHeight = 100; + } + + override public function addChild(child:DisplayObject):DisplayObject { + throw("You should add the helper functions to add children to this Canvas: addAvatarFor, addVideoFor, addCameraFor"); + return null; + } + + private function minContentAspectRatio(except:Object = null):Number { + var result:Number = Number.MAX_VALUE; + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + if (item != except && item.contentAspectRatio < result) { + result = item.contentAspectRatio; + } + } + return result; + } + + private function calculateCellDimensions(canvasWidth:int, canvasHeight:int, numColumns:int, numRows:int):Object { + var obj:Object = { + width: Math.floor(canvasWidth / numColumns)-5, + height: Math.floor(canvasHeight / numRows)-5 + } + if (obj.width / obj.height > _minContentAspectRatio) { + obj.width = Math.floor(obj.height * _minContentAspectRatio); + } else { + obj.height = Math.floor(obj.width / _minContentAspectRatio); + } + return obj; + } + + private function calculateOccupiedArea(canvasWidth:int, canvasHeight:int, numColumns:int, numRows:int):Object { + var obj:Object = calculateCellDimensions(canvasWidth, canvasHeight, numColumns, numRows); + obj.occupiedArea = obj.width * obj.height * numChildren; + obj.numColumns = numColumns; + obj.numRows = numRows; + obj.cellAspectRatio = _minContentAspectRatio; + return obj; + } + + private function findBestConfiguration(canvasWidth:int, canvasHeight:int, numChildrenInCanvas:int):Object { + var bestConfiguration:Object = { + occupiedArea: 0 + } + + for (var numColumns:int = 1; numColumns <= numChildrenInCanvas; ++numColumns) { + var numRows:int = Math.ceil(numChildrenInCanvas / numColumns); + var currentConfiguration:Object = calculateOccupiedArea(canvasWidth, canvasHeight, numColumns, numRows); + if (currentConfiguration.occupiedArea > bestConfiguration.occupiedArea) { + bestConfiguration = currentConfiguration; + } + } + return bestConfiguration; + } + + private function updateDisplayListHelper(unscaledWidth:Number, unscaledHeight:Number):void { + if (numChildren == 0) { + return; + } + + var bestConfiguration:Object = findBestConfiguration(unscaledWidth, unscaledHeight, numChildren); + var numColumns:int = bestConfiguration.numColumns; + var numRows:int = bestConfiguration.numRows; + var cellWidth:int = bestConfiguration.width; + var cellHeight:int = bestConfiguration.height; + var cellAspectRatio:Number = bestConfiguration.cellAspectRatio; + + var blockX:int = Math.floor((unscaledWidth - cellWidth * numColumns) / 2); + var blockY:int = Math.floor((unscaledHeight - cellHeight * numRows) / 2); + var itemX:int, + itemY:int, + itemWidth:int, + itemHeight:int; + + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + var cellOffsetX:int = 0; + var cellOffsetY:int = 0; + if (item.contentAspectRatio > cellAspectRatio) { + itemWidth = cellWidth; + itemHeight = Math.floor(cellWidth / item.contentAspectRatio); + cellOffsetY = (cellHeight - itemHeight) / 2; + } else { + itemWidth = Math.floor(cellHeight * item.contentAspectRatio); + itemHeight = cellHeight; + cellOffsetX = (cellWidth - itemWidth) / 2; + } + itemX = (i % numColumns) * cellWidth + blockX + cellOffsetX; + itemY = Math.floor(i / numColumns) * cellHeight + blockY + cellOffsetY; + + item.setActualSize(itemWidth, itemHeight); + item.move(itemX, itemY); + } + } + + private function findPriorityConfiguration(unscaledWidth:Number, unscaledHeight:Number):Object{ + var pBestConf:Object = { + numRows: 0, + numColumns: 0, + width: 0, + height: 0 + }; + var oBestConf:Object = pBestConf; + var isVertSplit:Boolean = false; + if (numChildren > 1){ + var pBestConfVer:Object = findBestConfiguration(Math.floor(unscaledWidth * priorityWeight), unscaledHeight, 1); + var pBestConfHor:Object = findBestConfiguration(unscaledWidth, Math.floor(unscaledHeight * priorityWeight), 1); + isVertSplit = (pBestConfVer.occupiedArea > pBestConfHor.occupiedArea); + if (isVertSplit) { + pBestConf = pBestConfVer; + oBestConf = findBestConfiguration(unscaledWidth - pBestConf.width, unscaledHeight, numChildren-1); + } else { + pBestConf = pBestConfHor; + oBestConf = findBestConfiguration(unscaledWidth, unscaledHeight - pBestConf.height, numChildren-1); + } + } else { + pBestConf = findBestConfiguration(unscaledWidth,unscaledHeight,1); + } + return {isVertSplit: isVertSplit, priorConf: pBestConf, otherConf: oBestConf}; + } + + private function updateDisplayListHelperByPriority(unscaledWidth:Number, unscaledHeight:Number):void { + if (numChildren < 2) { + updateDisplayListHelper(unscaledWidth, unscaledHeight); + return; + } + + var bestConf:Object = findPriorityConfiguration(unscaledWidth, unscaledHeight); + var numColumns:int = bestConf.otherConf.numColumns; + var numRows:int = bestConf.otherConf.numRows; + var oWidth:int = bestConf.otherConf.width; + var oHeight:int = bestConf.otherConf.height; + var pWidth:int = bestConf.priorConf.width; + var pHeight:int = bestConf.priorConf.height; + + var blockX:int = 0; + var blockY:int = 0; + var cellOffsetX:int = 0; + var cellOffsetY:int = 0; + var itemX:int, + itemY:int, + itemWidth:int, + itemHeight:int; + + var item:UserGraphicHolder = priorityItem as UserGraphicHolder; + + // set size and position of the prioritized video + if (item.contentAspectRatio > _minContentAspectRatio) { + itemWidth = pWidth; + itemHeight = Math.floor(pWidth / item.contentAspectRatio); + } else { + itemHeight = pHeight; + itemWidth = Math.floor(pHeight * item.contentAspectRatio); + } + + if (bestConf.isVertSplit) { + blockX = Math.floor((3*(unscaledWidth - oWidth*numColumns) + itemWidth)/4); + blockY = Math.floor((unscaledHeight - oHeight*numRows)/2); + itemX = Math.floor((unscaledWidth - itemWidth - oWidth*numColumns)/2); + itemY = Math.floor((unscaledHeight - itemHeight)/2); + } else { + blockX = Math.floor((unscaledWidth - oWidth*numColumns)/2); + blockY = Math.floor((3*(unscaledHeight - oHeight*numRows) + itemHeight)/4); + itemX = Math.floor((unscaledWidth - itemWidth)/2); + itemY = Math.floor((unscaledHeight - itemHeight - oHeight*numRows)/2); + } + item.setActualSize(itemWidth, itemHeight); + item.move(itemX, itemY); + + // set size and position of the other videos + var nonPriorityIndex:int=0; + for (var curItemIndex:int = 0; curItemIndex < numChildren; ++curItemIndex) { + item = getChildAt(curItemIndex) as UserGraphicHolder; + if (item != priorityItem) { + + if (item.contentAspectRatio > _minContentAspectRatio) { + itemWidth = oWidth; + itemHeight = Math.floor(oWidth / item.contentAspectRatio); + } else { + itemHeight = oHeight; + itemWidth = Math.floor(oHeight * item.contentAspectRatio); + } + cellOffsetX = (oWidth - itemWidth)/2; + cellOffsetY = (oHeight - itemHeight)/2; + + itemX = (nonPriorityIndex % numColumns) * oWidth + blockX + cellOffsetX; + itemY = Math.floor(nonPriorityIndex / numColumns) * oHeight + blockY + cellOffsetY; + nonPriorityIndex++; + + item.setActualSize(itemWidth, itemHeight); + item.move(itemX, itemY); + } + } + } + + override protected function updateDisplayList(w:Number, h:Number):void { + trace("[GraphicsWrapper::updateDisplayList]"); + super.updateDisplayList(w, h); + + if (priorityMode) { + updateDisplayListHelperByPriority(w, h); + } else { + updateDisplayListHelper(w, h); + } + } + +/* + override public function validateDisplayList():void { + super.validateDisplayList(); + + if (priorityMode) { + updateDisplayListHelperByPriority(this.width, this.height); + } else { + updateDisplayListHelper(this.width, this.height); + } + } +*/ + public function addAvatarFor(userId:String):void { + if (! UsersUtil.hasUser(userId)) return; + + var graphic:UserGraphicHolder = new UserGraphicHolder(); + graphic.userId = userId; + graphic.addEventListener(FlexEvent.CREATION_COMPLETE, function(event:FlexEvent):void { + graphic.loadAvatar(_options); + onChildAdd(event); + }); + graphic.addEventListener(MouseEvent.CLICK, onVBoxClick); + graphic.addEventListener(FlexEvent.REMOVE, onChildRemove); + + super.addChild(graphic); + } + + private function hasVideo(userId:String, streamName:String):Boolean { + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + if (item.user && item.user.userID == userId && item.visibleComponent is UserVideo && item.video.streamName == streamName) { + return true; + } + } + return false; + } + + public function addVideoFor(userId:String, connection:NetConnection, streamName:String):void { + // Check if video window is already open + if(hasVideo(userId, streamName)) { return; } + trace("[GraphicsWrapper:addVideoForHelper] streamName " + streamName); + var graphic:UserGraphicHolder = new UserGraphicHolder(); + graphic.userId = userId; + graphic.addEventListener(FlexEvent.CREATION_COMPLETE, function(event:FlexEvent):void { + graphic.loadVideo(_options, connection, streamName); + onChildAdd(event); + }); + graphic.addEventListener(MouseEvent.CLICK, onVBoxClick); + graphic.addEventListener(FlexEvent.REMOVE, onChildRemove); + + super.addChild(graphic); + } + + private function addCameraForHelper(userId:String, camIndex:int, videoProfile:VideoProfile, chromePermissionDenied:Boolean):void { + var graphic:UserGraphicHolder = new UserGraphicHolder(); + graphic.userId = userId; + graphic.addEventListener(FlexEvent.CREATION_COMPLETE, function(event:FlexEvent):void { + graphic.loadCamera(_options, camIndex, videoProfile, chromePermissionDenied); + onChildAdd(event); + }); + graphic.addEventListener(MouseEvent.CLICK, onVBoxClick); + graphic.addEventListener(FlexEvent.REMOVE, onChildRemove); + + super.addChild(graphic); + } + + private function onChildAdd(event:FlexEvent):void { + _minContentAspectRatio = minContentAspectRatio(); + invalidateDisplayList(); + } + + private function onChildRemove(event:FlexEvent):void { + if (priorityMode && event.target == priorityItem) { + priorityMode = false; + priorityItem = null; + } + + _minContentAspectRatio = minContentAspectRatio(event.target); + invalidateDisplayList(); + } + + protected function onVBoxClick(event:MouseEvent):void { + var item:UserGraphicHolder = event.currentTarget as UserGraphicHolder; + // when the user clicks to close the video, the click event is fired but the window + // is no longer child of this class, so we need to test it first + if (this.contains(item)) { + priorityMode = !priorityMode || item != priorityItem; + if (priorityMode) { + priorityItem = item; + } + invalidateDisplayList(); + } + } + + public function addCameraFor(userId:String, camIndex:int, videoProfile:VideoProfile, chromeWebcamPermissionDenied:Boolean = false):void { + if (! UsersUtil.hasUser(userId)) return; + + var alreadyPublishing:Boolean = false; + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + if (item.user && item.user.userID == userId && item.visibleComponent is UserVideo && item.video.camIndex == camIndex) { + alreadyPublishing = true; + break; + } + } + + if (!alreadyPublishing) { + addCameraForHelper(userId, camIndex, videoProfile, chromeWebcamPermissionDenied); + } + } + + private function removeChildHelper(child:UserGraphicHolder):void { + child.shutdown(); + + if (contains(child)) { + removeChild(child); + } + } + + public function removeAvatarFor(userId:String):void { + trace("[GraphicsWrapper:removeAvatarFor] userId " + userId); + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + if (item.user && item.user.userID == userId && item.visibleComponent is UserAvatar) { + trace("[GraphicsWrapper:removeAvatarFor] removing graphic"); + removeChildHelper(item); + // recursive call to remove all avatars for userId + removeAvatarFor(userId); + break; + } + } + } + + public function removeVideoByCamIndex(userId:String, camIndex:int):String { + trace("[GraphicsWrapper:removeVideoByCamIndex] userId " + userId + " camIndex " + camIndex); + var streamName:String = ""; + + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + if (item.user && item.user.userID == userId && item.visibleComponent is UserVideo && item.video.camIndex == camIndex) { + streamName = item.video.streamName; + removeChildHelper(item); + break; + } + } + return streamName; + } + + public function removeVideoByStreamName(userId:String, streamName:String):int { + var camIndex:int = -1; + + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + if (item.user && item.user.userID == userId && item.visibleComponent is UserVideo && item.video.streamName == streamName) { + camIndex = item.video.camIndex; + removeChildHelper(item); + break; + } + } + return camIndex; + } + + public function removeGraphicsFor(userId:String):void { + trace("[GraphicsWrapper:removeGraphicsFor] userId " + userId); + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + if (item.user && item.user.userID == userId) { + trace("[GraphicsWrapper:removeGraphicsFor] removing graphic"); + removeChildHelper(item); + // recursive call to remove all graphics for userId + removeGraphicsFor(userId); + break; + } + } + } + + public function hasGraphicsFor(userId:String):Boolean { + for (var i:int = 0; i < numChildren; ++i) { + var item:UserGraphicHolder = getChildAt(i) as UserGraphicHolder; + if (item.user && item.user.userID == userId) { + return true; + } + } + return false; + } + + public function shutdown():void { + while (numChildren > 0) { + var item:UserGraphicHolder = getChildAt(0) as UserGraphicHolder; + removeChildHelper(item); + } + } + } +} diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/PublishWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/PublishWindow.mxml deleted file mode 100755 index e697a7fd98aacad311e42a4a0dd3a419bc8cbefc..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/PublishWindow.mxml +++ /dev/null @@ -1,488 +0,0 @@ -<?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/>. - ---> - -<pubVid:VideoWindowItf - xmlns:mx="http://www.adobe.com/2006/mxml" - xmlns:pubVid="org.bigbluebutton.modules.videoconf.business.*" - initialize="init()" - implements="org.bigbluebutton.common.IBbbModuleWindow" - styleNameFocus="videoPublishStyleFocus" - styleNameNoFocus="videoPublishStyleNoFocus" - creationComplete="onCreationComplete()" - width="{defaultWidth + 6}" height="{defaultHeight + 6}" - xmlns:mate="http://mate.asfusion.com/" - resize="onResize()" - horizontalScrollPolicy="off" - verticalScrollPolicy="off" - layout="absolute"> - - <mate:Listener type="{BBBEvent.USER_VOICE_MUTED}" method="handleUserVoiceMutedEvent" /> - <mate:Listener type="{EventConstants.USER_TALKING}" method="handleUserTalkingEvent" /> - <mate:Listener type="{SwitchedPresenterEvent.SWITCHED_PRESENTER}" method="handleSwitchedPresenterEvent" /> - <mate:Listener type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" method="handleMadePresenterEvent" /> - <mate:Listener type="{BBBEvent.USER_VOICE_JOINED}" method="handleNewRoleEvent" /> - <mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleNewRoleEvent" /> - <mate:Listener type="{CloseAllWindowsEvent.CLOSE_ALL_WINDOWS}" method="closeWindow" /> - <mate:Listener type="{ShortcutEvent.REMOTE_FOCUS_WEBCAM}" method="remoteFocus" /> - - <mx:Script> - <![CDATA[ - import com.asfusion.mate.events.Dispatcher; - - import flexlib.mdi.events.MDIWindowEvent; - - import mx.core.UIComponent; - import mx.events.ResizeEvent; - - import org.bigbluebutton.common.Images; - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.common.Role; - import org.bigbluebutton.common.events.CloseWindowEvent; - import org.bigbluebutton.common.events.LocaleChangeEvent; - import org.bigbluebutton.common.events.ToolbarButtonEvent; - import org.bigbluebutton.core.EventConstants; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.events.CoreEvent; - import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.main.events.MadePresenterEvent; - import org.bigbluebutton.main.events.ShortcutEvent; - import org.bigbluebutton.main.events.SwitchedPresenterEvent; - import org.bigbluebutton.main.views.MainCanvas; - import org.bigbluebutton.modules.videoconf.business.TalkingButtonOverlay; - import org.bigbluebutton.modules.videoconf.events.CloseAllWindowsEvent; - import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; - import org.bigbluebutton.modules.videoconf.events.OpenVideoWindowEvent; - import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; - import org.bigbluebutton.modules.videoconf.events.StopBroadcastEvent; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; - import org.bigbluebutton.util.i18n.ResourceUtil; - - [Bindable] private var defaultWidth:Number = 320; - [Bindable] private var defaultHeight:Number = 240; - - - public var camIndex:int = 0; - private var camWidth:Number = 320; - private var camHeight:Number = 240; - private var _camera:Camera = null; - public var quality:Number = 0; - public var chromePermissionDenied:Boolean = false; - - // Timer to auto-publish webcam. We need this timer to delay - // the auto-publishing until after the Viewers's window has loaded - // to receive the publishing events. Otherwise, the user joining next - // won't be able to view the webcam. - private var autoPublishTimer:Timer = null; - - // Timer used to enable the start publishing button, only after get - // any activity on the camera. It avoids the problem of publishing - // a blank video - private var _activationTimer:Timer = null; - private var _waitingForActivation:Boolean = false; - - static private var _cameraAccessDenied:Boolean = false; - - [Bindable] public var videoOptions:VideoConfOptions; - [Bindable] public var baseIndex:int; - - [Bindable] public var glowColor:String = ""; - [Bindable] public var glowBlurSize:Number = 0; - - private function init():void{ - videoOptions = new VideoConfOptions(); - baseIndex = videoOptions.baseTabIndex; - } - - private var windowType:String = "PublishWindowType"; - - override public function getWindowType():String { - return windowType; - } - - private function onCreationComplete():void{ - this.glowColor = videoOptions.glowColor; - this.glowBlurSize = videoOptions.glowBlurSize; - - _videoHolder = new UIComponent(); - _videoHolder.width = camWidth; - _videoHolder.height = camHeight; - this.addChild(_videoHolder); - - this.minWidth = _minWidth; - this.minHeight = _minHeight; - maximizeRestoreBtn.visible = false; - this.resizable = false; - - this.visible = videoOptions.publishWindowVisible; - this.showCloseButton = videoOptions.showCloseButton; - - if (videoOptions.autoStart) { - /* - * Need to have a timer to trigger auto-publishing of webcam. - */ - autoPublishTimer = new Timer(3000, 1); - autoPublishTimer.addEventListener(TimerEvent.TIMER, autopublishTimerHandler); - autoPublishTimer.start(); - } - - - titleBarOverlay.tabIndex = baseIndex; - titleBarOverlay.accessibilityName = ResourceUtil.getInstance().getString('bbb.video.publish.titleBar'); - closeBtn.focusEnabled = true; - closeBtn.tabIndex = baseIndex+3; - - addEventListener(MDIWindowEvent.RESIZE_START, onResizeStart); - addEventListener(MDIWindowEvent.RESIZE_END, onResizeEnd); - addEventListener(MouseEvent.MOUSE_OVER, showButtons); - addEventListener(MouseEvent.MOUSE_OUT, hideButtons); - addEventListener(MouseEvent.DOUBLE_CLICK, onDoubleClick); - - // start the camera preview - updateCamera(); - } - - private function remoteFocus(e:ShortcutEvent):void{ - focusManager.setFocus(closeBtn); - } - - private function autopublishTimerHandler(event:TimerEvent):void { - startPublishing(); - autoPublishTimer.stop(); - } - - private function handleMadePresenterEvent(event:MadePresenterEvent):void { - trace("******** PublishWindow: HandleMadePresenter event *********"); - updateControlButtons(); - } - - private function handleSwitchedPresenterEvent(event:SwitchedPresenterEvent):void { - trace("******** PublishWindow: handleSwitchedPresenterEvent event *********"); - updateControlButtons(); - } - - private function handleNewRoleEvent(event:Event):void { - updateControlButtons(); - } - - private function handleUserVoiceMutedEvent(event:BBBEvent):void { - if (event.payload.userID == userID) { - userMuted(event.payload.muted); - } - } - - private function handleUserTalkingEvent(event:CoreEvent):void { - if (event.message.userID == userID) { - if (event.message.talking) { - notTalkingEffect.end(); - talkingEffect.play([this]); - simulateClick(); - } else { - talkingEffect.end(); - notTalkingEffect.play([this]); - } - } - } - - private function updateCamera():void { - stopCamera(); - - if (Camera.names.length == 0) { - showWarning('bbb.video.publish.hint.noCamera'); - return; - } - - _camera = Camera.getCamera(); - if (_camera == null) { - showWarning('bbb.video.publish.hint.cantOpenCamera'); - return; - } - - _camera.setMotionLevel(5, 1000); - - if (_camera.muted) { - if (_cameraAccessDenied) { - onCameraAccessDisallowed(); - return; - } else { - showWarning('bbb.video.publish.hint.waitingApproval'); - } - } else { - // if the camera isn't muted, that is because the user has - // previously allowed the camera capture on the flash privacy box - onCameraAccessAllowed(); - } - - _camera.addEventListener(ActivityEvent.ACTIVITY, onActivityEvent); - _camera.addEventListener(StatusEvent.STATUS, onStatusEvent); - - _camera.setKeyFrameInterval(videoOptions.camKeyFrameInterval); - _camera.setMode(camWidth, camHeight, videoOptions.camModeFps); - _camera.setQuality(videoOptions.camQualityBandwidth, videoOptions.camQualityPicture); - - if (_camera.width != camWidth || _camera.height != camHeight) { - trace("Resolution " + camWidth + "x" + camHeight + " is not supported, using " + _camera.width + "x" + _camera.height + " instead"); - setResolution(_camera.width, _camera.height); - } - - _video = new Video(); - _video.attachCamera(_camera); - _video.smoothing = true; - - if (aspectRatio > _videoHolder.width / _videoHolder.height) { - _video.width = _videoHolder.width; - _video.height = _videoHolder.width / aspectRatio; - _video.x = 0; - _video.y = (_videoHolder.height - _video.height) / 2; - } else { - _video.width = _videoHolder.height * aspectRatio; - _video.height = _videoHolder.height; - _video.x = (_videoHolder.width - _video.width) / 2; - _video.y = 0; - } - - _videoHolder.addChild(_video); - } - - private function onActivityEvent(e:ActivityEvent):void { - if (_waitingForActivation && e.activating) { - trace("Cam activity event: waitingForActivation = [" + _waitingForActivation + "] activating = [" + e.activating + "]"); - _activationTimer.stop(); - showWarning('bbb.video.publish.hint.videoPreview', false, "0xFFFF00"); - _waitingForActivation = false; - - trace("Starting auto-publisher timer."); - autoPublishTimer = new Timer(3000, 1); - autoPublishTimer.addEventListener(TimerEvent.TIMER, autopublishTimerHandler); - autoPublishTimer.start(); - } - } - - private function onStatusEvent(e:StatusEvent):void { - if (e.code == "Camera.Unmuted") { - onCameraAccessAllowed(); - // this is just to overwrite the message of waiting for approval - showWarning('bbb.video.publish.hint.openingCamera'); - } else if (e.code == "Camera.Muted") { - onCameraAccessDisallowed(); - } - } - - private function onCameraAccessAllowed():void { - // set timer to ensure that the camera activates. If not, it might be in use by another application - _waitingForActivation = true; - if (_activationTimer != null) { - _activationTimer.stop(); - } - - _activationTimer = new Timer(10000, 1); - _activationTimer.addEventListener(TimerEvent.TIMER, activationTimeout); - _activationTimer.start(); - } - - private function onCameraAccessDisallowed():void { - showWarning('bbb.video.publish.hint.cameraDenied'); - _cameraAccessDenied = true; - } - - private function activationTimeout(e:TimerEvent):void { - if(chromePermissionDenied) - { - showWarning('bbb.video.publish.hint.cameraDenied'); - return; - } - else - { - showWarning('bbb.video.publish.hint.cameraIsBeingUsed'); - } - // it will try to reopen the camera after the timeout - updateCamera(); - } - - private function startPublishing():void{ - if (_camera == null) return; - - if (autoPublishTimer != null) { - autoPublishTimer.stop(); - } - - showWarning('bbb.video.publish.hint.publishing', true, "0xFFFF00"); - - var e:StartBroadcastEvent = new StartBroadcastEvent(); - e.stream = this.streamName; - e.camera = _camera; - dispatchEvent(e); - - // Store the userid for the publisher. This allows us to control - // the user's status from the video window - userID = UsersUtil.getMyUserID(); - - createButtons(); - addControlButtons(); - - } - - private var _isClosing:Boolean = false; - - override public function close(event:MouseEvent=null):void{ - trace("PublishWindow:: closing"); - - if (!_isClosing) { - _isClosing = true; - stopPublishing(); - trace("Dispatching ClosePublishWindowEvent event"); - var gDispatcher:Dispatcher = new Dispatcher(); - gDispatcher.dispatchEvent(new ClosePublishWindowEvent()); - } - } - - private function stopCamera():void { - trace("PublishWindow:: stopping camera"); - - _camera = null; - if (_video != null) { - trace("PublishWindow:: removing video from display"); - _videoHolder.removeChild(_video); - - _video.attachCamera(null); - _video.clear(); - _video = null; - } - - } - - private function stopPublishing():void{ - trace("PublishWindow::stopPublishing"); - stopCamera(); - var e:StopBroadcastEvent = new StopBroadcastEvent() - e.stream = streamName; - dispatchEvent(e); - } - - override protected function onResize():void { - - super.onResize(); - - if (_videoHolder != null) { - - fitVideoToWindow(); - - _video.x = (this.width - PADDING_HORIZONTAL - _video.width) / 2; - _video.y = (this.height - PADDING_VERTICAL - _video.height) / 2; - } - - } - - private function fitVideoToWindow():void { - if (_videoHolder.width - PADDING_HORIZONTAL < _videoHolder.height - PADDING_VERTICAL) { - fitToHeightAndAdjustWidthToMaintainAspectRatio(); - } else { - fitToWidthAndAdjustHeightToMaintainAspectRatio(); - } - } - - private function videoIsSmallerThanWindow():Boolean { - return (camWidth < _videoHolder.width - PADDING_HORIZONTAL) && (camHeight < _videoHolder.height - PADDING_VERTICAL); - } - - private function fitToWidthAndAdjustHeightToMaintainAspectRatio():void { - var aspect:Number = camHeight / camWidth; - _video.width = _videoHolder.width - PADDING_HORIZONTAL; - - // Maintain aspect-ratio - _video.height = _video.width * aspect; - } - - private function fitToHeightAndAdjustWidthToMaintainAspectRatio():void { - var aspect:Number = camWidth / camHeight; - _video.height = _videoHolder.height - PADDING_VERTICAL; - - // Maintain aspect-ratio - _video.width = _video.height * aspect; - } - - public function setResolution(width:int, height:int):void { - camWidth = width; - camHeight = height; - setAspectRatio(camWidth, camHeight); - - /** - * Add timestamp to create a unique stream name. This way we can record - * stream without overwriting previously recorded streams. - */ - var d:Date = new Date(); - var curTime:Number = d.getTime(); - var uid:String = UserManager.getInstance().getConference().getMyUserId(); - var res:String = camWidth + "x" + camHeight; - this.streamName = res.concat("-" + uid) + "-" + curTime; - } - - private function isPresenter():Boolean{ - if (UsersUtil.amIModerator() || UsersUtil.amIPresenter()) return true; - else return false; - } - - private function closeWindow(e:CloseAllWindowsEvent):void{ - trace("PublishWindow::closeWindow"); - stopCamera(); - } - - private var hideWarningTimer:Timer = null; - - private function showWarning(resourceName:String, autoHide:Boolean=false, color:String="0xFF0000"):void { - const text:String = ResourceUtil.getInstance().getString(resourceName); - - if (hideWarningTimer != null) - hideWarningTimer.stop(); - if (autoHide) { - hideWarningTimer = new Timer(3000, 1); - hideWarningTimer.addEventListener(TimerEvent.TIMER, hideWarning); - hideWarningTimer.start(); - } - // bring the label to front - setChildIndex(lblWarning, getChildren().length - 1); - lblWarning.text = text; - lblWarning.setStyle("color", color); - lblWarning.visible = true; - LogUtil.debug("Showing warning: " + text); - } - - private function hideWarning(e:TimerEvent):void { - lblWarning.visible = false; - } - - ]]> - </mx:Script> - - <mx:Glow id="talkingEffect" duration="500" alphaFrom="1.0" alphaTo="0.3" - blurXFrom="0.0" blurXTo="{glowBlurSize}" blurYFrom="0.0" blurYTo="{glowBlurSize}" color="{glowColor}"/> - <mx:Glow id="notTalkingEffect" duration="500" alphaFrom="0.3" alphaTo="1.0" - blurXFrom="{glowBlurSize}" blurXTo="0.0" blurYFrom="{glowBlurSize}" blurYTo="0.0" color="{glowColor}"/> - - <mx:Fade id="dissolveOut" duration="1000" alphaFrom="1.0" alphaTo="0.0"/> - - <mx:Fade id="dissolveIn" duration="1000" alphaFrom="0.0" alphaTo="1.0"/> - <mx:Text id="lblWarning" width="100%" textAlign="center" fontSize="14" fontWeight="bold" y="{this.height - lblWarning.height - 30}" - visible="false" selectable="false" hideEffect="{dissolveOut}" showEffect="{dissolveIn}"/> - - <mate:Listener type="{CloseAllWindowsEvent.CLOSE_ALL_WINDOWS}" method="closeWindow" /> -</pubVid:VideoWindowItf> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarButton.mxml deleted file mode 100755 index f3d617fa3eea7577d527513c6f57cbab461193c7..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarButton.mxml +++ /dev/null @@ -1,163 +0,0 @@ -<?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/>. - ---> - -<mx:Button xmlns:mx="http://www.adobe.com/2006/mxml" styleName="webcamDefaultButtonStyle" - xmlns:mate="http://mate.asfusion.com/" - click="openPublishWindow()" creationComplete="init()" - mouseOver = "mouseOverHandler(event)" - mouseOut = "mouseOutHandler(event)" - height="24" - toolTip="{this.selected ? ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip.stop') : ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip.start')}" - visible="false" - implements="org.bigbluebutton.common.IBbbToolbarComponent"> - - <mate:Listener type="{ShortcutEvent.SHARE_WEBCAM}" method="remoteClick" /> - <mate:Listener type="{BBBEvent.CAM_SETTINGS_CLOSED}" method="handleCamSettingsClosedEvent"/> - <mate:Listener type="{ShareCameraRequestEvent.SHARE_CAMERA_REQUEST}" receive="enabled=false" /> - <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="lockSettingsChanged" /> - - <mx:Script> - <![CDATA[ - import com.asfusion.mate.events.Dispatcher; - - import org.bigbluebutton.common.Images; - import org.bigbluebutton.core.UsersUtil; - import org.bigbluebutton.core.events.LockControlEvent; - import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.main.events.ShortcutEvent; - import org.bigbluebutton.main.model.users.BBBUser; - import org.bigbluebutton.main.model.users.Conference; - import org.bigbluebutton.main.views.MainToolbar; - import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; - import org.bigbluebutton.modules.videoconf.events.ShareCameraRequestEvent; - import org.bigbluebutton.util.i18n.ResourceUtil; - - - public const OFF_STATE:Number = 0; - public const ON_STATE:Number = 1; - - public const STOP_PUBLISHING:Number = 0; - public const START_PUBLISHING:Number = 1; - - private var _currentState:Number = OFF_STATE; - - - private var dispatcher:Dispatcher; - private var _this:Button ; - - public function lockSettingsChanged(e:*):void{ - var userManager:UserManager = UserManager.getInstance(); - var conference:Conference = userManager.getConference(); - var me:BBBUser = conference.getMyUser(); - - _this.visible = !me.disableMyCam; - _this.includeInLayout = !me.disableMyCam; - } - - private function init():void{ - _this = this; - dispatcher = new Dispatcher(); - this.toolTip = ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip.start'); - this.styleName = "webcamDefaultButtonStyle"; - this.enabled = true; - this.selected = false; - _currentState = OFF_STATE; - lockSettingsChanged(null); - } - - public function remoteClick(e:ShortcutEvent):void{ - openPublishWindow(); - dispatchEvent(new ShortcutEvent(ShortcutEvent.REMOTE_FOCUS_WEBCAM)); - } - - public function publishingStatus(status:Number):void { - if(status == START_PUBLISHING) { - _currentState = ON_STATE; - //this.toolTip = ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip.stop'); - this.styleName = "webcamOnButtonStyle"; - this.enabled = true; - this.selected = true; - } - else { - _currentState = OFF_STATE; - this.styleName = "webcamDefaultButtonStyle"; - this.enabled = true; - this.selected = false; - } - } - - public function set isPresenter (presenter:Boolean):void{ - //Nothing - } - - private function openPublishWindow():void{ - if(_currentState == ON_STATE) { - trace("Close window"); - _currentState = OFF_STATE; - this.styleName = "webcamDefaultButtonStyle"; - this.selected = false; - dispatchEvent(new ClosePublishWindowEvent()); - } else { - trace("Share camera"); - _currentState = ON_STATE; - this.styleName = "webcamOnButtonStyle"; - this.selected = true; - dispatchEvent(new ShareCameraRequestEvent()); - } - } - - private function handleCamSettingsClosedEvent(e:BBBEvent):void { - this.setFocus(); - if(e.payload['clicked'] == "cancel"){ - this.enabled = true; - this.selected = false; - _currentState = OFF_STATE; - this.styleName = "webcamDefaultButtonStyle"; - } - } - - private function mouseOverHandler(event:MouseEvent):void { - if(_currentState == ON_STATE) - this.styleName = "webcamOffButtonStyle"; - else - this.styleName = "webcamOnButtonStyle"; - } - - private function mouseOutHandler(event:MouseEvent):void { - if(_currentState == ON_STATE) - this.styleName = "webcamOnButtonStyle"; - else - this.styleName = "webcamDefaultButtonStyle"; - } - - - public function getAlignment():String{ - return MainToolbar.ALIGN_LEFT; - } - - public function theory():String{ - return "Webcam button"; - } - ]]> - </mx:Script> -</mx:Button> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml new file mode 100755 index 0000000000000000000000000000000000000000..730b84678c1506b7e883802ad9bf6dfd5a20742c --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/ToolbarPopupButton.mxml @@ -0,0 +1,273 @@ +<?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/>. + +--> + +<mx:PopUpButton xmlns:mx="http://www.adobe.com/2006/mxml" styleName="webcamDefaultButtonStyle" + xmlns:mate="http://mate.asfusion.com/" + click="openPublishWindow()" initialize="init()" + mouseOver = "mouseOverHandler(event)" + mouseOut = "mouseOutHandler(event)" + height="24" + toolTip="{this.selected ? ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip.stop') : ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip.start')}" + visible="false" + enabled="true" + selected="false" + implements="org.bigbluebutton.common.IBbbToolbarComponent"> + + <mate:Listener type="{ShortcutEvent.SHARE_WEBCAM}" method="remoteClick" /> + <mate:Listener type="{BBBEvent.CAM_SETTINGS_CLOSED}" method="handleCamSettingsClosedEvent"/> + <mate:Listener type="{ShareCameraRequestEvent.SHARE_CAMERA_REQUEST}" receive="enabled=false" /> + <mate:Listener type="{LockControlEvent.CHANGED_LOCK_SETTINGS}" method="lockSettingsChanged" /> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="refreshRole" /> + + <mx:Script> + <![CDATA[ + import com.asfusion.mate.events.Dispatcher; + + import mx.controls.Menu; + import mx.events.MenuEvent; + import mx.styles.StyleManager; + import mx.styles.IStyleManager2; + + import org.bigbluebutton.common.Images; + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.events.LockControlEvent; + import org.bigbluebutton.core.managers.UserManager; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.events.ShortcutEvent; + import org.bigbluebutton.main.model.users.BBBUser; + import org.bigbluebutton.main.model.users.Conference; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; + import org.bigbluebutton.main.views.MainToolbar; + import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; + import org.bigbluebutton.modules.videoconf.events.ShareCameraRequestEvent; + import org.bigbluebutton.modules.videoconf.events.StopBroadcastEvent; + import org.bigbluebutton.modules.videoconf.events.StopShareCameraRequestEvent; + import org.bigbluebutton.util.i18n.ResourceUtil; + + public const OFF_STATE:Number = 0; + public const ON_STATE:Number = 1; + + public const STOP_PUBLISHING:Number = 0; + public const START_PUBLISHING:Number = 1; + + private var _currentState:Number = OFF_STATE; + + private var dp:Object = []; + private var dataMenu:Menu; + public var numberOfCamerasOff:int = 0; + + private var dispatcher:Dispatcher; + + public function lockSettingsChanged(e:*):void{ + if (UsersUtil.amIModerator() || UsersUtil.amIPresenter()){ + // Ignore lock setting changes as + // or presenter we are moderator. + return; + } + + updateButton(); + } + + private function refreshRole(e:ChangeMyRole):void { + updateButton(); + } + + private function updateButton():void { + var userManager:UserManager = UserManager.getInstance(); + var conference:Conference = userManager.getConference(); + var me:BBBUser = conference.getMyUser(); + + this.visible = me.moderator || me.presenter || !me.disableMyCam; + this.includeInLayout = me.moderator || me.presenter || !me.disableMyCam; + } + + private function init():void{ + dispatcher = new Dispatcher(); + numberOfCamerasOff = Camera.names.length; + for(var i:int = 0; i < Camera.names.length; i++) { + dp.push({label: Camera.names[i], status: OFF_STATE}); + } + + dataMenu = Menu.createMenu(this, dp, false); + dataMenu.addEventListener("itemClick", changeHandler); + dataMenu.addEventListener("mouseOver", mouseOverHandler); + dataMenu.addEventListener("mouseOut", mouseOutHandler); + dataMenu.iconFunction = webcamMenuIcon; + + this.popUp = dataMenu; + switchStateToNormal(); + } + + private function webcamMenuIcon(item:Object):Class { + var styleManager:IStyleManager2 = StyleManager.getStyleManager(null); + if(item.status == ON_STATE) { + return styleManager.getStyleDeclaration(".webcamOnButtonStyle").getStyle("icon"); + } + else { + return styleManager.getStyleDeclaration(".webcamDefaultButtonStyle").getStyle("icon"); + } + } + + private function switchStateToNormal():void { + this.toolTip = ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip.start'); + this.styleName = "webcamDefaultButtonStyle"; + this.enabled = true; + this.selected = false; + _currentState = OFF_STATE; + lockSettingsChanged(null); + } + + public function set isPresenter(presenter:Boolean):void { + visible = includeInLayout = presenter; + } + + public function remoteClick(e:ShortcutEvent):void{ + openPublishWindow(); + dispatchEvent(new ShortcutEvent(ShortcutEvent.REMOTE_FOCUS_WEBCAM)); + } + + public function publishingStatus(status:Number, camID:Number = -1):void { + if(status == START_PUBLISHING) { + _currentState = ON_STATE; + //this.toolTip = ResourceUtil.getInstance().getString('bbb.toolbar.video.toolTip.stop'); + this.styleName = "webcamOnButtonStyle"; + this.enabled = true; + this.selected = true; + } + else { + if(camID != -1) { + dp[camID].status = OFF_STATE; + if(numberOfCamerasOff < Camera.names.length) + numberOfCamerasOff++; + + } + if(numberOfCamerasOff == Camera.names.length) { + switchStateToNormal(); + } + } + var evt:BBBEvent = new BBBEvent("EnableToolbarPopupButton"); + dispatchEvent(evt); + + dataMenu.dataProvider = dp; + } + + + private function openPublishWindow():void{ + this.enabled = false; + if(_currentState == ON_STATE) { + trace("[ToolbarPopupButton:openPublishWindow] Close window"); + switchStateToNormal(); + var stopShareCameraRequestEvent:StopShareCameraRequestEvent = new StopShareCameraRequestEvent(StopShareCameraRequestEvent.STOP_SHARE_ALL_CAMERA_REQUEST); + dispatchEvent(stopShareCameraRequestEvent); + this.enabled = true; + } else { + trace("Share camera"); + if(numberOfCamerasOff > 0) + numberOfCamerasOff--; + _currentState = ON_STATE; + this.styleName = "webcamOnButtonStyle"; + this.selected = true; + var shareCameraRequestEvent:ShareCameraRequestEvent = new ShareCameraRequestEvent(); + shareCameraRequestEvent.camerasArray = dp; + dispatchEvent(shareCameraRequestEvent); + } + } + + private function handleCamSettingsClosedEvent(e:BBBEvent):void { + this.setFocus(); + this.enabled = true; + if(e.payload['clicked'] == "cancel") { + if(numberOfCamerasOff < Camera.names.length) + numberOfCamerasOff++; + if(numberOfCamerasOff == Camera.names.length) { + switchStateToNormal(); + } + } + } + + private function mouseOverHandler(event:MouseEvent):void { + if(_currentState == ON_STATE) + this.styleName = "webcamOffButtonStyle"; + else + this.styleName = "webcamOnButtonStyle"; + this.selected = false; + } + + private function mouseOutHandler(event:MouseEvent):void { + if(_currentState == ON_STATE) + this.styleName = "webcamOnButtonStyle"; + else + this.styleName = "webcamDefaultButtonStyle"; + this.selected = false; + } + + + public function getAlignment():String{ + return MainToolbar.ALIGN_LEFT; + } + + public function theory():String{ + return "Webcam button"; + } + + private function changeHandler(event:MenuEvent):void { + if(dp[event.index].status == ON_STATE) { + var stopShareCameraRequestEvent:StopShareCameraRequestEvent = new StopShareCameraRequestEvent(); + stopShareCameraRequestEvent.camId = event.index; + dispatchEvent(stopShareCameraRequestEvent); + } + else { + this.enabled = false; + if(numberOfCamerasOff > 0) + numberOfCamerasOff--; + var shareCameraRequestEvent:ShareCameraRequestEvent = new ShareCameraRequestEvent(); + shareCameraRequestEvent.defaultCamera = String(event.index); + shareCameraRequestEvent.camerasArray = dp; + dispatchEvent(shareCameraRequestEvent); + } + } + + public function setCamAsInactive(camIndex:int):void { + if(camIndex != -1) { + dp[camIndex].status = OFF_STATE; + dataMenu.dataProvider = dp; + } + } + + public function setAllCamAsInactive():void { + numberOfCamerasOff = Camera.names.length; + for(var i:int = 0; i < Camera.names.length; i++) { + setCamAsInactive(i); + } + switchStateToNormal(); + } + + public function setCamAsActive(camIndex:int):void { + if(camIndex != -1) { + dp[camIndex].status = ON_STATE; + dataMenu.dataProvider = dp; + mouseOverHandler(null); + } + } + ]]> + </mx:Script> +</mx:PopUpButton> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserAvatar.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserAvatar.as new file mode 100644 index 0000000000000000000000000000000000000000..4a88651d8ddabd76ab4fb44fe65a5afa52a0c5df --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserAvatar.as @@ -0,0 +1,42 @@ +package org.bigbluebutton.modules.videoconf.views +{ + import flash.display.Loader; + import flash.events.Event; + import flash.net.URLRequest; + import mx.events.FlexEvent; + import mx.utils.ObjectUtil; + + public class UserAvatar extends UserGraphic { + + private var _imageLoader:Loader = null; + private var _completed:Boolean; + + public function UserAvatar() { + super(); + } + + public function load(avatarUrl:String):void { + _imageLoader = new Loader; + _completed = false; + + _imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadingComplete); + addChild(_imageLoader); + var request:URLRequest = new URLRequest(avatarUrl); + _imageLoader.load(request); + } + + private function onLoadingComplete(event:Event):void { + _completed = true; + setOriginalDimensions(_imageLoader.width, _imageLoader.height); + } + + override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { + super.updateDisplayList(unscaledWidth, unscaledHeight); + + if (_completed) { + updateDisplayListHelper(unscaledWidth, unscaledHeight, _imageLoader); + } + } + + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserGraphic.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserGraphic.as new file mode 100644 index 0000000000000000000000000000000000000000..7cc1ba3eed473dc673ae2e7b738f1e8cbeed43b9 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserGraphic.as @@ -0,0 +1,96 @@ +package org.bigbluebutton.modules.videoconf.views +{ + import flash.display.DisplayObject; + import mx.containers.Canvas; + import mx.core.UIComponent; + + import org.bigbluebutton.main.model.users.BBBUser; + import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; + + public class UserGraphic extends UIComponent { + protected var _user:BBBUser = null; + protected var _options:VideoConfOptions; + protected var _origWidth:Number = 320; + protected var _origHeight:Number = 240; + protected var _background:Canvas; + + protected const BORDER_THICKNESS:int = 0; + + public function UserGraphic() { + super(); + + _background = new Canvas(); + _background.setStyle("backgroundColor", "white"); + _background.setStyle("borderStyle", "solid"); + _background.setStyle("borderColor", "#000000"); + _background.setStyle("borderThickness", BORDER_THICKNESS); + _background.horizontalScrollPolicy = "off"; + _background.verticalScrollPolicy = "off"; + + addChild(_background); + } + + override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { + super.updateDisplayList(unscaledWidth, unscaledHeight); + + _background.setActualSize(unscaledWidth, unscaledHeight); + } + + protected function setOriginalDimensions(width:Number, height:Number):void { + _origWidth = width; + _origHeight = height; + invalidateDisplayList(); + } + + public function get aspectRatio():Number { + return _origWidth / _origHeight; + } + + protected function updateDisplayListHelper(unscaledWidth:Number, unscaledHeight:Number, object:DisplayObject):void { + if (object == null) { + return; + } + + var object_x:Number; + var object_y:Number; + var object_width:Number; + var object_height:Number; + + unscaledHeight -= BORDER_THICKNESS * 2; + unscaledWidth -= BORDER_THICKNESS * 2; + + if (unscaledWidth / unscaledHeight > aspectRatio) { + object_height = unscaledHeight; + object_width = Math.ceil(unscaledHeight * aspectRatio); + object_y = BORDER_THICKNESS; + object_x = Math.floor((unscaledWidth - object.width) / 2); + } else { + object_width = unscaledWidth; + object_height = Math.ceil(unscaledWidth / aspectRatio); + object_x = BORDER_THICKNESS; + object_y = Math.floor((unscaledHeight - object.height) / 2); + } + + object.x = object_x; + object.y = object_y; + object.width = object_width; + object.height = object_height; + } + + public function set user(value:BBBUser):void { + _user = value; + } + + public function get user():BBBUser { + return _user; + } + + public function set options(value:VideoConfOptions):void { + _options = value; + } + + public function get options():VideoConfOptions { + return _options; + } + } +} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserGraphicHolder.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserGraphicHolder.mxml new file mode 100644 index 0000000000000000000000000000000000000000..ac27179c94250081be08050efa9d196052d152ee --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserGraphicHolder.mxml @@ -0,0 +1,325 @@ +<?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/>. + +--> + +<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" + xmlns:mate="http://mate.asfusion.com/" + xmlns:views="org.bigbluebutton.modules.videoconf.views.*" + initialize="init()" + creationComplete="onCreationComplete()" + backgroundColor="white" width="320" height="240" + mouseOver="onCanvasMouseOver()" mouseOut="onCanvasMouseOut()" > + + <mate:Listener type="{EventConstants.USER_TALKING}" method="handleUserTalkingEvent" /> + <mate:Listener type="{PresenterStatusEvent.PRESENTER_NAME_CHANGE}" method="handlePresenterChangedEvent" /> + <mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleUserVoiceChangedEvent" /> + <mate:Listener type="{BBBEvent.USER_VOICE_MUTED}" method="handleUserVoiceChangedEvent" /> + + <mx:Script> + <![CDATA[ + + import com.asfusion.mate.events.Dispatcher; + + import org.bigbluebutton.common.Images; + import org.bigbluebutton.core.EventConstants; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.core.events.CoreEvent; + import org.bigbluebutton.core.events.VoiceConfEvent; + import org.bigbluebutton.core.UsersUtil; + import org.bigbluebutton.core.model.VideoProfile; + import org.bigbluebutton.main.events.PresenterStatusEvent; + import org.bigbluebutton.main.model.users.BBBUser; + import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; + import org.bigbluebutton.util.i18n.ResourceUtil; + + [Bindable] + private var _rolledOverMuteBtn:Boolean = false; + [Bindable] + private var _rolledOverCanvas:Boolean = false; + [Bindable] + private var _username:String = ""; + [Bindable] + private var _me:Boolean = false; + private var _dispatcher:Dispatcher = new Dispatcher(); + private var _images:Images = new Images(); + private var _user:BBBUser = null; + + private var _hideMuteBtnTimer:Timer; + + protected function init():void { + _hideMuteBtnTimer = new Timer(500, 1); + _hideMuteBtnTimer.addEventListener(TimerEvent.TIMER, onHideMuteBtnTimerComplete); + } + + protected function onCreationComplete():void { + if (user) { + updateButtons(); + if (user.talking) { + titleBox.setStyle("styleName", "videoToolbarBackgroundTalkingStyle"); + } else { + titleBox.setStyle("styleName", "videoToolbarBackgroundNotTalkingStyle"); + } + } + } + + public function set userId(value:String):void { + _user = UsersUtil.getUser(value); + } + + public function loadAvatar(options:VideoConfOptions):void { + avatar.user = _user; + avatar.options = options; + avatar.load(UsersUtil.getAvatarURL()); + + avatarVisibility = true; + setUserProperties(); + } + + public function loadCamera(options:VideoConfOptions, camIndex:int, videoProfile:VideoProfile, chromePermissionDenied:Boolean):void { + video.user = _user; + video.options = options; + video.publish(camIndex, videoProfile, chromePermissionDenied); + + videoVisibility = true; + setUserProperties(); + } + + public function loadVideo(options:VideoConfOptions, connection:NetConnection, streamName:String):void { + video.user = _user; + video.options = options; + video.view(connection, streamName); + + videoVisibility = true; + setUserProperties(); + } + + private function setUserProperties():void { + _username = user.name; + _me = user.me; + + updateButtons(); + } + + public function get visibleComponent():UserGraphic { + if (avatar.visible) { + return avatar; + } else if (video.visible) { + return video; + } else { + return null; + } + } + + public function get contentAspectRatio():Number { + if (visibleComponent) { + return visibleComponent.aspectRatio; + } else { + return 320 / 240; + } + } + + public function get user():BBBUser { + return _user; + } + + public function shutdown():void { + trace("[UserGraphicHolder:shutdown]"); + video.shutdown(); + } + + private function set avatarVisibility(value:Boolean):void { + avatar.visible = avatar.includeInLayout = value; + video.visible = video.includeInLayout = !value; + } + + private function set videoVisibility(value:Boolean):void { + avatarVisibility = !value; + } + + private function hasPermissionToMute():Boolean { + return (_me || UsersUtil.amIModerator()); + } + + private function onMuteBtnClick(event:MouseEvent):void { + if (user && user.voiceJoined && hasPermissionToMute()) { + var e:VoiceConfEvent = new VoiceConfEvent(VoiceConfEvent.MUTE_USER); + e.userid = user.userID; + e.mute = !user.voiceMuted; + _dispatcher.dispatchEvent(e); + } + + event.stopPropagation(); + } + + private function onMuteBtnMouseOver():void { + if (hasPermissionToMute()) { + _rolledOverMuteBtn = true; + } + updateButtons(); + } + + private function onMuteBtnMouseOut():void { + if (hasPermissionToMute()) { + _rolledOverMuteBtn = false; + } + updateButtons(); + } + + private function onCanvasMouseOver():void { + _rolledOverCanvas = true; + updateButtons(); + } + + private function onCanvasMouseOut():void { + _rolledOverCanvas = false; + updateButtons(); + } + + private function updateButtons():void { + if (user != null) { + if (_rolledOverMuteBtn == user.voiceMuted) { + muteBtn.styleName = "muteOverlayBtn"; + } else { + muteBtn.styleName = "unmuteOverlayBtn"; + } + + if (_rolledOverCanvas || _rolledOverMuteBtn) { + // muteBtnWrapper.visible = user.voiceJoined; + setMuteBtnVisibility(user.voiceJoined); + } else { + // muteBtnWrapper.visible = user.voiceJoined && user.voiceMuted; + setMuteBtnVisibility(user.voiceJoined && user.voiceMuted); + } + + var userIconVisibility:Boolean; + if (user.presenter) { + userIcon.source = _images.presenter_white; + userIconVisibility = true; + } else { + if (user.role == BBBUser.MODERATOR) { + userIcon.source = _images.moderator_white; + userIconVisibility = true; + } else { + userIconVisibility = false; + } + } + userIconWrapper.visible = userIconWrapper.includeInLayout = userIconVisibility; + } + } + + private function handleUserTalkingEvent(event:CoreEvent):void { + if (user && event.message.userID == user.userID) { + updateButtons(); + if (event.message.talking) { + titleBox.setStyle("styleName", "videoToolbarBackgroundTalkingStyle"); + } else { + titleBox.setStyle("styleName", "videoToolbarBackgroundNotTalkingStyle"); + } + } + } + + private function handlePresenterChangedEvent(event:PresenterStatusEvent):void { + if (user && event.userID == user.userID) { + updateButtons(); + } + } + + private function handleUserVoiceChangedEvent(event:BBBEvent):void { + if (user && event.payload.userID == user.userID) { + updateButtons(); + } + } + + private function onHideMuteBtnTimerComplete(event:TimerEvent):void { + muteBtnWrapper.visible = false; + } + + private function setMuteBtnVisibility(visible:Boolean):void { + if (visible) { + showMuteBtn(); + } else { + hideMuteBtn(); + } + } + + private function hideMuteBtn():void { + _hideMuteBtnTimer.reset(); + _hideMuteBtnTimer.start(); + } + + private function showMuteBtn():void { + _hideMuteBtnTimer.reset(); + muteBtnWrapper.visible = true; + } + + ]]> + </mx:Script> + + <mx:Fade id="fadeOut" duration="200" alphaFrom="1.0" alphaTo="0.0" /> + <mx:Fade id="fadeIn" duration="200" alphaFrom="0.0" alphaTo="1.0" /> + + <mx:Canvas id="canvas" width="100%" height="100%" horizontalScrollPolicy="off" verticalScrollPolicy="off" > + <views:UserAvatar id="avatar" width="100%" height="100%" visible="false" includeInLayout="false" /> + <views:UserVideo id="video" width="100%" height="100%" visible="false" includeInLayout="false" /> + <mx:VBox id="overlay" width="100%" height="100%" > + <mx:HBox + id="titleBox" + width="100%" + verticalAlign="middle" + paddingRight="0" + paddingLeft="0" + styleName="videoToolbarBackgroundNotTalkingStyle" > + <mx:Box id="userIconWrapper" visible="false" includeInLayout="false" paddingLeft="3" paddingRight="0" > + <mx:Image id="userIcon" /> + </mx:Box> + <mx:Label + text="{_username + (_me? ' (' + ResourceUtil.getInstance().getString('bbb.users.usersGrid.nameItemRenderer.youIdentifier') + ')' : '')}" + fontWeight="{_me ? 'bold' : 'normal'}" + width="100%" + paddingLeft="0" + minWidth="0" + truncateToFit="true" + styleName="videoToolbarLabelStyle" /> + <mx:Box paddingRight="5"> + <mx:Button styleName="closeBtnFocus" buttonMode="true" click="shutdown()" /> + </mx:Box> + </mx:HBox> + <mx:Box + width="100%" + paddingTop="15" + paddingRight="15" + horizontalAlign="right" > + <mx:Box + id="muteBtnWrapper" + visible="false" + hideEffect="{fadeOut}" showEffect="{fadeIn}" > + <mx:Button + id="muteBtn" + styleName="talkingOverlayBtn" + buttonMode="true" + click="onMuteBtnClick(event)" + mouseOver="onMuteBtnMouseOver()" + mouseOut="onMuteBtnMouseOut()" /> + </mx:Box> + </mx:Box> + </mx:VBox> + </mx:Canvas> +</mx:VBox> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserVideo.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserVideo.as new file mode 100644 index 0000000000000000000000000000000000000000..9b647332162b951be3e141f9f47c679066936ad4 --- /dev/null +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/UserVideo.as @@ -0,0 +1,213 @@ +package org.bigbluebutton.modules.videoconf.views +{ + import com.asfusion.mate.events.Dispatcher; + + import flash.events.AsyncErrorEvent; + import flash.events.Event; + import flash.events.NetStatusEvent; + import flash.filters.ConvolutionFilter; + import flash.text.TextField; + import flash.media.Camera; + import flash.media.Video; + import flash.net.NetConnection; + import flash.net.NetStream; + import mx.utils.ObjectUtil; + + import org.bigbluebutton.core.BBB; + import org.bigbluebutton.core.model.VideoProfile; + import org.bigbluebutton.main.events.BBBEvent; + import org.bigbluebutton.main.events.StoppedViewingWebcamEvent; + import org.bigbluebutton.main.views.VideoWithWarnings; + import org.bigbluebutton.modules.videoconf.events.ClosePublishWindowEvent; + import org.bigbluebutton.modules.videoconf.events.StartBroadcastEvent; + import org.bigbluebutton.modules.videoconf.events.StopBroadcastEvent; + + public class UserVideo extends UserGraphic { + private static const LOG:String = "Videoconf::UserVideo - "; + + protected var _camIndex:int = -1; + + protected var _ns:NetStream; + + protected var _shuttingDown:Boolean = false; + protected var _streamName:String; + protected var _video:VideoWithWarnings = null; + protected var _videoProfile:VideoProfile; + protected var _dispatcher:Dispatcher = new Dispatcher(); + + public function UserVideo() { + super(); + + _video = new VideoWithWarnings(); + _background.addChild(_video); + } + + public function publish(camIndex:int, videoProfile:VideoProfile, chromePermissionDenied:Boolean):void { + _camIndex = camIndex; + _videoProfile = videoProfile; + setOriginalDimensions(_videoProfile.width, _videoProfile.height); + + _video.chromePermissionDenied = chromePermissionDenied; + _video.updateCamera(camIndex, _videoProfile, _background.width, _background.height); + + invalidateDisplayList(); + startPublishing(); + } + + private function newStreamName():String { + /** + * Add timestamp to create a unique stream name. This way we can record + * stream without overwriting previously recorded streams. + */ + var d:Date = new Date(); + var curTime:Number = d.getTime(); + var uid:String = user.userID; + return _videoProfile.id + "-" + uid + "-" + curTime; + } + + protected function getVideoProfile(stream:String):VideoProfile { + trace("Parsing stream name [" + stream + "]"); + var pattern:RegExp = new RegExp("([A-Za-z0-9]+)-([A-Za-z0-9]+)-\\d+", ""); + if (pattern.test(stream)) { + trace("The stream name is well formatted"); + trace("Video profile resolution is [" + pattern.exec(stream)[1] + "]"); + trace("Userid [" + pattern.exec(stream)[2] + "]"); + return BBB.getVideoProfileById(pattern.exec(stream)[1]); + } else { + trace("Bad stream name format"); + var profile:VideoProfile = BBB.defaultVideoProfile; + if (profile == null) { + profile = BBB.fallbackVideoProfile; + } + return profile; + } + } + + private function startPublishing():void { + _streamName = newStreamName(); + _shuttingDown = false; + + var e:StartBroadcastEvent = new StartBroadcastEvent(); + e.stream = _streamName; + e.camera = _video.getCamera(); + e.videoProfile = _videoProfile; + _dispatcher.dispatchEvent(e); + } + + public function shutdown():void { + if (!_shuttingDown) { + _shuttingDown = true; + if (_ns) { + stopViewing(); + _ns.close(); + _ns = null; + } + + if (_video.cameraState()) { + stopPublishing(); + } + + if (_video) { + _video.disableCamera(); + } + } + } + + private function stopViewing():void { + var stopEvent:StoppedViewingWebcamEvent = new StoppedViewingWebcamEvent(); + stopEvent.webcamUserID = user.userID; + stopEvent.streamName = _streamName; + _dispatcher.dispatchEvent(stopEvent); + user.removeViewingStream(_streamName); + } + + private function stopPublishing():void { + var e:StopBroadcastEvent = new StopBroadcastEvent(); + e.stream = _streamName; + e.camId = _camIndex; + _dispatcher.dispatchEvent(e); + } + + public function view(connection:NetConnection, streamName:String):void { + _streamName = streamName; + _shuttingDown = false; + + _ns = new NetStream(connection); + _ns.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus); + _ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); + _ns.client = this; + _ns.bufferTime = 0; + _ns.receiveVideo(true); + _ns.receiveAudio(false); + + _videoProfile = getVideoProfile(streamName); + trace("Remote video profile: " + _videoProfile.toString()); + if (_videoProfile == null) { + throw("Invalid video profile"); + return; + } + setOriginalDimensions(_videoProfile.width, _videoProfile.height); + + _video.attachNetStream(_ns, _videoProfile, _background.width, _background.height); + + if (options.applyConvolutionFilter) { + var filter:ConvolutionFilter = new ConvolutionFilter(); + filter.matrixX = 3; + filter.matrixY = 3; + trace("Applying convolution filter =[" + options.convolutionFilter + "]"); + filter.matrix = options.convolutionFilter; + filter.bias = options.filterBias; + filter.divisor = options.filterDivisor; + _video.videoFilters([filter]); + } + + _ns.play(streamName); + + user.addViewingStream(streamName); + invalidateDisplayList(); + } + + private function onNetStatus(e:NetStatusEvent):void{ + switch(e.info.code){ + case "NetStream.Publish.Start": + trace("NetStream.Publish.Start for broadcast stream " + _streamName); + break; + case "NetStream.Play.UnpublishNotify": + shutdown(); + break; + case "NetStream.Play.Start": + trace("Netstatus: " + e.info.code); + _dispatcher.dispatchEvent(new BBBEvent(BBBEvent.VIDEO_STARTED)); + break; + case "NetStream.Play.FileStructureInvalid": + trace("The MP4's file structure is invalid."); + break; + case "NetStream.Play.NoSupportedTrackFound": + trace("The MP4 doesn't contain any supported tracks"); + break; + } + } + + private function onAsyncError(e:AsyncErrorEvent):void{ + trace(LOG + e.text); + } + + private function onMetaData(info:Object):void { + trace(LOG + " width=" + info.width + " height=" + info.height); + } + + override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { + super.updateDisplayList(unscaledWidth, unscaledHeight); + + updateDisplayListHelper(unscaledWidth, unscaledHeight, _video); + } + + public function get camIndex():int { + return _camIndex; + } + + public function get streamName():String { + return _streamName; + } + } +} diff --git a/bigbluebutton-client/src/VideodockModule.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/VideoDock.mxml old mode 100755 new mode 100644 similarity index 51% rename from bigbluebutton-client/src/VideodockModule.mxml rename to bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/VideoDock.mxml index 9c2925d1bfc0d7a1f2c97789ffdc6dfb634e425f..d46fc6c24a3f41def5f7547169a59883e3fc1fe0 --- a/bigbluebutton-client/src/VideodockModule.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/VideoDock.mxml @@ -1,4 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> + <!-- BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ @@ -18,34 +19,36 @@ You should have received a copy of the GNU Lesser General Public License along with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. --> -<mx:Module xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="400" height="300" - xmlns:maps="org.bigbluebutton.modules.videodock.maps.*" implements="org.bigbluebutton.common.IBigBlueButtonModule"> + +<CustomMdiWindow xmlns="org.bigbluebutton.common.*" + xmlns:mx="http://www.adobe.com/2006/mxml" + initialize="init()" + horizontalScrollPolicy="off" + verticalScrollPolicy="off" + creationComplete="onCreationComplete()" + implements="org.bigbluebutton.common.IBbbModuleWindow" + title="{ResourceUtil.getInstance().getString('bbb.videodock.title')}" + xmlns:mate="http://mate.asfusion.com/" styleNameFocus="videoDockStyleFocus" + layout="absolute" visible="true" styleNameNoFocus="videoDockStyleNoFocus" + horizontalAlign="center" + verticalAlign="middle" > + <mx:Script> <![CDATA[ - import org.bigbluebutton.common.LogUtil; - - private var _moduleName:String = "Videodock Module"; - - private function onCreationComplete():void { - LogUtil.debug("VideodockModule initialized"); - } - - public function get moduleName():String { - return _moduleName; + import org.bigbluebutton.main.views.MainCanvas; + import org.bigbluebutton.util.i18n.ResourceUtil; + + private function init():void { + } - - public function start(attributes:Object):void { - LogUtil.debug("Videodock attr: " + attributes.username); - - eventMap.startModule(); + + private function onCreationComplete():void { + this.showCloseButton = false; } - - public function stop():void { - eventMap.stopModule(); + + public function getPrefferedPosition():String { + return MainCanvas.BOTTOM_LEFT; } - ]]> </mx:Script> - - <maps:VideoDockEventMap id="eventMap"/> -</mx:Module> \ No newline at end of file +</CustomMdiWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/VideoWindow.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/VideoWindow.mxml deleted file mode 100755 index d0a2d5b9e756be612fd40447155f0cf73d20eac1..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videoconf/views/VideoWindow.mxml +++ /dev/null @@ -1,278 +0,0 @@ -<?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/>. - ---> - -<viewVid:VideoWindowItf - xmlns:viewVid="org.bigbluebutton.modules.videoconf.business.*" - xmlns:mx="http://www.adobe.com/2006/mxml" - creationComplete="onCreationComplete()" - implements="org.bigbluebutton.common.IBbbModuleWindow" - xmlns:mate="http://mate.asfusion.com/" - styleNameFocus="videoViewStyleFocus" - styleNameNoFocus="videoViewStyleNoFocus" - horizontalScrollPolicy="off" - verticalScrollPolicy="off" - resize="onResize()" - layout="absolute"> - - <mate:Listener type="{BBBEvent.USER_VOICE_MUTED}" method="handleUserVoiceMutedEvent" /> - <mate:Listener type="{EventConstants.USER_TALKING}" method="handleUserTalkingEvent" /> - <mate:Listener type="{SwitchedPresenterEvent.SWITCHED_PRESENTER}" method="handleSwitchedPresenterEvent" /> - <mate:Listener type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" method="handleMadePresenterEvent" /> - <mate:Listener type="{BBBEvent.USER_VOICE_JOINED}" method="handleNewRoleEvent" /> - <mate:Listener type="{BBBEvent.USER_VOICE_LEFT}" method="handleNewRoleEvent" /> - <mate:Listener type="{CloseAllWindowsEvent.CLOSE_ALL_WINDOWS}" method="closeWindow" /> - - <mx:Script> - <![CDATA[ - import com.asfusion.mate.events.Dispatcher; - - import flexlib.mdi.events.MDIWindowEvent; - - import mx.core.UIComponent; - - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.common.Role; - import org.bigbluebutton.common.events.CloseWindowEvent; - import org.bigbluebutton.core.EventConstants; - import org.bigbluebutton.core.events.CoreEvent; - import org.bigbluebutton.core.managers.UserManager; - import org.bigbluebutton.main.events.BBBEvent; - import org.bigbluebutton.main.events.MadePresenterEvent; - import org.bigbluebutton.main.events.StoppedViewingWebcamEvent; - import org.bigbluebutton.main.events.SwitchedPresenterEvent; - import org.bigbluebutton.main.views.MainCanvas; - import org.bigbluebutton.modules.videoconf.business.TalkingButtonOverlay; - import org.bigbluebutton.modules.videoconf.events.CloseAllWindowsEvent; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; - - private var ns:NetStream; - private var globalDispatcher:Dispatcher; - - [Bindable] - public var videoOptions:VideoConfOptions = new VideoConfOptions(); - - private var windowType:String = "VideoWindowType"; - - [Bindable] public var glowColor:String = ""; - [Bindable] public var glowBlurSize:Number = 0; - - override public function getWindowType():String { - return windowType; - } - - private function onCreationComplete():void{ - this.glowColor = videoOptions.glowColor; - this.glowBlurSize = videoOptions.glowBlurSize; - - LogUtil.debug("checking glow values: "+this.glowColor+" and "+this.glowBlurSize); - - _videoHolder = new UIComponent(); - _videoHolder.addChild(_video); - this.addChild(_videoHolder); - - addEventListener(MDIWindowEvent.RESIZE_START, onResizeStart); - addEventListener(MDIWindowEvent.RESIZE_END, onResizeEnd); - addEventListener(MDIWindowEvent.CLOSE, onCloseEvent); - - addEventListener(MouseEvent.MOUSE_OVER, showButtons); - addEventListener(MouseEvent.MOUSE_OUT, hideButtons); - addEventListener(MouseEvent.DOUBLE_CLICK, onDoubleClick); - - createButtons(); - addControlButtons(); - - globalDispatcher = new Dispatcher(); - - this.minWidth = _minWidth; - this.minHeight = _minHeight; - maximizeRestoreBtn.visible = false; - this.resizable = true; - - /** - * At this point, the function startVideo has been called, and - * the video has the exactly dimensions of the original stream. - * It's needed to call onResize() to fit the video window in the - * main canvas in case that the video dimensions are larger than - * the parent window. - */ - onResize(); - - if (videoOptions.viewerWindowMaxed) - this.maximize(); - - this.showCloseButton = videoOptions.showCloseButton; - } - - private function handleMadePresenterEvent(event:MadePresenterEvent):void { - trace("******** VideoWindow: HandleMadePresenter event *********"); - updateControlButtons(); - } - - private function handleSwitchedPresenterEvent(event:SwitchedPresenterEvent):void { - trace("******** VideoWindow: handleSwitchedPresenterEvent event *********"); - updateControlButtons(); - } - - private function handleNewRoleEvent(event:Event):void { - updateControlButtons(); - } - - private function handleUserVoiceMutedEvent(event:BBBEvent):void { - if (event.payload.userID == userID) { - userMuted(event.payload.muted); - } - } - - private var _closing:Boolean = false; - - private function onCloseEvent(event:MDIWindowEvent = null):void { - LogUtil.debug("ViewWindow closing " + streamName); - if (!_closing) { - _closing = true; - var stopEvent:StoppedViewingWebcamEvent = new StoppedViewingWebcamEvent(); - stopEvent.webcamUserID = userID; - globalDispatcher.dispatchEvent(stopEvent); - - if (UserManager.getInstance().getConference().hasUser(userID)) { - UserManager.getInstance().getConference().getUser(userID).viewingStream = false; - } - } - - } - - private function handleUserTalkingEvent(event:CoreEvent):void { - if (event.message.userID == userID) { - if (event.message.talking) { - notTalkingEffect.end(); - talkingEffect.play([this]); - simulateClick(); - } else { - talkingEffect.end(); - notTalkingEffect.play([this]); - } - } - } - - - public function startVideo(connection:NetConnection, stream:String):void{ - ns = new NetStream(connection); - ns.addEventListener( NetStatusEvent.NET_STATUS, onNetStatus ); - ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onAsyncError); - ns.client = this; - ns.bufferTime = 0; - ns.receiveVideo(true); - ns.receiveAudio(false); - - var res:Array = getVideoResolution(stream); - if (res == null) // error - return; - _video = new Video(Number(res[0]), Number(res[1])); - _video.width = Number(res[0]); - _video.height = Number(res[1]); - _video.smoothing = true; - setAspectRatio(Number(res[0]), Number(res[1])); - _video.attachNetStream(ns); - - - if (videoOptions.smoothVideo) { - trace("Smoothing video.") - _video.smoothing = true; - } - - if (videoOptions.applyConvolutionFilter) { - var filter:ConvolutionFilter = new flash.filters.ConvolutionFilter(); - filter.matrixX = 3; - filter.matrixY = 3; - trace("Applying convolution filter =[" + videoOptions.convolutionFilter + "]"); - filter.matrix = videoOptions.convolutionFilter; - filter.bias = videoOptions.filterBias; - filter.divisor = videoOptions.filterDivisor; - _video.filters = [filter]; - } - - ns.play(stream); - this.streamName = stream; - - this.width = _video.width + paddingHorizontal; - this.height = _video.height + paddingVertical; - - if (UserManager.getInstance().getConference().hasUser(userID)) { - UserManager.getInstance().getConference().getUser(userID).viewingStream = true; - } - } - - private function onAsyncError(e:AsyncErrorEvent):void{ - LogUtil.debug("VideoWindow::asyncerror " + e.toString()); - } - - public function onMetaData(info:Object):void{ - trace("metadata: width=" + info.width + " height=" + info.height); - _video.width = info.width; - _video.height = info.height; - setAspectRatio(info.width, info.height); - onResize(); - } - - private function onNetStatus(e:NetStatusEvent):void{ - switch(e.info.code){ - case "NetStream.Publish.Start": - LogUtil.debug("NetStream.Publish.Start for broadcast stream " + streamName); - break; - case "NetStream.Play.UnpublishNotify": - ns.close(); - this.close(); - // shouldn't call onCloseEvent() here because of the viewer cam icon - super.close(); - break; - case "NetStream.Play.Start": - LogUtil.debug("Netstatus: " + e.info.code); - globalDispatcher.dispatchEvent(new BBBEvent(BBBEvent.VIDEO_STARTED)); - break; - case "NetStream.Play.FileStructureInvalid": - LogUtil.debug("The MP4's file structure is invalid."); - break; - case "NetStream.Play.NoSupportedTrackFound": - LogUtil.debug("The MP4 doesn't contain any supported tracks"); - break; - } - } - - override public function close(event:MouseEvent=null):void{ - ns.close(); - onCloseEvent(); - super.close(event); - } - - private function closeWindow(e:CloseAllWindowsEvent):void{ - this.close(); - } - - ]]> - </mx:Script> - - <mx:Glow id="talkingEffect" duration="500" alphaFrom="1.0" alphaTo="0.3" - blurXFrom="0.0" blurXTo="{glowBlurSize}" blurYFrom="0.0" blurYTo="{glowBlurSize}" color="{glowColor}"/> - <mx:Glow id="notTalkingEffect" duration="500" alphaFrom="0.3" alphaTo="1.0" - blurXFrom="{glowBlurSize}" blurXTo="0.0" blurYFrom="{glowBlurSize}" blurYTo="0.0" color="{glowColor}"/> - - -</viewVid:VideoWindowItf> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/maps/VideoDockEventMap.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/maps/VideoDockEventMap.mxml deleted file mode 100755 index 477f966ac50cb50613d424f6a00110dac09faa4b..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/maps/VideoDockEventMap.mxml +++ /dev/null @@ -1,49 +0,0 @@ -<?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/>. - ---> - -<EventMap xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="http://mate.asfusion.com/"> - <mx:Script> - <![CDATA[ - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.common.events.OpenWindowEvent; - import org.bigbluebutton.common.events.CloseWindowEvent; - - import org.bigbluebutton.modules.videodock.views.VideoDock; - - private var videoDock:VideoDock; - - public function startModule():void{ - videoDock = new VideoDock(); - - var windowEvent:OpenWindowEvent = new OpenWindowEvent(OpenWindowEvent.OPEN_WINDOW_EVENT); - windowEvent.window = videoDock; - globalDispatcher.dispatchEvent(windowEvent); - } - - public function stopModule():void { - videoDock.removeAllChildren(); - } - - ]]> - </mx:Script> - -</EventMap> \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/views/DockOptions.as b/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/views/DockOptions.as deleted file mode 100755 index ea74c5e6de1f7eb112e62febc763ca3a68068ecd..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/views/DockOptions.as +++ /dev/null @@ -1,93 +0,0 @@ -/** - * 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.modules.videodock.views -{ - import org.bigbluebutton.core.BBB; - - public class DockOptions - { - [Bindable] - public var showControls:Boolean = true; - - [Bindable] - public var autoDock:Boolean = true; - - [Bindable] - public var maximize:Boolean = false; - - [Bindable] - public var position:String = "bottom-right"; - - [Bindable] - public var width:int = 172; - - [Bindable] - public var height:int = 179; - - [Bindable] - public var layout:String = LAYOUT_SMART; - static public const LAYOUT_NONE:String = "NONE"; - static public const LAYOUT_HANGOUT:String = "HANGOUT"; - static public const LAYOUT_SMART:String = "SMART"; - - [Bindable] - public var oneAlwaysBigger:Boolean = false; - - [Bindable] public var baseTabIndex:int; - - public function DockOptions() - { - var vxml:XML = BBB.getConfigForModule("VideodockModule"); - if (vxml != null) { - if (vxml.@showControls != undefined) { - showControls = (vxml.@showControls.toString().toUpperCase() == "TRUE") ? true : false; - } - if (vxml.@autoDock != undefined) { - autoDock = (vxml.@autoDock.toString().toUpperCase() == "TRUE") ? true : false; - } - if (vxml.@maximizeWindow != undefined) { - maximize = (vxml.@maximizeWindow.toString().toUpperCase() == "TRUE") ? true : false; - } - if (vxml.@position != undefined) { - position = vxml.@position.toString(); - } - if (vxml.@width != undefined) { - width = Number(vxml.@width); - } - if (vxml.@height != undefined) { - height = Number(vxml.@height); - } - if (vxml.@layout != undefined) { - layout = vxml.@layout.toString().toUpperCase(); - if (layout != LAYOUT_NONE && layout != LAYOUT_HANGOUT && layout != LAYOUT_SMART) - layout = LAYOUT_NONE; - } - if (vxml.@oneAlwaysBigger != undefined) { - oneAlwaysBigger = (vxml.@oneAlwaysBigger.toString().toUpperCase() == "TRUE") ? true : false; - } - if (vxml.@baseTabIndex != undefined) { - baseTabIndex = vxml.@baseTabIndex; - } - else{ - baseTabIndex = 501; - } - } - } - } -} \ No newline at end of file diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/views/VideoDock.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/views/VideoDock.mxml deleted file mode 100755 index f4adc0e5130e08423e05d733ae16c1e78a95b1b6..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/videodock/views/VideoDock.mxml +++ /dev/null @@ -1,493 +0,0 @@ -<?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/>. - ---> - -<CustomMdiWindow xmlns="org.bigbluebutton.common.*" - xmlns:mx="http://www.adobe.com/2006/mxml" - initialize="init()" - horizontalScrollPolicy="off" - verticalScrollPolicy="off" - creationComplete="onCreationComplete()" - implements="org.bigbluebutton.common.IBbbModuleWindow" - title="{ResourceUtil.getInstance().getString('bbb.videodock.title')}" - xmlns:mate="http://mate.asfusion.com/" styleNameFocus="videoDockStyleFocus" - layout="absolute" visible="false" styleNameNoFocus="videoDockStyleNoFocus" - horizontalAlign="center" - verticalAlign="middle" - resize="onChildAdd()"> - - <mate:Listener type="{LocaleChangeEvent.LOCALE_CHANGED}" method="localeChanged" /> - <mate:Listener type="{ShortcutEvent.FOCUS_VIDEO_WINDOW}" method="focusWindow" /> - <mate:Listener type="{ShortcutEvent.MINIMIZE_DOCK}" method="remoteMinimize" /> - <mate:Listener type="{ShortcutEvent.MAXIMIZE_DOCK}" method="remoteMaximize" /> - <mx:Script> - <![CDATA[ - import org.bigbluebutton.main.events.ShortcutEvent; - import com.asfusion.mate.events.Dispatcher; - - import flexlib.mdi.containers.MDIWindow; - - import mx.events.ChildExistenceChangedEvent; - - import org.bigbluebutton.common.Images; - - import org.bigbluebutton.common.LogUtil; - import org.bigbluebutton.common.events.CloseWindowEvent; - import org.bigbluebutton.common.events.DragWindowEvent; - import org.bigbluebutton.common.events.OpenWindowEvent; - import org.bigbluebutton.core.BBB; - import org.bigbluebutton.core.events.ConnectAppEvent; - import org.bigbluebutton.main.model.users.Conference; - import org.bigbluebutton.main.views.MainCanvas; - import org.bigbluebutton.modules.videoconf.business.VideoWindowItf; - import org.bigbluebutton.modules.videoconf.events.OpenVideoWindowEvent; - import org.bigbluebutton.modules.videoconf.events.UserTalkingEvent; - import org.bigbluebutton.modules.videoconf.model.VideoConfOptions; - import org.bigbluebutton.util.i18n.ResourceUtil; - import org.bigbluebutton.common.events.LocaleChangeEvent; - - private var childrenDimension:Dictionary = new Dictionary(); - private var borderColor:String; - private var minChildAspectRatio:Number; - // the mutable array is used to change the order of the dock children - private var mutableChildrenArray:Array = new Array(); - - private var _prioritizeWindow:Boolean = false; - private var priorityWindow:VideoWindowItf = null; - // the priority video will fit a portion of the dock that is represented by this weight - // bigger the weight, bigger will be the window and less space the other windows will have - private var priorityWindowWeight:Number = 2/3; - - private var options:DockOptions = new DockOptions(); - private var confOptions:VideoConfOptions = new VideoConfOptions(); - private var dispatcher:Dispatcher; - private var keyCombos:Object; - - [Bindable] - private var baseIndex:int; - - private function init():void{ - baseIndex = options.baseTabIndex; - } - - private var images:Images = new Images(); - - [Bindable] - private var backgroundImage:Class = images.video_dock_bg; - - private function onCreationComplete():void { - this.showCloseButton = false; - this.showControls = options.showControls; - - this.minWidth = options.width; - this.minHeight = options.height; - this.maxWidth = this.parent.width; - this.maxHeight = this.parent.height; - - this.width = this.minWidth; - this.height = this.minHeight; - - addEventListener(ChildExistenceChangedEvent.CHILD_ADD, onChildAdd); - addEventListener(ChildExistenceChangedEvent.CHILD_REMOVE, onChildRemove); - - dispatcher = new Dispatcher(); - hotkeyCapture(); - - resourcesChanged(); - - titleBarOverlay.tabIndex = baseIndex; - - minimizeBtn.tabIndex = baseIndex+1; - maximizeRestoreBtn.tabIndex = baseIndex+2; - - closeBtn.tabIndex = baseIndex+3; - - if (options.maximize) this.maximize(); - - var gDispatcher:Dispatcher = new Dispatcher(); - var evt:ConnectAppEvent = new ConnectAppEvent(ConnectAppEvent.CONNECT_VIDEO_APP); - gDispatcher.dispatchEvent(evt); - } - - private function remoteMinimize(e:ShortcutEvent):void{ - if (!minimized){ - this.minimize(); - } - } - - private function remoteMaximize(e:ShortcutEvent):void{ - if (!maximized && !minimized){ - this.maximize(); - } - else{ - this.restore(); - } - } - - private function loadKeyCombos(modifier:String):void { - keyCombos = new Object(); // always start with a fresh array - keyCombos[modifier+(ResourceUtil.getInstance().getString('bbb.shortcutkey.general.maximize') as String)] = ShortcutEvent.MAXIMIZE_DOCK; - keyCombos[modifier+(ResourceUtil.getInstance().getString('bbb.shortcutkey.general.minimize') as String)] = ShortcutEvent.MINIMIZE_DOCK; - } - - private function hotkeyCapture():void{ - this.addEventListener(KeyboardEvent.KEY_DOWN, handleKeyDown); - ResourceUtil.getInstance().addEventListener(Event.CHANGE, localeChanged); // Listen for locale changing - } - - // Handle general-access hotkeys, regardless of what window the user is focused in - private function handleKeyDown(e:KeyboardEvent) :void { - var modifier:String = ExternalInterface.call("determineModifier"); - loadKeyCombos(modifier); - var keyPress:String = (e.ctrlKey ? "control+" : "") + (e.shiftKey ? "shift+" : "") + - (e.altKey ? "alt+" : "") + e.keyCode; - if (keyCombos[keyPress]) { - dispatcher.dispatchEvent(new ShortcutEvent(keyCombos[keyPress])); - } - } - - private function focusWindow(e:ShortcutEvent):void{ - focusManager.setFocus(titleBarOverlay); - } - - private function localeChanged(e:Event):void{ - resourcesChanged(); - } - - /** - * The windows docked are organized in a MxN grid equally divided. - * Then, the aspect ratio of the cells is the equal of the window - * with lower aspect ratio. For example, if there are two windows, - * one 4x3 and one 16x9, the aspect ratio of the grid cells will be - * 4x3, to better fit all the windows. The aspect ratio of the grid - * cells is updated every time a window is added or removed. - */ - private function updateMinAspectRatio(children:Array):void { - minChildAspectRatio = 0; - for each(var window:VideoWindowItf in children) { - const windowAspectRatio:Number = (window.width - VideoWindowItf.PADDING_HORIZONTAL) / (window.height - VideoWindowItf.PADDING_VERTICAL); - if (minChildAspectRatio == 0 || windowAspectRatio < minChildAspectRatio) - minChildAspectRatio = windowAspectRatio; - } - LogUtil.debug("Using aspect ratio grid = " + minChildAspectRatio); - } - - private function onChildAdd(e:ChildExistenceChangedEvent = null):void { - if (e != null) { - mutableChildrenArray.push(getChildren().pop()); - updateMinAspectRatio(mutableChildrenArray); - } - - if (options.oneAlwaysBigger && !_prioritizeWindow) - prioritizeAnyWindow(); - - updateChildrenDimensions(mutableChildrenArray); - } - - private function onChildRemove(e:ChildExistenceChangedEvent = null):void { - var index:int = mutableChildrenArray.indexOf(e.relatedObject); - if (index != -1) - mutableChildrenArray.splice(index, 1); - - if (e.relatedObject == priorityWindow || mutableChildrenArray.length <= 1) - deprioritizeWindow(); - - updateMinAspectRatio(mutableChildrenArray); - updateChildrenDimensions(mutableChildrenArray); - } - - public function getPrefferedPosition():String { - //return MainCanvas.BOTTOM_RIGHT; - return options.position; - } - - private function saveWindowDimensions(window:MDIWindow):void { - var dimensions:Object = {width:window.width, height:window.height}; - childrenDimension[window] = dimensions; - } - - private function restoreWindowDimensions(window:MDIWindow):void { - window.width = childrenDimension[window].width; - window.height = childrenDimension[window].height; - } - - private function repositionWindow(window:MDIWindow):void { - // \TODO reposition the window correctly between the windows - // one idea is to use a "shadow" window - // setChildIndex(window, ?); - } - - private function isVideoWindow(window:Object):Boolean { - return (getQualifiedSuperclassName(window) == "org.bigbluebutton.modules.videoconf.business::VideoWindowItf") - } - - private function onCloseWindow(e:CloseWindowEvent):void { - // it should not just undock the window, it should close the window forever - if (isVideoWindow(e.window) && this.contains(e.window as VideoWindowItf)) { - this.removeChild(e.window as VideoWindowItf); - } - - } - - private function onOpenWindow(e:OpenVideoWindowEvent):void { - if (isVideoWindow(e.window) && options.autoDock) { - addVideoChild(e.window as VideoWindowItf); - } - } - - private function addVideoChild(window:VideoWindowItf):void { - if (this.contains(window)) - return; - - /** - * Check if the window is visible. Right now, a publisher window can be made invisible by setting - * a param in config.xml. If the window is invisible, don't add it. We'll have to do this properly - * when we refactor to remove the window UIs. We should just be adding Videos here instead of windows. - * But for now, this is good enough. - */ - if (! window.visible) return; - - saveWindowDimensions(window); - - window.minimizeBtn.visible = false; - window.maximizeRestoreBtn.visible = false; - window.resizable = false; - window.draggable = false; - - window.addEventListener(MouseEvent.CLICK, onWindowClick); - window.addEventListener(UserTalkingEvent.TALKING, onTalking); - - var e:CloseWindowEvent = new CloseWindowEvent(); - e.window = window; - dispatchEvent(e); - - this.addChild(window); - } - - override public function close(event:MouseEvent = null):void { - for each(var windowItf:VideoWindowItf in mutableChildrenArray) { - windowItf.close(); - } - - removeAllChildren(); - super.close(event); - } - - private function updateChildrenDimensions(children:Array):void { - if (children.length == 0) return; - - const horizontalGap:int = getStyle("horizontalGap"); - const verticalGap:int = getStyle("verticalGap"); - - var availableWidth:int = this.width - this.borderMetrics.left - this.borderMetrics.right; - var availableHeight:int = this.height - this.borderMetrics.top - this.borderMetrics.bottom; - - var borderTop:int = 0; - var borderLeft:int = 0; - - if (_prioritizeWindow) { - // the first window will be prioritized - priorityWindow = children[0]; - - // if the aspect ratio of the dock is smaller than the window (like 1:1 against 16:9) - // the window will be on top of the dock - if (availableWidth / availableHeight < priorityWindow.width / priorityWindow.height - || options.layout == DockOptions.LAYOUT_HANGOUT) { - priorityWindow.width = availableWidth; - priorityWindow.updateHeight(); - if (priorityWindow.height > availableHeight * priorityWindowWeight) { - priorityWindow.height = availableHeight * priorityWindowWeight; - priorityWindow.updateWidth(); - } - priorityWindow.y = 0; - priorityWindow.x = (availableWidth - priorityWindow.width) / 2; - availableHeight -= (priorityWindow.height + verticalGap); - borderTop += priorityWindow.height + verticalGap; - } else { - // the window will be on left of the dock - priorityWindow.height = availableHeight; - priorityWindow.updateWidth(); - if (priorityWindow.width > availableWidth * priorityWindowWeight) { - priorityWindow.width = availableWidth * priorityWindowWeight; - priorityWindow.updateHeight(); - } - priorityWindow.y = (availableHeight - priorityWindow.height) / 2; - priorityWindow.x = 0; - availableWidth -= (priorityWindow.width + horizontalGap); - borderLeft += priorityWindow.width + horizontalGap; - } - } - - var childWidth:int = 0; - var childHeight:int = 0; - var nRows:Number = 0; - var nColumns:Number = 0; - - // we would like to maximize the window size - for (var rows:Number = 1; rows <= children.length - (_prioritizeWindow? 1: 0); ++rows) { - var columns:Number = Math.ceil((children.length - (_prioritizeWindow? 1: 0))/ rows); - var maxWidth:int = Math.floor((availableWidth - horizontalGap * (columns - 1)) / columns) - VideoWindowItf.PADDING_HORIZONTAL; - var maxHeight:int = Math.floor((availableHeight - verticalGap * (rows - 1)) / rows) - VideoWindowItf.PADDING_VERTICAL; - - // the possible dimensions shouldn't be less or equal 0 (it could happen with many videos) - if (maxWidth <= 0 || maxHeight <=0) - continue; - - var width:int = maxWidth; - var height:int = maxHeight; - - if (maxWidth / maxHeight > minChildAspectRatio) - width = Math.floor(maxHeight * minChildAspectRatio); - else - height = Math.floor(maxWidth / minChildAspectRatio); - - if (width > childWidth) { - childWidth = width; - childHeight = height; - nRows = rows; - nColumns = columns; - } - } - - childWidth += VideoWindowItf.PADDING_HORIZONTAL; - childHeight += VideoWindowItf.PADDING_VERTICAL; - - const horizontalBorder:int = availableWidth - nColumns * childWidth - (nColumns - 1) * horizontalGap; - const verticalBorder:int = availableHeight - nRows * childHeight - (nRows - 1) * verticalGap; - // this couple of lines will center the priority window on the available space for it - if (_prioritizeWindow) { - if (priorityWindow.x == 0) { - priorityWindow.x = horizontalBorder / 3; - borderTop += verticalBorder / 2; - borderLeft += horizontalBorder * (2 / 3); - } else { - priorityWindow.y = verticalBorder / 3; - borderTop += verticalBorder * (2 / 3); - borderLeft += horizontalBorder / 2; - } - } else { - // if there's no priority window, the border will be only around the little windows - borderTop += verticalBorder / 2; - borderLeft += horizontalBorder / 2; - } - - for (var childIndex:int = (_prioritizeWindow? 1: 0); childIndex < children.length; ++childIndex) { - var window:VideoWindowItf = children[childIndex]; - const wWidth:int = childWidth; - const wHeight:int = childHeight; - - window.width = wWidth; - window.updateHeight(); - - if (window.height > wHeight) { - window.height = wHeight; - window.updateWidth(); - } - - // the extra padding is used to center the windows with higher aspect ratio - const horizontalExtraPadding:int = (wWidth - window.width) / 2; - const verticalExtraPadding:int = (wHeight - window.height) / 2; - - var row:int = (childIndex - (_prioritizeWindow? 1: 0)) / nColumns; - var column:int = (childIndex - (_prioritizeWindow? 1: 0)) % nColumns; - - window.y = row * (wHeight + verticalGap) + borderTop + verticalExtraPadding;; - window.x = column * (wWidth + horizontalGap) + borderLeft + horizontalExtraPadding; - } - } - - protected function onWindowClick(event:MouseEvent = null):void { - prioritizeWindow(event.currentTarget); - } - - private function prioritizeWindow(window:Object):void { - if (mutableChildrenArray.length <= 1 - || options.layout == DockOptions.LAYOUT_NONE) - return; - - if (_prioritizeWindow && window == priorityWindow && !options.oneAlwaysBigger) { - deprioritizeWindow(); - } else { - var index:int = mutableChildrenArray.indexOf(window); - if (index != -1) { - mutableChildrenArray[index] = mutableChildrenArray[0]; - mutableChildrenArray[0] = window; - _prioritizeWindow = true; - } - } - updateChildrenDimensions(mutableChildrenArray); - } - - protected function onTalking(event:UserTalkingEvent):void { - prioritizeTalking(event.currentTarget); - } - - private function prioritizeTalking(window:Object):void { - if (mutableChildrenArray.length <= 1 - || options.layout == DockOptions.LAYOUT_NONE) - return; - - if (window == priorityWindow) //window already prioritized - return; - - var index:int = mutableChildrenArray.indexOf(window); - if (index != -1) { - mutableChildrenArray[index] = mutableChildrenArray[0]; - mutableChildrenArray[0] = window; - _prioritizeWindow = true; - } - - updateChildrenDimensions(mutableChildrenArray); - } - - private function prioritizeAnyWindow():void { - if (mutableChildrenArray.length > 0) - prioritizeWindow(mutableChildrenArray[0]); - } - - private function deprioritizeWindow():void { - if (options.oneAlwaysBigger && mutableChildrenArray.length > 1) - prioritizeAnyWindow(); - else { - _prioritizeWindow = false; - } - } - - override protected function resourcesChanged():void{ - super.resourcesChanged(); - this.title = ResourceUtil.getInstance().getString('bbb.videodock.title'); - - if (titleBarOverlay != null) { - titleBarOverlay.accessibilityName = ResourceUtil.getInstance().getString('bbb.videoDock.titleBar'); - } - - if (windowControls != null) { - maximizeRestoreBtn.accessibilityName = ResourceUtil.getInstance().getString('bbb.video.maximizeRestoreBtn.accessibilityName'); - minimizeBtn.accessibilityName = ResourceUtil.getInstance().getString('bbb.video.minimizeBtn.accessibilityName'); - } - } - - ]]> - </mx:Script> - - <mate:Listener type="{OpenVideoWindowEvent.OPEN_VIDEO_WINDOW_EVENT}" method="onOpenWindow" /> - <mate:Listener type="{CloseWindowEvent.CLOSE_WINDOW_EVENT}" method="onCloseWindow" /> -</CustomMdiWindow> diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/whiteboard/views/WhiteboardToolbar.mxml b/bigbluebutton-client/src/org/bigbluebutton/modules/whiteboard/views/WhiteboardToolbar.mxml index 7eefe580a6c120a6fbe967a8f9dd518224a09dd6..d0d296c695d7d8fc3cbc583aa64662288e40eb8e 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/whiteboard/views/WhiteboardToolbar.mxml +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/whiteboard/views/WhiteboardToolbar.mxml @@ -28,7 +28,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. xmlns:mate="http://mate.asfusion.com/" creationComplete="onCreationComplete()" visible="{showWhiteboardToolbar}" - styleName="whiteboardToolbarStyle"> + includeInLayout="{showWhiteboardToolbar}" + styleName="whiteboardToolbarStyle" + hideEffect="{fadeOut}" showEffect="{fadeIn}"> <mate:Listener type="{MadePresenterEvent.SWITCH_TO_PRESENTER_MODE}" method="presenterMode" /> <mate:Listener type="{MadePresenterEvent.SWITCH_TO_VIEWER_MODE}" method="viewerMode" /> @@ -37,6 +39,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. <mate:Listener type="{GraphicObjectFocusEvent.OBJECT_SELECTED}" method="graphicObjSelected" /> <mate:Listener type="{GraphicObjectFocusEvent.OBJECT_DESELECTED}" method="graphicObjDeselected" /> <mate:Listener type="{WhiteboardButtonEvent.WHITEBOARD_BUTTON_PRESSED}" method="handleWhiteboardButtonPressed"/> + <mate:Listener type="{ChangeMyRole.CHANGE_MY_ROLE_EVENT}" method="refreshRole" /> <mate:Listener type="{NavigationEvent.GOTO_PAGE}" method="handleSlideChange" /> <mate:Listener type="{DisplaySlideEvent.DISPLAY_SLIDE_EVENT}" method="handleSlideLoaded" /> <mate:Listener type="{UploadEvent.PRESENTATION_READY}" method="handlePresentationSwitch" /> @@ -74,6 +77,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. import org.bigbluebutton.core.managers.UserManager; import org.bigbluebutton.main.events.MadePresenterEvent; import org.bigbluebutton.main.events.ShortcutEvent; + import org.bigbluebutton.main.model.users.events.ChangeMyRole; import org.bigbluebutton.modules.present.events.DisplaySlideEvent; import org.bigbluebutton.modules.present.events.PresentationEvent; import org.bigbluebutton.modules.present.events.NavigationEvent; @@ -129,7 +133,9 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. [Bindable] private var colorPickerColours:Array = ['0x000000', '0xFFFFFF' , '0xFF0000', '0xFF8800', '0xCCFF00', '0x00FF00', '0x00FF88', '0x00FFFF', '0x0088FF', '0x0000FF', '0x8800FF', '0xFF00FF', '0xC0C0C0']; - + + private var _hideToolbarTimer:Timer = new Timer(500, 1); + private function init():void{ wbOptions = new WhiteboardOptions(); baseIndex = wbOptions.baseTabIndex; @@ -137,6 +143,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. private function onCreationComplete():void { setToolType(WhiteboardConstants.TYPE_ZOOM, null); + _hideToolbarTimer.addEventListener(TimerEvent.TIMER, onHideToolbarTimerComplete); } private function handleWhiteboardButtonPressed(e:WhiteboardButtonEvent):void { @@ -257,16 +264,34 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. checkVisibility(); } } + + private function refreshRole(e:ChangeMyRole):void { + checkVisibility(); + } private function checkVisibility(e:MadePresenterEvent = null):void { - if (toolbarAllowed() && slideLoaded && (wbOptions.keepToolbarVisible || mousedOver)) { + if (toolbarAllowed() && slideLoaded && (wbOptions.keepToolbarVisible || mousedOver) && !presentationWindow.minimized) { setPositionAndDepth(); - showWhiteboardToolbar = true; + showToolbar(); } else { - showWhiteboardToolbar = false; + hideToolbar(); } } - + + private function showToolbar():void { + _hideToolbarTimer.reset(); + showWhiteboardToolbar = true; + } + + private function hideToolbar():void { + _hideToolbarTimer.reset(); + _hideToolbarTimer.start(); + } + + private function onHideToolbarTimerComplete(event:TimerEvent):void { + showWhiteboardToolbar = false; + } + private function setPositionAndDepth(e:Event = null):void { this.x = presentationWindow.x + presentationWindow.width - 43; this.y = presentationWindow.y + 30; @@ -339,7 +364,10 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. ]]> </mx:Script> - + + <mx:Fade id="fadeOut" duration="200" alphaFrom="1.0" alphaTo="0.0" /> + <mx:Fade id="fadeIn" duration="200" alphaFrom="0.0" alphaTo="1.0" /> + <!-- Now, every 'tool' has two types of identifiers, one is found in WhiteboardConstants that identifies the "category" of the tool (ex. shape vs text), and the other specifies the tool itself (ex. line tool vs triangle tool, even though both are "shapes") diff --git a/bigbluebutton-client/src/org/red5/flash/bwcheck/ClientServerBandwidth.as b/bigbluebutton-client/src/org/red5/flash/bwcheck/ClientServerBandwidth.as index 22a97d38461ec4467f5740123b227bb07d59aad7..e16a3ec9b200c2e3dab6e89a3b4ff08bbf07a77a 100644 --- a/bigbluebutton-client/src/org/red5/flash/bwcheck/ClientServerBandwidth.as +++ b/bigbluebutton-client/src/org/red5/flash/bwcheck/ClientServerBandwidth.as @@ -139,5 +139,15 @@ package org.red5.flash.bwcheck break; } } + + public function onBWCheck(obj:Object):void + { +// dispatchStatus(obj); + } + + public function onBWDone(obj:Object):void + { +// dispatchComplete(obj); + } } } diff --git a/bigbluebutton-config/bin/bbb-record b/bigbluebutton-config/bin/bbb-record index b0fa602284955adde5a7efb2e98aa9c53bedd0d7..d06ce44ed0b735d0f87c48faea34c623173b516a 100755 --- a/bigbluebutton-config/bin/bbb-record +++ b/bigbluebutton-config/bin/bbb-record @@ -332,12 +332,12 @@ if [ $REPUBLISH ]; then fi if [ $CLEAN ]; then - sudo /etc/init.d/bbb-record stop + sudo /etc/init.d/bbb-record-core stop for type in $TYPES; do echo " clearning logs in /var/log/bigbluebutton/$type" find /var/log/bigbluebutton/$type -name "*.log" -exec sudo rm '{}' \; done - sudo /etc/init.d/bbb-record start + sudo /etc/init.d/bbb-record-core start fi if [ $DELETE ]; then diff --git a/bigbluebutton-config/web/default.pdf b/bigbluebutton-config/web/default.pdf old mode 100755 new mode 100644 index b3e08c05a74d53a7aec6852dd9d40b19d28cbc05..88d153dd5861cd3a0f17401efcd6cd7991513931 Binary files a/bigbluebutton-config/web/default.pdf and b/bigbluebutton-config/web/default.pdf differ diff --git a/bigbluebutton-config/web/favicon.ico b/bigbluebutton-config/web/favicon.ico index 2a399b83be1833fa8a206a7f33d35f8bb745031e..41c8cb123a1ebe2b686b808fb4f75e752b3b6b42 100644 Binary files a/bigbluebutton-config/web/favicon.ico and b/bigbluebutton-config/web/favicon.ico differ diff --git a/bigbluebutton-config/web/index.html b/bigbluebutton-config/web/index.html index 2cf527ac0d09df7c4d7acaa33b10d8f3c0858f19..e90bf452ed14a18fafb59fca4f4d1651448d90eb 100644 --- a/bigbluebutton-config/web/index.html +++ b/bigbluebutton-config/web/index.html @@ -1,205 +1,12 @@ -<!DOCTYPE html> -<html> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> - - <title>BigBlueButton - Open Source Web Conferencing</title> - <meta name="description" content="BigBlueButton enables universities and colleges to deliver a high-quality learning experience to remote students."> - <meta name="keywords" content="BigBlueButton, Open Source Web Conferencing, Distance Education, Courses Online, Web Conferencing, Open Source, Desktop Sharing, Video Conferencing, Video Collaboration, Presentation Sharing, Audio Sharing, Voice Collaboration, Public Chat, Webcam Sharing, Annotation, Whiteboard, Integrated Voice Over IP, Collaboration Software, Online Collaboration, Collaborative Learning, Virtual Classroom"> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <link rel="icon" href="images/favicon.png"> - - - <link rel="stylesheet" href="css/bijou.min.css"> - <link rel="stylesheet" href="css/style.css"> - <link rel="stylesheet" href="css/font-awesome.min.css"> - </head> - <body> - <div class='main'> - - <!-- Github Fork Ribbon --> - <a href="https://github.com/bigbluebutton/bigbluebutton"> - <img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png" alt="Fork me on GitHub"> - </a> - - <!-- Header --> - <div class='navbar'> - <div class='container'> - <div class="logo"> - <img src="images/bbb-logo.png" alt="BigBlueButton Demo"/> - </div> - </div> - </div> - - <!-- Body --> - <div class='container'> - - <!-- Welcome Message & Login Into Demo --> - <div class='row'> - <div class='span five'> - <h2>Welcome</h2> - <p> <a href="http://bigbluebutton.org/" target="_blank">BigBlueButton</a> is an open source web conferencing system for on-line learning. </p> - <p> We believe that every student with a web browser should have access to a high-quality on-line learning experience </p> - <p> We intend to make that possible with BigBlueButton.</p> - - <h4>For Developers</h4> - <p> The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. </p> - <p>See <a href="/demo/demo1.jsp" target="_blank">API examples </a> for how to integrate BigBlueButton with your project.</p> - </div> - <div class="span one"></div> - <div class='span six'> - <div class='join-metting pull-right'> - <h4>Try BigBlueButton</h4> - <p>Join a demo session on this server.</p> - - <form name="form1" method="GET" onsubmit="return checkform(this);" action="/demo/demo1.jsp"> - <input type="text" id="username" required="" name="username" placeholder="Enter Your Name" size="29" class="field input-default" autofocus> - <input type="submit" value="Join" class="submit_btn button success large"><br> - <input type="hidden" name="action" value="create"> - </form> - - <a class="watch" href="#video" class="pull-right">New to BigBlueButton? Watch these videos.</a> - - </div> - - </div> - </div> - - <hr class="featurette-divider"> - - <!-- BigBlueButton Features --> - <div class="bbb-features"> - <div class='row'> - <div class='featurette-heading'> - <h2>Features</h2> - </div> - - <div class='span four first'> - <i class="fa fa-play-circle-o"></i> - - <div class="bbb-features-content"> - <h3>Record and Playback</h3> - <p>BigBlueButton can record your sessions for later playback by students.</p> - </div> - </div> - - <div class='span four'> - <i class="fa fa-pencil-square-o"></i> - - <div class="bbb-features-content"> - <h3>Whiteboard</h3> - <p>The whiteboard controls let you annotate key parts of your presentation.</p> - </div> - </div> - - <div class='span four last'> - <i class="fa fa-desktop"></i> - - <div class="bbb-features-content"> - <h3>Desktop Sharing</h3> - <p>You can broadcast your desktop for all users to see (requires lastest version of Java for presenter only).</p> - </div> - </div> - </div> - - - <div class='row'> - <div class='span four first'> - <i class="fa fa-microphone"></i> - - <div class="bbb-features-content"> - <h3>WebRTC Audio</h3> - <p>Users of Chrome and FireFox browsers will benefit from high-quality, low-latency WebRTC audio. (Users of other browsers will seamlessly use Flash-based audio.)</p> - </div> - </div> - - <div class='span four'> - <i class="fa fa-bar-chart-o"></i> - - <div class="bbb-features-content"> - <h3>Presentation</h3> - <p>You can upload any PDF presentation or MS office document. BigBlueButton keeps everyone in sync with your current slide, zoom, pan, annotations, and mouse pointer.</p> - </div> - </div> - - <div class='span four last'> - <i class="fa fa-video-camera"></i> - - <div class="bbb-features-content"> - <h3>Web Cam</h3> - <p>Multiple users can share their webcam at the same time. There is no built-in limit on the number of simultaneously active webcams.</p> - </div> - </div> - </div> - </div> - <hr class="featurette-divider"> - - <!-- BigBlueButton Videos --> - <div id="video" class="bbb-videos"> - <div class='row'> - <div class='featurette-heading'> - <h2>Getting Started Quickly</h2> - </div> - - <div class='span four first video-item'> - <a href="https://www.youtube.com/watch?v=4Y__UsUrRx0&feature=youtu.be" target="_blank"> - <div class="video-btn"><i class="fa fa-play-circle-o"></i></div> - <img src="images/bbb-setup-audio.jpg" alt="Setting Up Audio"/> - </a> - <h3><a href="https://www.youtube.com/watch?v=4Y__UsUrRx0&feature=youtu.be" title="Setup Audio" target="_blank">Setting Up Audio</a></h3> - </div> - - <div class='span four video-item'> - <a href="https://www.youtube.com/watch?v=oh0bEk3YSwI" target="_blank"> - <div class="video-btn"><i class="fa fa-play-circle-o"></i></div> - <img src="images/bbb-viewer-overview.jpg" alt="BigBlueButton Viewer Overview Video"/> - </a> - <h3><a href="https://www.youtube.com/watch?v=oh0bEk3YSwI;feature=youtu.be" title="Student Overview" target="_blank">Viewer Overview</a></h3> - </div> - - <div class='span four last video-item'> - <a href="https://www.youtube.com/watch?v=J9mbw00P9W0&feature=youtu.be" target="_blank"> - <div class="video-btn"><i class="fa fa-play-circle-o"></i></div> - <img src="images/bbb-presenter-overview.jpg" alt="Moderator/Presenter Overview Video"/> - </a> - <h3><a href="https://www.youtube.com/watch?v=J9mbw00P9W0&feature=youtu.be" title="Moderator/Presenter Overview" target="_blank">Moderator/Presenter Overview</a></h3> - </div> - </div> - </div> - - </div> - </div> - - <!-- Footer --> - <footer> - <div class='container'> - - <div class="row"> - <div class="span six first"> - <p>BigBlueButton and the BigBlueButton logo are trademarks of <a href="http://bigbluebutton.org/">BigBlueButton Inc.</a></p> - </div> - <div class="span six last"> - <ul> - <li> - Follow Us: - </li> - <li><a class="twitter" href="http://www.twitter.com/bigbluebutton" title="BigBlueButton Twitter Page" target="_blank"><i class="fa fa-twitter"></i></a></li> - <li><a class="facebook" href="http://www.facebook.com/bigbluebutton" title="BigBlueButton Facebook Page" target="_blank"><i class="fa fa-facebook"></i></a></li> - <li><a class="youtube" href="http://www.youtube.com/bigbluebuttonshare" title="BigBlueButton YouTube Page" target="_blank"><i class="fa fa-youtube"></i> </a></li> - <li><a class="google" href="http://google.com/+bigbluebutton" title="BigBlueButton Google Plus" target="_blank"><i class="fa fa-google-plus"></i></a></li> - </ul> - </div> - </div> - - - <div class="row"> - <div class="span twelve center"> - <p>Copyright © 2015 BigBlueButton Inc.<br> - <small>Version <a href="http://docs.bigbluebutton.org/">0.9.0-RC</a></small> - </p> - </div> - </div> - </div> - </footer> - </body> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script type="text/javascript"> +<!-- +window.location = "/demo/demo_mconf.jsp" +//--> +</script> +</head> +<body> +</body> </html> diff --git a/bigbluebutton-web/grails-app/conf/UrlMappings.groovy b/bigbluebutton-web/grails-app/conf/UrlMappings.groovy index c6175a35e4afe881e76666616dc65619df0789b3..0833ea82f02574eee5cc580efc78c88804f85eab 100755 --- a/bigbluebutton-web/grails-app/conf/UrlMappings.groovy +++ b/bigbluebutton-web/grails-app/conf/UrlMappings.groovy @@ -40,6 +40,10 @@ class UrlMappings { "/presentation/$conference/$room/$presentation_name/textfiles/$id"(controller:"presentation") { action = [GET:'showTextfile'] } + + "/presentation/$conference/$room/$presentation_name/download"(controller:"presentation") { + action = [GET:'downloadFile'] + } "/api/setConfigXML"(controller:"api") { action = [POST:'setConfigXML'] diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 7db5c4fd8864631030ab3ca146f7cfe76f659569..27ba1cce7d1f28e7cd8bfee2bc5de5bcda73c2fc 100755 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -84,8 +84,8 @@ defaultDialAccessNumber=613-555-1234 # Default welcome message to display when the participant joins the web # conference. This is only used for the old scheduling which will be # removed in the future. Use the API to create a conference. -defaultWelcomeMessage=<br>Welcome to <b>%%CONFNAME%%</b>!<br><br>For help on using BigBlueButton see these (short) <a href="event:http://www.bigbluebutton.org/content/videos"><u>tutorial videos</u></a>.<br><br>To join the audio bridge click the headset icon (upper-left hand corner). Use a headset to avoid causing background noise for others.<br> -defaultWelcomeMessageFooter=This server is running a build of <a href="https://code.google.com/p/bigbluebutton/wiki/090Overview" target="_blank"><u>BigBlueButton 0.9.0-beta</u></a>. +defaultWelcomeMessage=<br>Welcome to <b>%%CONFNAME%%</b>!<br><br>To join the audio bridge click the headset icon (upper-left hand corner). Use a headset to avoid causing background noise for others.<br> +defaultWelcomeMessageFooter= # Default maximum number of users a meeting can have. # Doesn't get enforced yet but is the default value when the create @@ -127,7 +127,6 @@ allowStartStopRecording=true # enters a name and password, she is redirected here to load the client. bigbluebutton.web.serverURL=http://192.168.23.3 - #---------------------------------------------------- # Assign URL where the logged-out participant will be redirected after sign-out. # If "default", it returns to bigbluebutton.web.serverURL @@ -135,7 +134,7 @@ bigbluebutton.web.logoutURL=default # The url of the BigBlueButton client. User's will be redirected here when # successfully joining the meeting. -defaultClientUrl=${bigbluebutton.web.serverURL}/client/BigBlueButton.html +defaultClientUrl=${bigbluebutton.web.serverURL}/client/MconfLive.html #defaultClientUrl=http://192.168.0.235/3rd-party.html # The default avatar image to display if nothing is passed on the JOIN API (avatarURL) diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy index 7fe42aec407c7e32c556a8e27f2b00da25fa8d87..9158741ce724bee63d716f5cb3c2075a1dbb1219 100755 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/ApiController.groovy @@ -222,6 +222,12 @@ class ApiController { errors.missingParamError("checksum"); } + String guest; + if (!StringUtils.isEmpty(params.guest) && params.guest.equalsIgnoreCase("true")) + guest = "true"; + else + guest = "false"; + // Do we have a name for the user joining? If none, complain. if(!StringUtils.isEmpty(params.fullName)) { params.fullName = StringUtils.strip(params.fullName); @@ -391,6 +397,7 @@ class ApiController { us.mode = "LIVE" us.record = meeting.isRecord() us.welcome = meeting.getWelcomeMessage() + us.guest = guest us.logoutUrl = meeting.getLogoutUrl(); us.configXML = configxml; @@ -413,7 +420,7 @@ class ApiController { meetingService.addUserSession(session['user-token'], us); // Register user into the meeting. - meetingService.registerUser(us.meetingID, us.internalUserId, us.fullname, us.role, us.externUserID, us.authToken) + meetingService.registerUser(us.meetingID, us.internalUserId, us.fullname, us.role, us.externUserID, us.authToken, us.guest) log.info("Session user token for " + us.fullname + " [" + session['user-token'] + "]") session.setMaxInactiveInterval(SESSION_TIMEOUT); @@ -807,10 +814,16 @@ class ApiController { meetingName(m.getName()) createTime(m.getCreateTime()) createDate(formatPrettyDate(m.getCreateTime())) + voiceBridge(m.getTelVoice()) + dialNumber(m.getDialNumber()) attendeePW(m.getViewerPassword()) moderatorPW(m.getModeratorPassword()) hasBeenForciblyEnded(m.isForciblyEnded() ? "true" : "false") running(m.isRunning() ? "true" : "false") + participantCount(m.getNumUsers()) + listenerCount(m.getNumListenOnly()) + voiceParticipantCount(m.getNumVoiceJoined()) + videoCount(m.getNumVideos()) duration(m.duration) hasUserJoined(m.hasUserJoined()) } @@ -1454,6 +1467,7 @@ class ApiController { internalUserID = newInternalUserID authToken = us.authToken role = us.role + guest = us.guest conference = us.conference room = us.room voicebridge = us.voicebridge @@ -1652,6 +1666,7 @@ class ApiController { // Everything is good so far. Translate the external meeting ids to an internal meeting ids. ArrayList<String> internalMeetingIds = paramsProcessorUtil.convertToInternalMeetingId(externalMeetingIds); HashMap<String,Recording> recs = meetingService.getRecordings(internalMeetingIds); + recs = meetingService.filterRecordingsByMetadata(recs, ParamsProcessorUtil.processMetaParam(params)); if (recs.isEmpty()) { response.addHeader("Cache-Control", "no-cache") @@ -1701,6 +1716,17 @@ class ApiController { mkp.yield(item.getExtensions()) } } + } + download() { + r.getDownloads().each { item -> + format{ + type(item.getFormat()) + url(item.getUrl()) + md5(item.getMd5()) + key(item.getKey()) + length(item.getLength()) + } + } } } @@ -1987,6 +2013,7 @@ class ApiController { returncode(RESP_CODE_SUCCESS) meetingName(meeting.getName()) meetingID(meeting.getExternalId()) + internalMeetingID(meeting.getInternalId()) createTime(meeting.getCreateTime()) createDate(formatPrettyDate(meeting.getCreateTime())) voiceBridge(meeting.getTelVoice()) @@ -2001,6 +2028,8 @@ class ApiController { startTime(meeting.getStartTime()) endTime(meeting.getEndTime()) participantCount(meeting.getNumUsers()) + listenerCount(meeting.getNumListenOnly()) + voiceParticipantCount(meeting.getNumVoiceJoined()) maxUsers(meeting.getMaxUsers()) moderatorCount(meeting.getNumModerators()) attendees() { @@ -2009,6 +2038,16 @@ class ApiController { userID("${att.externalUserId}") fullName("${att.fullname}") role("${att.role}") + guest("${att.guest}") + waitingForAcceptance("${att.waitingForAcceptance}") + isPresenter("${att.isPresenter()}") + isListeningOnly("${att.isListeningOnly()}") + hasJoinedVoice("${att.isVoiceJoined()}") + videoStreams(){ + att.getStreams().each { s -> + streamName("${s}") + } + } customdata(){ meeting.getUserCustomData(att.externalUserId).each{ k,v -> "$k"("$v") @@ -2042,6 +2081,8 @@ class ApiController { attendeePW(meeting.getViewerPassword()) moderatorPW(meeting.getModeratorPassword()) createTime(meeting.getCreateTime()) + voiceBridge(meeting.getTelVoice()) + dialNumber(meeting.getDialNumber()) createDate(formatPrettyDate(meeting.getCreateTime())) hasUserJoined(meeting.hasUserJoined()) duration(meeting.duration) diff --git a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy index d82459beb84841c0eea149c2e34c6a80fbf54bfc..cb50e702af5325c8b0c0fe74ab71d30d9ca90fd5 100644 --- a/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy +++ b/bigbluebutton-web/grails-app/controllers/org/bigbluebutton/web/controllers/PresentationController.groovy @@ -79,10 +79,29 @@ class PresentationController { def newFilename = Util.createNewFilename(presId, filenameExt) def pres = new File(uploadDir.absolutePath + File.separatorChar + newFilename ) file.transferTo(pres) + + def isDownloadable = params.boolean('is_downloadable') //instead of params.is_downloadable + + if(isDownloadable) { + log.debug "@Creating download directory..." + File downloadDir = Util.downloadPresentationDirectory(uploadDir.absolutePath) + if (downloadDir != null) { + def notValidCharsRegExp = /[^0-9a-zA-Z_\.]/ + def downloadableFileName = presFilename.replaceAll(notValidCharsRegExp, '-') + def downloadableFile = new File( downloadDir.absolutePath + File.separatorChar + downloadableFileName ) + downloadableFile << pres.newInputStream() + } + } def presentationBaseUrl = presentationService.presentationBaseUrl UploadedPresentation uploadedPres = new UploadedPresentation(meetingId, presId, presFilename, presentationBaseUrl); - uploadedPres.setUploadedFile(pres); + + if(isDownloadable) { + log.debug "@Setting file to be downloadable..." + uploadedPres.setDownloadable(); + } + + uploadedPres.setUploadedFile(pres); presentationService.processUploadedPresentation(uploadedPres) } } else { @@ -211,6 +230,33 @@ class PresentationController { return null; } + + def downloadFile = { + def presentationName = params.presentation_name + def conf = params.conference + def rm = params.room + println "Controller: Download request for $presentationName" + + InputStream is = null; + try { + def pres = presentationService.getFile(conf, rm, presentationName) + if (pres.exists()) { + println "Controller: Sending pdf reply for $presentationName" + + def bytes = pres.readBytes() + def responseName = pres.getName(); + response.addHeader("content-disposition", "filename=$responseName") + response.addHeader("Cache-Control", "no-cache") + response.outputStream << bytes; + } else { + println "$pres does not exist." + } + } catch (IOException e) { + println("Error reading file.\n" + e.getMessage()); + } + + return null; + } def show = { //def filename = params.id.replace('###', '.') diff --git a/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy b/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy index 2b1f6b0cc22f96f6db24bf59f1a0532b23897d8c..791e6f6c79d9be3771236d9a72bc78fc45230921 100644 --- a/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy +++ b/bigbluebutton-web/grails-app/services/org/bigbluebutton/web/services/PresentationService.groovy @@ -1,173 +1,183 @@ -/** -* 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.web.services - -import java.util.concurrent.*; -import java.lang.InterruptedException -import org.bigbluebutton.presentation.DocumentConversionService -import org.bigbluebutton.presentation.UploadedPresentation - -class PresentationService { - - static transactional = false - DocumentConversionService documentConversionService - def presentationDir - def testConferenceMock - def testRoomMock - def testPresentationName - def testUploadedPresentation - def defaultUploadedPresentation - def presentationBaseUrl - - def deletePresentation = {conf, room, filename -> - def directory = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + filename) - deleteDirectory(directory) - } - - def deleteDirectory = {directory -> - log.debug "delete = ${directory}" - /** - * Go through each directory and check if it's not empty. - * We need to delete files inside a directory before a - * directory can be deleted. - **/ - File[] files = directory.listFiles(); - for (int i = 0; i < files.length; i++) { - if (files[i].isDirectory()) { - deleteDirectory(files[i]) - } else { - files[i].delete() - } - } - // Now that the directory is empty. Delete it. - directory.delete() - } - - def listPresentations = {conf, room -> - def presentationsList = [] - def directory = roomDirectory(conf, room) - log.debug "directory ${directory.absolutePath}" - if( directory.exists() ){ - directory.eachFile(){ file-> - System.out.println(file.name) - if( file.isDirectory() ) - presentationsList.add( file.name ) - } - } - return presentationsList - } - - def getPresentationDir = { - return presentationDir - } - - def processUploadedPresentation = {uploadedPres -> - // Run conversion on another thread. - Timer t = new Timer(uploadedPres.getName(), false) - - t.runAfter(1000) { - try { - documentConversionService.processDocument(uploadedPres) - } finally { - t.cancel() - } - } - } - - def showSlide(String conf, String room, String presentationName, String id) { - new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "slide-${id}.swf") - } - - def showPngImage(String conf, String room, String presentationName, String id) { - new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "pngs" + File.separatorChar + "slide${id}.png") - } - - def showPresentation = {conf, room, filename -> - new File(roomDirectory(conf, room).absolutePath + File.separatorChar + filename + File.separatorChar + "slides.swf") - } - - def showThumbnail = {conf, room, presentationName, thumb -> - println "Show thumbnails request for $presentationName $thumb" - def thumbFile = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + - "thumbnails" + File.separatorChar + "thumb-${thumb}.png" - log.debug "showing $thumbFile" - - new File(thumbFile) - } - - def showTextfile = {conf, room, presentationName, textfile -> - println "Show textfiles request for $presentationName $textfile" - def txt = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + - "textfiles" + File.separatorChar + "slide-${textfile}.txt" - log.debug "showing $txt" - - new File(txt) - } - - def numberOfThumbnails = {conf, room, name -> - def thumbDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "thumbnails") - thumbDir.listFiles().length - } - - def numberOfPngs = {conf, room, name -> - def PngsDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "pngs") - PngsDir.listFiles().length - } - - def numberOfTextfiles = {conf, room, name -> - log.debug roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles" - def textfilesDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles") - textfilesDir.listFiles().length - } - - def roomDirectory = {conf, room -> - return new File(presentationDir + File.separatorChar + conf + File.separatorChar + room) - } - - def testConversionProcess() { - File presDir = new File(roomDirectory(testConferenceMock, testRoomMock).absolutePath + File.separatorChar + testPresentationName) - - if (presDir.exists()) { - File pres = new File(presDir.getAbsolutePath() + File.separatorChar + testUploadedPresentation) - if (pres.exists()) { - UploadedPresentation uploadedPres = new UploadedPresentation(testConferenceMock, testRoomMock, testPresentationName); - uploadedPres.setUploadedFile(pres); - // Run conversion on another thread. - new Timer().runAfter(1000) - { - documentConversionService.processDocument(uploadedPres) - } - } else { - log.error "${pres.absolutePath} does NOT exist" - } - } else { - log.error "${presDir.absolutePath} does NOT exist." - } - - } -} - -/*** Helper classes **/ -import java.io.FilenameFilter; -import java.io.File; -class PngFilter implements FilenameFilter { - public boolean accept(File dir, String name) { - return (name.endsWith(".png")); - } -} +/** +* 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.web.services + +import java.util.concurrent.*; +import java.lang.InterruptedException +import org.bigbluebutton.presentation.DocumentConversionService +import org.bigbluebutton.presentation.UploadedPresentation + +class PresentationService { + + static transactional = false + DocumentConversionService documentConversionService + def presentationDir + def testConferenceMock + def testRoomMock + def testPresentationName + def testUploadedPresentation + def defaultUploadedPresentation + def presentationBaseUrl + + def deletePresentation = {conf, room, filename -> + def directory = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + filename) + deleteDirectory(directory) + } + + def deleteDirectory = {directory -> + log.debug "delete = ${directory}" + /** + * Go through each directory and check if it's not empty. + * We need to delete files inside a directory before a + * directory can be deleted. + **/ + File[] files = directory.listFiles(); + for (int i = 0; i < files.length; i++) { + if (files[i].isDirectory()) { + deleteDirectory(files[i]) + } else { + files[i].delete() + } + } + // Now that the directory is empty. Delete it. + directory.delete() + } + + def listPresentations = {conf, room -> + def presentationsList = [] + def directory = roomDirectory(conf, room) + log.debug "directory ${directory.absolutePath}" + if( directory.exists() ){ + directory.eachFile(){ file-> + System.out.println(file.name) + if( file.isDirectory() ) + presentationsList.add( file.name ) + } + } + return presentationsList + } + + def getPresentationDir = { + return presentationDir + } + + def processUploadedPresentation = {uploadedPres -> + // Run conversion on another thread. + Timer t = new Timer(uploadedPres.getName(), false) + + t.runAfter(1000) { + try { + documentConversionService.processDocument(uploadedPres) + } finally { + t.cancel() + } + } + } + + def showSlide(String conf, String room, String presentationName, String id) { + new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "slide-${id}.swf") + } + + def showPngImage(String conf, String room, String presentationName, String id) { + new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + "pngs" + File.separatorChar + "slide${id}.png") + } + + def showPresentation = {conf, room, filename -> + new File(roomDirectory(conf, room).absolutePath + File.separatorChar + filename + File.separatorChar + "slides.swf") + } + + def showThumbnail = {conf, room, presentationName, thumb -> + println "Show thumbnails request for $presentationName $thumb" + def thumbFile = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + + "thumbnails" + File.separatorChar + "thumb-${thumb}.png" + log.debug "showing $thumbFile" + + new File(thumbFile) + } + + def showTextfile = {conf, room, presentationName, textfile -> + println "Show textfiles request for $presentationName $textfile" + def txt = roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + + "textfiles" + File.separatorChar + "slide-${textfile}.txt" + log.debug "showing $txt" + + new File(txt) + } + + def numberOfThumbnails = {conf, room, name -> + def thumbDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "thumbnails") + thumbDir.listFiles().length + } + + def numberOfPngs = {conf, room, name -> + def PngsDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "pngs") + PngsDir.listFiles().length + } + + def numberOfTextfiles = {conf, room, name -> + log.debug roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles" + def textfilesDir = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + name + File.separatorChar + "textfiles") + textfilesDir.listFiles().length + } + + def roomDirectory = {conf, room -> + return new File(presentationDir + File.separatorChar + conf + File.separatorChar + room) + } + + def testConversionProcess() { + File presDir = new File(roomDirectory(testConferenceMock, testRoomMock).absolutePath + File.separatorChar + testPresentationName) + + if (presDir.exists()) { + File pres = new File(presDir.getAbsolutePath() + File.separatorChar + testUploadedPresentation) + if (pres.exists()) { + UploadedPresentation uploadedPres = new UploadedPresentation(testConferenceMock, testRoomMock, testPresentationName); + uploadedPres.setUploadedFile(pres); + // Run conversion on another thread. + new Timer().runAfter(1000) + { + documentConversionService.processDocument(uploadedPres) + } + } else { + log.error "${pres.absolutePath} does NOT exist" + } + } else { + log.error "${presDir.absolutePath} does NOT exist." + } + + } + + def getFile = {conf, room, presentationName -> + println "download request for $presentationName" + def fileDirectory = new File(roomDirectory(conf, room).absolutePath + File.separatorChar + presentationName + File.separatorChar + +"download") + //list the files of the download directory ; it must have only 1 file to download + def list = fileDirectory.listFiles() + //new File(pdfFile) + list[0] + } +} + +/*** Helper classes **/ +import java.io.FilenameFilter; +import java.io.File; +class PngFilter implements FilenameFilter { + public boolean accept(File dir, String name) { + return (name.endsWith(".png")); + } +} diff --git a/bigbluebutton-web/src/groovy/org/bigbluebutton/api/RecordingServiceHelperImp.groovy b/bigbluebutton-web/src/groovy/org/bigbluebutton/api/RecordingServiceHelperImp.groovy index 19dc8ed0029aa630a1abd025bb1174a21c518eca..8ff3552641bc55e7442caa5d322a2defc0b42fd7 100755 --- a/bigbluebutton-web/src/groovy/org/bigbluebutton/api/RecordingServiceHelperImp.groovy +++ b/bigbluebutton-web/src/groovy/org/bigbluebutton/api/RecordingServiceHelperImp.groovy @@ -70,6 +70,12 @@ public class RecordingServiceHelperImp implements RecordingServiceHelper { builder.duration(info.getPlaybackDuration()) builder.extension(info.getPlaybackExtensions()) } + builder.download { + builder.format(info.getDownloadFormat()) + builder.link(info.getDownloadLink()) + builder.md5(info.getDownloadMd5()) + builder.key(info.getDownloadKey()) + } Map<String,String> metainfo = info.getMetadata(); builder.meta{ metainfo.keySet().each { key -> @@ -104,7 +110,10 @@ public class RecordingServiceHelperImp implements RecordingServiceHelper { r.setPlaybackLink(rec.playback.link.text()); r.setPlaybackDuration(rec.playback.duration.text()); r.setPlaybackExtensions(rec.playback.extension.children()); - + r.setDownloadFormat(rec.download.format.text()); + r.setDownloadLink(rec.download.link.text()); + r.setDownloadMd5(rec.download.md5.text()); + r.setDownloadKey(rec.download.key.text()); Map<String, String> meta = new HashMap<String, String>(); rec.meta.children().each { anode -> log.debug("metadata: "+anode.name()+" "+anode.text()) diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/MeetingService.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/MeetingService.java index 968f6e72e71b503d0e0ddaf55a76f5826f0b29a4..41a4683dd9c3e59246074c30d7c19d3ee1b8c0e2 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/MeetingService.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/MeetingService.java @@ -21,6 +21,13 @@ package org.bigbluebutton.api; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -29,6 +36,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.*; +import org.bigbluebutton.api.domain.Download; import org.bigbluebutton.api.domain.Meeting; import org.bigbluebutton.api.domain.Playback; import org.bigbluebutton.api.domain.Recording; @@ -47,8 +55,14 @@ import org.bigbluebutton.api.messaging.messages.MeetingStarted; import org.bigbluebutton.api.messaging.messages.RegisterUser; import org.bigbluebutton.api.messaging.messages.RemoveExpiredMeetings; import org.bigbluebutton.api.messaging.messages.UserJoined; +import org.bigbluebutton.api.messaging.messages.UserJoinedVoice; import org.bigbluebutton.api.messaging.messages.UserLeft; +import org.bigbluebutton.api.messaging.messages.UserLeftVoice; +import org.bigbluebutton.api.messaging.messages.UserListeningOnly; +import org.bigbluebutton.api.messaging.messages.UserRoleChanged; +import org.bigbluebutton.api.messaging.messages.UserSharedWebcam; import org.bigbluebutton.api.messaging.messages.UserStatusChanged; +import org.bigbluebutton.api.messaging.messages.UserUnsharedWebcam; import org.bigbluebutton.web.services.ExpiredMeetingCleanupTimerTask; import org.bigbluebutton.web.services.KeepAliveService; import org.slf4j.Logger; @@ -87,8 +101,8 @@ public class MeetingService implements MessageListener { sessions.put(token, user); } - public void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken) { - handle(new RegisterUser(meetingID, internalUserId, fullname, role, externUserID, authToken)); + public void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken, String guest) { + handle(new RegisterUser(meetingID, internalUserId, fullname, role, externUserID, authToken, guest)); } public UserSession getUserSession(String token) { @@ -297,7 +311,7 @@ public class MeetingService implements MessageListener { } private void processRegisterUser(RegisterUser message) { - messagingService.registerUser(message.meetingID, message.internalUserId, message.fullname, message.role, message.externUserID, message.authToken); + messagingService.registerUser(message.meetingID, message.internalUserId, message.fullname, message.role, message.externUserID, message.authToken, message.guest); } public String addSubscription(String meetingId, String event, String callbackURL){ @@ -357,6 +371,10 @@ public class MeetingService implements MessageListener { return recs; } + public Map<String, Recording> filterRecordingsByMetadata(Map<String, Recording> recordings, Map<String, String> metadataFilters) { + return recordingService.filterRecordingsByMetadata(recordings, metadataFilters); + } + public HashMap<String,Recording> reorderRecordings(ArrayList<Recording> olds){ HashMap<String,Recording> map= new HashMap<String, Recording>(); for (Recording r:olds) { @@ -370,24 +388,46 @@ public class MeetingService implements MessageListener { ArrayList<Playback> plays = new ArrayList<Playback>(); - plays.add(new Playback(r.getPlaybackFormat(), r.getPlaybackLink(), - getDurationRecording(r.getPlaybackDuration(), - r.getEndTime(), r.getStartTime()), - r.getPlaybackExtensions())); + if (!r.getPlaybackFormat().isEmpty()) { + plays.add(new Playback(r.getPlaybackFormat(), r.getPlaybackLink(), + getDurationRecording(r.getPlaybackDuration(), + r.getEndTime(), r.getStartTime()), + r.getPlaybackExtensions())); + } r.setPlaybacks(plays); + + ArrayList<Download> downloads = new ArrayList<Download>(); + if (!r.getDownloadFormat().isEmpty()) { + downloads.add(new Download(r.getDownloadFormat(), r.getDownloadLink(), + r.getDownloadMd5(), r.getDownloadKey(), + getDurationRecording(r.getEndTime(), r.getStartTime()))); + } + r.setDownloads(downloads); + map.put(r.getId(), r); } else { Recording rec = map.get(r.getId()); - rec.getPlaybacks().add(new Playback(r.getPlaybackFormat(), r.getPlaybackLink(), - getDurationRecording(r.getPlaybackDuration(), - r.getEndTime(), r.getStartTime()), - r.getPlaybackExtensions())); + if (!r.getPlaybackFormat().isEmpty()) { + rec.getPlaybacks().add(new Playback(r.getPlaybackFormat(), r.getPlaybackLink(), + getDurationRecording(r.getPlaybackDuration(), + r.getEndTime(), r.getStartTime()), + r.getPlaybackExtensions())); + } + if (!r.getDownloadFormat().isEmpty()) { + rec.getDownloads().add(new Download(r.getDownloadFormat(), r.getDownloadLink(), + r.getDownloadMd5(), r.getDownloadKey(), + getDurationRecording(r.getEndTime(), r.getStartTime()))); + } } } return map; } + private int getDurationRecording(String end, String start) { + return getDurationRecording("", end, start); + } + private int getDurationRecording(String playbackDuration, String end, String start) { int duration; try { @@ -549,7 +589,7 @@ public class MeetingService implements MessageListener { log.debug("User joined in meeting[{}]", message.meetingId); Meeting m = getMeeting(message.meetingId); if (m != null) { - User user = new User(message.userId, message.externalUserId, message.name, message.role); + User user = new User(message.userId, message.externalUserId, message.name, message.role, message.guest, message.waitingForAcceptance); m.userJoined(user); log.info("New user in meeting [" + message.meetingId + "] user [" + user.getFullname() + "]"); @@ -561,8 +601,10 @@ public class MeetingService implements MessageListener { logData.put("externalUserId", user.getExternalUserId()); logData.put("username", user.getFullname()); logData.put("role", user.getRole()); - logData.put("event", "user_joined_message"); - logData.put("description", "User had joined the meeting."); + logData.put("guest", user.isGuest()); + logData.put("waitingForAcceptance", user.isWaitingForAcceptance()); + logData.put("event", MessagingConstants.USER_JOINED_EVENT); + logData.put("description", "User has joined the meeting."); Gson gson = new Gson(); String logStr = gson.toJson(logData); @@ -590,8 +632,10 @@ public class MeetingService implements MessageListener { logData.put("externalUserId", user.getExternalUserId()); logData.put("username", user.getFullname()); logData.put("role", user.getRole()); - logData.put("event", "user_left_message"); - logData.put("description", "User had left the meeting."); + logData.put("guest", user.isGuest()); + logData.put("waitingForAcceptance", user.isWaitingForAcceptance()); + logData.put("event", MessagingConstants.USER_LEFT_EVENT); + logData.put("description", "User left the meeting."); Gson gson = new Gson(); String logStr = gson.toJson(logData); @@ -621,6 +665,100 @@ public class MeetingService implements MessageListener { log.warn("The meeting " + message.meetingId + " doesn't exist"); } + public void userJoinedVoice(UserJoinedVoice message) { + Meeting m = getMeeting(message.meetingId); + if (m != null) { + User user = m.getUserById(message.userId); + if(user != null){ + user.setVoiceJoined(true); + log.info("User {} joined the voice conference in the meeting {}", user.getFullname(), message.meetingId); + return; + } + log.warn("The participant " + message.userId + " doesn't exist in the meeting " + message.meetingId); + return; + } + log.warn("The meeting " + message.meetingId + " doesn't exist"); + } + + public void userLeftVoice(UserLeftVoice message) { + Meeting m = getMeeting(message.meetingId); + if (m != null) { + User user = m.getUserById(message.userId); + if(user != null){ + user.setVoiceJoined(false); + log.info("User {} left the voice conference in the meeting {}", user.getFullname(), message.meetingId); + return; + } + log.warn("The participant " + message.userId + " doesn't exist in the meeting " + message.meetingId); + return; + } + log.warn("The meeting " + message.meetingId + " doesn't exist"); + } + + public void userListeningOnly(UserListeningOnly message) { + Meeting m = getMeeting(message.meetingId); + if (m != null) { + User user = m.getUserById(message.userId); + if(user != null){ + user.setListeningOnly(message.listenOnly); + if (message.listenOnly) { + log.info("User {} started to listen only in the meeting {}", user.getFullname(), message.meetingId); + } else { + log.info("User {} stopped to listen only in the meeting {}", user.getFullname(), message.meetingId); + } + return; + } + log.warn("The participant " + message.userId + " doesn't exist in the meeting " + message.meetingId); + return; + } + log.warn("The meeting " + message.meetingId + " doesn't exist"); + } + + public void userSharedWebcam(UserSharedWebcam message) { + Meeting m = getMeeting(message.meetingId); + if (m != null) { + User user = m.getUserById(message.userId); + if(user != null){ + user.addStream(message.stream); + log.info("User {} started to stream {} to the meeting {}", user.getFullname(), message.stream, message.meetingId); + return; + } + log.warn("The participant " + message.userId + " doesn't exist in the meeting " + message.meetingId); + return; + } + log.warn("The meeting " + message.meetingId + " doesn't exist"); + } + + public void userUnsharedWebcam(UserUnsharedWebcam message) { + Meeting m = getMeeting(message.meetingId); + if (m != null) { + User user = m.getUserById(message.userId); + if(user != null){ + user.removeStream(message.stream); + log.info("User {} stopped to stream {} to the meeting {}", user.getFullname(), message.stream, message.meetingId); + return; + } + log.warn("The participant " + message.userId + " doesn't exist in the meeting " + message.meetingId); + return; + } + log.warn("The meeting " + message.meetingId + " doesn't exist"); + } + + private void userRoleChanged(UserRoleChanged message) { + Meeting m = getMeeting(message.meetingId); + if (m != null) { + User user = m.getUserById(message.userId); + if(user != null){ + user.setRole(message.role); + log.debug("Setting new role in meeting " + message.meetingId + " for participant:" + user.getFullname()); + return; + } + log.warn("The participant " + message.userId + " doesn't exist in the meeting " + message.meetingId); + return; + } + log.warn("The meeting " + message.meetingId + " doesn't exist"); + } + private void processMessage(final IMessage message) { Runnable task = new Runnable() { public void run() { @@ -639,6 +777,23 @@ public class MeetingService implements MessageListener { userLeft((UserLeft)message); } else if (message instanceof UserStatusChanged) { updatedStatus((UserStatusChanged)message); + } else if (message instanceof UserRoleChanged) { + userRoleChanged((UserRoleChanged)message); + } else if (message instanceof UserJoinedVoice) { + log.info("Processing voice user joined message."); + userJoinedVoice((UserJoinedVoice)message); + } else if (message instanceof UserLeftVoice) { + log.info("Processing voice user left message."); + userLeftVoice((UserLeftVoice)message); + } else if (message instanceof UserListeningOnly) { + log.info("Processing user listening only message."); + userListeningOnly((UserListeningOnly)message); + } else if (message instanceof UserSharedWebcam) { + log.info("Processing user shared webcam message."); + userSharedWebcam((UserSharedWebcam)message); + } else if (message instanceof UserUnsharedWebcam) { + log.info("Processing user unshared webcam message."); + userUnsharedWebcam((UserUnsharedWebcam)message); } else if (message instanceof RemoveExpiredMeetings) { checkAndRemoveExpiredMeetings(); } else if (message instanceof CreateMeeting) { diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/RecordingService.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/RecordingService.java index 7fe4056edb776890f89ee2e0e8ed53999f7a98d5..a3d187a279a7b6216bd90ec40300d0ae44ec2d2c 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/RecordingService.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/RecordingService.java @@ -23,6 +23,9 @@ import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + import org.bigbluebutton.api.domain.Recording; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,6 +78,28 @@ public class RecordingService { return recs; } + public boolean recordingMatchesMetadata(Recording recording, Map<String, String> metadataFilters) { + for (Map.Entry<String, String> filter : metadataFilters.entrySet()) { + String metadataValue = recording.getMetadata().get(filter.getKey()); + if (metadataValue != null && metadataValue.equals(filter.getValue())) { + // the recording has the metadata specified + // AND the value is the same as the filter + } else { + return false; + } + } + return true; + } + + public Map<String, Recording> filterRecordingsByMetadata(Map<String, Recording> recordings, Map<String, String> metadataFilters) { + Map<String, Recording> resultRecordings = new HashMap<String, Recording>(); + for (Map.Entry<String, Recording> entry : recordings.entrySet()) { + if (recordingMatchesMetadata(entry.getValue(), metadataFilters)) + resultRecordings.put(entry.getKey(), entry.getValue()); + } + return resultRecordings; + } + public boolean existAnyRecording(ArrayList<String> idList){ ArrayList<String> publishList=getAllRecordingIds(publishedDir); ArrayList<String> unpublishList=getAllRecordingIds(unpublishedDir); diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/Util.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/Util.java index e405948a859036a04cd6cbb7104f8eeb0f3ea3c4..730c67972ad9ac614dbe12b11b4cfae520b16bdb 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/Util.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/Util.java @@ -28,4 +28,12 @@ public final class Util { } return null; } + + public static File downloadPresentationDirectory(String uploadDirectory) { + File dir = new File(uploadDirectory + File.separatorChar + "download"); + if (dir.mkdirs()) { + return dir; + } + return null; + } } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Download.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Download.java new file mode 100755 index 0000000000000000000000000000000000000000..c06cb2d379d4c46d697172e8df7ac6071be531b6 --- /dev/null +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Download.java @@ -0,0 +1,47 @@ +package org.bigbluebutton.api.domain; + +public class Download { + private String format; + private String url; + private int length; + private String md5; + private String key; + public Download(String format, String url, String md5, String key, int length) { + this.format = format; + this.url = url; + this.length = length; + this.md5 = md5; + this.key = key; + } + public String getFormat() { + return format; + } + public void setFormat(String format) { + this.format = format; + } + public String getUrl() { + return url; + } + public void setUrl(String url) { + this.url = url; + } + public int getLength() { + return length; + } + public void setLength(int length) { + this.length = length; + } + public void setMd5(String md5) { + this.md5 = md5; + } + public String getMd5() { + return md5; + } + public void setKey(String key) { + this.key = key; + } + public String getKey() { + return key; + } + +} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Meeting.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Meeting.java index 42908cd2b1ba3b2dbf3e3f0134fcf980a3573540..a5b9e7f38a8e09ba8fbca0d06e68f8803a286b2c 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Meeting.java @@ -29,7 +29,6 @@ import java.util.concurrent.ConcurrentMap; import org.apache.commons.lang.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.bigbluebutton.api.MeetingService; public class Meeting { private static Logger log = LoggerFactory.getLogger(Meeting.class); @@ -316,6 +315,33 @@ public class Meeting { private boolean hasEnded() { return endTime > 0; } + + public int getNumListenOnly() { + int sum = 0; + for (String key : users.keySet()) { + User u = (User) users.get(key); + if (u.isListeningOnly()) sum++; + } + return sum; + } + + public int getNumVoiceJoined() { + int sum = 0; + for (String key : users.keySet()) { + User u = (User) users.get(key); + if (u.isVoiceJoined()) sum++; + } + return sum; + } + + public int getNumVideos() { + int sum = 0; + for (String key : users.keySet()) { + User u = (User) users.get(key); + sum += u.getStreams().size(); + } + return sum; + } public void addUserCustomData(String userID, Map<String, String> data) { userCustomData.put(userID, data); diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Recording.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Recording.java index 86e6f2b34113b4aa7f6f7099f2c3ab7ea0a30e4c..a1007b92e401ef4778775ea5c3df57aa8363d795 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Recording.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/Recording.java @@ -36,6 +36,7 @@ public class Recording { private String endTime; private Map<String, String> metadata = new HashMap<String, String>(); private ArrayList<Playback> playbacks=new ArrayList<Playback>(); + private ArrayList<Download> downloads=new ArrayList<Download>(); //TODO: private String state; @@ -44,7 +45,11 @@ public class Recording { private String playbackDuration; private GPathResult playbackExtensions; - + private String downloadLink; + private String downloadFormat; + private String downloadMd5; + private String downloadKey; + public String getId() { return id; } @@ -84,7 +89,37 @@ public class Recording { public void setEndTime(String endTime) { this.endTime = convertOldDateFormat(endTime); } + public String getDownloadLink() { + return downloadLink; + } + + public void setDownloadLink(String downloadLink) { + this.downloadLink = downloadLink; + } + + public String getDownloadFormat() { + return downloadFormat; + } + + public void setDownloadFormat(String downloadFormat) { + this.downloadFormat = downloadFormat; + } + + public String getDownloadMd5() { + return downloadMd5; + } + + public void setDownloadMd5(String downloadMd5) { + this.downloadMd5 = downloadMd5; + } + + public String getDownloadKey() { + return downloadKey; + } + public void setDownloadKey(String downloadKey) { + this.downloadKey = downloadKey; + } public String getPlaybackLink() { return playbackLink; } @@ -141,6 +176,14 @@ public class Recording { this.name = name; } + public ArrayList<Download> getDownloads() { + return downloads; + } + + public void setDownloads(ArrayList<Download> downloads) { + this.downloads = downloads; + } + public ArrayList<Playback> getPlaybacks() { return playbacks; } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/User.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/User.java index 239af9df6cd29fb5e0ef5f3c65ff40d94c112ed1..87a6d9bbb436ece8d99dbb1d6d7d2b3f85455902 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/User.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/User.java @@ -19,6 +19,9 @@ package org.bigbluebutton.api.domain; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -28,13 +31,21 @@ public class User { private String fullname; private String role; private Map<String,String> status; + private Boolean guest; + private Boolean waitingForAcceptance; + private Boolean listeningOnly = false; + private Boolean voiceJoined = false; + private List<String> streams; - public User(String internalUserId, String externalUserId, String fullname, String role) { + public User(String internalUserId, String externalUserId, String fullname, String role, Boolean guest, Boolean waitingForAcceptance) { this.internalUserId = internalUserId; this.externalUserId = externalUserId; this.fullname = fullname; this.role = role; + this.guest = guest; + this.waitingForAcceptance = waitingForAcceptance; this.status = new ConcurrentHashMap<String, String>(); + this.streams = Collections.synchronizedList(new ArrayList<String>()); } public String getInternalUserId() { @@ -51,6 +62,22 @@ public class User { public void setExternalUserId(String externalUserId){ this.externalUserId = externalUserId; } + + public void setGuest(Boolean guest) { + this.guest = guest; + } + + public Boolean isGuest() { + return this.guest; + } + + public void setWaitingForAcceptance(Boolean waitingForAcceptance) { + this.waitingForAcceptance = waitingForAcceptance; + } + + public Boolean isWaitingForAcceptance() { + return this.waitingForAcceptance; + } public String getFullname() { return fullname; @@ -78,4 +105,40 @@ public class User { public Map<String,String> getStatus(){ return this.status; } + + public boolean isPresenter() { + String isPresenter = this.status.get("presenter"); + if (isPresenter != null) { + return isPresenter.equalsIgnoreCase("true"); + } + return false; + } + + public void addStream(String stream) { + streams.add(stream); + } + + public void removeStream(String stream) { + streams.remove(stream); + } + + public List<String> getStreams() { + return streams; + } + + public Boolean isListeningOnly() { + return listeningOnly; + } + + public void setListeningOnly(Boolean listeningOnly) { + this.listeningOnly = listeningOnly; + } + + public Boolean isVoiceJoined() { + return voiceJoined; + } + + public void setVoiceJoined(Boolean voiceJoined) { + this.voiceJoined = voiceJoined; + } } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java index 346b9ecb95aa01e9365d158aaf2e766c7260c9a7..b642f0269e1eaace3da61978c1869d6b053d1ac8 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/domain/UserSession.java @@ -32,6 +32,7 @@ public class UserSession { public String role = null; public String conference = null; public String room = null; + public String guest = "false"; public String voicebridge = null; public String webvoiceconf = null; public String mode = null; diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/Constants.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/Constants.java index 3179e5ff3404cf1f79803f16d7b77247d4d9ada8..a7472fdd4e5e87917395c661b04780b32f68a6ca 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/Constants.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/Constants.java @@ -92,4 +92,6 @@ 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"; + public static final String WAITING_FOR_ACCEPTANCE = "waiting_for_acceptance"; } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MeetingMessageHandler.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MeetingMessageHandler.java index c9cfc8a0ca0a418fc67c85563d7f2fb3e801a1d4..df6037d137dd437c88cc97fda70385830f58bd38 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MeetingMessageHandler.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MeetingMessageHandler.java @@ -9,8 +9,14 @@ import org.bigbluebutton.api.messaging.messages.MeetingDestroyed; import org.bigbluebutton.api.messaging.messages.MeetingEnded; import org.bigbluebutton.api.messaging.messages.MeetingStarted; import org.bigbluebutton.api.messaging.messages.UserJoined; +import org.bigbluebutton.api.messaging.messages.UserJoinedVoice; import org.bigbluebutton.api.messaging.messages.UserLeft; +import org.bigbluebutton.api.messaging.messages.UserLeftVoice; +import org.bigbluebutton.api.messaging.messages.UserListeningOnly; +import org.bigbluebutton.api.messaging.messages.UserRoleChanged; +import org.bigbluebutton.api.messaging.messages.UserSharedWebcam; import org.bigbluebutton.api.messaging.messages.UserStatusChanged; +import org.bigbluebutton.api.messaging.messages.UserUnsharedWebcam; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; @@ -99,9 +105,11 @@ public class MeetingMessageHandler implements MessageHandler { String externuserid = user.get("extern_userid").getAsString(); String username = user.get("name").getAsString(); String role = user.get("role").getAsString(); + Boolean guest = user.get("guest").getAsBoolean(); + Boolean waitingForAcceptance = user.get("waiting_for_acceptance").getAsBoolean(); for (MessageListener listener : listeners) { - listener.handle(new UserJoined(meetingId, userid, externuserid, username, role)); + listener.handle(new UserJoined(meetingId, userid, externuserid, username, role, guest, waitingForAcceptance)); } } else if(MessagingConstants.USER_STATUS_CHANGE_EVENT.equalsIgnoreCase(messageName)) { System.out.println("Handling [" + messageName + "] message."); @@ -121,8 +129,59 @@ public class MeetingMessageHandler implements MessageHandler { for (MessageListener listener : listeners) { listener.handle(new UserLeft(meetingId, userid)); } + } else if (MessagingConstants.USER_JOINED_VOICE_EVENT.equalsIgnoreCase(messageName)) { + System.out.println("Handling [" + messageName + "] message."); + String meetingId = payload.get("meeting_id").getAsString(); + JsonObject user = (JsonObject) payload.get("user"); + + String userid = user.get("userid").getAsString(); + for (MessageListener listener : listeners) { + listener.handle(new UserJoinedVoice(meetingId, userid)); + } + } else if (MessagingConstants.USER_LEFT_VOICE_EVENT.equalsIgnoreCase(messageName)) { + System.out.println("Handling [" + messageName + "] message."); + String meetingId = payload.get("meeting_id").getAsString(); + JsonObject user = (JsonObject) payload.get("user"); + + String userid = user.get("userid").getAsString(); + for (MessageListener listener : listeners) { + listener.handle(new UserLeftVoice(meetingId, userid)); + } + } else if (MessagingConstants.USER_LISTEN_ONLY_EVENT.equalsIgnoreCase(messageName)) { + System.out.println("Handling [" + messageName + "] message."); + String meetingId = payload.get("meeting_id").getAsString(); + String userid = payload.get("userid").getAsString(); + Boolean listenOnly = payload.get("listen_only").getAsBoolean(); + + for (MessageListener listener : listeners) { + listener.handle(new UserListeningOnly(meetingId, userid, listenOnly)); + } + } else if (MessagingConstants.USER_SHARE_WEBCAM_EVENT.equalsIgnoreCase(messageName)) { + System.out.println("Handling [" + messageName + "] message."); + String meetingId = payload.get("meeting_id").getAsString(); + String userid = payload.get("userid").getAsString(); + String stream = payload.get("stream").getAsString(); + for (MessageListener listener : listeners) { + listener.handle(new UserSharedWebcam(meetingId, userid, stream)); + } + } else if (MessagingConstants.USER_UNSHARE_WEBCAM_EVENT.equalsIgnoreCase(messageName)) { + System.out.println("Handling [" + messageName + "] message."); + String meetingId = payload.get("meeting_id").getAsString(); + String userid = payload.get("userid").getAsString(); + String stream = payload.get("stream").getAsString(); + for (MessageListener listener : listeners) { + listener.handle(new UserUnsharedWebcam(meetingId, userid, stream)); + } + } else if(MessagingConstants.USER_ROLE_CHANGE_EVENT.equalsIgnoreCase(messageName)) { + System.out.println("Handling [" + messageName + "] message."); + String meetingId = payload.get("meeting_id").getAsString(); + String userid = payload.get("userid").getAsString(); + String role = payload.get("role").getAsString(); + for (MessageListener listener : listeners) { + listener.handle(new UserRoleChanged(meetingId, userid, role)); + } } - } + } } } } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessageToJson.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessageToJson.java index 372950a73e1b6dd40ee3c654b1e8c6d9371e2ba0..c03e76c49684c580e68ad0eae92d60d9aa0a9625 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessageToJson.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessageToJson.java @@ -18,6 +18,7 @@ public class MessageToJson { payload.put(Constants.ROLE, message.role); payload.put(Constants.EXT_USER_ID, message.externUserID); payload.put(Constants.AUTH_TOKEN, message.authToken); + payload.put(Constants.GUEST, message.guest); java.util.HashMap<String, Object> header = MessageBuilder.buildHeader(RegisterUserMessage.REGISTER_USER, message.VERSION, null); diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessagingConstants.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessagingConstants.java index 7403792ea7fb510bcf5ba4f5fa61ac7152451b4e..33cc69c1dd6a459cdd5fefc71e995c3034b32946 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessagingConstants.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessagingConstants.java @@ -47,7 +47,12 @@ public class MessagingConstants { public static final String USER_JOINED_EVENT = "user_joined_message"; public static final String USER_LEFT_EVENT = "user_left_message"; public static final String USER_STATUS_CHANGE_EVENT = "user_status_changed_message"; - + public static final String USER_JOINED_VOICE_EVENT = "user_joined_voice_message"; + public static final String USER_LEFT_VOICE_EVENT = "user_left_voice_message"; + public static final String USER_LISTEN_ONLY_EVENT = "user_listening_only"; + public static final String USER_SHARE_WEBCAM_EVENT = "user_shared_webcam_message"; + public static final String USER_UNSHARE_WEBCAM_EVENT = "user_unshared_webcam_message"; + public static final String USER_ROLE_CHANGE_EVENT = "user_role_changed_message"; public static final String SEND_POLLS_EVENT = "SendPollsEvent"; public static final String KEEP_ALIVE_REPLY = "keep_alive_reply"; diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessagingService.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessagingService.java index d7069e4945ea17743b6892d29e60fc0fff0123c1..a824b4e848d8b3f37c1e2d29be421336462ce7e5 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessagingService.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/MessagingService.java @@ -35,6 +35,6 @@ public interface MessagingService { String storeSubscription(String meetingId, String externalMeetingID, String callbackURL); boolean removeSubscription(String meetingId, String subscriptionId); List<Map<String,String>> listSubscriptions(String meetingId); - void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken); + void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken, String guest); void sendKeepAlive(String keepAliveId); } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/RedisMessagingService.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/RedisMessagingService.java index 00423be8b08fc38aec58438ed9dc2f5e65bf0298..4a8253734f1a26b88e3a4252f8f3d58ccb9ca13e 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/RedisMessagingService.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/RedisMessagingService.java @@ -58,8 +58,8 @@ public class RedisMessagingService implements MessagingService { sender.send(MessagingConstants.TO_MEETING_CHANNEL, json); } - public void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken) { - RegisterUserMessage msg = new RegisterUserMessage(meetingID, internalUserId, fullname, role, externUserID, authToken); + public void registerUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken, String guest) { + RegisterUserMessage msg = new RegisterUserMessage(meetingID, internalUserId, fullname, role, externUserID, authToken, guest); String json = MessageToJson.registerUserToJson(msg); log.info("Sending register user message to bbb-apps:[{}]", json); sender.send(MessagingConstants.TO_MEETING_CHANNEL, json); diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/converters/messages/RegisterUserMessage.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/converters/messages/RegisterUserMessage.java index d9593dc34400c5c23d377566f07422ff057f652a..d514915a92d6076900e50e138522611fb3ba1f13 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/converters/messages/RegisterUserMessage.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/converters/messages/RegisterUserMessage.java @@ -10,13 +10,15 @@ public class RegisterUserMessage { public final String role; public final String externUserID; public final String authToken; + public final String 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, String guest) { this.meetingID = meetingID; this.internalUserId = internalUserId; this.fullname = fullname; this.role = role; this.externUserID = externUserID; this.authToken = authToken; + this.guest = guest; } } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/RegisterUser.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/RegisterUser.java index 9a9d3f48e7cdb9e0540ec833be525624b72846da..e3b93c7aaaecb32874304762db5fda1ac4a3ab39 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/RegisterUser.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/RegisterUser.java @@ -8,13 +8,15 @@ public class RegisterUser implements IMessage { public final String role; public final String externUserID; public final String authToken; + public final String guest; - public RegisterUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken) { + public RegisterUser(String meetingID, String internalUserId, String fullname, String role, String externUserID, String authToken, String guest) { this.meetingID = meetingID; this.internalUserId = internalUserId; this.fullname = fullname; this.role = role; this.externUserID = externUserID; this.authToken = authToken; + this.guest = guest; } } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserJoined.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserJoined.java index 1e415f1af2d8d8caff6eea2c177412bb44608bd2..0970fcea4876559cc2a4701b2ba0c704fe2f84f8 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserJoined.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserJoined.java @@ -6,12 +6,16 @@ public class UserJoined implements IMessage { public final String externalUserId; public final String name; public final String role; + public final Boolean guest; + public final Boolean waitingForAcceptance; - public UserJoined(String meetingId, String userId, String externalUserId, String name, String role) { + public UserJoined(String meetingId, String userId, String externalUserId, String name, String role, Boolean guest, Boolean waitingForAcceptance) { this.meetingId = meetingId; this.userId = userId; this.externalUserId = externalUserId; this.name = name; this.role = role; + this.guest = guest; + this.waitingForAcceptance = waitingForAcceptance; } } diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserJoinedVoice.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserJoinedVoice.java new file mode 100644 index 0000000000000000000000000000000000000000..95e9e76ee0901958b7ea472acb416122a2f55d94 --- /dev/null +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserJoinedVoice.java @@ -0,0 +1,11 @@ +package org.bigbluebutton.api.messaging.messages; + +public class UserJoinedVoice implements IMessage { + public final String userId; + public final String meetingId; + + public UserJoinedVoice(String meetingId, String userId) { + this.meetingId = meetingId; + this.userId = userId; + } +} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserLeftVoice.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserLeftVoice.java new file mode 100644 index 0000000000000000000000000000000000000000..b4060b2731267d9b1f4187f9612d320c45853a76 --- /dev/null +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserLeftVoice.java @@ -0,0 +1,11 @@ +package org.bigbluebutton.api.messaging.messages; + +public class UserLeftVoice implements IMessage { + public final String userId; + public final String meetingId; + + public UserLeftVoice(String meetingId, String userId) { + this.meetingId = meetingId; + this.userId = userId; + } +} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserListeningOnly.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserListeningOnly.java new file mode 100644 index 0000000000000000000000000000000000000000..fde4d2a28e995d8e5fc412e4ab936e8136ae407c --- /dev/null +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserListeningOnly.java @@ -0,0 +1,13 @@ +package org.bigbluebutton.api.messaging.messages; + +public class UserListeningOnly implements IMessage { + public final String userId; + public final String meetingId; + public final Boolean listenOnly; + + public UserListeningOnly(String meetingId, String userId, Boolean listenOnly) { + this.meetingId = meetingId; + this.userId = userId; + this.listenOnly = listenOnly; + } +} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserRoleChanged.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserRoleChanged.java new file mode 100644 index 0000000000000000000000000000000000000000..1373dc9e5ef6c733b8486c3a4cca2c1c87be14e8 --- /dev/null +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserRoleChanged.java @@ -0,0 +1,13 @@ +package org.bigbluebutton.api.messaging.messages; + +public class UserRoleChanged implements IMessage { + public final String meetingId; + public final String userId; + public final String role; + + public UserRoleChanged(String meetingId, String userId, String role) { + this.meetingId = meetingId; + this.userId = userId; + this.role = role; + } +} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserSharedWebcam.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserSharedWebcam.java new file mode 100644 index 0000000000000000000000000000000000000000..826255a2523239bbbfc38a8db42c5014ee5e93d5 --- /dev/null +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserSharedWebcam.java @@ -0,0 +1,13 @@ +package org.bigbluebutton.api.messaging.messages; + +public class UserSharedWebcam implements IMessage { + public final String userId; + public final String meetingId; + public final String stream; + + public UserSharedWebcam(String meetingId, String userId, String stream) { + this.meetingId = meetingId; + this.userId = userId; + this.stream = stream; + } +} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserUnsharedWebcam.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserUnsharedWebcam.java new file mode 100644 index 0000000000000000000000000000000000000000..3f8c5622af6c1e628354e19a3e451c6c657ee191 --- /dev/null +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/UserUnsharedWebcam.java @@ -0,0 +1,13 @@ +package org.bigbluebutton.api.messaging.messages; + +public class UserUnsharedWebcam implements IMessage { + public final String userId; + public final String meetingId; + public final String stream; + + public UserUnsharedWebcam(String meetingId, String userId, String stream) { + this.meetingId = meetingId; + this.userId = userId; + this.stream = stream; + } +} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/ConversionUpdateMessage.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/ConversionUpdateMessage.java index 126d1db12d114bdb57f09686fe77746a599ded56..3dd547398b6b5caad4794b8ff61e3d9c62923b27 100644 --- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/ConversionUpdateMessage.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/ConversionUpdateMessage.java @@ -44,6 +44,7 @@ public class ConversionUpdateMessage { message.put("presentationName", pres.getId()); message.put("presentationId", pres.getId()); message.put("filename", pres.getName()); + message.put("presDownloadable", pres.isDownloadable()); } public MessageBuilder entry(String key, Object value) { diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/UploadedPresentation.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/UploadedPresentation.java index ffa5da576c38d1d509f1c4e85c0c260b175bd160..742236fb1b94ba0cfcb69cf6302369d13325ca15 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/UploadedPresentation.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/UploadedPresentation.java @@ -30,6 +30,7 @@ public final class UploadedPresentation { private int numberOfPages = 0; private boolean lastStepSuccessful = false; private final String baseUrl; + private boolean isDownloadable = false; public UploadedPresentation(String meetingId, String id, String name, @@ -38,6 +39,15 @@ public final class UploadedPresentation { this.id = id; this.name = name; this.baseUrl = baseUrl; + this.isDownloadable = false; + } + + public boolean isDownloadable() { + return isDownloadable; + } + + public void setDownloadable() { + this.isDownloadable = true; } public File getUploadedFile() { diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/Pdf2SwfPageConverter.java b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/Pdf2SwfPageConverter.java index 09b2342f686e3d1a2a7d5d6fe2035d2949ae1af6..b79a1a654a8fcc49f30d5f53afa454432ea3fc3c 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/Pdf2SwfPageConverter.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/presentation/imp/Pdf2SwfPageConverter.java @@ -33,9 +33,11 @@ public class Pdf2SwfPageConverter implements PageConverter { public boolean convert(File presentation, File output, int page) { String source = presentation.getAbsolutePath(); String dest = output.getAbsolutePath(); - String AVM2SWF = "-T9"; + /* Using flash taget version 7 to support Adobe AIR. Otherwise pdf2swf will / + / insert an unsupported command "allowDomain" into the generated swf files */ + String flashVersion = "-T7"; - String COMMAND = SWFTOOLS_DIR + File.separator + "pdf2swf " + AVM2SWF + " -F " + fontsDir + " -p " + page + " " + source + " -o " + dest; + String COMMAND = SWFTOOLS_DIR + File.separator + "pdf2swf " + flashVersion + " -F " + fontsDir + " -p " + page + " " + source + " -o " + dest; log.debug("Executing: " + COMMAND); boolean done = new ExternalProcessExecutor().exec(COMMAND, 60000); @@ -44,7 +46,7 @@ public class Pdf2SwfPageConverter implements PageConverter { if (done && destFile.exists()) { return true; } else { - COMMAND = SWFTOOLS_DIR + File.separator + "pdf2swf " + AVM2SWF + " -s poly2bitmap -F " + fontsDir + " -p " + page + " " + source + " -o " + dest; + COMMAND = SWFTOOLS_DIR + File.separator + "pdf2swf " + flashVersion + " -s poly2bitmap -F " + fontsDir + " -p " + page + " " + source + " -o " + dest; log.debug("Converting graphics to bitmaps"); log.debug("Executing: " + COMMAND); done = new ExternalProcessExecutor().exec(COMMAND, 60000); diff --git a/record-and-playback/core/lib/recordandplayback.rb b/record-and-playback/core/lib/recordandplayback.rb index 43b15e1fd0eda979b1300e91b6a08f1a171150f6..9f1b51859a4114847f27a232e7c34ebdd20c53ba 100755 --- a/record-and-playback/core/lib/recordandplayback.rb +++ b/record-and-playback/core/lib/recordandplayback.rb @@ -31,6 +31,7 @@ require 'recordandplayback/deskshare_archiver' require 'recordandplayback/generators/events' require 'recordandplayback/generators/audio' require 'recordandplayback/generators/video' +require 'recordandplayback/generators/mconf_processor' require 'recordandplayback/generators/matterhorn_processor' require 'recordandplayback/generators/audio_processor' require 'recordandplayback/generators/presentation' diff --git a/record-and-playback/core/lib/recordandplayback/generators/mconf_processor.rb b/record-and-playback/core/lib/recordandplayback/generators/mconf_processor.rb new file mode 100755 index 0000000000000000000000000000000000000000..cff98408f4d273719a119d69613cace44ff2b77a --- /dev/null +++ b/record-and-playback/core/lib/recordandplayback/generators/mconf_processor.rb @@ -0,0 +1,54 @@ +# Set encoding to utf-8 +# 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/>. +# + + +require 'rubygems' +require 'fileutils' +require 'builder' +require 'mime/types' +require 'digest/md5' +require 'zip' + +module BigBlueButton + + class MconfProcessor + def self.zip_directory (directory, zipped_file) + BigBlueButton.logger.info("Task: Zipping directory... #{zipped_file} #{directory}") + #files = [webcam, deskshare, dublincore, manifest] + Zip::File.open(zipped_file, Zip::File::CREATE) do |zipfile| + Dir["#{directory}/**/**"].reject{|f|f==zipped_file}.each do |file| + zipfile.add(file.sub(directory+'/', ''), file) + end + end + end + + def self.unzip(unzip_dir, zipfile) + Zip::File.open(zipfile) do |zip_file| + zip_file.each do |f| + f_path=File.join(unzip_dir, f.name) + FileUtils.mkdir_p(File.dirname(f_path)) + zip_file.extract(f, f_path) unless File.exist?(f_path) + end + end + end + + end +end diff --git a/record-and-playback/core/scripts/rap-worker.rb b/record-and-playback/core/scripts/rap-worker.rb index c299fad126d5035a78a9799c64f612211f11b931..a9d49b85c7e0c4a864bbbfad1516a0817ba31fa8 100755 --- a/record-and-playback/core/scripts/rap-worker.rb +++ b/record-and-playback/core/scripts/rap-worker.rb @@ -150,7 +150,9 @@ def process_archived_meeting(recording_dir) step_stop_time = BigBlueButton.monotonic_clock step_time = step_stop_time - step_start_time - IO.write("#{recording_dir}/process/#{process_type}/#{meeting_id}/processing_time", step_time) + if BigBlueButton.dir_exists? "#{recording_dir}/process/#{process_type}/#{meeting_id}" + IO.write("#{recording_dir}/process/#{process_type}/#{meeting_id}/processing_time", step_time) + end step_succeeded = (ret == 0 and File.exists?(processed_done)) @@ -199,6 +201,13 @@ def publish_processed_meeting(recording_dir) match2 = /([^\/]*).rb$/.match(publish_script) publish_type = match2[1] + # Do not try to publish a format that failed in the process phase + processed_done = "#{recording_dir}/status/processed/#{meeting_id}-#{publish_type}.done" + if !File.exists?(processed_done) + publish_succeeded = false + next + end + published_done = "#{recording_dir}/status/published/#{meeting_id}-#{publish_type}.done" next if File.exists?(published_done) @@ -257,6 +266,24 @@ def publish_processed_meeting(recording_dir) end end +def clean_presentation_dependents(recording_dir) + # clean workspace so the formats that depend on the presentation format to be + # published will run + [ "presentation_export" ].each do |dependent_format| + presentation_published_done_files = Dir.glob("#{recording_dir}/status/published/*-presentation.done") + presentation_published_done_files.each do |published_done| + match = /([^\/]*)-([^\/-]*).done$/.match(published_done) + meeting_id = match[1] + process_type = match[2] + processed_fail = "#{recording_dir}/status/processed/#{meeting_id}-#{dependent_format}.fail" + if File.exists? processed_fail + BigBlueButton.logger.info "Removing #{processed_fail} so #{dependent_format} can execute in the next run of rap-worker" + FileUtils.rm processed_fail + end + end + end +end + def post_archive(meeting_id) Dir.glob("post_archive/*.rb").sort.each do |post_archive_script| match = /([^\/]*).rb$/.match(post_archive_script) @@ -351,6 +378,7 @@ begin sanity_archived_meeting(recording_dir) process_archived_meeting(recording_dir) publish_processed_meeting(recording_dir) + clean_presentation_dependents(recording_dir) BigBlueButton.logger.debug("rap-worker done") diff --git a/record-and-playback/deploy.sh b/record-and-playback/deploy.sh index cc7ac6ea90daad0d8f0bc5e32d95d9d21ad4caea..5905074ba46e9c3106154fb185a70b098b2419e4 100755 --- a/record-and-playback/deploy.sh +++ b/record-and-playback/deploy.sh @@ -38,7 +38,16 @@ function deploy_format() { done } -deploy_format "presentation" +RECORDING_SERVER=true +if $RECORDING_SERVER ; then + deploy_format "presentation" + deploy_format "presentation_export" + deploy_format "mconf_decrypter" + sudo mv /usr/local/bigbluebutton/core/scripts/mconf-recording-decrypter.initd /etc/init.d/mconf-recording-decrypter + sudo mv /usr/local/bigbluebutton/core/scripts/mconf-recording-decrypter.monit /etc/monit/conf.d/mconf-recording-decrypter +else + deploy_format "mconf_encrypted" +fi sudo mkdir -p /var/bigbluebutton/playback/ sudo mkdir -p /var/bigbluebutton/recording/raw/ @@ -49,10 +58,12 @@ sudo mkdir -p /var/bigbluebutton/recording/status/archived/ sudo mkdir -p /var/bigbluebutton/recording/status/processed/ sudo mkdir -p /var/bigbluebutton/recording/status/sanity/ -sudo mv /usr/local/bigbluebutton/core/scripts/*.nginx /etc/bigbluebutton/nginx/ sudo chown -R tomcat7:tomcat7 /var/bigbluebutton/ /var/log/bigbluebutton/ sudo chown -R red5:red5 /var/bigbluebutton/deskshare/ sudo chown -R freeswitch:daemon /var/bigbluebutton/meetings/ cd /usr/local/bigbluebutton/core/ -sudo bundle install \ No newline at end of file +sudo bundle install + +sudo mv /usr/local/bigbluebutton/core/scripts/*.nginx /etc/bigbluebutton/nginx/ +sudo service nginx reload diff --git a/record-and-playback/mconf_decrypter/scripts/mconf-decrypter.rb b/record-and-playback/mconf_decrypter/scripts/mconf-decrypter.rb new file mode 100755 index 0000000000000000000000000000000000000000..8a3effc267ef475a2bf39d2492dcfb09144cc315 --- /dev/null +++ b/record-and-playback/mconf_decrypter/scripts/mconf-decrypter.rb @@ -0,0 +1,168 @@ +#!/usr/bin/ruby +# Set encoding to utf-8 +# 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/>. +# + +require '../../core/lib/recordandplayback' +require 'rubygems' +require 'yaml' +require 'net/http' +require 'rexml/document' +require 'open-uri' +require 'digest/md5' + +BigBlueButton.logger = Logger.new("/var/log/bigbluebutton/mconf_decrypter.log",'daily' ) +#BigBlueButton.logger = Logger.new(STDOUT) + +bbb_props = YAML::load(File.open('../../core/scripts/bigbluebutton.yml')) +mconf_props = YAML::load(File.open('mconf-decrypter.yml')) + +# these properties must be global variables (starting with $) +$private_key = mconf_props['private_key'] +$get_recordings_url = mconf_props['get_recordings_url'] +$recording_dir = bbb_props['recording_dir'] +$raw_dir = "#{$recording_dir}/raw" +$archived_dir = "#{$recording_dir}/status/archived" + +def fetchRecordings(url) + #BigBlueButton.logger.debug("Fetching #{url}") + doc = nil + begin + doc = Nokogiri::XML(Net::HTTP.get_response(URI.parse(url)).body) + returncode = doc.xpath("//returncode") + if returncode.empty? or returncode.text != "SUCCESS" + BigBlueButton.logger.error "getRecordings didn't return success:\n#{doc.to_xml(:indent => 2)}" + return false + end + rescue + BigBlueButton.logger.error("Exception occurred: #{$!}") + return false + end + + doc.xpath("//recording").each do |recording| + record_id = recording.xpath(".//recordID").text + recording.xpath(".//download/format").each do |format| + type = format.xpath(".//type").text + if type == "encrypted" + meeting_id = record_id + file_url = format.xpath(".//url").text + key_file_url = format.xpath(".//key").text + md5_value = format.xpath(".//md5").text + + encrypted_file = file_url.split("/").last + decrypted_file = File.basename(encrypted_file, '.*') + ".zip" + # can't check only for archived.done because when the file is published, the archived flag is removed + # so we check for any .done file + if not Dir.glob("#{$recording_dir}/status/**/#{record_id}*.done").empty? then + Dir.chdir($raw_dir) do + BigBlueButton.logger.info("Next recording to be processed is #{meeting_id}") + + BigBlueButton.logger.debug("Removing any file previously downloaded related to this recording") + FileUtils.rm_r Dir.glob("#{$raw_dir}/#{record_id}*"), :force => true + + BigBlueButton.logger.debug("recordID = #{record_id}") + BigBlueButton.logger.debug("file_url = #{file_url}") + BigBlueButton.logger.debug("key_file_url = #{key_file_url}") + BigBlueButton.logger.debug("md5_value = #{md5_value}") + + BigBlueButton.logger.info("Downloading the encrypted file to #{encrypted_file}") + + begin + writeOut = open(encrypted_file, "wb") + writeOut.write(open(file_url).read) + writeOut.close + rescue Exception => e + BigBlueButton.logger.error "Failed to download the encrypted file: #{e.to_s}" + next + end + + md5_calculated = Digest::MD5.file(encrypted_file) + + if md5_calculated == md5_value + BigBlueButton.logger.info("The calculated MD5 matches the expected value") + key_file = key_file_url.split("/").last + decrypted_key_file = File.basename(key_file, '.*') + ".txt" + + BigBlueButton.logger.info("Downloading the key file to #{key_file}") + writeOut = open(key_file, "wb") + writeOut.write(open(key_file_url).read) + writeOut.close + + if key_file != decrypted_key_file + BigBlueButton.logger.debug("Locating private key") + if not File.exists?("#{$private_key}") + BigBlueButton.logger.error "Couldn't find the private key on #{$private_key}" + next + end + BigBlueButton.logger.debug("Decrypting recording key") + command = "openssl rsautl -decrypt -inkey #{$private_key} < #{key_file} > #{decrypted_key_file}" + status = BigBlueButton.execute(command, false) + if not status.success? + BigBlueButton.logger.error "Couldn't decrypt the random key with the server private key" + next + end + FileUtils.rm_r "#{key_file}" + else + BigBlueButton.logger.info("No public key was used to encrypt the random key") + end + + BigBlueButton.logger.debug("Decrypting the recording file") + command = "openssl enc -aes-256-cbc -d -pass file:#{decrypted_key_file} < #{encrypted_file} > #{decrypted_file}" + status = BigBlueButton.execute(command, false) + if not status.success? + BigBlueButton.logger.error "Couldn't decrypt the recording file using the random key" + next + end + + BigBlueButton::MconfProcessor.unzip("#{$raw_dir}/#{meeting_id}", decrypted_file) + + archived_done = File.new("#{$archived_dir}/#{meeting_id}.done", "w") + archived_done.write("Archived #{meeting_id}") + archived_done.close + + [ "#{encrypted_file}", "#{decrypted_file}", "#{decrypted_key_file}" ].each { |file| + BigBlueButton.logger.info("Removing #{file}") + FileUtils.rm_r "#{file}" + } + + BigBlueButton.logger.info("Recording #{record_id} decrypted successfully") + + else + BigBlueButton.logger.error("The calculated MD5 doesn't match the expected value") + FileUtils.rm_f(encrypted_file) + end + end + end + end + end + end + return true +end + +def processGetRecordingsUrlInput(input) + if input.respond_to? "each" + input.each do |url| + processGetRecordingsUrlInput url + end + else + fetchRecordings input + end +end + +processGetRecordingsUrlInput $get_recordings_url if !$get_recordings_url.nil? and !$get_recordings_url.empty? diff --git a/record-and-playback/mconf_decrypter/scripts/mconf-decrypter.yml b/record-and-playback/mconf_decrypter/scripts/mconf-decrypter.yml new file mode 100644 index 0000000000000000000000000000000000000000..74575b5488765347974992647e9656349c5caf4d --- /dev/null +++ b/record-and-playback/mconf_decrypter/scripts/mconf-decrypter.yml @@ -0,0 +1,2 @@ +get_recordings_url: +private_key: /usr/local/bigbluebutton/core/scripts/private.pem diff --git a/record-and-playback/mconf_decrypter/scripts/mconf-recording-decrypter.initd b/record-and-playback/mconf_decrypter/scripts/mconf-recording-decrypter.initd new file mode 100755 index 0000000000000000000000000000000000000000..276f08e05daa14b6a78a3eba690897be82babac2 --- /dev/null +++ b/record-and-playback/mconf_decrypter/scripts/mconf-recording-decrypter.initd @@ -0,0 +1,44 @@ +#!/bin/sh + +### BEGIN INIT INFO +# Provides: mconf-recording-decrypter +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Description: Starts the foo service +### END INIT INFO + +NAME=mconf-recording-decrypter +PID_FILE=/var/run/mconf-recording-decrypter.pid +DIR=/usr/local/bigbluebutton/core/scripts +EXEC=mconf-decrypter.rb +RUN_AS=tomcat7 + +if [ ! -f $DIR/$EXEC ]; then + echo "$DIR/$EXEC not found." + exit +fi + +case "$1" in + start) + echo "Starting $NAME" + cd $DIR + start-stop-daemon -d $DIR --start --background --pidfile $PID_FILE --chuid $RUN_AS:$RUN_AS --make-pidfile --exec $EXEC --quiet + ;; + stop) + echo "Stopping $NAME" + start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE --exec $EXEC + ;; + force-reload|restart) + $0 stop + $0 start + ;; + + *) + echo "Use: /etc/init.d/$NAME {start|stop|restart|force-reload}" + exit 1 + ;; +esac + +exit 0 diff --git a/record-and-playback/mconf_decrypter/scripts/mconf-recording-decrypter.monit b/record-and-playback/mconf_decrypter/scripts/mconf-recording-decrypter.monit new file mode 100644 index 0000000000000000000000000000000000000000..d977722e417fb883c09171369f0b4fce8a3f991d --- /dev/null +++ b/record-and-playback/mconf_decrypter/scripts/mconf-recording-decrypter.monit @@ -0,0 +1,3 @@ +check process mconf-recording-decrypter with pidfile /var/run/mconf-recording-decrypter.pid + start program = "/etc/init.d/mconf-recording-decrypter start" with timeout 60 seconds + stop program = "/etc/init.d/mconf-recording-decrypter stop" diff --git a/record-and-playback/mconf_encrypted/scripts/mconf_encrypted.nginx b/record-and-playback/mconf_encrypted/scripts/mconf_encrypted.nginx new file mode 100644 index 0000000000000000000000000000000000000000..bd531918b29798c1f2042c28c269306e29653ed9 --- /dev/null +++ b/record-and-playback/mconf_encrypted/scripts/mconf_encrypted.nginx @@ -0,0 +1,22 @@ + +# +# 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/>. +# + location /mconf_encrypted { + root /var/bigbluebutton/published; + index index.html index.htm; + } diff --git a/record-and-playback/mconf_encrypted/scripts/process/mconf_encrypted.rb b/record-and-playback/mconf_encrypted/scripts/process/mconf_encrypted.rb new file mode 100755 index 0000000000000000000000000000000000000000..05aaa5e22826896e70f28c0003e473eea524b000 --- /dev/null +++ b/record-and-playback/mconf_encrypted/scripts/process/mconf_encrypted.rb @@ -0,0 +1,62 @@ +# Set encoding to utf-8 +# 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/>. +# + + +require '../../core/lib/recordandplayback' +require 'rubygems' +require 'trollop' +require 'yaml' + +opts = Trollop::options do + opt :meeting_id, "Meeting id to archive", :default => '58f4a6b3-cd07-444d-8564-59116cb53974', :type => String +end + +meeting_id = opts[:meeting_id] + +#Mconf process log file +logger = Logger.new("/var/log/bigbluebutton/mconf_encrypted/process-#{meeting_id}.log", 'daily' ) +BigBlueButton.logger = logger + +# This script lives in scripts/archive/steps while bigbluebutton.yml lives in scripts/ +props = YAML::load(File.open('../../core/scripts/bigbluebutton.yml')) + +recording_dir = props['recording_dir'] +raw_presentation_src = props['raw_presentation_src'] + +meeting_raw_dir = "#{recording_dir}/raw/#{meeting_id}" +meeting_raw_presentation_dir = "#{raw_presentation_src}/#{meeting_id}" +meeting_process_dir = "#{recording_dir}/process/mconf_encrypted/#{meeting_id}" + +if not FileTest.directory?(meeting_process_dir) + FileUtils.mkdir_p "#{meeting_process_dir}" + # Create a copy of the raw archives + BigBlueButton.logger.info("Copying the recording raw files from #{meeting_raw_dir} to #{meeting_process_dir}") + FileUtils.cp_r Dir.glob("#{meeting_raw_dir}/*"), meeting_process_dir + + # There's no need to backup the presentation raw folder now +# FileUtils.mkdir_p "#{meeting_process_dir}/presentation_raw" +# BigBlueButton.logger.info("Copying the recording presentation from #{meeting_raw_presentation_dir}/#{meeting_id} to #{meeting_process_dir}/presentation_raw") +# FileUtils.cp_r Dir.glob("#{meeting_raw_presentation_dir}/#{meeting_id}/*"), "#{meeting_process_dir}/presentation_raw" + + process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-mconf_encrypted.done", "w") + process_done.write("Processed #{meeting_id}") + process_done.close +end + diff --git a/record-and-playback/mconf_encrypted/scripts/publish/mconf_encrypted.rb b/record-and-playback/mconf_encrypted/scripts/publish/mconf_encrypted.rb new file mode 100755 index 0000000000000000000000000000000000000000..d2bc7c8de43f6587b03ec765ff727b3150232978 --- /dev/null +++ b/record-and-playback/mconf_encrypted/scripts/publish/mconf_encrypted.rb @@ -0,0 +1,178 @@ +# Set encoding to utf-8 +# 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/>. +# + +require '../../core/lib/recordandplayback' +require 'rubygems' +require 'yaml' +require 'cgi' +require 'digest/md5' + +bbb_props = YAML::load(File.open('../../core/scripts/bigbluebutton.yml')) + +recording_dir = bbb_props['recording_dir'] +playback_host = bbb_props['playback_host'] +published_dir = bbb_props['published_dir'] +raw_presentation_src = bbb_props['raw_presentation_src'] + +done_files = Dir.glob("#{recording_dir}/status/processed/*.done") +done_files.each do |df| + match = /(.*)-(.*).done/.match df.sub(/.+\//, "") + meeting_id = match[1] + if (match[2] == "mconf_encrypted") + BigBlueButton.logger = Logger.new("/var/log/bigbluebutton/mconf_encrypted/publish-#{meeting_id}.log", 'daily' ) + + meeting_process_dir = "#{recording_dir}/process/mconf_encrypted/#{meeting_id}" + meeting_publish_dir = "#{recording_dir}/publish/mconf_encrypted/#{meeting_id}" + meeting_published_dir = "#{recording_dir}/published/mconf_encrypted/#{meeting_id}" + meeting_raw_dir = "#{recording_dir}/raw/#{meeting_id}" + meeting_raw_presentation_dir = "#{raw_presentation_src}/#{meeting_id}" + + if not FileTest.directory?(meeting_publish_dir) + FileUtils.mkdir_p meeting_publish_dir + + Dir.chdir(meeting_publish_dir) do + BigBlueButton::MconfProcessor.zip_directory(meeting_process_dir, "#{meeting_id}.zip") + + metadata = BigBlueButton::Events.get_meeting_metadata("#{meeting_process_dir}/events.xml") + + length = 16 + chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789' + password = '' + length.times { password << chars[rand(chars.size)] } + + passfile = File.new("#{meeting_id}.txt", "w") + passfile.write "#{password}" + passfile.close + + # encrypt files + command = "openssl enc -aes-256-cbc -pass file:#{meeting_id}.txt < #{meeting_id}.zip > #{meeting_id}.dat" + status = BigBlueButton.execute(command) + if not status.success? + raise "Couldn't encrypt the recording file using the random key" + end + + FileUtils.rm_f "#{meeting_id}.zip" + + key_filename = "" + if metadata.has_key?('mconflb-rec-server-key') and not metadata['mconflb-rec-server-key'].to_s.empty? + key_filename = "#{meeting_id}.enc" + # The key is already unescaped in the metadata!! + #BigBlueButton.logger.info("Unescaping public key") + #public_key_decoded = CGI::unescape("#{metadata['public-key'].to_s}") + public_key_decoded = "#{metadata['mconflb-rec-server-key'].to_s}" + public_key_filename = "public-key.pem" + public_key = File.new("#{public_key_filename}", "w") + public_key.write "#{public_key_decoded}" + public_key.close + +=begin + # Test: print the public key + public_key = File.new("#{public_key_filename}", "r") + counter = 0 + while (line = public_key.gets) + BigBlueButton.logger.info "#{counter}: #{line}" + counter = counter + 1 + end + public_key.close +=end + + command = "openssl rsautl -encrypt -pubin -inkey #{public_key_filename} < #{meeting_id}.txt > #{meeting_id}.enc" + status = BigBlueButton.execute(command) + if not status.success? + raise "Couldn't encrypt the random key using the server public key passed as metadata" + end + + # Comment it for testing + FileUtils.rm_f ["#{meeting_id}.txt", "#{public_key_filename}"] + else + key_filename = "#{meeting_id}.txt" + BigBlueButton.logger.warn "No public key was found in the meeting's metadata" + end + + # generate md5 checksum + md5sum = Digest::MD5.file("#{meeting_id}.dat") + + BigBlueButton.logger.info("Creating metadata.xml") + + # Get the real-time start and end timestamp + meeting_start = BigBlueButton::Events.first_event_timestamp("#{meeting_process_dir}/events.xml") + meeting_end = BigBlueButton::Events.last_event_timestamp("#{meeting_process_dir}/events.xml") + match = /.*-(\d+)$/.match(meeting_id) + real_start_time = match[1] + real_end_time = (real_start_time.to_i + (meeting_end.to_i - meeting_start.to_i)).to_s + + # Create metadata.xml + b = Builder::XmlMarkup.new(:indent => 2) + metaxml = b.recording { + b.id(meeting_id) + b.state("available") + b.published(true) + # Date Format for recordings: Thu Mar 04 14:05:56 UTC 2010 + b.start_time(real_start_time) + b.end_time(real_end_time) + b.download { + b.format("encrypted") + b.link("http://#{playback_host}/mconf_encrypted/#{meeting_id}/#{meeting_id}.dat") + b.md5(md5sum) + b.key("http://#{playback_host}/mconf_encrypted/#{meeting_id}/#{key_filename}") + } + b.meta { + BigBlueButton::Events.get_meeting_metadata("#{meeting_process_dir}/events.xml").each { |k,v| b.method_missing(k,v) } + } + } + + metadata_xml = File.new("metadata.xml","w") + metadata_xml.write(metaxml) + metadata_xml.close + + BigBlueButton.logger.info("Publishing mconf_encrypted") + + # Now publish this recording + if not FileTest.directory?("#{published_dir}/mconf_encrypted") + FileUtils.mkdir_p "#{published_dir}/mconf_encrypted" + end + BigBlueButton.logger.info("Publishing files") + FileUtils.cp_r(meeting_publish_dir, "#{published_dir}/mconf_encrypted") + + # it doesn't work since video and deskshare files are owned by red5, + # freeswitch files are owned by freeswitch, and this script is ran by + # tomcat6, so it can just remove files owned by tomcat6 + FileUtils.rm_r [ "/usr/share/red5/webapps/video/streams/#{meeting_id}", + "/usr/share/red5/webapps/deskshare/streams/#{meeting_id}", + Dir.glob("/var/freeswitch/meetings/#{meeting_id}*.wav") ], :force => true + + # Remove all the recording flags + FileUtils.rm_f [ "#{recording_dir}/status/sanity/#{meeting_id}.done", + "#{recording_dir}/status/recorded/#{meeting_id}.done", + "#{recording_dir}/status/archived/#{meeting_id}.done" ] + + # Comment it for testing + BigBlueButton.logger.info("Removing the recording raw files: #{meeting_raw_dir}") + FileUtils.rm_r meeting_raw_dir, :force => true + BigBlueButton.logger.info("Removing the recording presentation: #{meeting_raw_presentation_dir}") + FileUtils.rm_r meeting_raw_presentation_dir, :force => true + + publish_done = File.new("#{recording_dir}/status/published/#{meeting_id}-mconf_encrypted.done", "w") + publish_done.write("Published #{meeting_id}") + publish_done.close + end + end + end +end diff --git a/record-and-playback/presentation/playback/presentation/0.81/logo.png b/record-and-playback/presentation/playback/presentation/0.81/logo.png index 8f181420c325b665b8a49972f137e1471c305410..ce4ab709dbe496ea387b0ff14164adad7a60a947 100755 Binary files a/record-and-playback/presentation/playback/presentation/0.81/logo.png and b/record-and-playback/presentation/playback/presentation/0.81/logo.png differ diff --git a/record-and-playback/presentation/playback/presentation/0.81/playback.html b/record-and-playback/presentation/playback/presentation/0.81/playback.html index e9824c538b1603b35bbb914769a1d02f4b12676e..ef51674823d038740d06463bde36881c589027b6 100755 --- a/record-and-playback/presentation/playback/presentation/0.81/playback.html +++ b/record-and-playback/presentation/playback/presentation/0.81/playback.html @@ -18,7 +18,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. --> <html> <head> - <title>BigBlueButton Playback</title> + <title>Recording Playback</title> <link rel="stylesheet" href="css/bbb.playback.css"> <script type="text/javascript" src="lib/jquery.min.js"></script> <script type="text/javascript" src="lib/jquery-ui.min.js"></script> @@ -46,7 +46,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. </style> </head> <body> - <h1 class="visually-hidden">BigBlueButton Playback</h1> + <h1 class="visually-hidden">Recording Playback</h1> <div id="playbackArea" class="clearfix" role="main"> <h2 class="visually-hidden">Slide Thumbnails</h2> <div id="thumbnails" role="region" aria-label="Slide thumbnails"></div> @@ -90,7 +90,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. </div> --> <div id="footer"> - <p>Recorded with <a target="_blank" href="http://bigbluebutton.org/">BigBlueButton</a>. </p> + <p>Recorded with <a target="_blank" href="http://mconf.org/">Mconf</a>. </p> <p>Use <a target="_blank" href="http://mozilla.org/firefox">Mozilla Firefox</a> or <a target="_blank" href="http://google.com/chrome/">Google Chrome</a> to play this recording. </p> diff --git a/record-and-playback/presentation/playback/presentation/0.9.0/logo.png b/record-and-playback/presentation/playback/presentation/0.9.0/logo.png index 8f181420c325b665b8a49972f137e1471c305410..ce4ab709dbe496ea387b0ff14164adad7a60a947 100755 Binary files a/record-and-playback/presentation/playback/presentation/0.9.0/logo.png and b/record-and-playback/presentation/playback/presentation/0.9.0/logo.png differ diff --git a/record-and-playback/presentation/playback/presentation/0.9.0/playback.html b/record-and-playback/presentation/playback/presentation/0.9.0/playback.html index b531444d74508bf47ab7258e4baa4fc804c53458..f106997f04a6c09c042cbd27d88b2f871ae2cfbc 100755 --- a/record-and-playback/presentation/playback/presentation/0.9.0/playback.html +++ b/record-and-playback/presentation/playback/presentation/0.9.0/playback.html @@ -18,7 +18,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. --> <html> <head> - <title>BigBlueButton Playback</title> + <title>Recording Playback</title> <link rel="stylesheet" href="css/bbb.playback.css"> <script type="text/javascript" src="lib/jquery.min.js"></script> <script type="text/javascript" src="lib/jquery-ui.min.js"></script> @@ -97,7 +97,7 @@ with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. --> </div> <div id="footer"> - <p>Recorded with <a target="_blank" href="http://bigbluebutton.org/">BigBlueButton</a>. </p> + <p>Recorded with <a target="_blank" href="http://mconf.org/">Mconf</a>. </p> <p>Use <a target="_blank" href="http://mozilla.org/firefox">Mozilla Firefox</a> or <a target="_blank" href="http://google.com/chrome/">Google Chrome</a> to play this recording. </p> diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/acornmediaplayer.base.css b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/acornmediaplayer.base.css new file mode 100755 index 0000000000000000000000000000000000000000..06378fdfb078be972aad5f31a57e7542d971ecde --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/acornmediaplayer.base.css @@ -0,0 +1,222 @@ +/* + * Acorn Media Player - jQuery plugin 1.0 + * + * Copyright (C) 2010 Cristian I. Colceriu + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * www.ghinda.net + * contact@ghinda.net + * + * Base stylesheet + * + */ + +/* Main elements */ +.acorn-player, .acorn-controls { + position: relative; +} +.acorn-timer { + cursor: default; +} +.acorn-buffer { + width: 0px; +} +/* <video> */ +.acorn-player video { + background-color: #000; +} +/* <audio> */ +.acorn-player.audio-player { + width: 500px; +} +.acorn-player.audio-player audio { + display: none; +} +/* Captions and Transcript */ +.acorn-transcript { + clear: both; + display: none; + + overflow: auto; + height: 15em; +} +.acorn-transcript-button { + display: none; +} +/* + * Show the timings in square brackets before each "subtitle" in the transcript. + * Borrowed and adapted from Bruce Lawson's “Accessible HTML5 Video with JavaScripted captions†+ * http://dev.opera.com/articles/view/accessible-html5-video-with-javascripted-captions/ + */ +.acorn-transcript span { + display: block; + float: left; + width: 100%; + line-height: 1.5em; + + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} +.acorn-transcript span:hover { + background-color: #cadde7 !important; + + font-weight: bold; +} +.acorn-transcript span:nth-of-type(even) { + background-color: #efefef; +} +.acorn-transcript [data-begin]:before { + display: block; + float: left; + content: " [" attr(data-begin) "s-" attr(data-end)"s] "; + width: 15%; + padding: 0.2em 1.5em 0.2em 0.2em; +} +.acorn-caption { + display: none; + position: absolute; + bottom: 75px; + width: 100%; + + text-align: center; +} +.acorn-caption-button { + display: none; +} +.acorn-caption-selector { + position: absolute; + display: none; + width: 170px; + padding: 5px; + height: 75px; + margin-bottom: 10px; + overflow: auto; + + background-color: #000; + border: 3px solid #fff; + + z-index: 3; + + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + + -moz-box-shadow: 0px 1px 5px #000; + -webkit-box-shadow: 0px 1px 5px #000; + box-shadow: 0px 1px 5px #000; +} +.acorn-caption-selector label { + display: block; + + font-weight: bold; + color: #fff; +} +.acorn-caption-selector ul, .acorn-caption-selector li { + list-style-type: none; + margin: 0px; + padding: 0px; +} +/* Fullscreen Mode */ +.fullscreen-video { + position: fixed !important; + top: 0px; + left: 0px; + z-index: 99999 !important; + + background-color: #000; +} +.acorn-controls.fullscreen-controls { + position: fixed !important; + z-index: 100000 !important; +} +/* Loading */ +.show-loading .loading-media { + visibility: visible; +} + +.loading-media { + visibility: hidden; + position: absolute; + left: 25%; + top: 50%; + width: 20px; + height: 20px; + margin-top: -10px; + margin-lefT: -10px; + + background-color: #000; + border: 5px solid #fff; + border-top: 5px solid rgba(0,0,0,0); + border-left: 5px solid rgba(0,0,0,0); + border-radius: 20px; + + animation: spin 1s infinite linear; + -o-animation: spin 1s infinite linear; + -moz-animation: spin 1s infinite linear; + -webkit-animation: spin 1s infinite linear; +} + +@-o-keyframes spin { + 0% { -o-transform:rotate(0deg); } + 100% { -o-transform:rotate(360deg); } +} +@-ms-keyframes spin { + 0% { -ms-transform:rotate(0deg); } + 100% { -ms-transform:rotate(360deg); } +} +@-moz-keyframes spin { + 0% { -moz-transform:rotate(0deg); } + 100% { -moz-transform:rotate(360deg); } +} +@-webkit-keyframes spin { + 0% { -webkit-transform:rotate(0deg); } + 100% { -webkit-transform:rotate(360deg); } +} +@keyframes spin { + 0% { transform:rotate(0deg); } + 100% { transform:rotate(360deg); } +} + +/* Controls overlay while loading */ +.show-loading .acorn-controls:after { + content: ''; + position: absolute; + top: -2px; /* Slider handle goes above */ + padding-bottom: 2px; + left: 0; + z-index: 10; + width: 100%; + height: 100%; + + background: #000; + opacity: 0.9; +} + +/* Styles needed for the jQuery UI slider + * We're declaring these so we don't have to use jQuery UI's stylesheet + */ +a.ui-slider-handle { + position: absolute; + display: block; + margin-left: -0.6em; + z-index: 2; + cursor: default; + outline: none; +} +.ui-slider { + position: relative; +} +.ui-slider-range { + position: absolute; + display: block; + width: 100%; + height: 100%; + left: 0; + bottom: 0; + border: none; + z-index: 1; +} diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/jquery.acornmediaplayer.js b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/jquery.acornmediaplayer.js new file mode 100755 index 0000000000000000000000000000000000000000..84f3416a4f2cd0a30f38ecd8ca43f4bc5b454ac4 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/jquery.acornmediaplayer.js @@ -0,0 +1,1158 @@ +/* + * Acorn Media Player - jQuery plugin 1.6 + * + * Copyright (C) 2012 Ionut Cristian Colceriu + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * www.ghinda.net + * contact@ghinda.net + * + */ + +(function($) { + $.fn.acornMediaPlayer = function(options) { + /* + * Define default plugin options + */ + var defaults = { + theme: 'access', + nativeSliders: false, + volumeSlider: 'horizontal', + captionsOn: false + }; + options = $.extend(defaults, options); + + /* + * Function for generating a unique identifier using the current date and time + * Used for generating an ID for the media elmenet when none is available + */ + var uniqueID = function() { + var currentDate = new Date(); + return currentDate.getTime(); + }; + + /* + * Detect support for localStorage + */ + function supports_local_storage() { + try { + return 'localStorage' in window && window.localStorage !== null; + } catch(e){ + return false; + } + } + + /* Detect Touch support + */ + var is_touch_device = 'ontouchstart' in document.documentElement; + + /* + * Get the volume value from localStorage + * If no value is present, define as maximum + */ + var volume = (supports_local_storage) ? localStorage.getItem('acornvolume') : 1; + if(!volume) { + volume = 1; + } + + /* + * Main plugin function + * It will be called on each element in the matched set + */ + var acornPlayer = function() { + // set the acorn object, will contain the needed DOM nodes and others + var acorn = { + $self: $(this) + }; + + var loadedMetadata; // Is the metadata loaded + var seeking; // The user is seeking the media + var wasPlaying; // Media was playing when the seeking started + var fullscreenMode; // The media is in fullscreen mode + var captionsActive; // Captions are active + + /* Define all the texts used + * This makes it easier to maintain, make translations, etc. + */ + var text = { + play: 'Play', + playTitle: 'Start the playback', + pause: 'Pause', + pauseTitle: 'Pause the playback', + mute: 'Mute', + unmute: 'Unmute', + fullscreen: 'Fullscreen', + fullscreenTitle: 'Toggle fullscreen mode', + swap: 'Swap', + swapTitle: 'Toggle video and presention swap', + volumeTitle: 'Volume control', + seekTitle: 'Video seek control', + captions: 'Captions', + captionsTitle: 'Show captions', + captionsChoose: 'Choose caption', + transcript: 'Transcript', + transcriptTitle: 'Show transcript' + }; + + // main wrapper element + var $wrapper = $('<div class="acorn-player" role="application"></div>').addClass(options.theme); + + /* + * Define attribute tabindex on the main element to make it readchable by keyboard + * Useful when "aria-describedby" is present + * + * It makes more sense for screen reader users to first reach the actual <video> or <audio> elment and read of description of it, + * than directly reach the Media Player controls, without knowing what they control. + */ + acorn.$self.attr('tabindex', '0'); + + /* + * Check if the main element has an ID attribute + * If not present, generate one + */ + acorn.id = acorn.$self.attr('id'); + if(!acorn.id) { + acorn.id = 'acorn' + uniqueID(); + acorn.$self.attr('id', acorn.id); + } + + /* + * Markup for the fullscreen button + * If the element is not <video> we leave if blank, as the button if useless on <audio> elements + */ + var fullscreenBtnMarkup = (acorn.$self.is('video')) ? '<button class="acorn-fullscreen-button" title="' + text.fullscreenTitle + '" aria-controls="' + acorn.id + '" tabindex="3">' + text.fullscreen + '</button>' : ''; + + /* + * Markup for the swap button + * If the element is not <video> we leave if blank, as the button if useless on <audio> elements + */ + var swapBtnMarkup = (acorn.$self.is('video')) ? '<button class="acorn-swap-button" title="' + text.swapTitle + '" aria-controls="' + acorn.id + '" tabindex="4" >' + text.swap + '</button>' : ''; + + + /* + * Complete markup + */ + var template = '<div class="acorn-controls">' + + '<button class="acorn-play-button" title="' + text.playTitle + '" aria-controls="' + acorn.id + '" tabindex="1">' + text.play + '</button>' + + '<input type="range" class="acorn-seek-slider" title="' + text.seekTitle + '" value="0" min="0" max="150" step="0.1" aria-controls="' + acorn.id + '" tabindex="2" />' + + '<span class="acorn-timer">00:00</span>' + + '<div class="acorn-volume-box">' + + '<button class="acorn-volume-button" title="' + text.mute + '" aria-controls="' + acorn.id + '" tabindex="5" >' + text.mute + '</button>' + + '<input type="range" class="acorn-volume-slider" title="' + text.volumeTitle + '" value="1" min="0" max="1" step="0.05" aria-controls="' + acorn.id + '" tabindex="6" />' + + '</div>' + + swapBtnMarkup + + fullscreenBtnMarkup + + '<button class="acorn-caption-button" title="' + text.captionsTitle + '" aria-controls="' + acorn.id + '">' + text.captions + '</button>' + + '<div class="acorn-caption-selector"></div>' + + '<button class="acorn-transcript-button" title="' + text.transcriptTitle + '">' + text.transcript + '</button>' + + '</div>'; + + var captionMarkup = '<div class="acorn-caption"></div>'; + var transcriptMarkup = '<div class="acorn-transcript" role="region" aria-live="assertive"></div>'; + + /* + * Append the HTML markup + */ + + // append the wrapper + acorn.$self.after($wrapper); + + // For iOS support, I have to clone the node, remove the original, and get a reference to the new one. + // This is because iOS doesn't want to play videos that have just been `moved around`. + // More details on the issue: http://bugs.jquery.com/ticket/8015 + $wrapper[0].appendChild( acorn.$self[0].cloneNode(true) ); + + acorn.$self.trigger('pause'); + acorn.$self.remove(); + acorn.$self = $wrapper.find('video, audio'); + + // append the controls and loading mask + acorn.$self.after(template).after('<div class="loading-media"></div>'); + + /* + * Define the newly created DOM nodes + */ + acorn.$container = acorn.$self.parent('.acorn-player'); + + acorn.$controls = $('.acorn-controls', acorn.$container); + acorn.$playBtn = $('.acorn-play-button', acorn.$container); + acorn.$seek = $('.acorn-seek-slider', acorn.$container); + acorn.$timer = $('.acorn-timer', acorn.$container); + acorn.$volume = $('.acorn-volume-slider', acorn.$container); + acorn.$volumeBtn = $('.acorn-volume-button', acorn.$container); + acorn.$fullscreenBtn = $('.acorn-fullscreen-button', acorn.$container); + acorn.$swapBtn = $('.acorn-swap-button', acorn.$container); + /* + * Append the markup for the Captions and Transcript + * and define newly created DOM nodes for these + */ + acorn.$controls.after(captionMarkup); + acorn.$container.after(transcriptMarkup); + + acorn.$transcript = acorn.$container.next('.acorn-transcript'); + acorn.$transcriptBtn = $('.acorn-transcript-button', acorn.$container); + + acorn.$caption = $('.acorn-caption', acorn.$container); + acorn.$captionBtn = $('.acorn-caption-button', acorn.$container); + acorn.$captionSelector = $('.acorn-caption-selector', acorn.$container); + + /* + * Use HTML5 "data-" attributes to set the original Width&Height for the <video> + * These are used when returning from Fullscreen Mode + */ + acorn.$self.attr('data-width', acorn.$self.width()); + acorn.$self.attr('data-height', acorn.$self.height()); + + /* + * Time formatting function + * Takes the number of seconds as a parameter and return a readable format "minutes:seconds" + * Used with the number of seconds returned by "currentTime" + */ + var timeFormat = function(sec) { + var m = Math.floor(sec/60)<10?"0" + Math.floor(sec/60):Math.floor(sec/60); + var s = Math.floor(sec-(m*60))<10?"0" + Math.floor(sec-(m*60)):Math.floor(sec-(m*60)); + return m + ":" + s; + }; + + /* + * PLAY/PAUSE Behaviour + * + * Function for the Play button + * It triggers the native Play or Pause events + */ + var playMedia = function() { + if(!acorn.$self.prop('paused')) { + acorn.$self.trigger('pause'); + } else { + //acorn.$self.trigger('play'); + acorn.$self[0].play(); + } + }; + + /* + * Functions for native playback events (Play, Pause, Ended) + * These are attached to the native media events. + * + * Even if the user is still using some form of native playback control (such as using the Context Menu) + * it will not break the behviour of our player. + */ + var startPlayback = function() { + acorn.$playBtn.text(text.pause).attr('title', text.pauseTitle); + acorn.$playBtn.addClass('acorn-paused-button'); + + // if the metadata is not loaded yet, add the loading class + if (!loadedMetadata) $wrapper.addClass('show-loading'); + }; + + var stopPlayback = function() { + acorn.$playBtn.text(text.play).attr('title', text.playTitle); + acorn.$playBtn.removeClass('acorn-paused-button'); + }; + + /* + * SEEK SLIDER Behaviour + * + * Updates the Timer and Seek Slider values + * Is called on each "timeupdate" + */ + var seekUpdate = function() { + var currenttime = acorn.$self.prop('currentTime'); + acorn.$timer.text(timeFormat(currenttime)); + + // If the user is not manualy seeking + if(!seeking) { + // Check type of sliders (Range <input> or jQuery UI) + if(options.nativeSliders) { + acorn.$seek.attr('value', currenttime); + } else { + acorn.$seek.slider('value', currenttime); + } + } + + // If captions are active, update them + if(captionsActive) { + updateCaption(); + } + }; + + /* + * Time formatting function + * Takes the number of seconds as a paramenter + * + * Used with "aria-valuetext" on the Seek Slider to provide a human readable time format to AT + * Returns "X minutes Y seconds" + */ + var ariaTimeFormat = function(sec) { + var m = Math.floor(sec/60)<10?"" + Math.floor(sec/60):Math.floor(sec/60); + var s = Math.floor(sec-(m*60))<10?"" + Math.floor(sec-(m*60)):Math.floor(sec-(m*60)); + var formatedTime; + + var mins = 'minutes'; + var secs = 'seconds'; + + if(m == 1) { + min = 'minute'; + } + if(s == 1) { + sec = 'second'; + } + + if(m === 0) { + formatedTime = s + ' ' + secs; + } else { + formatedTime = m + ' ' + mins + ' ' + s + ' ' + secs; + } + + return formatedTime; + }; + + /* + * jQuery UI slider uses preventDefault when clicking any element + * so it stops the Blur event from being fired. + * This causes problems with the Caption Selector. + * We trigger the Blur event manually. + */ + var blurCaptionBtn = function() { + acorn.$captionBtn.trigger('blur'); + }; + + /* + * Triggered when the user starts to seek manually + * Pauses the media during seek and changes the "currentTime" to the slider's value + */ + var startSeek = function(e, ui) { + if(!acorn.$self.attr('paused')) { + wasPlaying = true; + } + acorn.$self.trigger('pause'); + seeking = true; + + var seekLocation; + if(options.nativeSliders) { + seekLocation = acorn.$seek.val(); + } else { + seekLocation = ui.value; + } + + acorn.$self[0].currentTime = seekLocation; + + // manually blur the Caption Button + blurCaptionBtn(); + }; + + /* + * Triggered when user stoped manual seek + * If the media was playing when seek started, it triggeres the playback, + * and updates ARIA attributes + */ + var endSeek = function(e, ui) { + if(wasPlaying) { + acorn.$self.trigger('play'); + wasPlaying = false; + } + seeking = false; + var sliderUI = $(ui.handle); + sliderUI.attr("aria-valuenow", parseInt(ui.value, 10)); + sliderUI.attr("aria-valuetext", ariaTimeFormat(ui.value)); + }; + + /* + * Transforms element into ARIA Slider adding attributes and "tabindex" + * Used on jQuery UI sliders + * + * Will not needed once the jQuery UI slider gets built-in ARIA + */ + var initSliderAccess = function (elem, opts) { + var accessDefaults = { + 'role': 'slider', + 'aria-valuenow': parseInt(opts.value, 10), + 'aria-valuemin': parseInt(opts.min, 10), + 'aria-valuemax': parseInt(opts.max, 10), + 'aria-valuetext': opts.valuetext + }; + elem.attr(accessDefaults); + }; + + /* + * Init jQuery UI slider + */ + var initSeek = function() { + + // get existing classes + var seekClass = acorn.$seek.attr('class'); + + // create the new markup + var divSeek = '<div class="' + seekClass + '" title="' + text.seekTitle + '"></div>'; + acorn.$seek.after(divSeek).remove(); + + // get the newly created DOM node + acorn.$seek = $('.' + seekClass, acorn.$container); + + // create the buffer element + var bufferBar = '<div class="ui-slider-range acorn-buffer"></div>'; + acorn.$seek.append(bufferBar); + + // get the buffer element DOM node + acorn.$buffer = $('.acorn-buffer', acorn.$container); + + // set up the slider options for the jQuery UI slider + var sliderOptions = { + value: 0, + step: 1, + orientation: 'horizontal', + range: 'min', + min: 0, + max: 100 + }; + // init the jQuery UI slider + acorn.$seek.slider(sliderOptions); + + }; + + /* + * Seek slider update, after metadata is loaded + * Attach events, add the "duration" attribute and generate the jQuery UI Seek Slider + */ + var updateSeek = function() { + // Get the duration of the media + var duration = acorn.$self[0].duration; + + // Check for the nativeSliders option + if(options.nativeSliders) { + acorn.$seek.attr('max', duration); + acorn.$seek.bind('change', startSeek); + + acorn.$seek.bind('mousedown', startSeek); + acorn.$seek.bind('mouseup', endSeek); + + } else { + + // set up the slider options for the jQuery UI slider + var sliderOptions = { + value: 0, + step: 1, + orientation: 'horizontal', + range: 'min', + min: 0, + max: duration, + slide: startSeek, + stop: endSeek + }; + // init the jQuery UI slider + acorn.$seek.slider('option', sliderOptions); + + // add valuetext value to the slider options for better ARIA values + sliderOptions.valuetext = ariaTimeFormat(sliderOptions.value); + // accessify the slider + initSliderAccess(acorn.$seek.find('.ui-slider-handle'), sliderOptions); + + // manully blur the Caption Button when clicking the handle + $('.ui-slider-handle', acorn.$seek).click(blurCaptionBtn); + + // set the tab index + $('.ui-slider-handle', acorn.$seek).attr("tabindex", "2"); + + // show buffering progress on progress + acorn.$self.bind('progress', showBuffer); + } + + + $wrapper.removeClass('show-loading'); + // remove the loading element + //acorn.$self.next('.loading-media').remove(); + + }; + + /* + * Show buffering progress + */ + var showBuffer = function(e) { + var max = parseInt(acorn.$self.prop('duration'), 10); + var tr = this.buffered; + if(tr && tr.length) { + var buffer = parseInt(this.buffered.end(0)-this.buffered.start(0), 10); + var bufferWidth = (buffer*100)/max; + + acorn.$buffer.css('width', bufferWidth + '%'); + } + }; + + /* + * VOLUME BUTTON and SLIDER Behaviour + * + * Change volume using the Volume Slider + * Also update ARIA attributes and set the volume value as a localStorage item + */ + var changeVolume = function(e, ui) { + // get the slider value + volume = ui.value; + // set the value as a localStorage item + localStorage.setItem('acornvolume', volume); + + // check if the volume was muted before + if(acorn.$self.prop('muted')) { + acorn.$self.prop('muted', false); + acorn.$volumeBtn.removeClass('acorn-volume-mute'); + acorn.$volumeBtn.text(text.mute).attr('title', text.mute); + } + + // set the new volume on the media + acorn.$self.prop('volume', volume); + + // set the ARIA attributes + acorn.$volume.$handle.attr("aria-valuenow", Math.round(volume*100)); + acorn.$volume.$handle.attr("aria-valuetext", Math.round(volume*100) + ' percent'); + // manually trigger the Blur event on the Caption Button + blurCaptionBtn(); + }; + + /* + * Mute and Unmute volume + * Also add classes and change label on the Volume Button + */ + var muteVolume = function() { + if(acorn.$self.prop('muted') === true) { + acorn.$self.prop('muted', false); + if(options.nativeSliders) { + acorn.$volume.val(volume); + } else { + acorn.$volume.slider('value', volume); + } + + acorn.$volumeBtn.removeClass('acorn-volume-mute'); + acorn.$volumeBtn.text(text.mute).attr('title', text.mute); + } else { + acorn.$self.prop('muted', true); + + if(options.nativeSliders) { + acorn.$volume.val('0'); + } else { + acorn.$volume.slider('value', '0'); + } + + acorn.$volumeBtn.addClass('acorn-volume-mute'); + acorn.$volumeBtn.text(text.unmute).attr('title', text.unmute); + } + }; + + /* + * Init the Volume Button and Slider + * + * Attach events, create the jQuery UI Slider for the Volume Slider and add ARIA support + */ + var initVolume = function() { + if(options.nativeSliders) { + acorn.$volume.bind('change', function() { + acorn.$self.prop('muted',false); + volume = acorn.$volume.val(); + acorn.$self.prop('volume', volume); + }); + } else { + var volumeClass = acorn.$volume.attr('class'); + + var divVolume = '<div class="' + volumeClass + '" title="' + text.volumeTitle + '"></div>'; + acorn.$volume.after(divVolume).remove(); + + acorn.$volume = $('.' + volumeClass, acorn.$container); + + var volumeSliderOptions = { + value: volume, + orientation: options.volumeSlider, + range: "min", + max: 1, + min: 0, + step: 0.1, + animate: false, + slide: changeVolume + }; + + acorn.$volume.slider(volumeSliderOptions); + + acorn.$volume.$handle = acorn.$volume.find('.ui-slider-handle'); + + // change and add values to volumeSliderOptions for better values in the ARIA attributes + volumeSliderOptions.max = 100; + volumeSliderOptions.value = volumeSliderOptions.value * 100; + volumeSliderOptions.valuetext = volumeSliderOptions.value + ' percent'; + initSliderAccess(acorn.$volume.$handle, volumeSliderOptions); + acorn.$volume.$handle.attr("tabindex", "6"); + + // show the volume slider when it is tabbed into + acorn.$volume.$handle.focus(function(){ + if (!acorn.$volume.parent().is(":hover")) { + acorn.$volume.addClass("handle-focused"); + } + }); + acorn.$volume.$handle.blur(function(){ + acorn.$volume.removeClass("handle-focused"); + }); + // manully blur the Caption Button when clicking the handle + $('.ui-slider-handle', acorn.$volume).click(blurCaptionBtn); + } + + acorn.$volumeBtn.click(muteVolume); + }; + + /* + * FULLSCREEN Behviour + * + * Resize the video while in Fullscreen Mode + * Attached to window.resize + */ + var resizeFullscreenVideo = function() { + acorn.$self.attr({ + 'width': $(window).width(), + 'height': $(window).height() + }); + }; + + /* + * Enter and exit Fullscreen Mode + * + * Resizes the Width & Height of the <video> element + * and add classes to the controls and wrapper + */ + var goFullscreen = function() { + if(fullscreenMode) { + if(acorn.$self[0].webkitSupportsFullscreen) { + acorn.$self[0].webkitExitFullScreen(); + } else { + $('body').css('overflow', 'auto'); + + var w = acorn.$self.attr('data-width'); + var h = acorn.$self.attr('data-height'); + + acorn.$self.removeClass('fullscreen-video').attr({ + 'width': w, + 'height': h + }); + + $(window).unbind('resize'); + + acorn.$controls.removeClass('fullscreen-controls'); + } + + fullscreenMode = false; + + } else { + if(acorn.$self[0].webkitSupportsFullscreen) { + acorn.$self[0].webkitEnterFullScreen(); + } else if (acorn.$self[0].mozRequestFullScreen) { + acorn.$self[0].mozRequestFullScreen(); + acorn.$self.attr('controls', 'controls'); + document.addEventListener('mozfullscreenchange', function() { + console.log('screenchange event found'); + if (!document.mozFullScreenElement) { + acorn.$self.removeAttr('controls'); + //document.removeEventListener('mozfullscreenchange'); + } + }); + } else { + $('body').css('overflow', 'hidden'); + + acorn.$self.addClass('fullscreen-video').attr({ + width: $(window).width(), + height: $(window).height() + }); + + $(window).resize(resizeFullscreenVideo); + + acorn.$controls.addClass('fullscreen-controls'); + } + + fullscreenMode = true; + + } + }; + + /* + * Swap the video and presentation areas + * + * Resizes and moves based on hard coded numbers + * Uses css to move it + */ + + var swapped = false; + + var goSwap = function() { + if (swapped === false) { + $('#slide').css("width", "400px"); + $('#slide').css("height", "300px"); + $('#slide > object').attr("width", "400px"); + $('#slide > object').attr("height", "300px"); + var svgfile = $('#slide > object')[0].contentDocument.getElementById("svgfile"); + svgfile.style.width = "400px"; + svgfile.style.height = "300px"; + + var slide = document.getElementById("slide"); + var cursor = document.getElementById("cursor"); + var slideT = document.getElementById("slideText"); + var video = document.getElementById("video"); + + video.style.width = "800px"; + video.style.height = "600px"; + + $('#videoRecordingWrapper').position({ + "my": "left top", + "at": "right top", + "of": '#thumbnails', + "collision": "none none", + "offset": "10 0" + }); + $('#videoRecordingWrapper').width("800px"); + $('#videoRecordingWrapper').height("600px"); + + $('#presentation').position({ + "my": "left top", + "at": "left bottom", + "of": '#chat', + "collision": "none none" + }); + $('#presentation').width("400px"); + $('#presentation').height("300px"); + + $('.acorn-controls').position({ + "my": "left top", + "at": "left bottom", + "of": '#videoRecordingWrapper', + "collision": "none none", + "offset": "10 7" + }); + + draw(0,0); + + swapped = true; + } else { + $('#slide').css("width", "800px"); + $('#slide').css("height", "600px"); + $('#slide > object').attr("width", "800px"); + $('#slide > object').attr("height", "600px"); + var svgfile = $('#slide > object')[0].contentDocument.getElementById("svgfile"); + svgfile.style.width = "800px"; + svgfile.style.height = "600px"; + + var video = document.getElementById("video"); + + video.style.width = "400px"; + video.style.height = "300px"; + + $('#presentation').position({ + "my": "left top", + "at": "right top", + "of": '#thumbnails', + "collision": "none none", + "offset": "10 0" + }); + $('#presentation').width("800px"); + $('#presentation').height("600px"); + + $('#videoRecordingWrapper').position({ + "my": "left top", + "at": "left bottom", + "of": '#chat', + "collision": "none none" + }); + $('#videoRecordingWrapper').width("400px"); + $('#videoRecordingWrapper').height("300px"); + + $('.acorn-controls').position({ + "my": "left top", + "at": "left bottom", + "of": '#presentation', + "collision": "none none", + "offset": "10 7" + }); + + swapped = false; + } + } + + /* + * CAPTIONS Behaviour + * + * Turning off the captions + * When selecting "None" from the Caption Selector or when the caption fails to load + */ + var captionBtnActiveClass = 'acorn-caption-active'; + var captionBtnLoadingClass = 'acorn-caption-loading'; + var transcriptBtnActiveClass = 'acorn-transcript-active'; + + var captionRadioName = 'acornCaptions' + uniqueID(); + + var captionOff = function() { + captions = ''; + acorn.$caption.hide(); + activeCaptions = false; + + acorn.$transcriptBtn.removeClass(transcriptBtnActiveClass).hide(); + acorn.$transcript.hide(); + + acorn.$captionBtn.removeClass(captionBtnActiveClass); + }; + + /* + * Update caption based on "currentTime" + * Borrowed and adapted from Bruce Lawson's “Accessible HTML5 Video with JavaScripted captions†+ * http://dev.opera.com/articles/view/accessible-html5-video-with-javascripted-captions/ + */ + var updateCaption = function() { + var now = acorn.$self[0].currentTime; // how soon is now? + var text = ""; + for (var i = 0; i < captions.length; i++) { + if (now >= captions[i].start && now <= captions[i].end) { + text = captions[i].content; // yes? then load it into a variable called text + break; + } + } + acorn.$caption.html(text); // and put contents of text into caption div + }; + + /* + * Initialize the Caption Selector + * Used when multiple <track>s are present + */ + var initCaptionSelector = function() { + // calculate the position relative to the parent controls element + var setUpCaptionSelector = function() { + var pos = acorn.$captionBtn.offset(); + var top = pos.top - acorn.$captionSelector.outerHeight(true); + var left = pos.left - ((acorn.$captionSelector.outerWidth(true) - acorn.$captionBtn.outerWidth(true))/2); + + var parentPos = acorn.$controls.offset(); + + left = left - parentPos.left; + top = top - parentPos.top; + + acorn.$captionSelector.css({ + 'top': top, + 'left': left + }); + }; + + acorn.$fullscreenBtn.click(setUpCaptionSelector); + $(window).resize(function() { + setUpCaptionSelector(); + }); + + setUpCaptionSelector(); + + /* + * Show and hide the caption selector based on focus rather than hover. + * This benefits both touchscreen and AT users. + */ + var hideSelector; // timeout for hiding the Caption Selector + var showCaptionSelector = function() { + if(hideSelector) { + clearTimeout(hideSelector); + } + acorn.$captionSelector.show(); + }; + var hideCaptionSelector = function() { + hideSelector = setTimeout(function() { + acorn.$captionSelector.hide(); + }, 200); + }; + + /* Little TEMPORARY hack to focus the caption button on click + This is because Webkit does not focus the button on click */ + acorn.$captionBtn.click(function() { + $(this).focus(); + }); + + acorn.$captionBtn.bind('focus', showCaptionSelector); + acorn.$captionBtn.bind('blur', hideCaptionSelector); + + $('input[name=' + captionRadioName + ']', acorn.$container).bind('focus', showCaptionSelector); + $('input[name=' + captionRadioName + ']', acorn.$container).bind('blur', hideCaptionSelector); + + /* + * Make the Caption Selector focusable and attach events to it + * If we wouldn't do this, when we'd use the scroll on the Caption Selector, it would dissapear + */ + acorn.$captionSelector.attr('tabindex', '-1'); + acorn.$captionSelector.bind('focus', showCaptionSelector); + acorn.$captionSelector.bind('blur', hideCaptionSelector); + }; + + /* + * Current caption loader + * Loads a SRT file and uses it as captions + * Takes the url as a parameter + */ + var loadCaption = function(url) { + // add a loading class to the Caption Button when starting to load the caption + acorn.$captionBtn.addClass(captionBtnLoadingClass); + // make an AJAX request to load the file + $.ajax({ + url: url, + success: function(data) { + /* + * On success use a SRT parser on the loaded data + * Using JavaScript SRT parser by Silvia Pfeiffer <silvia@siliva-pfeiffer.de> + * parseSrt included at the end of this file + */ + captions = parseSrt(data); + + // show the Transcript Button + acorn.$transcriptBtn.show(); + + /* + * Generate the markup for the transcript + * Markup based on Bruce Lawson's “Accessible HTML5 Video with JavaScripted captions†+ * http://dev.opera.com/articles/view/accessible-html5-video-with-javascripted-captions/ + */ + var transcriptText = ''; + $(captions).each(function() { + transcriptText += '<span data-begin="' + parseInt(this.start, 10) + '" data-end=' + parseInt(this.end, 10) + '>' + this.content.replace("'","") + '</span>'; + }); + // append the generated markup + acorn.$transcript.html(transcriptText); + + // show caption + acorn.$caption.show(); + captionsActive = true; + + // in case the media is paused and timeUpdate is not triggered, trigger it + if(acorn.$self.prop('paused')) { + updateCaption(); + } + + acorn.$captionBtn.addClass(captionBtnActiveClass).removeClass(captionBtnLoadingClass); + }, + error: function() { + // if an error occurs while loading the caption, turn captions off + captionOff(); + // if a console is available, log error + if(console) { + console.log('Error loading captions'); + } + } + }); + }; + + /* + * Show or hide the Transcript based on the presence of the active class + */ + var showTranscript = function() { + if($(this).hasClass(transcriptBtnActiveClass)) { + acorn.$transcript.hide(); + } else { + acorn.$transcript.show(); + } + $(this).toggleClass(transcriptBtnActiveClass); + }; + + /* + * Caption loading and initialization + */ + var initCaption = function() { + // get all <track> elements + acorn.$track = $('track', acorn.$self); + + // if there is at least one <track> element, show the Caption Button + if(acorn.$track.length) { + acorn.$captionBtn.show(); + } + + // check if there is more than one <track> element + // if there is more than one track element we'll create the Caption Selector + if(acorn.$track.length>1) { + // set a different "title" attribute + acorn.$captionBtn.attr('title', text.captionsChoose); + + // markup for the Caption Selector + var captionList = '<ul><li><label><input type="radio" name="' + captionRadioName + '" checked="true" />None</label></li>'; + acorn.$track.each(function() { + var tracksrc = $(this).attr('src'); + captionList += '<li><label><input type="radio" name="' + captionRadioName + '" data-url="' + $(this).attr('src') + '" />' + $(this).attr('label') + '</label></li>'; + }); + captionList += '</ul>'; + + // append the generated markup + acorn.$captionSelector.html(captionList); + + // change selected caption + var changeCaption = function() { + // get the original <track> "src" attribute from the custom "data-url" attribute of the radio input + var tracksrc = $(this).attr('data-url'); + if(tracksrc) { + loadCaption(tracksrc); + } else { + // if there's not "data-url" attribute, turn off the caption + captionOff(); + } + }; + + // attach event handler + $('input[name=' + captionRadioName + ']', acorn.$container).change(changeCaption); + + // initialize Caption Selector + initCaptionSelector(); + + // load first caption if captionsOn is true + var firstCaption = acorn.$track.first().attr('src'); + if(options.captionsOn) { + loadCaption(firstCaption); + $('input[name=' + captionRadioName + ']', acorn.$container).removeAttr('checked'); + $('input[name=' + captionRadioName + ']:eq(1)', acorn.$container).attr('checked', 'true'); + }; + } else if(acorn.$track.length) { + // if there's only one <track> element + // load the specific caption when activating the Caption Button + var tracksrc = acorn.$track.attr('src'); + + acorn.$captionBtn.bind('click', function() { + if($(this).hasClass(captionBtnActiveClass)) { + captionOff(); + } else { + loadCaption(tracksrc); + } + $(this).toggleClass(captionBtnActiveClass); + }); + + // load default caption if captionsOn is true + if(options.captionsOn) loadCaption(tracksrc); + } + + // attach event to Transcript Button + acorn.$transcriptBtn.bind('click', showTranscript); + }; + + /* + * Initialization self-invoking function + * Runs other initialization functions, attaches events, removes native controls + */ + var init = function() { + // attach playback handlers + acorn.$playBtn.bind( (is_touch_device) ? 'touchstart' : 'click', playMedia); + acorn.$self.bind( (is_touch_device) ? 'touchstart' : 'click' , playMedia); + + acorn.$self.bind('play', startPlayback); + acorn.$self.bind('pause', stopPlayback); + acorn.$self.bind('ended', stopPlayback); + + // update the Seek Slider when timeupdate is triggered + acorn.$self.bind('timeupdate', seekUpdate); + + // bind Fullscreen Button + acorn.$fullscreenBtn.click(goFullscreen); + + // bind Swap Button + acorn.$swapBtn.click(goSwap); + + // initialize volume controls + initVolume(); + + // add the loading class + $wrapper.addClass(''); + + if(!options.nativeSliders) initSeek(); + + // once the metadata has loaded + acorn.$self.bind('loadedmetadata', function() { + /* I use an interval to make sure the video has the right readyState + * to bypass a known webkit bug that causes loadedmetadata to be triggered + * before the duration is available + */ + + var t = window.setInterval(function() { + if (acorn.$self[0].readyState > 0) { + loadedMetadata = true; + updateSeek(); + + clearInterval(t); + } + }, 500); + + initCaption(); + }); + + // trigger update seek manualy for the first time, for iOS support + updateSeek(); + + // remove the native controls + acorn.$self.removeAttr('controls'); + + if(acorn.$self.is('audio')) { + /* + * If the media is <audio>, we're adding the 'audio-player' class to the element. + * This is because Opera 10.62 does not allow the <audio> element to be targeted by CSS + * and this can cause problems with themeing. + */ + acorn.$container.addClass('audio-player'); + } + }(); + + }; + + // iterate and reformat each matched element + return this.each(acornPlayer); + }; + +})(jQuery); + +/* + * parseSrt function + * JavaScript SRT parser by Silvia Pfeiffer <silvia@siliva-pfeiffer.de> + * http://silvia-pfeiffer.de/ + * + * Tri-licensed under MPL 1.1/GPL 2.0/LGPL 2.1 + * http://www.gnu.org/licenses/gpl.html + * http://www.gnu.org/licenses/lgpl.html + * http://www.mozilla.org/MPL/ + * + * The Initial Developer of the Original Code is Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Silvia Pfeiffer <silvia@siliva-pfeiffer.de> + * + * + */ +function parseSrt(data) { + var srt = data.replace(/\r+/g, ''); // remove dos newlines + srt = srt.replace(/^\s+|\s+$/g, ''); // trim white space start and end + srt = srt.replace(/<[a-zA-Z\/][^>]*>/g, ''); // remove all html tags for security reasons + + // get captions + var captions = []; + var caplist = srt.split('\n\n'); + for (var i = 0; i < caplist.length; i=i+1) { + var caption = ""; + var content, start, end, s; + caption = caplist[i]; + s = caption.split(/\n/); + if (s[0].match(/^\d+$/) && s[1].match(/\d+:\d+:\d+/)) { + // ignore caption number in s[0] + // parse time string + var m = s[1].match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/); + if (m) { + start = + (parseInt(m[1], 10) * 60 * 60) + + (parseInt(m[2], 10) * 60) + + (parseInt(m[3], 10)) + + (parseInt(m[4], 10) / 1000); + end = + (parseInt(m[5], 10) * 60 * 60) + + (parseInt(m[6], 10) * 60) + + (parseInt(m[7], 10)) + + (parseInt(m[8], 10) / 1000); + } else { + // Unrecognized timestring + continue; + } + // concatenate text lines to html text + content = s.slice(2).join("<br>"); + } else { + // file format error or comment lines + continue; + } + captions.push({start: start, end: end, content: content}); + } + + return captions; +} diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-captions-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-captions-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..f4a244a436a53c4ec4e4bee00ba400eef9af6d60 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-captions-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-captions.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-captions.png new file mode 100755 index 0000000000000000000000000000000000000000..93f11096297b248273dfef200a56c7a29bebe144 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-captions.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-exit-fullscreen-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-exit-fullscreen-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..ee211f809e8c3518cb8b1fc590e578dce09246a8 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-exit-fullscreen-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-exit-fullscreen.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-exit-fullscreen.png new file mode 100755 index 0000000000000000000000000000000000000000..aaa0893de804099069e5309cc982db329ad7bff8 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-exit-fullscreen.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-fullscreen-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-fullscreen-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..ea0a9ad23d31030ea4c49baae2bc9029a1a84560 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-fullscreen-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-fullscreen.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-fullscreen.png new file mode 100755 index 0000000000000000000000000000000000000000..f15827a3f7e2ec5b316fc0537c4443669639d7dc Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-fullscreen.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-pause-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-pause-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..95975c91d54bde8f4365f6fc61d6d48cc87fc338 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-pause-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-pause.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-pause.png new file mode 100755 index 0000000000000000000000000000000000000000..241593c84fdf4bba9a4f4df97b67091e1ae9ead6 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-pause.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-play-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-play-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..3db3f26cf0c78f61180aa1da02928e3ec1661894 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-play-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-play.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-play.png new file mode 100755 index 0000000000000000000000000000000000000000..28f84741ac14f602634476ad0bd2722a1f09b0b2 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-play.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-transcript-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-transcript-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..1c5d6eadd3ffb25be420eb8e5bfd6de7395c5c3e Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-transcript-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-transcript.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-transcript.png new file mode 100755 index 0000000000000000000000000000000000000000..6c63973a6a3132bfa89cfaba2a29abba715961aa Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-transcript.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..c2a34cdc4f34b709bbbd45e22ef9485e752b3e6f Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-full-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-full-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..4caea2f3f765c99b931013ac49e92b0f5c5fae3b Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-full-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-full.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-full.png new file mode 100755 index 0000000000000000000000000000000000000000..f4d4030d984f69fdc521ed63d567f30b66e31c4b Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume-full.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume.png new file mode 100755 index 0000000000000000000000000000000000000000..088c13204c789837cac92dcb0b178076af808793 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/access-volume.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/acorn.access.css b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/acorn.access.css new file mode 100755 index 0000000000000000000000000000000000000000..3a2356e2356b21506244bf089f970e95951c8386 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/acorn.access.css @@ -0,0 +1,314 @@ +/* + * acccess - Accessible Theme for Acorn Media Player + * accesslight - Child theme of access + * + * To be used with the horizontal volume slider. + * + * Copyright (C) 2010 Cristian I. Colceriu + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * www.ghinda.net + * contact@ghinda.net + * + */ + +/* Start of access theme */ +.acorn-player.access { + float: left; + position: relative; + overflow: hidden; + + font-family: Arial, Helvetica, sans-serif; +} +/* <video> element */ +.acorn-player.access video { + float: left; + clear: both; + background-color: #000; +} +/* Player Controls */ +.acorn-player.access .acorn-controls { + position: relative; + float: left; + clear: both; + width: 100%; + padding-top: 15px; + + background-image: url(controls-background-dark.png); + background-position: bottom left; +} +/* <button>s */ +.acorn-player.access button { + position: relative; + margin: 0; + padding-left: 25px; + height: 35px; + border: 1px solid #333; + background-color: #3F3F3F; + background-position: 5px center, top left; + background-repeat: no-repeat, repeat-x; + + font-weight: bold; + color: #fff; + text-shadow: 0px -1px 1px #000; + + cursor: pointer; +} +.acorn-player.access button:hover, .acorn-player.access button:focus { + background-color: #044293; + background-position: 5px center, left -33px; +} +.acorn-player.access button:active { + top: 1px; + box-shadow: inset 1px 1px 10px #000; +} +/* Playback Controls(Play, Pause) */ +.acorn-player.access .acorn-play-button { + float: left; + display: block; + width: 75px; + background-image: url(access-play.png), url(button-background-dark.png); +} +.acorn-player.access .acorn-paused-button { + background-image: url(access-pause.png), url(button-background-dark.png); +} +/* Seek Slider */ +.acorn-player.access .acorn-seek-slider { + position:absolute; + top: 0px; + display: block; + width: 100%; + height: 15px; + + background: #7289A8; + z-index: 2; +} +.acorn-player.access .acorn-seek-slider .ui-slider-handle { + display: block; + position: absolute; + width: 13px; + height: 13px; + border: 3px solid #fff; + top: -2px; + + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; + + -moz-box-shadow: 0px 2px 8px #000; + -webkit-box-shadow: 0px 2px 8px #000; + box-shadow: 0px 2px 8px #000; + + background: #888; +} +.acorn-player.access .acorn-seek-slider .ui-slider-range { + background: #0750B2; +} +.acorn-player.access .acorn-buffer { + background: #8E9DAF !important; +} +.acorn-player.access .acorn-seek-slider .ui-state-focus, .acorn-player.access .acorn-seek-slider .ui-slider-handle.ui-state-hover { + background: #0750B2 !important; + + -moz-box-shadow: 0px 2px 15px #000; + -webkit-box-shadow: 0px 2px 15px #000; + box-shadow: 0px 2px 15px #000; +} +/* Timer */ +.acorn-player.access .acorn-timer { + position: absolute; + top: 25px; + left: 260px; + + color: #efefef; + font-size: 14px; + font-weight: bold; + text-shadow: 0px -1px 2px #000; +} +/* Volume Container */ +.acorn-player.access .acorn-volume-box { + float: left; + overflow: hidden; + padding-right: 10px; + + -moz-box-shadow: 2px 0px 5px #111; + -webkit-box-shadow: 2px 0px 5px #111; + box-shadow: 2px 0px 5px #111; +} +/* Volume Button */ +.acorn-player.access .acorn-volume-button { + float: left; + width: 85px; + border-left: none; + background-image: url(access-volume-full.png), url(button-background-dark.png); + + -moz-box-shadow: 2px 0px 5px #111; + -webkit-box-shadow: 2px 0px 5px #111; + box-shadow: 2px 0px 5px #111; +} +.acorn-player.access .acorn-volume-mute { + background-image: url(access-volume.png), url(button-background-dark.png); +} +/* Volume Slider */ +.acorn-player.access .acorn-volume-slider { + float: left; + height: 5px; + width: 70px; + margin-left: 10px; + margin-top: 15px; + border: 1px solid #333; + + background: #111; + + -moz-box-shadow: 0px 1px 1px #777; + -webkit-box-shadow: 0px 1px 1px #777; + box-shadow: 0px 1px 1px #777; +} +.acorn-player.access .acorn-volume-slider .ui-slider-handle { + width: 5px; + height: 15px; + margin-top: -5px; + margin-left: -5px; + + border: 1px solid #333; + background: #BCBCBC; + + -moz-box-shadow: 0px 0px 5px #000; + -webkit-box-shadow: 0px 0px 5px #000; + box-shadow: 0px 0px 5px #000; +} +.acorn-player.access .acorn-volume-slider .ui-slider-handle.ui-state-hover, .acorn-player.access .acorn-volume-slider .ui-slider-handle.ui-state-focus { + background: #fff !important; +} +.acorn-player.access .acorn-volume-slider .ui-slider-range { + background: #636F7C; +} +/* Fullscreen Button */ +.acorn-player.access .acorn-fullscreen-button { + float: right; + background-image: url(access-fullscreen.png), url(button-background-dark.png); + + -moz-box-shadow: -2px 0px 5px #111; + -webkit-box-shadow: -2px 0px 5px #111; + box-shadow: -2px 0px 5px #111; +} +/* Fullscreen Mode */ +.acorn-player.access .fullscreen-controls { + left: 0px; + bottom: 0px; +} +.acorn-player.access .fullscreen-controls .acorn-fullscreen-button { + background-image: url(access-exit-fullscreen.png), url(button-background-dark.png); +} +/* Caption Button */ +.acorn-player.access .acorn-caption-button { + float: right; + border-right: none; + background-image: url(access-captions.png), url(button-background-dark.png); + + -moz-box-shadow: -2px 0px 5px #111; + -webkit-box-shadow: -2px 0px 5px #111; + box-shadow: -2px 0px 5px #111; +} +.acorn-player.access .acorn-caption { + font-size: 14px; + font-weight: bold; + color: #fff; + + text-shadow: 0px 1px 5px #000; +} +/* Transcript */ +.acorn-player.access .acorn-transcript-button { + float: right; + border-right: none; + background-image: url(access-transcript.png), url(button-background-dark.png); + + -moz-box-shadow: -2px 0px 5px #111; + -webkit-box-shadow: -2px 0px 5px #111; + box-shadow: -2px 0px 5px #111; +} +.acorn-player.access .acorn-caption-active, .acorn-player.access .acorn-transcript-active { + background-position: 5px center, left bottom; +} +/* + * acesslight Child Theme + */ +.acorn-player.access.accesslight .acorn-controls { + background-image: url(controls-background-light.png); +} +/* <button>s */ +.acorn-player.access.accesslight button { + border: 1px solid #bdbdbd; + + color: #333; + text-shadow: 0px 1px 0px #fff; +} +/* Playback Controls(Play, Pause) */ +.acorn-player.access.accesslight .acorn-play-button { + background-image: url(access-play-dark.png), url(button-background-light.png); +} +.acorn-player.access.accesslight .acorn-paused-button { + background-image: url(access-pause-dark.png), url(button-background-light.png); +} +/* Volume Button */ +.acorn-player.access.accesslight .acorn-volume-button { + background-image: url(access-volume-full-dark.png), url(button-background-light.png); + + -moz-box-shadow: 2px 0px 5px #8c8c8c; + -webkit-box-shadow: 2px 0px 5px #8c8c8c; + box-shadow: 2px 0px 5px #8c8c8c; +} +.acorn-player.access.accesslight .acorn-volume-mute { + background-image: url(access-volume-dark.png), url(button-background-light.png); +} +/* Caption Buttton */ +.acorn-player.access.accesslight .acorn-caption-button { + background-image: url(access-captions-dark.png), url(button-background-light.png); + + -moz-box-shadow: -2px 0px 5px #8c8c8c; + -webkit-box-shadow: -2px 0px 5px #8c8c8c; + box-shadow: -2px 0px 5px #8c8c8c; +} +/* Transcript */ +.acorn-player.access.accesslight .acorn-transcript-button { + background-image: url(access-transcript-dark.png), url(button-background-light.png); + + -moz-box-shadow: -2px 0px 5px #8c8c8c; + -webkit-box-shadow: -2px 0px 5px #8c8c8c; + box-shadow: -2px 0px 5px #8c8c8c; +} +.acorn-player.access.accesslight .acorn-caption-active, .acorn-player.access.accesslight .acorn-transcript-active { + color: #000; + text-shadow: none; +} +/* Fullscreen Button */ +.acorn-player.access.accesslight .acorn-fullscreen-button { + background-image: url(access-fullscreen-dark.png), url(button-background-light.png); + + -moz-box-shadow: -2px 0px 5px #8c8c8c; + -webkit-box-shadow: -2px 0px 5px #8c8c8c; + box-shadow: -2px 0px 5px #8c8c8c; +} +/* Volume Container */ +.acorn-player.access.accesslight .acorn-volume-box { + -moz-box-shadow: 2px 0px 5px #8c8c8c; + -webkit-box-shadow: 2px 0px 5px #8c8c8c; + box-shadow: 2px 0px 5px #8c8c8c; +} +/* Timer */ +.acorn-player.access.accesslight .acorn-timer { + color: #333; + text-shadow: 0px 1px 2px #fff; +} +/* Volume Slider */ +.acorn-player.access.accesslight .acorn-volume-slider { + border: 1px solid #333; + background: #c1c1c1; + + -moz-box-shadow: 0px 1px 1px #fff; + -webkit-box-shadow: 0px 1px 1px #fff; + box-shadow: 0px 1px 1px #fff; +} \ No newline at end of file diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/button-background-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/button-background-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..eb3ac270b7cffa473db459a8743b6cde71c93cc7 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/button-background-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/button-background-light.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/button-background-light.png new file mode 100755 index 0000000000000000000000000000000000000000..952c285996aa7020655731fc468d580f3305cca9 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/button-background-light.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/controls-background-dark.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/controls-background-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..88443bf0626331bec2a018df9800bf6d99be0e84 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/controls-background-dark.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/controls-background-light.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/controls-background-light.png new file mode 100755 index 0000000000000000000000000000000000000000..7e4c85aa5c0aac77f5fb335032f77b39e0ef0741 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/access/controls-background-light.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/barebones/acorn.barebones.css b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/barebones/acorn.barebones.css new file mode 100755 index 0000000000000000000000000000000000000000..60471b604eb1f10122d8e1b234ba47619203fe7f --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/barebones/acorn.barebones.css @@ -0,0 +1,143 @@ +/* + * barebones - Theme for Acorn Media Player + * + * To be used with the horizontal volume slider. + * + * Copyright (C) 2010 Cristian I. Colceriu + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * www.ghinda.net + * contact@ghinda.net + * + */ + +/* Start of barebones theme */ +.acorn-player.barebones { + float: left; + position: relative; + + font-family: Arial, Helvetica, sans-serif; +} +/* <video> element */ +.acorn-player.barebones video { + float: left; + clear: both; + + margin-bottom: 5px; +} +/* Player Controls */ +.acorn-player.barebones .acorn-controls { + position: relative; + float: left; + clear: both; + + width: 100%; +} +/* <button>s */ +.acorn-player.barebones button { +} +/* Playback controls(Play, Pause) */ +.acorn-player.barebones .acorn-play-button { + float: left; +} +.acorn-player.barebones .acorn-paused-button { +} +/* Seek Slider */ +.acorn-player.barebones .acorn-seek-slider { + position: relative; + display: block; + float: left; + width: 40%; + height: 10px; + margin: 6px 0px 0px 10px; + background: #ADADAD; +} +.acorn-player.barebones .acorn-seek-slider .ui-slider-handle { + display: block; + position: absolute; + width: 15px; + height: 15px; + top: -3px; + background: #e6e6e6; + border: 1px solid #000; +} +.acorn-player.barebones .acorn-seek-slider .ui-slider-range { + background: #4cbae8; +} +.acorn-player.barebones .acorn-buffer { + background: #939393 !important; +} +.acorn-player.barebones .acorn-seek-slider .ui-state-focus, .acorn-player.barebones .acorn-seek-slider .ui-slider-handle.ui-state-hover { + background: #fff !important; +} +/* Timer */ +.acorn-player.barebones .acorn-timer { + float: left; + margin: 2px 0px 0px 5px; +} +/* Volume Box */ +.acorn-player.barebones .acorn-volume-box { + float: left; + margin-left: 10px; +} +/* Volume Slider */ +.acorn-player.barebones .acorn-volume-slider { + float: left; + height: 10px; + width: 50px; + left: 4px; + margin: 6px 0px 0px 10px; + + background: #535353; +} +.acorn-player.barebones .acorn-volume-slider .ui-slider-handle { + width: 12px; + height: 12px; + left: -4px; + top: -2px; + border: 1px solid #000; + background: #e6e6e6; +} +.acorn-player.barebones .acorn-volume-slider .ui-slider-handle.ui-state-hover { + background: #fff; +} +.acorn-player.barebones .acorn-volume-slider .ui-slider-range { + background: #e6e6e6; +} +/* Volume Button */ +.acorn-player.barebones .acorn-volume-button { + float: left; +} +.acorn-player.barebones .acorn-volume-mute { +} +/* Fullscreen Button */ +.acorn-player.barebones .acorn-fullscreen-button { + float: right; +} +/* Fullscreen Mode */ +.acorn-player.barebones .fullscreen-controls { + left: 0px; + bottom: 0px; +} +/* Caption Button */ +.acorn-player.barebones .acorn-caption-button { + float: right; +} +.acorn-player.barebones .acorn-caption { + font-size: 14px; + font-weight: bold; + color: #fff; +} +.acorn-player.barebones .acorn-caption-active { + border: 2px solid #8F0000 !important; +} +.acorn-player.barebones .acorn-transcript-active { + border: 2px solid #8F0000 !important; +} +/* Transcript Button */ +.acorn-player.barebones .acorn-transcript-button { + float: right; +} \ No newline at end of file diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/acorn.darkglass.css b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/acorn.darkglass.css new file mode 100755 index 0000000000000000000000000000000000000000..907e253c706c135f86051ecabf4cb8c9e66bea39 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/acorn.darkglass.css @@ -0,0 +1,363 @@ +/* + * darkglass - Theme for Acorn Media Player + * darkglasssmall - Child theme of darkglass + * + * To be used with the vertical volume slider. + * + * Copyright (C) 2010 Cristian I. Colceriu + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * www.ghinda.net + * contact@ghinda.net + * + */ + +/* Start of darkglass theme */ +.acorn-player.darkglass { + float: left; + position: relative; + padding: 2px; + font-family: Arial, Helvetica, sans-serif; +} +/* <video> element */ +.acorn-player.darkglass video { + float: left; + clear: both; + margin-bottom: 5px; +} +/* Audio player */ +/* + * If you're playing <audio>, we're assigning a Width larger by 10%, because we're missing two buttons(Captions and Transcript) + * each with a 5% Width + */ +.acorn-player.darkglass.audio-player .acorn-seek-slider { + width: 82%; +} +/* Player Controls */ +.acorn-player.darkglass .acorn-controls { + position: relative; + float: left; + clear: both; + width: 95%; + padding-right: 5%; + padding-left: 1%; + border: 2px solid #61625d; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + background: #000000; + background-image: -moz-linear-gradient(top, #313131, #000000); + background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0, #313131),color-stop(1, #000000)); +} +/* <button>s */ +.acorn-player.darkglass button { + position: relative; + height: 22px; + width: 4%; + margin-right: 1%; + padding: 0px; + border: none; + background-color: transparent; + background-repeat: no-repeat; + background-position: center center; + background-size: auto 100%; + + opacity: 0.7; + -moz-transition: all 0.2s ease-in-out; + -webkit-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + + cursor: pointer; + text-indent: -9999px; +} +.acorn-player.darkglass button:hover, .acorn-player.darkglass button:focus { + opacity: 1; +} +.acorn-player.darkglass button:active { + top: 1px; +} +/* Playback controls(Play, Pause) */ +.acorn-player.darkglass .acorn-play-button { + float: left; + display: block; + background-image: url(darkglass-play.png); +} +.acorn-player.darkglass .acorn-paused-button { + background-image: url(darkglass-pause.png); +} +/* Seek Slider */ +.acorn-player.darkglass .acorn-seek-slider { + position: relative; + display: block; + float: left; + width: 72%; + height: 10px; + margin: 5px 1% 0px 1%; + background: #7289A8; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; + border-radius: 15px; +} +.acorn-player.darkglass .acorn-seek-slider .ui-slider-handle { + display: block; + position: absolute; + width: 15px; + height: 15px; + border: 1px solid #333; + top: -4px; + background: #e6e6e6; + + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; +} +.acorn-player.darkglass .acorn-seek-slider .ui-slider-range { + background: #0750B2; + + -moz-border-radius:10px; + -webkit-border-radius:10px; + border-radius:10px; +} +.acorn-player.darkglass .acorn-buffer { + background: #8E9DAF !important; +} +.acorn-player.darkglass .acorn-seek-slider .ui-state-focus, .acorn-player.darkglass .acorn-seek-slider .ui-slider-handle.ui-state-hover { + background: #fff !important; + + -moz-box-shadow: 0px 2px 15px #ff0000; + -webkit-box-shadow: 0px 2px 15px #ff0000; + box-shadow: 0px 2px 15px #ff0000; +} +/* Timer */ +.acorn-player.darkglass .acorn-timer { + float: left; + width: 6%; + overflow: hidden; + margin-top: 5px; + + color: #999; + font-size: 0.7em; + font-weight: bold; +} +/* Volume Box */ +.acorn-player.darkglass .acorn-volume-box { + position: absolute; + float: left; + bottom: 0px; + right: 0px; + overflow: visible; + width: 5%; + height: 35px; + color: #fff; + + -moz-transition: all 0.1s ease-in-out; + -webkit-transition: all 0.1s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.1s ease-in-out; +} +.acorn-player.darkglass .acorn-volume-box:hover { + height: 135px; +} +.acorn-player.darkglass .acorn-volume-slider.handle-focused { + position: relative; + visibility: visible; + height: 100px; + opacity: 1; + top: -100px; +} +.acorn-player.darkglass .acorn-volume-box:hover .acorn-volume-slider { + position: relative; + visibility: visible; + height: 100px; + opacity: 1; + top: 0px; +} +/* Volume Slider */ +.acorn-player.darkglass .acorn-volume-slider { + position: relative; + height: 1px; + width: 7px; + left: 4px; + + visibility: visible; + opacity: 0; + + border: 1px solid #444; + + -moz-border-radius: 15px; + -webkit-border-radius: 15px; + border-radius: 15px; + + background: #535353; + background-image: -moz-linear-gradient(top, #535353, #333333); + background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0, #535353),color-stop(1, #333333)); + + box-shadow: inset 0 3px 3px #333333; + + -moz-transition: all 0.1s ease-in-out; + -webkit-transition: all 0.1s ease-in-out; + -o-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; +} +.acorn-player.darkglass .acorn-volume-slider .ui-slider-handle { + width: 12px; + height: 12px; + left: -4px; + margin-bottom:-0.6em; + margin-left:0; + border: 1px solid #333; + + -moz-border-radius:10px; + -webkit-border-radius:10px; + border-radius:10px; + + background: #e6e6e6; + background-image: -moz-linear-gradient(top, #e6e6e6, #d5d5d5); + background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0, #e6e6e6),color-stop(1, #d5d5d5)); + + box-shadow: inset 0 3px 3px #d5d5d5; +} +.acorn-player.darkglass .acorn-volume-slider .ui-slider-handle:hover, .acorn-player.darkglass .acorn-volume-slider.handle-focused .ui-slider-handle { + background: #fff; + + -moz-box-shadow: 0px 2px 15px #ff0000; + -webkit-box-shadow: 0px 2px 15px #ff0000; + box-shadow: 0px 2px 15px #ff0000; +} +.acorn-player.darkglass .acorn-volume-slider .ui-slider-range { + -moz-border-radius: 15px; + -webkit-border-radius: 15px; + border-radius: 15px; + + background: #e6e6e6; + background-image: -moz-linear-gradient(top, #e6e6e6, #d5d5d5); + background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0, #e6e6e6),color-stop(1, #d5d5d5)); + + box-shadow: inset 0 3px 3px #d5d5d5; +} +/* Volume Button */ +.acorn-player.darkglass .acorn-volume-button { + position: absolute; + bottom: 0px; + width: 100%; + display: block; + background: url(darkglass-volume-full.png) no-repeat; + text-indent: -9999px; + + opacity: 0.8; +} +.acorn-player.darkglass .acorn-volume-button:active { + top: auto; +} +.acorn-player.darkglass .acorn-volume-mute { + background-image: url(darkglass-volume.png); +} +/* Swap Button */ +.acorn-player.darkglass .acorn-swap-button { + float: right; + background-image: url(darkglass-swap.png); +} +/* Fullscreen Button */ +.acorn-player.darkglass .acorn-fullscreen-button { + float: right; + background-image: url(darkglass-fullscreen.png); +} +/* Fullscreen Mode */ +.acorn-player.darkglass .fullscreen-controls { + left: 0px; + bottom: 0px; +} +.acorn-player.darkglass .fullscreen-controls button { + height: 35px; +} +.acorn-player.darkglass .fullscreen-controls .acorn-fullscreen-button { + background-image: url(darkglass-exit-fullscreen.png); +} +.acorn-player.darkglass .fullscreen-controls .acorn-seek-slider { + margin-top: 10px; +} +/* Caption Button */ +.acorn-player.darkglass .acorn-caption-button { + float: right; + background-image: url(darkglass-caption.png); +} +.acorn-player.darkglass .acorn-caption { + font-size: 14px; + font-weight: bold; + color: #fff; + + text-shadow: 0px 1px 5px #000; +} +.acorn-player.darkglass .acorn-caption-active { + background-color: #8F0000 !important; +} +.acorn-player.darkglass .acorn-transcript-active { + background-color: #8F0000 !important; +} +/* Transcript Button */ +.acorn-player.darkglass .acorn-transcript-button { + float: right; + background-image: url(darkglass-transcript.png); +} +/* + * darkglasssmall Child Theme + */ +.acorn-player.darkglasssmall { + padding: 0px; +} +.acorn-player.darkglasssmall video:hover + .acorn-controls { + visibility: visible; + opacity: 0.7; +} +.acorn-player.darkglasssmall .acorn-controls:hover { + visibility: visible; + opacity: 0.7; +} +.acorn-player.darkglasssmall .acorn-controls { + position: absolute; + bottom: 5%; + width: 87%; + margin-left: 2%; + padding: 2% 7% 2% 2%; + + border: 1px solid #2E2E2E; + + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + + background: #000000; + background-image: -moz-linear-gradient(top, #313131, #000000); + background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0, #313131),color-stop(1, #000000)); + + opacity: 0; + visibility: hidden; + + -moz-transition: all 0.1s ease-in-out; + -webkit-transition: all 0.1s ease-in-out; + -o-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; +} +.acorn-player.darkglasssmall .acorn-volume-box { + margin-right: 2%; + margin-bottom: 2%; +} +/* Audio player */ +.acorn-player.darkglasssmall.audio-player .acorn-controls { + display: block; + position: relative; + visibility: visible; + opacity: 1; + margin-left: 0px; + width: 91%; + + border: none; +} diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-caption.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-caption.png new file mode 100755 index 0000000000000000000000000000000000000000..9c814ec7547919896fac5df9ccbff0988c784b4e Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-caption.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-exit-fullscreen.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-exit-fullscreen.png new file mode 100755 index 0000000000000000000000000000000000000000..3df3bf468cc385d8a39a7e6e427fbe4d8d1a1dee Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-exit-fullscreen.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-fullscreen.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-fullscreen.png new file mode 100755 index 0000000000000000000000000000000000000000..c70fb08ee7ce2ae3253151d978c353b33b049311 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-fullscreen.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-pause.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-pause.png new file mode 100755 index 0000000000000000000000000000000000000000..26462e66f6ed8522650890f93de8a7861cc35b49 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-pause.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-play.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-play.png new file mode 100755 index 0000000000000000000000000000000000000000..5d5aacae17c9e7ff391b1fcefec296943f074a1e Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-play.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-swap.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-swap.png new file mode 100755 index 0000000000000000000000000000000000000000..055dacceac1cc04d28cdd69f4dd2995fe6ebeaa0 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-swap.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-transcript.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-transcript.png new file mode 100755 index 0000000000000000000000000000000000000000..e6d6dac51651d27e804eccdcca62087276aeff02 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-transcript.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-volume-full.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-volume-full.png new file mode 100755 index 0000000000000000000000000000000000000000..485a1133d82ef535b98f118893cd1575df3cdcb7 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-volume-full.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-volume.png b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-volume.png new file mode 100755 index 0000000000000000000000000000000000000000..d4bdcbf622ee54121870c17e9763c76eb8569d00 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/acornmediaplayer/themes/darkglass/darkglass-volume.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/css/bbb.playback.css b/record-and-playback/presentation_export/playback/presentation_export/css/bbb.playback.css new file mode 100755 index 0000000000000000000000000000000000000000..4364dbb9e2cac1f1fb4a9ff7ed0470eea4a3975b --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/css/bbb.playback.css @@ -0,0 +1,256 @@ +/* + +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/>. + +*/ + +html{ +} +body { + font-family: Verdana; + background: #fff; + padding-top: 30px; +} +h1 { + text-align:center +} +br{ + display:none +} + +/* + * clearfix + * see: http://css-tricks.com/snippets/css/clear-fix/ + */ +.clearfix:after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; +} +* html .clearfix { zoom: 1; } /* IE6 */ +*:first-child+html .clearfix { zoom: 1; } /* IE7 */ + +#playbackArea { + width: 1360px; /* #slide.width + #chat.width + #audioRecording.width */ + height: 650px; + margin: 0 auto; + overflow: hidden; +} + +#audioRecordingWrapper{ + float: left; + width: 800px; +} + +#audioRecording{ + display: block; + margin-left: auto; + margin-right: auto; + width: 100%; +} + +#audioRecordingWrapper .acorn-controls, #videoRecordingWrapper .acorn-controls{ + position: relative; + top: 0; + left: -810px; + width: 730px; +} + +#playbackControls{ + width: 1360px; + margin: 0 auto; + margin-top: -50px; +} + +#autoscrollWrapper{ + float: left; + margin-left: 10px; + margin-top: 8px; + font-size: 14px; +} + +.webcam{ + width: 402px; + height: 300px; +} + +#video{ + width: 402px; + background: white; +} + +/* To remove the white space on top of the audio tag in Firefox + * See: http://stackoverflow.com/questions/9527453/firefox-and-html5-audio-element-dont-play-well-together + */ +@-moz-document url-prefix() { + #audioRecordingWrapper{ + position: relative; + height: 28px; + } + #audioRecording { + position: absolute; + bottom: 0; + } +} + +#presentation { + float: left; + position: relative; + height: 600px; +} + +#slide { + background-image: url('../logo.png'); + text-align: center; + border: 0px solid #ccc; + width: 800px; + height: 600px; /* same as slide images */ + position: relative; + top: -12px; +} + +/* Visually hides text + * see: yaccessibilityblog.com/library/css-clip-hidden-content.html + */ +.visually-hidden { + position: absolute !important; + clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ + clip: rect(1px, 1px, 1px, 1px); + padding: 0 !important; + border: 0 !important; + height: 1px !important; + width: 1px !important; + overflow: hidden; +} + +#mediaArea { + float: right; + background: white; + width: 402px; +} + +#chatAndMediaArea{ + float: right; + background: white; + height: 600px; + width: 402px; +} +#chat{ + margin: 0 auto; + padding: 0 10px; + border: 0px solid #ccc; + height: 300px; + overflow: auto; +} +#chat div{ + padding:0px; + font-size:13px; +} +#big {display:none} +#mid {display:none} + +#thumbnails { + float: left; + width: 130px; + height: 600px; /* same as #slide */ + background: #fff; + border: 0px solid #bbb; + margin-right: 10px; + overflow-y: scroll; + overflow-x: hidden; +} + +#thumbnails img.thumbnail { + width: 100px; + height: auto; + border: 0px solid #eee; + margin: 5px; + cursor: pointer; + vertical-align: bottom; +} + +#thumbnails .thumbnail-wrapper { + position: relative; + margin: 0; + padding: 0; +} + +#thumbnails .thumbnail-wrapper.active { + background-color: #D9EDF7; +} + +#thumbnails .thumbnail-wrapper.active img.thumbnail { + border-color: #3A87AD; +} + +#thumbnails .thumbnail-label { + color: #fff; + background: #3A87AD; + font-weight: bold; + font-size: 12px; + position: absolute; + bottom: 5px; + left: 5px; + max-width: 90px; + text-align: center; + display: none; + padding: 2px 5px; + cursor: pointer; +} + +#accInfo{ + margin: 20px auto; + font-size:0.75em; + text-align: center; + clear: both; + padding-top: 75px; +} + +#footer{ + margin: 20px auto; + font-size:0.75em; + color: #666; + text-align: center; + clear: both; + padding-top: 35px; +} + +.circle { + height: 12px; + width: 12px; + border-radius: 50%; +} + + +#cursor { + position: relative; + background: red; + z-index: 10; +} + +#load-recording-msg { + text-align: center; + height: 50px; + width: 200px; + position: absolute; + left: 50%; + margin-left: -100px; + top:60%; +} \ No newline at end of file diff --git a/record-and-playback/presentation_export/playback/presentation_export/lib/jquery-ui-1.8.23.custom.min.js b/record-and-playback/presentation_export/playback/presentation_export/lib/jquery-ui-1.8.23.custom.min.js new file mode 100644 index 0000000000000000000000000000000000000000..2812f523c0ae8c4dd12c4ce56287f43f860789dc --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/lib/jquery-ui-1.8.23.custom.min.js @@ -0,0 +1,21 @@ +/*! jQuery UI - v1.8.23 - 2012-08-15 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.core.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){function c(b,c){var e=b.nodeName.toLowerCase();if("area"===e){var f=b.parentNode,g=f.name,h;return!b.href||!g||f.nodeName.toLowerCase()!=="map"?!1:(h=a("img[usemap=#"+g+"]")[0],!!h&&d(h))}return(/input|select|textarea|button|object/.test(e)?!b.disabled:"a"==e?b.href||c:c)&&d(b)}function d(b){return!a(b).parents().andSelf().filter(function(){return a.curCSS(this,"visibility")==="hidden"||a.expr.filters.hidden(this)}).length}a.ui=a.ui||{};if(a.ui.version)return;a.extend(a.ui,{version:"1.8.23",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}}),a.fn.extend({propAttr:a.fn.prop||a.fn.attr,_focus:a.fn.focus,focus:function(b,c){return typeof b=="number"?this.each(function(){var d=this;setTimeout(function(){a(d).focus(),c&&c.call(d)},b)}):this._focus.apply(this,arguments)},scrollParent:function(){var b;return a.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?b=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(a.curCSS(this,"position",1))&&/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0):b=this.parents().filter(function(){return/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0),/fixed/.test(this.css("position"))||!b.length?a(document):b},zIndex:function(c){if(c!==b)return this.css("zIndex",c);if(this.length){var d=a(this[0]),e,f;while(d.length&&d[0]!==document){e=d.css("position");if(e==="absolute"||e==="relative"||e==="fixed"){f=parseInt(d.css("zIndex"),10);if(!isNaN(f)&&f!==0)return f}d=d.parent()}}return 0},disableSelection:function(){return this.bind((a.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),a("<a>").outerWidth(1).jquery||a.each(["Width","Height"],function(c,d){function h(b,c,d,f){return a.each(e,function(){c-=parseFloat(a.curCSS(b,"padding"+this,!0))||0,d&&(c-=parseFloat(a.curCSS(b,"border"+this+"Width",!0))||0),f&&(c-=parseFloat(a.curCSS(b,"margin"+this,!0))||0)}),c}var e=d==="Width"?["Left","Right"]:["Top","Bottom"],f=d.toLowerCase(),g={innerWidth:a.fn.innerWidth,innerHeight:a.fn.innerHeight,outerWidth:a.fn.outerWidth,outerHeight:a.fn.outerHeight};a.fn["inner"+d]=function(c){return c===b?g["inner"+d].call(this):this.each(function(){a(this).css(f,h(this,c)+"px")})},a.fn["outer"+d]=function(b,c){return typeof b!="number"?g["outer"+d].call(this,b):this.each(function(){a(this).css(f,h(this,b,!0,c)+"px")})}}),a.extend(a.expr[":"],{data:a.expr.createPseudo?a.expr.createPseudo(function(b){return function(c){return!!a.data(c,b)}}):function(b,c,d){return!!a.data(b,d[3])},focusable:function(b){return c(b,!isNaN(a.attr(b,"tabindex")))},tabbable:function(b){var d=a.attr(b,"tabindex"),e=isNaN(d);return(e||d>=0)&&c(b,!e)}}),a(function(){var b=document.body,c=b.appendChild(c=document.createElement("div"));c.offsetHeight,a.extend(c.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),a.support.minHeight=c.offsetHeight===100,a.support.selectstart="onselectstart"in c,b.removeChild(c).style.display="none"}),a.curCSS||(a.curCSS=a.css),a.extend(a.ui,{plugin:{add:function(b,c,d){var e=a.ui[b].prototype;for(var f in d)e.plugins[f]=e.plugins[f]||[],e.plugins[f].push([c,d[f]])},call:function(a,b,c){var d=a.plugins[b];if(!d||!a.element[0].parentNode)return;for(var e=0;e<d.length;e++)a.options[d[e][0]]&&d[e][1].apply(a.element,c)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(b,c){if(a(b).css("overflow")==="hidden")return!1;var d=c&&c==="left"?"scrollLeft":"scrollTop",e=!1;return b[d]>0?!0:(b[d]=1,e=b[d]>0,b[d]=0,e)},isOverAxis:function(a,b,c){return a>b&&a<b+c},isOver:function(b,c,d,e,f,g){return a.ui.isOverAxis(b,d,f)&&a.ui.isOverAxis(c,e,g)}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.widget.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){if(a.cleanData){var c=a.cleanData;a.cleanData=function(b){for(var d=0,e;(e=b[d])!=null;d++)try{a(e).triggerHandler("remove")}catch(f){}c(b)}}else{var d=a.fn.remove;a.fn.remove=function(b,c){return this.each(function(){return c||(!b||a.filter(b,[this]).length)&&a("*",this).add([this]).each(function(){try{a(this).triggerHandler("remove")}catch(b){}}),d.call(a(this),b,c)})}}a.widget=function(b,c,d){var e=b.split(".")[0],f;b=b.split(".")[1],f=e+"-"+b,d||(d=c,c=a.Widget),a.expr[":"][f]=function(c){return!!a.data(c,b)},a[e]=a[e]||{},a[e][b]=function(a,b){arguments.length&&this._createWidget(a,b)};var g=new c;g.options=a.extend(!0,{},g.options),a[e][b].prototype=a.extend(!0,g,{namespace:e,widgetName:b,widgetEventPrefix:a[e][b].prototype.widgetEventPrefix||b,widgetBaseClass:f},d),a.widget.bridge(b,a[e][b])},a.widget.bridge=function(c,d){a.fn[c]=function(e){var f=typeof e=="string",g=Array.prototype.slice.call(arguments,1),h=this;return e=!f&&g.length?a.extend.apply(null,[!0,e].concat(g)):e,f&&e.charAt(0)==="_"?h:(f?this.each(function(){var d=a.data(this,c),f=d&&a.isFunction(d[e])?d[e].apply(d,g):d;if(f!==d&&f!==b)return h=f,!1}):this.each(function(){var b=a.data(this,c);b?b.option(e||{})._init():a.data(this,c,new d(e,this))}),h)}},a.Widget=function(a,b){arguments.length&&this._createWidget(a,b)},a.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:!1},_createWidget:function(b,c){a.data(c,this.widgetName,this),this.element=a(c),this.options=a.extend(!0,{},this.options,this._getCreateOptions(),b);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()}),this._create(),this._trigger("create"),this._init()},_getCreateOptions:function(){return a.metadata&&a.metadata.get(this.element[0])[this.widgetName]},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName),this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled "+"ui-state-disabled")},widget:function(){return this.element},option:function(c,d){var e=c;if(arguments.length===0)return a.extend({},this.options);if(typeof c=="string"){if(d===b)return this.options[c];e={},e[c]=d}return this._setOptions(e),this},_setOptions:function(b){var c=this;return a.each(b,function(a,b){c._setOption(a,b)}),this},_setOption:function(a,b){return this.options[a]=b,a==="disabled"&&this.widget()[b?"addClass":"removeClass"](this.widgetBaseClass+"-disabled"+" "+"ui-state-disabled").attr("aria-disabled",b),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_trigger:function(b,c,d){var e,f,g=this.options[b];d=d||{},c=a.Event(c),c.type=(b===this.widgetEventPrefix?b:this.widgetEventPrefix+b).toLowerCase(),c.target=this.element[0],f=c.originalEvent;if(f)for(e in f)e in c||(c[e]=f[e]);return this.element.trigger(c,d),!(a.isFunction(g)&&g.call(this.element[0],c,d)===!1||c.isDefaultPrevented())}}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.mouse.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){var c=!1;a(document).mouseup(function(a){c=!1}),a.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var b=this;this.element.bind("mousedown."+this.widgetName,function(a){return b._mouseDown(a)}).bind("click."+this.widgetName,function(c){if(!0===a.data(c.target,b.widgetName+".preventClickEvent"))return a.removeData(c.target,b.widgetName+".preventClickEvent"),c.stopImmediatePropagation(),!1}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(b){if(c)return;this._mouseStarted&&this._mouseUp(b),this._mouseDownEvent=b;var d=this,e=b.which==1,f=typeof this.options.cancel=="string"&&b.target.nodeName?a(b.target).closest(this.options.cancel).length:!1;if(!e||f||!this._mouseCapture(b))return!0;this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){d.mouseDelayMet=!0},this.options.delay));if(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)){this._mouseStarted=this._mouseStart(b)!==!1;if(!this._mouseStarted)return b.preventDefault(),!0}return!0===a.data(b.target,this.widgetName+".preventClickEvent")&&a.removeData(b.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(a){return d._mouseMove(a)},this._mouseUpDelegate=function(a){return d._mouseUp(a)},a(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),b.preventDefault(),c=!0,!0},_mouseMove:function(b){return!a.browser.msie||document.documentMode>=9||!!b.button?this._mouseStarted?(this._mouseDrag(b),b.preventDefault()):(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,b)!==!1,this._mouseStarted?this._mouseDrag(b):this._mouseUp(b)),!this._mouseStarted):this._mouseUp(b)},_mouseUp:function(b){return a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,b.target==this._mouseDownEvent.target&&a.data(b.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(b)),!1},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(a){return this.mouseDelayMet},_mouseStart:function(a){},_mouseDrag:function(a){},_mouseStop:function(a){},_mouseCapture:function(a){return!0}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.position.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){a.ui=a.ui||{};var c=/left|center|right/,d=/top|center|bottom/,e="center",f={},g=a.fn.position,h=a.fn.offset;a.fn.position=function(b){if(!b||!b.of)return g.apply(this,arguments);b=a.extend({},b);var h=a(b.of),i=h[0],j=(b.collision||"flip").split(" "),k=b.offset?b.offset.split(" "):[0,0],l,m,n;return i.nodeType===9?(l=h.width(),m=h.height(),n={top:0,left:0}):i.setTimeout?(l=h.width(),m=h.height(),n={top:h.scrollTop(),left:h.scrollLeft()}):i.preventDefault?(b.at="left top",l=m=0,n={top:b.of.pageY,left:b.of.pageX}):(l=h.outerWidth(),m=h.outerHeight(),n=h.offset()),a.each(["my","at"],function(){var a=(b[this]||"").split(" ");a.length===1&&(a=c.test(a[0])?a.concat([e]):d.test(a[0])?[e].concat(a):[e,e]),a[0]=c.test(a[0])?a[0]:e,a[1]=d.test(a[1])?a[1]:e,b[this]=a}),j.length===1&&(j[1]=j[0]),k[0]=parseInt(k[0],10)||0,k.length===1&&(k[1]=k[0]),k[1]=parseInt(k[1],10)||0,b.at[0]==="right"?n.left+=l:b.at[0]===e&&(n.left+=l/2),b.at[1]==="bottom"?n.top+=m:b.at[1]===e&&(n.top+=m/2),n.left+=k[0],n.top+=k[1],this.each(function(){var c=a(this),d=c.outerWidth(),g=c.outerHeight(),h=parseInt(a.curCSS(this,"marginLeft",!0))||0,i=parseInt(a.curCSS(this,"marginTop",!0))||0,o=d+h+(parseInt(a.curCSS(this,"marginRight",!0))||0),p=g+i+(parseInt(a.curCSS(this,"marginBottom",!0))||0),q=a.extend({},n),r;b.my[0]==="right"?q.left-=d:b.my[0]===e&&(q.left-=d/2),b.my[1]==="bottom"?q.top-=g:b.my[1]===e&&(q.top-=g/2),f.fractions||(q.left=Math.round(q.left),q.top=Math.round(q.top)),r={left:q.left-h,top:q.top-i},a.each(["left","top"],function(c,e){a.ui.position[j[c]]&&a.ui.position[j[c]][e](q,{targetWidth:l,targetHeight:m,elemWidth:d,elemHeight:g,collisionPosition:r,collisionWidth:o,collisionHeight:p,offset:k,my:b.my,at:b.at})}),a.fn.bgiframe&&c.bgiframe(),c.offset(a.extend(q,{using:b.using}))})},a.ui.position={fit:{left:function(b,c){var d=a(window),e=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft();b.left=e>0?b.left-e:Math.max(b.left-c.collisionPosition.left,b.left)},top:function(b,c){var d=a(window),e=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop();b.top=e>0?b.top-e:Math.max(b.top-c.collisionPosition.top,b.top)}},flip:{left:function(b,c){if(c.at[0]===e)return;var d=a(window),f=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft(),g=c.my[0]==="left"?-c.elemWidth:c.my[0]==="right"?c.elemWidth:0,h=c.at[0]==="left"?c.targetWidth:-c.targetWidth,i=-2*c.offset[0];b.left+=c.collisionPosition.left<0?g+h+i:f>0?g+h+i:0},top:function(b,c){if(c.at[1]===e)return;var d=a(window),f=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop(),g=c.my[1]==="top"?-c.elemHeight:c.my[1]==="bottom"?c.elemHeight:0,h=c.at[1]==="top"?c.targetHeight:-c.targetHeight,i=-2*c.offset[1];b.top+=c.collisionPosition.top<0?g+h+i:f>0?g+h+i:0}}},a.offset.setOffset||(a.offset.setOffset=function(b,c){/static/.test(a.curCSS(b,"position"))&&(b.style.position="relative");var d=a(b),e=d.offset(),f=parseInt(a.curCSS(b,"top",!0),10)||0,g=parseInt(a.curCSS(b,"left",!0),10)||0,h={top:c.top-e.top+f,left:c.left-e.left+g};"using"in c?c.using.call(b,h):d.css(h)},a.fn.offset=function(b){var c=this[0];return!c||!c.ownerDocument?null:b?a.isFunction(b)?this.each(function(c){a(this).offset(b.call(this,c,a(this).offset()))}):this.each(function(){a.offset.setOffset(this,b)}):h.call(this)}),a.curCSS||(a.curCSS=a.css),function(){var b=document.getElementsByTagName("body")[0],c=document.createElement("div"),d,e,g,h,i;d=document.createElement(b?"div":"body"),g={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},b&&a.extend(g,{position:"absolute",left:"-1000px",top:"-1000px"});for(var j in g)d.style[j]=g[j];d.appendChild(c),e=b||document.documentElement,e.insertBefore(d,e.firstChild),c.style.cssText="position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;",h=a(c).offset(function(a,b){return b}).offset(),d.innerHTML="",e.removeChild(d),i=h.top+h.left+(b?2e3:0),f.fractions=i>21&&i<22}()})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.slider.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){var c=5;a.widget("ui.slider",a.ui.mouse,{widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null},_create:function(){var b=this,d=this.options,e=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),f="<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>",g=d.values&&d.values.length||1,h=[];this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"+(d.disabled?" ui-slider-disabled ui-disabled":"")),this.range=a([]),d.range&&(d.range===!0&&(d.values||(d.values=[this._valueMin(),this._valueMin()]),d.values.length&&d.values.length!==2&&(d.values=[d.values[0],d.values[0]])),this.range=a("<div></div>").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(d.range==="min"||d.range==="max"?" ui-slider-range-"+d.range:"")));for(var i=e.length;i<g;i+=1)h.push(f);this.handles=e.add(a(h.join("")).appendTo(b.element)),this.handle=this.handles.eq(0),this.handles.add(this.range).filter("a").click(function(a){a.preventDefault()}).hover(function(){d.disabled||a(this).addClass("ui-state-hover")},function(){a(this).removeClass("ui-state-hover")}).focus(function(){d.disabled?a(this).blur():(a(".ui-slider .ui-state-focus").removeClass("ui-state-focus"),a(this).addClass("ui-state-focus"))}).blur(function(){a(this).removeClass("ui-state-focus")}),this.handles.each(function(b){a(this).data("index.ui-slider-handle",b)}),this.handles.keydown(function(d){var e=a(this).data("index.ui-slider-handle"),f,g,h,i;if(b.options.disabled)return;switch(d.keyCode){case a.ui.keyCode.HOME:case a.ui.keyCode.END:case a.ui.keyCode.PAGE_UP:case a.ui.keyCode.PAGE_DOWN:case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:d.preventDefault();if(!b._keySliding){b._keySliding=!0,a(this).addClass("ui-state-active"),f=b._start(d,e);if(f===!1)return}}i=b.options.step,b.options.values&&b.options.values.length?g=h=b.values(e):g=h=b.value();switch(d.keyCode){case a.ui.keyCode.HOME:h=b._valueMin();break;case a.ui.keyCode.END:h=b._valueMax();break;case a.ui.keyCode.PAGE_UP:h=b._trimAlignValue(g+(b._valueMax()-b._valueMin())/c);break;case a.ui.keyCode.PAGE_DOWN:h=b._trimAlignValue(g-(b._valueMax()-b._valueMin())/c);break;case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:if(g===b._valueMax())return;h=b._trimAlignValue(g+i);break;case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:if(g===b._valueMin())return;h=b._trimAlignValue(g-i)}b._slide(d,e,h)}).keyup(function(c){var d=a(this).data("index.ui-slider-handle");b._keySliding&&(b._keySliding=!1,b._stop(c,d),b._change(c,d),a(this).removeClass("ui-state-active"))}),this._refreshValue(),this._animateOff=!1},destroy:function(){return this.handles.remove(),this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"),this._mouseDestroy(),this},_mouseCapture:function(b){var c=this.options,d,e,f,g,h,i,j,k,l;return c.disabled?!1:(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),d={x:b.pageX,y:b.pageY},e=this._normValueFromMouse(d),f=this._valueMax()-this._valueMin()+1,h=this,this.handles.each(function(b){var c=Math.abs(e-h.values(b));f>c&&(f=c,g=a(this),i=b)}),c.range===!0&&this.values(1)===c.min&&(i+=1,g=a(this.handles[i])),j=this._start(b,i),j===!1?!1:(this._mouseSliding=!0,h._handleIndex=i,g.addClass("ui-state-active").focus(),k=g.offset(),l=!a(b.target).parents().andSelf().is(".ui-slider-handle"),this._clickOffset=l?{left:0,top:0}:{left:b.pageX-k.left-g.width()/2,top:b.pageY-k.top-g.height()/2-(parseInt(g.css("borderTopWidth"),10)||0)-(parseInt(g.css("borderBottomWidth"),10)||0)+(parseInt(g.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(b,i,e),this._animateOff=!0,!0))},_mouseStart:function(a){return!0},_mouseDrag:function(a){var b={x:a.pageX,y:a.pageY},c=this._normValueFromMouse(b);return this._slide(a,this._handleIndex,c),!1},_mouseStop:function(a){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(a,this._handleIndex),this._change(a,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b,c,d,e,f;return this.orientation==="horizontal"?(b=this.elementSize.width,c=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(b=this.elementSize.height,c=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),d=c/b,d>1&&(d=1),d<0&&(d=0),this.orientation==="vertical"&&(d=1-d),e=this._valueMax()-this._valueMin(),f=this._valueMin()+d*e,this._trimAlignValue(f)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};return this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("start",a,c)},_slide:function(a,b,c){var d,e,f;this.options.values&&this.options.values.length?(d=this.values(b?0:1),this.options.values.length===2&&this.options.range===!0&&(b===0&&c>d||b===1&&c<d)&&(c=d),c!==this.values(b)&&(e=this.values(),e[b]=c,f=this._trigger("slide",a,{handle:this.handles[b],value:c,values:e}),d=this.values(b?0:1),f!==!1&&this.values(b,c,!0))):c!==this.value()&&(f=this._trigger("slide",a,{handle:this.handles[b],value:c}),f!==!1&&this.value(c))},_stop:function(a,b){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("stop",a,c)},_change:function(a,b){if(!this._keySliding&&!this._mouseSliding){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("change",a,c)}},value:function(a){if(arguments.length){this.options.value=this._trimAlignValue(a),this._refreshValue(),this._change(null,0);return}return this._value()},values:function(b,c){var d,e,f;if(arguments.length>1){this.options.values[b]=this._trimAlignValue(c),this._refreshValue(),this._change(null,b);return}if(!arguments.length)return this._values();if(!a.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(b):this.value();d=this.options.values,e=arguments[0];for(f=0;f<d.length;f+=1)d[f]=this._trimAlignValue(e[f]),this._change(null,f);this._refreshValue()},_setOption:function(b,c){var d,e=0;a.isArray(this.options.values)&&(e=this.options.values.length),a.Widget.prototype._setOption.apply(this,arguments);switch(b){case"disabled":c?(this.handles.filter(".ui-state-focus").blur(),this.handles.removeClass("ui-state-hover"),this.handles.propAttr("disabled",!0),this.element.addClass("ui-disabled")):(this.handles.propAttr("disabled",!1),this.element.removeClass("ui-disabled"));break;case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":this._animateOff=!0,this._refreshValue();for(d=0;d<e;d+=1)this._change(null,d);this._animateOff=!1}},_value:function(){var a=this.options.value;return a=this._trimAlignValue(a),a},_values:function(a){var b,c,d;if(arguments.length)return b=this.options.values[a],b=this._trimAlignValue(b),b;c=this.options.values.slice();for(d=0;d<c.length;d+=1)c[d]=this._trimAlignValue(c[d]);return c},_trimAlignValue:function(a){if(a<=this._valueMin())return this._valueMin();if(a>=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b,d=a-c;return Math.abs(c)*2>=b&&(d+=c>0?b:-b),parseFloat(d.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var b=this.options.range,c=this.options,d=this,e=this._animateOff?!1:c.animate,f,g={},h,i,j,k;this.options.values&&this.options.values.length?this.handles.each(function(b,i){f=(d.values(b)-d._valueMin())/(d._valueMax()-d._valueMin())*100,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",a(this).stop(1,1)[e?"animate":"css"](g,c.animate),d.options.range===!0&&(d.orientation==="horizontal"?(b===0&&d.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({width:f-h+"%"},{queue:!1,duration:c.animate})):(b===0&&d.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({height:f-h+"%"},{queue:!1,duration:c.animate}))),h=f}):(i=this.value(),j=this._valueMin(),k=this._valueMax(),f=k!==j?(i-j)/(k-j)*100:0,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",this.handle.stop(1,1)[e?"animate":"css"](g,c.animate),b==="min"&&this.orientation==="horizontal"&&this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},c.animate),b==="max"&&this.orientation==="horizontal"&&this.range[e?"animate":"css"]({width:100-f+"%"},{queue:!1,duration:c.animate}),b==="min"&&this.orientation==="vertical"&&this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},c.animate),b==="max"&&this.orientation==="vertical"&&this.range[e?"animate":"css"]({height:100-f+"%"},{queue:!1,duration:c.animate}))}}),a.extend(a.ui.slider,{version:"1.8.23"})})(jQuery);; \ No newline at end of file diff --git a/record-and-playback/presentation_export/playback/presentation_export/lib/jquery-ui.min.js b/record-and-playback/presentation_export/playback/presentation_export/lib/jquery-ui.min.js new file mode 100644 index 0000000000000000000000000000000000000000..62acf7c16cd63d33d60900e62c5edfb754ac25c6 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/lib/jquery-ui.min.js @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.8.23 - 2012-08-15 +* https://github.com/jquery/jquery-ui +* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.resizable.js, jquery.ui.selectable.js, jquery.ui.sortable.js, jquery.effects.core.js, jquery.effects.blind.js, jquery.effects.bounce.js, jquery.effects.clip.js, jquery.effects.drop.js, jquery.effects.explode.js, jquery.effects.fade.js, jquery.effects.fold.js, jquery.effects.highlight.js, jquery.effects.pulsate.js, jquery.effects.scale.js, jquery.effects.shake.js, jquery.effects.slide.js, jquery.effects.transfer.js, jquery.ui.accordion.js, jquery.ui.autocomplete.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.position.js, jquery.ui.progressbar.js, jquery.ui.slider.js, jquery.ui.tabs.js +* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ +(function(a,b){function c(b,c){var e=b.nodeName.toLowerCase();if("area"===e){var f=b.parentNode,g=f.name,h;return!b.href||!g||f.nodeName.toLowerCase()!=="map"?!1:(h=a("img[usemap=#"+g+"]")[0],!!h&&d(h))}return(/input|select|textarea|button|object/.test(e)?!b.disabled:"a"==e?b.href||c:c)&&d(b)}function d(b){return!a(b).parents().andSelf().filter(function(){return a.curCSS(this,"visibility")==="hidden"||a.expr.filters.hidden(this)}).length}a.ui=a.ui||{};if(a.ui.version)return;a.extend(a.ui,{version:"1.8.23",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}}),a.fn.extend({propAttr:a.fn.prop||a.fn.attr,_focus:a.fn.focus,focus:function(b,c){return typeof b=="number"?this.each(function(){var d=this;setTimeout(function(){a(d).focus(),c&&c.call(d)},b)}):this._focus.apply(this,arguments)},scrollParent:function(){var b;return a.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?b=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(a.curCSS(this,"position",1))&&/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0):b=this.parents().filter(function(){return/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0),/fixed/.test(this.css("position"))||!b.length?a(document):b},zIndex:function(c){if(c!==b)return this.css("zIndex",c);if(this.length){var d=a(this[0]),e,f;while(d.length&&d[0]!==document){e=d.css("position");if(e==="absolute"||e==="relative"||e==="fixed"){f=parseInt(d.css("zIndex"),10);if(!isNaN(f)&&f!==0)return f}d=d.parent()}}return 0},disableSelection:function(){return this.bind((a.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),a("<a>").outerWidth(1).jquery||a.each(["Width","Height"],function(c,d){function h(b,c,d,f){return a.each(e,function(){c-=parseFloat(a.curCSS(b,"padding"+this,!0))||0,d&&(c-=parseFloat(a.curCSS(b,"border"+this+"Width",!0))||0),f&&(c-=parseFloat(a.curCSS(b,"margin"+this,!0))||0)}),c}var e=d==="Width"?["Left","Right"]:["Top","Bottom"],f=d.toLowerCase(),g={innerWidth:a.fn.innerWidth,innerHeight:a.fn.innerHeight,outerWidth:a.fn.outerWidth,outerHeight:a.fn.outerHeight};a.fn["inner"+d]=function(c){return c===b?g["inner"+d].call(this):this.each(function(){a(this).css(f,h(this,c)+"px")})},a.fn["outer"+d]=function(b,c){return typeof b!="number"?g["outer"+d].call(this,b):this.each(function(){a(this).css(f,h(this,b,!0,c)+"px")})}}),a.extend(a.expr[":"],{data:a.expr.createPseudo?a.expr.createPseudo(function(b){return function(c){return!!a.data(c,b)}}):function(b,c,d){return!!a.data(b,d[3])},focusable:function(b){return c(b,!isNaN(a.attr(b,"tabindex")))},tabbable:function(b){var d=a.attr(b,"tabindex"),e=isNaN(d);return(e||d>=0)&&c(b,!e)}}),a(function(){var b=document.body,c=b.appendChild(c=document.createElement("div"));c.offsetHeight,a.extend(c.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),a.support.minHeight=c.offsetHeight===100,a.support.selectstart="onselectstart"in c,b.removeChild(c).style.display="none"}),a.curCSS||(a.curCSS=a.css),a.extend(a.ui,{plugin:{add:function(b,c,d){var e=a.ui[b].prototype;for(var f in d)e.plugins[f]=e.plugins[f]||[],e.plugins[f].push([c,d[f]])},call:function(a,b,c){var d=a.plugins[b];if(!d||!a.element[0].parentNode)return;for(var e=0;e<d.length;e++)a.options[d[e][0]]&&d[e][1].apply(a.element,c)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(b,c){if(a(b).css("overflow")==="hidden")return!1;var d=c&&c==="left"?"scrollLeft":"scrollTop",e=!1;return b[d]>0?!0:(b[d]=1,e=b[d]>0,b[d]=0,e)},isOverAxis:function(a,b,c){return a>b&&a<b+c},isOver:function(b,c,d,e,f,g){return a.ui.isOverAxis(b,d,f)&&a.ui.isOverAxis(c,e,g)}})})(jQuery),function(a,b){if(a.cleanData){var c=a.cleanData;a.cleanData=function(b){for(var d=0,e;(e=b[d])!=null;d++)try{a(e).triggerHandler("remove")}catch(f){}c(b)}}else{var d=a.fn.remove;a.fn.remove=function(b,c){return this.each(function(){return c||(!b||a.filter(b,[this]).length)&&a("*",this).add([this]).each(function(){try{a(this).triggerHandler("remove")}catch(b){}}),d.call(a(this),b,c)})}}a.widget=function(b,c,d){var e=b.split(".")[0],f;b=b.split(".")[1],f=e+"-"+b,d||(d=c,c=a.Widget),a.expr[":"][f]=function(c){return!!a.data(c,b)},a[e]=a[e]||{},a[e][b]=function(a,b){arguments.length&&this._createWidget(a,b)};var g=new c;g.options=a.extend(!0,{},g.options),a[e][b].prototype=a.extend(!0,g,{namespace:e,widgetName:b,widgetEventPrefix:a[e][b].prototype.widgetEventPrefix||b,widgetBaseClass:f},d),a.widget.bridge(b,a[e][b])},a.widget.bridge=function(c,d){a.fn[c]=function(e){var f=typeof e=="string",g=Array.prototype.slice.call(arguments,1),h=this;return e=!f&&g.length?a.extend.apply(null,[!0,e].concat(g)):e,f&&e.charAt(0)==="_"?h:(f?this.each(function(){var d=a.data(this,c),f=d&&a.isFunction(d[e])?d[e].apply(d,g):d;if(f!==d&&f!==b)return h=f,!1}):this.each(function(){var b=a.data(this,c);b?b.option(e||{})._init():a.data(this,c,new d(e,this))}),h)}},a.Widget=function(a,b){arguments.length&&this._createWidget(a,b)},a.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:!1},_createWidget:function(b,c){a.data(c,this.widgetName,this),this.element=a(c),this.options=a.extend(!0,{},this.options,this._getCreateOptions(),b);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()}),this._create(),this._trigger("create"),this._init()},_getCreateOptions:function(){return a.metadata&&a.metadata.get(this.element[0])[this.widgetName]},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName),this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled "+"ui-state-disabled")},widget:function(){return this.element},option:function(c,d){var e=c;if(arguments.length===0)return a.extend({},this.options);if(typeof c=="string"){if(d===b)return this.options[c];e={},e[c]=d}return this._setOptions(e),this},_setOptions:function(b){var c=this;return a.each(b,function(a,b){c._setOption(a,b)}),this},_setOption:function(a,b){return this.options[a]=b,a==="disabled"&&this.widget()[b?"addClass":"removeClass"](this.widgetBaseClass+"-disabled"+" "+"ui-state-disabled").attr("aria-disabled",b),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_trigger:function(b,c,d){var e,f,g=this.options[b];d=d||{},c=a.Event(c),c.type=(b===this.widgetEventPrefix?b:this.widgetEventPrefix+b).toLowerCase(),c.target=this.element[0],f=c.originalEvent;if(f)for(e in f)e in c||(c[e]=f[e]);return this.element.trigger(c,d),!(a.isFunction(g)&&g.call(this.element[0],c,d)===!1||c.isDefaultPrevented())}}}(jQuery),function(a,b){var c=!1;a(document).mouseup(function(a){c=!1}),a.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var b=this;this.element.bind("mousedown."+this.widgetName,function(a){return b._mouseDown(a)}).bind("click."+this.widgetName,function(c){if(!0===a.data(c.target,b.widgetName+".preventClickEvent"))return a.removeData(c.target,b.widgetName+".preventClickEvent"),c.stopImmediatePropagation(),!1}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(b){if(c)return;this._mouseStarted&&this._mouseUp(b),this._mouseDownEvent=b;var d=this,e=b.which==1,f=typeof this.options.cancel=="string"&&b.target.nodeName?a(b.target).closest(this.options.cancel).length:!1;if(!e||f||!this._mouseCapture(b))return!0;this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){d.mouseDelayMet=!0},this.options.delay));if(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)){this._mouseStarted=this._mouseStart(b)!==!1;if(!this._mouseStarted)return b.preventDefault(),!0}return!0===a.data(b.target,this.widgetName+".preventClickEvent")&&a.removeData(b.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(a){return d._mouseMove(a)},this._mouseUpDelegate=function(a){return d._mouseUp(a)},a(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),b.preventDefault(),c=!0,!0},_mouseMove:function(b){return!a.browser.msie||document.documentMode>=9||!!b.button?this._mouseStarted?(this._mouseDrag(b),b.preventDefault()):(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,b)!==!1,this._mouseStarted?this._mouseDrag(b):this._mouseUp(b)),!this._mouseStarted):this._mouseUp(b)},_mouseUp:function(b){return a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,b.target==this._mouseDownEvent.target&&a.data(b.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(b)),!1},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(a){return this.mouseDelayMet},_mouseStart:function(a){},_mouseDrag:function(a){},_mouseStop:function(a){},_mouseCapture:function(a){return!0}})}(jQuery),function(a,b){a.widget("ui.draggable",a.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1},_create:function(){this.options.helper=="original"&&!/^(?:r|a|f)/.test(this.element.css("position"))&&(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},destroy:function(){if(!this.element.data("draggable"))return;return this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy(),this},_mouseCapture:function(b){var c=this.options;return this.helper||c.disabled||a(b.target).is(".ui-resizable-handle")?!1:(this.handle=this._getHandle(b),this.handle?(c.iframeFix&&a(c.iframeFix===!0?"iframe":c.iframeFix).each(function(){a('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(a(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(b){var c=this.options;return this.helper=this._createHelper(b),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),a.ui.ddmanager&&(a.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,c.cursorAt&&this._adjustOffsetFromHelper(c.cursorAt),c.containment&&this._setContainment(),this._trigger("start",b)===!1?(this._clear(),!1):(this._cacheHelperProportions(),a.ui.ddmanager&&!c.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this._mouseDrag(b,!0),a.ui.ddmanager&&a.ui.ddmanager.dragStart(this,b),!0)},_mouseDrag:function(b,c){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute");if(!c){var d=this._uiHash();if(this._trigger("drag",b,d)===!1)return this._mouseUp({}),!1;this.position=d.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";return a.ui.ddmanager&&a.ui.ddmanager.drag(this,b),!1},_mouseStop:function(b){var c=!1;a.ui.ddmanager&&!this.options.dropBehaviour&&(c=a.ui.ddmanager.drop(this,b)),this.dropped&&(c=this.dropped,this.dropped=!1);var d=this.element[0],e=!1;while(d&&(d=d.parentNode))d==document&&(e=!0);if(!e&&this.options.helper==="original")return!1;if(this.options.revert=="invalid"&&!c||this.options.revert=="valid"&&c||this.options.revert===!0||a.isFunction(this.options.revert)&&this.options.revert.call(this.element,c)){var f=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){f._trigger("stop",b)!==!1&&f._clear()})}else this._trigger("stop",b)!==!1&&this._clear();return!1},_mouseUp:function(b){return this.options.iframeFix===!0&&a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),a.ui.ddmanager&&a.ui.ddmanager.dragStop(this,b),a.ui.mouse.prototype._mouseUp.call(this,b)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?!0:!1;return a(this.options.handle,this.element).find("*").andSelf().each(function(){this==b.target&&(c=!0)}),c},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b])):c.helper=="clone"?this.element.clone().removeAttr("id"):this.element;return d.parents("body").length||d.appendTo(c.appendTo=="parent"?this.element[0].parentNode:c.appendTo),d[0]!=this.element[0]&&!/(fixed|absolute)/.test(d.css("position"))&&d.css("position","absolute"),d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[b.containment=="document"?0:a(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,b.containment=="document"?0:a(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,(b.containment=="document"?0:a(window).scrollLeft())+a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(b.containment=="document"?0:a(window).scrollTop())+(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)&&b.containment.constructor!=Array){var c=a(b.containment),d=c[0];if(!d)return;var e=c.offset(),f=a(d).css("overflow")!="hidden";this.containment=[(parseInt(a(d).css("borderLeftWidth"),10)||0)+(parseInt(a(d).css("paddingLeft"),10)||0),(parseInt(a(d).css("borderTopWidth"),10)||0)+(parseInt(a(d).css("paddingTop"),10)||0),(f?Math.max(d.scrollWidth,d.offsetWidth):d.offsetWidth)-(parseInt(a(d).css("borderLeftWidth"),10)||0)-(parseInt(a(d).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(f?Math.max(d.scrollHeight,d.offsetHeight):d.offsetHeight)-(parseInt(a(d).css("borderTopWidth"),10)||0)-(parseInt(a(d).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=c}else b.containment.constructor==Array&&(this.containment=b.containment)},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName),f=b.pageX,g=b.pageY;if(this.originalPosition){var h;if(this.containment){if(this.relative_container){var i=this.relative_container.offset();h=[this.containment[0]+i.left,this.containment[1]+i.top,this.containment[2]+i.left,this.containment[3]+i.top]}else h=this.containment;b.pageX-this.offset.click.left<h[0]&&(f=h[0]+this.offset.click.left),b.pageY-this.offset.click.top<h[1]&&(g=h[1]+this.offset.click.top),b.pageX-this.offset.click.left>h[2]&&(f=h[2]+this.offset.click.left),b.pageY-this.offset.click.top>h[3]&&(g=h[3]+this.offset.click.top)}if(c.grid){var j=c.grid[1]?this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1]:this.originalPageY;g=h?j-this.offset.click.top<h[1]||j-this.offset.click.top>h[3]?j-this.offset.click.top<h[1]?j+c.grid[1]:j-c.grid[1]:j:j;var k=c.grid[0]?this.originalPageX+Math.round((f-this.originalPageX)/c.grid[0])*c.grid[0]:this.originalPageX;f=h?k-this.offset.click.left<h[0]||k-this.offset.click.left>h[2]?k-this.offset.click.left<h[0]?k+c.grid[0]:k-c.grid[0]:k:k}}return{top:g-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:d.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:d.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(b,c,d){return d=d||this._uiHash(),a.ui.plugin.call(this,b,[c,d]),b=="drag"&&(this.positionAbs=this._convertPositionTo("absolute")),a.Widget.prototype._trigger.call(this,b,c,d)},plugins:{},_uiHash:function(a){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),a.extend(a.ui.draggable,{version:"1.8.23"}),a.ui.plugin.add("draggable","connectToSortable",{start:function(b,c){var d=a(this).data("draggable"),e=d.options,f=a.extend({},c,{item:d.element});d.sortables=[],a(e.connectToSortable).each(function(){var c=a.data(this,"sortable");c&&!c.options.disabled&&(d.sortables.push({instance:c,shouldRevert:c.options.revert}),c.refreshPositions(),c._trigger("activate",b,f))})},stop:function(b,c){var d=a(this).data("draggable"),e=a.extend({},c,{item:d.element});a.each(d.sortables,function(){this.instance.isOver?(this.instance.isOver=0,d.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=!0),this.instance._mouseStop(b),this.instance.options.helper=this.instance.options._helper,d.options.helper=="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",b,e))})},drag:function(b,c){var d=a(this).data("draggable"),e=this,f=function(b){var c=this.offset.click.top,d=this.offset.click.left,e=this.positionAbs.top,f=this.positionAbs.left,g=b.height,h=b.width,i=b.top,j=b.left;return a.ui.isOver(e+c,f+d,i,j,g,h)};a.each(d.sortables,function(f){this.instance.positionAbs=d.positionAbs,this.instance.helperProportions=d.helperProportions,this.instance.offset.click=d.offset.click,this.instance._intersectsWith(this.instance.containerCache)?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=a(e).clone().removeAttr("id").appendTo(this.instance.element).data("sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return c.helper[0]},b.target=this.instance.currentItem[0],this.instance._mouseCapture(b,!0),this.instance._mouseStart(b,!0,!0),this.instance.offset.click.top=d.offset.click.top,this.instance.offset.click.left=d.offset.click.left,this.instance.offset.parent.left-=d.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=d.offset.parent.top-this.instance.offset.parent.top,d._trigger("toSortable",b),d.dropped=this.instance.element,d.currentItem=d.element,this.instance.fromOutside=d),this.instance.currentItem&&this.instance._mouseDrag(b)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",b,this.instance._uiHash(this.instance)),this.instance._mouseStop(b,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),d._trigger("fromSortable",b),d.dropped=!1)})}}),a.ui.plugin.add("draggable","cursor",{start:function(b,c){var d=a("body"),e=a(this).data("draggable").options;d.css("cursor")&&(e._cursor=d.css("cursor")),d.css("cursor",e.cursor)},stop:function(b,c){var d=a(this).data("draggable").options;d._cursor&&a("body").css("cursor",d._cursor)}}),a.ui.plugin.add("draggable","opacity",{start:function(b,c){var d=a(c.helper),e=a(this).data("draggable").options;d.css("opacity")&&(e._opacity=d.css("opacity")),d.css("opacity",e.opacity)},stop:function(b,c){var d=a(this).data("draggable").options;d._opacity&&a(c.helper).css("opacity",d._opacity)}}),a.ui.plugin.add("draggable","scroll",{start:function(b,c){var d=a(this).data("draggable");d.scrollParent[0]!=document&&d.scrollParent[0].tagName!="HTML"&&(d.overflowOffset=d.scrollParent.offset())},drag:function(b,c){var d=a(this).data("draggable"),e=d.options,f=!1;if(d.scrollParent[0]!=document&&d.scrollParent[0].tagName!="HTML"){if(!e.axis||e.axis!="x")d.overflowOffset.top+d.scrollParent[0].offsetHeight-b.pageY<e.scrollSensitivity?d.scrollParent[0].scrollTop=f=d.scrollParent[0].scrollTop+e.scrollSpeed:b.pageY-d.overflowOffset.top<e.scrollSensitivity&&(d.scrollParent[0].scrollTop=f=d.scrollParent[0].scrollTop-e.scrollSpeed);if(!e.axis||e.axis!="y")d.overflowOffset.left+d.scrollParent[0].offsetWidth-b.pageX<e.scrollSensitivity?d.scrollParent[0].scrollLeft=f=d.scrollParent[0].scrollLeft+e.scrollSpeed:b.pageX-d.overflowOffset.left<e.scrollSensitivity&&(d.scrollParent[0].scrollLeft=f=d.scrollParent[0].scrollLeft-e.scrollSpeed)}else{if(!e.axis||e.axis!="x")b.pageY-a(document).scrollTop()<e.scrollSensitivity?f=a(document).scrollTop(a(document).scrollTop()-e.scrollSpeed):a(window).height()-(b.pageY-a(document).scrollTop())<e.scrollSensitivity&&(f=a(document).scrollTop(a(document).scrollTop()+e.scrollSpeed));if(!e.axis||e.axis!="y")b.pageX-a(document).scrollLeft()<e.scrollSensitivity?f=a(document).scrollLeft(a(document).scrollLeft()-e.scrollSpeed):a(window).width()-(b.pageX-a(document).scrollLeft())<e.scrollSensitivity&&(f=a(document).scrollLeft(a(document).scrollLeft()+e.scrollSpeed))}f!==!1&&a.ui.ddmanager&&!e.dropBehaviour&&a.ui.ddmanager.prepareOffsets(d,b)}}),a.ui.plugin.add("draggable","snap",{start:function(b,c){var d=a(this).data("draggable"),e=d.options;d.snapElements=[],a(e.snap.constructor!=String?e.snap.items||":data(draggable)":e.snap).each(function(){var b=a(this),c=b.offset();this!=d.element[0]&&d.snapElements.push({item:this,width:b.outerWidth(),height:b.outerHeight(),top:c.top,left:c.left})})},drag:function(b,c){var d=a(this).data("draggable"),e=d.options,f=e.snapTolerance,g=c.offset.left,h=g+d.helperProportions.width,i=c.offset.top,j=i+d.helperProportions.height;for(var k=d.snapElements.length-1;k>=0;k--){var l=d.snapElements[k].left,m=l+d.snapElements[k].width,n=d.snapElements[k].top,o=n+d.snapElements[k].height;if(!(l-f<g&&g<m+f&&n-f<i&&i<o+f||l-f<g&&g<m+f&&n-f<j&&j<o+f||l-f<h&&h<m+f&&n-f<i&&i<o+f||l-f<h&&h<m+f&&n-f<j&&j<o+f)){d.snapElements[k].snapping&&d.options.snap.release&&d.options.snap.release.call(d.element,b,a.extend(d._uiHash(),{snapItem:d.snapElements[k].item})),d.snapElements[k].snapping=!1;continue}if(e.snapMode!="inner"){var p=Math.abs(n-j)<=f,q=Math.abs(o-i)<=f,r=Math.abs(l-h)<=f,s=Math.abs(m-g)<=f;p&&(c.position.top=d._convertPositionTo("relative",{top:n-d.helperProportions.height,left:0}).top-d.margins.top),q&&(c.position.top=d._convertPositionTo("relative",{top:o,left:0}).top-d.margins.top),r&&(c.position.left=d._convertPositionTo("relative",{top:0,left:l-d.helperProportions.width}).left-d.margins.left),s&&(c.position.left=d._convertPositionTo("relative",{top:0,left:m}).left-d.margins.left)}var t=p||q||r||s;if(e.snapMode!="outer"){var p=Math.abs(n-i)<=f,q=Math.abs(o-j)<=f,r=Math.abs(l-g)<=f,s=Math.abs(m-h)<=f;p&&(c.position.top=d._convertPositionTo("relative",{top:n,left:0}).top-d.margins.top),q&&(c.position.top=d._convertPositionTo("relative",{top:o-d.helperProportions.height,left:0}).top-d.margins.top),r&&(c.position.left=d._convertPositionTo("relative",{top:0,left:l}).left-d.margins.left),s&&(c.position.left=d._convertPositionTo("relative",{top:0,left:m-d.helperProportions.width}).left-d.margins.left)}!d.snapElements[k].snapping&&(p||q||r||s||t)&&d.options.snap.snap&&d.options.snap.snap.call(d.element,b,a.extend(d._uiHash(),{snapItem:d.snapElements[k].item})),d.snapElements[k].snapping=p||q||r||s||t}}}),a.ui.plugin.add("draggable","stack",{start:function(b,c){var d=a(this).data("draggable").options,e=a.makeArray(a(d.stack)).sort(function(b,c){return(parseInt(a(b).css("zIndex"),10)||0)-(parseInt(a(c).css("zIndex"),10)||0)});if(!e.length)return;var f=parseInt(e[0].style.zIndex)||0;a(e).each(function(a){this.style.zIndex=f+a}),this[0].style.zIndex=f+e.length}}),a.ui.plugin.add("draggable","zIndex",{start:function(b,c){var d=a(c.helper),e=a(this).data("draggable").options;d.css("zIndex")&&(e._zIndex=d.css("zIndex")),d.css("zIndex",e.zIndex)},stop:function(b,c){var d=a(this).data("draggable").options;d._zIndex&&a(c.helper).css("zIndex",d._zIndex)}})}(jQuery),function(a,b){a.widget("ui.droppable",{widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect"},_create:function(){var b=this.options,c=b.accept;this.isover=0,this.isout=1,this.accept=a.isFunction(c)?c:function(a){return a.is(c)},this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight},a.ui.ddmanager.droppables[b.scope]=a.ui.ddmanager.droppables[b.scope]||[],a.ui.ddmanager.droppables[b.scope].push(this),b.addClasses&&this.element.addClass("ui-droppable")},destroy:function(){var b=a.ui.ddmanager.droppables[this.options.scope];for(var c=0;c<b.length;c++)b[c]==this&&b.splice(c,1);return this.element.removeClass("ui-droppable ui-droppable-disabled").removeData("droppable").unbind(".droppable"),this},_setOption:function(b,c){b=="accept"&&(this.accept=a.isFunction(c)?c:function(a){return a.is(c)}),a.Widget.prototype._setOption.apply(this,arguments)},_activate:function(b){var c=a.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),c&&this._trigger("activate",b,this.ui(c))},_deactivate:function(b){var c=a.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),c&&this._trigger("deactivate",b,this.ui(c))},_over:function(b){var c=a.ui.ddmanager.current;if(!c||(c.currentItem||c.element)[0]==this.element[0])return;this.accept.call(this.element[0],c.currentItem||c.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",b,this.ui(c)))},_out:function(b){var c=a.ui.ddmanager.current;if(!c||(c.currentItem||c.element)[0]==this.element[0])return;this.accept.call(this.element[0],c.currentItem||c.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",b,this.ui(c)))},_drop:function(b,c){var d=c||a.ui.ddmanager.current;if(!d||(d.currentItem||d.element)[0]==this.element[0])return!1;var e=!1;return this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function(){var b=a.data(this,"droppable");if(b.options.greedy&&!b.options.disabled&&b.options.scope==d.options.scope&&b.accept.call(b.element[0],d.currentItem||d.element)&&a.ui.intersect(d,a.extend(b,{offset:b.element.offset()}),b.options.tolerance))return e=!0,!1}),e?!1:this.accept.call(this.element[0],d.currentItem||d.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",b,this.ui(d)),this.element):!1},ui:function(a){return{draggable:a.currentItem||a.element,helper:a.helper,position:a.position,offset:a.positionAbs}}}),a.extend(a.ui.droppable,{version:"1.8.23"}),a.ui.intersect=function(b,c,d){if(!c.offset)return!1;var e=(b.positionAbs||b.position.absolute).left,f=e+b.helperProportions.width,g=(b.positionAbs||b.position.absolute).top,h=g+b.helperProportions.height,i=c.offset.left,j=i+c.proportions.width,k=c.offset.top,l=k+c.proportions.height;switch(d){case"fit":return i<=e&&f<=j&&k<=g&&h<=l;case"intersect":return i<e+b.helperProportions.width/2&&f-b.helperProportions.width/2<j&&k<g+b.helperProportions.height/2&&h-b.helperProportions.height/2<l;case"pointer":var m=(b.positionAbs||b.position.absolute).left+(b.clickOffset||b.offset.click).left,n=(b.positionAbs||b.position.absolute).top+(b.clickOffset||b.offset.click).top,o=a.ui.isOver(n,m,k,i,c.proportions.height,c.proportions.width);return o;case"touch":return(g>=k&&g<=l||h>=k&&h<=l||g<k&&h>l)&&(e>=i&&e<=j||f>=i&&f<=j||e<i&&f>j);default:return!1}},a.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(b,c){var d=a.ui.ddmanager.droppables[b.options.scope]||[],e=c?c.type:null,f=(b.currentItem||b.element).find(":data(droppable)").andSelf();g:for(var h=0;h<d.length;h++){if(d[h].options.disabled||b&&!d[h].accept.call(d[h].element[0],b.currentItem||b.element))continue;for(var i=0;i<f.length;i++)if(f[i]==d[h].element[0]){d[h].proportions.height=0;continue g}d[h].visible=d[h].element.css("display")!="none";if(!d[h].visible)continue;e=="mousedown"&&d[h]._activate.call(d[h],c),d[h].offset=d[h].element.offset(),d[h].proportions={width:d[h].element[0].offsetWidth,height:d[h].element[0].offsetHeight}}},drop:function(b,c){var d=!1;return a.each(a.ui.ddmanager.droppables[b.options.scope]||[],function(){if(!this.options)return;!this.options.disabled&&this.visible&&a.ui.intersect(b,this,this.options.tolerance)&&(d=this._drop.call(this,c)||d),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],b.currentItem||b.element)&&(this.isout=1,this.isover=0,this._deactivate.call(this,c))}),d},dragStart:function(b,c){b.element.parents(":not(body,html)").bind("scroll.droppable",function(){b.options.refreshPositions||a.ui.ddmanager.prepareOffsets(b,c)})},drag:function(b,c){b.options.refreshPositions&&a.ui.ddmanager.prepareOffsets(b,c),a.each(a.ui.ddmanager.droppables[b.options.scope]||[],function(){if(this.options.disabled||this.greedyChild||!this.visible)return;var d=a.ui.intersect(b,this,this.options.tolerance),e=!d&&this.isover==1?"isout":d&&this.isover==0?"isover":null;if(!e)return;var f;if(this.options.greedy){var g=this.element.parents(":data(droppable):eq(0)");g.length&&(f=a.data(g[0],"droppable"),f.greedyChild=e=="isover"?1:0)}f&&e=="isover"&&(f.isover=0,f.isout=1,f._out.call(f,c)),this[e]=1,this[e=="isout"?"isover":"isout"]=0,this[e=="isover"?"_over":"_out"].call(this,c),f&&e=="isout"&&(f.isout=0,f.isover=1,f._over.call(f,c))})},dragStop:function(b,c){b.element.parents(":not(body,html)").unbind("scroll.droppable"),b.options.refreshPositions||a.ui.ddmanager.prepareOffsets(b,c)}}}(jQuery),function(a,b){a.widget("ui.resizable",a.ui.mouse,{widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1e3},_create:function(){var b=this,c=this.options;this.element.addClass("ui-resizable"),a.extend(this,{_aspectRatio:!!c.aspectRatio,aspectRatio:c.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:c.helper||c.ghost||c.animate?c.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(a('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("resizable",this.element.data("resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=c.handles||(a(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se");if(this.handles.constructor==String){this.handles=="all"&&(this.handles="n,e,s,w,se,sw,ne,nw");var d=this.handles.split(",");this.handles={};for(var e=0;e<d.length;e++){var f=a.trim(d[e]),g="ui-resizable-"+f,h=a('<div class="ui-resizable-handle '+g+'"></div>');h.css({zIndex:c.zIndex}),"se"==f&&h.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[f]=".ui-resizable-"+f,this.element.append(h)}}this._renderAxis=function(b){b=b||this.element;for(var c in this.handles){this.handles[c].constructor==String&&(this.handles[c]=a(this.handles[c],this.element).show());if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var d=a(this.handles[c],this.element),e=0;e=/sw|ne|nw|se|n|s/.test(c)?d.outerHeight():d.outerWidth();var f=["padding",/ne|nw|n/.test(c)?"Top":/se|sw|s/.test(c)?"Bottom":/^e$/.test(c)?"Right":"Left"].join("");b.css(f,e),this._proportionallyResize()}if(!a(this.handles[c]).length)continue}},this._renderAxis(this.element),this._handles=a(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){if(!b.resizing){if(this.className)var a=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=a&&a[1]?a[1]:"se"}}),c.autoHide&&(this._handles.hide(),a(this.element).addClass("ui-resizable-autohide").hover(function(){if(c.disabled)return;a(this).removeClass("ui-resizable-autohide"),b._handles.show()},function(){if(c.disabled)return;b.resizing||(a(this).addClass("ui-resizable-autohide"),b._handles.hide())})),this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(b){a(b).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){b(this.element);var c=this.element;c.after(this.originalElement.css({position:c.css("position"),width:c.outerWidth(),height:c.outerHeight(),top:c.css("top"),left:c.css("left")})).remove()}return this.originalElement.css("resize",this.originalResizeStyle),b(this.originalElement),this},_mouseCapture:function(b){var c=!1;for(var d in this.handles)a(this.handles[d])[0]==b.target&&(c=!0);return!this.options.disabled&&c},_mouseStart:function(b){var d=this.options,e=this.element.position(),f=this.element;this.resizing=!0,this.documentScroll={top:a(document).scrollTop(),left:a(document).scrollLeft()},(f.is(".ui-draggable")||/absolute/.test(f.css("position")))&&f.css({position:"absolute",top:e.top,left:e.left}),this._renderProxy();var g=c(this.helper.css("left")),h=c(this.helper.css("top"));d.containment&&(g+=a(d.containment).scrollLeft()||0,h+=a(d.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:g,top:h},this.size=this._helper?{width:f.outerWidth(),height:f.outerHeight()}:{width:f.width(),height:f.height()},this.originalSize=this._helper?{width:f.outerWidth(),height:f.outerHeight()}:{width:f.width(),height:f.height()},this.originalPosition={left:g,top:h},this.sizeDiff={width:f.outerWidth()-f.width(),height:f.outerHeight()-f.height()},this.originalMousePosition={left:b.pageX,top:b.pageY},this.aspectRatio=typeof d.aspectRatio=="number"?d.aspectRatio:this.originalSize.width/this.originalSize.height||1;var i=a(".ui-resizable-"+this.axis).css("cursor");return a("body").css("cursor",i=="auto"?this.axis+"-resize":i),f.addClass("ui-resizable-resizing"),this._propagate("start",b),!0},_mouseDrag:function(b){var c=this.helper,d=this.options,e={},f=this,g=this.originalMousePosition,h=this.axis,i=b.pageX-g.left||0,j=b.pageY-g.top||0,k=this._change[h];if(!k)return!1;var l=k.apply(this,[b,i,j]),m=a.browser.msie&&a.browser.version<7,n=this.sizeDiff;this._updateVirtualBoundaries(b.shiftKey);if(this._aspectRatio||b.shiftKey)l=this._updateRatio(l,b);return l=this._respectSize(l,b),this._propagate("resize",b),c.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"}),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),this._updateCache(l),this._trigger("resize",b,this.ui()),!1},_mouseStop:function(b){this.resizing=!1;var c=this.options,d=this;if(this._helper){var e=this._proportionallyResizeElements,f=e.length&&/textarea/i.test(e[0].nodeName),g=f&&a.ui.hasScroll(e[0],"left")?0:d.sizeDiff.height,h=f?0:d.sizeDiff.width,i={width:d.helper.width()-h,height:d.helper.height()-g},j=parseInt(d.element.css("left"),10)+(d.position.left-d.originalPosition.left)||null,k=parseInt(d.element.css("top"),10)+(d.position.top-d.originalPosition.top)||null;c.animate||this.element.css(a.extend(i,{top:k,left:j})),d.helper.height(d.size.height),d.helper.width(d.size.width),this._helper&&!c.animate&&this._proportionallyResize()}return a("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",b),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(a){var b=this.options,c,e,f,g,h;h={minWidth:d(b.minWidth)?b.minWidth:0,maxWidth:d(b.maxWidth)?b.maxWidth:Infinity,minHeight:d(b.minHeight)?b.minHeight:0,maxHeight:d(b.maxHeight)?b.maxHeight:Infinity};if(this._aspectRatio||a)c=h.minHeight*this.aspectRatio,f=h.minWidth/this.aspectRatio,e=h.maxHeight*this.aspectRatio,g=h.maxWidth/this.aspectRatio,c>h.minWidth&&(h.minWidth=c),f>h.minHeight&&(h.minHeight=f),e<h.maxWidth&&(h.maxWidth=e),g<h.maxHeight&&(h.maxHeight=g);this._vBoundaries=h},_updateCache:function(a){var b=this.options;this.offset=this.helper.offset(),d(a.left)&&(this.position.left=a.left),d(a.top)&&(this.position.top=a.top),d(a.height)&&(this.size.height=a.height),d(a.width)&&(this.size.width=a.width)},_updateRatio:function(a,b){var c=this.options,e=this.position,f=this.size,g=this.axis;return d(a.height)?a.width=a.height*this.aspectRatio:d(a.width)&&(a.height=a.width/this.aspectRatio),g=="sw"&&(a.left=e.left+(f.width-a.width),a.top=null),g=="nw"&&(a.top=e.top+(f.height-a.height),a.left=e.left+(f.width-a.width)),a},_respectSize:function(a,b){var c=this.helper,e=this._vBoundaries,f=this._aspectRatio||b.shiftKey,g=this.axis,h=d(a.width)&&e.maxWidth&&e.maxWidth<a.width,i=d(a.height)&&e.maxHeight&&e.maxHeight<a.height,j=d(a.width)&&e.minWidth&&e.minWidth>a.width,k=d(a.height)&&e.minHeight&&e.minHeight>a.height;j&&(a.width=e.minWidth),k&&(a.height=e.minHeight),h&&(a.width=e.maxWidth),i&&(a.height=e.maxHeight);var l=this.originalPosition.left+this.originalSize.width,m=this.position.top+this.size.height,n=/sw|nw|w/.test(g),o=/nw|ne|n/.test(g);j&&n&&(a.left=l-e.minWidth),h&&n&&(a.left=l-e.maxWidth),k&&o&&(a.top=m-e.minHeight),i&&o&&(a.top=m-e.maxHeight);var p=!a.width&&!a.height;return p&&!a.left&&a.top?a.top=null:p&&!a.top&&a.left&&(a.left=null),a},_proportionallyResize:function(){var b=this.options;if(!this._proportionallyResizeElements.length)return;var c=this.helper||this.element;for(var d=0;d<this._proportionallyResizeElements.length;d++){var e=this._proportionallyResizeElements[d];if(!this.borderDif){var f=[e.css("borderTopWidth"),e.css("borderRightWidth"),e.css("borderBottomWidth"),e.css("borderLeftWidth")],g=[e.css("paddingTop"),e.css("paddingRight"),e.css("paddingBottom"),e.css("paddingLeft")];this.borderDif=a.map(f,function(a,b){var c=parseInt(a,10)||0,d=parseInt(g[b],10)||0;return c+d})}if(!a.browser.msie||!a(c).is(":hidden")&&!a(c).parents(":hidden").length)e.css({height:c.height()-this.borderDif[0]-this.borderDif[2]||0,width:c.width()-this.borderDif[1]-this.borderDif[3]||0});else continue}},_renderProxy:function(){var b=this.element,c=this.options;this.elementOffset=b.offset();if(this._helper){this.helper=this.helper||a('<div style="overflow:hidden;"></div>');var d=a.browser.msie&&a.browser.version<7,e=d?1:0,f=d?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+f,height:this.element.outerHeight()+f,position:"absolute",left:this.elementOffset.left-e+"px",top:this.elementOffset.top-e+"px",zIndex:++c.zIndex}),this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(a,b,c){return{width:this.originalSize.width+b}},w:function(a,b,c){var d=this.options,e=this.originalSize,f=this.originalPosition;return{left:f.left+b,width:e.width-b}},n:function(a,b,c){var d=this.options,e=this.originalSize,f=this.originalPosition;return{top:f.top+c,height:e.height-c}},s:function(a,b,c){return{height:this.originalSize.height+c}},se:function(b,c,d){return a.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,c,d]))},sw:function(b,c,d){return a.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,c,d]))},ne:function(b,c,d){return a.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[b,c,d]))},nw:function(b,c,d){return a.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,c,d]))}},_propagate:function(b,c){a.ui.plugin.call(this,b,[c,this.ui()]),b!="resize"&&this._trigger(b,c,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),a.extend(a.ui.resizable,{version:"1.8.23"}),a.ui.plugin.add("resizable","alsoResize",{start:function(b,c){var d=a(this).data("resizable"),e=d.options,f=function(b){a(b).each(function(){var b=a(this);b.data("resizable-alsoresize",{width:parseInt(b.width(),10),height:parseInt(b.height(),10),left:parseInt(b.css("left"),10),top:parseInt(b.css("top"),10)})})};typeof e.alsoResize=="object"&&!e.alsoResize.parentNode?e.alsoResize.length?(e.alsoResize=e.alsoResize[0],f(e.alsoResize)):a.each(e.alsoResize,function(a){f(a)}):f(e.alsoResize)},resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.originalSize,g=d.originalPosition,h={height:d.size.height-f.height||0,width:d.size.width-f.width||0,top:d.position.top-g.top||0,left:d.position.left-g.left||0},i=function(b,d){a(b).each(function(){var b=a(this),e=a(this).data("resizable-alsoresize"),f={},g=d&&d.length?d:b.parents(c.originalElement[0]).length?["width","height"]:["width","height","top","left"];a.each(g,function(a,b){var c=(e[b]||0)+(h[b]||0);c&&c>=0&&(f[b]=c||null)}),b.css(f)})};typeof e.alsoResize=="object"&&!e.alsoResize.nodeType?a.each(e.alsoResize,function(a,b){i(a,b)}):i(e.alsoResize)},stop:function(b,c){a(this).removeData("resizable-alsoresize")}}),a.ui.plugin.add("resizable","animate",{stop:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d._proportionallyResizeElements,g=f.length&&/textarea/i.test(f[0].nodeName),h=g&&a.ui.hasScroll(f[0],"left")?0:d.sizeDiff.height,i=g?0:d.sizeDiff.width,j={width:d.size.width-i,height:d.size.height-h},k=parseInt(d.element.css("left"),10)+(d.position.left-d.originalPosition.left)||null,l=parseInt(d.element.css("top"),10)+(d.position.top-d.originalPosition.top)||null;d.element.animate(a.extend(j,l&&k?{top:l,left:k}:{}),{duration:e.animateDuration,easing:e.animateEasing,step:function(){var c={width:parseInt(d.element.css("width"),10),height:parseInt(d.element.css("height"),10),top:parseInt(d.element.css("top"),10),left:parseInt(d.element.css("left"),10)};f&&f.length&&a(f[0]).css({width:c.width,height:c.height}),d._updateCache(c),d._propagate("resize",b)}})}}),a.ui.plugin.add("resizable","containment",{start:function(b,d){var e=a(this).data("resizable"),f=e.options,g=e.element,h=f.containment,i=h instanceof a?h.get(0):/parent/.test(h)?g.parent().get(0):h;if(!i)return;e.containerElement=a(i);if(/document/.test(h)||h==document)e.containerOffset={left:0,top:0},e.containerPosition={left:0,top:0},e.parentData={element:a(document),left:0,top:0,width:a(document).width(),height:a(document).height()||document.body.parentNode.scrollHeight};else{var j=a(i),k=[];a(["Top","Right","Left","Bottom"]).each(function(a,b){k[a]=c(j.css("padding"+b))}),e.containerOffset=j.offset(),e.containerPosition=j.position(),e.containerSize={height:j.innerHeight()-k[3],width:j.innerWidth()-k[1]};var l=e.containerOffset,m=e.containerSize.height,n=e.containerSize.width,o=a.ui.hasScroll(i,"left")?i.scrollWidth:n,p=a.ui.hasScroll(i)?i.scrollHeight:m;e.parentData={element:i,left:l.left,top:l.top,width:o,height:p}}},resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.containerSize,g=d.containerOffset,h=d.size,i=d.position,j=d._aspectRatio||b.shiftKey,k={top:0,left:0},l=d.containerElement;l[0]!=document&&/static/.test(l.css("position"))&&(k=g),i.left<(d._helper?g.left:0)&&(d.size.width=d.size.width+(d._helper?d.position.left-g.left:d.position.left-k.left),j&&(d.size.height=d.size.width/d.aspectRatio),d.position.left=e.helper?g.left:0),i.top<(d._helper?g.top:0)&&(d.size.height=d.size.height+(d._helper?d.position.top-g.top:d.position.top),j&&(d.size.width=d.size.height*d.aspectRatio),d.position.top=d._helper?g.top:0),d.offset.left=d.parentData.left+d.position.left,d.offset.top=d.parentData.top+d.position.top;var m=Math.abs((d._helper?d.offset.left-k.left:d.offset.left-k.left)+d.sizeDiff.width),n=Math.abs((d._helper?d.offset.top-k.top:d.offset.top-g.top)+d.sizeDiff.height),o=d.containerElement.get(0)==d.element.parent().get(0),p=/relative|absolute/.test(d.containerElement.css("position"));o&&p&&(m-=d.parentData.left),m+d.size.width>=d.parentData.width&&(d.size.width=d.parentData.width-m,j&&(d.size.height=d.size.width/d.aspectRatio)),n+d.size.height>=d.parentData.height&&(d.size.height=d.parentData.height-n,j&&(d.size.width=d.size.height*d.aspectRatio))},stop:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.position,g=d.containerOffset,h=d.containerPosition,i=d.containerElement,j=a(d.helper),k=j.offset(),l=j.outerWidth()-d.sizeDiff.width,m=j.outerHeight()-d.sizeDiff.height;d._helper&&!e.animate&&/relative/.test(i.css("position"))&&a(this).css({left:k.left-h.left-g.left,width:l,height:m}),d._helper&&!e.animate&&/static/.test(i.css("position"))&&a(this).css({left:k.left-h.left-g.left,width:l,height:m})}}),a.ui.plugin.add("resizable","ghost",{start:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.size;d.ghost=d.originalElement.clone(),d.ghost.css({opacity:.25,display:"block",position:"relative",height:f.height,width:f.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof e.ghost=="string"?e.ghost:""),d.ghost.appendTo(d.helper)},resize:function(b,c){var d=a(this).data("resizable"),e=d.options;d.ghost&&d.ghost.css({position:"relative",height:d.size.height,width:d.size.width})},stop:function(b,c){var d=a(this).data("resizable"),e=d.options;d.ghost&&d.helper&&d.helper.get(0).removeChild(d.ghost.get(0))}}),a.ui.plugin.add("resizable","grid",{resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.size,g=d.originalSize,h=d.originalPosition,i=d.axis,j=e._aspectRatio||b.shiftKey;e.grid=typeof e.grid=="number"?[e.grid,e.grid]:e.grid;var k=Math.round((f.width-g.width)/(e.grid[0]||1))*(e.grid[0]||1),l=Math.round((f.height-g.height)/(e.grid[1]||1))*(e.grid[1]||1);/^(se|s|e)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l):/^(ne)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l,d.position.top=h.top-l):/^(sw)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l,d.position.left=h.left-k):(d.size.width=g.width+k,d.size.height=g.height+l,d.position.top=h.top-l,d.position.left=h.left-k)}});var c=function(a){return parseInt(a,10)||0},d=function(a){return!isNaN(parseInt(a,10))}}(jQuery),function(a,b){a.widget("ui.selectable",a.ui.mouse,{options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch"},_create:function(){var b=this;this.element.addClass("ui-selectable"),this.dragged=!1;var c;this.refresh=function(){c=a(b.options.filter,b.element[0]),c.addClass("ui-selectee"),c.each(function(){var b=a(this),c=b.offset();a.data(this,"selectable-item",{element:this,$element:b,left:c.left,top:c.top,right:c.left+b.outerWidth(),bottom:c.top+b.outerHeight(),startselected:!1,selected:b.hasClass("ui-selected"),selecting:b.hasClass("ui-selecting"),unselecting:b.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=c.addClass("ui-selectee"),this._mouseInit(),this.helper=a("<div class='ui-selectable-helper'></div>")},destroy:function(){return this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable"),this._mouseDestroy(),this},_mouseStart:function(b){var c=this;this.opos=[b.pageX,b.pageY];if(this.options.disabled)return;var d=this.options;this.selectees=a(d.filter,this.element[0]),this._trigger("start",b),a(d.appendTo).append(this.helper),this.helper.css({left:b.clientX,top:b.clientY,width:0,height:0}),d.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var d=a.data(this,"selectable-item");d.startselected=!0,!b.metaKey&&!b.ctrlKey&&(d.$element.removeClass("ui-selected"),d.selected=!1,d.$element.addClass("ui-unselecting"),d.unselecting=!0,c._trigger("unselecting",b,{unselecting:d.element}))}),a(b.target).parents().andSelf().each(function(){var d=a.data(this,"selectable-item");if(d){var e=!b.metaKey&&!b.ctrlKey||!d.$element.hasClass("ui-selected");return d.$element.removeClass(e?"ui-unselecting":"ui-selected").addClass(e?"ui-selecting":"ui-unselecting"),d.unselecting=!e,d.selecting=e,d.selected=e,e?c._trigger("selecting",b,{selecting:d.element}):c._trigger("unselecting",b,{unselecting:d.element}),!1}})},_mouseDrag:function(b){var c=this;this.dragged=!0;if(this.options.disabled)return;var d=this.options,e=this.opos[0],f=this.opos[1],g=b.pageX,h=b.pageY;if(e>g){var i=g;g=e,e=i}if(f>h){var i=h;h=f,f=i}return this.helper.css({left:e,top:f,width:g-e,height:h-f}),this.selectees.each(function(){var i=a.data(this,"selectable-item");if(!i||i.element==c.element[0])return;var j=!1;d.tolerance=="touch"?j=!(i.left>g||i.right<e||i.top>h||i.bottom<f):d.tolerance=="fit"&&(j=i.left>e&&i.right<g&&i.top>f&&i.bottom<h),j?(i.selected&&(i.$element.removeClass("ui-selected"),i.selected=!1),i.unselecting&&(i.$element.removeClass("ui-unselecting"),i.unselecting=!1),i.selecting||(i.$element.addClass("ui-selecting"),i.selecting=!0,c._trigger("selecting",b,{selecting:i.element}))):(i.selecting&&((b.metaKey||b.ctrlKey)&&i.startselected?(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.$element.addClass("ui-selected"),i.selected=!0):(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.startselected&&(i.$element.addClass("ui-unselecting"),i.unselecting=!0),c._trigger("unselecting",b,{unselecting:i.element}))),i.selected&&!b.metaKey&&!b.ctrlKey&&!i.startselected&&(i.$element.removeClass("ui-selected"),i.selected=!1,i.$element.addClass("ui-unselecting"),i.unselecting=!0,c._trigger("unselecting",b,{unselecting:i.element})))}),!1},_mouseStop:function(b){var c=this;this.dragged=!1;var d=this.options;return a(".ui-unselecting",this.element[0]).each(function(){var d=a.data(this,"selectable-item");d.$element.removeClass("ui-unselecting"),d.unselecting=!1,d.startselected=!1,c._trigger("unselected",b,{unselected:d.element})}),a(".ui-selecting",this.element[0]).each(function(){var d=a.data(this,"selectable-item");d.$element.removeClass("ui-selecting").addClass("ui-selected"),d.selecting=!1,d.selected=!0,d.startselected=!0,c._trigger("selected",b,{selected:d.element})}),this._trigger("stop",b),this.helper.remove(),!1}}),a.extend(a.ui.selectable,{version:"1.8.23"})}(jQuery),function(a,b){a.widget("ui.sortable",a.ui.mouse,{widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3},_create:function(){var a=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},destroy:function(){a.Widget.prototype.destroy.call(this),this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var b=this.items.length-1;b>=0;b--)this.items[b].item.removeData(this.widgetName+"-item");return this},_setOption:function(b,c){b==="disabled"?(this.options[b]=c,this.widget()[c?"addClass":"removeClass"]("ui-sortable-disabled")):a.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(b,c){var d=this;if(this.reverting)return!1;if(this.options.disabled||this.options.type=="static")return!1;this._refreshItems(b);var e=null,f=this,g=a(b.target).parents().each(function(){if(a.data(this,d.widgetName+"-item")==f)return e=a(this),!1});a.data(b.target,d.widgetName+"-item")==f&&(e=a(b.target));if(!e)return!1;if(this.options.handle&&!c){var h=!1;a(this.options.handle,e).find("*").andSelf().each(function(){this==b.target&&(h=!0)});if(!h)return!1}return this.currentItem=e,this._removeCurrentsFromItems(),!0},_mouseStart:function(b,c,d){var e=this.options,f=this;this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(b),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,e.cursorAt&&this._adjustOffsetFromHelper(e.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!=this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),e.containment&&this._setContainment(),e.cursor&&(a("body").css("cursor")&&(this._storedCursor=a("body").css("cursor")),a("body").css("cursor",e.cursor)),e.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",e.opacity)),e.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",e.zIndex)),this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",b,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions();if(!d)for(var g=this.containers.length-1;g>=0;g--)this.containers[g]._trigger("activate",b,f._uiHash(this));return a.ui.ddmanager&&(a.ui.ddmanager.current=this),a.ui.ddmanager&&!e.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(b),!0},_mouseDrag:function(b){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs);if(this.options.scroll){var c=this.options,d=!1;this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-b.pageY<c.scrollSensitivity?this.scrollParent[0].scrollTop=d=this.scrollParent[0].scrollTop+c.scrollSpeed:b.pageY-this.overflowOffset.top<c.scrollSensitivity&&(this.scrollParent[0].scrollTop=d=this.scrollParent[0].scrollTop-c.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-b.pageX<c.scrollSensitivity?this.scrollParent[0].scrollLeft=d=this.scrollParent[0].scrollLeft+c.scrollSpeed:b.pageX-this.overflowOffset.left<c.scrollSensitivity&&(this.scrollParent[0].scrollLeft=d=this.scrollParent[0].scrollLeft-c.scrollSpeed)):(b.pageY-a(document).scrollTop()<c.scrollSensitivity?d=a(document).scrollTop(a(document).scrollTop()-c.scrollSpeed):a(window).height()-(b.pageY-a(document).scrollTop())<c.scrollSensitivity&&(d=a(document).scrollTop(a(document).scrollTop()+c.scrollSpeed)),b.pageX-a(document).scrollLeft()<c.scrollSensitivity?d=a(document).scrollLeft(a(document).scrollLeft()-c.scrollSpeed):a(window).width()-(b.pageX-a(document).scrollLeft())<c.scrollSensitivity&&(d=a(document).scrollLeft(a(document).scrollLeft()+c.scrollSpeed))),d!==!1&&a.ui.ddmanager&&!c.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b)}this.positionAbs=this._convertPositionTo("absolute");if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";for(var e=this.items.length-1;e>=0;e--){var f=this.items[e],g=f.item[0],h=this._intersectsWithPointer(f);if(!h)continue;if(g!=this.currentItem[0]&&this.placeholder[h==1?"next":"prev"]()[0]!=g&&!a.ui.contains(this.placeholder[0],g)&&(this.options.type=="semi-dynamic"?!a.ui.contains(this.element[0],g):!0)){this.direction=h==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(f))this._rearrange(b,f);else break;this._trigger("change",b,this._uiHash());break}}return this._contactContainers(b),a.ui.ddmanager&&a.ui.ddmanager.drag(this,b),this._trigger("sort",b,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(b,c){if(!b)return;a.ui.ddmanager&&!this.options.dropBehaviour&&a.ui.ddmanager.drop(this,b);if(this.options.revert){var d=this,e=d.placeholder.offset();d.reverting=!0,a(this.helper).animate({left:e.left-this.offset.parent.left-d.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:e.top-this.offset.parent.top-d.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){d._clear(b)})}else this._clear(b,c);return!1},cancel:function(){var b=this;if(this.dragging){this._mouseUp({target:null}),this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("deactivate",null,b._uiHash(this)),this.containers[c].containerCache.over&&(this.containers[c]._trigger("out",null,b._uiHash(this)),this.containers[c].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),a.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?a(this.domPosition.prev).after(this.currentItem):a(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(b){var c=this._getItemsAsjQuery(b&&b.connected),d=[];return b=b||{},a(c).each(function(){var c=(a(b.item||this).attr(b.attribute||"id")||"").match(b.expression||/(.+)[-=_](.+)/);c&&d.push((b.key||c[1]+"[]")+"="+(b.key&&b.expression?c[1]:c[2]))}),!d.length&&b.key&&d.push(b.key+"="),d.join("&")},toArray:function(b){var c=this._getItemsAsjQuery(b&&b.connected),d=[];return b=b||{},c.each(function(){d.push(a(b.item||this).attr(b.attribute||"id")||"")}),d},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,d=this.positionAbs.top,e=d+this.helperProportions.height,f=a.left,g=f+a.width,h=a.top,i=h+a.height,j=this.offset.click.top,k=this.offset.click.left,l=d+j>h&&d+j<i&&b+k>f&&b+k<g;return this.options.tolerance=="pointer"||this.options.forcePointerForContainers||this.options.tolerance!="pointer"&&this.helperProportions[this.floating?"width":"height"]>a[this.floating?"width":"height"]?l:f<b+this.helperProportions.width/2&&c-this.helperProportions.width/2<g&&h<d+this.helperProportions.height/2&&e-this.helperProportions.height/2<i},_intersectsWithPointer:function(b){var c=this.options.axis==="x"||a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,b.top,b.height),d=this.options.axis==="y"||a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,b.left,b.width),e=c&&d,f=this._getDragVerticalDirection(),g=this._getDragHorizontalDirection();return e?this.floating?g&&g=="right"||f=="down"?2:1:f&&(f=="down"?2:1):!1},_intersectsWithSides:function(b){var c=a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,b.top+b.height/2,b.height),d=a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,b.left+b.width/2,b.width),e=this._getDragVerticalDirection(),f=this._getDragHorizontalDirection();return this.floating&&f?f=="right"&&d||f=="left"&&!d:e&&(e=="down"&&c||e=="up"&&!c)},_getDragVerticalDirection:function(){var a=this.positionAbs.top-this.lastPositionAbs.top;return a!=0&&(a>0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){return this._refreshItems(a),this.refreshPositions(),this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(b){var c=this,d=[],e=[],f=this._connectWith();if(f&&b)for(var g=f.length-1;g>=0;g--){var h=a(f[g]);for(var i=h.length-1;i>=0;i--){var j=a.data(h[i],this.widgetName);j&&j!=this&&!j.options.disabled&&e.push([a.isFunction(j.options.items)?j.options.items.call(j.element):a(j.options.items,j.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),j])}}e.push([a.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):a(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(var g=e.length-1;g>=0;g--)e[g][0].each(function(){d.push(this)});return a(d)},_removeCurrentsFromItems:function(){var a=this.currentItem.find(":data("+this.widgetName+"-item)");for(var b=0;b<this.items.length;b++)for(var c=0;c<a.length;c++)a[c]==this.items[b].item[0]&&this.items.splice(b,1)},_refreshItems:function(b){this.items=[],this.containers=[this];var c=this.items,d=this,e=[[a.isFunction(this.options.items)?this.options.items.call(this.element[0],b,{item:this.currentItem}):a(this.options.items,this.element),this]],f=this._connectWith();if(f&&this.ready)for(var g=f.length-1;g>=0;g--){var h=a(f[g]);for(var i=h.length-1;i>=0;i--){var j=a.data(h[i],this.widgetName);j&&j!=this&&!j.options.disabled&&(e.push([a.isFunction(j.options.items)?j.options.items.call(j.element[0],b,{item:this.currentItem}):a(j.options.items,j.element),j]),this.containers.push(j))}}for(var g=e.length-1;g>=0;g--){var k=e[g][1],l=e[g][0];for(var i=0,m=l.length;i<m;i++){var n=a(l[i]);n.data(this.widgetName+"-item",k),c.push({item:n,instance:k,width:0,height:0,left:0,top:0})}}},refreshPositions:function(b){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());for(var c=this.items.length-1;c>=0;c--){var d=this.items[c];if(d.instance!=this.currentContainer&&this.currentContainer&&d.item[0]!=this.currentItem[0])continue;var e=this.options.toleranceElement?a(this.options.toleranceElement,d.item):d.item;b||(d.width=e.outerWidth(),d.height=e.outerHeight());var f=e.offset();d.left=f.left,d.top=f.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(var c=this.containers.length-1;c>=0;c--){var f=this.containers[c].element.offset();this.containers[c].containerCache.left=f.left,this.containers[c].containerCache.top=f.top,this.containers[c].containerCache.width=this.containers[c].element.outerWidth(),this.containers[c].containerCache.height=this.containers[c].element.outerHeight()}return this},_createPlaceholder:function(b){var c=b||this,d=c.options;if(!d.placeholder||d.placeholder.constructor==String){var e=d.placeholder;d.placeholder={element:function(){var b=a(document.createElement(c.currentItem[0].nodeName)).addClass(e||c.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];return e||(b.style.visibility="hidden"),b},update:function(a,b){if(e&&!d.forcePlaceholderSize)return;b.height()||b.height(c.currentItem.innerHeight()-parseInt(c.currentItem.css("paddingTop")||0,10)-parseInt(c.currentItem.css("paddingBottom")||0,10)),b.width()||b.width(c.currentItem.innerWidth()-parseInt(c.currentItem.css("paddingLeft")||0,10)-parseInt(c.currentItem.css("paddingRight")||0,10))}}}c.placeholder=a(d.placeholder.element.call(c.element,c.currentItem)),c.currentItem.after(c.placeholder),d.placeholder.update(c,c.placeholder)},_contactContainers:function(b){var c=null,d=null;for(var e=this.containers.length-1;e>=0;e--){if(a.ui.contains(this.currentItem[0],this.containers[e].element[0]))continue;if(this._intersectsWith(this.containers[e].containerCache)){if(c&&a.ui.contains(this.containers[e].element[0],c.element[0]))continue;c=this.containers[e],d=e}else this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",b,this._uiHash(this)),this.containers[e].containerCache.over=0)}if(!c)return;if(this.containers.length===1)this.containers[d]._trigger("over",b,this._uiHash(this)),this.containers[d].containerCache.over=1;else if(this.currentContainer!=this.containers[d]){var f=1e4,g=null,h=this.positionAbs[this.containers[d].floating?"left":"top"];for(var i=this.items.length-1;i>=0;i--){if(!a.ui.contains(this.containers[d].element[0],this.items[i].item[0]))continue;var j=this.containers[d].floating?this.items[i].item.offset().left:this.items[i].item.offset().top;Math.abs(j-h)<f&&(f=Math.abs(j-h),g=this.items[i],this.direction=j-h>0?"down":"up")}if(!g&&!this.options.dropOnEmpty)return;this.currentContainer=this.containers[d],g?this._rearrange(b,g,null,!0):this._rearrange(b,null,this.containers[d].element,!0),this._trigger("change",b,this._uiHash()),this.containers[d]._trigger("change",b,this._uiHash(this)),this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[d]._trigger("over",b,this._uiHash(this)),this.containers[d].containerCache.over=1}},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b,this.currentItem])):c.helper=="clone"?this.currentItem.clone():this.currentItem;return d.parents("body").length||a(c.appendTo!="parent"?c.appendTo:this.currentItem[0].parentNode)[0].appendChild(d[0]),d[0]==this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(d[0].style.width==""||c.forceHelperSize)&&d.width(this.currentItem.width()),(d[0].style.height==""||c.forceHelperSize)&&d.height(this.currentItem.height()),d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.currentItem.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)){var c=a(b.containment)[0],d=a(b.containment).offset(),e=a(c).css("overflow")!="hidden";this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(e?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(e?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName);this.cssPosition=="relative"&&(this.scrollParent[0]==document||this.scrollParent[0]==this.offsetParent[0])&&(this.offset.relative=this._getRelativeOffset());var f=b.pageX,g=b.pageY;if(this.originalPosition){this.containment&&(b.pageX-this.offset.click.left<this.containment[0]&&(f=this.containment[0]+this.offset.click.left),b.pageY-this.offset.click.top<this.containment[1]&&(g=this.containment[1]+this.offset.click.top),b.pageX-this.offset.click.left>this.containment[2]&&(f=this.containment[2]+this.offset.click.left),b.pageY-this.offset.click.top>this.containment[3]&&(g=this.containment[3]+this.offset.click.top));if(c.grid){var h=this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1];g=this.containment?h-this.offset.click.top<this.containment[1]||h-this.offset.click.top>this.containment[3]?h-this.offset.click.top<this.containment[1]?h+c.grid[1]:h-c.grid[1]:h:h;var i=this.originalPageX+Math.round((f-this.originalPageX)/c.grid[0])*c.grid[0];f=this.containment?i-this.offset.click.left<this.containment[0]||i-this.offset.click.left>this.containment[2]?i-this.offset.click.left<this.containment[0]?i+c.grid[0]:i-c.grid[0]:i:i}}return{top:g-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:d.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:d.scrollLeft())}},_rearrange:function(a,b,c,d){c?c[0].appendChild(this.placeholder[0]):b.item[0].parentNode.insertBefore(this.placeholder[0],this.direction=="down"?b.item[0]:b.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var e=this,f=this.counter;window.setTimeout(function(){f==e.counter&&e.refreshPositions(!d)},0)},_clear:function(b,c){this.reverting=!1;var d=[],e=this;!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null;if(this.helper[0]==this.currentItem[0]){for(var f in this._storedCSS)if(this._storedCSS[f]=="auto"||this._storedCSS[f]=="static")this._storedCSS[f]="";this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();this.fromOutside&&!c&&d.push(function(a){this._trigger("receive",a,this._uiHash(this.fromOutside))}),(this.fromOutside||this.domPosition.prev!=this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!=this.currentItem.parent()[0])&&!c&&d.push(function(a){this._trigger("update",a,this._uiHash())});if(!a.ui.contains(this.element[0],this.currentItem[0])){c||d.push(function(a){this._trigger("remove",a,this._uiHash())});for(var f=this.containers.length-1;f>=0;f--)a.ui.contains(this.containers[f].element[0],this.currentItem[0])&&!c&&(d.push(function(a){return function(b){a._trigger("receive",b,this._uiHash(this))}}.call(this,this.containers[f])),d.push(function(a){return function(b){a._trigger("update",b,this._uiHash(this))}}.call(this,this.containers[f])))}for(var f=this.containers.length-1;f>=0;f--)c||d.push(function(a){return function(b){a._trigger("deactivate",b,this._uiHash(this))}}.call(this,this.containers[f])),this.containers[f].containerCache.over&&(d.push(function(a){return function(b){a._trigger("out",b,this._uiHash(this))}}.call(this,this.containers[f])),this.containers[f].containerCache.over=0);this._storedCursor&&a("body").css("cursor",this._storedCursor),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex),this.dragging=!1;if(this.cancelHelperRemoval){if(!c){this._trigger("beforeStop",b,this._uiHash());for(var f=0;f<d.length;f++)d[f].call(this,b);this._trigger("stop",b,this._uiHash())}return this.fromOutside=!1,!1}c||this._trigger("beforeStop",b,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!=this.currentItem[0]&&this.helper.remove(),this.helper=null;if(!c){for(var f=0;f<d.length;f++)d[f].call(this,b);this._trigger("stop",b,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){a.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(b){var c=b||this;return{helper:c.helper,placeholder:c.placeholder||a([]),position:c.position,originalPosition:c.originalPosition,offset:c.positionAbs,item:c.currentItem,sender:b?b.element:null}}}),a.extend(a.ui.sortable,{version:"1.8.23"})}(jQuery),jQuery.effects||function(a,b){function c(b){var c;return b&&b.constructor==Array&&b.length==3?b:(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(b))?[parseInt(c[1],10),parseInt(c[2],10),parseInt(c[3],10)]:(c=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(b))?[parseFloat(c[1])*2.55,parseFloat(c[2])*2.55,parseFloat(c[3])*2.55]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(b))?[parseInt(c[1],16),parseInt(c[2],16),parseInt(c[3],16)]:(c=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(b))?[parseInt(c[1]+c[1],16),parseInt(c[2]+c[2],16),parseInt(c[3]+c[3],16)]:(c=/rgba\(0, 0, 0, 0\)/.exec(b))?e.transparent:e[a.trim(b).toLowerCase()]}function d(b,d){var e;do{e=(a.curCSS||a.css)(b,d);if(e!=""&&e!="transparent"||a.nodeName(b,"body"))break;d="backgroundColor"}while(b=b.parentNode);return c(e)}function h(){var a=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle,b={},c,d;if(a&&a.length&&a[0]&&a[a[0]]){var e=a.length;while(e--)c=a[e],typeof a[c]=="string"&&(d=c.replace(/\-(\w)/g,function(a,b){return b.toUpperCase()}),b[d]=a[c])}else for(c in a)typeof a[c]=="string"&&(b[c]=a[c]);return b}function i(b){var c,d;for(c in b)d=b[c],(d==null||a.isFunction(d)||c in g||/scrollbar/.test(c)||!/color/i.test(c)&&isNaN(parseFloat(d)))&&delete b[c];return b}function j(a,b){var c={_:0},d;for(d in b)a[d]!=b[d]&&(c[d]=b[d]);return c}function k(b,c,d,e){typeof b=="object"&&(e=c,d=null,c=b,b=c.effect),a.isFunction(c)&&(e=c,d=null,c={});if(typeof c=="number"||a.fx.speeds[c])e=d,d=c,c={};return a.isFunction(d)&&(e=d,d=null),c=c||{},d=d||c.duration,d=a.fx.off?0:typeof d=="number"?d:d in a.fx.speeds?a.fx.speeds[d]:a.fx.speeds._default,e=e||c.complete,[b,c,d,e]}function l(b){return!b||typeof b=="number"||a.fx.speeds[b]?!0:typeof b=="string"&&!a.effects[b]?!0:!1}a.effects={},a.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","borderColor","color","outlineColor"],function(b,e){a.fx.step[e]=function(a){a.colorInit||(a.start=d(a.elem,e),a.end=c(a.end),a.colorInit=!0),a.elem.style[e]="rgb("+Math.max(Math.min(parseInt(a.pos*(a.end[0]-a.start[0])+a.start[0],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[1]-a.start[1])+a.start[1],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[2]-a.start[2])+a.start[2],10),255),0)+")"}});var e={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},f=["add","remove","toggle"],g={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};a.effects.animateClass=function(b,c,d,e){return a.isFunction(d)&&(e=d,d=null),this.queue(function(){var g=a(this),k=g.attr("style")||" ",l=i(h.call(this)),m,n=g.attr("class")||"";a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),m=i(h.call(this)),g.attr("class",n),g.animate(j(l,m),{queue:!1,duration:c,easing:d,complete:function(){a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),typeof g.attr("style")=="object"?(g.attr("style").cssText="",g.attr("style").cssText=k):g.attr("style",k),e&&e.apply(this,arguments),a.dequeue(this)}})})},a.fn.extend({_addClass:a.fn.addClass,addClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{add:b},c,d,e]):this._addClass(b)},_removeClass:a.fn.removeClass,removeClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{remove:b},c,d,e]):this._removeClass(b)},_toggleClass:a.fn.toggleClass,toggleClass:function(c,d,e,f,g){return typeof d=="boolean"||d===b?e?a.effects.animateClass.apply(this,[d?{add:c}:{remove:c},e,f,g]):this._toggleClass(c,d):a.effects.animateClass.apply(this,[{toggle:c},d,e,f])},switchClass:function(b,c,d,e,f){return a.effects.animateClass.apply(this,[{add:c,remove:b},d,e,f])}}),a.extend(a.effects,{version:"1.8.23",save:function(a,b){for(var c=0;c<b.length;c++)b[c]!==null&&a.data("ec.storage."+b[c],a[0].style[b[c]])},restore:function(a,b){for(var c=0;c<b.length;c++)b[c]!==null&&a.css(b[c],a.data("ec.storage."+b[c]))},setMode:function(a,b){return b=="toggle"&&(b=a.is(":hidden")?"show":"hide"),b},getBaseline:function(a,b){var c,d;switch(a[0]){case"top":c=0;break;case"middle":c=.5;break;case"bottom":c=1;break;default:c=a[0]/b.height}switch(a[1]){case"left":d=0;break;case"center":d=.5;break;case"right":d=1;break;default:d=a[1]/b.width}return{x:d,y:c}},createWrapper:function(b){if(b.parent().is(".ui-effects-wrapper"))return b.parent();var c={width:b.outerWidth(!0),height:b.outerHeight(!0),"float":b.css("float")},d=a("<div></div>").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e=document.activeElement;try{e.id}catch(f){e=document.body}return b.wrap(d),(b[0]===e||a.contains(b[0],e))&&a(e).focus(),d=b.parent(),b.css("position")=="static"?(d.css({position:"relative"}),b.css({position:"relative"})):(a.extend(c,{position:b.css("position"),zIndex:b.css("z-index")}),a.each(["top","left","bottom","right"],function(a,d){c[d]=b.css(d),isNaN(parseInt(c[d],10))&&(c[d]="auto")}),b.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),d.css(c).show()},removeWrapper:function(b){var c,d=document.activeElement;return b.parent().is(".ui-effects-wrapper")?(c=b.parent().replaceWith(b),(b[0]===d||a.contains(b[0],d))&&a(d).focus(),c):b},setTransition:function(b,c,d,e){return e=e||{},a.each(c,function(a,c){var f=b.cssUnit(c);f[0]>0&&(e[c]=f[0]*d+f[1])}),e}}),a.fn.extend({effect:function(b,c,d,e){var f=k.apply(this,arguments),g={options:f[1],duration:f[2],callback:f[3]},h=g.options.mode,i=a.effects[b];return a.fx.off||!i?h?this[h](g.duration,g.callback):this.each(function(){g.callback&&g.callback.call(this)}):i.call(this,g)},_show:a.fn.show,show:function(a){if(l(a))return this._show.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="show",this.effect.apply(this,b)},_hide:a.fn.hide,hide:function(a){if(l(a))return this._hide.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="hide",this.effect.apply(this,b)},__toggle:a.fn.toggle,toggle:function(b){if(l(b)||typeof b=="boolean"||a.isFunction(b))return this.__toggle.apply(this,arguments);var c=k.apply(this,arguments);return c[1].mode="toggle",this.effect.apply(this,c)},cssUnit:function(b){var c=this.css(b),d=[];return a.each(["em","px","%","pt"],function(a,b){c.indexOf(b)>0&&(d=[parseFloat(c),b])}),d}});var m={};a.each(["Quad","Cubic","Quart","Quint","Expo"],function(a,b){m[b]=function(b){return Math.pow(b,a+2)}}),a.extend(m,{Sine:function(a){return 1-Math.cos(a*Math.PI/2)},Circ:function(a){return 1-Math.sqrt(1-a*a)},Elastic:function(a){return a===0||a===1?a:-Math.pow(2,8*(a-1))*Math.sin(((a-1)*80-7.5)*Math.PI/15)},Back:function(a){return a*a*(3*a-2)},Bounce:function(a){var b,c=4;while(a<((b=Math.pow(2,--c))-1)/11);return 1/Math.pow(4,3-c)-7.5625*Math.pow((b*3-2)/22-a,2)}}),a.each(m,function(b,c){a.easing["easeIn"+b]=c,a.easing["easeOut"+b]=function(a){return 1-c(1-a)},a.easing["easeInOut"+b]=function(a){return a<.5?c(a*2)/2:c(a*-2+2)/-2+1}})}(jQuery),function(a,b){a.effects.blind=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"vertical";a.effects.save(c,d),c.show();var g=a.effects.createWrapper(c).css({overflow:"hidden"}),h=f=="vertical"?"height":"width",i=f=="vertical"?g.height():g.width();e=="show"&&g.css(h,0);var j={};j[h]=e=="show"?i:0,g.animate(j,b.duration,b.options.easing,function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}}(jQuery),function(a,b){a.effects.bounce=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"effect"),f=b.options.direction||"up",g=b.options.distance||20,h=b.options.times||5,i=b.duration||250;/show|hide/.test(e)&&d.push("opacity"),a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var j=f=="up"||f=="down"?"top":"left",k=f=="up"||f=="left"?"pos":"neg",g=b.options.distance||(j=="top"?c.outerHeight(!0)/3:c.outerWidth(!0)/3);e=="show"&&c.css("opacity",0).css(j,k=="pos"?-g:g),e=="hide"&&(g=g/(h*2)),e!="hide"&&h--;if(e=="show"){var l={opacity:1};l[j]=(k=="pos"?"+=":"-=")+g,c.animate(l,i/2,b.options.easing),g=g/2,h--}for(var m=0;m<h;m++){var n={},p={};n[j]=(k=="pos"?"-=":"+=")+g,p[j]=(k=="pos"?"+=":"-=")+g,c.animate(n,i/2,b.options.easing).animate(p,i/2,b.options.easing),g=e=="hide"?g*2:g/2}if(e=="hide"){var l={opacity:0};l[j]=(k=="pos"?"-=":"+=")+g,c.animate(l,i/2,b.options.easing,function(){c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)})}else{var n={},p={};n[j]=(k=="pos"?"-=":"+=")+g,p[j]=(k=="pos"?"+=":"-=")+g,c.animate(n,i/2,b.options.easing).animate(p,i/2,b.options.easing,function(){a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)})}c.queue("fx",function(){c.dequeue()}),c.dequeue()})}}(jQuery),function(a,b){a.effects.clip=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","height","width"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"vertical";a.effects.save(c,d),c.show();var g=a.effects.createWrapper(c).css({overflow:"hidden"}),h=c[0].tagName=="IMG"?g:c,i={size:f=="vertical"?"height":"width",position:f=="vertical"?"top":"left"},j=f=="vertical"?h.height():h.width();e=="show"&&(h.css(i.size,0),h.css(i.position,j/2));var k={};k[i.size]=e=="show"?j:0,k[i.position]=e=="show"?0:j/2,h.animate(k,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()}})})}}(jQuery),function(a,b){a.effects.drop=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","opacity"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"left";a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var g=f=="up"||f=="down"?"top":"left",h=f=="up"||f=="left"?"pos":"neg",i=b.options.distance||(g=="top"?c.outerHeight(!0)/2:c.outerWidth(!0)/2);e=="show"&&c.css("opacity",0).css(g,h=="pos"?-i:i);var j={opacity:e=="show"?1:0};j[g]=(e=="show"?h=="pos"?"+=":"-=":h=="pos"?"-=":"+=")+i,c.animate(j,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}}(jQuery),function(a,b){a.effects.explode=function(b){return this.queue(function(){var c=b.options.pieces?Math.round(Math.sqrt(b.options.pieces)):3,d=b.options.pieces?Math.round(Math.sqrt(b.options.pieces)):3;b.options.mode=b.options.mode=="toggle"?a(this).is(":visible")?"hide":"show":b.options.mode;var e=a(this).show().css("visibility","hidden"),f=e.offset();f.top-=parseInt(e.css("marginTop"),10)||0,f.left-=parseInt(e.css("marginLeft"),10)||0;var g=e.outerWidth(!0),h=e.outerHeight(!0);for(var i=0;i<c;i++)for(var j=0;j<d;j++)e.clone().appendTo("body").wrap("<div></div>").css({position:"absolute",visibility:"visible",left:-j*(g/d),top:-i*(h/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:g/d,height:h/c,left:f.left+j*(g/d)+(b.options.mode=="show"?(j-Math.floor(d/2))*(g/d):0),top:f.top+i*(h/c)+(b.options.mode=="show"?(i-Math.floor(c/2))*(h/c):0),opacity:b.options.mode=="show"?0:1}).animate({left:f.left+j*(g/d)+(b.options.mode=="show"?0:(j-Math.floor(d/2))*(g/d)),top:f.top+i*(h/c)+(b.options.mode=="show"?0:(i-Math.floor(c/2))*(h/c)),opacity:b.options.mode=="show"?1:0},b.duration||500);setTimeout(function(){b.options.mode=="show"?e.css({visibility:"visible"}):e.css({visibility:"visible"}).hide(),b.callback&&b.callback.apply(e[0]),e.dequeue(),a("div.ui-effects-explode").remove()},b.duration||500)})}}(jQuery),function(a,b){a.effects.fade=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"hide");c.animate({opacity:d},{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}}(jQuery),function(a,b){a.effects.fold=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.size||15,g=!!b.options.horizFirst,h=b.duration?b.duration/2:a.fx.speeds._default/2;a.effects.save(c,d),c.show();var i=a.effects.createWrapper(c).css({overflow:"hidden"}),j=e=="show"!=g,k=j?["width","height"]:["height","width"],l=j?[i.width(),i.height()]:[i.height(),i.width()],m=/([0-9]+)%/.exec(f);m&&(f=parseInt(m[1],10)/100*l[e=="hide"?0:1]),e=="show"&&i.css(g?{height:0,width:f}:{height:f,width:0});var n={},p={};n[k[0]]=e=="show"?l[0]:f,p[k[1]]=e=="show"?l[1]:0,i.animate(n,h,b.options.easing).animate(p,h,b.options.easing,function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}}(jQuery),function(a,b){a.effects.highlight=function(b){return this.queue(function(){var c=a(this),d=["backgroundImage","backgroundColor","opacity"],e=a.effects.setMode(c,b.options.mode||"show"),f={backgroundColor:c.css("backgroundColor")};e=="hide"&&(f.opacity=0),a.effects.save(c,d),c.show().css({backgroundImage:"none",backgroundColor:b.options.color||"#ffff99"}).animate(f,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),e=="show"&&!a.support.opacity&&this.style.removeAttribute("filter"),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}}(jQuery),function(a,b){a.effects.pulsate=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"show"),e=(b.options.times||5)*2-1,f=b.duration?b.duration/2:a.fx.speeds._default/2,g=c.is(":visible"),h=0;g||(c.css("opacity",0).show(),h=1),(d=="hide"&&g||d=="show"&&!g)&&e--;for(var i=0;i<e;i++)c.animate({opacity:h},f,b.options.easing),h=(h+1)%2;c.animate({opacity:h},f,b.options.easing,function(){h==0&&c.hide(),b.callback&&b.callback.apply(this,arguments)}),c.queue("fx",function(){c.dequeue()}).dequeue()})}}(jQuery),function(a,b){a.effects.puff=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"hide"),e=parseInt(b.options.percent,10)||150,f=e/100,g={height:c.height(),width:c.width()};a.extend(b.options,{fade:!0,mode:d,percent:d=="hide"?e:100,from:d=="hide"?g:{height:g.height*f,width:g.width*f}}),c.effect("scale",b.options,b.duration,b.callback),c.dequeue()})},a.effects.scale=function(b){return this.queue(function(){var c=a(this),d=a.extend(!0,{},b.options),e=a.effects.setMode(c,b.options.mode||"effect"),f=parseInt(b.options.percent,10)||(parseInt(b.options.percent,10)==0?0:e=="hide"?0:100),g=b.options.direction||"both",h=b.options.origin;e!="effect"&&(d.origin=h||["middle","center"],d.restore=!0);var i={height:c.height(),width:c.width()};c.from=b.options.from||(e=="show"?{height:0,width:0}:i);var j={y:g!="horizontal"?f/100:1,x:g!="vertical"?f/100:1};c.to={height:i.height*j.y,width:i.width*j.x},b.options.fade&&(e=="show"&&(c.from.opacity=0,c.to.opacity=1),e=="hide"&&(c.from.opacity=1,c.to.opacity=0)),d.from=c.from,d.to=c.to,d.mode=e,c.effect("size",d,b.duration,b.callback),c.dequeue()})},a.effects.size=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","width","height","overflow","opacity"],e=["position","top","bottom","left","right","overflow","opacity"],f=["width","height","overflow"],g=["fontSize"],h=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],i=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],j=a.effects.setMode(c,b.options.mode||"effect"),k=b.options.restore||!1,l=b.options.scale||"both",m=b.options.origin,n={height:c.height(),width:c.width()};c.from=b.options.from||n,c.to=b.options.to||n;if(m){var p=a.effects.getBaseline(m,n);c.from.top=(n.height-c.from.height)*p.y,c.from.left=(n.width-c.from.width)*p.x,c.to.top=(n.height-c.to.height)*p.y,c.to.left=(n.width-c.to.width)*p.x}var q={from:{y:c.from.height/n.height,x:c.from.width/n.width},to:{y:c.to.height/n.height,x:c.to.width/n.width}};if(l=="box"||l=="both")q.from.y!=q.to.y&&(d=d.concat(h),c.from=a.effects.setTransition(c,h,q.from.y,c.from),c.to=a.effects.setTransition(c,h,q.to.y,c.to)),q.from.x!=q.to.x&&(d=d.concat(i),c.from=a.effects.setTransition(c,i,q.from.x,c.from),c.to=a.effects.setTransition(c,i,q.to.x,c.to));(l=="content"||l=="both")&&q.from.y!=q.to.y&&(d=d.concat(g),c.from=a.effects.setTransition(c,g,q.from.y,c.from),c.to=a.effects.setTransition(c,g,q.to.y,c.to)),a.effects.save(c,k?d:e),c.show(),a.effects.createWrapper(c),c.css("overflow","hidden").css(c.from);if(l=="content"||l=="both")h=h.concat(["marginTop","marginBottom"]).concat(g),i=i.concat(["marginLeft","marginRight"]),f=d.concat(h).concat(i),c.find("*[width]").each(function(){var c=a(this);k&&a.effects.save(c,f);var d={height:c.height(),width:c.width()};c.from={height:d.height*q.from.y,width:d.width*q.from.x},c.to={height:d.height*q.to.y,width:d.width*q.to.x},q.from.y!=q.to.y&&(c.from=a.effects.setTransition(c,h,q.from.y,c.from),c.to=a.effects.setTransition(c,h,q.to.y,c.to)),q.from.x!=q.to.x&&(c.from=a.effects.setTransition(c,i,q.from.x,c.from),c.to=a.effects.setTransition(c,i,q.to.x,c.to)),c.css(c.from),c.animate(c.to,b.duration,b.options.easing,function(){k&&a.effects.restore(c,f)})});c.animate(c.to,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){c.to.opacity===0&&c.css("opacity",c.from.opacity),j=="hide"&&c.hide(),a.effects.restore(c,k?d:e),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}}(jQuery),function(a,b){a.effects.shake=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"effect"),f=b.options.direction||"left",g=b.options.distance||20,h=b.options.times||3,i=b.duration||b.options.duration||140;a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var j=f=="up"||f=="down"?"top":"left",k=f=="up"||f=="left"?"pos":"neg",l={},m={},n={};l[j]=(k=="pos"?"-=":"+=")+g,m[j]=(k=="pos"?"+=":"-=")+g*2,n[j]=(k=="pos"?"-=":"+=")+g*2,c.animate(l,i,b.options.easing);for(var p=1;p<h;p++)c.animate(m,i,b.options.easing).animate(n,i,b.options.easing);c.animate(m,i,b.options.easing).animate(l,i/2,b.options.easing,function(){a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)}),c.queue("fx",function(){c.dequeue()}),c.dequeue()})}}(jQuery),function(a,b){a.effects.slide=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"show"),f=b.options.direction||"left";a.effects.save(c,d),c.show(),a.effects.createWrapper(c).css({overflow:"hidden"});var g=f=="up"||f=="down"?"top":"left",h=f=="up"||f=="left"?"pos":"neg",i=b.options.distance||(g=="top"?c.outerHeight(!0):c.outerWidth(!0));e=="show"&&c.css(g,h=="pos"?isNaN(i)?"-"+i:-i:i);var j={};j[g]=(e=="show"?h=="pos"?"+=":"-=":h=="pos"?"-=":"+=")+i,c.animate(j,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}}(jQuery),function(a,b){a.effects.transfer=function(b){return this.queue(function(){var c=a(this),d=a(b.options.to),e=d.offset(),f={top:e.top,left:e.left,height:d.innerHeight(),width:d.innerWidth()},g=c.offset(),h=a('<div class="ui-effects-transfer"></div>').appendTo(document.body).addClass(b.options.className).css({top:g.top,left:g.left,height:c.innerHeight(),width:c.innerWidth(),position:"absolute"}).animate(f,b.duration,b.options.easing,function(){h.remove(),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}}(jQuery),function(a,b){a.widget("ui.accordion",{options:{active:0,animated:"slide",autoHeight:!0,clearStyle:!1,collapsible:!1,event:"click",fillSpace:!1,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:!1,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var b=this,c=b.options;b.running=0,b.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"),b.headers=b.element.find(c.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){if(c.disabled)return;a(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){if(c.disabled)return;a(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){if(c.disabled)return;a(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){if(c.disabled)return;a(this).removeClass("ui-state-focus")}),b.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");if(c.navigation){var d=b.element.find("a").filter(c.navigationFilter).eq(0);if(d.length){var e=d.closest(".ui-accordion-header");e.length?b.active=e:b.active=d.closest(".ui-accordion-content").prev()}}b.active=b._findActive(b.active||c.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top"),b.active.next().addClass("ui-accordion-content-active"),b._createIcons(),b.resize(),b.element.attr("role","tablist"),b.headers.attr("role","tab").bind("keydown.accordion",function(a){return b._keydown(a)}).next().attr("role","tabpanel"),b.headers.not(b.active||"").attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).next().hide(),b.active.length?b.active.attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}):b.headers.eq(0).attr("tabIndex",0),a.browser.safari||b.headers.find("a").attr("tabIndex",-1),c.event&&b.headers.bind(c.event.split(" ").join(".accordion ")+".accordion",function(a){b._clickHandler.call(b,a,this),a.preventDefault()})},_createIcons:function(){var b=this.options;b.icons&&(a("<span></span>").addClass("ui-icon "+b.icons.header).prependTo(this.headers),this.active.children(".ui-icon").toggleClass(b.icons.header).toggleClass(b.icons.headerSelected),this.element.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.children(".ui-icon").remove(),this.element.removeClass("ui-accordion-icons")},destroy:function(){var b=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("tabIndex"),this.headers.find("a").removeAttr("tabIndex"),this._destroyIcons();var c=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");return(b.autoHeight||b.fillHeight)&&c.css("height",""),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments),b=="active"&&this.activate(c),b=="icons"&&(this._destroyIcons(),c&&this._createIcons()),b=="disabled"&&this.headers.add(this.headers.next())[c?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(b){if(this.options.disabled||b.altKey||b.ctrlKey)return;var c=a.ui.keyCode,d=this.headers.length,e=this.headers.index(b.target),f=!1;switch(b.keyCode){case c.RIGHT:case c.DOWN:f=this.headers[(e+1)%d];break;case c.LEFT:case c.UP:f=this.headers[(e-1+d)%d];break;case c.SPACE:case c.ENTER:this._clickHandler({target:b.target},b.target),b.preventDefault()}return f?(a(b.target).attr("tabIndex",-1),a(f).attr("tabIndex",0),f.focus(),!1):!0},resize:function(){var b=this.options,c;if(b.fillSpace){if(a.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}c=this.element.parent().height(),a.browser.msie&&this.element.parent().css("overflow",d),this.headers.each(function(){c-=a(this).outerHeight(!0)}),this.headers.next().each(function(){a(this).height(Math.max(0,c-a(this).innerHeight()+a(this).height()))}).css("overflow","auto")}else b.autoHeight&&(c=0,this.headers.next().each(function(){c=Math.max(c,a(this).height("").height())}).height(c));return this},activate:function(a){this.options.active=a;var b=this._findActive(a)[0];return this._clickHandler({target:b},b),this},_findActive:function(b){return b?typeof b=="number"?this.headers.filter(":eq("+b+")"):this.headers.not(this.headers.not(b)):b===!1?a([]):this.headers.filter(":eq(0)")},_clickHandler:function(b,c){var d=this.options;if(d.disabled)return;if(!b.target){if(!d.collapsible)return;this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header),this.active.next().addClass("ui-accordion-content-active");var e=this.active.next(),f={options:d,newHeader:a([]),oldHeader:d.active,newContent:a([]),oldContent:e},g=this.active=a([]);this._toggle(g,e,f);return}var h=a(b.currentTarget||c),i=h[0]===this.active[0];d.active=d.collapsible&&i?!1:this.headers.index(h);if(this.running||!d.collapsible&&i)return;var j=this.active,g=h.next(),e=this.active.next(),f={options:d,newHeader:i&&d.collapsible?a([]):h,oldHeader:this.active,newContent:i&&d.collapsible?a([]):g,oldContent:e},k=this.headers.index(this.active[0])>this.headers.index(h[0]);this.active=i?a([]):h,this._toggle(g,e,f,i,k),j.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header),i||(h.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected),h.next().addClass("ui-accordion-content-active"));return},_toggle:function(b,c,d,e,f){var g=this,h=g.options;g.toShow=b,g.toHide=c,g.data=d;var i=function(){if(!g)return;return g._completed.apply(g,arguments)};g._trigger("changestart",null,g.data),g.running=c.size()===0?b.size():c.size();if(h.animated){var j={};h.collapsible&&e?j={toShow:a([]),toHide:c,complete:i,down:f,autoHeight:h.autoHeight||h.fillSpace}:j={toShow:b,toHide:c,complete:i,down:f,autoHeight:h.autoHeight||h.fillSpace},h.proxied||(h.proxied=h.animated),h.proxiedDuration||(h.proxiedDuration=h.duration),h.animated=a.isFunction(h.proxied)?h.proxied(j):h.proxied,h.duration=a.isFunction(h.proxiedDuration)?h.proxiedDuration(j):h.proxiedDuration;var k=a.ui.accordion.animations,l=h.duration,m=h.animated;m&&!k[m]&&!a.easing[m]&&(m="slide"),k[m]||(k[m]=function(a){this.slide(a,{easing:m,duration:l||700})}),k[m](j)}else h.collapsible&&e?b.toggle():(c.hide(),b.show()),i(!0);c.prev().attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).blur(),b.prev().attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(this.running)return;this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""}),this.toHide.removeClass("ui-accordion-content-active"),this.toHide.length&&(this.toHide.parent()[0].className=this.toHide.parent()[0].className),this._trigger("change",null,this.data)}}),a.extend(a.ui.accordion,{version:"1.8.23",animations:{slide:function(b,c){b=a.extend({easing:"swing",duration:300},b,c);if(!b.toHide.size()){b.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},b);return}if(!b.toShow.size()){b.toHide.animate({height:"hide",paddingTop:"hide",paddingBottom:"hide"},b);return}var d=b.toShow.css("overflow"),e=0,f={},g={},h=["height","paddingTop","paddingBottom"],i,j=b.toShow;i=j[0].style.width,j.width(j.parent().width()-parseFloat(j.css("paddingLeft"))-parseFloat(j.css("paddingRight"))-(parseFloat(j.css("borderLeftWidth"))||0)-(parseFloat(j.css("borderRightWidth"))||0)),a.each(h,function(c,d){g[d]="hide";var e=(""+a.css(b.toShow[0],d)).match(/^([\d+-.]+)(.*)$/);f[d]={value:e[1],unit:e[2]||"px"}}),b.toShow.css({height:0,overflow:"hidden"}).show(),b.toHide.filter(":hidden").each(b.complete).end().filter(":visible").animate(g,{step:function(a,c){c.prop=="height"&&(e=c.end-c.start===0?0:(c.now-c.start)/(c.end-c.start)),b.toShow[0].style[c.prop]=e*f[c.prop].value+f[c.prop].unit},duration:b.duration,easing:b.easing,complete:function(){b.autoHeight||b.toShow.css("height",""),b.toShow.css({width:i,overflow:d}),b.complete()}})},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1e3:200})}}})}(jQuery),function(a,b){var c=0;a.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var b=this,c=this.element[0].ownerDocument,d;this.isMultiLine=this.element.is("textarea"),this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(b.options.disabled||b.element.propAttr("readOnly"))return;d=!1;var e=a.ui.keyCode;switch(c.keyCode){case e.PAGE_UP:b._move("previousPage",c);break;case e.PAGE_DOWN:b._move("nextPage",c);break;case e.UP:b._keyEvent("previous",c);break;case e.DOWN:b._keyEvent("next",c);break;case e.ENTER:case e.NUMPAD_ENTER:b.menu.active&&(d=!0,c.preventDefault());case e.TAB:if(!b.menu.active)return;b.menu.select(c);break;case e.ESCAPE:b.element.val(b.term),b.close(c);break;default:clearTimeout(b.searching),b.searching=setTimeout(function(){b.term!=b.element.val()&&(b.selectedItem=null,b.search(null,c))},b.options.delay)}}).bind("keypress.autocomplete",function(a){d&&(d=!1,a.preventDefault())}).bind("focus.autocomplete",function(){if(b.options.disabled)return;b.selectedItem=null,b.previous=b.element.val()}).bind("blur.autocomplete",function(a){if(b.options.disabled)return;clearTimeout(b.searching),b.closing=setTimeout(function(){b.close(a),b._change(a)},150)}),this._initSource(),this.menu=a("<ul></ul>").addClass("ui-autocomplete").appendTo(a(this.options.appendTo||"body",c)[0]).mousedown(function(c){var d=b.menu.element[0];a(c.target).closest(".ui-menu-item").length||setTimeout(function(){a(document).one("mousedown",function(c){c.target!==b.element[0]&&c.target!==d&&!a.ui.contains(d,c.target)&&b.close()})},1),setTimeout(function(){clearTimeout(b.closing)},13)}).menu({focus:function(a,c){var d=c.item.data("item.autocomplete");!1!==b._trigger("focus",a,{item:d})&&/^key/.test(a.originalEvent.type)&&b.element.val(d.value)},selected:function(a,d){var e=d.item.data("item.autocomplete"),f=b.previous;b.element[0]!==c.activeElement&&(b.element.focus(),b.previous=f,setTimeout(function(){b.previous=f,b.selectedItem=e},1)),!1!==b._trigger("select",a,{item:e})&&b.element.val(e.value),b.term=b.element.val(),b.close(a),b.selectedItem=e},blur:function(a,c){b.menu.element.is(":visible")&&b.element.val()!==b.term&&b.element.val(b.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu"),a.fn.bgiframe&&this.menu.element.bgiframe(),b.beforeunloadHandler=function(){b.element.removeAttr("autocomplete")},a(window).bind("beforeunload",b.beforeunloadHandler)},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup"),this.menu.element.remove(),a(window).unbind("beforeunload",this.beforeunloadHandler),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments),b==="source"&&this._initSource(),b==="appendTo"&&this.menu.element.appendTo(a(c||"body",this.element[0].ownerDocument)[0]),b==="disabled"&&c&&this.xhr&&this.xhr.abort()},_initSource:function(){var b=this,c,d;a.isArray(this.options.source)?(c=this.options.source,this.source=function(b,d){d(a.ui.autocomplete.filter(c,b.term))}):typeof this.options.source=="string"?(d=this.options.source,this.source=function(c,e){b.xhr&&b.xhr.abort(),b.xhr=a.ajax({url:d,data:c,dataType:"json",success:function(a,b){e(a)},error:function(){e([])}})}):this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val(),this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search",b)===!1)return;return this._search(a)},_search:function(a){this.pending++,this.element.addClass("ui-autocomplete-loading"),this.source({term:a},this._response())},_response:function(){var a=this,b=++c;return function(d){b===c&&a.__response(d),a.pending--,a.pending||a.element.removeClass("ui-autocomplete-loading")}},__response:function(a){!this.options.disabled&&a&&a.length?(a=this._normalize(a),this._suggest(a),this._trigger("open")):this.close()},close:function(a){clearTimeout(this.closing),this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.deactivate(),this._trigger("close",a))},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(b){return b.length&&b[0].label&&b[0].value?b:a.map(b,function(b){return typeof b=="string"?{label:b,value:b}:a.extend({label:b.label||b.value,value:b.value||b.label},b)})},_suggest:function(b){var c=this.menu.element.empty().zIndex(this.element.zIndex()+1);this._renderMenu(c,b),this.menu.deactivate(),this.menu.refresh(),c.show(),this._resizeMenu(),c.position(a.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next(new a.Event("mouseover"))},_resizeMenu:function(){var a=this.menu.element;a.outerWidth(Math.max(a.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(b,c){var d=this;a.each(c,function(a,c){d._renderItem(b,c)})},_renderItem:function(b,c){return a("<li></li>").data("item.autocomplete",c).append(a("<a></a>").text(c.label)).appendTo(b)},_move:function(a,b){if(!this.menu.element.is(":visible")){this.search(null,b);return}if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term),this.menu.deactivate();return}this.menu[a](b)},widget:function(){return this.menu.element},_keyEvent:function(a,b){if(!this.isMultiLine||this.menu.element.is(":visible"))this._move(a,b),b.preventDefault()}}),a.extend(a.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")},filter:function(b,c){var d=new RegExp(a.ui.autocomplete.escapeRegex(c),"i");return a.grep(b,function(a){return d.test(a.label||a.value||a)})}})}(jQuery),function(a){a.widget("ui.menu",{_create:function(){var b=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(c){if(!a(c.target).closest(".ui-menu-item a").length)return;c.preventDefault(),b.select(c)}),this.refresh()},refresh:function(){var b=this,c=this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem");c.children("a").addClass("ui-corner-all").attr("tabindex",-1).mouseenter(function(c){b.activate(c,a(this).parent())}).mouseleave(function(){b.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.scrollTop(),e=this.element.height();c<0?this.element.scrollTop(d+c):c>=e&&this.element.scrollTop(d+c-e+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end(),this._trigger("focus",a,{item:b})},deactivate:function(){if(!this.active)return;this.active.children("a").removeClass("ui-state-hover").removeAttr("id"),this._trigger("blur"),this.active=null},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(!this.active){this.activate(c,this.element.children(b));return}var d=this.active[a+"All"](".ui-menu-item").eq(0);d.length?this.activate(c,d):this.activate(c,this.element.children(b))},nextPage:function(b){if(this.hasScroll()){if(!this.active||this.last()){this.activate(b,this.element.children(".ui-menu-item:first"));return}var c=this.active.offset().top,d=this.element.height(),e=this.element.children(".ui-menu-item").filter(function(){var b=a(this).offset().top-c-d+a(this).height();return b<10&&b>-10});e.length||(e=this.element.children(".ui-menu-item:last")),this.activate(b,e)}else this.activate(b,this.element.children(".ui-menu-item").filter(!this.active||this.last()?":first":":last"))},previousPage:function(b){if(this.hasScroll()){if(!this.active||this.first()){this.activate(b,this.element.children(".ui-menu-item:last"));return}var c=this.active.offset().top,d=this.element.height(),e=this.element.children(".ui-menu-item").filter(function(){var b=a(this).offset().top-c+d-a(this).height();return b<10&&b>-10});e.length||(e=this.element.children(".ui-menu-item:first")),this.activate(b,e)}else this.activate(b,this.element.children(".ui-menu-item").filter(!this.active||this.first()?":last":":first"))},hasScroll:function(){return this.element.height()<this.element[a.fn.prop?"prop":"attr"]("scrollHeight")},select:function(a){this._trigger("selected",a,{item:this.active})}})}(jQuery),function(a,b){var c,d,e,f,g="ui-button ui-widget ui-state-default ui-corner-all",h="ui-state-hover ui-state-active ",i="ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",j=function(){var b=a(this).find(":ui-button");setTimeout(function(){b.button("refresh")},1)},k=function(b){var c=b.name,d=b.form,e=a([]);return c&&(d?e=a(d).find("[name='"+c+"']"):e=a("[name='"+c+"']",b.ownerDocument).filter(function(){return!this.form})),e};a.widget("ui.button",{options:{disabled:null,text:!0,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset.button").bind("reset.button",j),typeof this.options.disabled!="boolean"?this.options.disabled=!!this.element.propAttr("disabled"):this.element.propAttr("disabled",this.options.disabled),this._determineButtonType(),this.hasTitle=!!this.buttonElement.attr("title");var b=this,h=this.options,i=this.type==="checkbox"||this.type==="radio",l="ui-state-hover"+(i?"":" ui-state-active"),m="ui-state-focus";h.label===null&&(h.label=this.buttonElement.html()),this.buttonElement.addClass(g).attr("role","button").bind("mouseenter.button",function(){if(h.disabled)return;a(this).addClass("ui-state-hover"),this===c&&a(this).addClass("ui-state-active")}).bind("mouseleave.button",function(){if(h.disabled)return;a(this).removeClass(l)}).bind("click.button",function(a){h.disabled&&(a.preventDefault(),a.stopImmediatePropagation())}),this.element.bind("focus.button",function(){b.buttonElement.addClass(m)}).bind("blur.button",function(){b.buttonElement.removeClass(m)}),i&&(this.element.bind("change.button",function(){if(f)return;b.refresh()}),this.buttonElement.bind("mousedown.button",function(a){if(h.disabled)return;f=!1,d=a.pageX,e=a.pageY}).bind("mouseup.button",function(a){if(h.disabled)return;if(d!==a.pageX||e!==a.pageY)f=!0})),this.type==="checkbox"?this.buttonElement.bind("click.button",function(){if(h.disabled||f)return!1;a(this).toggleClass("ui-state-active"),b.buttonElement.attr("aria-pressed",b.element[0].checked)}):this.type==="radio"?this.buttonElement.bind("click.button",function(){if(h.disabled||f)return!1;a(this).addClass("ui-state-active"),b.buttonElement.attr("aria-pressed","true");var c=b.element[0];k(c).not(c).map(function(){return a(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed","false")}):(this.buttonElement.bind("mousedown.button",function(){if(h.disabled)return!1;a(this).addClass("ui-state-active"),c=this,a(document).one("mouseup",function(){c=null})}).bind("mouseup.button",function(){if(h.disabled)return!1;a(this).removeClass("ui-state-active")}).bind("keydown.button",function(b){if(h.disabled)return!1;(b.keyCode==a.ui.keyCode.SPACE||b.keyCode==a.ui.keyCode.ENTER)&&a(this).addClass("ui-state-active")}).bind("keyup.button",function(){a(this).removeClass("ui-state-active")}),this.buttonElement.is("a")&&this.buttonElement.keyup(function(b){b.keyCode===a.ui.keyCode.SPACE&&a(this).click()})),this._setOption("disabled",h.disabled),this._resetButton()},_determineButtonType:function(){this.element.is(":checkbox")?this.type="checkbox":this.element.is(":radio")?this.type="radio":this.element.is("input")?this.type="input":this.type="button";if(this.type==="checkbox"||this.type==="radio"){var a=this.element.parents().filter(":last"),b="label[for='"+this.element.attr("id")+"']";this.buttonElement=a.find(b),this.buttonElement.length||(a=a.length?a.siblings():this.element.siblings(),this.buttonElement=a.filter(b),this.buttonElement.length||(this.buttonElement=a.find(b))),this.element.addClass("ui-helper-hidden-accessible");var c=this.element.is(":checked");c&&this.buttonElement.addClass("ui-state-active"),this.buttonElement.attr("aria-pressed",c)}else this.buttonElement=this.element},widget:function(){return this.buttonElement},destroy:function(){this.element.removeClass("ui-helper-hidden-accessible"),this.buttonElement.removeClass(g+" "+h+" "+i).removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html()),this.hasTitle||this.buttonElement.removeAttr("title"),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments);if(b==="disabled"){c?this.element.propAttr("disabled",!0):this.element.propAttr("disabled",!1);return}this._resetButton()},refresh:function(){var b=this.element.is(":disabled");b!==this.options.disabled&&this._setOption("disabled",b),this.type==="radio"?k(this.element[0]).each(function(){a(this).is(":checked")?a(this).button("widget").addClass("ui-state-active").attr("aria-pressed","true"):a(this).button("widget").removeClass("ui-state-active").attr("aria-pressed","false")}):this.type==="checkbox"&&(this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed","true"):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed","false"))},_resetButton:function(){if(this.type==="input"){this.options.label&&this.element.val(this.options.label);return}var b=this.buttonElement.removeClass(i),c=a("<span></span>",this.element[0].ownerDocument).addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary,f=[];d.primary||d.secondary?(this.options.text&&f.push("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary")),d.primary&&b.prepend("<span class='ui-button-icon-primary ui-icon "+d.primary+"'></span>"),d.secondary&&b.append("<span class='ui-button-icon-secondary ui-icon "+d.secondary+"'></span>"),this.options.text||(f.push(e?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||b.attr("title",c))):f.push("ui-button-text-only"),b.addClass(f.join(" "))}}),a.widget("ui.buttonset",{options:{items:":button, :submit, :reset, :checkbox, :radio, a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c),a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){var b=this.element.css("direction")==="rtl";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(b?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(b?"ui-corner-left":"ui-corner-right").end().end()},destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy"),a.Widget.prototype.destroy.call(this)}})}(jQuery),function($,undefined){function Datepicker(){this.debug=!1,this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},$.extend(this._defaults,this.regional[""]),this.dpDiv=bindHover($('<div id="'+this._mainDivId+'" class="ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'))}function bindHover(a){var b="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return a.bind("mouseout",function(a){var c=$(a.target).closest(b);if(!c.length)return;c.removeClass("ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover")}).bind("mouseover",function(c){var d=$(c.target).closest(b);if($.datepicker._isDisabledDatepicker(instActive.inline?a.parent()[0]:instActive.input[0])||!d.length)return;d.parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),d.addClass("ui-state-hover"),d.hasClass("ui-datepicker-prev")&&d.addClass("ui-datepicker-prev-hover"),d.hasClass("ui-datepicker-next")&&d.addClass("ui-datepicker-next-hover")})}function extendRemove(a,b){$.extend(a,b);for(var c in b)if(b[c]==null||b[c]==undefined)a[c]=b[c];return a}function isArray(a){return a&&($.browser.safari&&typeof a=="object"&&a.length||a.constructor&&a.constructor.toString().match(/\Array\(\)/))}$.extend($.ui,{datepicker:{version:"1.8.23"}});var PROP_NAME="datepicker",dpuuid=(new Date).getTime(),instActive;$.extend(Datepicker.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(a){return extendRemove(this._defaults,a||{}),this},_attachDatepicker:function(target,settings){var inlineSettings=null;for(var attrName in this._defaults){var attrValue=target.getAttribute("date:"+attrName);if(attrValue){inlineSettings=inlineSettings||{};try{inlineSettings[attrName]=eval(attrValue)}catch(err){inlineSettings[attrName]=attrValue}}}var nodeName=target.nodeName.toLowerCase(),inline=nodeName=="div"||nodeName=="span";target.id||(this.uuid+=1,target.id="dp"+this.uuid);var inst=this._newInst($(target),inline);inst.settings=$.extend({},settings||{},inlineSettings||{}),nodeName=="input"?this._connectDatepicker(target,inst):inline&&this._inlineDatepicker(target,inst)},_newInst:function(a,b){var c=a[0].id.replace(/([^A-Za-z0-9_-])/g,"\\\\$1");return{id:c,input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:b?bindHover($('<div class="'+this._inlineClass+' ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>')):this.dpDiv}},_connectDatepicker:function(a,b){var c=$(a);b.append=$([]),b.trigger=$([]);if(c.hasClass(this.markerClassName))return;this._attachments(c,b),c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(a,c,d){b.settings[c]=d}).bind("getData.datepicker",function(a,c){return this._get(b,c)}),this._autoSize(b),$.data(a,PROP_NAME,b),b.settings.disabled&&this._disableDatepicker(a)},_attachments:function(a,b){var c=this._get(b,"appendText"),d=this._get(b,"isRTL");b.append&&b.append.remove(),c&&(b.append=$('<span class="'+this._appendClass+'">'+c+"</span>"),a[d?"before":"after"](b.append)),a.unbind("focus",this._showDatepicker),b.trigger&&b.trigger.remove();var e=this._get(b,"showOn");(e=="focus"||e=="both")&&a.focus(this._showDatepicker);if(e=="button"||e=="both"){var f=this._get(b,"buttonText"),g=this._get(b,"buttonImage");b.trigger=$(this._get(b,"buttonImageOnly")?$("<img/>").addClass(this._triggerClass).attr({src:g,alt:f,title:f}):$('<button type="button"></button>').addClass(this._triggerClass).html(g==""?f:$("<img/>").attr({src:g,alt:f,title:f}))),a[d?"before":"after"](b.trigger),b.trigger.click(function(){return $.datepicker._datepickerShowing&&$.datepicker._lastInput==a[0]?$.datepicker._hideDatepicker():$.datepicker._datepickerShowing&&$.datepicker._lastInput!=a[0]?($.datepicker._hideDatepicker(),$.datepicker._showDatepicker(a[0])):$.datepicker._showDatepicker(a[0]),!1})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var d=function(a){var b=0,c=0;for(var d=0;d<a.length;d++)a[d].length>b&&(b=a[d].length,c=d);return c};b.setMonth(d(this._get(a,c.match(/MM/)?"monthNames":"monthNamesShort"))),b.setDate(d(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,b){var c=$(a);if(c.hasClass(this.markerClassName))return;c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(a,c,d){b.settings[c]=d}).bind("getData.datepicker",function(a,c){return this._get(b,c)}),$.data(a,PROP_NAME,b),this._setDate(b,this._getDefaultDate(b),!0),this._updateDatepicker(b),this._updateAlternate(b),b.settings.disabled&&this._disableDatepicker(a),b.dpDiv.css("display","block")},_dialogDatepicker:function(a,b,c,d,e){var f=this._dialogInst;if(!f){this.uuid+=1;var g="dp"+this.uuid;this._dialogInput=$('<input type="text" id="'+g+'" style="position: absolute; top: -100px; width: 0px;"/>'),this._dialogInput.keydown(this._doKeyDown),$("body").append(this._dialogInput),f=this._dialogInst=this._newInst(this._dialogInput,!1),f.settings={},$.data(this._dialogInput[0],PROP_NAME,f)}extendRemove(f.settings,d||{}),b=b&&b.constructor==Date?this._formatDate(f,b):b,this._dialogInput.val(b),this._pos=e?e.length?e:[e.pageX,e.pageY]:null;if(!this._pos){var h=document.documentElement.clientWidth,i=document.documentElement.clientHeight,j=document.documentElement.scrollLeft||document.body.scrollLeft,k=document.documentElement.scrollTop||document.body.scrollTop;this._pos=[h/2-100+j,i/2-150+k]}return this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),f.settings.onSelect=c,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),$.blockUI&&$.blockUI(this.dpDiv),$.data(this._dialogInput[0],PROP_NAME,f),this},_destroyDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();$.removeData(a,PROP_NAME),d=="input"?(c.append.remove(),c.trigger.remove(),b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):(d=="div"||d=="span")&&b.removeClass(this.markerClassName).empty()},_enableDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();if(d=="input")a.disabled=!1,c.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""});else if(d=="div"||d=="span"){var e=b.children("."+this._inlineClass);e.children().removeClass("ui-state-disabled"),e.find("select.ui-datepicker-month, select.ui-datepicker-year").removeAttr("disabled")}this._disabledInputs=$.map(this._disabledInputs,function(b){return b==a?null:b})},_disableDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();if(d=="input")a.disabled=!0,c.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"});else if(d=="div"||d=="span"){var e=b.children("."+this._inlineClass);e.children().addClass("ui-state-disabled"),e.find("select.ui-datepicker-month, select.ui-datepicker-year").attr("disabled","disabled")}this._disabledInputs=$.map(this._disabledInputs,function(b){return b==a?null:b}),this._disabledInputs[this._disabledInputs.length]=a},_isDisabledDatepicker:function(a){if(!a)return!1;for(var b=0;b<this._disabledInputs.length;b++)if(this._disabledInputs[b]==a)return!0;return!1},_getInst:function(a){try{return $.data(a,PROP_NAME)}catch(b){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(a,b,c){var d=this._getInst(a);if(arguments.length==2&&typeof b=="string")return b=="defaults"?$.extend({},$.datepicker._defaults):d?b=="all"?$.extend({},d.settings):this._get(d,b):null;var e=b||{};typeof b=="string"&&(e={},e[b]=c);if(d){this._curInst==d&&this._hideDatepicker();var f=this._getDateDatepicker(a,!0),g=this._getMinMaxDate(d,"min"),h=this._getMinMaxDate(d,"max");extendRemove(d.settings,e),g!==null&&e.dateFormat!==undefined&&e.minDate===undefined&&(d.settings.minDate=this._formatDate(d,g)),h!==null&&e.dateFormat!==undefined&&e.maxDate===undefined&&(d.settings.maxDate=this._formatDate(d,h)),this._attachments($(a),d),this._autoSize(d),this._setDate(d,f),this._updateAlternate(d),this._updateDatepicker(d)}},_changeDatepicker:function(a,b,c){this._optionDatepicker(a,b,c)},_refreshDatepicker:function(a){var b=this._getInst(a);b&&this._updateDatepicker(b)},_setDateDatepicker:function(a,b){var c=this._getInst(a);c&&(this._setDate(c,b),this._updateDatepicker(c),this._updateAlternate(c))},_getDateDatepicker:function(a,b){var c=this._getInst(a);return c&&!c.inline&&this._setDateFromField(c,b),c?this._getDate(c):null},_doKeyDown:function(a){var b=$.datepicker._getInst(a.target),c=!0,d=b.dpDiv.is(".ui-datepicker-rtl");b._keyEvent=!0;if($.datepicker._datepickerShowing)switch(a.keyCode){case 9:$.datepicker._hideDatepicker(),c=!1;break;case 13:var e=$("td."+$.datepicker._dayOverClass+":not(."+$.datepicker._currentClass+")",b.dpDiv);e[0]&&$.datepicker._selectDay(a.target,b.selectedMonth,b.selectedYear,e[0]);var f=$.datepicker._get(b,"onSelect");if(f){var g=$.datepicker._formatDate(b);f.apply(b.input?b.input[0]:null,[g,b])}else $.datepicker._hideDatepicker();return!1;case 27:$.datepicker._hideDatepicker();break;case 33:$.datepicker._adjustDate(a.target,a.ctrlKey?-$.datepicker._get(b,"stepBigMonths"):-$.datepicker._get(b,"stepMonths"),"M");break;case 34:$.datepicker._adjustDate(a.target,a.ctrlKey?+$.datepicker._get(b,"stepBigMonths"):+$.datepicker._get(b,"stepMonths"),"M");break;case 35:(a.ctrlKey||a.metaKey)&&$.datepicker._clearDate(a.target),c=a.ctrlKey||a.metaKey;break;case 36:(a.ctrlKey||a.metaKey)&&$.datepicker._gotoToday(a.target),c=a.ctrlKey||a.metaKey;break;case 37:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,d?1:-1,"D"),c=a.ctrlKey||a.metaKey,a.originalEvent.altKey&&$.datepicker._adjustDate(a.target,a.ctrlKey?-$.datepicker._get(b,"stepBigMonths"):-$.datepicker._get(b,"stepMonths"),"M");break;case 38:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,-7,"D"),c=a.ctrlKey||a.metaKey;break;case 39:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,d?-1:1,"D"),c=a.ctrlKey||a.metaKey,a.originalEvent.altKey&&$.datepicker._adjustDate(a.target,a.ctrlKey?+$.datepicker._get(b,"stepBigMonths"):+$.datepicker._get(b,"stepMonths"),"M");break;case 40:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,7,"D"),c=a.ctrlKey||a.metaKey;break;default:c=!1}else a.keyCode==36&&a.ctrlKey?$.datepicker._showDatepicker(this):c=!1;c&&(a.preventDefault(),a.stopPropagation())},_doKeyPress:function(a){var b=$.datepicker._getInst(a.target);if($.datepicker._get(b,"constrainInput")){var c=$.datepicker._possibleChars($.datepicker._get(b,"dateFormat")),d=String.fromCharCode(a.charCode==undefined?a.keyCode:a.charCode);return a.ctrlKey||a.metaKey||d<" "||!c||c.indexOf(d)>-1}},_doKeyUp:function(a){var b=$.datepicker._getInst(a.target);if(b.input.val()!=b.lastVal)try{var c=$.datepicker.parseDate($.datepicker._get(b,"dateFormat"),b.input?b.input.val():null,$.datepicker._getFormatConfig(b));c&&($.datepicker._setDateFromField(b),$.datepicker._updateAlternate(b),$.datepicker._updateDatepicker(b))}catch(d){$.datepicker.log(d)}return!0},_showDatepicker:function(a){a=a.target||a,a.nodeName.toLowerCase()!="input"&&(a=$("input",a.parentNode)[0]);if($.datepicker._isDisabledDatepicker(a)||$.datepicker._lastInput==a)return;var b=$.datepicker._getInst(a);$.datepicker._curInst&&$.datepicker._curInst!=b&&($.datepicker._curInst.dpDiv.stop(!0,!0),b&&$.datepicker._datepickerShowing&&$.datepicker._hideDatepicker($.datepicker._curInst.input[0]));var c=$.datepicker._get(b,"beforeShow"),d=c?c.apply(a,[a,b]):{};if(d===!1)return;extendRemove(b.settings,d),b.lastVal=null,$.datepicker._lastInput=a,$.datepicker._setDateFromField(b),$.datepicker._inDialog&&(a.value=""),$.datepicker._pos||($.datepicker._pos=$.datepicker._findPos(a),$.datepicker._pos[1]+=a.offsetHeight);var e=!1;$(a).parents().each(function(){return e|=$(this).css("position")=="fixed",!e}),e&&$.browser.opera&&($.datepicker._pos[0]-=document.documentElement.scrollLeft,$.datepicker._pos[1]-=document.documentElement.scrollTop);var f={left:$.datepicker._pos[0],top:$.datepicker._pos[1]};$.datepicker._pos=null,b.dpDiv.empty(),b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),$.datepicker._updateDatepicker(b),f=$.datepicker._checkOffset(b,f,e),b.dpDiv.css({position:$.datepicker._inDialog&&$.blockUI?"static":e?"fixed":"absolute",display:"none",left:f.left+"px",top:f.top+"px"});if(!b.inline){var g=$.datepicker._get(b,"showAnim"),h=$.datepicker._get(b,"duration"),i=function(){var a=b.dpDiv.find("iframe.ui-datepicker-cover");if(!!a.length){var c=$.datepicker._getBorders(b.dpDiv);a.css({left:-c[0],top:-c[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})}};b.dpDiv.zIndex($(a).zIndex()+1),$.datepicker._datepickerShowing=!0,$.effects&&$.effects[g]?b.dpDiv.show(g,$.datepicker._get(b,"showOptions"),h,i):b.dpDiv[g||"show"](g?h:null,i),(!g||!h)&&i(),b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus(),$.datepicker._curInst=b}},_updateDatepicker:function(a){var b=this;b.maxRows=4;var c=$.datepicker._getBorders(a.dpDiv);instActive=a,a.dpDiv.empty().append(this._generateHTML(a)),this._attachHandlers(a);var d=a.dpDiv.find("iframe.ui-datepicker-cover");!d.length||d.css({left:-c[0],top:-c[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}),a.dpDiv.find("."+this._dayOverClass+" a").mouseover();var e=this._getNumberOfMonths(a),f=e[1],g=17;a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),f>1&&a.dpDiv.addClass("ui-datepicker-multi-"+f).css("width",g*f+"em"),a.dpDiv[(e[0]!=1||e[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi"),a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),a==$.datepicker._curInst&&$.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&a.input[0]!=document.activeElement&&a.input.focus();if(a.yearshtml){var h=a.yearshtml;setTimeout(function(){h===a.yearshtml&&a.yearshtml&&a.dpDiv.find("select.ui-datepicker-year:first").replaceWith(a.yearshtml),h=a.yearshtml=null},0)}},_getBorders:function(a){var b=function(a){return{thin:1,medium:2,thick:3}[a]||a};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var d=a.dpDiv.outerWidth(),e=a.dpDiv.outerHeight(),f=a.input?a.input.outerWidth():0,g=a.input?a.input.outerHeight():0,h=document.documentElement.clientWidth+(c?0:$(document).scrollLeft()),i=document.documentElement.clientHeight+(c?0:$(document).scrollTop());return b.left-=this._get(a,"isRTL")?d-f:0,b.left-=c&&b.left==a.input.offset().left?$(document).scrollLeft():0,b.top-=c&&b.top==a.input.offset().top+g?$(document).scrollTop():0,b.left-=Math.min(b.left,b.left+d>h&&h>d?Math.abs(b.left+d-h):0),b.top-=Math.min(b.top,b.top+e>i&&i>e?Math.abs(e+g):0),b},_findPos:function(a){var b=this._getInst(a),c=this._get(b,"isRTL");while(a&&(a.type=="hidden"||a.nodeType!=1||$.expr.filters.hidden(a)))a=a[c?"previousSibling":"nextSibling"];var d=$(a).offset();return[d.left,d.top]},_hideDatepicker:function(a){var b=this._curInst;if(!b||a&&b!=$.data(a,PROP_NAME))return;if(this._datepickerShowing){var c=this._get(b,"showAnim"),d=this._get(b,"duration"),e=function(){$.datepicker._tidyDialog(b)};$.effects&&$.effects[c]?b.dpDiv.hide(c,$.datepicker._get(b,"showOptions"),d,e):b.dpDiv[c=="slideDown"?"slideUp":c=="fadeIn"?"fadeOut":"hide"](c?d:null,e),c||e(),this._datepickerShowing=!1;var f=this._get(b,"onClose");f&&f.apply(b.input?b.input[0]:null,[b.input?b.input.val():"",b]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),$.blockUI&&($.unblockUI(),$("body").append(this.dpDiv))),this._inDialog=!1}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(a){if(!$.datepicker._curInst)return;var b=$(a.target),c=$.datepicker._getInst(b[0]);(b[0].id!=$.datepicker._mainDivId&&b.parents("#"+$.datepicker._mainDivId).length==0&&!b.hasClass($.datepicker.markerClassName)&&!b.closest("."+$.datepicker._triggerClass).length&&$.datepicker._datepickerShowing&&(!$.datepicker._inDialog||!$.blockUI)||b.hasClass($.datepicker.markerClassName)&&$.datepicker._curInst!=c)&&$.datepicker._hideDatepicker()},_adjustDate:function(a,b,c){var d=$(a),e=this._getInst(d[0]);if(this._isDisabledDatepicker(d[0]))return;this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):0),c),this._updateDatepicker(e)},_gotoToday:function(a){var b=$(a),c=this._getInst(b[0]);if(this._get(c,"gotoCurrent")&&c.currentDay)c.selectedDay=c.currentDay,c.drawMonth=c.selectedMonth=c.currentMonth,c.drawYear=c.selectedYear=c.currentYear;else{var d=new Date;c.selectedDay=d.getDate(),c.drawMonth=c.selectedMonth=d.getMonth(),c.drawYear=c.selectedYear=d.getFullYear()}this._notifyChange(c),this._adjustDate(b)},_selectMonthYear:function(a,b,c){var d=$(a),e=this._getInst(d[0]);e["selected"+(c=="M"?"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10),this._notifyChange(e),this._adjustDate(d)},_selectDay:function(a,b,c,d){var e=$(a);if($(d).hasClass(this._unselectableClass)||this._isDisabledDatepicker(e[0]))return;var f=this._getInst(e[0]);f.selectedDay=f.currentDay=$("a",d).html(),f.selectedMonth=f.currentMonth=b,f.selectedYear=f.currentYear=c,this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))},_clearDate:function(a){var b=$(a),c=this._getInst(b[0]);this._selectDate(b,"")},_selectDate:function(a,b){var c=$(a),d=this._getInst(c[0]);b=b!=null?b:this._formatDate(d),d.input&&d.input.val(b),this._updateAlternate(d);var e=this._get(d,"onSelect");e?e.apply(d.input?d.input[0]:null,[b,d]):d.input&&d.input.trigger("change"),d.inline?this._updateDatepicker(d):(this._hideDatepicker(),this._lastInput=d.input[0],typeof d.input[0]!="object"&&d.input.focus(),this._lastInput=null)},_updateAlternate:function(a){var b=this._get(a,"altField");if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),d=this._getDate(a),e=this.formatDate(c,d,this._getFormatConfig(a));$(b).each(function(){$(this).val(e)})}},noWeekends:function(a){var b=a.getDay();return[b>0&&b<6,""]},iso8601Week:function(a){var b=new Date(a.getTime());b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b=="object"?b.toString():b+"";if(b=="")return null;var d=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff;d=typeof d!="string"?d:(new Date).getFullYear()%100+parseInt(d,10);var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,g=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,h=(c?c.monthNames:null)||this._defaults.monthNames,i=-1,j=-1,k=-1,l=-1,m=!1,n=function(b){var c=s+1<a.length&&a.charAt(s+1)==b;return c&&s++,c},o=function(a){var c=n(a),d=a=="@"?14:a=="!"?20:a=="y"&&c?4:a=="o"?3:2,e=new RegExp("^\\d{1,"+d+"}"),f=b.substring(r).match(e);if(!f)throw"Missing number at position "+r;return r+=f[0].length,parseInt(f[0],10)},p=function(a,c,d){var e=$.map(n(a)?d:c,function(a,b){return[[b,a]]}).sort(function(a,b){return-(a[1].length-b[1].length)}),f=-1;$.each(e,function(a,c){var d=c[1];if(b.substr(r,d.length).toLowerCase()==d.toLowerCase())return f=c[0],r+=d.length,!1});if(f!=-1)return f+1;throw"Unknown name at position "+r},q=function(){if(b.charAt(r)!=a.charAt(s))throw"Unexpected literal at position "+r;r++},r=0;for(var s=0;s<a.length;s++)if(m)a.charAt(s)=="'"&&!n("'")?m=!1:q();else switch(a.charAt(s)){case"d":k=o("d");break;case"D":p("D",e,f);break;case"o":l=o("o");break;case"m":j=o("m");break;case"M":j=p("M",g,h);break;case"y":i=o("y");break;case"@":var t=new Date(o("@"));i=t.getFullYear(),j=t.getMonth()+1,k=t.getDate();break;case"!":var t=new Date((o("!")-this._ticksTo1970)/1e4);i=t.getFullYear(),j=t.getMonth()+1,k=t.getDate();break;case"'":n("'")?q():m=!0;break;default:q()}if(r<b.length)throw"Extra/unparsed characters found in date: "+b.substring(r);i==-1?i=(new Date).getFullYear():i<100&&(i+=(new Date).getFullYear()-(new Date).getFullYear()%100+(i<=d?0:-100));if(l>-1){j=1,k=l;do{var u=this._getDaysInMonth(i,j-1);if(k<=u)break;j++,k-=u}while(!0)}var t=this._daylightSavingAdjust(new Date(i,j-1,k));if(t.getFullYear()!=i||t.getMonth()+1!=j||t.getDate()!=k)throw"Invalid date";return t},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1e7,formatDate:function(a,b,c){if(!b)return"";var d=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,e=(c?c.dayNames:null)||this._defaults.dayNames,f=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,h=function(b){var c=m+1<a.length&&a.charAt(m+1)==b;return c&&m++,c},i=function(a,b,c){var d=""+b;if(h(a))while(d.length<c)d="0"+d;return d},j=function(a,b,c,d){return h(a)?d[b]:c[b]},k="",l=!1;if(b)for(var m=0;m<a.length;m++)if(l)a.charAt(m)=="'"&&!h("'")?l=!1:k+=a.charAt(m);else switch(a.charAt(m)){case"d":k+=i("d",b.getDate(),2);break;case"D":k+=j("D",b.getDay(),d,e);break;case"o":k+=i("o",Math.round(((new Date(b.getFullYear(),b.getMonth(),b.getDate())).getTime()-(new Date(b.getFullYear(),0,0)).getTime())/864e5),3);break;case"m":k+=i("m",b.getMonth()+1,2);break;case"M":k+=j("M",b.getMonth(),f,g);break;case"y":k+=h("y")?b.getFullYear():(b.getYear()%100<10?"0":"")+b.getYear()%100;break;case"@":k+=b.getTime();break;case"!":k+=b.getTime()*1e4+this._ticksTo1970;break;case"'":h("'")?k+="'":l=!0;break;default:k+=a.charAt(m)}return k},_possibleChars:function(a){var b="",c=!1,d=function(b){var c=e+1<a.length&&a.charAt(e+1)==b;return c&&e++,c};for(var e=0;e<a.length;e++)if(c)a.charAt(e)=="'"&&!d("'")?c=!1:b+=a.charAt(e);else switch(a.charAt(e)){case"d":case"m":case"y":case"@":b+="0123456789";break;case"D":case"M":return null;case"'":d("'")?b+="'":c=!0;break;default:b+=a.charAt(e)}return b},_get:function(a,b){return a.settings[b]!==undefined?a.settings[b]:this._defaults[b]},_setDateFromField:function(a,b){if(a.input.val()==a.lastVal)return;var c=this._get(a,"dateFormat"),d=a.lastVal=a.input?a.input.val():null,e,f;e=f=this._getDefaultDate(a);var g=this._getFormatConfig(a);try{e=this.parseDate(c,d,g)||f}catch(h){this.log(h),d=b?"":d}a.selectedDay=e.getDate(),a.drawMonth=a.selectedMonth=e.getMonth(),a.drawYear=a.selectedYear=e.getFullYear(),a.currentDay=d?e.getDate():0,a.currentMonth=d?e.getMonth():0,a.currentYear=d?e.getFullYear():0,this._adjustInstDate(a)},_getDefaultDate:function(a){return this._restrictMinMax(a,this._determineDate(a,this._get(a,"defaultDate"),new Date))},_determineDate:function(a,b,c){var d=function(a){var b=new Date;return b.setDate(b.getDate()+a),b},e=function(b){try{return $.datepicker.parseDate($.datepicker._get(a,"dateFormat"),b,$.datepicker._getFormatConfig(a))}catch(c){}var d=(b.toLowerCase().match(/^c/)?$.datepicker._getDate(a):null)||new Date,e=d.getFullYear(),f=d.getMonth(),g=d.getDate(),h=/([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,i=h.exec(b);while(i){switch(i[2]||"d"){case"d":case"D":g+=parseInt(i[1],10);break;case"w":case"W":g+=parseInt(i[1],10)*7;break;case"m":case"M":f+=parseInt(i[1],10),g=Math.min(g,$.datepicker._getDaysInMonth(e,f));break;case"y":case"Y":e+=parseInt(i[1],10),g=Math.min(g,$.datepicker._getDaysInMonth(e,f))}i=h.exec(b)}return new Date(e,f,g)},f=b==null||b===""?c:typeof b=="string"?e(b):typeof b=="number"?isNaN(b)?c:d(b):new Date(b.getTime());return f=f&&f.toString()=="Invalid Date"?c:f,f&&(f.setHours(0),f.setMinutes(0),f.setSeconds(0),f.setMilliseconds(0)),this._daylightSavingAdjust(f)},_daylightSavingAdjust:function(a){return a?(a.setHours(a.getHours()>12?a.getHours()+2:0),a):null},_setDate:function(a,b,c){var d=!b,e=a.selectedMonth,f=a.selectedYear,g=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=g.getDate(),a.drawMonth=a.selectedMonth=a.currentMonth=g.getMonth(),a.drawYear=a.selectedYear=a.currentYear=g.getFullYear(),(e!=a.selectedMonth||f!=a.selectedYear)&&!c&&this._notifyChange(a),this._adjustInstDate(a),a.input&&a.input.val(d?"":this._formatDate(a))},_getDate:function(a){var b=!a.currentYear||a.input&&a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return b},_attachHandlers:function(a){var b=this._get(a,"stepMonths"),c="#"+a.id.replace(/\\\\/g,"\\");a.dpDiv.find("[data-handler]").map(function(){var a={prev:function(){window["DP_jQuery_"+dpuuid].datepicker._adjustDate(c,-b,"M")},next:function(){window["DP_jQuery_"+dpuuid].datepicker._adjustDate(c,+b,"M")},hide:function(){window["DP_jQuery_"+dpuuid].datepicker._hideDatepicker()},today:function(){window["DP_jQuery_"+dpuuid].datepicker._gotoToday(c)},selectDay:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectDay(c,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectMonthYear(c,this,"M"),!1},selectYear:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectMonthYear(c,this,"Y"),!1}};$(this).bind(this.getAttribute("data-event"),a[this.getAttribute("data-handler")])})},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),d=this._get(a,"showButtonPanel"),e=this._get(a,"hideIfNoPrevNext"),f=this._get(a,"navigationAsDateFormat"),g=this._getNumberOfMonths(a),h=this._get(a,"showCurrentAtPos"),i=this._get(a,"stepMonths"),j=g[0]!=1||g[1]!=1,k=this._daylightSavingAdjust(a.currentDay?new Date(a.currentYear,a.currentMonth,a.currentDay):new Date(9999,9,9)),l=this._getMinMaxDate(a,"min"),m=this._getMinMaxDate(a,"max"),n=a.drawMonth-h,o=a.drawYear;n<0&&(n+=12,o--);if(m){var p=this._daylightSavingAdjust(new Date(m.getFullYear(),m.getMonth()-g[0]*g[1]+1,m.getDate()));p=l&&p<l?l:p;while(this._daylightSavingAdjust(new Date(o,n,1))>p)n--,n<0&&(n=11,o--)}a.drawMonth=n,a.drawYear=o;var q=this._get(a,"prevText");q=f?this.formatDate(q,this._daylightSavingAdjust(new Date(o,n-i,1)),this._getFormatConfig(a)):q;var r=this._canAdjustMonth(a,-1,o,n)?'<a class="ui-datepicker-prev ui-corner-all" data-handler="prev" data-event="click" title="'+q+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"e":"w")+'">'+q+"</span></a>":e?"":'<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="'+q+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"e":"w")+'">'+q+"</span></a>",s=this._get(a,"nextText");s=f?this.formatDate(s,this._daylightSavingAdjust(new Date(o,n+i,1)),this._getFormatConfig(a)):s;var t=this._canAdjustMonth(a,1,o,n)?'<a class="ui-datepicker-next ui-corner-all" data-handler="next" data-event="click" title="'+s+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"w":"e")+'">'+s+"</span></a>":e?"":'<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="'+s+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"w":"e")+'">'+s+"</span></a>",u=this._get(a,"currentText"),v=this._get(a,"gotoCurrent")&&a.currentDay?k:b;u=f?this.formatDate(u,v,this._getFormatConfig(a)):u;var w=a.inline?"":'<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" data-handler="hide" data-event="click">'+this._get(a,"closeText")+"</button>",x=d?'<div class="ui-datepicker-buttonpane ui-widget-content">'+(c?w:"")+(this._isInRange(a,v)?'<button type="button" class="ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all" data-handler="today" data-event="click">'+u+"</button>":"")+(c?"":w)+"</div>":"",y=parseInt(this._get(a,"firstDay"),10);y=isNaN(y)?0:y;var z=this._get(a,"showWeek"),A=this._get(a,"dayNames"),B=this._get(a,"dayNamesShort"),C=this._get(a,"dayNamesMin"),D=this._get(a,"monthNames"),E=this._get(a,"monthNamesShort"),F=this._get(a,"beforeShowDay"),G=this._get(a,"showOtherMonths"),H=this._get(a,"selectOtherMonths"),I=this._get(a,"calculateWeek")||this.iso8601Week,J=this._getDefaultDate(a),K="";for(var L=0;L<g[0];L++){var M="";this.maxRows=4;for(var N=0;N<g[1];N++){var O=this._daylightSavingAdjust(new Date(o,n,a.selectedDay)),P=" ui-corner-all",Q="";if(j){Q+='<div class="ui-datepicker-group';if(g[1]>1)switch(N){case 0:Q+=" ui-datepicker-group-first",P=" ui-corner-"+(c?"right":"left");break;case g[1]-1:Q+=" ui-datepicker-group-last",P=" ui-corner-"+(c?"left":"right");break;default:Q+=" ui-datepicker-group-middle",P=""}Q+='">'}Q+='<div class="ui-datepicker-header ui-widget-header ui-helper-clearfix'+P+'">'+(/all|left/.test(P)&&L==0?c?t:r:"")+(/all|right/.test(P)&&L==0?c?r:t:"")+this._generateMonthYearHeader(a,n,o,l,m,L>0||N>0,D,E)+'</div><table class="ui-datepicker-calendar"><thead>'+"<tr>";var R=z?'<th class="ui-datepicker-week-col">'+this._get(a,"weekHeader")+"</th>":"";for(var S=0;S<7;S++){var T=(S+y)%7;R+="<th"+((S+y+6)%7>=5?' class="ui-datepicker-week-end"':"")+">"+'<span title="'+A[T]+'">'+C[T]+"</span></th>"}Q+=R+"</tr></thead><tbody>";var U=this._getDaysInMonth(o,n);o==a.selectedYear&&n==a.selectedMonth&&(a.selectedDay=Math.min(a.selectedDay,U));var V=(this._getFirstDayOfMonth(o,n)-y+7)%7,W=Math.ceil((V+U)/7),X=j?this.maxRows>W?this.maxRows:W:W;this.maxRows=X;var Y=this._daylightSavingAdjust(new Date(o,n,1-V));for(var Z=0;Z<X;Z++){Q+="<tr>";var _=z?'<td class="ui-datepicker-week-col">'+this._get(a,"calculateWeek")(Y)+"</td>":"";for(var S=0;S<7;S++){var ba=F?F.apply(a.input?a.input[0]:null,[Y]):[!0,""],bb=Y.getMonth()!=n,bc=bb&&!H||!ba[0]||l&&Y<l||m&&Y>m;_+='<td class="'+((S+y+6)%7>=5?" ui-datepicker-week-end":"")+(bb?" ui-datepicker-other-month":"")+(Y.getTime()==O.getTime()&&n==a.selectedMonth&&a._keyEvent||J.getTime()==Y.getTime()&&J.getTime()==O.getTime()?" "+this._dayOverClass:"")+(bc?" "+this._unselectableClass+" ui-state-disabled":"")+(bb&&!G?"":" "+ba[1]+(Y.getTime()==k.getTime()?" "+this._currentClass:"")+(Y.getTime()==b.getTime()?" ui-datepicker-today":""))+'"'+((!bb||G)&&ba[2]?' title="'+ba[2]+'"':"")+(bc?"":' data-handler="selectDay" data-event="click" data-month="'+Y.getMonth()+'" data-year="'+Y.getFullYear()+'"')+">"+(bb&&!G?" ":bc?'<span class="ui-state-default">'+Y.getDate()+"</span>":'<a class="ui-state-default'+(Y.getTime()==b.getTime()?" ui-state-highlight":"")+(Y.getTime()==k.getTime()?" ui-state-active":"")+(bb?" ui-priority-secondary":"")+'" href="#">'+Y.getDate()+"</a>")+"</td>",Y.setDate(Y.getDate()+1),Y=this._daylightSavingAdjust(Y)}Q+=_+"</tr>"}n++,n>11&&(n=0,o++),Q+="</tbody></table>"+(j?"</div>"+(g[0]>0&&N==g[1]-1?'<div class="ui-datepicker-row-break"></div>':""):""),M+=Q}K+=M}return K+=x+($.browser.msie&&parseInt($.browser.version,10)<7&&!a.inline?'<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>':""),a._keyEvent=!1,K},_generateMonthYearHeader:function(a,b,c,d,e,f,g,h){var i=this._get(a,"changeMonth"),j=this._get(a,"changeYear"),k=this._get(a,"showMonthAfterYear"),l='<div class="ui-datepicker-title">',m="";if(f||!i)m+='<span class="ui-datepicker-month">'+g[b]+"</span>";else{var n=d&&d.getFullYear()==c,o=e&&e.getFullYear()==c;m+='<select class="ui-datepicker-month" data-handler="selectMonth" data-event="change">';for(var p=0;p<12;p++)(!n||p>=d.getMonth())&&(!o||p<=e.getMonth())&&(m+='<option value="'+p+'"'+(p==b?' selected="selected"':"")+">"+h[p]+"</option>");m+="</select>"}k||(l+=m+(f||!i||!j?" ":""));if(!a.yearshtml){a.yearshtml="";if(f||!j)l+='<span class="ui-datepicker-year">'+c+"</span>";else{var q=this._get(a,"yearRange").split(":"),r=(new Date).getFullYear(),s=function(a){var b=a.match(/c[+-].*/)?c+parseInt(a.substring(1),10):a.match(/[+-].*/)?r+parseInt(a,10):parseInt(a,10);return isNaN(b)?r:b},t=s(q[0]),u=Math.max(t,s(q[1]||""));t=d?Math.max(t,d.getFullYear()):t,u=e?Math.min(u,e.getFullYear()):u,a.yearshtml+='<select class="ui-datepicker-year" data-handler="selectYear" data-event="change">';for(;t<=u;t++)a.yearshtml+='<option value="'+t+'"'+(t==c?' selected="selected"':"")+">"+t+"</option>";a.yearshtml+="</select>",l+=a.yearshtml,a.yearshtml=null}}return l+=this._get(a,"yearSuffix"),k&&(l+=(f||!i||!j?" ":"")+m),l+="</div>",l},_adjustInstDate:function(a,b,c){var d=a.drawYear+(c=="Y"?b:0),e=a.drawMonth+(c=="M"?b:0),f=Math.min(a.selectedDay,this._getDaysInMonth(d,e))+(c=="D"?b:0),g=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(d,e,f)));a.selectedDay=g.getDate(),a.drawMonth=a.selectedMonth=g.getMonth(),a.drawYear=a.selectedYear=g.getFullYear(),(c=="M"||c=="Y")&&this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max"),e=c&&b<c?c:b;return e=d&&e>d?d:e,e},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");b&&b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){var b=this._get(a,"numberOfMonths");return b==null?[1,1]:typeof b=="number"?[1,b]:b},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,d){var e=this._getNumberOfMonths(a),f=this._daylightSavingAdjust(new Date(c,d+(b<0?b:e[0]*e[1]),1));return b<0&&f.setDate(this._getDaysInMonth(f.getFullYear(),f.getMonth())),this._isInRange(a,f)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!d||b.getTime()<=d.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");return b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10),{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,d){b||(a.currentDay=a.selectedDay,a.currentMonth=a.selectedMonth,a.currentYear=a.selectedYear);var e=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(d,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),e,this._getFormatConfig(a))}}),$.fn.datepicker=function(a){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find("body").append($.datepicker.dpDiv),$.datepicker.initialized=!0);var b=Array.prototype.slice.call(arguments,1);return typeof a!="string"||a!="isDisabled"&&a!="getDate"&&a!="widget"?a=="option"&&arguments.length==2&&typeof arguments[1]=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b)):this.each(function(){typeof a=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this].concat(b)):$.datepicker._attachDatepicker(this,a)}):$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b))},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.8.23",window["DP_jQuery_"+dpuuid]=$}(jQuery),function(a,b){var c="ui-dialog ui-widget ui-widget-content ui-corner-all ",d={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},e={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};a.widget("ui.dialog",{options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",collision:"fit",using:function(b){var c=a(this).css(b).offset().top;c<0&&a(this).css("top",b.top-c)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),typeof this.originalTitle!="string"&&(this.originalTitle=""),this.options.title=this.options.title||this.originalTitle;var b=this,d=b.options,e=d.title||" ",f=a.ui.dialog.getTitleId(b.element),g=(b.uiDialog=a("<div></div>")).appendTo(document.body).hide().addClass(c+d.dialogClass).css({zIndex:d.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(c){d.closeOnEscape&&!c.isDefaultPrevented()&&c.keyCode&&c.keyCode===a.ui.keyCode.ESCAPE&&(b.close(c),c.preventDefault())}).attr({role:"dialog","aria-labelledby":f}).mousedown(function(a){b.moveToTop(!1,a)}),h=b.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g),i=(b.uiDialogTitlebar=a("<div></div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),j=a('<a href="#"></a>').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){j.addClass("ui-state-hover")},function(){j.removeClass("ui-state-hover")}).focus(function(){j.addClass("ui-state-focus")}).blur(function(){j.removeClass("ui-state-focus")}).click(function(a){return b.close(a),!1}).appendTo(i),k=(b.uiDialogTitlebarCloseText=a("<span></span>")).addClass("ui-icon ui-icon-closethick").text(d.closeText).appendTo(j),l=a("<span></span>").addClass("ui-dialog-title").attr("id",f).html(e).prependTo(i);a.isFunction(d.beforeclose)&&!a.isFunction(d.beforeClose)&&(d.beforeClose=d.beforeclose),i.find("*").add(i).disableSelection(),d.draggable&&a.fn.draggable&&b._makeDraggable(),d.resizable&&a.fn.resizable&&b._makeResizable(),b._createButtons(d.buttons),b._isOpen=!1,a.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;return a.overlay&&a.overlay.destroy(),a.uiDialog.hide(),a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),a.uiDialog.remove(),a.originalTitle&&a.element.attr("title",a.originalTitle),a},widget:function(){return this.uiDialog},close:function(b){var c=this,d,e;if(!1===c._trigger("beforeClose",b))return;return c.overlay&&c.overlay.destroy(),c.uiDialog.unbind("keypress.ui-dialog"),c._isOpen=!1,c.options.hide?c.uiDialog.hide(c.options.hide,function(){c._trigger("close",b)}):(c.uiDialog.hide(),c._trigger("close",b)),a.ui.dialog.overlay.resize(),c.options.modal&&(d=0,a(".ui-dialog").each(function(){this!==c.uiDialog[0]&&(e=a(this).css("z-index"),isNaN(e)||(d=Math.max(d,e)))}),a.ui.dialog.maxZ=d),c},isOpen:function(){return this._isOpen},moveToTop:function(b,c){var d=this,e=d.options,f;return e.modal&&!b||!e.stack&&!e.modal?d._trigger("focus",c):(e.zIndex>a.ui.dialog.maxZ&&(a.ui.dialog.maxZ=e.zIndex),d.overlay&&(a.ui.dialog.maxZ+=1,d.overlay.$el.css("z-index",a.ui.dialog.overlay.maxZ=a.ui.dialog.maxZ)),f={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()},a.ui.dialog.maxZ+=1,d.uiDialog.css("z-index",a.ui.dialog.maxZ),d.element.attr(f),d._trigger("focus",c),d)},open:function(){if(this._isOpen)return;var b=this,c=b.options,d=b.uiDialog;return b.overlay=c.modal?new a.ui.dialog.overlay(b):null,b._size(),b._position(c.position),d.show(c.show),b.moveToTop(!0),c.modal&&d.bind("keydown.ui-dialog",function(b){if(b.keyCode!==a.ui.keyCode.TAB)return;var c=a(":tabbable",this),d=c.filter(":first"),e=c.filter(":last");if(b.target===e[0]&&!b.shiftKey)return d.focus(1),!1;if(b.target===d[0]&&b.shiftKey)return e.focus(1),!1}),a(b.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus(),b._isOpen=!0,b._trigger("open"),b},_createButtons:function(b){var c=this,d=!1,e=a("<div></div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),f=a("<div></div>").addClass("ui-dialog-buttonset").appendTo(e);c.uiDialog.find(".ui-dialog-buttonpane").remove(),typeof b=="object"&&b!==null&&a.each(b,function(){return!(d=!0)}),d&&(a.each(b,function(b,d){d=a.isFunction(d)?{click:d,text:b}:d;var e=a('<button type="button"></button>').click(function(){d.click.apply(c.element[0],arguments)}).appendTo(f);a.each(d,function(a,b){if(a==="click")return;a in e?e[a](b):e.attr(a,b)}),a.fn.button&&e.button()}),e.appendTo(c.uiDialog))},_makeDraggable:function(){function f(a){return{position:a.position,offset:a.offset}}var b=this,c=b.options,d=a(document),e;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(d,g){e=c.height==="auto"?"auto":a(this).height(),a(this).height(a(this).height()).addClass("ui-dialog-dragging"),b._trigger("dragStart",d,f(g))},drag:function(a,c){b._trigger("drag",a,f(c))},stop:function(g,h){c.position=[h.position.left-d.scrollLeft(),h.position.top-d.scrollTop()],a(this).removeClass("ui-dialog-dragging").height(e),b._trigger("dragStop",g,f(h)),a.ui.dialog.overlay.resize()}})},_makeResizable:function(c){function h(a){return{originalPosition:a.originalPosition,originalSize:a.originalSize,position:a.position,size:a.size}}c=c===b?this.options.resizable:c;var d=this,e=d.options,f=d.uiDialog.css("position"),g=typeof c=="string"?c:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:g,start:function(b,c){a(this).addClass("ui-dialog-resizing"),d._trigger("resizeStart",b,h(c))},resize:function(a,b){d._trigger("resize",a,h(b))},stop:function(b,c){a(this).removeClass("ui-dialog-resizing"),e.height=a(this).height(),e.width=a(this).width(),d._trigger("resizeStop",b,h(c)),a.ui.dialog.overlay.resize()}}).css("position",f).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(b){var c=[],d=[0,0],e;if(b){if(typeof b=="string"||typeof b=="object"&&"0"in b)c=b.split?b.split(" "):[b[0],b[1]],c.length===1&&(c[1]=c[0]),a.each(["left","top"],function(a,b){+c[a]===c[a]&&(d[a]=c[a],c[a]=b)}),b={my:c.join(" "),at:c.join(" "),offset:d.join(" ")};b=a.extend({},a.ui.dialog.prototype.options.position,b)}else b=a.ui.dialog.prototype.options.position;e=this.uiDialog.is(":visible"),e||this.uiDialog.show(),this.uiDialog.css({top:0,left:0}).position(a.extend({of:window},b)),e||this.uiDialog.hide()},_setOptions:function(b){var c=this,f={},g=!1;a.each(b,function(a,b){c._setOption(a,b),a in d&&(g=!0),a in e&&(f[a]=b)}),g&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",f)},_setOption:function(b,d){var e=this,f=e.uiDialog;switch(b){case"beforeclose":b="beforeClose";break;case"buttons":e._createButtons(d);break;case"closeText":e.uiDialogTitlebarCloseText.text(""+d);break;case"dialogClass":f.removeClass(e.options.dialogClass).addClass(c+d);break;case"disabled":d?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case"draggable":var g=f.is(":data(draggable)");g&&!d&&f.draggable("destroy"),!g&&d&&e._makeDraggable();break;case"position":e._position(d);break;case"resizable":var h=f.is(":data(resizable)");h&&!d&&f.resizable("destroy"),h&&typeof d=="string"&&f.resizable("option","handles",d),!h&&d!==!1&&e._makeResizable(d);break;case"title":a(".ui-dialog-title",e.uiDialogTitlebar).html(""+(d||" "))}a.Widget.prototype._setOption.apply(e,arguments)},_size:function(){var b=this.options,c,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),b.minWidth>b.width&&(b.width=b.minWidth),c=this.uiDialog.css({height:"auto",width:b.width}).height(),d=Math.max(0,b.minHeight-c);if(b.height==="auto")if(a.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();var f=this.element.css("height","auto").height();e||this.uiDialog.hide(),this.element.height(Math.max(f,d))}else this.element.height(Math.max(b.height-c,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),a.extend(a.ui.dialog,{version:"1.8.23",uuid:0,maxZ:0,getTitleId:function(a){var b=a.attr("id");return b||(this.uuid+=1,b=this.uuid),"ui-dialog-title-"+b},overlay:function(b){this.$el=a.ui.dialog.overlay.create(b)}}),a.extend(a.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:a.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "),create:function(b){this.instances.length===0&&(setTimeout(function(){a.ui.dialog.overlay.instances.length&&a(document).bind(a.ui.dialog.overlay.events,function(b){if(a(b.target).zIndex()<a.ui.dialog.overlay.maxZ)return!1})},1),a(document).bind("keydown.dialog-overlay",function(c){b.options.closeOnEscape&&!c.isDefaultPrevented()&&c.keyCode&&c.keyCode===a.ui.keyCode.ESCAPE&&(b.close(c),c.preventDefault())}),a(window).bind("resize.dialog-overlay",a.ui.dialog.overlay.resize));var c=(this.oldInstances.pop()||a("<div></div>").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});return a.fn.bgiframe&&c.bgiframe(),this.instances.push(c),c},destroy:function(b){var c=a.inArray(b,this.instances);c!=-1&&this.oldInstances.push(this.instances.splice(c,1)[0]),this.instances.length===0&&a([document,window]).unbind(".dialog-overlay"),b.remove();var d=0;a.each(this.instances,function(){d=Math.max(d,this.css("z-index"))}),this.maxZ=d},height:function(){var b,c;return a.browser.msie&&a.browser.version<7?(b=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),c=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight),b<c?a(window).height()+"px":b+"px"):a(document).height()+"px"},width:function(){var b,c;return a.browser.msie?(b=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth),c=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth),b<c?a(window).width()+"px":b+"px"):a(document).width()+"px"},resize:function(){var b=a([]);a.each(a.ui.dialog.overlay.instances,function(){b=b.add(this)}),b.css({width:0,height:0}).css({width:a.ui.dialog.overlay.width(),height:a.ui.dialog.overlay.height()})}}),a.extend(a.ui.dialog.overlay.prototype,{destroy:function(){a.ui.dialog.overlay.destroy(this.$el)}})}(jQuery),function(a,b){a.ui=a.ui||{};var c=/left|center|right/,d=/top|center|bottom/,e="center",f={},g=a.fn.position,h=a.fn.offset;a.fn.position=function(b){if(!b||!b.of)return g.apply(this,arguments);b=a.extend({},b);var h=a(b.of),i=h[0],j=(b.collision||"flip").split(" "),k=b.offset?b.offset.split(" "):[0,0],l,m,n;return i.nodeType===9?(l=h.width(),m=h.height(),n={top:0,left:0}):i.setTimeout?(l=h.width(),m=h.height(),n={top:h.scrollTop(),left:h.scrollLeft()}):i.preventDefault?(b.at="left top",l=m=0,n={top:b.of.pageY,left:b.of.pageX}):(l=h.outerWidth(),m=h.outerHeight(),n=h.offset()),a.each(["my","at"],function(){var a=(b[this]||"").split(" ");a.length===1&&(a=c.test(a[0])?a.concat([e]):d.test(a[0])?[e].concat(a):[e,e]),a[0]=c.test(a[0])?a[0]:e,a[1]=d.test(a[1])?a[1]:e,b[this]=a}),j.length===1&&(j[1]=j[0]),k[0]=parseInt(k[0],10)||0,k.length===1&&(k[1]=k[0]),k[1]=parseInt(k[1],10)||0,b.at[0]==="right"?n.left+=l:b.at[0]===e&&(n.left+=l/2),b.at[1]==="bottom"?n.top+=m:b.at[1]===e&&(n.top+=m/2),n.left+=k[0],n.top+=k[1],this.each(function(){var c=a(this),d=c.outerWidth(),g=c.outerHeight(),h=parseInt(a.curCSS(this,"marginLeft",!0))||0,i=parseInt(a.curCSS(this,"marginTop",!0))||0,o=d+h+(parseInt(a.curCSS(this,"marginRight",!0))||0),p=g+i+(parseInt(a.curCSS(this,"marginBottom",!0))||0),q=a.extend({},n),r;b.my[0]==="right"?q.left-=d:b.my[0]===e&&(q.left-=d/2),b.my[1]==="bottom"?q.top-=g:b.my[1]===e&&(q.top-=g/2),f.fractions||(q.left=Math.round(q.left),q.top=Math.round(q.top)),r={left:q.left-h,top:q.top-i},a.each(["left","top"],function(c,e){a.ui.position[j[c]]&&a.ui.position[j[c]][e](q,{targetWidth:l,targetHeight:m,elemWidth:d,elemHeight:g,collisionPosition:r,collisionWidth:o,collisionHeight:p,offset:k,my:b.my,at:b.at})}),a.fn.bgiframe&&c.bgiframe(),c.offset(a.extend(q,{using:b.using}))})},a.ui.position={fit:{left:function(b,c){var d=a(window),e=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft();b.left=e>0?b.left-e:Math.max(b.left-c.collisionPosition.left,b.left)},top:function(b,c){var d=a(window),e=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop();b.top=e>0?b.top-e:Math.max(b.top-c.collisionPosition.top,b.top)}},flip:{left:function(b,c){if(c.at[0]===e)return;var d=a(window),f=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft(),g=c.my[0]==="left"?-c.elemWidth:c.my[0]==="right"?c.elemWidth:0,h=c.at[0]==="left"?c.targetWidth:-c.targetWidth,i=-2*c.offset[0];b.left+=c.collisionPosition.left<0?g+h+i:f>0?g+h+i:0},top:function(b,c){if(c.at[1]===e)return;var d=a(window),f=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop(),g=c.my[1]==="top"?-c.elemHeight:c.my[1]==="bottom"?c.elemHeight:0,h=c.at[1]==="top"?c.targetHeight:-c.targetHeight,i=-2*c.offset[1];b.top+=c.collisionPosition.top<0?g+h+i:f>0?g+h+i:0}}},a.offset.setOffset||(a.offset.setOffset=function(b,c){/static/.test(a.curCSS(b,"position"))&&(b.style.position="relative");var d=a(b),e=d.offset(),f=parseInt(a.curCSS(b,"top",!0),10)||0,g=parseInt(a.curCSS(b,"left",!0),10)||0,h={top:c.top-e.top+f,left:c.left-e.left+g};"using"in c?c.using.call(b,h):d.css(h)},a.fn.offset=function(b){var c=this[0];return!c||!c.ownerDocument?null:b?a.isFunction(b)?this.each(function(c){a(this).offset(b.call(this,c,a(this).offset()))}):this.each(function(){a.offset.setOffset(this,b)}):h.call(this)}),a.curCSS||(a.curCSS=a.css),function(){var b=document.getElementsByTagName("body")[0],c=document.createElement("div"),d,e,g,h,i;d=document.createElement(b?"div":"body"),g={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},b&&a.extend(g,{position:"absolute",left:"-1000px",top:"-1000px"});for(var j in g)d.style[j]=g[j];d.appendChild(c),e=b||document.documentElement,e.insertBefore(d,e.firstChild),c.style.cssText="position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;",h=a(c).offset(function(a,b){return b}).offset(),d.innerHTML="",e.removeChild(d),i=h.top+h.left+(b?2e3:0),f.fractions=i>21&&i<22}()}(jQuery),function(a,b){a.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()}),this.valueDiv=a("<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>").appendTo(this.element),this.oldValue=this._value(),this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove(),a.Widget.prototype.destroy.apply(this,arguments)},value:function(a){return a===b?this._value():(this._setOption("value",a),this)},_setOption:function(b,c){b==="value"&&(this.options.value=c,this._refreshValue(),this._value()===this.options.max&&this._trigger("complete")),a.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;return typeof a!="number"&&(a=0),Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100*this._value()/this.options.max},_refreshValue:function(){var a=this.value(),b=this._percentage();this.oldValue!==a&&(this.oldValue=a,this._trigger("change")),this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(b.toFixed(0)+"%"),this.element.attr("aria-valuenow",a)}}),a.extend(a.ui.progressbar,{version:"1.8.23"})}(jQuery),function(a,b){var c=5;a.widget("ui.slider",a.ui.mouse,{widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null},_create:function(){var b=this,d=this.options,e=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),f="<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>",g=d.values&&d.values.length||1,h=[];this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"+(d.disabled?" ui-slider-disabled ui-disabled":"")),this.range=a([]),d.range&&(d.range===!0&&(d.values||(d.values=[this._valueMin(),this._valueMin()]),d.values.length&&d.values.length!==2&&(d.values=[d.values[0],d.values[0]])),this.range=a("<div></div>").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(d.range==="min"||d.range==="max"?" ui-slider-range-"+d.range:"")));for(var i=e.length;i<g;i+=1)h.push(f);this.handles=e.add(a(h.join("")).appendTo(b.element)),this.handle=this.handles.eq(0),this.handles.add(this.range).filter("a").click(function(a){a.preventDefault()}).hover(function(){d.disabled||a(this).addClass("ui-state-hover")},function(){a(this).removeClass("ui-state-hover")}).focus(function(){d.disabled?a(this).blur():(a(".ui-slider .ui-state-focus").removeClass("ui-state-focus"),a(this).addClass("ui-state-focus"))}).blur(function(){a(this).removeClass("ui-state-focus")}),this.handles.each(function(b){a(this).data("index.ui-slider-handle",b)}),this.handles.keydown(function(d){var e=a(this).data("index.ui-slider-handle"),f,g,h,i;if(b.options.disabled)return;switch(d.keyCode){case a.ui.keyCode.HOME:case a.ui.keyCode.END:case a.ui.keyCode.PAGE_UP:case a.ui.keyCode.PAGE_DOWN:case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:d.preventDefault();if(!b._keySliding){b._keySliding=!0,a(this).addClass("ui-state-active"),f=b._start(d,e);if(f===!1)return}}i=b.options.step,b.options.values&&b.options.values.length?g=h=b.values(e):g=h=b.value();switch(d.keyCode){case a.ui.keyCode.HOME:h=b._valueMin();break;case a.ui.keyCode.END:h=b._valueMax();break;case a.ui.keyCode.PAGE_UP:h=b._trimAlignValue(g+(b._valueMax()-b._valueMin())/c);break;case a.ui.keyCode.PAGE_DOWN:h=b._trimAlignValue(g-(b._valueMax()-b._valueMin())/c);break;case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:if(g===b._valueMax())return;h=b._trimAlignValue(g+i);break;case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:if(g===b._valueMin())return;h=b._trimAlignValue(g-i)}b._slide(d,e,h)}).keyup(function(c){var d=a(this).data("index.ui-slider-handle");b._keySliding&&(b._keySliding=!1,b._stop(c,d),b._change(c,d),a(this).removeClass("ui-state-active"))}),this._refreshValue(),this._animateOff=!1},destroy:function(){return this.handles.remove(),this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"),this._mouseDestroy(),this},_mouseCapture:function(b){var c=this.options,d,e,f,g,h,i,j,k,l;return c.disabled?!1:(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),d={x:b.pageX,y:b.pageY},e=this._normValueFromMouse(d),f=this._valueMax()-this._valueMin()+1,h=this,this.handles.each(function(b){var c=Math.abs(e-h.values(b));f>c&&(f=c,g=a(this),i=b)}),c.range===!0&&this.values(1)===c.min&&(i+=1,g=a(this.handles[i])),j=this._start(b,i),j===!1?!1:(this._mouseSliding=!0,h._handleIndex=i,g.addClass("ui-state-active").focus(),k=g.offset(),l=!a(b.target).parents().andSelf().is(".ui-slider-handle"),this._clickOffset=l?{left:0,top:0}:{left:b.pageX-k.left-g.width()/2,top:b.pageY-k.top-g.height()/2-(parseInt(g.css("borderTopWidth"),10)||0)-(parseInt(g.css("borderBottomWidth"),10)||0)+(parseInt(g.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(b,i,e),this._animateOff=!0,!0))},_mouseStart:function(a){return!0},_mouseDrag:function(a){var b={x:a.pageX,y:a.pageY},c=this._normValueFromMouse(b);return this._slide(a,this._handleIndex,c),!1},_mouseStop:function(a){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(a,this._handleIndex),this._change(a,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b,c,d,e,f;return this.orientation==="horizontal"?(b=this.elementSize.width,c=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(b=this.elementSize.height,c=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),d=c/b,d>1&&(d=1),d<0&&(d=0),this.orientation==="vertical"&&(d=1-d),e=this._valueMax()-this._valueMin(),f=this._valueMin()+d*e,this._trimAlignValue(f)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};return this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("start",a,c)},_slide:function(a,b,c){var d,e,f;this.options.values&&this.options.values.length?(d=this.values(b?0:1),this.options.values.length===2&&this.options.range===!0&&(b===0&&c>d||b===1&&c<d)&&(c=d),c!==this.values(b)&&(e=this.values(),e[b]=c,f=this._trigger("slide",a,{handle:this.handles[b],value:c,values:e}),d=this.values(b?0:1),f!==!1&&this.values(b,c,!0))):c!==this.value()&&(f=this._trigger("slide",a,{handle:this.handles[b],value:c}),f!==!1&&this.value(c))},_stop:function(a,b){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("stop",a,c)},_change:function(a,b){if(!this._keySliding&&!this._mouseSliding){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("change",a,c)}},value:function(a){if(arguments.length){this.options.value=this._trimAlignValue(a),this._refreshValue(),this._change(null,0);return}return this._value()},values:function(b,c){var d,e,f;if(arguments.length>1){this.options.values[b]=this._trimAlignValue(c),this._refreshValue(),this._change(null,b);return}if(!arguments.length)return this._values();if(!a.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(b):this.value();d=this.options.values,e=arguments[0];for(f=0;f<d.length;f+=1)d[f]=this._trimAlignValue(e[f]),this._change(null,f);this._refreshValue()},_setOption:function(b,c){var d,e=0;a.isArray(this.options.values)&&(e=this.options.values.length),a.Widget.prototype._setOption.apply(this,arguments);switch(b){case"disabled":c?(this.handles.filter(".ui-state-focus").blur(),this.handles.removeClass("ui-state-hover"),this.handles.propAttr("disabled",!0),this.element.addClass("ui-disabled")):(this.handles.propAttr("disabled",!1),this.element.removeClass("ui-disabled"));break;case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":this._animateOff=!0,this._refreshValue();for(d=0;d<e;d+=1)this._change(null,d);this._animateOff=!1}},_value:function(){var a=this.options.value;return a=this._trimAlignValue(a),a},_values:function(a){var b,c,d;if(arguments.length)return b=this.options.values[a],b=this._trimAlignValue(b),b;c=this.options.values.slice();for(d=0;d<c.length;d+=1)c[d]=this._trimAlignValue(c[d]);return c},_trimAlignValue:function(a){if(a<=this._valueMin())return this._valueMin();if(a>=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b,d=a-c;return Math.abs(c)*2>=b&&(d+=c>0?b:-b),parseFloat(d.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var b=this.options.range,c=this.options,d=this,e=this._animateOff?!1:c.animate,f,g={},h,i,j,k;this.options.values&&this.options.values.length?this.handles.each(function(b,i){f=(d.values(b)-d._valueMin())/(d._valueMax()-d._valueMin())*100,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",a(this).stop(1,1)[e?"animate":"css"](g,c.animate),d.options.range===!0&&(d.orientation==="horizontal"?(b===0&&d.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({width:f-h+"%"},{queue:!1,duration:c.animate})):(b===0&&d.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({height:f-h+"%"},{queue:!1,duration:c.animate}))),h=f}):(i=this.value(),j=this._valueMin(),k=this._valueMax(),f=k!==j?(i-j)/(k-j)*100:0,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",this.handle.stop(1,1)[e?"animate":"css"](g,c.animate),b==="min"&&this.orientation==="horizontal"&&this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},c.animate),b==="max"&&this.orientation==="horizontal"&&this.range[e?"animate":"css"]({width:100-f+"%"},{queue:!1,duration:c.animate}),b==="min"&&this.orientation==="vertical"&&this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},c.animate),b==="max"&&this.orientation==="vertical"&&this.range[e?"animate":"css"]({height:100-f+"%"},{queue:!1,duration:c.animate}))}}),a.extend(a.ui.slider,{version:"1.8.23"})}(jQuery),function(a,b){function e(){return++c}function f(){return++d}var c=0,d=0;a.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:!1,cookie:null,collapsible:!1,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"<div></div>",remove:null,select:null,show:null,spinner:"<em>Loading…</em>",tabTemplate:"<li><a href='#{href}'><span>#{label}</span></a></li>"},_create:function(){this._tabify(!0)},_setOption:function(a,b){if(a=="selected"){if(this.options.collapsible&&b==this.options.selected)return;this.select(b)}else this.options[a]=b,this._tabify()},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+e()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+f());return a.cookie.apply(null,[b].concat(a.makeArray(arguments)))},_ui:function(a,b){return{tab:a,panel:b,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b=a(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(c){function m(b,c){b.css("display",""),!a.support.opacity&&c.opacity&&b[0].style.removeAttribute("filter")}var d=this,e=this.options,f=/^#.+/;this.list=this.element.find("ol,ul").eq(0),this.lis=a(" > li:has(a[href])",this.list),this.anchors=this.lis.map(function(){return a("a",this)[0]}),this.panels=a([]),this.anchors.each(function(b,c){var g=a(c).attr("href"),h=g.split("#")[0],i;h&&(h===location.toString().split("#")[0]||(i=a("base")[0])&&h===i.href)&&(g=c.hash,c.href=g);if(f.test(g))d.panels=d.panels.add(d.element.find(d._sanitizeSelector(g)));else if(g&&g!=="#"){a.data(c,"href.tabs",g),a.data(c,"load.tabs",g.replace(/#.*$/,""));var j=d._tabId(c);c.href="#"+j;var k=d.element.find("#"+j);k.length||(k=a(e.panelTemplate).attr("id",j).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(d.panels[b-1]||d.list),k.data("destroy.tabs",!0)),d.panels=d.panels.add(k)}else e.disabled.push(b)}),c?(this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"),this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.lis.addClass("ui-state-default ui-corner-top"),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom"),e.selected===b?(location.hash&&this.anchors.each(function(a,b){if(b.hash==location.hash)return e.selected=a,!1}),typeof e.selected!="number"&&e.cookie&&(e.selected=parseInt(d._cookie(),10)),typeof e.selected!="number"&&this.lis.filter(".ui-tabs-selected").length&&(e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"))),e.selected=e.selected||(this.lis.length?0:-1)):e.selected===null&&(e.selected=-1),e.selected=e.selected>=0&&this.anchors[e.selected]||e.selected<0?e.selected:0,e.disabled=a.unique(e.disabled.concat(a.map(this.lis.filter(".ui-state-disabled"),function(a,b){return d.lis.index(a)}))).sort(),a.inArray(e.selected,e.disabled)!=-1&&e.disabled.splice(a.inArray(e.selected,e.disabled),1),this.panels.addClass("ui-tabs-hide"),this.lis.removeClass("ui-tabs-selected ui-state-active"),e.selected>=0&&this.anchors.length&&(d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash)).removeClass("ui-tabs-hide"),this.lis.eq(e.selected).addClass("ui-tabs-selected ui-state-active"),d.element.queue("tabs",function(){d._trigger("show",null,d._ui(d.anchors[e.selected],d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash))[0]))}),this.load(e.selected)),a(window).bind("unload",function(){d.lis.add(d.anchors).unbind(".tabs"),d.lis=d.anchors=d.panels=null})):e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected")),this.element[e.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible"),e.cookie&&this._cookie(e.selected,e.cookie);for(var g=0,h;h=this.lis[g];g++)a(h)[a.inArray(g,e.disabled)!=-1&&!a(h).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");e.cache===!1&&this.anchors.removeData("cache.tabs"),this.lis.add(this.anchors).unbind(".tabs");if(e.event!=="mouseover"){var i=function(a,b){b.is(":not(.ui-state-disabled)")&&b.addClass("ui-state-"+a)},j=function(a,b){b.removeClass("ui-state-"+a)};this.lis.bind("mouseover.tabs",function(){i("hover",a(this))}),this.lis.bind("mouseout.tabs",function(){j("hover",a(this))}),this.anchors.bind("focus.tabs",function(){i("focus",a(this).closest("li"))}),this.anchors.bind("blur.tabs",function(){j("focus",a(this).closest("li"))})}var k,l;e.fx&&(a.isArray(e.fx)?(k=e.fx[0],l=e.fx[1]):k=l=e.fx);var n=l?function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.hide().removeClass("ui-tabs-hide").animate(l,l.duration||"normal",function(){m(c,l),d._trigger("show",null,d._ui(b,c[0]))})}:function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.removeClass("ui-tabs-hide"),d._trigger("show",null,d._ui(b,c[0]))},o=k?function(a,b){b.animate(k,k.duration||"normal",function(){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),m(b,k),d.element.dequeue("tabs")})}:function(a,b,c){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),d.element.dequeue("tabs")};this.anchors.bind(e.event+".tabs",function(){var b=this,c=a(b).closest("li"),f=d.panels.filter(":not(.ui-tabs-hide)"),g=d.element.find(d._sanitizeSelector(b.hash));if(c.hasClass("ui-tabs-selected")&&!e.collapsible||c.hasClass("ui-state-disabled")||c.hasClass("ui-state-processing")||d.panels.filter(":animated").length||d._trigger("select",null,d._ui(this,g[0]))===!1)return this.blur(),!1;e.selected=d.anchors.index(this),d.abort();if(e.collapsible){if(c.hasClass("ui-tabs-selected"))return e.selected=-1,e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){o(b,f)}).dequeue("tabs"),this.blur(),!1;if(!f.length)return e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this)),this.blur(),!1}e.cookie&&d._cookie(e.selected,e.cookie);if(g.length)f.length&&d.element.queue("tabs",function(){o(b,f)}),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this));else throw"jQuery UI Tabs: Mismatching fragment identifier.";a.browser.msie&&this.blur()}),this.anchors.bind("click.tabs",function(){return!1})},_getIndex:function(a){return typeof a=="string"&&(a=this.anchors.index(this.anchors.filter("[href$='"+a+"']"))),a},destroy:function(){var b=this.options;return this.abort(),this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs"),this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.anchors.each(function(){var b=a.data(this,"href.tabs");b&&(this.href=b);var c=a(this).unbind(".tabs");a.each(["href","load","cache"],function(a,b){c.removeData(b+".tabs")})}),this.lis.unbind(".tabs").add(this.panels).each(function(){a.data(this,"destroy.tabs")?a(this).remove():a(this).removeClass(["ui-state-default","ui-corner-top","ui-tabs-selected","ui-state-active","ui-state-hover","ui-state-focus","ui-state-disabled","ui-tabs-panel","ui-widget-content","ui-corner-bottom","ui-tabs-hide"].join(" "))}),b.cookie&&this._cookie(null,b.cookie),this},add:function(c,d,e){e===b&&(e=this.anchors.length);var f=this,g=this.options,h=a(g.tabTemplate.replace(/#\{href\}/g,c).replace(/#\{label\}/g,d)),i=c.indexOf("#")?this._tabId(a("a",h)[0]):c.replace("#","");h.addClass("ui-state-default ui-corner-top").data("destroy.tabs",!0);var j=f.element.find("#"+i);return j.length||(j=a(g.panelTemplate).attr("id",i).data("destroy.tabs",!0)),j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide"),e>=this.lis.length?(h.appendTo(this.list),j.appendTo(this.list[0].parentNode)):(h.insertBefore(this.lis[e]),j.insertBefore(this.panels[e])),g.disabled=a.map(g.disabled,function(a,b){return a>=e?++a:a}),this._tabify(),this.anchors.length==1&&(g.selected=0,h.addClass("ui-tabs-selected ui-state-active"),j.removeClass("ui-tabs-hide"),this.element.queue("tabs",function(){f._trigger("show",null,f._ui(f.anchors[0],f.panels[0]))}),this.load(0)),this._trigger("add",null,this._ui(this.anchors[e],this.panels[e])),this},remove:function(b){b=this._getIndex(b);var c=this.options,d=this.lis.eq(b).remove(),e=this.panels.eq(b).remove();return d.hasClass("ui-tabs-selected")&&this.anchors.length>1&&this.select(b+(b+1<this.anchors.length?1:-1)),c.disabled=a.map(a.grep(c.disabled,function(a,c){return a!=b}),function(a,c){return a>=b?--a:a}),this._tabify(),this._trigger("remove",null,this._ui(d.find("a")[0],e[0])),this},enable:function(b){b=this._getIndex(b);var c=this.options;if(a.inArray(b,c.disabled)==-1)return;return this.lis.eq(b).removeClass("ui-state-disabled"),c.disabled=a.grep(c.disabled,function(a,c){return a!=b}),this._trigger("enable",null,this._ui(this.anchors[b],this.panels[b])),this},disable:function(a){a=this._getIndex(a);var b=this,c=this.options;return a!=c.selected&&(this.lis.eq(a).addClass("ui-state-disabled"),c.disabled.push(a),c.disabled.sort(),this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a]))),this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;return this.anchors.eq(a).trigger(this.options.event+".tabs"),this},load:function(b){b=this._getIndex(b);var c=this,d=this.options,e=this.anchors.eq(b)[0],f=a.data(e,"load.tabs");this.abort();if(!f||this.element.queue("tabs").length!==0&&a.data(e,"cache.tabs")){this.element.dequeue("tabs");return}this.lis.eq(b).addClass("ui-state-processing");if(d.spinner){var g=a("span",e);g.data("label.tabs",g.html()).html(d.spinner)}return this.xhr=a.ajax(a.extend({},d.ajaxOptions,{url:f,success:function(f,g){c.element.find(c._sanitizeSelector(e.hash)).html(f),c._cleanup(),d.cache&&a.data(e,"cache.tabs",!0),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.success(f,g)}catch(h){}},error:function(a,f,g){c._cleanup(),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.error(a,f,b,e)}catch(g){}}})),c.element.dequeue("tabs"),this},abort:function(){return this.element.queue([]),this.panels.stop(!1,!0),this.element.queue("tabs",this.element.queue("tabs").splice(-2,2)),this.xhr&&(this.xhr.abort(),delete this.xhr),this._cleanup(),this},url:function(a,b){return this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",b),this},length:function(){return this.anchors.length}}),a.extend(a.ui.tabs,{version:"1.8.23"}),a.extend(a.ui.tabs.prototype,{rotation:null,rotate:function(a,b){var c=this,d=this.options,e=c._rotate||(c._rotate=function(b){clearTimeout(c.rotation),c.rotation=setTimeout(function(){var a=d.selected;c.select(++a<c.anchors.length?a:0)},a),b&&b.stopPropagation()}),f=c._unrotate||(c._unrotate=b?function(a){e()}:function(a){a.clientX&&c.rotate(null)});return a?(this.element.bind("tabsshow",e),this.anchors.bind(d.event+".tabs",f),e()):(clearTimeout(c.rotation),this.element.unbind("tabsshow",e),this.anchors.unbind(d.event+".tabs",f),delete this._rotate,delete this._unrotate),this}})}(jQuery); diff --git a/record-and-playback/presentation_export/playback/presentation_export/lib/jquery.min.js b/record-and-playback/presentation_export/playback/presentation_export/lib/jquery.min.js new file mode 100644 index 0000000000000000000000000000000000000000..673769cbb77c0a6f7d45b9624841365edf8f4b6e --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/lib/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v1.8.2 jquery.com | jquery.org/license */ +(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d<e;d++)p.event.add(b,c,h[c][d])}g.data&&(g.data=p.extend({},g.data))}function bE(a,b){var c;if(b.nodeType!==1)return;b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?(b.parentNode&&(b.outerHTML=a.outerHTML),p.support.html5Clone&&a.innerHTML&&!p.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):c==="input"&&bv.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text),b.removeAttribute(p.expando)}function bF(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bG(a){bv.test(a.type)&&(a.defaultChecked=a.checked)}function bY(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=bW.length;while(e--){b=bW[e]+c;if(b in a)return b}return d}function bZ(a,b){return a=b||a,p.css(a,"display")==="none"||!p.contains(a.ownerDocument,a)}function b$(a,b){var c,d,e=[],f=0,g=a.length;for(;f<g;f++){c=a[f];if(!c.style)continue;e[f]=p._data(c,"olddisplay"),b?(!e[f]&&c.style.display==="none"&&(c.style.display=""),c.style.display===""&&bZ(c)&&(e[f]=p._data(c,"olddisplay",cc(c.nodeName)))):(d=bH(c,"display"),!e[f]&&d!=="none"&&p._data(c,"olddisplay",d))}for(f=0;f<g;f++){c=a[f];if(!c.style)continue;if(!b||c.style.display==="none"||c.style.display==="")c.style.display=b?e[f]||"":"none"}return a}function b_(a,b,c){var d=bP.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function ca(a,b,c,d){var e=c===(d?"border":"content")?4:b==="width"?1:0,f=0;for(;e<4;e+=2)c==="margin"&&(f+=p.css(a,c+bV[e],!0)),d?(c==="content"&&(f-=parseFloat(bH(a,"padding"+bV[e]))||0),c!=="margin"&&(f-=parseFloat(bH(a,"border"+bV[e]+"Width"))||0)):(f+=parseFloat(bH(a,"padding"+bV[e]))||0,c!=="padding"&&(f+=parseFloat(bH(a,"border"+bV[e]+"Width"))||0));return f}function cb(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=!0,f=p.support.boxSizing&&p.css(a,"boxSizing")==="border-box";if(d<=0||d==null){d=bH(a,b);if(d<0||d==null)d=a.style[b];if(bQ.test(d))return d;e=f&&(p.support.boxSizingReliable||d===a.style[b]),d=parseFloat(d)||0}return d+ca(a,b,c||(f?"border":"content"),e)+"px"}function cc(a){if(bS[a])return bS[a];var b=p("<"+a+">").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write("<!doctype html><html><body>"),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h<i;h++)d=g[h],f=/^\+/.test(d),f&&(d=d.substr(1)||"*"),e=a[d]=a[d]||[],e[f?"unshift":"push"](c)}}function cA(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h,i=a[f],j=0,k=i?i.length:0,l=a===cv;for(;j<k&&(l||!h);j++)h=i[j](c,d,e),typeof h=="string"&&(!l||g[h]?h=b:(c.dataTypes.unshift(h),h=cA(a,c,d,e,h,g)));return(l||!h)&&!g["*"]&&(h=cA(a,c,d,e,"*",g)),h}function cB(a,c){var d,e,f=p.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((f[d]?a:e||(e={}))[d]=c[d]);e&&p.extend(!0,a,e)}function cC(a,c,d){var e,f,g,h,i=a.contents,j=a.dataTypes,k=a.responseFields;for(f in k)f in d&&(c[k[f]]=d[f]);while(j[0]==="*")j.shift(),e===b&&(e=a.mimeType||c.getResponseHeader("content-type"));if(e)for(f in i)if(i[f]&&i[f].test(e)){j.unshift(f);break}if(j[0]in d)g=j[0];else{for(f in d){if(!j[0]||a.converters[f+" "+j[0]]){g=f;break}h||(h=f)}g=g||h}if(g)return g!==j[0]&&j.unshift(g),d[g]}function cD(a,b){var c,d,e,f,g=a.dataTypes.slice(),h=g[0],i={},j=0;a.dataFilter&&(b=a.dataFilter(b,a.dataType));if(g[1])for(c in a.converters)i[c.toLowerCase()]=a.converters[c];for(;e=g[++j];)if(e!=="*"){if(h!=="*"&&h!==e){c=i[h+" "+e]||i["* "+e];if(!c)for(d in i){f=d.split(" ");if(f[1]===e){c=i[h+" "+f[0]]||i["* "+f[0]];if(c){c===!0?c=i[d]:i[d]!==!0&&(e=f[0],g.splice(j--,0,e));break}}}if(c!==!0)if(c&&a["throws"])b=c(b);else try{b=c(b)}catch(k){return{state:"parsererror",error:c?k:"No conversion from "+h+" to "+e}}}h=e}return{state:"success",data:b}}function cL(){try{return new a.XMLHttpRequest}catch(b){}}function cM(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cU(){return setTimeout(function(){cN=b},0),cN=p.now()}function cV(a,b){p.each(b,function(b,c){var d=(cT[b]||[]).concat(cT["*"]),e=0,f=d.length;for(;e<f;e++)if(d[e].call(a,b,c))return})}function cW(a,b,c){var d,e=0,f=0,g=cS.length,h=p.Deferred().always(function(){delete i.elem}),i=function(){var b=cN||cU(),c=Math.max(0,j.startTime+j.duration-b),d=1-(c/j.duration||0),e=0,f=j.tweens.length;for(;e<f;e++)j.tweens[e].run(d);return h.notifyWith(a,[j,d,c]),d<1&&f?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:p.extend({},b),opts:p.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:cN||cU(),duration:c.duration,tweens:[],createTween:function(b,c,d){var e=p.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(e),e},stop:function(b){var c=0,d=b?j.tweens.length:0;for(;c<d;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;cX(k,j.opts.specialEasing);for(;e<g;e++){d=cS[e].call(j,a,k,j.opts);if(d)return d}return cV(j,k),p.isFunction(j.opts.start)&&j.opts.start.call(a,j),p.fx.timer(p.extend(i,{anim:j,queue:j.opts.queue,elem:a})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}function cX(a,b){var c,d,e,f,g;for(c in a){d=p.camelCase(c),e=b[d],f=a[c],p.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=p.cssHooks[d];if(g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}}function cY(a,b,c){var d,e,f,g,h,i,j,k,l=this,m=a.style,n={},o=[],q=a.nodeType&&bZ(a);c.queue||(j=p._queueHooks(a,"fx"),j.unqueued==null&&(j.unqueued=0,k=j.empty.fire,j.empty.fire=function(){j.unqueued||k()}),j.unqueued++,l.always(function(){l.always(function(){j.unqueued--,p.queue(a,"fx").length||j.empty.fire()})})),a.nodeType===1&&("height"in b||"width"in b)&&(c.overflow=[m.overflow,m.overflowX,m.overflowY],p.css(a,"display")==="inline"&&p.css(a,"float")==="none"&&(!p.support.inlineBlockNeedsLayout||cc(a.nodeName)==="inline"?m.display="inline-block":m.zoom=1)),c.overflow&&(m.overflow="hidden",p.support.shrinkWrapBlocks||l.done(function(){m.overflow=c.overflow[0],m.overflowX=c.overflow[1],m.overflowY=c.overflow[2]}));for(d in b){f=b[d];if(cP.exec(f)){delete b[d];if(f===(q?"hide":"show"))continue;o.push(d)}}g=o.length;if(g){h=p._data(a,"fxshow")||p._data(a,"fxshow",{}),q?p(a).show():l.done(function(){p(a).hide()}),l.done(function(){var b;p.removeData(a,"fxshow",!0);for(b in n)p.style(a,b,n[b])});for(d=0;d<g;d++)e=o[d],i=l.createTween(e,q?h[e]:0),n[e]=h[e]||p.style(a,e),e in h||(h[e]=i.start,q&&(i.end=i.start,i.start=e==="width"||e==="height"?1:0))}}function cZ(a,b,c,d,e){return new cZ.prototype.init(a,b,c,d,e)}function c$(a,b){var c,d={height:a},e=0;b=b?1:0;for(;e<4;e+=2-b)c=bV[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function da(a){return p.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}var c,d,e=a.document,f=a.location,g=a.navigator,h=a.jQuery,i=a.$,j=Array.prototype.push,k=Array.prototype.slice,l=Array.prototype.indexOf,m=Object.prototype.toString,n=Object.prototype.hasOwnProperty,o=String.prototype.trim,p=function(a,b){return new p.fn.init(a,b,c)},q=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,r=/\S/,s=/\s+/,t=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,u=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.2",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i<j;i++)if((a=arguments[i])!=null)for(c in a){d=h[c],e=a[c];if(h===e)continue;k&&e&&(p.isPlainObject(e)||(f=p.isArray(e)))?(f?(f=!1,g=d&&p.isArray(d)?d:[]):g=d&&p.isPlainObject(d)?d:{},h[c]=p.extend(k,g,e)):e!==b&&(h[c]=e)}return h},p.extend({noConflict:function(b){return a.$===p&&(a.$=i),b&&a.jQuery===p&&(a.jQuery=h),p},isReady:!1,readyWait:1,holdReady:function(a){a?p.readyWait++:p.ready(!0)},ready:function(a){if(a===!0?--p.readyWait:p.isReady)return;if(!e.body)return setTimeout(p.ready,1);p.isReady=!0;if(a!==!0&&--p.readyWait>0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f<g;)if(c.apply(a[f++],d)===!1)break}else if(h){for(e in a)if(c.call(a[e],e,a[e])===!1)break}else for(;f<g;)if(c.call(a[f],f,a[f++])===!1)break;return a},trim:o&&!o.call(". ")?function(a){return a==null?"":o.call(a)}:function(a){return a==null?"":(a+"").replace(t,"")},makeArray:function(a,b){var c,d=b||[];return a!=null&&(c=p.type(a),a.length==null||c==="string"||c==="function"||c==="regexp"||p.isWindow(a)?j.call(d,a):p.merge(d,a)),d},inArray:function(a,b,c){var d;if(b){if(l)return l.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=c.length,e=a.length,f=0;if(typeof d=="number")for(;f<d;f++)a[e++]=c[f];else while(c[f]!==b)a[e++]=c[f++];return a.length=e,a},grep:function(a,b,c){var d,e=[],f=0,g=a.length;c=!!c;for(;f<g;f++)d=!!b(a[f],f),c!==d&&e.push(a[f]);return e},map:function(a,c,d){var e,f,g=[],h=0,i=a.length,j=a instanceof p||i!==b&&typeof i=="number"&&(i>0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h<i;h++)e=c(a[h],h,d),e!=null&&(g[g.length]=e);else for(f in a)e=c(a[f],f,d),e!=null&&(g[g.length]=e);return g.concat.apply([],g)},guid:1,proxy:function(a,c){var d,e,f;return typeof c=="string"&&(d=a[c],c=a,a=d),p.isFunction(a)?(e=k.call(arguments,2),f=function(){return a.apply(c,e.concat(k.call(arguments)))},f.guid=a.guid=a.guid||p.guid++,f):b},access:function(a,c,d,e,f,g,h){var i,j=d==null,k=0,l=a.length;if(d&&typeof d=="object"){for(k in d)p.access(a,c,k,d[k],1,g,e);f=1}else if(e!==b){i=h===b&&p.isFunction(e),j&&(i?(i=c,c=function(a,b,c){return i.call(p(a),c)}):(c.call(a,e),c=null));if(c)for(;k<l;k++)c(a[k],d,i?e.call(a[k],k,c(a[k],d)):e,h);f=1}return f?a:j?c.call(a):l?c(a[0],d):g},now:function(){return(new Date).getTime()}}),p.ready.promise=function(b){if(!d){d=p.Deferred();if(e.readyState==="complete")setTimeout(p.ready,1);else if(e.addEventListener)e.addEventListener("DOMContentLoaded",D,!1),a.addEventListener("load",p.ready,!1);else{e.attachEvent("onreadystatechange",D),a.attachEvent("onload",p.ready);var c=!1;try{c=a.frameElement==null&&e.documentElement}catch(f){}c&&c.doScroll&&function g(){if(!p.isReady){try{c.doScroll("left")}catch(a){return setTimeout(g,50)}p.ready()}}()}}return d.promise(b)},p.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){E["[object "+b+"]"]=b.toLowerCase()}),c=p(e);var F={};p.Callbacks=function(a){a=typeof a=="string"?F[a]||G(a):p.extend({},a);var c,d,e,f,g,h,i=[],j=!a.once&&[],k=function(b){c=a.memory&&b,d=!0,h=f||0,f=0,g=i.length,e=!0;for(;i&&h<g;h++)if(i[h].apply(b[0],b[1])===!1&&a.stopOnFalse){c=!1;break}e=!1,i&&(j?j.length&&k(j.shift()):c?i=[]:l.disable())},l={add:function(){if(i){var b=i.length;(function d(b){p.each(b,function(b,c){var e=p.type(c);e==="function"&&(!a.unique||!l.has(c))?i.push(c):c&&c.length&&e!=="string"&&d(c)})})(arguments),e?g=i.length:c&&(f=b,k(c))}return this},remove:function(){return i&&p.each(arguments,function(a,b){var c;while((c=p.inArray(b,i,c))>-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return a!=null?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b<d;b++)c[b]&&p.isFunction(c[b].promise)?c[b].promise().done(g(b,j,c)).fail(f.reject).progress(g(b,i,h)):--e}return e||f.resolveWith(j,c),f.promise()}}),p.support=function(){var b,c,d,f,g,h,i,j,k,l,m,n=e.createElement("div");n.setAttribute("className","t"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="<div></div>",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||p.guid++:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e<f;e++)delete d[b[e]];if(!(c?K:p.isEmptyObject)(d))return}}if(!c){delete h[i].data;if(!K(h[i]))return}g?p.cleanData([a],!0):p.support.deleteExpando||h!=h.window?delete h[i]:h[i]=null},_data:function(a,b,c){return p.data(a,b,c,!0)},acceptData:function(a){var b=a.nodeName&&p.noData[a.nodeName.toLowerCase()];return!b||b!==!0&&a.getAttribute("classid")===b}}),p.fn.extend({data:function(a,c){var d,e,f,g,h,i=this[0],j=0,k=null;if(a===b){if(this.length){k=p.data(i);if(i.nodeType===1&&!p._data(i,"parsedAttrs")){f=i.attributes;for(h=f.length;j<h;j++)g=f[j].name,g.indexOf("data-")||(g=p.camelCase(g.substring(5)),J(i,g,k[g]));p._data(i,"parsedAttrs",!0)}}return k}return typeof a=="object"?this.each(function(){p.data(this,a)}):(d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!",p.access(this,function(c){if(c===b)return k=this.triggerHandler("getData"+e,[d[0]]),k===b&&i&&(k=p.data(i,a),k=J(i,a,k)),k===b&&d[1]?this.data(d[0]):k;d[1]=c,this.each(function(){var b=p(this);b.triggerHandler("setData"+e,d),p.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length<d?p.queue(this[0],a):c===b?this:this.each(function(){var b=p.queue(this,a,c);p._queueHooks(this,a),a==="fx"&&b[0]!=="inprogress"&&p.dequeue(this,a)})},dequeue:function(a){return this.each(function(){p.dequeue(this,a)})},delay:function(a,b){return a=p.fx?p.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){var d,e=1,f=p.Deferred(),g=this,h=this.length,i=function(){--e||f.resolveWith(g,[g])};typeof a!="string"&&(c=a,a=b),a=a||"fx";while(h--)d=p._data(g[h],a+"queueHooks"),d&&d.empty&&(e++,d.empty.add(i));return i(),f.promise(c)}});var L,M,N,O=/[\t\r\n]/g,P=/\r/g,Q=/^(?:button|input)$/i,R=/^(?:button|input|object|select|textarea)$/i,S=/^a(?:rea|)$/i,T=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,U=p.support.getSetAttribute;p.fn.extend({attr:function(a,b){return p.access(this,p.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{f=" "+e.className+" ";for(g=0,h=b.length;g<h;g++)f.indexOf(" "+b[g]+" ")<0&&(f+=b[g]+" ");e.className=p.trim(f)}}}return this},removeClass:function(a){var c,d,e,f,g,h,i;if(p.isFunction(a))return this.each(function(b){p(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(s);for(h=0,i=this.length;h<i;h++){e=this[h];if(e.nodeType===1&&e.className){d=(" "+e.className+" ").replace(O," ");for(f=0,g=c.length;f<g;f++)while(d.indexOf(" "+c[f]+" ")>=0)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(O," ").indexOf(b)>=0)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c<d;c++){e=h[c];if(e.selected&&(p.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!p.nodeName(e.parentNode,"optgroup"))){b=p(e).val();if(i)return b;g.push(b)}}return i&&!g.length&&h.length?p(h[f]).val():g},set:function(a,b){var c=p.makeArray(b);return p(a).find("option").each(function(){this.selected=p.inArray(p(this).val(),c)>=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,d+""),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g<d.length;g++)e=d[g],e&&(c=p.propFix[e]||e,f=T.test(e),f||p.attr(a,e,""),a.removeAttribute(U?e:c),f&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(Q.test(a.nodeName)&&a.parentNode)p.error("type property can't be changed");else if(!p.support.radioValue&&b==="radio"&&p.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}},value:{get:function(a,b){return L&&p.nodeName(a,"button")?L.get(a,b):b in a?a.value:null},set:function(a,b,c){if(L&&p.nodeName(a,"button"))return L.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,f,g,h=a.nodeType;if(!a||h===3||h===8||h===2)return;return g=h!==1||!p.isXMLDoc(a),g&&(c=p.propFix[c]||c,f=p.propHooks[c]),d!==b?f&&"set"in f&&(e=f.set(a,d,c))!==b?e:a[c]=d:f&&"get"in f&&(e=f.get(a,c))!==null?e:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):R.test(a.nodeName)||S.test(a.nodeName)&&a.href?0:b}}}}),M={get:function(a,c){var d,e=p.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;return b===!1?p.removeAttr(a,c):(d=p.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase())),c}},U||(N={name:!0,id:!0,coords:!0},L=p.valHooks.button={get:function(a,c){var d;return d=a.getAttributeNode(c),d&&(N[c]?d.value!=="":d.specified)?d.value:b},set:function(a,b,c){var d=a.getAttributeNode(c);return d||(d=e.createAttribute(c),a.setAttributeNode(d)),d.value=b+""}},p.each(["width","height"],function(a,b){p.attrHooks[b]=p.extend(p.attrHooks[b],{set:function(a,c){if(c==="")return a.setAttribute(b,"auto"),c}})}),p.attrHooks.contenteditable={get:L.get,set:function(a,b,c){b===""&&(b="false"),L.set(a,b,c)}}),p.support.hrefNormalized||p.each(["href","src","width","height"],function(a,c){p.attrHooks[c]=p.extend(p.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),p.support.style||(p.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=b+""}}),p.support.optSelected||(p.propHooks.selected=p.extend(p.propHooks.selected,{get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}})),p.support.enctype||(p.propFix.enctype="encoding"),p.support.checkOn||p.each(["radio","checkbox"],function(){p.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),p.each(["radio","checkbox"],function(){p.valHooks[this]=p.extend(p.valHooks[this],{set:function(a,b){if(p.isArray(b))return a.checked=p.inArray(p(a).val(),b)>=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j<c.length;j++){k=W.exec(c[j])||[],l=k[1],m=(k[2]||"").split(".").sort(),r=p.event.special[l]||{},l=(f?r.delegateType:r.bindType)||l,r=p.event.special[l]||{},n=p.extend({type:l,origType:k[1],data:e,handler:d,guid:d.guid,selector:f,needsContext:f&&p.expr.match.needsContext.test(f),namespace:m.join(".")},o),q=i[l];if(!q){q=i[l]=[],q.delegateCount=0;if(!r.setup||r.setup.call(a,e,m,h)===!1)a.addEventListener?a.addEventListener(l,h,!1):a.attachEvent&&a.attachEvent("on"+l,h)}r.add&&(r.add.call(a,n),n.handler.guid||(n.handler.guid=d.guid)),f?q.splice(q.delegateCount++,0,n):q.push(n),p.event.global[l]=!0}a=null},global:{},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,q,r=p.hasData(a)&&p._data(a);if(!r||!(m=r.events))return;b=p.trim(_(b||"")).split(" ");for(f=0;f<b.length;f++){g=W.exec(b[f])||[],h=i=g[1],j=g[2];if(!h){for(h in m)p.event.remove(a,h+b[f],c,d,!0);continue}n=p.event.special[h]||{},h=(d?n.delegateType:n.bindType)||h,o=m[h]||[],k=o.length,j=j?new RegExp("(^|\\.)"+j.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(l=0;l<o.length;l++)q=o[l],(e||i===q.origType)&&(!c||c.guid===q.guid)&&(!j||j.test(q.namespace))&&(!d||d===q.selector||d==="**"&&q.selector)&&(o.splice(l--,1),q.selector&&o.delegateCount--,n.remove&&n.remove.call(a,q));o.length===0&&k!==o.length&&((!n.teardown||n.teardown.call(a,j,r.handle)===!1)&&p.removeEvent(a,h,r.handle),delete m[h])}p.isEmptyObject(m)&&(delete r.handle,p.removeData(a,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,f,g){if(!f||f.nodeType!==3&&f.nodeType!==8){var h,i,j,k,l,m,n,o,q,r,s=c.type||c,t=[];if($.test(s+p.event.triggered))return;s.indexOf("!")>=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j<q.length&&!c.isPropagationStopped();j++)k=q[j][0],c.type=q[j][1],o=(p._data(k,"events")||{})[c.type]&&p._data(k,"handle"),o&&o.apply(k,d),o=m&&k[m],o&&p.acceptData(k)&&o.apply&&o.apply(k,d)===!1&&c.preventDefault();return c.type=s,!g&&!c.isDefaultPrevented()&&(!n._default||n._default.apply(f.ownerDocument,d)===!1)&&(s!=="click"||!p.nodeName(f,"a"))&&p.acceptData(f)&&m&&f[s]&&(s!=="focus"&&s!=="blur"||c.target.offsetWidth!==0)&&!p.isWindow(f)&&(l=f[m],l&&(f[m]=null),p.event.triggered=s,f[s](),p.event.triggered=b,l&&(f[m]=l)),c.result}return},dispatch:function(c){c=p.event.fix(c||a.event);var d,e,f,g,h,i,j,l,m,n,o=(p._data(this,"events")||{})[c.type]||[],q=o.delegateCount,r=k.call(arguments),s=!c.exclusive&&!c.namespace,t=p.event.special[c.type]||{},u=[];r[0]=c,c.delegateTarget=this;if(t.preDispatch&&t.preDispatch.call(this,c)===!1)return;if(q&&(!c.button||c.type!=="click"))for(f=c.target;f!=this;f=f.parentNode||this)if(f.disabled!==!0||c.type!=="click"){h={},j=[];for(d=0;d<q;d++)l=o[d],m=l.selector,h[m]===b&&(h[m]=l.needsContext?p(m,this).index(f)>=0:p.find(m,this,null,[f]).length),h[m]&&j.push(l);j.length&&u.push({elem:f,matches:j})}o.length>q&&u.push({elem:this,matches:o.slice(q)});for(d=0;d<u.length&&!c.isPropagationStopped();d++){i=u[d],c.currentTarget=i.elem;for(e=0;e<i.matches.length&&!c.isImmediatePropagationStopped();e++){l=i.matches[e];if(s||!c.namespace&&!l.namespace||c.namespace_re&&c.namespace_re.test(l.namespace))c.data=l.data,c.handleObj=l,g=((p.event.special[l.origType]||{}).handle||l.handler).apply(i.elem,r),g!==b&&(c.result=g,g===!1&&(c.preventDefault(),c.stopPropagation()))}}return t.postDispatch&&t.postDispatch.call(this,c),c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,c){var d,f,g,h=c.button,i=c.fromElement;return a.pageX==null&&c.clientX!=null&&(d=a.target.ownerDocument||e,f=d.documentElement,g=d.body,a.pageX=c.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=c.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?c.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0),a}},fix:function(a){if(a[p.expando])return a;var b,c,d=a,f=p.event.fixHooks[a.type]||{},g=f.props?this.props.concat(f.props):this.props;a=p.Event(d);for(b=g.length;b;)c=g[--b],a[c]=d[c];return a.target||(a.target=d.srcElement||e),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,f.filter?f.filter(a,d):a},special:{load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){p.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=p.extend(new p.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?p.event.trigger(e,null,b):p.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},p.event.handle=p.event.dispatch,p.removeEvent=e.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]=="undefined"&&(a[d]=null),a.detachEvent(d,c))},p.Event=function(a,b){if(this instanceof p.Event)a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?bb:ba):this.type=a,b&&p.extend(this,b),this.timeStamp=a&&a.timeStamp||p.now(),this[p.expando]=!0;else return new p.Event(a,b)},p.Event.prototype={preventDefault:function(){this.isDefaultPrevented=bb;var a=this.originalEvent;if(!a)return;a.preventDefault?a.preventDefault():a.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=bb;var a=this.originalEvent;if(!a)return;a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=bb,this.stopPropagation()},isDefaultPrevented:ba,isPropagationStopped:ba,isImmediatePropagationStopped:ba},p.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){p.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj,g=f.selector;if(!e||e!==d&&!p.contains(d,e))a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b;return c}}}),p.support.submitBubbles||(p.event.special.submit={setup:function(){if(p.nodeName(this,"form"))return!1;p.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=p.nodeName(c,"input")||p.nodeName(c,"button")?c.form:b;d&&!p._data(d,"_submit_attached")&&(p.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),p._data(d,"_submit_attached",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&p.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(p.nodeName(this,"form"))return!1;p.event.remove(this,"._submit")}}),p.support.changeBubbles||(p.event.special.change={setup:function(){if(V.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")p.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),p.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),p.event.simulate("change",this,a,!0)});return!1}p.event.add(this,"beforeactivate._change",function(a){var b=a.target;V.test(b.nodeName)&&!p._data(b,"_change_attached")&&(p.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&p.event.simulate("change",this.parentNode,a,!0)}),p._data(b,"_change_attached",!0))})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){return p.event.remove(this,"._change"),!V.test(this.nodeName)}}),p.support.focusinBubbles||p.each({focus:"focusin",blur:"focusout"},function(a,b){var c=0,d=function(a){p.event.simulate(b,a.target,p.event.fix(a),!0)};p.event.special[b]={setup:function(){c++===0&&e.addEventListener(a,d,!0)},teardown:function(){--c===0&&e.removeEventListener(a,d,!0)}}}),p.fn.extend({on:function(a,c,d,e,f){var g,h;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(h in a)this.on(h,c,d,a[h],f);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=ba;else if(!e)return this;return f===1&&(g=e,e=function(a){return p().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=p.guid++)),this.each(function(){p.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){var e,f;if(a&&a.preventDefault&&a.handleObj)return e=a.handleObj,p(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler),this;if(typeof a=="object"){for(f in a)this.off(f,c,a[f]);return this}if(c===!1||typeof c=="function")d=c,c=b;return d===!1&&(d=ba),this.each(function(){p.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){return p(this.context).on(a,this.selector,b,c),this},die:function(a,b){return p(this.context).off(a,this.selector||"**",b),this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length===1?this.off(a,"**"):this.off(b,a||"**",c)},trigger:function(a,b){return this.each(function(){p.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return p.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||p.guid++,d=0,e=function(c){var e=(p._data(this,"lastToggle"+a.guid)||0)%d;return p._data(this,"lastToggle"+a.guid,e+1),c.preventDefault(),b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),p.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){p.fn[b]=function(a,c){return c==null&&(c=a,a=null),arguments.length>0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function bc(a,b,c,d){c=c||[],b=b||r;var e,f,i,j,k=b.nodeType;if(!a||typeof a!="string")return c;if(k!==1&&k!==9)return[];i=g(b);if(!i&&!d)if(e=P.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&h(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return w.apply(c,x.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&_&&b.getElementsByClassName)return w.apply(c,x.call(b.getElementsByClassName(j),0)),c}return bp(a.replace(L,"$1"),b,c,d,i)}function bd(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function be(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bf(a){return z(function(b){return b=+b,z(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function bg(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bh(a,b){var c,d,f,g,h,i,j,k=C[o][a];if(k)return b?0:k.slice(0);h=a,i=[],j=e.preFilter;while(h){if(!c||(d=M.exec(h)))d&&(h=h.slice(d[0].length)),i.push(f=[]);c=!1;if(d=N.exec(h))f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=d[0].replace(L," ");for(g in e.filter)(d=W[g].exec(h))&&(!j[g]||(d=j[g](d,r,!0)))&&(f.push(c=new q(d.shift())),h=h.slice(c.length),c.type=g,c.matches=d);if(!c)break}return b?h.length:h?bc.error(a):C(a,i).slice(0)}function bi(a,b,d){var e=b.dir,f=d&&b.dir==="parentNode",g=u++;return b.first?function(b,c,d){while(b=b[e])if(f||b.nodeType===1)return a(b,c,d)}:function(b,d,h){if(!h){var i,j=t+" "+g+" ",k=j+c;while(b=b[e])if(f||b.nodeType===1){if((i=b[o])===k)return b.sizset;if(typeof i=="string"&&i.indexOf(j)===0){if(b.sizset)return b}else{b[o]=k;if(a(b,d,h))return b.sizset=!0,b;b.sizset=!1}}}else while(b=b[e])if(f||b.nodeType===1)if(a(b,d,h))return b}}function bj(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function bk(a,b,c,d,e){var f,g=[],h=0,i=a.length,j=b!=null;for(;h<i;h++)if(f=a[h])if(!c||c(f,d,e))g.push(f),j&&b.push(h);return g}function bl(a,b,c,d,e,f){return d&&!d[o]&&(d=bl(d)),e&&!e[o]&&(e=bl(e,f)),z(function(f,g,h,i){if(f&&e)return;var j,k,l,m=[],n=[],o=g.length,p=f||bo(b||"*",h.nodeType?[h]:h,[],f),q=a&&(f||!b)?bk(p,m,a,h,i):p,r=c?e||(f?a:o||d)?[]:g:q;c&&c(q,r,h,i);if(d){l=bk(r,n),d(l,[],h,i),j=l.length;while(j--)if(k=l[j])r[n[j]]=!(q[n[j]]=k)}if(f){j=a&&r.length;while(j--)if(k=r[j])f[m[j]]=!(g[m[j]]=k)}else r=bk(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):w.apply(g,r)})}function bm(a){var b,c,d,f=a.length,g=e.relative[a[0].type],h=g||e.relative[" "],i=g?1:0,j=bi(function(a){return a===b},h,!0),k=bi(function(a){return y.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==l)||((b=c).nodeType?j(a,c,d):k(a,c,d))}];for(;i<f;i++)if(c=e.relative[a[i].type])m=[bi(bj(m),c)];else{c=e.filter[a[i].type].apply(null,a[i].matches);if(c[o]){d=++i;for(;d<f;d++)if(e.relative[a[d].type])break;return bl(i>1&&bj(m),i>1&&a.slice(0,i-1).join("").replace(L,"$1"),c,i<d&&bm(a.slice(i,d)),d<f&&bm(a=a.slice(d)),d<f&&a.join(""))}m.push(c)}return bj(m)}function bn(a,b){var d=b.length>0,f=a.length>0,g=function(h,i,j,k,m){var n,o,p,q=[],s=0,u="0",x=h&&[],y=m!=null,z=l,A=h||f&&e.find.TAG("*",m&&i.parentNode||i),B=t+=z==null?1:Math.E;y&&(l=i!==r&&i,c=g.el);for(;(n=A[u])!=null;u++){if(f&&n){for(o=0;p=a[o];o++)if(p(n,i,j)){k.push(n);break}y&&(t=B,c=++g.el)}d&&((n=!p&&n)&&s--,h&&x.push(n))}s+=u;if(d&&u!==s){for(o=0;p=b[o];o++)p(x,q,i,j);if(h){if(s>0)while(u--)!x[u]&&!q[u]&&(q[u]=v.call(k));q=bk(q)}w.apply(k,q),y&&!h&&q.length>0&&s+b.length>1&&bc.uniqueSort(k)}return y&&(t=B,l=z),x};return g.el=0,d?z(g):g}function bo(a,b,c,d){var e=0,f=b.length;for(;e<f;e++)bc(a,b[e],c,d);return c}function bp(a,b,c,d,f){var g,h,j,k,l,m=bh(a),n=m.length;if(!d&&m.length===1){h=m[0]=m[0].slice(0);if(h.length>2&&(j=h[0]).type==="ID"&&b.nodeType===9&&!f&&e.relative[h[1].type]){b=e.find.ID(j.matches[0].replace(V,""),b,f)[0];if(!b)return c;a=a.slice(h.shift().length)}for(g=W.POS.test(a)?-1:h.length-1;g>=0;g--){j=h[g];if(e.relative[k=j.type])break;if(l=e.find[k])if(d=l(j.matches[0].replace(V,""),R.test(h[0].type)&&b.parentNode||b,f)){h.splice(g,1),a=d.length&&h.join("");if(!a)return w.apply(c,x.call(d,0)),c;break}}}return i(a,m)(d,b,f,c,R.test(a)),c}function bq(){}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=String,r=a.document,s=r.documentElement,t=0,u=0,v=[].pop,w=[].push,x=[].slice,y=[].indexOf||function(a){var b=0,c=this.length;for(;b<c;b++)if(this[b]===a)return b;return-1},z=function(a,b){return a[o]=b==null||b,a},A=function(){var a={},b=[];return z(function(c,d){return b.push(c)>e.cacheLength&&delete a[b.shift()],a[c]=d},a)},B=A(),C=A(),D=A(),E="[\\x20\\t\\r\\n\\f]",F="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",G=F.replace("w","w#"),H="([*^$|!~]?=)",I="\\["+E+"*("+F+")"+E+"*(?:"+H+E+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+G+")|)|)"+E+"*\\]",J=":("+F+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+I+")|[^:]|\\\\.)*|.*))\\)|)",K=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+E+"*((?:-\\d)?\\d*)"+E+"*\\)|)(?=[^-]|$)",L=new RegExp("^"+E+"+|((?:^|[^\\\\])(?:\\\\.)*)"+E+"+$","g"),M=new RegExp("^"+E+"*,"+E+"*"),N=new RegExp("^"+E+"*([\\x20\\t\\r\\n\\f>+~])"+E+"*"),O=new RegExp(J),P=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,Q=/^:not/,R=/[\x20\t\r\n\f]*[+~]/,S=/:not\($/,T=/h\d/i,U=/input|select|textarea|button/i,V=/\\(?!\\)/g,W={ID:new RegExp("^#("+F+")"),CLASS:new RegExp("^\\.("+F+")"),NAME:new RegExp("^\\[name=['\"]?("+F+")['\"]?\\]"),TAG:new RegExp("^("+F.replace("w","w*")+")"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+J),POS:new RegExp(K,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+E+"*(even|odd|(([+-]|)(\\d*)n|)"+E+"*(?:([+-]|)"+E+"*(\\d+)|))"+E+"*\\)|)","i"),needsContext:new RegExp("^"+E+"*[>+~]|"+K,"i")},X=function(a){var b=r.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},Y=X(function(a){return a.appendChild(r.createComment("")),!a.getElementsByTagName("*").length}),Z=X(function(a){return a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),$=X(function(a){a.innerHTML="<select></select>";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),_=X(function(a){return a.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),ba=X(function(a){a.id=o+0,a.innerHTML="<a name='"+o+"'></a><div name='"+o+"'></div>",s.insertBefore(a,s.firstChild);var b=r.getElementsByName&&r.getElementsByName(o).length===2+r.getElementsByName(o+0).length;return d=!r.getElementById(o),s.removeChild(a),b});try{x.call(s.childNodes,0)[0].nodeType}catch(bb){x=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}bc.matches=function(a,b){return bc(a,null,null,b)},bc.matchesSelector=function(a,b){return bc(b,null,null,[a]).length>0},f=bc.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=f(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=f(b);return c},g=bc.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},h=bc.contains=s.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:s.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},bc.attr=function(a,b){var c,d=g(a);return d||(b=b.toLowerCase()),(c=e.attrHandle[b])?c(a):d||$?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},e=bc.selectors={cacheLength:50,createPseudo:z,match:W,attrHandle:Z?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:d?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:Y?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:ba&&function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:_&&function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(V,""),a[3]=(a[4]||a[5]||"").replace(V,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||bc.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&bc.error(a[0]),a},PSEUDO:function(a){var b,c;if(W.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(b=a[4])O.test(b)&&(c=bh(b,!0))&&(c=b.indexOf(")",b.length-c)-b.length)&&(b=b.slice(0,c),a[0]=a[0].slice(0,c)),a[2]=b;return a.slice(0,3)}},filter:{ID:d?function(a){return a=a.replace(V,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(V,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(V,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=B[o][a];return b||(b=B(a,new RegExp("(^|"+E+")"+a+"("+E+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return function(d,e){var f=bc.attr(d,a);return f==null?b==="!=":b?(f+="",b==="="?f===c:b==="!="?f!==c:b==="^="?c&&f.indexOf(c)===0:b==="*="?c&&f.indexOf(c)>-1:b==="$="?c&&f.substr(f.length-c.length)===c:b==="~="?(" "+f+" ").indexOf(c)>-1:b==="|="?f===c||f.substr(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d){return a==="nth"?function(a){var b,e,f=a.parentNode;if(c===1&&d===0)return!0;if(f){e=0;for(b=f.firstChild;b;b=b.nextSibling)if(b.nodeType===1){e++;if(a===b)break}}return e-=d,e===c||e%c===0&&e/c>=0}:function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b){var c,d=e.pseudos[a]||e.setFilters[a.toLowerCase()]||bc.error("unsupported pseudo: "+a);return d[o]?d(b):d.length>1?(c=[a,a,"",b],e.setFilters.hasOwnProperty(a.toLowerCase())?z(function(a,c){var e,f=d(a,b),g=f.length;while(g--)e=y.call(a,f[g]),a[e]=!(c[e]=f[g])}):function(a){return d(a,0,c)}):d}},pseudos:{not:z(function(a){var b=[],c=[],d=i(a.replace(L,"$1"));return d[o]?z(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)if(f=g[h])a[h]=!(b[h]=f)}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:z(function(a){return function(b){return bc(a,b).length>0}}),contains:z(function(a){return function(b){return(b.textContent||b.innerText||f(b)).indexOf(a)>-1}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!e.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},header:function(a){return T.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:bd("radio"),checkbox:bd("checkbox"),file:bd("file"),password:bd("password"),image:bd("image"),submit:be("submit"),reset:be("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return U.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement},first:bf(function(a,b,c){return[0]}),last:bf(function(a,b,c){return[b-1]}),eq:bf(function(a,b,c){return[c<0?c+b:c]}),even:bf(function(a,b,c){for(var d=0;d<b;d+=2)a.push(d);return a}),odd:bf(function(a,b,c){for(var d=1;d<b;d+=2)a.push(d);return a}),lt:bf(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:bf(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},j=s.compareDocumentPosition?function(a,b){return a===b?(k=!0,0):(!a.compareDocumentPosition||!b.compareDocumentPosition?a.compareDocumentPosition:a.compareDocumentPosition(b)&4)?-1:1}:function(a,b){if(a===b)return k=!0,0;if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,h=b.parentNode,i=g;if(g===h)return bg(a,b);if(!g)return-1;if(!h)return 1;while(i)e.unshift(i),i=i.parentNode;i=h;while(i)f.unshift(i),i=i.parentNode;c=e.length,d=f.length;for(var j=0;j<c&&j<d;j++)if(e[j]!==f[j])return bg(e[j],f[j]);return j===c?bg(a,f[j],-1):bg(e[j],b,1)},[0,0].sort(j),m=!k,bc.uniqueSort=function(a){var b,c=1;k=m,a.sort(j);if(k)for(;b=a[c];c++)b===a[c-1]&&a.splice(c--,1);return a},bc.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},i=bc.compile=function(a,b){var c,d=[],e=[],f=D[o][a];if(!f){b||(b=bh(a)),c=b.length;while(c--)f=bm(b[c]),f[o]?d.push(f):e.push(f);f=D(a,bn(e,d))}return f},r.querySelectorAll&&function(){var a,b=bp,c=/'|\\/g,d=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,e=[":focus"],f=[":active",":focus"],h=s.matchesSelector||s.mozMatchesSelector||s.webkitMatchesSelector||s.oMatchesSelector||s.msMatchesSelector;X(function(a){a.innerHTML="<select><option selected=''></option></select>",a.querySelectorAll("[selected]").length||e.push("\\["+E+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),X(function(a){a.innerHTML="<p test=''></p>",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+E+"*(?:\"\"|'')"),a.innerHTML="<input type='hidden'/>",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=new RegExp(e.join("|")),bp=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a))){var i,j,k=!0,l=o,m=d,n=d.nodeType===9&&a;if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){i=bh(a),(k=d.getAttribute("id"))?l=k.replace(c,"\\$&"):d.setAttribute("id",l),l="[id='"+l+"'] ",j=i.length;while(j--)i[j]=l+i[j].join("");m=R.test(a)&&d.parentNode||d,n=i.join(",")}if(n)try{return w.apply(f,x.call(m.querySelectorAll(n),0)),f}catch(p){}finally{k||d.removeAttribute("id")}}return b(a,d,f,g,h)},h&&(X(function(b){a=h.call(b,"div");try{h.call(b,"[test!='']:sizzle"),f.push("!=",J)}catch(c){}}),f=new RegExp(f.join("|")),bc.matchesSelector=function(b,c){c=c.replace(d,"='$1']");if(!g(b)&&!f.test(c)&&(!e||!e.test(c)))try{var i=h.call(b,c);if(i||a||b.document&&b.document.nodeType!==11)return i}catch(j){}return bc(c,null,null,[b]).length>0})}(),e.pseudos.nth=e.pseudos.eq,e.filters=bq.prototype=e.pseudos,e.setFilters=new bq,bc.attr=p.attr,p.find=bc,p.expr=bc.selectors,p.expr[":"]=p.expr.pseudos,p.unique=bc.uniqueSort,p.text=bc.getText,p.isXMLDoc=bc.isXML,p.contains=bc.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b<c;b++)if(p.contains(h[b],this))return!0});g=this.pushStack("","find",a);for(b=0,c=this.length;b<c;b++){d=g.length,p.find(a,this[b],g);if(b>0)for(e=d;e<g.length;e++)for(f=0;f<d;f++)if(g[f]===g[e]){g.splice(e--,1);break}}return g},has:function(a){var b,c=p(a,this),d=c.length;return this.filter(function(){for(b=0;b<d;b++)if(p.contains(this,c[b]))return!0})},not:function(a){return this.pushStack(bj(this,a,!1),"not",a)},filter:function(a){return this.pushStack(bj(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?bf.test(a)?p(a,this.context).index(this[0])>=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d<e;d++){c=this[d];while(c&&c.ownerDocument&&c!==b&&c.nodeType!==11){if(g?g.index(c)>-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/<tbody/i,br=/<|&#?\w+;/,bs=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,bu=new RegExp("<(?:"+bl+")[\\s/>]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,bz={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X<div>","</div>"]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(f){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){return bh(this[0])?this.length?this.pushStack(p(p.isFunction(a)?a():a),"replaceWith",a):this:p.isFunction(a)?this.each(function(b){var c=p(this),d=c.html();c.replaceWith(a.call(this,b,d))}):(typeof a!="string"&&(a=p(a).detach()),this.each(function(){var b=this.nextSibling,c=this.parentNode;p(this).remove(),b?p(b).before(a):p(c).append(a)}))},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){a=[].concat.apply([],a);var e,f,g,h,i=0,j=a[0],k=[],l=this.length;if(!p.support.checkClone&&l>1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i<l;i++)d.call(c&&p.nodeName(this[i],"table")?bC(this[i],"tbody"):this[i],i===h?g:p.clone(g,!0,!0))}g=f=null,k.length&&p.each(k,function(a,b){b.src?p.ajax?p.ajax({url:b.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):p.error("no ajax"):p.globalEval((b.text||b.textContent||b.innerHTML||"").replace(by,"")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),p.buildFragment=function(a,c,d){var f,g,h,i=a[0];return c=c||e,c=!c.nodeType&&c[0]||c,c=c.ownerDocument||c,a.length===1&&typeof i=="string"&&i.length<512&&c===e&&i.charAt(0)==="<"&&!bt.test(i)&&(p.support.checkClone||!bw.test(i))&&(p.support.html5Clone||!bu.test(i))&&(g=!0,f=p.fragments[i],h=f!==b),f||(f=c.createDocumentFragment(),p.clean(a,c,f,d),g&&(p.fragments[i]=h&&f)),{fragment:f,cacheable:g}},p.fragments={},p.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){p.fn[a]=function(c){var d,e=0,f=[],g=p(c),h=g.length,i=this.length===1&&this[0].parentNode;if((i==null||i&&i.nodeType===11&&i.childNodes.length===1)&&h===1)return g[b](this[0]),this;for(;e<h;e++)d=(e>0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1></$2>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]==="<table>"&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{ck=f.href}catch(cy){ck=e.createElement("a"),ck.href="",ck=ck.href}cj=ct.exec(ck.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("<div>").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:ck,isLocal:cn.test(cj[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=(c||y)+"",k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,cj[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase())||!1,l.crossDomain=i&&i.join(":")+(i[3]?"":i[1]==="http:"?80:443)!==cj.join(":")+(cj[3]?"":cj[1]==="http:"?80:443)),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e=this.createTween(a,b),f=cQ.exec(b),g=e.cur(),h=+g||0,i=1,j=20;if(f){c=+f[2],d=f[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&h){h=p.css(e.elem,a,!0)||c||1;do i=i||".5",h=h/i,p.style(e.elem,a,h+d);while(i!==(i=e.cur()/g)&&i!==1&&--j)}e.unit=d,e.start=h,e.end=f[1]?h+(f[1]+1)*c:c}return e}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d<e;d++)c=a[d],cT[c]=cT[c]||[],cT[c].unshift(b)},prefilter:function(a,b){b?cS.unshift(a):cS.push(a)}}),p.Tween=cZ,cZ.prototype={constructor:cZ,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(p.cssNumber[c]?"":"px")},cur:function(){var a=cZ.propHooks[this.prop];return a&&a.get?a.get(this):cZ.propHooks._default.get(this)},run:function(a){var b,c=cZ.propHooks[this.prop];return this.options.duration?this.pos=b=p.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):cZ.propHooks._default.set(this),this}},cZ.prototype.init.prototype=cZ.prototype,cZ.propHooks={_default:{get:function(a){var b;return a.elem[a.prop]==null||!!a.elem.style&&a.elem.style[a.prop]!=null?(b=p.css(a.elem,a.prop,!1,""),!b||b==="auto"?0:b):a.elem[a.prop]},set:function(a){p.fx.step[a.prop]?p.fx.step[a.prop](a):a.elem.style&&(a.elem.style[p.cssProps[a.prop]]!=null||p.cssHooks[a.prop])?p.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},cZ.propHooks.scrollTop=cZ.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},p.each(["toggle","show","hide"],function(a,b){var c=p.fn[b];p.fn[b]=function(d,e,f){return d==null||typeof d=="boolean"||!a&&p.isFunction(d)&&p.isFunction(e)?c.apply(this,arguments):this.animate(c$(b,!0),d,e,f)}}),p.fn.extend({fadeTo:function(a,b,c,d){return this.filter(bZ).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=p.isEmptyObject(a),f=p.speed(b,c,d),g=function(){var b=cW(this,p.extend({},a),f);e&&b.stop(!0)};return e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,c,d){var e=function(a){var b=a.stop;delete a.stop,b(d)};return typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,c=a!=null&&a+"queueHooks",f=p.timers,g=p._data(this);if(c)g[c]&&g[c].stop&&e(g[c]);else for(c in g)g[c]&&g[c].stop&&cR.test(c)&&e(g[c]);for(c=f.length;c--;)f[c].elem===this&&(a==null||f[c].queue===a)&&(f[c].anim.stop(d),b=!1,f.splice(c,1));(b||!d)&&p.dequeue(this,a)})}}),p.each({slideDown:c$("show"),slideUp:c$("hide"),slideToggle:c$("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){p.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),p.speed=function(a,b,c){var d=a&&typeof a=="object"?p.extend({},a):{complete:c||!c&&b||p.isFunction(a)&&a,duration:a,easing:c&&b||b&&!p.isFunction(b)&&b};d.duration=p.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in p.fx.speeds?p.fx.speeds[d.duration]:p.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";return d.old=d.complete,d.complete=function(){p.isFunction(d.old)&&d.old.call(this),d.queue&&p.dequeue(this,d.queue)},d},p.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},p.timers=[],p.fx=cZ.prototype.init,p.fx.tick=function(){var a,b=p.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||p.fx.stop()},p.fx.timer=function(a){a()&&p.timers.push(a)&&!cO&&(cO=setInterval(p.fx.tick,p.fx.interval))},p.fx.interval=13,p.fx.stop=function(){clearInterval(cO),cO=null},p.fx.speeds={slow:600,fast:200,_default:400},p.fx.step={},p.expr&&p.expr.filters&&(p.expr.filters.animated=function(a){return p.grep(p.timers,function(b){return a===b.elem}).length});var c_=/^(?:body|html)$/i;p.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){p.offset.setOffset(this,a,b)});var c,d,e,f,g,h,i,j={top:0,left:0},k=this[0],l=k&&k.ownerDocument;if(!l)return;return(d=l.body)===k?p.offset.bodyOffset(k):(c=l.documentElement,p.contains(c,k)?(typeof k.getBoundingClientRect!="undefined"&&(j=k.getBoundingClientRect()),e=da(l),f=c.clientTop||d.clientTop||0,g=c.clientLeft||d.clientLeft||0,h=e.pageYOffset||c.scrollTop,i=e.pageXOffset||c.scrollLeft,{top:j.top+h-f,left:j.left+i-g}):j)},p.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;return p.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(p.css(a,"marginTop"))||0,c+=parseFloat(p.css(a,"marginLeft"))||0),{top:b,left:c}},setOffset:function(a,b,c){var d=p.css(a,"position");d==="static"&&(a.style.position="relative");var e=p(a),f=e.offset(),g=p.css(a,"top"),h=p.css(a,"left"),i=(d==="absolute"||d==="fixed")&&p.inArray("auto",[g,h])>-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window); diff --git a/record-and-playback/presentation_export/playback/presentation_export/lib/popcorn-complete.min.js b/record-and-playback/presentation_export/playback/presentation_export/lib/popcorn-complete.min.js new file mode 100755 index 0000000000000000000000000000000000000000..8aeee4f2b58abed7b4b0fc7ffadfe30ae8db1cb4 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/lib/popcorn-complete.min.js @@ -0,0 +1,160 @@ +/* + * popcorn.js version 1.3 + * http://popcornjs.org + * + * Copyright 2011, Mozilla Foundation + * Licensed under the MIT license + */ + +(function(r,f){function n(a,g){return function(){if(d.plugin.debug)return a.apply(this,arguments);try{return a.apply(this,arguments)}catch(l){d.plugin.errors.push({plugin:g,thrown:l,source:a.toString()});this.emit("pluginerror",d.plugin.errors)}}}if(f.addEventListener){var c=Array.prototype,b=Object.prototype,e=c.forEach,h=c.slice,i=b.hasOwnProperty,j=b.toString,p=r.Popcorn,m=[],o=false,q={events:{hash:{},apis:{}}},s=function(){return r.requestAnimationFrame||r.webkitRequestAnimationFrame||r.mozRequestAnimationFrame|| +r.oRequestAnimationFrame||r.msRequestAnimationFrame||function(a){r.setTimeout(a,16)}}(),d=function(a,g){return new d.p.init(a,g||null)};d.version="1.3";d.isSupported=true;d.instances=[];d.p=d.prototype={init:function(a,g){var l,k=this;if(typeof a==="function")if(f.readyState==="complete")a(f,d);else{m.push(a);if(!o){o=true;var t=function(){f.removeEventListener("DOMContentLoaded",t,false);for(var z=0,C=m.length;z<C;z++)m[z].call(f,d);m=null};f.addEventListener("DOMContentLoaded",t,false)}}else{if(typeof a=== +"string")try{l=f.querySelector(a)}catch(u){throw Error("Popcorn.js Error: Invalid media element selector: "+a);}this.media=l||a;l=this.media.nodeName&&this.media.nodeName.toLowerCase()||"video";this[l]=this.media;this.options=g||{};this.id=this.options.id||d.guid(l);if(d.byId(this.id))throw Error("Popcorn.js Error: Cannot use duplicate ID ("+this.id+")");this.isDestroyed=false;this.data={running:{cue:[]},timeUpdate:d.nop,disabled:{},events:{},hooks:{},history:[],state:{volume:this.media.volume},trackRefs:{}, +trackEvents:{byStart:[{start:-1,end:-1}],byEnd:[{start:-1,end:-1}],animating:[],startIndex:0,endIndex:0,previousUpdateTime:-1}};d.instances.push(this);var v=function(){if(k.media.currentTime<0)k.media.currentTime=0;k.media.removeEventListener("loadeddata",v,false);var z,C,E,B,w;z=k.media.duration;z=z!=z?Number.MAX_VALUE:z+1;d.addTrackEvent(k,{start:z,end:z});if(k.options.frameAnimation){k.data.timeUpdate=function(){d.timeUpdate(k,{});d.forEach(d.manifest,function(D,F){if(C=k.data.running[F]){B=C.length; +for(var I=0;I<B;I++){E=C[I];(w=E._natives)&&w.frame&&w.frame.call(k,{},E,k.currentTime())}}});k.emit("timeupdate");!k.isDestroyed&&s(k.data.timeUpdate)};!k.isDestroyed&&s(k.data.timeUpdate)}else{k.data.timeUpdate=function(D){d.timeUpdate(k,D)};k.isDestroyed||k.media.addEventListener("timeupdate",k.data.timeUpdate,false)}};Object.defineProperty(this,"error",{get:function(){return k.media.error}});k.media.readyState>=2?v():k.media.addEventListener("loadeddata",v,false);return this}}};d.p.init.prototype= +d.p;d.byId=function(a){for(var g=d.instances,l=g.length,k=0;k<l;k++)if(g[k].id===a)return g[k];return null};d.forEach=function(a,g,l){if(!a||!g)return{};l=l||this;var k,t;if(e&&a.forEach===e)return a.forEach(g,l);if(j.call(a)==="[object NodeList]"){k=0;for(t=a.length;k<t;k++)g.call(l,a[k],k,a);return a}for(k in a)i.call(a,k)&&g.call(l,a[k],k,a);return a};d.extend=function(a){var g=h.call(arguments,1);d.forEach(g,function(l){for(var k in l)a[k]=l[k]});return a};d.extend(d,{noConflict:function(a){if(a)r.Popcorn= +p;return d},error:function(a){throw Error(a);},guid:function(a){d.guid.counter++;return(a?a:"")+(+new Date+d.guid.counter)},sizeOf:function(a){var g=0,l;for(l in a)g++;return g},isArray:Array.isArray||function(a){return j.call(a)==="[object Array]"},nop:function(){},position:function(a){a=a.getBoundingClientRect();var g={},l=f.documentElement,k=f.body,t,u,v;t=l.clientTop||k.clientTop||0;u=l.clientLeft||k.clientLeft||0;v=r.pageYOffset&&l.scrollTop||k.scrollTop;l=r.pageXOffset&&l.scrollLeft||k.scrollLeft; +t=Math.ceil(a.top+v-t);u=Math.ceil(a.left+l-u);for(var z in a)g[z]=Math.round(a[z]);return d.extend({},g,{top:t,left:u})},disable:function(a,g){if(!a.data.disabled[g]){a.data.disabled[g]=true;for(var l=a.data.running[g].length-1,k;l>=0;l--){k=a.data.running[g][l];k._natives.end.call(a,null,k)}}return a},enable:function(a,g){if(a.data.disabled[g]){a.data.disabled[g]=false;for(var l=a.data.running[g].length-1,k;l>=0;l--){k=a.data.running[g][l];k._natives.start.call(a,null,k)}}return a},destroy:function(a){var g= +a.data.events,l=a.data.trackEvents,k,t,u,v;for(t in g){k=g[t];for(u in k)delete k[u];g[t]=null}for(v in d.registryByName)d.removePlugin(a,v);l.byStart.length=0;l.byEnd.length=0;if(!a.isDestroyed){a.data.timeUpdate&&a.media.removeEventListener("timeupdate",a.data.timeUpdate,false);a.isDestroyed=true}}});d.guid.counter=1;d.extend(d.p,function(){var a={};d.forEach("load play pause currentTime playbackRate volume duration preload playbackRate autoplay loop controls muted buffered readyState seeking paused played seekable ended".split(/\s+/g), +function(g){a[g]=function(l){var k;if(typeof this.media[g]==="function"){if(l!=null&&/play|pause/.test(g))this.media.currentTime=d.util.toSeconds(l);this.media[g]();return this}if(l!=null){k=this.media[g];this.media[g]=l;k!==l&&this.emit("attrchange",{attribute:g,previousValue:k,currentValue:l});return this}return this.media[g]}});return a}());d.forEach("enable disable".split(" "),function(a){d.p[a]=function(g){return d[a](this,g)}});d.extend(d.p,{roundTime:function(){return Math.round(this.media.currentTime)}, +exec:function(a,g,l){var k=arguments.length,t,u;try{u=d.util.toSeconds(a)}catch(v){}if(typeof u==="number")a=u;if(typeof a==="number"&&k===2){l=g;g=a;a=d.guid("cue")}else if(k===1)g=-1;else if(t=this.getTrackEvent(a)){if(typeof a==="string"&&k===2){if(typeof g==="number")l=t._natives.start;if(typeof g==="function"){l=g;g=t.start}}}else if(k>=2){if(typeof g==="string"){try{u=d.util.toSeconds(g)}catch(z){}g=u}if(typeof g==="number")l=d.nop();if(typeof g==="function"){l=g;g=-1}}d.addTrackEvent(this, +{id:a,start:g,end:g+1,_running:false,_natives:{start:l||d.nop,end:d.nop,type:"cue"}});return this},mute:function(a){a=a==null||a===true?"muted":"unmuted";if(a==="unmuted"){this.media.muted=false;this.media.volume=this.data.state.volume}if(a==="muted"){this.data.state.volume=this.media.volume;this.media.muted=true}this.emit(a);return this},unmute:function(a){return this.mute(a==null?false:!a)},position:function(){return d.position(this.media)},toggle:function(a){return d[this.data.disabled[a]?"enable": +"disable"](this,a)},defaults:function(a,g){if(d.isArray(a)){d.forEach(a,function(l){for(var k in l)this.defaults(k,l[k])},this);return this}if(!this.options.defaults)this.options.defaults={};this.options.defaults[a]||(this.options.defaults[a]={});d.extend(this.options.defaults[a],g);return this}});d.Events={UIEvents:"blur focus focusin focusout load resize scroll unload",MouseEvents:"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave click dblclick",Events:"loadstart progress suspend emptied stalled play pause error loadedmetadata loadeddata waiting playing canplay canplaythrough seeking seeked timeupdate ended ratechange durationchange volumechange"}; +d.Events.Natives=d.Events.UIEvents+" "+d.Events.MouseEvents+" "+d.Events.Events;q.events.apiTypes=["UIEvents","MouseEvents","Events"];(function(a,g){for(var l=q.events.apiTypes,k=a.Natives.split(/\s+/g),t=0,u=k.length;t<u;t++)g.hash[k[t]]=true;l.forEach(function(v){g.apis[v]={};for(var z=a[v].split(/\s+/g),C=z.length,E=0;E<C;E++)g.apis[v][z[E]]=true})})(d.Events,q.events);d.events={isNative:function(a){return!!q.events.hash[a]},getInterface:function(a){if(!d.events.isNative(a))return false;var g= +q.events,l=g.apiTypes;g=g.apis;for(var k=0,t=l.length,u,v;k<t;k++){v=l[k];if(g[v][a]){u=v;break}}return u},all:d.Events.Natives.split(/\s+/g),fn:{trigger:function(a,g){var l;if(this.data.events[a]&&d.sizeOf(this.data.events[a])){if(l=d.events.getInterface(a)){l=f.createEvent(l);l.initEvent(a,true,true,r,1);this.media.dispatchEvent(l);return this}d.forEach(this.data.events[a],function(k){k.call(this,g)},this)}return this},listen:function(a,g){var l=this,k=true,t=d.events.hooks[a],u;if(!this.data.events[a]){this.data.events[a]= +{};k=false}if(t){t.add&&t.add.call(this,{},g);if(t.bind)a=t.bind;if(t.handler){u=g;g=function(v){t.handler.call(l,v,u)}}k=true;if(!this.data.events[a]){this.data.events[a]={};k=false}}this.data.events[a][g.name||g.toString()+d.guid()]=g;!k&&d.events.all.indexOf(a)>-1&&this.media.addEventListener(a,function(v){d.forEach(l.data.events[a],function(z){typeof z==="function"&&z.call(l,v)})},false);return this},unlisten:function(a,g){if(this.data.events[a]&&this.data.events[a][g]){delete this.data.events[a][g]; +return this}this.data.events[a]=null;return this}},hooks:{canplayall:{bind:"canplaythrough",add:function(a,g){var l=false;if(this.media.readyState){g.call(this,a);l=true}this.data.hooks.canplayall={fired:l}},handler:function(a,g){if(!this.data.hooks.canplayall.fired){g.call(this,a);this.data.hooks.canplayall.fired=true}}}}};d.forEach([["trigger","emit"],["listen","on"],["unlisten","off"]],function(a){d.p[a[0]]=d.p[a[1]]=d.events.fn[a[0]]});d.addTrackEvent=function(a,g){var l,k;if(g.id)l=a.getTrackEvent(g.id); +if(l){k=true;g=d.extend({},l,g);a.removeTrackEvent(g.id)}if(g&&g._natives&&g._natives.type&&a.options.defaults&&a.options.defaults[g._natives.type])g=d.extend({},a.options.defaults[g._natives.type],g);if(g._natives){g._id=g.id||g._id||d.guid(g._natives.type);a.data.history.push(g._id)}g.start=d.util.toSeconds(g.start,a.options.framerate);g.end=d.util.toSeconds(g.end,a.options.framerate);var t=a.data.trackEvents.byStart,u=a.data.trackEvents.byEnd,v;for(v=t.length-1;v>=0;v--)if(g.start>=t[v].start){t.splice(v+ +1,0,g);break}for(t=u.length-1;t>=0;t--)if(g.end>u[t].end){u.splice(t+1,0,g);break}if(g.end>a.media.currentTime&&g.start<=a.media.currentTime){g._running=true;a.data.running[g._natives.type].push(g);a.data.disabled[g._natives.type]||g._natives.start.call(a,null,g)}v<=a.data.trackEvents.startIndex&&g.start<=a.data.trackEvents.previousUpdateTime&&a.data.trackEvents.startIndex++;t<=a.data.trackEvents.endIndex&&g.end<a.data.trackEvents.previousUpdateTime&&a.data.trackEvents.endIndex++;this.timeUpdate(a, +null,true);g._id&&d.addTrackEvent.ref(a,g);if(k){k=g._natives.type==="cue"?"cuechange":"trackchange";a.emit(k,{id:g.id,previousValue:{time:l.start,fn:l._natives.start},currentValue:{time:g.start,fn:g._natives.start}})}};d.addTrackEvent.ref=function(a,g){a.data.trackRefs[g._id]=g;return a};d.removeTrackEvent=function(a,g){for(var l,k,t=a.data.history.length,u=a.data.trackEvents.byStart.length,v=0,z=0,C=[],E=[],B=[],w=[];--u>-1;){l=a.data.trackEvents.byStart[v];k=a.data.trackEvents.byEnd[v];if(!l._id){C.push(l); +E.push(k)}if(l._id){l._id!==g&&C.push(l);k._id!==g&&E.push(k);if(l._id===g){z=v;l._natives._teardown&&l._natives._teardown.call(a,l)}}v++}u=a.data.trackEvents.animating.length;v=0;if(u)for(;--u>-1;){l=a.data.trackEvents.animating[v];l._id||B.push(l);l._id&&l._id!==g&&B.push(l);v++}z<=a.data.trackEvents.startIndex&&a.data.trackEvents.startIndex--;z<=a.data.trackEvents.endIndex&&a.data.trackEvents.endIndex--;a.data.trackEvents.byStart=C;a.data.trackEvents.byEnd=E;a.data.trackEvents.animating=B;for(u= +0;u<t;u++)a.data.history[u]!==g&&w.push(a.data.history[u]);a.data.history=w;d.removeTrackEvent.ref(a,g)};d.removeTrackEvent.ref=function(a,g){delete a.data.trackRefs[g];return a};d.getTrackEvents=function(a){var g=[];a=a.data.trackEvents.byStart;for(var l=a.length,k=0,t;k<l;k++){t=a[k];t._id&&g.push(t)}return g};d.getTrackEvents.ref=function(a){return a.data.trackRefs};d.getTrackEvent=function(a,g){return a.data.trackRefs[g]};d.getTrackEvent.ref=function(a,g){return a.data.trackRefs[g]};d.getLastTrackEventId= +function(a){return a.data.history[a.data.history.length-1]};d.timeUpdate=function(a,g){var l=a.media.currentTime,k=a.data.trackEvents.previousUpdateTime,t=a.data.trackEvents,u=t.endIndex,v=t.startIndex,z=t.byStart.length,C=t.byEnd.length,E=d.registryByName,B,w,D;if(k<=l){for(;t.byEnd[u]&&t.byEnd[u].end<=l;){B=t.byEnd[u];w=(k=B._natives)&&k.type;if(!k||E[w]||a[w]){if(B._running===true){B._running=false;D=a.data.running[w];D.splice(D.indexOf(B),1);if(!a.data.disabled[w]){k.end.call(a,g,B);a.emit("trackend", +d.extend({},B,{plugin:w,type:"trackend"}))}}u++}else{d.removeTrackEvent(a,B._id);return}}for(;t.byStart[v]&&t.byStart[v].start<=l;){B=t.byStart[v];w=(k=B._natives)&&k.type;if(!k||E[w]||a[w]){if(B.end>l&&B._running===false){B._running=true;a.data.running[w].push(B);if(!a.data.disabled[w]){k.start.call(a,g,B);a.emit("trackstart",d.extend({},B,{plugin:w,type:"trackstart"}))}}v++}else{d.removeTrackEvent(a,B._id);return}}}else if(k>l){for(;t.byStart[v]&&t.byStart[v].start>l;){B=t.byStart[v];w=(k=B._natives)&& +k.type;if(!k||E[w]||a[w]){if(B._running===true){B._running=false;D=a.data.running[w];D.splice(D.indexOf(B),1);if(!a.data.disabled[w]){k.end.call(a,g,B);a.emit("trackend",d.extend({},B,{plugin:w,type:"trackend"}))}}v--}else{d.removeTrackEvent(a,B._id);return}}for(;t.byEnd[u]&&t.byEnd[u].end>l;){B=t.byEnd[u];w=(k=B._natives)&&k.type;if(!k||E[w]||a[w]){if(B.start<=l&&B._running===false){B._running=true;a.data.running[w].push(B);if(!a.data.disabled[w]){k.start.call(a,g,B);a.emit("trackstart",d.extend({}, +B,{plugin:w,type:"trackstart"}))}}u--}else{d.removeTrackEvent(a,B._id);return}}}t.endIndex=u;t.startIndex=v;t.previousUpdateTime=l;t.byStart.length<z&&t.startIndex--;t.byEnd.length<C&&t.endIndex--};d.extend(d.p,{getTrackEvents:function(){return d.getTrackEvents.call(null,this)},getTrackEvent:function(a){return d.getTrackEvent.call(null,this,a)},getLastTrackEventId:function(){return d.getLastTrackEventId.call(null,this)},removeTrackEvent:function(a){d.removeTrackEvent.call(null,this,a);return this}, +removePlugin:function(a){d.removePlugin.call(null,this,a);return this},timeUpdate:function(a){d.timeUpdate.call(null,this,a);return this},destroy:function(){d.destroy.call(null,this);return this}});d.manifest={};d.registry=[];d.registryByName={};d.plugin=function(a,g,l){if(d.protect.natives.indexOf(a.toLowerCase())>=0)d.error("'"+a+"' is a protected function name");else{var k=["start","end"],t={},u=typeof g==="function",v=["_setup","_teardown","start","end","frame"],z=function(B,w){B=B||d.nop;w=w|| +d.nop;return function(){B.apply(this,arguments);w.apply(this,arguments)}};d.manifest[a]=l=l||g.manifest||{};v.forEach(function(B){g[B]=n(g[B]||d.nop,a)});var C=function(B,w){if(!w)return this;if(w.ranges&&d.isArray(w.ranges)){d.forEach(w.ranges,function(G){G=d.extend({},w,G);delete G.ranges;this[a](G)},this);return this}var D=w._natives={},F="",I;d.extend(D,B);w._natives.type=a;w._running=false;D.start=D.start||D["in"];D.end=D.end||D.out;if(w.once)D.end=z(D.end,function(){this.removeTrackEvent(w._id)}); +D._teardown=z(function(){var G=h.call(arguments),H=this.data.running[D.type];G.unshift(null);G[1]._running&&H.splice(H.indexOf(w),1)&&D.end.apply(this,G)},D._teardown);w.compose=w.compose&&w.compose.split(" ")||[];w.effect=w.effect&&w.effect.split(" ")||[];w.compose=w.compose.concat(w.effect);w.compose.forEach(function(G){F=d.compositions[G]||{};v.forEach(function(H){D[H]=z(D[H],F[H])})});w._natives.manifest=l;if(!("start"in w))w.start=w["in"]||0;if(!w.end&&w.end!==0)w.end=w.out||Number.MAX_VALUE; +if(!i.call(w,"toString"))w.toString=function(){var G=["start: "+w.start,"end: "+w.end,"id: "+(w.id||w._id)];w.target!=null&&G.push("target: "+w.target);return a+" ( "+G.join(", ")+" )"};if(!w.target){I="options"in l&&l.options;w.target=I&&"target"in I&&I.target}if(w._natives)w._id=d.guid(w._natives.type);w._natives._setup&&w._natives._setup.call(this,w);d.addTrackEvent(this,w);d.forEach(B,function(G,H){H!=="type"&&k.indexOf(H)===-1&&this.on(H,G)},this);return this};d.p[a]=t[a]=function(B,w){var D; +if(B&&!w)w=B;else if(D=this.getTrackEvent(B)){w=d.extend({},D,w);d.addTrackEvent(this,w);return this}else w.id=B;this.data.running[a]=this.data.running[a]||[];D=d.extend({},this.options.defaults&&this.options.defaults[a]||{},w);return C.call(this,u?g.call(this,D):g,D)};l&&d.extend(g,{manifest:l});var E={fn:t[a],definition:g,base:g,parents:[],name:a};d.registry.push(d.extend(t,E,{type:a}));d.registryByName[a]=E;return t}};d.plugin.errors=[];d.plugin.debug=d.version==="1.3";d.removePlugin=function(a, +g){if(!g){g=a;a=d.p;if(d.protect.natives.indexOf(g.toLowerCase())>=0){d.error("'"+g+"' is a protected function name");return}var l=d.registry.length,k;for(k=0;k<l;k++)if(d.registry[k].name===g){d.registry.splice(k,1);delete d.registryByName[g];delete d.manifest[g];delete a[g];return}}l=a.data.trackEvents.byStart;k=a.data.trackEvents.byEnd;var t=a.data.trackEvents.animating,u,v;u=0;for(v=l.length;u<v;u++){if(l[u]&&l[u]._natives&&l[u]._natives.type===g){l[u]._natives._teardown&&l[u]._natives._teardown.call(a, +l[u]);l.splice(u,1);u--;v--;if(a.data.trackEvents.startIndex<=u){a.data.trackEvents.startIndex--;a.data.trackEvents.endIndex--}}k[u]&&k[u]._natives&&k[u]._natives.type===g&&k.splice(u,1)}u=0;for(v=t.length;u<v;u++)if(t[u]&&t[u]._natives&&t[u]._natives.type===g){t.splice(u,1);u--;v--}};d.compositions={};d.compose=function(a,g,l){d.manifest[a]=l||g.manifest||{};d.compositions[a]=g};d.plugin.effect=d.effect=d.compose;var A=/^(?:\.|#|\[)/;d.dom={debug:false,find:function(a,g){var l=null;a=a.trim();g= +g||f;if(a){if(!A.test(a)){l=f.getElementById(a);if(l!==null)return l}try{l=g.querySelector(a)}catch(k){if(d.dom.debug)throw Error(k);}}return l}};var y=/\?/,x={url:"",data:"",dataType:"",success:d.nop,type:"GET",async:true,xhr:function(){return new r.XMLHttpRequest}};d.xhr=function(a){a.dataType=a.dataType&&a.dataType.toLowerCase()||null;if(a.dataType&&(a.dataType==="jsonp"||a.dataType==="script"))d.xhr.getJSONP(a.url,a.success,a.dataType==="script");else{a=d.extend({},x,a);a.ajax=a.xhr();if(a.ajax){if(a.type=== +"GET"&&a.data){a.url+=(y.test(a.url)?"&":"?")+a.data;a.data=null}a.ajax.open(a.type,a.url,a.async);a.ajax.send(a.data||null);return d.xhr.httpData(a)}}};d.xhr.httpData=function(a){var g,l=null,k,t=null;a.ajax.onreadystatechange=function(){if(a.ajax.readyState===4){try{l=JSON.parse(a.ajax.responseText)}catch(u){}g={xml:a.ajax.responseXML,text:a.ajax.responseText,json:l};if(!g.xml||!g.xml.documentElement){g.xml=null;try{k=new DOMParser;t=k.parseFromString(a.ajax.responseText,"text/xml");if(!t.getElementsByTagName("parsererror").length)g.xml= +t}catch(v){}}if(a.dataType)g=g[a.dataType];a.success.call(a.ajax,g)}};return g};d.xhr.getJSONP=function(a,g,l){var k=f.head||f.getElementsByTagName("head")[0]||f.documentElement,t=f.createElement("script"),u=false,v=[];v=/(=)\?(?=&|$)|\?\?/;var z,C;if(!l){C=a.match(/(callback=[^&]*)/);if(C!==null&&C.length){v=C[1].split("=")[1];if(v==="?")v="jsonp";z=d.guid(v);a=a.replace(/(callback=[^&]*)/,"callback="+z)}else{z=d.guid("jsonp");if(v.test(a))a=a.replace(v,"$1"+z);v=a.split(/\?(.+)?/);a=v[0]+"?";if(v[1])a+= +v[1]+"&";a+="callback="+z}window[z]=function(E){g&&g(E);u=true}}t.addEventListener("load",function(){l&&g&&g();u&&delete window[z];k.removeChild(t)},false);t.src=a;k.insertBefore(t,k.firstChild)};d.getJSONP=d.xhr.getJSONP;d.getScript=d.xhr.getScript=function(a,g){return d.xhr.getJSONP(a,g,true)};d.util={toSeconds:function(a,g){var l=/^([0-9]+:){0,2}[0-9]+([.;][0-9]+)?$/,k,t,u;if(typeof a==="number")return a;typeof a==="string"&&!l.test(a)&&d.error("Invalid time format");l=a.split(":");k=l.length- +1;t=l[k];if(t.indexOf(";")>-1){t=t.split(";");u=0;if(g&&typeof g==="number")u=parseFloat(t[1],10)/g;l[k]=parseInt(t[0],10)+u}k=l[0];return{1:parseFloat(k,10),2:parseInt(k,10)*60+parseFloat(l[1],10),3:parseInt(k,10)*3600+parseInt(l[1],10)*60+parseFloat(l[2],10)}[l.length||1]}};d.p.cue=d.p.exec;d.protect={natives:function(a){return Object.keys?Object.keys(a):function(g){var l,k=[];for(l in g)i.call(g,l)&&k.push(l);return k}(a)}(d.p).map(function(a){return a.toLowerCase()})};d.forEach({listen:"on",unlisten:"off", +trigger:"emit",exec:"cue"},function(a,g){var l=d.p[g];d.p[g]=function(){if(typeof console!=="undefined"&&console.warn){console.warn("Deprecated method '"+g+"', "+(a==null?"do not use.":"use '"+a+"' instead."));d.p[g]=l}return d.p[a].apply(this,[].slice.call(arguments))}});r.Popcorn=d}else{r.Popcorn={isSupported:false};for(c="byId forEach extend effects error guid sizeOf isArray nop position disable enable destroyaddTrackEvent removeTrackEvent getTrackEvents getTrackEvent getLastTrackEventId timeUpdate plugin removePlugin compose effect xhr getJSONP getScript".split(/\s+/);c.length;)r.Popcorn[c.shift()]= +function(){}}})(window,window.document);(function(r,f){var n=r.document,c=r.location,b=/:\/\//,e=c.href.replace(c.href.split("/").slice(-1)[0],""),h=function(j,p,m){j=j||0;p=(p||j||0)+1;m=m||1;p=Math.ceil((p-j)/m)||0;var o=0,q=[];for(q.length=p;o<p;){q[o++]=j;j+=m}return q};f.sequence=function(j,p){return new f.sequence.init(j,p)};f.sequence.init=function(j,p){this.parent=n.getElementById(j);this.seqId=f.guid("__sequenced");this.queue=[];this.playlist=[];this.inOuts={ofVideos:[],ofClips:[]};this.dims={width:0,height:0};this.active=0;this.playing= +this.cycling=false;this.times={last:0};this.events={};var m=this,o=0;f.forEach(p,function(q,s){var d=n.createElement("video");d.preload="auto";d.controls=true;d.style.display=s&&"none"||"";d.id=m.seqId+"-"+s;m.queue.push(d);var A=q["in"],y=q.out;m.inOuts.ofVideos.push({"in":A!==undefined&&A||1,out:y!==undefined&&y||0});m.inOuts.ofVideos[s].out=m.inOuts.ofVideos[s].out||m.inOuts.ofVideos[s]["in"]+2;d.src=!b.test(q.src)?e+q.src:q.src;d.setAttribute("data-sequence-owner",j);d.setAttribute("data-sequence-guid", +m.seqId);d.setAttribute("data-sequence-id",s);d.setAttribute("data-sequence-clip",[m.inOuts.ofVideos[s]["in"],m.inOuts.ofVideos[s].out].join(":"));m.parent.appendChild(d);m.playlist.push(f("#"+d.id))});m.inOuts.ofVideos.forEach(function(q){q={"in":o,out:o+(q.out-q["in"])};m.inOuts.ofClips.push(q);o=q.out+1});f.forEach(this.queue,function(q,s){function d(){if(!s){m.dims.width=q.videoWidth;m.dims.height=q.videoHeight}q.currentTime=m.inOuts.ofVideos[s]["in"]-0.5;q.removeEventListener("canplaythrough", +d,false);return true}q.addEventListener("canplaythrough",d,false);q.addEventListener("play",function(){m.playing=true},false);q.addEventListener("pause",function(){m.playing=false},false);q.addEventListener("timeupdate",function(A){A=A.srcElement||A.target;A=+(A.dataset&&A.dataset.sequenceId||A.getAttribute("data-sequence-id"));var y=Math.floor(q.currentTime);if(m.times.last!==y&&A===m.active){m.times.last=y;y===m.inOuts.ofVideos[A].out&&f.sequence.cycle.call(m,A)}},false)});return this};f.sequence.init.prototype= +f.sequence.prototype;f.sequence.cycle=function(j){this.queue||f.error("Popcorn.sequence.cycle is not a public method");var p=this.queue,m=this.inOuts.ofVideos,o=p[j],q=0,s;if(p[j+1])q=j+1;if(p[j+1]){p=p[q];m=m[q];f.extend(p,{width:this.dims.width,height:this.dims.height});s=this.playlist[q];o.pause();this.active=q;this.times.last=m["in"]-1;s.currentTime(m["in"]);s[q?"play":"pause"]();this.trigger("cycle",{position:{previous:j,current:q}});if(q){o.style.display="none";p.style.display=""}this.cycling= +false}else this.playlist[j].pause();return this};var i=["timeupdate","play","pause"];f.extend(f.sequence.prototype,{eq:function(j){return this.playlist[j]},remove:function(){this.parent.innerHTML=null},clip:function(j){return this.inOuts.ofVideos[j]},duration:function(){for(var j=0,p=this.inOuts.ofClips,m=0;m<p.length;m++)j+=p[m].out-p[m]["in"]+1;return j-1},play:function(){this.playlist[this.active].play();return this},exec:function(j,p){var m=this.active;this.inOuts.ofClips.forEach(function(o,q){if(j>= +o["in"]&&j<=o.out)m=q});j+=this.inOuts.ofVideos[m]["in"]-this.inOuts.ofClips[m]["in"];f.addTrackEvent(this.playlist[m],{start:j-1,end:j,_running:false,_natives:{start:p||f.nop,end:f.nop,type:"exec"}});return this},listen:function(j,p){var m=this,o=this.playlist,q=o.length,s=0;if(!p)p=f.nop;if(f.Events.Natives.indexOf(j)>-1)f.forEach(o,function(d){d.listen(j,function(A){A.active=m;if(i.indexOf(j)>-1)p.call(d,A);else++s===q&&p.call(d,A)})});else{this.events[j]||(this.events[j]={});o=p.name||f.guid("__"+ +j);this.events[j][o]=p}return this},unlisten:function(){},trigger:function(j,p){var m=this;if(!(f.Events.Natives.indexOf(j)>-1)){this.events[j]&&f.forEach(this.events[j],function(o){o.call(m,{type:j},p)});return this}}});f.forEach(f.manifest,function(j,p){f.sequence.prototype[p]=function(m){var o={},q=[],s,d,A,y,x;for(s=0;s<this.inOuts.ofClips.length;s++){q=this.inOuts.ofClips[s];d=h(q["in"],q.out);A=d.indexOf(m.start);y=d.indexOf(m.end);if(A>-1)o[s]=f.extend({},q,{start:d[A],clipIdx:A});if(y>-1)o[s]= +f.extend({},q,{end:d[y],clipIdx:y})}s=Object.keys(o).map(function(g){return+g});q=h(s[0],s[1]);for(s=0;s<q.length;s++){A={};y=q[s];var a=o[y];if(a){x=this.inOuts.ofVideos[y];d=a.clipIdx;x=h(x["in"],x.out);if(a.start){A.start=x[d];A.end=x[x.length-1]}if(a.end){A.start=x[0];A.end=x[d]}}else{A.start=this.inOuts.ofVideos[y]["in"];A.end=this.inOuts.ofVideos[y].out}this.playlist[y][p](f.extend({},m,A))}return this}})})(this,Popcorn);(function(r){document.addEventListener("DOMContentLoaded",function(){var f=document.querySelectorAll("[data-timeline-sources]");r.forEach(f,function(n,c){var b=f[c],e,h,i;if(!b.id)b.id=r.guid("__popcorn");if(b.nodeType&&b.nodeType===1){i=r("#"+b.id);e=(b.getAttribute("data-timeline-sources")||"").split(",");e[0]&&r.forEach(e,function(j){h=j.split("!");if(h.length===1){h=j.match(/(.*)[\/\\]([^\/\\]+\.\w+)$/)[2].split(".");h[0]="parse"+h[1].toUpperCase();h[1]=j}e[0]&&i[h[0]]&&i[h[0]](h[1])});i.autoplay()&& +i.play()}})},false)})(Popcorn);(function(r,f){function n(e){e=typeof e==="string"?e:[e.language,e.region].join("-");var h=e.split("-");return{iso6391:e,language:h[0]||"",region:h[1]||""}}var c=r.navigator,b=n(c.userLanguage||c.language);f.locale={get:function(){return b},set:function(e){b=n(e);f.locale.broadcast();return b},broadcast:function(e){var h=f.instances,i=h.length,j=0,p;for(e=e||"locale:changed";j<i;j++){p=h[j];e in p.data.events&&p.trigger(e)}}}})(this,this.Popcorn);(function(r){var f=Object.prototype.hasOwnProperty;r.parsers={};r.parser=function(n,c,b){if(r.protect.natives.indexOf(n.toLowerCase())>=0)r.error("'"+n+"' is a protected function name");else{if(typeof c==="function"&&!b){b=c;c=""}if(!(typeof b!=="function"||typeof c!=="string")){var e={};e[n]=function(h,i){if(!h)return this;var j=this;r.xhr({url:h,dataType:c,success:function(p){var m,o,q=0;p=b(p).data||[];if(m=p.length){for(;q<m;q++){o=p[q];for(var s in o)f.call(o,s)&&j[s]&&j[s](o[s])}i&&i()}}}); +return this};r.extend(r.p,e);return e}}}})(Popcorn);(function(r){var f=function(b,e){b=b||r.nop;e=e||r.nop;return function(){b.apply(this,arguments);e.apply(this,arguments)}},n=/^.*\.(ogg|oga|aac|mp3|wav)($|\?)/,c=/^.*\.(ogg|oga|aac|mp3|wav|ogg|ogv|mp4|webm)($|\?)/;r.player=function(b,e){if(!r[b]){e=e||{};var h=function(i,j,p){p=p||{};var m=new Date/1E3,o=m,q=0,s=0,d=1,A=false,y={},x=typeof i==="string"?r.dom.find(i):i,a={};Object.prototype.__defineGetter__||(a=x||document.createElement("div"));for(var g in x)if(!(g in a))if(typeof x[g]==="object")a[g]= +x[g];else if(typeof x[g]==="function")a[g]=function(k){return"length"in x[k]&&!x[k].call?x[k]:function(){return x[k].apply(x,arguments)}}(g);else r.player.defineProperty(a,g,{get:function(k){return function(){return x[k]}}(g),set:r.nop,configurable:true});var l=function(){m=new Date/1E3;if(!a.paused){a.currentTime+=m-o;a.dispatchEvent("timeupdate");setTimeout(l,10)}o=m};a.play=function(){this.paused=false;if(a.readyState>=4){o=new Date/1E3;a.dispatchEvent("play");l()}};a.pause=function(){this.paused= +true;a.dispatchEvent("pause")};r.player.defineProperty(a,"currentTime",{get:function(){return q},set:function(k){q=+k;a.dispatchEvent("timeupdate");return q},configurable:true});r.player.defineProperty(a,"volume",{get:function(){return d},set:function(k){d=+k;a.dispatchEvent("volumechange");return d},configurable:true});r.player.defineProperty(a,"muted",{get:function(){return A},set:function(k){A=+k;a.dispatchEvent("volumechange");return A},configurable:true});r.player.defineProperty(a,"readyState", +{get:function(){return s},set:function(k){return s=k},configurable:true});a.addEventListener=function(k,t){y[k]||(y[k]=[]);y[k].push(t);return t};a.removeEventListener=function(k,t){var u,v=y[k];if(v){for(u=y[k].length-1;u>=0;u--)t===v[u]&&v.splice(u,1);return t}};a.dispatchEvent=function(k){var t,u=k.type;if(!u){u=k;if(k=r.events.getInterface(u)){t=document.createEvent(k);t.initEvent(u,true,true,window,1)}}if(y[u])for(k=y[u].length-1;k>=0;k--)y[u][k].call(this,t,this)};a.src=j||"";a.duration=0;a.paused= +true;a.ended=0;p&&p.events&&r.forEach(p.events,function(k,t){a.addEventListener(t,k,false)});if(e._canPlayType(x.nodeName,j)!==false)if(e._setup)e._setup.call(a,p);else{a.readyState=4;a.dispatchEvent("loadedmetadata");a.dispatchEvent("loadeddata");a.dispatchEvent("canplaythrough")}else setTimeout(function(){a.dispatchEvent("error")},0);i=new r.p.init(a,p);if(e._teardown)i.destroy=f(i.destroy,function(){e._teardown.call(a,p)});return i};h.canPlayType=e._canPlayType=e._canPlayType||r.nop;r[b]=r.player.registry[b]= +h}};r.player.registry={};r.player.defineProperty=Object.defineProperty||function(b,e,h){b.__defineGetter__(e,h.get||r.nop);b.__defineSetter__(e,h.set||r.nop)};r.player.playerQueue=function(){var b=[],e=false;return{next:function(){e=false;b.shift();b[0]&&b[0]()},add:function(h){b.push(function(){e=true;h&&h()});!e&&b[0]()}}};r.smart=function(b,e,h){var i=["AUDIO","VIDEO"],j,p=r.dom.find(b),m;j=document.createElement("video");var o={ogg:"video/ogg",ogv:"video/ogg",oga:"audio/ogg",webm:"video/webm", +mp4:"video/mp4",mp3:"audio/mp3"};if(p){if(i.indexOf(p.nodeName)>-1&&!e){if(typeof e==="object")h=e;return r(p,h)}if(typeof e==="string")e=[e];b=0;for(srcLength=e.length;b<srcLength;b++){m=c.exec(e[b]);m=!m||!m[1]?false:j.canPlayType(o[m[1]]);if(m){e=e[b];break}for(var q in r.player.registry)if(r.player.registry.hasOwnProperty(q))if(r.player.registry[q].canPlayType(p.nodeName,e[b]))return r[q](p,e[b],h)}if(i.indexOf(p.nodeName)===-1){j=typeof e==="string"?e:e.length?e[0]:e;b=document.createElement(n.exec(j)? +i[0]:i[1]);b.controls=true;p.appendChild(b);p=b}h&&h.events&&h.events.error&&p.addEventListener("error",h.events.error,false);p.src=e;return r(p,h)}else r.error("Specified target "+b+" was not found.")}})(Popcorn);(function(r){var f=function(n,c){var b=0,e=0,h;r.forEach(c.classes,function(i,j){h=[];if(i==="parent")h[0]=document.querySelectorAll("#"+c.target)[0].parentNode;else h=document.querySelectorAll("#"+c.target+" "+i);b=0;for(e=h.length;b<e;b++)h[b].classList.toggle(j)})};r.compose("applyclass",{manifest:{about:{name:"Popcorn applyclass Effect",version:"0.1",author:"@scottdowne",website:"scottdowne.wordpress.com"},options:{}},_setup:function(n){n.classes={};n.applyclass=n.applyclass||"";for(var c=n.applyclass.replace(/\s/g, +"").split(","),b=[],e=0,h=c.length;e<h;e++){b=c[e].split(":");if(b[0])n.classes[b[0]]=b[1]||""}},start:f,end:f})})(Popcorn);(function(r){var f=/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu|vimeo|soundcloud|baseplayer)/,n={},c={vimeo:false,youtube:false,soundcloud:false,module:false};Object.defineProperty(n,void 0,{get:function(){return c[void 0]},set:function(b){c[void 0]=b}});r.plugin("mediaspawner",{manifest:{about:{name:"Popcorn Media Spawner Plugin",version:"0.1",author:"Matthew Schranz, @mjschranz",website:"mschranz.wordpress.com"},options:{source:{elem:"input",type:"text",label:"Media Source","default":"http://www.youtube.com/watch?v=CXDstfD9eJ0"}, +caption:{elem:"input",type:"text",label:"Media Caption","default":"Popcorn Popping",optional:true},target:"mediaspawner-container",start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},autoplay:{elem:"input",type:"checkbox",label:"Autoplay Video",optional:true},width:{elem:"input",type:"number",label:"Media Width","default":400,units:"px",optional:true},height:{elem:"input",type:"number",label:"Media Height","default":200,units:"px",optional:true}}},_setup:function(b){function e(){function o(){if(j!== +"HTML5"&&!window.Popcorn[j])setTimeout(function(){o()},300);else{b.id=b._container.id;b._container.style.width=b.width+"px";b._container.style.height=b.height+"px";b.popcorn=r.smart("#"+b.id,b.source);j==="HTML5"&&b.popcorn.controls(true);b._container.style.width="0px";b._container.style.height="0px";b._container.style.visibility="hidden";b._container.style.overflow="hidden"}}if(j!=="HTML5"&&!window.Popcorn[j]&&!n[j]){n[j]=true;r.getScript("http://popcornjs.org/code/players/"+j+"/popcorn."+j+".js", +function(){o()})}else o()}function h(){window.Popcorn.player?e():setTimeout(function(){h()},300)}var i=document.getElementById(b.target)||{},j,p,m;if(p=f.exec(b.source)){j=p[1];if(j==="youtu")j="youtube"}else j="HTML5";b._type=j;b._container=document.createElement("div");p=b._container;p.id="mediaSpawnerdiv-"+r.guid();b.width=b.width||400;b.height=b.height||200;if(b.caption){m=document.createElement("div");m.innerHTML=b.caption;m.style.display="none";b._capCont=m;p.appendChild(m)}i&&i.appendChild(p); +if(!window.Popcorn.player&&!n.module){n.module=true;r.getScript("http://popcornjs.org/code/modules/player/popcorn.player.js",h)}else h()},start:function(b,e){if(e._capCont)e._capCont.style.display="";e._container.style.width=e.width+"px";e._container.style.height=e.height+"px";e._container.style.visibility="visible";e._container.style.overflow="visible";e.autoplay&&e.popcorn.play()},end:function(b,e){if(e._capCont)e._capCont.style.display="none";e._container.style.width="0px";e._container.style.height= +"0px";e._container.style.visibility="hidden";e._container.style.overflow="hidden";e.popcorn.pause()},_teardown:function(b){b.popcorn&&b.popcorn.destory&&b.popcorn.destroy();document.getElementById(b.target)&&document.getElementById(b.target).removeChild(b._container)}})})(Popcorn,this);(function(r){r.plugin("code",function(f){var n=false,c=this,b=function(){var e=function(h){return function(i,j){var p=function(){n&&i.call(c,j);n&&h(p)};p()}};return window.webkitRequestAnimationFrame?e(window.webkitRequestAnimationFrame):window.mozRequestAnimationFrame?e(window.mozRequestAnimationFrame):e(function(h){window.setTimeout(h,16)})}();if(!f.onStart||typeof f.onStart!=="function")f.onStart=r.nop;if(f.onEnd&&typeof f.onEnd!=="function")f.onEnd=undefined;if(f.onFrame&&typeof f.onFrame!== +"function")f.onFrame=undefined;return{start:function(e,h){h.onStart.call(c,h);if(h.onFrame){n=true;b(h.onFrame,h)}},end:function(e,h){if(h.onFrame)n=false;h.onEnd&&h.onEnd.call(c,h)}}},{about:{name:"Popcorn Code Plugin",version:"0.1",author:"David Humphrey (@humphd)",website:"http://vocamus.net/dave"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},onStart:{elem:"input",type:"function",label:"onStart"},onFrame:{elem:"input",type:"function",label:"onFrame", +optional:true},onEnd:{elem:"input",type:"function",label:"onEnd"}}})})(Popcorn);(function(r){var f=0;r.plugin("flickr",function(n){var c,b=document.getElementById(n.target),e,h,i,j,p=n.numberofimages||4,m=n.height||"50px",o=n.width||"50px",q=n.padding||"5px",s=n.border||"0px";c=document.createElement("div");c.id="flickr"+f;c.style.width="100%";c.style.height="100%";c.style.display="none";f++;b&&b.appendChild(c);var d=function(){if(e)setTimeout(function(){d()},5);else{h="http://api.flickr.com/services/rest/?method=flickr.people.findByUsername&";h+="username="+n.username+"&api_key="+ +n.apikey+"&format=json&jsoncallback=flickr";r.getJSONP(h,function(y){e=y.user.nsid;A()})}},A=function(){h="http://api.flickr.com/services/feeds/photos_public.gne?";if(e)h+="id="+e+"&";if(n.tags)h+="tags="+n.tags+"&";h+="lang=en-us&format=json&jsoncallback=flickr";r.xhr.getJSONP(h,function(y){var x=document.createElement("div");x.innerHTML="<p style='padding:"+q+";'>"+y.title+"<p/>";r.forEach(y.items,function(a,g){if(g<p){i=document.createElement("a");i.setAttribute("href",a.link);i.setAttribute("target", +"_blank");j=document.createElement("img");j.setAttribute("src",a.media.m);j.setAttribute("height",m);j.setAttribute("width",o);j.setAttribute("style","border:"+s+";padding:"+q);i.appendChild(j);x.appendChild(i)}else return false});c.appendChild(x)})};if(n.username&&n.apikey)d();else{e=n.userid;A()}return{start:function(){c.style.display="inline"},end:function(){c.style.display="none"},_teardown:function(y){document.getElementById(y.target)&&document.getElementById(y.target).removeChild(c)}}},{about:{name:"Popcorn Flickr Plugin", +version:"0.2",author:"Scott Downe, Steven Weerdenburg, Annasob",website:"http://scottdowne.wordpress.com/"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},userid:{elem:"input",type:"text",label:"User ID",optional:true},tags:{elem:"input",type:"text",label:"Tags"},username:{elem:"input",type:"text",label:"Username",optional:true},apikey:{elem:"input",type:"text",label:"API Key",optional:true},target:"flickr-container",height:{elem:"input",type:"text", +label:"Height","default":"50px",optional:true},width:{elem:"input",type:"text",label:"Width","default":"50px",optional:true},padding:{elem:"input",type:"text",label:"Padding",optional:true},border:{elem:"input",type:"text",label:"Border","default":"5px",optional:true},numberofimages:{elem:"input",type:"number","default":4,label:"Number of Images"}}})})(Popcorn);(function(r){r.plugin("footnote",{manifest:{about:{name:"Popcorn Footnote Plugin",version:"0.2",author:"@annasob, @rwaldron",website:"annasob.wordpress.com"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},text:{elem:"input",type:"text",label:"Text"},target:"footnote-container"}},_setup:function(f){var n=r.dom.find(f.target);f._container=document.createElement("div");f._container.style.display="none";f._container.innerHTML=f.text;n.appendChild(f._container)}, +start:function(f,n){n._container.style.display="inline"},end:function(f,n){n._container.style.display="none"},_teardown:function(f){var n=r.dom.find(f.target);n&&n.removeChild(f._container)}})})(Popcorn);(function(r){function f(b){return String(b).replace(/&(?!\w+;)|[<>"']/g,function(e){return c[e]||e})}function n(b,e){var h=b.container=document.createElement("div"),i=h.style,j=b.media,p=function(){var m=b.position();i.fontSize="18px";i.width=j.offsetWidth+"px";i.top=m.top+j.offsetHeight-h.offsetHeight-40+"px";i.left=m.left+"px";setTimeout(p,10)};h.id=e||"";i.position="absolute";i.color="white";i.textShadow="black 2px 2px 6px";i.fontWeight="bold";i.textAlign="center";p();b.media.parentNode.appendChild(h); +return h}var c={"&":"&","<":"<",">":">",'"':""","'":"'"};r.plugin("text",{manifest:{about:{name:"Popcorn Text Plugin",version:"0.1",author:"@humphd"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},text:{elem:"input",type:"text",label:"Text","default":"Popcorn.js"},escape:{elem:"input",type:"checkbox",label:"Escape"},multiline:{elem:"input",type:"checkbox",label:"Multiline"}}},_setup:function(b){var e,h,i=b._container=document.createElement("div"); +i.style.display="none";if(b.target)if(e=r.dom.find(b.target)){if(["VIDEO","AUDIO"].indexOf(e.nodeName)>-1)e=n(this,b.target+"-overlay")}else e=n(this,b.target);else e=this.container?this.container:n(this);b._target=e;h=b.escape?f(b.text):b.text;h=b.multiline?h.replace(/\r?\n/gm,"<br>"):h;i.innerHTML=h||"";e.appendChild(i)},start:function(b,e){e._container.style.display="inline"},end:function(b,e){e._container.style.display="none"},_teardown:function(b){var e=b._target;e&&e.removeChild(b._container)}})})(Popcorn);var googleCallback; +(function(r){function f(i,j,p){i=i.type?i.type.toUpperCase():"HYBRID";var m;if(i==="STAMEN-WATERCOLOR"||i==="STAMEN-TERRAIN"||i==="STAMEN-TONER")m=i.replace("STAMEN-","").toLowerCase();p=new google.maps.Map(p,{mapTypeId:m?m:google.maps.MapTypeId[i],mapTypeControlOptions:{mapTypeIds:[]}});m&&p.mapTypes.set(m,new google.maps.StamenMapType(m));p.getDiv().style.display="none";return p}var n=1,c=false,b=false,e,h;googleCallback=function(i){if(typeof google!=="undefined"&&google.maps&&google.maps.Geocoder&& +google.maps.LatLng){e=new google.maps.Geocoder;r.getScript("//maps.stamen.com/js/tile.stamen.js",function(){b=true})}else setTimeout(function(){googleCallback(i)},1)};h=function(){if(document.body){c=true;r.getScript("//maps.google.com/maps/api/js?sensor=false&callback=googleCallback")}else setTimeout(function(){h()},1)};r.plugin("googlemap",function(i){var j,p,m,o=document.getElementById(i.target);i.type=i.type||"ROADMAP";i.zoom=i.zoom||1;i.lat=i.lat||0;i.lng=i.lng||0;c||h();j=document.createElement("div"); +j.id="actualmap"+n;j.style.width=i.width||"100%";j.style.height=i.height?i.height:o&&o.clientHeight?o.clientHeight+"px":"100%";n++;o&&o.appendChild(j);var q=function(){if(b){if(j)if(i.location)e.geocode({address:i.location},function(s,d){if(j&&d===google.maps.GeocoderStatus.OK){i.lat=s[0].geometry.location.lat();i.lng=s[0].geometry.location.lng();m=new google.maps.LatLng(i.lat,i.lng);p=f(i,m,j)}});else{m=new google.maps.LatLng(i.lat,i.lng);p=f(i,m,j)}}else setTimeout(function(){q()},5)};q();return{start:function(s, +d){var A=this,y,x=function(){if(p){d._map=p;p.getDiv().style.display="block";google.maps.event.trigger(p,"resize");p.setCenter(m);if(d.zoom&&typeof d.zoom!=="number")d.zoom=+d.zoom;p.setZoom(d.zoom);if(d.heading&&typeof d.heading!=="number")d.heading=+d.heading;if(d.pitch&&typeof d.pitch!=="number")d.pitch=+d.pitch;if(d.type==="STREETVIEW"){p.setStreetView(y=new google.maps.StreetViewPanorama(j,{position:m,pov:{heading:d.heading=d.heading||0,pitch:d.pitch=d.pitch||0,zoom:d.zoom}}));var a=function(z, +C){var E=google.maps.geometry.spherical.computeHeading;setTimeout(function(){var B=A.media.currentTime;if(typeof d.tween==="object"){for(var w=0,D=z.length;w<D;w++){var F=z[w];if(B>=F.interval*(w+1)/1E3&&(B<=F.interval*(w+2)/1E3||B>=F.interval*D/1E3)){u.setPosition(new google.maps.LatLng(F.position.lat,F.position.lng));u.setPov({heading:F.pov.heading||E(F,z[w+1])||0,zoom:F.pov.zoom||0,pitch:F.pov.pitch||0})}}a(z,z[0].interval)}else{w=0;for(D=z.length;w<D;w++){F=d.interval;if(B>=F*(w+1)/1E3&&(B<=F* +(w+2)/1E3||B>=F*D/1E3)){g.setPov({heading:E(z[w],z[w+1])||0,zoom:d.zoom,pitch:d.pitch||0});g.setPosition(l[w])}}a(l,d.interval)}},C)};if(d.location&&typeof d.tween==="string"){var g=y,l=[],k=new google.maps.DirectionsService,t=new google.maps.DirectionsRenderer(g);k.route({origin:d.location,destination:d.tween,travelMode:google.maps.TravelMode.DRIVING},function(z,C){if(C==google.maps.DirectionsStatus.OK){t.setDirections(z);for(var E=z.routes[0].overview_path,B=0,w=E.length;B<w;B++)l.push(new google.maps.LatLng(E[B].lat(), +E[B].lng()));d.interval=d.interval||1E3;a(l,10)}})}else if(typeof d.tween==="object"){var u=y;k=0;for(var v=d.tween.length;k<v;k++){d.tween[k].interval=d.tween[k].interval||1E3;a(d.tween,10)}}}d.onmaploaded&&d.onmaploaded(d,p)}else setTimeout(function(){x()},13)};x()},end:function(){if(p)p.getDiv().style.display="none"},_teardown:function(s){var d=document.getElementById(s.target);d&&d.removeChild(j);j=p=m=null;s._map=null}}},{about:{name:"Popcorn Google Map Plugin",version:"0.1",author:"@annasob", +website:"annasob.wordpress.com"},options:{start:{elem:"input",type:"start",label:"Start"},end:{elem:"input",type:"start",label:"End"},target:"map-container",type:{elem:"select",options:["ROADMAP","SATELLITE","STREETVIEW","HYBRID","TERRAIN","STAMEN-WATERCOLOR","STAMEN-TERRAIN","STAMEN-TONER"],label:"Map Type",optional:true},zoom:{elem:"input",type:"text",label:"Zoom","default":0,optional:true},lat:{elem:"input",type:"text",label:"Lat",optional:true},lng:{elem:"input",type:"text",label:"Lng",optional:true}, +location:{elem:"input",type:"text",label:"Location","default":"Toronto, Ontario, Canada"},heading:{elem:"input",type:"text",label:"Heading","default":0,optional:true},pitch:{elem:"input",type:"text",label:"Pitch","default":1,optional:true}}})})(Popcorn);(function(r){function f(b){function e(){var p=b.getBoundingClientRect(),m=i.getBoundingClientRect();if(m.left!==p.left)i.style.left=p.left+"px";if(m.top!==p.top)i.style.top=p.top+"px"}var h=-1,i=document.createElement("div"),j=getComputedStyle(b).zIndex;i.setAttribute("data-popcorn-helper-container",true);i.style.position="absolute";i.style.zIndex=isNaN(j)?n:j+1;document.body.appendChild(i);return{element:i,start:function(){h=setInterval(e,c)},stop:function(){clearInterval(h);h=-1},destroy:function(){document.body.removeChild(i); +h!==-1&&clearInterval(h)}}}var n=2E3,c=10;r.plugin("image",{manifest:{about:{name:"Popcorn image Plugin",version:"0.1",author:"Scott Downe",website:"http://scottdowne.wordpress.com/"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},src:{elem:"input",type:"url",label:"Image URL","default":"http://mozillapopcorn.org/wp-content/themes/popcorn/images/for_developers.png"},href:{elem:"input",type:"url",label:"Link","default":"http://mozillapopcorn.org/wp-content/themes/popcorn/images/for_developers.png", +optional:true},target:"image-container",text:{elem:"input",type:"text",label:"Caption","default":"Popcorn.js",optional:true}}},_setup:function(b){var e=document.createElement("img"),h=document.getElementById(b.target);b.anchor=document.createElement("a");b.anchor.style.position="relative";b.anchor.style.textDecoration="none";b.anchor.style.display="none";if(h)if(["VIDEO","AUDIO"].indexOf(h.nodeName)>-1){b.trackedContainer=f(h);b.trackedContainer.element.appendChild(b.anchor)}else h&&h.appendChild(b.anchor); +e.addEventListener("load",function(){e.style.borderStyle="none";b.anchor.href=b.href||b.src||"#";b.anchor.target="_blank";var i,j;e.style.height=h.style.height;e.style.width=h.style.width;b.anchor.appendChild(e);if(b.text){i=e.height/12+"px";j=document.createElement("div");r.extend(j.style,{color:"black",fontSize:i,fontWeight:"bold",position:"relative",textAlign:"center",width:e.style.width||e.width+"px",zIndex:"10"});j.innerHTML=b.text||"";j.style.top=(e.style.height.replace("px","")||e.height)/ +2-j.offsetHeight/2+"px";b.anchor.insertBefore(j,e)}},false);e.src=b.src},start:function(b,e){e.anchor.style.display="inline";e.trackedContainer&&e.trackedContainer.start()},end:function(b,e){e.anchor.style.display="none";e.trackedContainer&&e.trackedContainer.stop()},_teardown:function(b){if(b.trackedContainer)b.trackedContainer.destroy();else b.anchor.parentNode&&b.anchor.parentNode.removeChild(b.anchor)}})})(Popcorn);(function(r){var f=1,n=false;r.plugin("googlefeed",function(c){var b=function(){var j=false,p=0,m=document.getElementsByTagName("link"),o=m.length,q=document.head||document.getElementsByTagName("head")[0],s=document.createElement("link");if(window.GFdynamicFeedControl)n=true;else r.getScript("//www.google.com/uds/solutions/dynamicfeed/gfdynamicfeedcontrol.js",function(){n=true});for(;p<o;p++)if(m[p].href==="//www.google.com/uds/solutions/dynamicfeed/gfdynamicfeedcontrol.css")j=true;if(!j){s.type= +"text/css";s.rel="stylesheet";s.href="//www.google.com/uds/solutions/dynamicfeed/gfdynamicfeedcontrol.css";q.insertBefore(s,q.firstChild)}};window.google?b():r.getScript("//www.google.com/jsapi",function(){google.load("feeds","1",{callback:function(){b()}})});var e=document.createElement("div"),h=document.getElementById(c.target),i=function(){if(n)c.feed=new GFdynamicFeedControl(c.url,e,{vertical:c.orientation.toLowerCase()==="vertical"?true:false,horizontal:c.orientation.toLowerCase()==="horizontal"? +true:false,title:c.title=c.title||"Blog"});else setTimeout(function(){i()},5)};if(!c.orientation||c.orientation.toLowerCase()!=="vertical"&&c.orientation.toLowerCase()!=="horizontal")c.orientation="vertical";e.style.display="none";e.id="_feed"+f;e.style.width="100%";e.style.height="100%";f++;h&&h.appendChild(e);i();return{start:function(){e.setAttribute("style","display:inline")},end:function(){e.setAttribute("style","display:none")},_teardown:function(j){document.getElementById(j.target)&&document.getElementById(j.target).removeChild(e); +delete j.feed}}},{about:{name:"Popcorn Google Feed Plugin",version:"0.1",author:"David Seifried",website:"dseifried.wordpress.com"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},target:"feed-container",url:{elem:"input",type:"url",label:"Feed URL","default":"http://planet.mozilla.org/rss20.xml"},title:{elem:"input",type:"text",label:"Title","default":"Planet Mozilla",optional:true},orientation:{elem:"select",options:["Vertical","Horizontal"], +label:"Orientation","default":"Vertical",optional:true}}})})(Popcorn);(function(r){var f=0,n=function(c,b){var e=c.container=document.createElement("div"),h=e.style,i=c.media,j=function(){var p=c.position();h.fontSize="18px";h.width=i.offsetWidth+"px";h.top=p.top+i.offsetHeight-e.offsetHeight-40+"px";h.left=p.left+"px";setTimeout(j,10)};e.id=b||r.guid();h.position="absolute";h.color="white";h.textShadow="black 2px 2px 6px";h.fontWeight="bold";h.textAlign="center";j();c.media.parentNode.appendChild(e);return e};r.plugin("subtitle",{manifest:{about:{name:"Popcorn Subtitle Plugin", +version:"0.1",author:"Scott Downe",website:"http://scottdowne.wordpress.com/"},options:{start:{elem:"input",type:"text",label:"Start"},end:{elem:"input",type:"text",label:"End"},target:"subtitle-container",text:{elem:"input",type:"text",label:"Text"}}},_setup:function(c){var b=document.createElement("div");b.id="subtitle-"+f++;b.style.display="none";!this.container&&(!c.target||c.target==="subtitle-container")&&n(this);c.container=c.target&&c.target!=="subtitle-container"?document.getElementById(c.target)|| +n(this,c.target):this.container;document.getElementById(c.container.id)&&document.getElementById(c.container.id).appendChild(b);c.innerContainer=b;c.showSubtitle=function(){c.innerContainer.innerHTML=c.text||""}},start:function(c,b){b.innerContainer.style.display="inline";b.showSubtitle(b,b.text)},end:function(c,b){b.innerContainer.style.display="none";b.innerContainer.innerHTML=""},_teardown:function(c){c.container.removeChild(c.innerContainer)}})})(Popcorn);(function(r){var f=false;r.plugin("twitter",{manifest:{about:{name:"Popcorn Twitter Plugin",version:"0.1",author:"Scott Downe",website:"http://scottdowne.wordpress.com/"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},src:{elem:"input",type:"text",label:"Tweet Source (# or @)","default":"@popcornjs"},target:"twitter-container",height:{elem:"input",type:"number",label:"Height","default":"200",optional:true},width:{elem:"input",type:"number",label:"Width", +"default":"250",optional:true}}},_setup:function(n){if(!window.TWTR&&!f){f=true;r.getScript("//widgets.twimg.com/j/2/widget.js")}var c=document.getElementById(n.target);n.container=document.createElement("div");n.container.setAttribute("id",r.guid());n.container.style.display="none";c&&c.appendChild(n.container);var b=n.src||"";c=n.width||250;var e=n.height||200,h=/^@/.test(b),i={version:2,id:n.container.getAttribute("id"),rpp:30,width:c,height:e,interval:6E3,theme:{shell:{background:"#ffffff",color:"#000000"}, +tweets:{background:"#ffffff",color:"#444444",links:"#1985b5"}},features:{loop:true,timestamp:true,avatars:true,hashtags:true,toptweets:true,live:true,scrollbar:false,behavior:"default"}},j=function(p){if(window.TWTR)if(h){i.type="profile";(new TWTR.Widget(i)).render().setUser(b).start()}else{i.type="search";i.search=b;i.subject=b;(new TWTR.Widget(i)).render().start()}else setTimeout(function(){j(p)},1)};j(this)},start:function(n,c){c.container.style.display="inline"},end:function(n,c){c.container.style.display= +"none"},_teardown:function(n){document.getElementById(n.target)&&document.getElementById(n.target).removeChild(n.container)}})})(Popcorn);(function(r){r.plugin("webpage",{manifest:{about:{name:"Popcorn Webpage Plugin",version:"0.1",author:"@annasob",website:"annasob.wordpress.com"},options:{id:{elem:"input",type:"text",label:"Id",optional:true},start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},src:{elem:"input",type:"url",label:"Webpage URL","default":"http://mozillapopcorn.org"},target:"iframe-container"}},_setup:function(f){var n=document.getElementById(f.target);f.src=f.src.replace(/^(https?:)?(\/\/)?/, +"//");f._iframe=document.createElement("iframe");f._iframe.setAttribute("width","100%");f._iframe.setAttribute("height","100%");f._iframe.id=f.id;f._iframe.src=f.src;f._iframe.style.display="none";n&&n.appendChild(f._iframe)},start:function(f,n){n._iframe.src=n.src;n._iframe.style.display="inline"},end:function(f,n){n._iframe.style.display="none"},_teardown:function(f){document.getElementById(f.target)&&document.getElementById(f.target).removeChild(f._iframe)}})})(Popcorn);var wikiCallback; +(function(r){r.plugin("wikipedia",{manifest:{about:{name:"Popcorn Wikipedia Plugin",version:"0.1",author:"@annasob",website:"annasob.wordpress.com"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},lang:{elem:"input",type:"text",label:"Language","default":"english",optional:true},src:{elem:"input",type:"url",label:"Wikipedia URL","default":"http://en.wikipedia.org/wiki/Cat"},title:{elem:"input",type:"text",label:"Title","default":"Cats",optional:true}, +numberofwords:{elem:"input",type:"number",label:"Number of Words","default":"200",optional:true},target:"wikipedia-container"}},_setup:function(f){var n,c=r.guid();if(!f.lang)f.lang="en";f.numberofwords=f.numberofwords||200;window["wikiCallback"+c]=function(b){f._link=document.createElement("a");f._link.setAttribute("href",f.src);f._link.setAttribute("target","_blank");f._link.innerHTML=f.title||b.parse.displaytitle;f._desc=document.createElement("p");n=b.parse.text["*"].substr(b.parse.text["*"].indexOf("<p>")); +n=n.replace(/((<(.|\n)+?>)|(\((.*?)\) )|(\[(.*?)\]))/g,"");n=n.split(" ");f._desc.innerHTML=n.slice(0,n.length>=f.numberofwords?f.numberofwords:n.length).join(" ")+" ...";f._fired=true};f.src&&r.getScript("//"+f.lang+".wikipedia.org/w/api.php?action=parse&props=text&redirects&page="+f.src.slice(f.src.lastIndexOf("/")+1)+"&format=json&callback=wikiCallback"+c)},start:function(f,n){var c=function(){if(n._fired){if(n._link&&n._desc)if(document.getElementById(n.target)){document.getElementById(n.target).appendChild(n._link); +document.getElementById(n.target).appendChild(n._desc);n._added=true}}else setTimeout(function(){c()},13)};c()},end:function(f,n){if(n._added){document.getElementById(n.target).removeChild(n._link);document.getElementById(n.target).removeChild(n._desc)}},_teardown:function(f){if(f._added){f._link.parentNode&&document.getElementById(f.target).removeChild(f._link);f._desc.parentNode&&document.getElementById(f.target).removeChild(f._desc);delete f.target}}})})(Popcorn);(function(r){r.plugin("mustache",function(f){var n,c,b,e;r.getScript("http://mustache.github.com/extras/mustache.js");var h=!!f.dynamic,i=typeof f.template,j=typeof f.data,p=document.getElementById(f.target);f.container=p||document.createElement("div");if(i==="function")if(h)b=f.template;else e=f.template(f);else e=i==="string"?f.template:"";if(j==="function")if(h)n=f.data;else c=f.data(f);else c=j==="string"?JSON.parse(f.data):j==="object"?f.data:"";return{start:function(m,o){var q=function(){if(window.Mustache){if(n)c= +n(o);if(b)e=b(o);var s=Mustache.to_html(e,c).replace(/^\s*/mg,"");o.container.innerHTML=s}else setTimeout(function(){q()},10)};q()},end:function(m,o){o.container.innerHTML=""},_teardown:function(){n=c=b=e=null}}},{about:{name:"Popcorn Mustache Plugin",version:"0.1",author:"David Humphrey (@humphd)",website:"http://vocamus.net/dave"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},target:"mustache-container",template:{elem:"input",type:"text", +label:"Template"},data:{elem:"input",type:"text",label:"Data"},dynamic:{elem:"input",type:"checkbox",label:"Dynamic","default":true}}})})(Popcorn);(function(r){function f(c,b){if(c.map)c.map.div.style.display=b;else setTimeout(function(){f(c,b)},10)}var n=1;r.plugin("openmap",function(c){var b,e,h,i,j,p,m,o,q=document.getElementById(c.target);b=document.createElement("div");b.id="openmapdiv"+n;b.style.width="100%";b.style.height="100%";n++;q&&q.appendChild(b);o=function(){if(window.OpenLayers&&window.OpenLayers.Layer.Stamen){if(c.location){location=new OpenLayers.LonLat(0,0);r.getJSONP("//tinygeocoder.com/create-api.php?q="+c.location+"&callback=jsonp", +function(d){e=new OpenLayers.LonLat(d[1],d[0])})}else e=new OpenLayers.LonLat(c.lng,c.lat);c.type=c.type||"ROADMAP";switch(c.type){case "SATELLITE":c.map=new OpenLayers.Map({div:b,maxResolution:0.28125,tileSize:new OpenLayers.Size(512,512)});var s=new OpenLayers.Layer.WorldWind("LANDSAT","//worldwind25.arc.nasa.gov/tile/tile.aspx",2.25,4,{T:"105"});c.map.addLayer(s);i=new OpenLayers.Projection("EPSG:4326");h=new OpenLayers.Projection("EPSG:4326");break;case "TERRAIN":i=new OpenLayers.Projection("EPSG:4326"); +h=new OpenLayers.Projection("EPSG:4326");c.map=new OpenLayers.Map({div:b,projection:h});s=new OpenLayers.Layer.WMS("USGS Terraserver","//terraserver-usa.org/ogcmap.ashx?",{layers:"DRG"});c.map.addLayer(s);break;case "STAMEN-TONER":case "STAMEN-WATERCOLOR":case "STAMEN-TERRAIN":s=c.type.replace("STAMEN-","").toLowerCase();s=new OpenLayers.Layer.Stamen(s);i=new OpenLayers.Projection("EPSG:4326");h=new OpenLayers.Projection("EPSG:900913");e=e.transform(i,h);c.map=new OpenLayers.Map({div:b,projection:h, +displayProjection:i,controls:[new OpenLayers.Control.Navigation,new OpenLayers.Control.PanPanel,new OpenLayers.Control.ZoomPanel]});c.map.addLayer(s);break;default:h=new OpenLayers.Projection("EPSG:900913");i=new OpenLayers.Projection("EPSG:4326");e=e.transform(i,h);c.map=new OpenLayers.Map({div:b,projection:h,displayProjection:i});s=new OpenLayers.Layer.OSM;c.map.addLayer(s)}if(c.map){c.map.setCenter(e,c.zoom||10);c.map.div.style.display="none"}}else setTimeout(function(){o()},50)};o();return{_setup:function(s){window.OpenLayers|| +r.getScript("//openlayers.org/api/OpenLayers.js",function(){r.getScript("//maps.stamen.com/js/tile.stamen.js")});var d=function(){if(s.map){s.zoom=s.zoom||2;if(s.zoom&&typeof s.zoom!=="number")s.zoom=+s.zoom;s.map.setCenter(e,s.zoom);if(s.markers){var A=OpenLayers.Util.extend({},OpenLayers.Feature.Vector.style["default"]),y=function(v){clickedFeature=v.feature;if(clickedFeature.attributes.text){m=new OpenLayers.Popup.FramedCloud("featurePopup",clickedFeature.geometry.getBounds().getCenterLonLat(), +new OpenLayers.Size(120,250),clickedFeature.attributes.text,null,true,function(){p.unselect(this.feature)});clickedFeature.popup=m;m.feature=clickedFeature;s.map.addPopup(m)}},x=function(v){feature=v.feature;if(feature.popup){m.feature=null;s.map.removePopup(feature.popup);feature.popup.destroy();feature.popup=null}},a=function(v){r.getJSONP("//tinygeocoder.com/create-api.php?q="+v.location+"&callback=jsonp",function(z){z=(new OpenLayers.Geometry.Point(z[1],z[0])).transform(i,h);var C=OpenLayers.Util.extend({}, +A);if(!v.size||isNaN(v.size))v.size=14;C.pointRadius=v.size;C.graphicOpacity=1;C.externalGraphic=v.icon;z=new OpenLayers.Feature.Vector(z,null,C);if(v.text)z.attributes={text:v.text};j.addFeatures([z])})};j=new OpenLayers.Layer.Vector("Point Layer",{style:A});s.map.addLayer(j);for(var g=0,l=s.markers.length;g<l;g++){var k=s.markers[g];if(k.text)if(!p){p=new OpenLayers.Control.SelectFeature(j);s.map.addControl(p);p.activate();j.events.on({featureselected:y,featureunselected:x})}if(k.location)a(k); +else{var t=(new OpenLayers.Geometry.Point(k.lng,k.lat)).transform(i,h),u=OpenLayers.Util.extend({},A);if(!k.size||isNaN(k.size))k.size=14;u.pointRadius=k.size;u.graphicOpacity=1;u.externalGraphic=k.icon;t=new OpenLayers.Feature.Vector(t,null,u);if(k.text)t.attributes={text:k.text};j.addFeatures([t])}}}}else setTimeout(function(){d()},13)};d()},start:function(s,d){f(d,"block")},end:function(s,d){f(d,"none")},_teardown:function(){q&&q.removeChild(b);b=map=e=h=i=j=p=m=null}}},{about:{name:"Popcorn OpenMap Plugin", +version:"0.3",author:"@mapmeld",website:"mapadelsur.blogspot.com"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},target:"map-container",type:{elem:"select",options:["ROADMAP","SATELLITE","TERRAIN"],label:"Map Type",optional:true},zoom:{elem:"input",type:"number",label:"Zoom","default":2},lat:{elem:"input",type:"text",label:"Lat",optional:true},lng:{elem:"input",type:"text",label:"Lng",optional:true},location:{elem:"input",type:"text",label:"Location", +"default":"Toronto, Ontario, Canada"},markers:{elem:"input",type:"text",label:"List Markers",optional:true}}})})(Popcorn);document.addEventListener("click",function(r){r=r.target;if(r.nodeName==="A"||r.parentNode&&r.parentNode.nodeName==="A")Popcorn.instances.forEach(function(f){f.options.pauseOnLinkClicked&&f.pause()})},false);(function(r){var f={},n=0,c=document.createElement("span"),b=["webkit","Moz","ms","O",""],e=["Transform","TransitionDuration","TransitionTimingFunction"],h={},i;document.getElementsByTagName("head")[0].appendChild(c);for(var j=0,p=e.length;j<p;j++)for(var m=0,o=b.length;m<o;m++){i=b[m]+e[j];if(i in c.style){h[e[j].toLowerCase()]=i;break}}document.getElementsByTagName("head")[0].appendChild(c);r.plugin("wordriver",{manifest:{about:{name:"Popcorn WordRiver Plugin"},options:{start:{elem:"input",type:"number", +label:"Start"},end:{elem:"input",type:"number",label:"End"},target:"wordriver-container",text:{elem:"input",type:"text",label:"Text","default":"Popcorn.js"},color:{elem:"input",type:"text",label:"Color","default":"Green",optional:true}}},_setup:function(q){q._duration=q.end-q.start;var s;if(!(s=f[q.target])){s=q.target;f[s]=document.createElement("div");var d=document.getElementById(s);d&&d.appendChild(f[s]);f[s].style.height="100%";f[s].style.position="relative";s=f[s]}q._container=s;q.word=document.createElement("span"); +q.word.style.position="absolute";q.word.style.whiteSpace="nowrap";q.word.style.opacity=0;q.word.style.MozTransitionProperty="opacity, -moz-transform";q.word.style.webkitTransitionProperty="opacity, -webkit-transform";q.word.style.OTransitionProperty="opacity, -o-transform";q.word.style.transitionProperty="opacity, transform";q.word.style[h.transitionduration]="1s, "+q._duration+"s";q.word.style[h.transitiontimingfunction]="linear";q.word.innerHTML=q.text;q.word.style.color=q.color||"black"},start:function(q, +s){s._container.appendChild(s.word);s.word.style[h.transform]="";s.word.style.fontSize=~~(30+20*Math.random())+"px";n%=s._container.offsetWidth-s.word.offsetWidth;s.word.style.left=n+"px";n+=s.word.offsetWidth+10;s.word.style[h.transform]="translateY("+(s._container.offsetHeight-s.word.offsetHeight)+"px)";s.word.style.opacity=1;setTimeout(function(){s.word.style.opacity=0},(s.end-s.start-1||1)*1E3)},end:function(q,s){s.word.style.opacity=0},_teardown:function(q){var s=document.getElementById(q.target); +q.word.parentNode&&q._container.removeChild(q.word);f[q.target]&&!f[q.target].childElementCount&&s&&s.removeChild(f[q.target])&&delete f[q.target]}})})(Popcorn);(function(r){var f=1;r.plugin("timeline",function(n){var c=document.getElementById(n.target),b=document.createElement("div"),e,h=true;if(c&&!c.firstChild){c.appendChild(e=document.createElement("div"));e.style.width="inherit";e.style.height="inherit";e.style.overflow="auto"}else e=c.firstChild;b.style.display="none";b.id="timelineDiv"+f;n.direction=n.direction||"up";if(n.direction.toLowerCase()==="down")h=false;if(c&&e)h?e.insertBefore(b,e.firstChild):e.appendChild(b);f++;b.innerHTML="<p><span id='big' style='font-size:24px; line-height: 130%;' >"+ +n.title+"</span><br /><span id='mid' style='font-size: 16px;'>"+n.text+"</span><br />"+n.innerHTML;return{start:function(i,j){b.style.display="block";if(j.direction==="down")e.scrollTop=e.scrollHeight},end:function(){b.style.display="none"},_teardown:function(){e&&b&&e.removeChild(b)&&!e.firstChild&&c.removeChild(e)}}},{about:{name:"Popcorn Timeline Plugin",version:"0.1",author:"David Seifried @dcseifried",website:"dseifried.wordpress.com"},options:{start:{elem:"input",type:"number",label:"Start"}, +end:{elem:"input",type:"number",label:"End"},target:"feed-container",title:{elem:"input",type:"text",label:"Title"},text:{elem:"input",type:"text",label:"Text"},innerHTML:{elem:"input",type:"text",label:"HTML Code",optional:true},direction:{elem:"select",options:["DOWN","UP"],label:"Direction",optional:true}}})})(Popcorn);(function(r,f){var n={};r.plugin("documentcloud",{manifest:{about:{name:"Popcorn Document Cloud Plugin",version:"0.1",author:"@humphd, @ChrisDeCairos",website:"http://vocamus.net/dave"},options:{start:{elem:"input",type:"number",label:"Start"},end:{elem:"input",type:"number",label:"End"},target:"documentcloud-container",width:{elem:"input",type:"text",label:"Width",optional:true},height:{elem:"input",type:"text",label:"Height",optional:true},src:{elem:"input",type:"url",label:"PDF URL","default":"http://www.documentcloud.org/documents/70050-urbina-day-1-in-progress.html"}, +preload:{elem:"input",type:"checkbox",label:"Preload","default":true},page:{elem:"input",type:"number",label:"Page Number",optional:true},aid:{elem:"input",type:"number",label:"Annotation Id",optional:true}}},_setup:function(c){function b(){function m(v){c._key=v.api.getId();c._changeView=function(z){c.aid?z.pageSet.showAnnotation(z.api.getAnnotation(c.aid)):z.api.setCurrentPage(c.page)}}function o(){n[c._key]={num:1,id:c._containerId};h.loaded=true}h.loaded=false;var q=c.url.replace(/\.html$/,".js"), +s=c.target,d=f.getElementById(s),A=f.createElement("div"),y=r.position(d),x=c.width||y.width;y=c.height||y.height;var a=c.sidebar||true,g=c.text||true,l=c.pdf||true,k=c.showAnnotations||true,t=c.zoom||700,u=c.search||true;if(!function(v){var z=false;r.forEach(h.viewers,function(C){if(C.api.getSchema().canonicalURL===v){m(C);C=n[c._key];c._containerId=C.id;C.num+=1;z=true;h.loaded=true}});return z}(c.url)){A.id=c._containerId=r.guid(s);s="#"+A.id;d.appendChild(A);i.trigger("documentready");h.load(q, +{width:x,height:y,sidebar:a,text:g,pdf:l,showAnnotations:k,zoom:t,search:u,container:s,afterLoad:c.page||c.aid?function(v){m(v);c._changeView(v);A.style.visibility="hidden";v.elements.pages.hide();o()}:function(v){m(v);o();A.style.visibility="hidden";v.elements.pages.hide()}})}}function e(){window.DV.loaded?b():setTimeout(e,25)}var h=window.DV=window.DV||{},i=this;if(h.loading)e();else{h.loading=true;h.recordHit="//www.documentcloud.org/pixel.gif";var j=f.createElement("link"),p=f.getElementsByTagName("head")[0]; +j.rel="stylesheet";j.type="text/css";j.media="screen";j.href="//s3.documentcloud.org/viewer/viewer-datauri.css";p.appendChild(j);h.loaded=false;r.getScript("http://s3.documentcloud.org/viewer/viewer.js",function(){h.loading=false;b()})}},start:function(c,b){var e=f.getElementById(b._containerId),h=DV.viewers[b._key];(b.page||b.aid)&&h&&b._changeView(h);if(e&&h){e.style.visibility="visible";h.elements.pages.show()}},end:function(c,b){var e=f.getElementById(b._containerId);if(e&&DV.viewers[b._key]){e.style.visibility= +"hidden";DV.viewers[b._key].elements.pages.hide()}},_teardown:function(c){var b=f.getElementById(c._containerId);if((c=c._key)&&DV.viewers[c]&&--n[c].num===0){for(DV.viewers[c].api.unload();b.hasChildNodes();)b.removeChild(b.lastChild);b.parentNode.removeChild(b)}}})})(Popcorn,window.document);(function(r){r.parser("parseJSON","JSON",function(f){var n={title:"",remote:"",data:[]};r.forEach(f.data,function(c){n.data.push(c)});return n})})(Popcorn);(function(r){r.parser("parseSBV",function(f){var n={title:"",remote:"",data:[]},c=[],b=0,e=0,h=function(q){q=q.split(":");var s=q.length-1,d;try{d=parseInt(q[s-1],10)*60+parseFloat(q[s],10);if(s===2)d+=parseInt(q[0],10)*3600}catch(A){throw"Bad cue";}return d},i=function(q,s){var d={};d[q]=s;return d};f=f.text.split(/(?:\r\n|\r|\n)/gm);for(e=f.length;b<e;){var j={},p=[],m=f[b++].split(",");try{j.start=h(m[0]);for(j.end=h(m[1]);b<e&&f[b];)p.push(f[b++]);j.text=p.join("<br />");c.push(i("subtitle",j))}catch(o){for(;b< +e&&f[b];)b++}for(;b<e&&!f[b];)b++}n.data=c;return n})})(Popcorn);(function(r){function f(c,b){var e={};e[c]=b;return e}function n(c){c=c.split(":");try{var b=c[2].split(",");if(b.length===1)b=c[2].split(".");return parseFloat(c[0],10)*3600+parseFloat(c[1],10)*60+parseFloat(b[0],10)+parseFloat(b[1],10)/1E3}catch(e){return 0}}r.parser("parseSRT",function(c){var b={title:"",remote:"",data:[]},e=[],h=0,i=0,j,p,m,o;c=c.text.split(/(?:\r\n|\r|\n)/gm);for(h=c.length-1;h>=0&&!c[h];)h--;m=h+1;for(h=0;h<m;h++){o={};p=[];o.id=parseInt(c[h++],10);j=c[h++].split(/[\t ]*--\>[\t ]*/); +o.start=n(j[0]);i=j[1].indexOf(" ");if(i!==-1)j[1]=j[1].substr(0,i);for(o.end=n(j[1]);h<m&&c[h];)p.push(c[h++]);o.text=p.join("\\N").replace(/\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi,"");o.text=o.text.replace(/</g,"<").replace(/>/g,">");o.text=o.text.replace(/<(\/?(font|b|u|i|s))((\s+(\w|\w[\w\-]*\w)(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)(\/?)>/gi,"<$1$3$7>");o.text=o.text.replace(/\\N/gi,"<br />");e.push(f("subtitle",o))}b.data=e;return b})})(Popcorn);(function(r){function f(b,e){var h=b.substr(10).split(","),i;i={start:n(h[e.start]),end:n(h[e.end])};if(i.start===-1||i.end===-1)throw"Invalid time";var j=q.call(m,/\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi,""),p=j.replace,m;m=h.length;q=[];for(var o=e.text;o<m;o++)q.push(h[o]);m=q.join(",");var q=m.replace;i.text=p.call(j,/\\N/gi,"<br />");return i}function n(b){var e=b.split(":");if(b.length!==10||e.length<3)return-1;return parseInt(e[0],10)*3600+parseInt(e[1],10)*60+parseFloat(e[2],10)}function c(b, +e){var h={};h[b]=e;return h}r.parser("parseSSA",function(b){var e={title:"",remote:"",data:[]},h=[],i=0,j;b=b.text.split(/(?:\r\n|\r|\n)/gm);for(j=b.length;i<j&&b[i]!=="[Events]";)i++;var p=b[++i].substr(8).split(", "),m={},o,q;q=0;for(o=p.length;q<o;q++)if(p[q]==="Start")m.start=q;else if(p[q]==="End")m.end=q;else if(p[q]==="Text")m.text=q;for(;++i<j&&b[i]&&b[i][0]!=="[";)try{h.push(c("subtitle",f(b[i],m)))}catch(s){}e.data=h;return e})})(Popcorn);(function(r){function f(i,j,p){var m=i.firstChild;i=n(i,p);p=[];for(var o;m;){if(m.nodeType===1)if(m.nodeName==="p")p.push(c(m,j,i));else if(m.nodeName==="div"){o=b(m.getAttribute("begin"));if(o<0)o=j;p.push.apply(p,f(m,o,i))}m=m.nextSibling}return p}function n(i,j){var p=i.getAttribute("region");return p!==null?p:j||""}function c(i,j,p){var m={};m.text=(i.textContent||i.text).replace(e,"").replace(h,"<br />");m.id=i.getAttribute("xml:id")||i.getAttribute("id");m.start=b(i.getAttribute("begin"),j); +m.end=b(i.getAttribute("end"),j);m.target=n(i,p);if(m.end<0){m.end=b(i.getAttribute("duration"),0);if(m.end>=0)m.end+=m.start;else m.end=Number.MAX_VALUE}return{subtitle:m}}function b(i,j){var p;if(!i)return-1;try{return r.util.toSeconds(i)}catch(m){for(var o=i.length-1;o>=0&&i[o]<="9"&&i[o]>="0";)o--;p=o;o=parseFloat(i.substring(0,p));p=i.substring(p);return o*({h:3600,m:60,s:1,ms:0.0010}[p]||-1)+(j||0)}}var e=/^[\s]+|[\s]+$/gm,h=/(?:\r\n|\r|\n)/gm;r.parser("parseTTML",function(i){var j={title:"", +remote:"",data:[]};if(!i.xml||!i.xml.documentElement)return j;i=i.xml.documentElement.firstChild;if(!i)return j;for(;i.nodeName!=="body";)i=i.nextSibling;if(i)j.data=f(i,0);return j})})(Popcorn);(function(r){r.parser("parseTTXT",function(f){var n={title:"",remote:"",data:[]},c=function(j){j=j.split(":");var p=0;try{return parseFloat(j[0],10)*60*60+parseFloat(j[1],10)*60+parseFloat(j[2],10)}catch(m){p=0}return p},b=function(j,p){var m={};m[j]=p;return m};f=f.xml.lastChild.lastChild;for(var e=Number.MAX_VALUE,h=[];f;){if(f.nodeType===1&&f.nodeName==="TextSample"){var i={};i.start=c(f.getAttribute("sampleTime"));i.text=f.getAttribute("text");if(i.text){i.end=e-0.0010;h.push(b("subtitle",i))}e= +i.start}f=f.previousSibling}n.data=h.reverse();return n})})(Popcorn);(function(r){function f(c){var b=c.split(":");c=c.length;var e;if(c!==12&&c!==9)throw"Bad cue";c=b.length-1;try{e=parseInt(b[c-1],10)*60+parseFloat(b[c],10);if(c===2)e+=parseInt(b[0],10)*3600}catch(h){throw"Bad cue";}return e}function n(c,b){var e={};e[c]=b;return e}r.parser("parseVTT",function(c){var b={title:"",remote:"",data:[]},e=[],h=0,i=0,j,p;c=c.text.split(/(?:\r\n|\r|\n)/gm);i=c.length;if(i===0||c[0]!=="WEBVTT")return b;for(h++;h<i;){j=[];try{for(var m=h;m<i&&!c[m];)m++;h=m;var o=c[h++];m= +void 0;var q={};if(!o||o.indexOf("--\>")===-1)throw"Bad cue";m=o.replace(/--\>/," --\> ").split(/[\t ]+/);if(m.length<2)throw"Bad cue";q.id=o;q.start=f(m[0]);q.end=f(m[2]);for(p=q;h<i&&c[h];)j.push(c[h++]);p.text=j.join("<br />");e.push(n("subtitle",p))}catch(s){for(h=h;h<i&&c[h];)h++;h=h}}b.data=e;return b})})(Popcorn);(function(r){r.parser("parseXML","XML",function(f){var n={title:"",remote:"",data:[]},c={},b=function(m){m=m.split(":");if(m.length===1)return parseFloat(m[0],10);else if(m.length===2)return parseFloat(m[0],10)+parseFloat(m[1]/12,10);else if(m.length===3)return parseInt(m[0]*60,10)+parseFloat(m[1],10)+parseFloat(m[2]/12,10);else if(m.length===4)return parseInt(m[0]*3600,10)+parseInt(m[1]*60,10)+parseFloat(m[2],10)+parseFloat(m[3]/12,10)},e=function(m){for(var o={},q=0,s=m.length;q<s;q++){var d=m.item(q).nodeName, +A=m.item(q).nodeValue,y=c[A];if(d==="in")o.start=b(A);else if(d==="out")o.end=b(A);else if(d==="resourceid")for(var x in y){if(y.hasOwnProperty(x))if(!o[x]&&x!=="id")o[x]=y[x]}else o[d]=A}return o},h=function(m,o){var q={};q[m]=o;return q},i=function(m,o,q){var s={};r.extend(s,o,e(m.attributes),{text:m.textContent||m.text});o=m.childNodes;if(o.length<1||o.length===1&&o[0].nodeType===3)if(q)c[s.id]=s;else n.data.push(h(m.nodeName,s));else for(m=0;m<o.length;m++)o[m].nodeType===1&&i(o[m],s,q)};f=f.documentElement.childNodes; +for(var j=0,p=f.length;j<p;j++)if(f[j].nodeType===1)f[j].nodeName==="manifest"?i(f[j],{},true):i(f[j],{},false);return n})})(Popcorn);(function(){var r=false,f=false;Popcorn.player("soundcloud",{_canPlayType:function(n,c){return/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(soundcloud)/.test(c)&&n.toLowerCase()!=="video"},_setup:function(n){function c(){r=true;SC.initialize({client_id:"PRaNFlda6Bhf5utPjUsptg"});SC.get("/resolve",{url:e.src},function(A){e.width=e.style.width?""+e.offsetWidth:"560";e.height=e.style.height?""+e.offsetHeight:"315";h.scrolling="no";h.frameborder="no";h.id="soundcloud-"+Popcorn.guid();h.src="http://w.soundcloud.com/player/?url="+ +A.uri+"&show_artwork=false&buying=false&liking=false&sharing=false";h.width="100%";h.height="100%";n.loadListener=function(){n.widget=o=SC.Widget(h.id);o.bind(SC.Widget.Events.FINISH,function(){e.pause();e.dispatchEvent("ended")});o.bind(SC.Widget.Events.PLAY_PROGRESS,function(y){j=y.currentPosition/1E3;e.dispatchEvent("timeupdate")});o.bind(SC.Widget.Events.PLAY,function(){p=m=false;e.dispatchEvent("play");e.dispatchEvent("playing");e.currentTime=j;d.next()});o.bind(SC.Widget.Events.PAUSE,function(){p= +m=true;e.dispatchEvent("pause");d.next()});o.bind(SC.Widget.Events.READY,function(){o.getDuration(function(y){q=y/1E3;e.style.visibility="visible";e.dispatchEvent("durationchange");e.readyState=4;e.dispatchEvent("readystatechange");e.dispatchEvent("loadedmetadata");e.dispatchEvent("loadeddata");e.dispatchEvent("canplaythrough");e.dispatchEvent("load");!e.paused&&e.play()});o.getVolume(function(y){i=y/100})})};h.addEventListener("load",n.loadListener,false);e.appendChild(h)})}function b(){if(f)(function A(){setTimeout(function(){r? +c():A()},100)})();else{f=true;Popcorn.getScript("http://w.soundcloud.com/player/api.js",function(){Popcorn.getScript("http://connect.soundcloud.com/sdk.js",function(){c()})})}}var e=this,h=document.createElement("iframe"),i=1,j=0,p=true,m=true,o,q=0,s=false,d=Popcorn.player.playerQueue();n._container=h;e.style.visibility="hidden";e.play=function(){p=false;d.add(function(){if(m)o&&o.play();else d.next()})};e.pause=function(){p=true;d.add(function(){if(m)d.next();else o&&o.pause()})};Object.defineProperties(e, +{muted:{set:function(A){if(A){o&&o.getVolume(function(y){i=y/100});o&&o.setVolume(0);s=true}else{o&&o.setVolume(i*100);s=false}e.dispatchEvent("volumechange")},get:function(){return s}},volume:{set:function(A){o&&o.setVolume(A*100);i=A;e.dispatchEvent("volumechange")},get:function(){return s?0:i}},currentTime:{set:function(A){j=A;o&&o.seekTo(A*1E3);e.dispatchEvent("seeked");e.dispatchEvent("timeupdate")},get:function(){return j}},duration:{get:function(){return q}},paused:{get:function(){return p}}}); +r?c():b()},_teardown:function(n){var c=n.widget,b=SC.Widget.Events,e=n._container;n.destroyed=true;if(c)for(var h in b)c&&c.unbind(b[h]);else e.removeEventListener("load",n.loadEventListener,false)}})})();(function(){function r(n){var c=r.options;n=c.parser[c.strictMode?"strict":"loose"].exec(n);for(var b={},e=14;e--;)b[c.key[e]]=n[e]||"";b[c.q.name]={};b[c.key[12]].replace(c.q.parser,function(h,i,j){if(i)b[c.q.name][i]=j});return b}function f(n,c){return/player.vimeo.com\/video\/\d+/.test(c)||/vimeo.com\/\d+/.test(c)}r.options={strictMode:false,key:["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],q:{name:"queryKey", +parser:/(?:^|&)([^&=]*)=?([^&]*)/g},parser:{strict:/^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,loose:/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/}};Popcorn.player("vimeo",{_canPlayType:f,_setup:function(n){function c(l,k){var t=y.src.split("?")[0],u=JSON.stringify({method:l, +value:k});if(t.substr(0,2)==="//")t=window.location.protocol+t;y.contentWindow?y.contentWindow.postMessage(u,t):o.unload()}function b(l){if(l.origin==="http://player.vimeo.com"){var k;try{k=JSON.parse(l.data)}catch(t){console.warn(t)}if(k.player_id==m){k.method&&a[k.method]&&a[k.method](k);k.event&&g[k.event]&&g[k.event](k)}}}function e(){d||(d=setInterval(function(){o.dispatchEvent("timeupdate")},i));s||(s=setInterval(function(){c("getCurrentTime")},j))}function h(){if(d){clearInterval(d);d=0}if(s){clearInterval(s); +s=0}}var i=250,j=16,p={MEDIA_ERR_ABORTED:1,MEDIA_ERR_NETWORK:2,MEDIA_ERR_DECODE:3,MEDIA_ERR_SRC_NOT_SUPPORTED:4},m,o=this,q={q:[],queue:function(l){this.q.push(l);this.process()},process:function(){if(A)for(;this.q.length;)this.q.shift()()}},s,d,A,y=document.createElement("iframe"),x={error:null,src:o.src,NETWORK_EMPTY:0,NETWORK_IDLE:1,NETWORK_LOADING:2,NETWORK_NO_SOURCE:3,networkState:0,HAVE_NOTHING:0,HAVE_METADATA:1,HAVE_CURRENT_DATA:2,HAVE_FUTURE_DATA:3,HAVE_ENOUGH_DATA:4,readyState:0,seeking:false, +currentTime:0,duration:NaN,paused:true,ended:false,autoplay:false,loop:false,volume:1,muted:false,width:0,height:0};Popcorn.forEach("error networkState readyState seeking duration paused ended".split(" "),function(l){Object.defineProperty(o,l,{get:function(){return x[l]}})});Object.defineProperties(o,{src:{get:function(){return x.src},set:function(l){x.src=l;o.load()}},currentTime:{get:function(){return x.currentTime},set:function(l){q.queue(function(){c("seekTo",l)});x.seeking=true;o.dispatchEvent("seeking")}}, +autoplay:{get:function(){return x.autoplay},set:function(l){x.autoplay=!!l}},loop:{get:function(){return x.loop},set:function(l){x.loop=!!l;q.queue(function(){c("setLoop",loop)})}},volume:{get:function(){return x.volume},set:function(l){x.volume=l;q.queue(function(){c("setVolume",x.muted?0:x.volume)});o.dispatchEvent("volumechange")}},muted:{get:function(){return x.muted},set:function(l){x.muted=!!l;q.queue(function(){c("setVolume",x.muted?0:x.volume)});o.dispatchEvent("volumechange")}},width:{get:function(){return y.width}, +set:function(l){y.width=l}},height:{get:function(){return y.height},set:function(l){y.height=l}}});var a={getCurrentTime:function(l){x.currentTime=parseFloat(l.value)},getDuration:function(l){x.duration=parseFloat(l.value);if(!isNaN(x.duration)){x.readyState=4;o.dispatchEvent("durationchange");o.dispatchEvent("loadedmetadata");o.dispatchEvent("loadeddata");o.dispatchEvent("canplay");o.dispatchEvent("canplaythrough")}},getVolume:function(l){x.volume=parseFloat(l.value)}},g={ready:function(){c("addEventListener", +"loadProgress");c("addEventListener","playProgress");c("addEventListener","play");c("addEventListener","pause");c("addEventListener","finish");c("addEventListener","seek");c("getDuration");A=true;q.process();o.dispatchEvent("loadstart")},loadProgress:function(l){o.dispatchEvent("progress");x.duration=parseFloat(l.data.duration)},playProgress:function(l){x.currentTime=parseFloat(l.data.seconds)},play:function(){if(x.seeking){x.seeking=false;o.dispatchEvent("seeked")}x.paused=false;x.ended=false;e(); +o.dispatchEvent("play")},pause:function(){x.paused=true;h();o.dispatchEvent("pause")},finish:function(){x.ended=true;h();o.dispatchEvent("ended")},seek:function(l){x.currentTime=parseFloat(l.data.seconds);x.seeking=false;x.ended=false;o.dispatchEvent("timeupdate");o.dispatchEvent("seeked")}};o.load=function(){A=false;m=Popcorn.guid();var l=r(x.src),k={},t=[],u={api:1,player_id:m};if(f(o.nodeName,l.source)){Popcorn.extend(k,n);Popcorn.extend(k,l.queryKey);Popcorn.extend(k,u);l="http://player.vimeo.com/video/"+ +/\d+$/.exec(l.path)+"?";for(var v in k)k.hasOwnProperty(v)&&t.push(encodeURIComponent(v)+"="+encodeURIComponent(k[v]));l+=t.join("&");x.loop=!!l.match(/loop=1/);x.autoplay=!!l.match(/autoplay=1/);y.width=o.style.width?o.style.width:500;y.height=o.style.height?o.style.height:281;y.frameBorder=0;y.webkitAllowFullScreen=true;y.mozAllowFullScreen=true;y.allowFullScreen=true;y.src=l;o.appendChild(y)}else{l=x.MEDIA_ERR_SRC_NOT_SUPPORTED;x.error={};Popcorn.extend(x.error,p);x.error.code=l;o.dispatchEvent("error")}}; +o.unload=function(){h();window.removeEventListener("message",b,false)};o.play=function(){q.queue(function(){c("play")})};o.pause=function(){q.queue(function(){c("pause")})};setTimeout(function(){window.addEventListener("message",b,false);o.load()},0)},_teardown:function(){this.unload&&this.unload()}})})();(function(r,f){r.onYouTubePlayerAPIReady=function(){onYouTubePlayerAPIReady.ready=true;for(var c=0;c<onYouTubePlayerAPIReady.waiting.length;c++)onYouTubePlayerAPIReady.waiting[c]()};if(r.YT){r.quarantineYT=r.YT;r.YT=null}onYouTubePlayerAPIReady.waiting=[];var n=false;f.player("youtube",{_canPlayType:function(c,b){return typeof b==="string"&&/(?:http:\/\/www\.|http:\/\/|www\.|\.|^)(youtu)/.test(b)&&c.toLowerCase()!=="video"},_setup:function(c){if(!r.YT&&!n){n=true;f.getScript("//youtube.com/player_api")}var b= +this,e=false,h=document.createElement("div"),i=0,j=true,p=false,m=0,o=false,q=100,s=f.player.playerQueue(),d=function(){f.player.defineProperty(b,"currentTime",{set:function(y){if(!c.destroyed){p=true;i=Math.round(+y*100)/100}},get:function(){return i}});f.player.defineProperty(b,"paused",{get:function(){return j}});f.player.defineProperty(b,"muted",{set:function(y){if(c.destroyed)return y;if(c.youtubeObject.isMuted()!==y){y?c.youtubeObject.mute():c.youtubeObject.unMute();o=c.youtubeObject.isMuted(); +b.dispatchEvent("volumechange")}return c.youtubeObject.isMuted()},get:function(){if(c.destroyed)return 0;return c.youtubeObject.isMuted()}});f.player.defineProperty(b,"volume",{set:function(y){if(c.destroyed)return y;if(c.youtubeObject.getVolume()/100!==y){c.youtubeObject.setVolume(y*100);q=c.youtubeObject.getVolume();b.dispatchEvent("volumechange")}return c.youtubeObject.getVolume()/100},get:function(){if(c.destroyed)return 0;return c.youtubeObject.getVolume()/100}});b.play=function(){if(!c.destroyed){j= +false;s.add(function(){if(c.youtubeObject.getPlayerState()!==1){p=false;c.youtubeObject.playVideo()}else s.next()})}};b.pause=function(){if(!c.destroyed){j=true;s.add(function(){c.youtubeObject.getPlayerState()!==2?c.youtubeObject.pauseVideo():s.next()})}}};h.id=b.id+f.guid();c._container=h;b.appendChild(h);var A=function(){var y,x,a,g,l=true,k=function(){if(!c.destroyed){if(p)if(i===c.youtubeObject.getCurrentTime()){p=false;b.dispatchEvent("seeked");b.dispatchEvent("timeupdate")}else c.youtubeObject.seekTo(i); +else{i=c.youtubeObject.getCurrentTime();b.dispatchEvent("timeupdate")}setTimeout(k,250)}},t=function(z){var C=c.youtubeObject.getDuration();if(isNaN(C)||C===0)setTimeout(function(){t(z*2)},z*1E3);else{b.duration=C;b.dispatchEvent("durationchange");b.dispatchEvent("loadedmetadata");b.dispatchEvent("loadeddata");b.readyState=4;k();b.dispatchEvent("canplaythrough")}};c.controls=+c.controls===0||+c.controls===1?c.controls:1;c.annotations=+c.annotations===1||+c.annotations===3?c.annotations:1;y=/^.*(?:\/|v=)(.{11})/.exec(b.src)[1]; +x=(b.src.split("?")[1]||"").replace(/v=.{11}/,"");x=x.replace(/&t=(?:(\d+)m)?(?:(\d+)s)?/,function(z,C,E){C|=0;E|=0;m=+E+C*60;return""});x=x.replace(/&start=(\d+)?/,function(z,C){C|=0;m=C;return""});e=/autoplay=1/.test(x);x=x.split(/[\&\?]/g);a={wmode:"transparent"};for(var u=0;u<x.length;u++){g=x[u].split("=");a[g[0]]=g[1]}c.youtubeObject=new YT.Player(h.id,{height:"100%",width:"100%",wmode:"transparent",playerVars:a,videoId:y,events:{onReady:function(){q=b.volume;o=b.muted;v();j=b.paused;d();c.youtubeObject.playVideo(); +b.currentTime=m},onStateChange:function(z){if(!(c.destroyed||z.data===-1))if(z.data===2){j=true;b.dispatchEvent("pause");s.next()}else if(z.data===1&&!l){j=false;b.dispatchEvent("play");b.dispatchEvent("playing");s.next()}else if(z.data===0)b.dispatchEvent("ended");else if(z.data===1&&l){l=false;if(e||!b.paused)j=false;j&&c.youtubeObject.pauseVideo();t(0.025)}},onError:function(z){if([2,100,101,150].indexOf(z.data)!==-1){b.error={customCode:z.data};b.dispatchEvent("error")}}}});var v=function(){if(!c.destroyed){if(o!== +c.youtubeObject.isMuted()){o=c.youtubeObject.isMuted();b.dispatchEvent("volumechange")}if(q!==c.youtubeObject.getVolume()){q=c.youtubeObject.getVolume();b.dispatchEvent("volumechange")}setTimeout(v,250)}}};onYouTubePlayerAPIReady.ready?A():onYouTubePlayerAPIReady.waiting.push(A)},_teardown:function(c){c.destroyed=true;var b=c.youtubeObject;if(b){b.stopVideo();b.clearVideo&&b.clearVideo()}this.removeChild(document.getElementById(c._container.id))}})})(window,Popcorn); diff --git a/record-and-playback/presentation_export/playback/presentation_export/lib/popcorn.chattimeline.js b/record-and-playback/presentation_export/playback/presentation_export/lib/popcorn.chattimeline.js new file mode 100755 index 0000000000000000000000000000000000000000..d530cb7e046543cff3cee9053a6f63b27d942454 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/lib/popcorn.chattimeline.js @@ -0,0 +1,125 @@ +// PLUGIN: Timeline +(function ( Popcorn ) { + + /** + * chat-timeline popcorn plug-in + * Adds data associated with a certain time in the video, which creates a scrolling view of each item as the video progresses + * Options parameter will need a start, target, title, and text + * -Start is the time that you want this plug-in to execute + * -End is the time that you want this plug-in to stop executing, tho for this plugin an end time may not be needed ( optional ) + * -Target is the id of the DOM element that you want the timeline to appear in. This element must be in the DOM + * -Name is the name of the current chat message sender + * -Text is text is simply related text that will be displayed + * -direction specifies whether the timeline will grow from the top or the bottom, receives input as "UP" or "DOWN" + * @param {Object} options + * + * Example: + var p = Popcorn("#video") + .timeline( { + start: 5, // seconds + target: "timeline", + name: "Seneca", + text: "Welcome to seneca", + } ) + * + */ + + var i = 1; + + Popcorn.plugin( "chattimeline" , function( options ) { + + var target = document.getElementById( options.target ), + contentDiv = document.createElement( "div" ), + goingUp = true; + + contentDiv.style.display = "none"; + contentDiv.setAttribute('aria-hidden', true); + contentDiv.id = "timelineDiv" + i; + + // Default to up if options.direction is non-existant or not up or down + options.direction = options.direction || "up"; + if ( options.direction.toLowerCase() === "down" ) { + + goingUp = false; + } + + if ( target ) { + // if this isnt the first div added to the target div + if( goingUp ){ + // insert the current div before the previous div inserted + target.insertBefore( contentDiv, target.firstChild ); + } + else { + + target.appendChild( contentDiv ); + } + + } + + i++; + + // Default to empty if not used + //options.innerHTML = options.innerHTML || ""; + + contentDiv.innerHTML = "<strong>" + options.name + ":</strong>" + options.message; + + return { + + start: function( event, options ) { + contentDiv.style.display = "block"; + if ($("#exposechat").is(':checked')) { + contentDiv.setAttribute('aria-hidden', false); + } + if( options.direction === "down" ) { + target.scrollTop = target.scrollHeight; + } + + if ($("#accEnabled").is(':checked')) + addTime(7); + }, + + end: function( event, options ) { + contentDiv.style.display = "none"; + contentDiv.setAttribute('aria-hidden', true); + }, + + _teardown: function( options ) { + + ( target && contentDiv ) && target.removeChild( contentDiv ) && !target.firstChild + } + }; + }, + { + + options: { + start: { + elem: "input", + type: "number", + label: "Start" + }, + end: { + elem: "input", + type: "number", + label: "End" + }, + target: "feed-container", + name: { + elem: "input", + type: "text", + label: "Name" + }, + message: { + elem: "input", + type: "text", + label: "Message" + }, + direction: { + elem: "select", + options: [ "DOWN", "UP" ], + label: "Direction", + optional: true + } + } + }); + +})( Popcorn ); diff --git a/record-and-playback/presentation_export/playback/presentation_export/lib/spin.min.js b/record-and-playback/presentation_export/playback/presentation_export/lib/spin.min.js new file mode 100644 index 0000000000000000000000000000000000000000..ebfbb1a2e2e433736a408aefcc1c34d9bcd68028 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/lib/spin.min.js @@ -0,0 +1,2 @@ +//fgnass.github.com/spin.js#v2.0.1 +!function(a,b){"object"==typeof exports?module.exports=b():"function"==typeof define&&define.amd?define(b):a.Spinner=b()}(this,function(){"use strict";function a(a,b){var c,d=document.createElement(a||"div");for(c in b)d[c]=b[c];return d}function b(a){for(var b=1,c=arguments.length;c>b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return l[e]||(m.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",m.cssRules.length),l[e]=1),e}function d(a,b){var c,d,e=a.style;for(b=b.charAt(0).toUpperCase()+b.slice(1),d=0;d<k.length;d++)if(c=k[d]+b,void 0!==e[c])return c;return void 0!==e[b]?b:void 0}function e(a,b){for(var c in b)a.style[d(a,c)||c]=b[c];return a}function f(a){for(var b=1;b<arguments.length;b++){var c=arguments[b];for(var d in c)void 0===a[d]&&(a[d]=c[d])}return a}function g(a,b){return"string"==typeof a?a:a[b%a.length]}function h(a){this.opts=f(a||{},h.defaults,n)}function i(){function c(b,c){return a("<"+b+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',c)}m.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.width,left:d.radius,top:-d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.length+d.width,k=2*j,l=2*-(d.width+d.length)+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d<e.childNodes.length&&(e=e.childNodes[b+d],e=e&&e.firstChild,e=e&&e.firstChild,e&&(e.opacity=c))}}var j,k=["webkit","Moz","ms","O"],l={},m=function(){var c=a("style",{type:"text/css"});return b(document.getElementsByTagName("head")[0],c),c.sheet||c.styleSheet}(),n={lines:12,length:7,width:5,radius:10,rotate:0,corners:1,color:"#000",direction:1,speed:1,trail:100,opacity:.25,fps:20,zIndex:2e9,className:"spinner",top:"50%",left:"50%",position:"absolute"};h.defaults={},f(h.prototype,{spin:function(b){this.stop();{var c=this,d=c.opts,f=c.el=e(a(0,{className:d.className}),{position:d.position,width:0,zIndex:d.zIndex});d.radius+d.length+d.width}if(e(f,{left:d.left,top:d.top}),b&&b.insertBefore(f,b.firstChild||null),f.setAttribute("role","progressbar"),c.lines(f,c.opts),!j){var g,h=0,i=(d.lines-1)*(1-d.direction)/2,k=d.fps,l=k/d.speed,m=(1-d.opacity)/(l*d.trail/100),n=l/d.lines;!function o(){h++;for(var a=0;a<d.lines;a++)g=Math.max(1-(h+(d.lines-a)*n)%l*m,d.opacity),c.opacity(f,a*d.direction+i,g,d);c.timeout=c.el&&setTimeout(o,~~(1e3/k))}()}return c},stop:function(){var a=this.el;return a&&(clearTimeout(this.timeout),a.parentNode&&a.parentNode.removeChild(a),this.el=void 0),this},lines:function(d,f){function h(b,c){return e(a(),{position:"absolute",width:f.length+f.width+"px",height:f.width+"px",background:b,boxShadow:c,transformOrigin:"left",transform:"rotate("+~~(360/f.lines*k+f.rotate)+"deg) translate("+f.radius+"px,0)",borderRadius:(f.corners*f.width>>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k<f.lines;k++)i=e(a(),{position:"absolute",top:1+~(f.width/2)+"px",transform:f.hwaccel?"translate3d(0,0,0)":"",opacity:f.opacity,animation:j&&c(f.opacity,f.trail,l+k*f.direction,f.lines)+" "+1/f.speed+"s linear infinite"}),f.shadow&&b(i,e(h("#000","0 0 4px #000"),{top:"2px"})),b(d,b(i,h(g(f.color,k),"0 0 1px rgba(0,0,0,.1)")));return d},opacity:function(a,b,c){b<a.childNodes.length&&(a.childNodes[b].style.opacity=c)}});var o=e(a("group"),{behavior:"url(#default#VML)"});return!d(o,"transform")&&o.adj?i():j=d(o,"animation"),h}); \ No newline at end of file diff --git a/record-and-playback/presentation_export/playback/presentation_export/lib/writing.js b/record-and-playback/presentation_export/playback/presentation_export/lib/writing.js new file mode 100755 index 0000000000000000000000000000000000000000..74321e0c6d4b93b8d36021d2051c0e0d492dba26 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/lib/writing.js @@ -0,0 +1,516 @@ +/** +* 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/>. +* +*/ + + +// - - - START OF GLOBAL VARIABLES - - - // +"use strict"; + +function getUrlParameters() { + console.log("** Getting url params"); + var map = {}; + window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) { map[key] = value; }); + return map; +} + +// - - - END OF GLOBAL VARIABLES - - - // + +// - - - START OF JAVASCRIPT FUNCTIONS - - - // + +// Draw the cursor at a specific point +function draw(x, y) { + cursorStyle = document.getElementById("cursor").style; + var slide = document.getElementById("slide"); + var obj = $("#slide > object"); + var scaledX = parseInt(x, 10) * (parseInt(obj.attr("width"), 10) / 800); + var scaledY = parseInt(y, 10) * (parseInt(obj.attr("height"), 10) / 600); + + //move to the next place + var leftValue = parseInt(slide.offsetLeft, 10) + parseInt(scaledX, 10) + var topValue = parseInt(slide.offsetTop, 10) + parseInt(scaledY, 10) + if (leftValue < 0){ + leftValue = 0 + } + if (topValue < 0){ + topValue = 0 + } + cursorStyle.left = leftValue + "px"; + cursorStyle.top = topValue + "px"; + +} + + +// Shows or hides the cursor object depending on true/false parameter passed. +function showCursor(boolVal) { + cursorStyle = document.getElementById("cursor").style; + if(boolVal === false) { + cursorStyle.height = "0px"; + cursorStyle.width = "0px"; + } + else { + cursorStyle.height = "10px"; + cursorStyle.width = "10px"; + } +} + +function setViewBox(val) { + if(svgobj.contentDocument) svgfile = svgobj.contentDocument.getElementById("svgfile"); + else svgfile = svgobj.getSVGDocument('svgfile').getElementById("svgfile"); + svgfile.setAttribute('viewBox', val); +} + +function setCursor(val) { + draw(val[0], val[1]); +} + +function getImageAtTime(time) { + var curr_t = parseFloat(time); + var key; + for (key in imageAtTime) { + if(imageAtTime.hasOwnProperty(key)) { + var arry = key.split(","); + if ((parseFloat(arry[0]) <= curr_t) && (parseFloat(arry[1]) >= curr_t)) { + return imageAtTime[key]; + } + } + } +} + +function getViewboxAtTime(time) { + var curr_t = parseFloat(time); + var key; + for (key in vboxValues) { + if(vboxValues.hasOwnProperty(key)) { + var arry = key.split(","); + if(arry[1] == "end") { + return vboxValues[key]; + } + else if ((parseFloat(arry[0]) <= curr_t) && (parseFloat(arry[1]) >= curr_t)) { + return vboxValues[key]; + } + } + } +} + +function getCursorAtTime(time) { + var coords = cursorValues[time]; + if(coords) return coords.split(' '); +} + +function removeSlideChangeAttribute() { + $('#video').removeAttr('slide-change'); + Popcorn('#video').unlisten(Popcorn.play, 'removeSlideChangeAttribute'); +} + +// - - - END OF JAVASCRIPT FUNCTIONS - - - // + +function runPopcorn() { + console.log("** Running popcorn"); + if(svgobj.contentDocument) svgfile = svgobj.contentDocument.getElementById("svgfile"); + else svgfile = svgobj.getSVGDocument('svgfile'); + + //making the object for requesting the read of the XML files. + if (window.XMLHttpRequest) { + // code for IE7+, Firefox, Chrome, Opera, Safari + var xmlhttp = new XMLHttpRequest(); + } else { + // code for IE6, IE5 + var xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + + // PROCESS SHAPES.SVG (in XML format). + console.log("** Getting shapes_svg"); + xmlhttp.open("GET", shapes_svg, false); + xmlhttp.send(); + var xmlDoc = xmlhttp.responseXML; + + console.log("** Processing shapes_svg"); + //getting all the event tags + var shapeelements = xmlDoc.getElementsByTagName("svg"); + + //get the array of values for the first shape (getDataPoints(0) is the first shape). + var array = $(shapeelements[0]).find("g").filter(function(){ //get all the lines from the svg file + return $(this).attr('class') == 'shape'; + }); + var images = shapeelements[0].getElementsByTagName("image"); + + + //create a map from timestamp to id list + var timestampToId = {}; + for (var j = 0; j < array.length; j++) { + shapeTime = array[j].getAttribute("timestamp"); + shapeId = array[j].getAttribute("id"); + + if (timestampToId[shapeTime] == undefined) { + timestampToId[shapeTime] = new Array(0); + } + timestampToId[shapeTime].push(shapeId); + } + + //fill the times array with the times of the svg images. + for (var j = 0; j < array.length; j++) { + times[j] = array[j].getAttribute("timestamp"); + } + + var times_length = times.length; //get the length of the times array. + + console.log("** Getting text files"); + for(var m = 0; m < images.length; m++) { + len = images[m].getAttribute("in").split(" ").length; + for(var n = 0; n < len; n++) { + imageAtTime[[images[m].getAttribute("in").split(" ")[n], images[m].getAttribute("out").split(" ")[n]]] = images[m].getAttribute("id"); + } + + // the logo at the start has no text attribute + if (images[m].getAttribute("text")) { + var txtFile = false; + if (window.XMLHttpRequest) { + // code for IE7+, Firefox, Chrome, Opera, Safari + txtFile = new XMLHttpRequest(); + } else { + // code for IE6, IE5 + txtFile = new ActiveXObject("Microsoft.XMLHTTP"); + } + var imgid = images[m].getAttribute("id"); //have to save the value because images array might go out of scope + txtFile.open("GET", url + "/" + images[m].getAttribute("text"), false); + txtFile.onreadystatechange = function() { + if (txtFile.readyState === 4) { + if (txtFile.status === 200) { + slidePlainText[imgid] = $('<div/>').text(txtFile.responseText).html(); + //console.log("**Text file read " + imgid); + } + } + }; + txtFile.send(null); + } + } + + // PROCESS PANZOOMS.XML + console.log("** Getting panzooms.xml"); + xmlhttp.open("GET", events_xml, false); + xmlhttp.send(); + xmlDoc = xmlhttp.responseXML; + //getting all the event tags + console.log("** Processing panzooms.xml"); + var panelements = xmlDoc.getElementsByTagName("recording"); + var panZoomArray = panelements[0].getElementsByTagName("event"); + viewBoxes = xmlDoc.getElementsByTagName("viewBox"); + + var pzlen = panZoomArray.length; + var second_val; + //fill the times array with the times of the svg images. + for (var k = 0;k < pzlen; k++) { + if(panZoomArray[k+1] == undefined) { + second_val = "end"; + } + else second_val = panZoomArray[k+1].getAttribute("timestamp"); + vboxValues[[panZoomArray[k].getAttribute("timestamp"), second_val]] = viewBoxes[k].childNodes[0].data; + } + + + // PROCESS CURSOR.XML + console.log("** Getting cursor.xml"); + xmlhttp.open("GET", cursor_xml, false); + xmlhttp.send(); + xmlDoc = xmlhttp.responseXML; + //getting all the event tags + console.log("** Processing cursor.xml"); + var curelements = xmlDoc.getElementsByTagName("recording"); + var cursorArray = curelements[0].getElementsByTagName("event"); + coords = xmlDoc.getElementsByTagName("cursor"); + + var clen = cursorArray.length; + //fill the times array with the times of the svg images. + if(cursorArray.length != 0) cursorValues["0"] = "0 0"; + for (var m = 0; m < clen; m++) { + cursorValues[cursorArray[m].getAttribute("timestamp")] = coords[m].childNodes[0].data; + } + + + + svgobj.style.left = document.getElementById("slide").offsetLeft + "px"; + svgobj.style.top = "8px"; + var next_shape; + var shape; + for (var j = 0; j < array.length - 1; j++) { //iterate through all the shapes and pick out the main ones + var time = array[j].getAttribute("timestamp"); + shape = array[j].getAttribute("shape"); + next_shape = array[j+1].getAttribute("shape"); + + if(shape !== next_shape) { + main_shapes_ids.push(array[j].getAttribute("id")); + } + } + if (array.length !== 0) { + main_shapes_ids.push(array[array.length-1].getAttribute("id")); //put last value into this array always! + } + + var get_shapes_in_time = function(t) { + console.log("** Getting shapes in time"); + var shapes_in_time = timestampToId[t]; + var shapes = []; + if (shapes_in_time != undefined) { + var shape = null; + for (var i = 0; i < shapes_in_time.length; i++) { + var id = shapes_in_time[i]; + if(svgobj.contentDocument) shape = svgobj.contentDocument.getElementById(id); + else shape = svgobj.getSVGDocument('svgfile').getElementById(id); + + if (shape !== null) { //if there is actually a new shape to be displayed + shape = shape.getAttribute("shape"); //get actual shape tag for this specific time of playback + shapes.push(shape); + } + } + } + return shapes; + } + + var p = new Popcorn("#video"); + //update 60x / second the position of the next value. + p.code({ + start: 1, // start time + end: p.duration(), + onFrame: function(options) { + //console.log("**Popcorn video onframe"); + if(!((p.paused() === true) && (p.seeking() === false))) { + var t = p.currentTime().toFixed(1); //get the time and round to 1 decimal place + + current_shapes = get_shapes_in_time(t); + + //redraw everything (only way to make everything elegant) + for (var i = 0; i < array.length; i++) { + var time_s = array[i].getAttribute("timestamp"); + var time_f = parseFloat(time_s); + + if(svgobj.contentDocument) shape = svgobj.contentDocument.getElementById(array[i].getAttribute("id")); + else shape = svgobj.getSVGDocument('svgfile').getElementById(array[i].getAttribute("id")); + + var shape_i = shape.getAttribute("shape"); + if (time_f < t) { + if(current_shapes.indexOf(shape_i) > -1) { //currently drawing the same shape so don't draw the older steps + shape.style.visibility = "hidden"; //hide older steps to shape + } else if(main_shapes_ids.indexOf(shape.getAttribute("id")) > -1) { //as long as it is a main shape, it can be drawn... no intermediate steps. + if(parseFloat(shape.getAttribute("undo")) === -1) { //As long as the undo event hasn't happened yet... + shape.style.visibility = "visible"; + } else if (parseFloat(shape.getAttribute("undo")) > t) { + shape.style.visibility = "visible"; + } else { + shape.style.visibility = "hidden"; + } + } + } else if(time_s === t) { //for the shapes with the time specific to the current time + // only makes visible the last drawing of a given shape + var idx = current_shapes.indexOf(shape_i); + if (idx > -1) { + current_shapes.splice(idx, 1); + idx = current_shapes.indexOf(shape_i); + if (idx > -1) { + shape.style.visibility = "hidden"; + } else { + shape.style.visibility = "visible"; + } + } else { + // this is an inconsistent state, since current_shapes should have at least one drawing of this shape + shape.style.visibility = "hidden"; + } + } else { //for shapes that shouldn't be drawn yet (larger time than current time), don't draw them. + shape.style.visibility = "hidden"; + } + } + + var next_image = getImageAtTime(t); //fetch the name of the image at this time. + var imageXOffset = 0; + var imageYOffset = 0; + if((current_image !== next_image) && (next_image !== undefined)){ //changing slide image + if(svgobj.contentDocument) { + svgobj.contentDocument.getElementById(current_image).style.visibility = "hidden"; + var ni = svgobj.contentDocument.getElementById(next_image); + } + else { + svgobj.getSVGDocument('svgfile').getElementById(current_image).style.visibility = "hidden"; + var ni = svgobj.getSVGDocument('svgfile').getElementById(next_image); + } + document.getElementById("slideText").innerHTML = ""; //destroy old plain text + + ni.style.visibility = "visible"; + document.getElementById("slideText").innerHTML = slidePlainText[next_image] + next_image; //set new plain text + + if ($("#accEnabled").is(':checked')) { + //pause the playback on slide change + p.pause(); + $('#video').attr('slide-change', 'slide-change'); + p.listen(Popcorn.play, removeSlideChangeAttribute); + } + + var num_current = current_image.substr(5); + var num_next = next_image.substr(5); + + if(svgobj.contentDocument) currentcanvas = svgobj.contentDocument.getElementById("canvas" + num_current); + else currentcanvas = svgobj.getSVGDocument('svgfile').getElementById("canvas" + num_current); + + if(currentcanvas !== null) { + currentcanvas.setAttribute("display", "none"); + } + + if(svgobj.contentDocument) nextcanvas = svgobj.contentDocument.getElementById("canvas" + num_next); + else nextcanvas = svgobj.getSVGDocument('svgfile').getElementById("canvas" + num_next); + + if((nextcanvas !== undefined) && (nextcanvas != null)) { + nextcanvas.setAttribute("display", ""); + } + current_image = next_image; + } + + if(svgobj.contentDocument) var thisimg = svgobj.contentDocument.getElementById(current_image); + else var thisimg = svgobj.getSVGDocument('svgfile').getElementById(current_image); + + var offsets = thisimg.getBoundingClientRect(); + // Offsets divided by 4. By 2 because of the padding and by 2 again because 800x600 is half 1600x1200 + imageXOffset = (1600 - parseInt(thisimg.getAttribute("width"), 10))/4; + imageYOffset = (1200 - parseInt(thisimg.getAttribute("height"), 10))/4; + + + var vboxVal = getViewboxAtTime(t); + if(vboxVal !== undefined) { + setViewBox(vboxVal); + } + + var cursorVal = getCursorAtTime(t); + var cursor_on = false; + if(cursorVal != null) { + if(!cursor_on) { + document.getElementById("cursor").style.visibility = 'visible'; //make visible + cursor_on = true; + } + setCursor([parseFloat(cursorVal[0]) + imageXOffset - 6, parseFloat(cursorVal[1]) + imageYOffset - 6]); //-6 is for radius of cursor offset + } + } + } + }); + +}; + +function defineStartTime() { + console.log("** Defining start time"); + spinner.stop(); + $("#playback-content").css('visibility','visible'); + $("#load-recording-msg").css('display','none'); + + if (params.t === undefined) + return 1; + + var extractNumber = /\d+/g; + var extractUnit = /\D+/g; + var temp_start_time = 0; + + while (true) { + var param1 = extractUnit.exec(params.t); + var param2 = extractNumber.exec(params.t); + if (param1 == null || param2 == null) + break; + + var unit = String(param1).toLowerCase(); + var value = parseInt(String(param2)); + + if (unit == "h") + value *= 3600; + else if (unit == "m") + value *= 60; + + temp_start_time += value; + } + + console.log("Start time: " + temp_start_time); + return temp_start_time; +} + +var current_canvas = "canvas0"; +var current_image = "image0"; +var currentcanvas; +var shape; +var nextcanvas; +var next_image; +var next_pgid; +var curr_pgid; +var svgfile; +//current time +var t; +var len; +var current_shapes = []; +//coordinates for x and y for each second +var panAndZoomTimes = []; +var viewBoxes = []; +var coords = []; +var times = []; +// timestamp and id for drawings +var shapeTime; +var shapeId; +var clearTimes = []; +var main_shapes_ids = []; +var vboxValues = {}; +var cursorValues = {}; +var imageAtTime = {}; +var slidePlainText = {}; //holds slide plain text for retrieval +var cursorStyle; + +var url = "resources/"; +var shapes_svg = url + '/shapes.svg'; +var events_xml = url + '/panzooms.xml'; +var cursor_xml = url + '/cursor.xml'; + +var svgobj = document.createElement('object'); +svgobj.setAttribute('data', shapes_svg); +svgobj.setAttribute('height', '600px'); +svgobj.setAttribute('width', '800px'); +svgobj.addEventListener('load', runPopcorn, false); + +/** + * we need an urgently refactor here + * first the writing.js must be loaded, and then runPopcorn loads, but it loads + * only after the svg file gets loaded, and the generation of thumbnails must + * came after that because it needs the popcorn element to be created properly + */ +svgobj.addEventListener('load', function() { + console.log("** svgobj [load] listener activated"); + generateThumbnails(); + var p = Popcorn("#video"); + p.on('loadeddata', function() { + console.log("** popcorn video: [onloadeddata] activaded"); + p.currentTime(defineStartTime()); + + }); + + // Sometimes media has already loaded before our loadeddata listener is + // attached. If the media is already past the loadeddata stage then we + // trigger the event manually ourselves + if ($('#video')[0].readyState > 0) { + console.log("** Video tag readyState >0"); + p.emit('loadeddata'); + } + + + +}, false); + + +document.getElementById('slide').appendChild(svgobj); + +window.onresize = function(event) { + svgobj.style.left = document.getElementById("slide").offsetLeft + "px"; + svgobj.style.top = "8px"; +}; diff --git a/record-and-playback/presentation_export/playback/presentation_export/logo.png b/record-and-playback/presentation_export/playback/presentation_export/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..ce4ab709dbe496ea387b0ff14164adad7a60a947 Binary files /dev/null and b/record-and-playback/presentation_export/playback/presentation_export/logo.png differ diff --git a/record-and-playback/presentation_export/playback/presentation_export/playback.html b/record-and-playback/presentation_export/playback/presentation_export/playback.html new file mode 100755 index 0000000000000000000000000000000000000000..6f48bbffa0d0b6659f4f1ac56a18a088bc4a3083 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/playback.html @@ -0,0 +1,105 @@ +<!DOCTYPE html> +<!-- +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/>. +--> +<html> +<head> + <title>Recording Playback</title> + <link rel="stylesheet" href="playback/css/bbb.playback.css"> + <script type="text/javascript" src="playback/lib/jquery.min.js"></script> + <script type="text/javascript" src="playback/lib/jquery-ui.min.js"></script> + <script type="text/javascript" src="playback/lib/spin.min.js"></script> + + <link rel="stylesheet" type="text/css" href="playback/acornmediaplayer/acornmediaplayer.base.css" /> + <script type="text/javascript" src="playback/acornmediaplayer/jquery.acornmediaplayer.js"></script> + <link rel="stylesheet" type="text/css" href="playback/acornmediaplayer/themes/access/acorn.access.css" /> + <link rel="stylesheet" type="text/css" href="playback/acornmediaplayer/themes/darkglass/acorn.darkglass.css" /> + + + <script type="text/javascript" src="playback/playback.js"></script> + <script type="text/javascript" src="playback/lib/popcorn-complete.min.js"></script> + <script type="text/javascript" src="playback/lib/popcorn.chattimeline.js"></script> + + <!--script type="text/javascript"> + jQuery(function() { + + }); + </script--> + + <style> + .acorn-player.darkglass { + overflow: visible; + } + </style> +</head> +<body> + <div id="loading-recording"> + <div id="spinner"></div> + <p id="load-recording-msg">Initializing recording </p> + </div> + <div id="playback-content"> + <h1 class="visually-hidden">BigBlueButton Playback</h1> + <div id="playbackArea" class="clearfix" role="main"> + <h2 class="visually-hidden">Slide Thumbnails</h2> + <div id="thumbnails" role="region" aria-label="Slide thumbnails"></div> + <h2 class="visually-hidden">Presentation Slides</h2> + <div id="presentation"> + <div class="circle" id="cursor" style="visibility:hidden;"></div> + <div id="slide" role="img" aria-labelledby="slideText"></div> + <div id="slideText" class="visually-hidden" aria-live="polite"></div> + </div> + <div id="chatAndMediaArea"> + <h2 class="visually-hidden">Chat Messages</h2> + <input type="checkbox" name="exposechat" id="exposechat" value="exposechat" class="visually-hidden" checked="checked" /> + <label for="exposechat" class="visually-hidden">Read chat messages</label> + <div id="chat" aria-live="polite" role="region" aria-label="Chat messages"></div> + <div id="videoRecordingWrapper" role="region" aria-label="Video"> + <!-- + <video id="webcam" >You must use an HTML5 capable browser to view this page. + This playback requires modern browser, please use FF, Safari, Chrome</video> + --> + </div> + <div id="audioRecordingWrapper" role="region" aria-label="Audio"> + <!-- + <audio id="video">You must use an HTML5 capable browser to view this page. + This playback requires modern browser, please use FF, Safari, Chrome</audio> + --> + </div> + </div> + </div> + <!--div id="playbackControls"> + <div id="autoscrollWrapper"> + <input id="autoscrollEnabled" type="checkbox" name="autoscrollEnabled" checked="true" aria-label="Toggle autoscroll"/> + <label for="autoscrollEnabled">Auto scroll</label> + </div> + </div--> + + <!-- + <div id="accInfo"> + <input id="accEnabled" type="checkbox" value="accEnabled" /> + <label for="accEnabled">Enable accessibility pauses</label><br/> + <div id="countdown" /> + </div> + --> + </div> + <div id="footer"> + <p>Recorded with <a target="_blank" href="http://mconf.org/">Mconf</a>. </p> + <p>Compatible with <a target="_blank" href="http://mozilla.org/firefox">Mozilla Firefox</a> only.</p> + </div> +<!-- <script src='playback/lib/writing.js'></script> --> +</body> +</html> diff --git a/record-and-playback/presentation_export/playback/presentation_export/playback.js b/record-and-playback/presentation_export/playback/presentation_export/playback.js new file mode 100755 index 0000000000000000000000000000000000000000..e25a77ca799db98ea6cbb9603eec2736a49757e9 --- /dev/null +++ b/record-and-playback/presentation_export/playback/presentation_export/playback.js @@ -0,0 +1,477 @@ +/* + +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/>. + +*/ + +goToSlide = function(time) { + console.log("==Going to slide"); + var pop = Popcorn("#video"); + pop.currentTime(time); +} + +getUrlParameters = function() { + console.log("==Getting url parameters"); + var map = {}; + var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { + map[key] = value; + }); + return map; +} + +/* + * From: http://stackoverflow.com/questions/1634748/how-can-i-delete-a-query-string-parameter-in-javascript/4827730#4827730 + */ +removeUrlParameter = function(url, param) { + console.log("==Removing url params"); + var urlparts= url.split('?'); + if (urlparts.length>=2) { + var prefix= encodeURIComponent(param)+'='; + var pars= urlparts[1].split(/[&;]/g); + for (var i=pars.length; i-- > 0;) { + if (pars[i].indexOf(prefix, 0)==0) + pars.splice(i, 1); + } + if (pars.length > 0) { + return urlparts[0]+'?'+pars.join('&'); + } else { + return urlparts[0]; + } + } else { + return url; + } +} + +/* + * Converts seconds to HH:MM:SS + * From: http://stackoverflow.com/questions/6312993/javascript-seconds-to-time-with-format-hhmmss#6313008 + */ +secondsToHHMMSS = function(secs) { + var hours = Math.floor(secs / 3600); + var minutes = Math.floor((secs - (hours * 3600)) / 60); + var seconds = secs - (hours * 3600) - (minutes * 60); + + if (hours < 10) {hours = "0"+hours;} + if (minutes < 10) {minutes = "0"+minutes;} + if (seconds < 10) {seconds = "0"+seconds;} + var time = hours+':'+minutes+':'+seconds; + return time; +} + +secondsToYouTubeFormat = function(secs) { + var hours = Math.floor(secs / 3600); + var minutes = Math.floor((secs - (hours * 3600)) / 60); + var seconds = secs - (hours * 3600) - (minutes * 60); + + var time = ""; + if (hours > 0) {time += hours+"h";} + if (minutes > 0) {time += minutes+"m";} + if (seconds > 0) {time += seconds+"s";} + if (secs == 0) {time = "0s";} + + return time; +} + +/* + * Full word version of the above function for screen readers + */ +secondsToHHMMSSText = function(secs) { + var hours = Math.floor(secs / 3600); + var minutes = Math.floor((secs - (hours * 3600)) / 60); + var seconds = secs - (hours * 3600) - (minutes * 60); + + var time = ""; + if (hours > 1) {time += hours + " hours ";} + else if (hours == 1) {time += hours + " hour ";} + if (minutes > 1) {time += minutes + " minutes ";} + else if (minutes == 1) {time += minutes + " minute ";} + if (seconds > 1) {time += seconds + " seconds ";} + else if (seconds == 1) {time += seconds + " second ";} + + return time; +} + +replaceTimeOnUrl = function(secs) { + var newUrl = removeUrlParameter(document.URL, "t") + "&t=" + secondsToYouTubeFormat(secs); + window.history.replaceState({}, "", newUrl); +} + +var params = getUrlParameters(); +var MEETINGID = params['meetingId']; +var RECORDINGS = "./resources"; +var SLIDES_XML = RECORDINGS + '/slides_new.xml'; +var SHAPES_SVG = RECORDINGS + '/shapes.svg'; + +/* + * Sets the title attribute in a thumbnail. + */ +setTitleOnThumbnail = function($thumb) { + var src = $thumb.attr("src") + if (src !== undefined) { + var num = "?"; + var name = "undefined"; + var match = src.match(/slide-(.*).png/) + if (match) { num = match[1]; } + match = src.match(/([^/]*)\/slide-.*\.png/) + if (match) { name = match[1]; } + $thumb.attr("title", name + " (" + num + ")") + } +} + +/* + * Associates several events on a thumbnail, e.g. click to change slide, + * mouse over/out functions, etc. + */ +setEventsOnThumbnail = function($thumb) { + //console.log("== Setting event on thumbnail," ,$thumb); + // Popcorn event to mark a thumbnail when its slide is being shown + var timeIn = $thumb.attr("data-in"); + var timeOut = $thumb.attr("data-out"); + var pop = Popcorn("#video"); + pop.code({ + start: timeIn, + end: timeOut, + onStart: function( options ) { + $parent = $("#thumbnail-" + options.start).parent(); + $parent.addClass("active"); + $(".thumbnail-label", $parent).show(); + + animateToCurrentSlide(); + }, + onEnd: function( options ) { + $parent = $("#thumbnail-" + options.start).parent(); + $parent.removeClass("active"); + $(".thumbnail-label", $parent).hide(); + } + }); + + // Click on thumbnail changes the slide in popcorn + $thumb.parent().on("click", function() { + goToSlide($thumb.attr("data-in")); + replaceTimeOnUrl($thumb.attr("data-in")); + }); + + // Mouse over/out to show/hide the label over the thumbnail + $wrapper = $thumb.parent(); + $wrapper.on("mouseover", function() { + $(".thumbnail-label", $(this)).show(); + }); + $wrapper.on("mouseout", function() { + if (!$(this).hasClass("active")) { + $(".thumbnail-label", $(this)).hide(); + } + }); +} + +$("input[name='autoscrollEnabled']").live('change', function() { + animateToCurrentSlide(); +}); + +animateToCurrentSlide = function(force) { + console.log("==Animating to current slide"); + force = typeof force !== 'undefined' ? force : false; + + if (force || isAutoscrollEnabled()) { + var currentSlide = getCurrentSlide(); + // animate the scroll of thumbnails to center the current slide + var thumbnailOffset = currentSlide.prop('offsetTop') - $("#thumbnails").prop('offsetTop') + (currentSlide.prop('offsetHeight') - $("#thumbnails").prop('offsetHeight')) / 2; + $("#thumbnails").animate({ scrollTop: thumbnailOffset }, 'slow'); + } +} + +isAutoscrollEnabled = function() { + return $("input[name='autoscrollEnabled']").is(':checked'); +} + +setAutoscrollEnabled = function(value) { + $('input[name=autoscrollEnabled]').attr('checked', value); +} + +getCurrentSlide = function() { + return $(".thumbnail-wrapper.active"); +} + +/* + * Generates the list of thumbnails using shapes.svg + */ +generateThumbnails = function() { + console.log("== Generating thumbnails"); + var xmlhttp; + if (window.XMLHttpRequest) {// code for IE7+, Firefox, Chrome, Opera, Safari + xmlhttp = new XMLHttpRequest(); + } else {// code for IE6, IE5 + xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + xmlhttp.open("GET", SHAPES_SVG, false); + xmlhttp.send(null); + + if (xmlhttp.responseXML) + var xmlDoc = xmlhttp.responseXML; + else { + var parser = new DOMParser(); + var xmlDoc = parser.parseFromString(xmlhttp.responseText, "image/svg+xml"); + } + + var elementsMap = {}; + var imagesList = new Array(); + + xmlList = xmlDoc.getElementsByTagName("image"); + var slideCount = 0; + + console.log("== Setting title on thumbnails"); + for (var i = 0; i < xmlList.length; i++) { + var element = xmlList[i]; + + if (!$(element).attr("xlink:href")) + continue; + var src = RECORDINGS + "/" + element.getAttribute("xlink:href"); + if (src.match(/\/presentation\/.*slide-.*\.png/)) { + var timeInList = xmlList[i].getAttribute("in").split(" "); + var timeOutList = xmlList[i].getAttribute("out").split(" "); + for (var j = 0; j < timeInList.length; j++) { + var timeIn = Math.floor(timeInList[j]); + var timeOut = Math.floor(timeOutList[j]); + + var img = $(document.createElement('img')); + img.attr("src", src); + img.attr("id", "thumbnail-" + timeIn); + img.attr("data-in", timeIn); + img.attr("data-out", timeOut); + img.addClass("thumbnail"); + img.attr("alt", " "); + img.attr("aria-hidden", "true"); //doesn't need to be focusable for blind users + + // a label with the time the slide starts + var label = $(document.createElement('span')); + label.addClass("thumbnail-label"); + label.attr("aria-hidden", "true"); //doesn't need to be focusable for blind users + label.html(secondsToHHMMSS(timeIn)); + + var hiddenDesc = $(document.createElement('span')); + hiddenDesc.attr("id", img.attr("id") + "description"); + hiddenDesc.attr("class", "visually-hidden"); + hiddenDesc.html("Slide " + ++slideCount + " " + secondsToHHMMSSText(timeIn)); + + // a wrapper around the img and label + var div = $(document.createElement('div')); + div.addClass("thumbnail-wrapper"); + div.attr("role", "link"); //tells accessibility software it can be clicked + div.attr("aria-describedby", img.attr("id") + "description"); + div.append(img); + div.append(label); + div.append(hiddenDesc); + + if (parseFloat(timeIn) == 0 ) { + div.addClass("active"); + $(".thumbnail-label", div).show(); + } + + imagesList.push(timeIn); + elementsMap[timeIn] = div; + + setEventsOnThumbnail(img); + setTitleOnThumbnail(img); + } + } + } + + imagesList.sort(function(a,b){return a - b}); + for (var i in imagesList) { + $("#thumbnails").append(elementsMap[imagesList[i]]); + } +} + +google_frame_warning = function(){ + console.log("==Google frame warning"); + var message = "To support this playback please install 'Google Chrome Frame', or use other browser: Firefox, Safari, Chrome, Opera"; + var line = document.createElement("p"); + var link = document.createElement("a"); + line.appendChild(document.createTextNode(message)); + link.setAttribute("href", "http://www.google.com/chromeframe") + link.setAttribute("target", "_blank") + link.appendChild(document.createTextNode("Install Google Chrome Frame")); + document.getElementById("chat").appendChild(line); + document.getElementById("chat").appendChild(link); +} + +function checkUrl(url) +{ + console.log("==Checking Url",url) + var http = new XMLHttpRequest(); + http.open('HEAD', url, false); + try { + http.send(); + } catch(e) { + return false; + } + return http.status==200; +} + +load_video = function(){ + console.log("==Loading video") + //document.getElementById("video").style.visibility = "hidden" + var video = document.createElement("video") + video.setAttribute('id','video'); + video.setAttribute('class','webcam'); + + var webmsource = document.createElement("source"); + webmsource.setAttribute('src', RECORDINGS + '/video/webcams.webm'); + webmsource.setAttribute('type','video/webm; codecs="vp8.0, vorbis"'); + video.appendChild(webmsource); + + /*var time_manager = Popcorn("#video"); + var pc_webcam = Popcorn("#webcam"); + time_manager.on( "timeupdate", function() { + pc_webcam.currentTime( this.currentTime() ); + });*/ + + video.setAttribute('data-timeline-sources', SLIDES_XML); + //video.setAttribute('controls',''); + //leave auto play turned off for accessiblity support + //video.setAttribute('autoplay','autoplay'); + + document.getElementById("videoRecordingWrapper").appendChild(video); +} + +load_audio = function() { + console.log("Loading audio") + var audio = document.createElement("audio") ; + audio.setAttribute('id', 'video'); + + // The webm file will work in IE with WebM components installed, + // and should load faster in Chrome too + var webmsource = document.createElement("source"); + webmsource.setAttribute('src', RECORDINGS + '/audio/audio.webm'); + webmsource.setAttribute('type', 'audio/webm; codecs="vorbis"'); + + // Need to keep the ogg source around for compat with old recordings + var oggsource = document.createElement("source"); + oggsource.setAttribute('src', RECORDINGS + '/audio/audio.ogg'); + oggsource.setAttribute('type', 'audio/ogg; codecs="vorbis"'); + + // Browser Bug Workaround: The ogg file should be preferred in Firefox, + // since it can't seek in audio-only webm files. + if (navigator.userAgent.indexOf("Firefox") != -1) { + audio.appendChild(oggsource); + audio.appendChild(webmsource); + } else { + audio.appendChild(webmsource); + audio.appendChild(oggsource); + } + + audio.setAttribute('data-timeline-sources', SLIDES_XML); + //audio.setAttribute('controls',''); + //leave auto play turned off for accessiblity support + //audio.setAttribute('autoplay','autoplay'); + document.getElementById("audioRecordingWrapper").appendChild(audio); +} + +load_script = function(file){ + console.log("==Loading script "+ file) + script = document.createElement('script'); + script.src = file; + script.type = 'text/javascript'; + document.getElementsByTagName('body').item(0).appendChild(script); +} + +load_spinner = function(){ + console.log("==Loading spinner"); + var opts = { + lines: 13, // The number of lines to draw + length: 24, // The length of each line + width: 4, // The line thickness + radius: 24, // The radius of the inner circle + corners: 1, // Corner roundness (0..1) + rotate: 24, // The rotation offset + direction: 1, // 1: clockwise, -1: counterclockwise + color: '#000', // #rgb or #rrggbb or array of colors + speed: 1, // Rounds per second + trail: 87, // Afterglow percentage + shadow: false, // Whether to render a shadow + hwaccel: false, // Whether to use hardware acceleration + className: 'spinner', // The CSS class to assign to the spinner + zIndex: 2e9, // The z-index (defaults to 2000000000) + top: '50%', // Top position relative to parent + left: '50%' // Left position relative to parent + }; + var target = document.getElementById('spinner'); + spinner = new Spinner(opts).spin(target); +} + + +document.addEventListener( "DOMContentLoaded", function() { + console.log("==DOM content loaded"); + var appName = navigator.appName; + var appVersion = navigator.appVersion; + var spinner; + //var video = document.getElementById("webcam"); + + if (appName == "Microsoft Internet Explorer" && navigator.userAgent.match("chromeframe") == false ) { + google_frame_warning + } + + if (checkUrl(RECORDINGS + '/video/webcams.webm') == true){ + videoContainer = document.getElementById("audioRecordingWrapper").style.display = "none"; + load_video(); + }else{ + videoContainer = document.getElementById("videoRecordingWrapper").style.display = "none"; + chat = document.getElementById("chat"); + chat.style.height = "600px"; + chat.style.backgroundColor = "white"; + load_audio(); + } + + load_spinner(); + console.log("==Hide playback content"); + $("#playback-content").css('visibility', 'hidden'); + //load_audio(); + load_script("playback/lib/writing.js"); + //generateThumbnails(); + + //load up the acorn controls + console.log("==Loading acorn media player "); + jQuery('#video').acornMediaPlayer({ + theme: 'darkglass', + volumeSlider: 'vertical' + }); + + + +}, false); + +var secondsToWait = 0; + +function addTime(time){ + if (secondsToWait === 0) { + Popcorn('#video').pause(); + window.setTimeout("Tick()", 1000); + } + secondsToWait += time; +} + +function Tick() { + if (secondsToWait <= 0 || !($("#accEnabled").is(':checked'))) { + secondsToWait = 0; + Popcorn('#video').play(); + $('#countdown').html(""); // remove the timer + return; + } + + secondsToWait -= 1; + $('#countdown').html(secondsToWait); + window.setTimeout("Tick()", 1000); +} diff --git a/record-and-playback/presentation_export/scripts/presentation_export.nginx b/record-and-playback/presentation_export/scripts/presentation_export.nginx new file mode 100644 index 0000000000000000000000000000000000000000..4f9781b65027cb8e41e16bb88b6e2ecd9852bd69 --- /dev/null +++ b/record-and-playback/presentation_export/scripts/presentation_export.nginx @@ -0,0 +1,27 @@ +# +# 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/>. +# + + location /playback/presentation_export { + root /var/bigbluebutton; + index index.html index.htm; + } + + location /presentation_export { + root /var/bigbluebutton/published; + index index.html index.htm; + } diff --git a/record-and-playback/presentation_export/scripts/presentation_export.yml b/record-and-playback/presentation_export/scripts/presentation_export.yml new file mode 100644 index 0000000000000000000000000000000000000000..2bc9bb8b883dca341003298ff39bd31dd5d0fa1a --- /dev/null +++ b/record-and-playback/presentation_export/scripts/presentation_export.yml @@ -0,0 +1,5 @@ +publish_dir: /var/bigbluebutton/published/presentation_export +playback_dir: /var/bigbluebutton/playback/presentation_export + +presentation_published_dir: /var/bigbluebutton/published/presentation +presentation_unpublished_dir: /var/bigbluebutton/unpublished/presentation diff --git a/record-and-playback/presentation_export/scripts/process/presentation_export.rb b/record-and-playback/presentation_export/scripts/process/presentation_export.rb new file mode 100755 index 0000000000000000000000000000000000000000..00bda56dbb88d03ed1ccfc57655c2bd9aeef909f --- /dev/null +++ b/record-and-playback/presentation_export/scripts/process/presentation_export.rb @@ -0,0 +1,87 @@ +# Set encoding to utf-8 +# 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/>. +# + +require File.expand_path('../../../lib/recordandplayback', __FILE__) +require 'rubygems' +require 'trollop' +require 'yaml' + +opts = Trollop::options do + opt :meeting_id, "Meeting id to archive", :default => '58f4a6b3-cd07-444d-8564-59116cb53974', :type => String +end + +meeting_id = opts[:meeting_id] + +# This script lives in scripts/archive/steps while properties.yaml lives in scripts/ +bbb_props = YAML::load(File.open('../../core/scripts/bigbluebutton.yml')) +recording_dir = bbb_props['recording_dir'] + +props = YAML::load(File.open('presentation_export.yml')) +presentation_published_dir = props['presentation_published_dir'] +presentation_unpublished_dir = props['presentation_unpublished_dir'] +playback_dir = props['playback_dir'] + +target_dir = "#{recording_dir}/process/presentation_export/#{meeting_id}" +if not FileTest.directory?(target_dir) + # this recording has never been processed + + logger = Logger.new("/var/log/bigbluebutton/presentation_export/process-#{meeting_id}.log", 'daily' ) + BigBlueButton.logger = logger + + if not File.exists? "#{recording_dir}/status/published/#{meeting_id}-presentation.done" + BigBlueButton.logger.info "Presentation not published yet, aborting" + abort + end + + FileUtils.mkdir_p "/var/log/bigbluebutton/presentation_export" + + publish_dir = "#{recording_dir}/publish/presentation/#{meeting_id}" + if FileTest.directory?(publish_dir) + # this recording has already been published (or publish processed), need to + # figure out if it's published or unpublished + + meeting_published_dir = "#{presentation_published_dir}/#{meeting_id}" + if not FileTest.directory?(meeting_published_dir) + meeting_published_dir = "#{presentation_unpublished_dir}/#{meeting_id}" + if not FileTest.directory?(meeting_published_dir) + meeting_published_dir = nil + end + end + + if meeting_published_dir + BigBlueButton.logger.info("Processing script presentation_export.rb") + FileUtils.mkdir_p target_dir + + resources_dir = "#{target_dir}/resources" + FileUtils.mkdir_p resources_dir + FileUtils.cp_r Dir.glob("#{meeting_published_dir}/*"), resources_dir + + player_dir = "#{target_dir}/playback" + FileUtils.mkdir_p player_dir + FileUtils.cp_r Dir.glob("#{playback_dir}/*"), player_dir + + process_done = File.new("#{recording_dir}/status/processed/#{meeting_id}-presentation_export.done", "w") + process_done.write("Processed #{meeting_id}") + process_done.close + end + end +end + diff --git a/record-and-playback/presentation_export/scripts/publish/presentation_export.rb b/record-and-playback/presentation_export/scripts/publish/presentation_export.rb new file mode 100755 index 0000000000000000000000000000000000000000..b7c82ff0ec24f519ed74e038d33eddf439e57f82 --- /dev/null +++ b/record-and-playback/presentation_export/scripts/publish/presentation_export.rb @@ -0,0 +1,124 @@ +# Set encoding to utf-8 +# 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/>. +# + +require File.expand_path('../../../lib/recordandplayback', __FILE__) +require 'rubygems' +require 'trollop' +require 'yaml' +require 'zip' + +opts = Trollop::options do + opt :meeting_id, "Meeting id to archive", :default => '58f4a6b3-cd07-444d-8564-59116cb53974', :type => String +end + +$meeting_id = opts[:meeting_id] +match = /(.*)-(.*)/.match $meeting_id +$meeting_id = match[1] +$playback = match[2] + +if ($playback == "presentation_export") + logger = Logger.new("/var/log/bigbluebutton/presentation_export/publish-#{$meeting_id}.log", 'daily' ) + BigBlueButton.logger = logger + # This script lives in scripts/archive/steps while properties.yaml lives in scripts/ + + bbb_props = YAML::load(File.open('../../core/scripts/bigbluebutton.yml')) + simple_props = YAML::load(File.open('presentation_export.yml')) + BigBlueButton.logger.info("Setting recording dir") + recording_dir = bbb_props['recording_dir'] + BigBlueButton.logger.info("Setting process dir") + process_dir = "#{recording_dir}/process/presentation_export/#{$meeting_id}" + BigBlueButton.logger.info("setting publish dir") + publish_dir = simple_props['publish_dir'] + BigBlueButton.logger.info("setting playback host") + playback_host = bbb_props['playback_host'] + BigBlueButton.logger.info("setting target dir") + target_dir = "#{recording_dir}/publish/presentation_export/#{$meeting_id}" + + raw_archive_dir = "#{recording_dir}/raw/#{$meeting_id}" + + if not FileTest.directory?(target_dir) + if not File.exists? "#{recording_dir}/status/published/#{$meeting_id}-presentation.done" + BigBlueButton.logger.info "Presentation not published yet, aborting" + abort + end + + BigBlueButton.logger.info("Making dir target_dir") + FileUtils.mkdir_p target_dir + + temp_dir = "#{target_dir}/temp" + FileUtils.mkdir_p temp_dir + zipped_directory = "#{temp_dir}/zipped" + FileUtils.mkdir_p zipped_directory + + FileUtils.cp_r "#{process_dir}/resources", zipped_directory + FileUtils.cp_r "#{process_dir}/playback", zipped_directory + FileUtils.mv "#{zipped_directory}/playback/playback.html", zipped_directory + + package_dir = "#{target_dir}/#{$meeting_id}" + BigBlueButton.logger.info("Making dir package_dir") + FileUtils.mkdir_p package_dir + + BigBlueButton.logger.info("Creating the .zip file") + + zipped_file = "#{package_dir}/#{$meeting_id}.zip" + Zip::File.open(zipped_file, Zip::File::CREATE) do |zipfile| + Dir["#{zipped_directory}/**/**"].reject{|f|f==zipped_file}.each do |file| + zipfile.add(file.sub(zipped_directory+'/', ''), file) + end + end + FileUtils.chmod 0644, zipped_file + + BigBlueButton.logger.info("Creating metadata.xml") + presentation_metadata = "#{process_dir}/resources/metadata.xml" + BigBlueButton.logger.info "Parsing metadata on #{presentation_metadata}" + doc = nil + begin + doc = Nokogiri::XML(open(presentation_metadata).read) + rescue Exception => e + BigBlueButton.logger.error "Something went wrong: #{$!}" + raise e + end + doc.at("published").content = true; + doc.at("format").content = "presentation_export" + doc.at("link").content = "http://#{playback_host}/presentation_export/#{$meeting_id}/#{$meeting_id}.zip" + + metadata_xml = File.new("#{package_dir}/metadata.xml","w") + metadata_xml.write(doc.to_xml(:indent => 2)) + metadata_xml.close + + if not FileTest.directory?(publish_dir) + FileUtils.mkdir_p publish_dir + end + FileUtils.cp_r(package_dir, publish_dir) # Copy all the files. + BigBlueButton.logger.info("Finished publishing script presentation.rb successfully.") + + BigBlueButton.logger.info("Removing processed files.") + FileUtils.rm_r(Dir.glob("#{process_dir}/*")) + + BigBlueButton.logger.info("Removing published files.") + FileUtils.rm_r(Dir.glob("#{target_dir}/*")) + + publish_done = File.new("#{recording_dir}/status/published/#{$meeting_id}-presentation_export.done", "w") + publish_done.write("Published #{$meeting_id}") + publish_done.close + end + +end