diff --git a/bbb-screenshare/.classpath b/bbb-screenshare/.classpath new file mode 100755 index 0000000000000000000000000000000000000000..ddb4e05b80d8e90cdb0a2414d56ffe0cf2c51fd2 --- /dev/null +++ b/bbb-screenshare/.classpath @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="app/src/main/java"/> + <classpathentry kind="src" path="app/src/main/scala"/> + <classpathentry kind="src" path="app/src/test/java"/> + <classpathentry kind="src" path="jws/webstart/src/main/java"/> + <classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="lib" path="app/lib/aopalliance-1.0.jar"/> + <classpathentry kind="lib" path="app/lib/commons-fileupload-1.2.2.jar"/> + <classpathentry kind="lib" path="app/lib/commons-io-2.1.jar"/> + <classpathentry kind="lib" path="app/lib/commons-pool-1.5.6.jar"/> + <classpathentry kind="lib" path="app/lib/configgy-2.0.0.jar"/> + <classpathentry kind="lib" path="app/lib/easymock-2.4.jar"/> + <classpathentry kind="lib" path="app/lib/gson-1.7.1.jar"/> + <classpathentry kind="lib" path="app/lib/jcl-over-slf4j-1.7.9.jar"/> + <classpathentry kind="lib" path="app/lib/jedis-1.5.1.jar"/> + <classpathentry kind="lib" path="app/lib/jul-to-slf4j-1.7.9.jar"/> + <classpathentry kind="lib" path="app/lib/log4j-over-slf4j-1.7.9.jar"/> + <classpathentry kind="lib" path="app/lib/logback-classic-1.1.2.jar"/> + <classpathentry kind="lib" path="app/lib/logback-core-1.1.2.jar"/> + <classpathentry kind="lib" path="app/lib/mina-core-2.0.8.jar"/> + <classpathentry kind="lib" path="app/lib/mina-integration-beans-2.0.8.jar"/> + <classpathentry kind="lib" path="app/lib/mina-integration-jmx-2.0.8.jar"/> + <classpathentry kind="lib" path="app/lib/red5-io-1.0.6-SNAPSHOT.jar"/> + <classpathentry kind="lib" path="app/lib/red5-server-1.0.6-SNAPSHOT.jar"/> + <classpathentry kind="lib" path="app/lib/red5-server-common-1.0.6-SNAPSHOT.jar"/> + <classpathentry kind="lib" path="app/lib/scala-library-2.9.2.jar"/> + <classpathentry kind="lib" path="app/lib/servlet-api-2.5.jar"/> + <classpathentry kind="lib" path="app/lib/slf4j-api-1.7.9.jar"/> + <classpathentry kind="lib" path="app/lib/spring-aop-4.0.8.RELEASE.jar"/> + <classpathentry kind="lib" path="app/lib/spring-beans-4.0.8.RELEASE.jar"/> + <classpathentry kind="lib" path="app/lib/spring-context-4.0.8.RELEASE.jar"/> + <classpathentry kind="lib" path="app/lib/spring-core-4.0.8.RELEASE.jar"/> + <classpathentry kind="lib" path="app/lib/spring-web-4.0.8.RELEASE.jar"/> + <classpathentry kind="lib" path="app/lib/spring-webmvc-4.0.7.RELEASE.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/bbb-screenshare/.gitignore b/bbb-screenshare/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..20d12dd51408a0acf0f81cef518c9f7b5564e8c0 --- /dev/null +++ b/bbb-screenshare/.gitignore @@ -0,0 +1,8 @@ +.manager +.scala_dependencies +.classpath +.gradle/ +.project +app/build/ +lib/ +build diff --git a/bbb-screenshare/.project b/bbb-screenshare/.project new file mode 100755 index 0000000000000000000000000000000000000000..7e258648efc7fccd62009ea00e461fc2f8647d96 --- /dev/null +++ b/bbb-screenshare/.project @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>s-bbb-screenshare</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.scala-ide.sdt.core.scalabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.scala-ide.sdt.core.scalanature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/bbb-screenshare/app/.classpath.old b/bbb-screenshare/app/.classpath.old new file mode 100755 index 0000000000000000000000000000000000000000..263d60672590638b500b7dd3c7835f47d355c50c --- /dev/null +++ b/bbb-screenshare/app/.classpath.old @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<classpath> + <classpathentry kind="src" path="src/main/java"/> + <classpathentry kind="output" path="build/classes/main"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="src" path="/common" combineaccessrules="false"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/commons-fileupload/commons-fileupload/jars/commons-fileupload-1.2.1.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/commons-io/commons-io/jars/commons-io-1.4.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/javax.servlet/servlet-api/jars/servlet-api-2.5.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/log4j-over-slf4j/jars/log4j-over-slf4j-1.5.6.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/logback-classic/jars/logback-classic-0.9.14.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/logback-core/jars/logback-core-0.9.14.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/net/lag/configgy/configgy/jars/configgy-1.5.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/org.apache.mina/mina-core/jars/mina-core-2.0.0-RC1.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/org.apache.mina/mina-integration-spring/jars/mina-integration-spring-1.1.7.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/org.scala-lang/scala-library/jars/scala-library-2.7.7.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/org/red5/red5/jars/red5-0.91.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/slf4j-api/jars/slf4j-api-1.5.6.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/spring/spring-aop/jars/spring-aop-3.0.0.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/spring/spring-beans/jars/spring-beans-3.0.0.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/spring/spring-context/jars/spring-context-3.0.0.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/spring/spring-core/jars/spring-core-3.0.0.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/spring/spring-web/jars/spring-web-3.0.0.jar"/> + <classpathentry kind="lib" path="/home/firstuser/.gradle/cache/spring/spring-webmvc/jars/spring-webmvc-2.5.6.jar"/> +</classpath> diff --git a/bbb-screenshare/app/build.gradle b/bbb-screenshare/app/build.gradle new file mode 100755 index 0000000000000000000000000000000000000000..cebd3fe968f3dff9d5d5e55abb1a9761a2e91e8f --- /dev/null +++ b/bbb-screenshare/app/build.gradle @@ -0,0 +1,160 @@ +apply plugin: 'scala' +apply plugin: 'java' +apply plugin: 'war' +apply plugin: 'eclipse' + +version = '0.8' +jar.enabled = true + +def appName = 'bbb-screenshare' + +archivesBaseName = appName + +task resolveDeps(type: Copy) { + into('lib') + from configurations.default + from configurations.default.allArtifacts*.file +} + +repositories { + mavenCentral() + mavenLocal() + add(new org.apache.ivy.plugins.resolver.ChainResolver()) { + name = 'remote' + returnFirst = true + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = "googlecode" + addArtifactPattern "http://red5.googlecode.com/svn/repository/[artifact](-[revision]).[ext]" + addArtifactPattern "http://red5.googlecode.com/svn/repository/[organisation]/[artifact](-[revision]).[ext]" + } + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = "blindside-repos" + addArtifactPattern "http://blindside.googlecode.com/svn/repository/[artifact](-[revision]).[ext]" + addArtifactPattern "http://blindside.googlecode.com/svn/repository/[organisation]/[artifact](-[revision]).[ext]" + } + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = "maven2-central" + m2compatible = true + addArtifactPattern "http://repo1.maven.org/maven2/[organisation]/[module]/[revision]/[artifact](-[revision]).[ext]" + addArtifactPattern "http://repo1.maven.org/maven2/[organisation]/[artifact]/[revision]/[artifact](-[revision]).[ext]" + } + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = "testng_ibiblio_maven2" + m2compatible = true + addArtifactPattern "http://repo1.maven.org/maven2/[organisation]/[module]/[revision]/[artifact](-[revision])-jdk15.[ext]" + addArtifactPattern "http://repo1.maven.org/maven2/[organisation]/[artifact]/[revision]/[artifact](-[revision])-jdk15.[ext]" + } + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = "netty-dependency" + m2compatible = true + addArtifactPattern "http://repository.jboss.org/nexus/content/groups/public-jboss/[organisation]/[module]/[revision]/[artifact](-[revision]).[ext]" + addArtifactPattern "http://repo1.maven.org/maven2/[organisation]/[artifact]/[revision]/[artifact](-[revision]).[ext]" + } + add(new org.apache.ivy.plugins.resolver.URLResolver()) { + name = "spring-bundles" + m2compatible = true + addArtifactPattern "http://repository.springsource.com/maven/bundles/external/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]" + addArtifactPattern "http://repository.springsource.com/maven/bundles/release/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]" + } + mavenRepo name: "jboss", urls: "http://repository.jboss.org/nexus/content/groups/public-jboss" + mavenRepo name: "sonatype-snapshot", urls: "http://oss.sonatype.org/content/repositories/snapshots" + mavenRepo name: "sonatype-releases", urls: "http://oss.sonatype.org/content/repositories/releases" + } +} + +dependencies { + // Servlet + providedCompile 'javax.servlet:servlet-api:2.5@jar' + + // Mina + providedCompile 'org.apache.mina:mina-core:2.0.8@jar' + providedCompile 'org.apache.mina:mina-integration-beans:2.0.8@jar' + providedCompile 'org.apache.mina:mina-integration-jmx:2.0.8@jar' + + // Spring + providedCompile 'org.springframework:spring-web:4.0.8.RELEASE@jar' + providedCompile 'org.springframework:spring-beans:4.0.8.RELEASE@jar' + providedCompile 'org.springframework:spring-context:4.0.8.RELEASE@jar' + providedCompile 'org.springframework:spring-core:4.0.8.RELEASE@jar' + + // Red5 + providedCompile 'org.red5:red5-server:1.0.6-SNAPSHOT@jar' + providedCompile 'org.red5:red5-server-common:1.0.6-SNAPSHOT@jar' + providedCompile 'org.red5:red5-io:1.0.6-SNAPSHOT@jar' + + // Logging + providedCompile 'ch.qos.logback:logback-core:1.1.2@jar' + providedCompile 'ch.qos.logback:logback-classic:1.1.2@jar' + providedCompile 'org.slf4j:log4j-over-slf4j:1.7.9@jar' + providedCompile 'org.slf4j:jcl-over-slf4j:1.7.9@jar' + providedCompile 'org.slf4j:jul-to-slf4j:1.7.9@jar' + providedCompile 'org.slf4j:slf4j-api:1.7.9@jar' + + + // Needed for the JVM shutdown hook but needs to be put into red5/lib dir. + // Otherwise we get exception on aop utils class not found. + providedCompile 'org.springframework:spring-aop:4.0.8.RELEASE@jar' + providedCompile 'aopalliance:aopalliance:1.0@jar' + + // Testing + //compile 'org.testng:testng:5.8@jar' + compile 'org.easymock:easymock:2.4@jar' + + // Testing + //testRuntime 'org/testng:testng:5.8@jar' + testRuntime 'org.easymock:easymock:2.4@jar' + + // Tunnelling servlet + compile 'org.springframework:spring-webmvc:4.0.7.RELEASE@jar' + + // Need to put commons-fileupload and commons-io in red5/lib dir. Otherwise, we get an + // java.lang.NoClassDefFoundError: org/apache/commons/fileupload/FileItemFactory or + // java.lang.NoClassDefFoundError: org/apache/commons/io/output/DeferredFileOutputStream + // ralam (Feb 27, 2013) + providedCompile 'commons-fileupload:commons-fileupload:1.2.2@jar' + providedCompile 'commons-io:commons-io:2.1@jar' + + // Libraries needed to run the scala tools + scalaTools 'org.scala-lang:scala-compiler:2.9.2' + scalaTools 'org.scala-lang:scala-library:2.9.2' + + // workaround for http://issues.gradle.org/browse/GRADLE-1273 + //compileScala.classpath = sourceSets.main.compileClasspath + files(sourceSets.main.classesDir) + //compileTestScala.classpath = sourceSets.test.compileClasspath + files(sourceSets.test.classesDir) + + // Libraries needed for scala api + compile 'org.scala-lang:scala-library:2.9.2' + compile 'net.lag:configgy:2.0.0@jar' + + //redis + compile 'redis.clients:jedis:1.5.1' + providedCompile 'commons-pool:commons-pool:1.5.6' + compile 'com.google.code.gson:gson:1.7.1' +} + +test { + useTestNG() +} + +war.doLast { + ant.unzip(src: war.archivePath, dest: "$buildDir/screenshare") +} + +task deploy() << { + def red5AppsDir = '/usr/share/red5/webapps' + def screenshareDir = new File("${red5AppsDir}/screenshare") + println "Deleting $screenshareDir" + ant.delete(dir: screenshareDir) + ant.mkdir(dir: screenshareDir) + ant.copy(todir: screenshareDir) { + fileset(dir: "$buildDir/screenshare") + } + def jwsLibDir = new File("${red5AppsDir}/screenshare/lib") + ant.mkdir(dir: jwsLibDir) + ant.copy(todir: jwsLibDir) { + fileset(dir: "jws/lib") + } + ant.copy(todir: screenshareDir) { + fileset(file: "jws/screenshare.jnlp") + } +} diff --git a/bbb-screenshare/app/deploy.sh b/bbb-screenshare/app/deploy.sh new file mode 100755 index 0000000000000000000000000000000000000000..408e1f44f41827b219f8b7e004016f5c17f2ac94 --- /dev/null +++ b/bbb-screenshare/app/deploy.sh @@ -0,0 +1,4 @@ +sudo chmod -R 777 /usr/share/red5/webapps +gradle clean war deploy +sudo chmod -R 777 /usr/share/red5/webapps + diff --git a/bbb-screenshare/app/jws/screenshare.jnlp b/bbb-screenshare/app/jws/screenshare.jnlp new file mode 100755 index 0000000000000000000000000000000000000000..f88969b672b728dc5b4a308ee4e624bddc01c074 --- /dev/null +++ b/bbb-screenshare/app/jws/screenshare.jnlp @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<jnlp spec="1.0+" codebase="." href=""> +<!-- + Keep href empty. Otherwise this jnlp file will always be cached. + http://www.coderanch.com/t/284889/JSP/java/Caching-JNLP +--> + <information> + <title>BigBlueButton Screen Share</title> + <vendor>BigBlueButton</vendor> + </information> + + <resources> + <j2se version="1.7+" href="http://java.sun.com/products/autodl/j2se"/> + <jar href="$$jnlpUrl/lib/javacv-screenshare-0.0.1.jar" main="true" /> + <jar href="$$jnlpUrl/lib/javacv.jar" /> + <jar href="$$jnlpUrl/lib/javacpp.jar" /> + <jar href="$$jnlpUrl/lib/ffmpeg.jar" /> + </resources> + + <resources os="Windows" arch="amd64"> + <nativelib href="$$jnlpUrl/lib/ffmpeg-windows-x86_64.jar" download="eager"/> + </resources> + + <resources os="Windows" arch="x86"> + <nativelib href="$$jnlpUrl/lib/ffmpeg-windows-x86.jar" download="eager"/> + </resources> + + <resources os="Linux" arch="x86_64 amd64"> + <nativelib href="$$jnlpUrl/lib/ffmpeg-linux-x86_64.jar" download="eager"/> + </resources> + + <resources os="Linux" arch="x86 i386 i486 i586 i686"> + <nativelib href="$$jnlpUrl/lib/ffmpeg-linux-x86.jar" download="eager"/> + </resources> + + + <application-desc + name="Desktop Sharing Demo Application" + main-class="org.bigbluebutton.screenshare.client.DeskshareMain"> + <argument>$$publishUrl</argument> + <argument>$$serverUrl</argument> + <argument>$$meetingId</argument> + <argument>$$streamId</argument> + <argument>$$fullScreen</argument> + <argument>$$codecOptions</argument> + <argument>$$errorMessage</argument> + </application-desc> + <security><all-permissions/></security> + <update check="always" policy="always"/> +</jnlp> diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiff.java b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiff.java new file mode 100755 index 0000000000000000000000000000000000000000..97fee6d3bf07ab0b540100052d73bebb029dbae8 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiff.java @@ -0,0 +1,642 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.jardiff; + +import java.io.*; +import java.util.*; +import java.util.jar.*; +import java.util.zip.*; + + +/** + * JarDiff is able to create a jar file containing the delta between two + * jar files (old and new). The delta jar file can then be applied to the + * old jar file to reconstruct the new jar file. + * <p> + * Refer to the JNLP spec for details on how this is done. + * + * @version 1.13, 06/26/03 + */ +public class JarDiff implements JarDiffConstants { + private static final int DEFAULT_READ_SIZE = 2048; + private static byte[] newBytes = new byte[DEFAULT_READ_SIZE]; + private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE]; + private static ResourceBundle _resources = null; + + // The JARDiff.java is the stand-along jardiff.jar tool. Thus, we do not + // depend on Globals.java and other stuff here. Instead, we use an explicit + // _debug flag. + private static boolean _debug; + + public static ResourceBundle getResources() { + if (_resources == null) { + _resources = ResourceBundle.getBundle("jnlp/sample/jardiff/resources/strings"); + } + return _resources; + } + + /** + * Creates a patch from the two passed in files, writing the result + * to <code>os</code>. + */ + public static void createPatch(String oldPath, String newPath, + OutputStream os, boolean minimal) throws IOException{ + JarFile2 oldJar = new JarFile2(oldPath); + JarFile2 newJar = new JarFile2(newPath); + + try { + Iterator entries; + HashMap moved = new HashMap(); + HashSet visited = new HashSet(); + HashSet implicit = new HashSet(); + HashSet moveSrc = new HashSet(); + HashSet newEntries = new HashSet(); + + + // FIRST PASS + // Go through the entries in new jar and + // determine which files are candidates for implicit moves + // ( files that has the same filename and same content in old.jar + // and new.jar ) + // and for files that cannot be implicitly moved, we will either + // find out whether it is moved or new (modified) + entries = newJar.getJarEntries(); + if (entries != null) { + while (entries.hasNext()) { + JarEntry newEntry = (JarEntry)entries.next(); + String newname = newEntry.getName(); + + // Return best match of contents, will return a name match if possible + String oldname = oldJar.getBestMatch(newJar, newEntry); + if (oldname == null) { + // New or modified entry + if (_debug) { + System.out.println("NEW: "+ newname); + } + newEntries.add(newname); + } else { + // Content already exist - need to do a move + + // Should do implicit move? Yes, if names are the same, and + // no move command already exist from oldJar + if (oldname.equals(newname) && !moveSrc.contains(oldname)) { + if (_debug) { + System.out.println(newname + " added to implicit set!"); + } + implicit.add(newname); + } else { + // The 1.0.1/1.0 JarDiffPatcher cannot handle + // multiple MOVE command with same src. + // The work around here is if we are going to generate + // a MOVE command with duplicate src, we will + // instead add the target as a new file. This way + // the jardiff can be applied by 1.0.1/1.0 + // JarDiffPatcher also. + if (!minimal && (implicit.contains(oldname) || + moveSrc.contains(oldname) )) { + + // generate non-minimal jardiff + // for backward compatibility + + if (_debug) { + + System.out.println("NEW: "+ newname); + } + newEntries.add(newname); + } else { + // Use newname as key, since they are unique + if (_debug) { + System.err.println("moved.put " + newname + " " + oldname); + } + moved.put(newname, oldname); + moveSrc.add(oldname); + } + // Check if this disables an implicit 'move <oldname> <oldname>' + if (implicit.contains(oldname) && minimal) { + + if (_debug) { + System.err.println("implicit.remove " + oldname); + + System.err.println("moved.put " + oldname + " " + oldname); + + } + implicit.remove(oldname); + moved.put(oldname, oldname); + moveSrc.add(oldname); + } + + + } + } + } + } //if (entries != null) + + // SECOND PASS: <deleted files> = <oldjarnames> - <implicitmoves> - + // <source of move commands> - <new or modified entries> + ArrayList deleted = new ArrayList(); + entries = oldJar.getJarEntries(); + if (entries != null) { + while (entries.hasNext()) { + JarEntry oldEntry = (JarEntry)entries.next(); + String oldName = oldEntry.getName(); + if (!implicit.contains(oldName) && !moveSrc.contains(oldName) + && !newEntries.contains(oldName)) { + if (_debug) { + System.err.println("deleted.add " + oldName); + } + deleted.add(oldName); + } + } + } + + //DEBUG + if (_debug) { + //DEBUG: print out moved map + entries = moved.keySet().iterator(); + if (entries != null) { + System.out.println("MOVED MAP!!!"); + while (entries.hasNext()) { + String newName = (String)entries.next(); + String oldName = (String)moved.get(newName); + System.out.println("key is " + newName + " value is " + oldName); + } + } + + //DEBUG: print out IMOVE map + entries = implicit.iterator(); + if (entries != null) { + System.out.println("IMOVE MAP!!!"); + while (entries.hasNext()) { + String newName = (String)entries.next(); + System.out.println("key is " + newName); + } + } + } + + JarOutputStream jos = new JarOutputStream(os); + + // Write out all the MOVEs and REMOVEs + createIndex(jos, deleted, moved); + + // Put in New and Modified entries + entries = newEntries.iterator(); + if (entries != null) { + + while (entries.hasNext()) { + String newName = (String)entries.next(); + if (_debug) { + System.out.println("New File: " + newName); + } + writeEntry(jos, newJar.getEntryByName(newName), newJar); + } + } + + + jos.finish(); + jos.close(); + + } catch (IOException ioE){ + throw ioE; + } finally { + try { + oldJar.getJarFile().close(); + } catch (IOException e1) { + //ignore + } + try { + newJar.getJarFile().close(); + } catch (IOException e1) { + //ignore + } + } // finally + } + + /** + * Writes the index file out to <code>jos</code>. + * <code>oldEntries</code> gives the names of the files that were removed, + * <code>movedMap</code> maps from the new name to the old name. + */ + private static void createIndex(JarOutputStream jos, List oldEntries, + Map movedMap) throws + IOException { + StringWriter writer = new StringWriter(); + + writer.write(VERSION_HEADER); + writer.write("\r\n"); + + // Write out entries that have been removed + for (int counter = 0; counter < oldEntries.size(); counter++) { + String name = (String)oldEntries.get(counter); + + writer.write(REMOVE_COMMAND); + writer.write(" "); + writeEscapedString(writer, name); + writer.write("\r\n"); + } + + // And those that have moved + Iterator names = movedMap.keySet().iterator(); + + if (names != null) { + while (names.hasNext()) { + String newName = (String)names.next(); + String oldName = (String)movedMap.get(newName); + + writer.write(MOVE_COMMAND); + writer.write(" "); + writeEscapedString(writer, oldName); + writer.write(" "); + writeEscapedString(writer, newName); + writer.write("\r\n"); + + } + } + + JarEntry je = new JarEntry(INDEX_NAME); + byte[] bytes = writer.toString().getBytes("UTF-8"); + + writer.close(); + jos.putNextEntry(je); + jos.write(bytes, 0, bytes.length); + } + + private static void writeEscapedString(Writer writer, String string) + throws IOException { + int index = 0; + int last = 0; + char[] chars = null; + + while ((index = string.indexOf(' ', index)) != -1) { + if (last != index) { + if (chars == null) { + chars = string.toCharArray(); + } + writer.write(chars, last, index - last); + } + last = index; + index++; + writer.write('\\'); + } + if (last != 0) { + writer.write(chars, last, chars.length - last); + } + else { + // no spaces + writer.write(string); + } + } + + private static void writeEntry(JarOutputStream jos, JarEntry entry, + JarFile2 file) throws IOException { + writeEntry(jos, entry, file.getJarFile().getInputStream(entry)); + } + + private static void writeEntry(JarOutputStream jos, JarEntry entry, + InputStream data) throws IOException { + jos.putNextEntry(entry); + + try { + // Read the entry + int size = data.read(newBytes); + + while (size != -1) { + jos.write(newBytes, 0, size); + size = data.read(newBytes); + } + } catch(IOException ioE) { + throw ioE; + } finally { + try { + data.close(); + } catch(IOException e){ + //Ignore + } + + } + } + + + + + /** + * JarFile2 wraps a JarFile providing some convenience methods. + */ + private static class JarFile2 { + private JarFile _jar; + private List _entries; + private HashMap _nameToEntryMap; + private HashMap _crcToEntryMap; + + public JarFile2(String path) throws IOException { + _jar = new JarFile(new File(path)); + index(); + } + + public JarFile getJarFile() { + return _jar; + } + + public Iterator getJarEntries() { + return _entries.iterator(); + } + + public JarEntry getEntryByName(String name) { + return (JarEntry)_nameToEntryMap.get(name); + } + + /** + * Returns true if the two InputStreams differ. + */ + private static boolean differs(InputStream oldIS, InputStream newIS) + throws IOException { + int newSize = 0; + int oldSize; + int total = 0; + boolean retVal = false; + + try{ + while (newSize != -1) { + newSize = newIS.read(newBytes); + oldSize = oldIS.read(oldBytes); + + if (newSize != oldSize) { + if (_debug) { + System.out.println("\tread sizes differ: " + newSize + + " " + oldSize + " total " + total); + } + retVal = true; + break; + } + if (newSize > 0) { + while (--newSize >= 0) { + total++; + if (newBytes[newSize] != oldBytes[newSize]) { + if (_debug) { + System.out.println("\tbytes differ at " + + total); + } + retVal = true; + break; + } + if ( retVal ) { + //Jump out + break; + } + newSize = 0; + } + } + } + } catch(IOException ioE){ + throw ioE; + } finally { + try { + oldIS.close(); + } catch(IOException e){ + //Ignore + } + try { + newIS.close(); + } catch(IOException e){ + //Ignore + } + } + return retVal; + } + + public String getBestMatch(JarFile2 file, JarEntry entry) throws IOException { + // check for same name and same content, return name if found + if (contains(file, entry)) { + return (entry.getName()); + } + + // return name of same content file or null + return (hasSameContent(file,entry)); + } + + public boolean contains(JarFile2 f, JarEntry e) throws IOException { + + JarEntry thisEntry = getEntryByName(e.getName()); + + // Look up name in 'this' Jar2File - if not exist return false + if (thisEntry == null) + return false; + + // Check CRC - if no match - return false + if (thisEntry.getCrc() != e.getCrc()) + return false; + + // Check contents - if no match - return false + InputStream oldIS = getJarFile().getInputStream(thisEntry); + InputStream newIS = f.getJarFile().getInputStream(e); + boolean retValue = differs(oldIS, newIS); + + return !retValue; + } + + public String hasSameContent(JarFile2 file, JarEntry entry) throws + IOException { + + String thisName = null; + + Long crcL = new Long(entry.getCrc()); + + // check if this jar contains files with the passed in entry's crc + if (_crcToEntryMap.containsKey(crcL)) { + // get the Linked List with files with the crc + LinkedList ll = (LinkedList)_crcToEntryMap.get(crcL); + // go through the list and check for content match + ListIterator li = ll.listIterator(0); + if (li != null) { + while (li.hasNext()) { + JarEntry thisEntry = (JarEntry)li.next(); + + // check for content match + InputStream oldIS = getJarFile().getInputStream(thisEntry); + InputStream newIS = file.getJarFile().getInputStream(entry); + + if (!differs(oldIS, newIS)) { + thisName = thisEntry.getName(); + return thisName; + } + } + } + } + + return thisName; + + } + + + + + + private void index() throws IOException { + Enumeration entries = _jar.entries(); + + _nameToEntryMap = new HashMap(); + _crcToEntryMap = new HashMap(); + + _entries = new ArrayList(); + if (_debug) { + System.out.println("indexing: " + _jar.getName()); + } + if (entries != null) { + while (entries.hasMoreElements()) { + JarEntry entry = (JarEntry)entries.nextElement(); + + long crc = entry.getCrc(); + + Long crcL = new Long(crc); + + if (_debug) { + System.out.println("\t" + entry.getName() + " CRC " + + crc); + } + + _nameToEntryMap.put(entry.getName(), entry); + _entries.add(entry); + + // generate the CRC to entries map + if (_crcToEntryMap.containsKey(crcL)) { + // key exist, add the entry to the correcponding + // linked list + + // get the linked list + LinkedList ll = (LinkedList)_crcToEntryMap.get(crcL); + + // put in the new entry + ll.add(entry); + + // put it back in the hash map + _crcToEntryMap.put(crcL, ll); + } else { + // create a new entry in the hashmap for the new key + + // first create the linked list and put in the new + // entry + LinkedList ll = new LinkedList(); + ll.add(entry); + + // create the new entry in the hashmap + _crcToEntryMap.put(crcL, ll); + } + + } + } + } + + } + + + private static void showHelp() { + System.out.println("JarDiff: [-nonminimal (for backward compatibility with 1.0.1/1.0] [-creatediff | -applydiff] [-output file] old.jar new.jar"); + } + + // -creatediff -applydiff -debug -output file + public static void main(String[] args) throws IOException { + boolean diff = true; + boolean minimal = true; + String outputFile = "out.jardiff"; + + for (int counter = 0; counter < args.length; counter++) { + // for backward compatibilty with 1.0.1/1.0 + if (args[counter].equals("-nonminimal") || + args[counter].equals("-n")) { + minimal = false; + } + else if (args[counter].equals("-creatediff") || + args[counter].equals("-c")) { + diff = true; + } + else if (args[counter].equals("-applydiff") || + args[counter].equals("-a")) { + diff = false; + } + else if (args[counter].equals("-debug") || + args[counter].equals("-d")) { + _debug = true; + } + else if (args[counter].equals("-output") || + args[counter].equals("-o")) { + if (++counter < args.length) { + outputFile = args[counter]; + } + } + else if (args[counter].equals("-applydiff") || + args[counter].equals("-a")) { + diff = false; + } + else { + if ((counter + 2) != args.length) { + showHelp(); + System.exit(0); + } + if (diff) { + try { + OutputStream os = new FileOutputStream(outputFile); + + JarDiff.createPatch(args[counter], + args[counter + 1], os, minimal); + os.close(); + } catch (IOException ioe) { + try { + System.out.println(getResources().getString("jardiff.error.create") + " " + ioe); + } catch (MissingResourceException mre) { + } + } + } + else { + try { + OutputStream os = new FileOutputStream(outputFile); + + new JarDiffPatcher().applyPatch( + null, + args[counter], + args[counter + 1], + os); + os.close(); + } catch (IOException ioe) { + try { + System.out.println(getResources().getString("jardiff.error.apply") + " " + ioe); + } catch (MissingResourceException mre) { + } + } + } + System.exit(0); + } + } + showHelp(); + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiffConstants.java b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiffConstants.java new file mode 100755 index 0000000000000000000000000000000000000000..4483030b599e8da6e921fc794acfb7c474f389ae --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiffConstants.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.jardiff; + +import java.io.*; +import java.util.*; +import java.util.jar.*; +import java.util.zip.*; + +/** + * Constants used by creating patch and applying patch for JarDiff. + * + * @version 1.8, 06/26/03 + */ +public interface JarDiffConstants { + public final String VERSION_HEADER = "version 1.0"; + public final String INDEX_NAME = "META-INF/INDEX.JD"; + public final String REMOVE_COMMAND = "remove"; + public final String MOVE_COMMAND = "move"; +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiffPatcher.java b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiffPatcher.java new file mode 100755 index 0000000000000000000000000000000000000000..e260ee120c60cc241d07c472ca38337ecc7a63c6 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/JarDiffPatcher.java @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.jardiff; + +import java.io.*; +import java.util.*; +import java.net.URL; +import java.util.jar.*; +import java.util.zip.*; + +/** + * JarDiff is able to create a jar file containing the delta between two + * jar files (old and new). The delta jar file can then be applied to the + * old jar file to reconstruct the new jar file. + * <p> + * Refer to the JNLP spec for details on how this is done. + * + * @version 1.11, 06/26/03 + */ +public class JarDiffPatcher implements JarDiffConstants, Patcher { + private static final int DEFAULT_READ_SIZE = 2048; + private static byte[] newBytes = new byte[DEFAULT_READ_SIZE]; + private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE]; + private static ResourceBundle _resources = JarDiff.getResources(); + + public static ResourceBundle getResources() { + return JarDiff.getResources(); + } + + public void applyPatch(Patcher.PatchDelegate delegate, String oldJarPath, + String jarDiffPath, OutputStream result) throws IOException { + File oldFile = new File(oldJarPath); + File diffFile = new File(jarDiffPath); + JarOutputStream jos = new JarOutputStream(result); + JarFile oldJar = new JarFile(oldFile); + JarFile jarDiff = new JarFile(diffFile); + Set ignoreSet = new HashSet(); + Map renameMap = new HashMap(); + + + determineNameMapping(jarDiff, ignoreSet, renameMap); + + // get all keys in renameMap + Object[] keys = renameMap.keySet().toArray(); + + + // Files to implicit move + Set oldjarNames = new HashSet(); + + Enumeration oldEntries = oldJar.entries(); + if (oldEntries != null) { + while (oldEntries.hasMoreElements()) { + oldjarNames.add(((JarEntry)oldEntries.nextElement()).getName()); + } + } + + // size depends on the three parameters below, which is + // basically the counter for each loop that do the actual + // writes to the output file + // since oldjarNames.size() changes in the first two loop + // below, we need to adjust the size accordingly also when + // oldjarNames.size() changes + double size = oldjarNames.size() + keys.length + jarDiff.size(); + double currentEntry = 0; + + // Handle all remove commands + oldjarNames.removeAll(ignoreSet); + size -= ignoreSet.size(); + + + // Add content from JARDiff + Enumeration entries = jarDiff.entries(); + if (entries != null) { + while (entries.hasMoreElements()) { + JarEntry entry = (JarEntry)entries.nextElement(); + + + + if (!INDEX_NAME.equals(entry.getName())) { + + updateDelegate(delegate, currentEntry, size); + currentEntry++; + + writeEntry(jos, entry, jarDiff); + + // Remove entry from oldjarNames since no implicit + //move is needed + boolean wasInOld = oldjarNames.remove(entry.getName()); + + // Update progress counters. If it was in old, we do + // not need an implicit move, so adjust total size. + if (wasInOld) size--; + + } + else { + // no write is done, decrement size + size--; + } + } + } + + + + // go through the renameMap and apply move for each entry + for (int j = 0; j < keys.length; j++) { + + + + // Apply move <oldName> <newName> command + String newName = (String)keys[j]; + String oldName = (String)renameMap.get(newName); + + // Get source JarEntry + JarEntry oldEntry = oldJar.getJarEntry(oldName); + + if (oldEntry == null) { + String moveCmd = MOVE_COMMAND + oldName + " " + newName; + handleException("jardiff.error.badmove", moveCmd); + } + + // Create dest JarEntry + JarEntry newEntry = new JarEntry(newName); + newEntry.setTime(oldEntry.getTime()); + newEntry.setSize(oldEntry.getSize()); + newEntry.setCompressedSize(oldEntry.getCompressedSize()); + newEntry.setCrc(oldEntry.getCrc()); + newEntry.setMethod(oldEntry.getMethod()); + newEntry.setExtra(oldEntry.getExtra()); + newEntry.setComment(oldEntry.getComment()); + + + updateDelegate(delegate, currentEntry, size); + currentEntry++; + + writeEntry(jos, newEntry, oldJar.getInputStream(oldEntry)); + + // Remove entry from oldjarNames since no implicit + //move is needed + boolean wasInOld = oldjarNames.remove(oldName); + + // Update progress counters. If it was in old, we do + // not need an implicit move, so adjust total size. + if (wasInOld) size--; + + } + + // implicit move + Iterator iEntries = oldjarNames.iterator(); + if (iEntries != null) { + while (iEntries.hasNext()) { + + String name = (String)iEntries.next(); + JarEntry entry = oldJar.getJarEntry(name); + + updateDelegate(delegate, currentEntry, size); + currentEntry++; + + writeEntry(jos, entry, oldJar); + } + } + + updateDelegate(delegate, currentEntry, size); + + jos.finish(); + } + + private void updateDelegate(Patcher.PatchDelegate delegate, double currentSize, double size) { + if (delegate != null) { + delegate.patching((int)(currentSize/size)); + } + } + + private void determineNameMapping(JarFile jarDiff, Set ignoreSet, + Map renameMap) throws IOException { + InputStream is = jarDiff.getInputStream(jarDiff.getEntry(INDEX_NAME)); + + if (is == null) { + handleException("jardiff.error.noindex", null); + + } + LineNumberReader indexReader = new LineNumberReader + (new InputStreamReader(is, "UTF-8")); + String line = indexReader.readLine(); + + if (line == null || !line.equals(VERSION_HEADER)) { + handleException("jardiff.error.badheader", line); + + } + + while ((line = indexReader.readLine()) != null) { + if (line.startsWith(REMOVE_COMMAND)) { + List sub = getSubpaths(line.substring(REMOVE_COMMAND. + length())); + + if (sub.size() != 1) { + handleException("jardiff.error.badremove", line); + + } + ignoreSet.add(sub.get(0)); + } + else if (line.startsWith(MOVE_COMMAND)) { + List sub = getSubpaths(line.substring(MOVE_COMMAND.length())); + + if (sub.size() != 2) { + handleException("jardiff.error.badmove", line); + + } + // target of move should be the key + if (renameMap.put(sub.get(1), sub.get(0)) != null) { + // invalid move - should not move to same target twice + handleException("jardiff.error.badmove", line); + } + } + else if (line.length() > 0) { + handleException("jardiff.error.badcommand", line); + + } + } + } + + private void handleException(String errorMsg, String line) throws IOException { + try { + throw new IOException(getResources().getString(errorMsg) + " " + line); + } catch (MissingResourceException mre) { + System.err.println("Fatal error: " + errorMsg); + new Throwable().printStackTrace(System.err); + System.exit(-1); + } + } + + private List getSubpaths(String path) { + int index = 0; + int length = path.length(); + ArrayList sub = new ArrayList(); + + while (index < length) { + while (index < length && Character.isWhitespace + (path.charAt(index))) { + index++; + } + if (index < length) { + int start = index; + int last = start; + String subString = null; + + while (index < length) { + char aChar = path.charAt(index); + if (aChar == '\\' && (index + 1) < length && + path.charAt(index + 1) == ' ') { + + if (subString == null) { + subString = path.substring(last, index); + } + else { + subString += path.substring(last, index); + } + last = ++index; + } + else if (Character.isWhitespace(aChar)) { + break; + } + index++; + } + if (last != index) { + if (subString == null) { + subString = path.substring(last, index); + } + else { + subString += path.substring(last, index); + } + } + sub.add(subString); + } + } + return sub; + } + + private void writeEntry(JarOutputStream jos, JarEntry entry, + JarFile file) throws IOException { + writeEntry(jos, entry, file.getInputStream(entry)); + } + + private void writeEntry(JarOutputStream jos, JarEntry entry, + InputStream data) throws IOException { + //Create a new ZipEntry to clear the compressed size. 5079423 + jos.putNextEntry(new ZipEntry(entry.getName())); + + // Read the entry + int size = data.read(newBytes); + + while (size != -1) { + jos.write(newBytes, 0, size); + size = data.read(newBytes); + } + data.close(); + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/Patcher.java b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/Patcher.java new file mode 100755 index 0000000000000000000000000000000000000000..457cb9e4361e48ada2727a053e27445a6578c045 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/Patcher.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.jardiff; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; + +/** + * Patcher describes the necessary method to apply and create deltas. + * + * @version 1.8, 01/23/03 + */ +public interface Patcher { + /** + * Applies a patch previously created with <code>createPatch</code>. + * Pass in a delegate to be notified of the status of the patch. + */ + public void applyPatch(PatchDelegate delegate, String oldJarPath, + String deltaPath, OutputStream result) throws IOException; + + /** + * Callback used when patching a file. + */ + public interface PatchDelegate { + public void patching(int percentDone); + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/resources/strings.properties b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/resources/strings.properties new file mode 100755 index 0000000000000000000000000000000000000000..e735688fa9925a0d569c9b19cf7d684b82fab1d8 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/jardiff/resources/strings.properties @@ -0,0 +1,41 @@ +# +# Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# -Redistribution of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# -Redistribution in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# Neither the name of Oracle nor the names of contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# This software is provided "AS IS," without a warranty of any kind. ALL +# EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING +# ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE +# OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") +# AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE +# AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS +# DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST +# REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, +# INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY +# OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, +# EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +# +# You acknowledge that this software is not designed, licensed or intended +# for use in the design, construction, operation or maintenance of any +# nuclear facility. +# + +jardiff.error.create=Unable to successfully create +jardiff.error.apply=Unable to successfully apply +jardiff.error.noindex=Invalid jardiff, no index! +jardiff.error.badheader=Invalid jardiff header: +jardiff.error.badremove=Invalid remove command: +jardiff.error.badmove=Invalid move command: +jardiff.error.badcommand=Invalid command: diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/DownloadRequest.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/DownloadRequest.java new file mode 100755 index 0000000000000000000000000000000000000000..cb46e81002bc11cf0bd57521ab6fcd0b7345f87a --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/DownloadRequest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; +import java.io.File; +import java.util.ArrayList; +import javax.servlet.*; +import javax.servlet.http.*; + +/** + * The DownloadRequest incapsulates all the data in a request + * SQE: We need to address query string + */ +public class DownloadRequest { + // Arguments + private static final String ARG_ARCH = "arch"; + private static final String ARG_OS = "os"; + private static final String ARG_LOCALE = "locale"; + private static final String ARG_VERSION_ID = "version-id"; + private static final String ARG_CURRENT_VERSION_ID = "current-version-id"; + private static final String ARG_PLATFORM_VERSION_ID = "platform-version-id"; + private static final String ARG_KNOWN_PLATFORMS = "known-platforms"; + private static final String TEST_JRE = "TestJRE"; + + private String _path = null; + private String _version = null; + private String _currentVersionId = null; + private String[] _os = null; + private String[] _arch = null; + private String[] _locale = null; + private String[] _knownPlatforms = null; + private String _query = null; + private String _testJRE = null; + private boolean _isPlatformRequest = false; + private ServletContext _context = null; + private String _encoding = null; + + private HttpServletRequest _httpRequest = null; + + // HTTP Compression RFC 2616 : Standard headers + public static final String ACCEPT_ENCODING = "accept-encoding"; + + // Contruct Request object based on HTTP request + public DownloadRequest(HttpServletRequest request) { + this((ServletContext)null, request); + } + + public DownloadRequest(ServletContext context, HttpServletRequest request) { + _context = context; + _httpRequest = request; + _path = request.getRequestURI(); + _encoding = request.getHeader(ACCEPT_ENCODING); + String context_path = request.getContextPath(); + if (context_path != null) _path = _path.substring(context_path.length()); + if (_path == null) _path = request.getServletPath(); // This works for *.<ext> invocations + if (_path == null) _path = "/"; // No path given + _path = _path.trim(); + if (_context != null && !_path.endsWith("/")) { + String realPath = _context.getRealPath(_path); + // fix for 4474021 - getRealPath might returns NULL + if (realPath != null) { + File f = new File(realPath); + if (f != null && f.exists() && f.isDirectory()) { + _path += "/"; + } + } + } + // Append default file for a directory + if (_path.endsWith("/")) _path += "launch.jnlp"; + _version = getParameter(request, ARG_VERSION_ID); + _currentVersionId = getParameter(request, ARG_CURRENT_VERSION_ID); + _os = getParameterList(request, ARG_OS); + _arch = getParameterList(request, ARG_ARCH); + _locale = getParameterList(request, ARG_LOCALE); + _knownPlatforms = getParameterList(request, ARG_KNOWN_PLATFORMS); + String platformVersion = getParameter(request, ARG_PLATFORM_VERSION_ID); + _isPlatformRequest = (platformVersion != null); + if (_isPlatformRequest) _version = platformVersion; + _query = request.getQueryString(); + _testJRE = getParameter(request, TEST_JRE); + } + + /** Returns a DownloadRequest for the currentVersionId, that can be used + * to lookup the existing cached version + */ + private DownloadRequest(DownloadRequest dreq) { + _encoding = dreq._encoding; + _context = dreq._context; + _httpRequest = dreq._httpRequest; + _path = dreq._path; + _version = dreq._currentVersionId; + _currentVersionId = null; + _os = dreq._os; + _arch = dreq._arch; + _locale = dreq._locale; + _knownPlatforms = dreq._knownPlatforms; + _isPlatformRequest = dreq._isPlatformRequest; + _query = dreq._query; + _testJRE = dreq._testJRE; + } + + + private String getParameter(HttpServletRequest req, String key) { + String res = req.getParameter(key); + return (res == null) ? null : res.trim(); + } + + /** Converts a space delimitered string to a list of strings */ + static private String[] getStringList(String str) { + if (str == null) return null; + ArrayList list = new ArrayList(); + int i = 0; + int length = str.length(); + StringBuffer sb = null; + while(i < length) { + char ch = str.charAt(i); + if (ch == ' ') { + // A space was hit. Add string to list + if (sb != null) { + list.add(sb.toString()); + sb = null; + } + } else if (ch == '\\') { + // It is a delimiter. Add next character + if (i + 1 < length) { + ch = str.charAt(++i); + if (sb == null) sb = new StringBuffer(); + sb.append(ch); + } + } else { + if (sb == null) sb = new StringBuffer(); + sb.append(ch); + } + i++; // Next character + } + // Make sure to add the last part to the list too + if (sb != null) { + list.add(sb.toString()); + } + if (list.size() == 0) return null; + String[] results = new String[list.size()]; + return (String[])list.toArray(results); + } + + /* Split parameter at spaces. Convert '\ ' insto a space */ + private String[] getParameterList(HttpServletRequest req, String key) { + String res = req.getParameter(key); + return (res == null) ? null : getStringList(res.trim()); + } + + // Query + public String getPath() { return _path; } + public String getVersion() { return _version; } + public String getCurrentVersionId() { return _currentVersionId; } + public String getQuery() { return _query; } + public String getTestJRE() { return _testJRE; } + public String getEncoding() { return _encoding; } + public String[] getOS() { return _os; } + public String[] getArch() { return _arch; } + public String[] getLocale() { return _locale; } + public String[] getKnownPlatforms() { return _knownPlatforms; } + public boolean isPlatformRequest() { return _isPlatformRequest; } + public HttpServletRequest getHttpRequest() { return _httpRequest; } + + /** Returns a DownloadRequest for the currentVersionId, that can be used + * to lookup the existing cached version + */ + DownloadRequest getFromDownloadRequest() { + return new DownloadRequest(this); + } + + // Debug + public String toString() { + return "DownloadRequest[path=" + _path + + showEntry(" encoding=", _encoding) + + showEntry(" query=", _query) + + showEntry(" TestJRE=", _testJRE) + + showEntry(" version=", _version) + + showEntry(" currentVersionId=", _currentVersionId) + + showEntry(" os=", _os) + + showEntry(" arch=", _arch) + + showEntry(" locale=", _locale) + + showEntry(" knownPlatforms=", _knownPlatforms) + + " isPlatformRequest=" + _isPlatformRequest + "]"; + } + + private String showEntry(String msg, String value) { + if (value == null) return ""; + return msg + value; + } + + private String showEntry(String msg, String[] value) { + if (value == null) return ""; + return msg + java.util.Arrays.asList(value).toString(); + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/DownloadResponse.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/DownloadResponse.java new file mode 100755 index 0000000000000000000000000000000000000000..2b134f372efad84385b873908639446f5c121de0 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/DownloadResponse.java @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; +import java.io.*; +import java.util.*; +import java.net.URL; +import java.net.URLConnection; +import javax.servlet.http.HttpServletResponse; + +/** + * A class used to encapsulate a file response, and + * factory methods to create some common types. + */ +abstract public class DownloadResponse { + private static final String HEADER_LASTMOD = "Last-Modified"; + private static final String HEADER_JNLP_VERSION = "x-java-jnlp-version-id"; + private static final String JNLP_ERROR_MIMETYPE = "application/x-java-jnlp-error"; + + public static final int STS_00_OK = 0; + public static final int ERR_10_NO_RESOURCE = 10; + public static final int ERR_11_NO_VERSION = 11; + public static final int ERR_20_UNSUP_OS = 20; + public static final int ERR_21_UNSUP_ARCH = 21; + public static final int ERR_22_UNSUP_LOCALE = 22; + public static final int ERR_23_UNSUP_JRE = 23; + public static final int ERR_99_UNKNOWN = 99; + + // HTTP Compression RFC 2616 : Standard headers + public static final String CONTENT_ENCODING = "content-encoding"; + // HTTP Compression RFC 2616 : Standard header for HTTP/Pack200 Compression + public static final String GZIP_ENCODING = "gzip"; + public static final String PACK200_GZIP_ENCODING = "pack200-gzip"; + + public DownloadResponse() { /* do nothing */ } + + public String toString() { return getClass().getName(); } + + /** Post information to an HttpResponse */ + abstract void sendRespond(HttpServletResponse response) throws IOException; + + /** Factory methods for error responses */ + static DownloadResponse getNotFoundResponse() { return new NotFoundResponse(); } + static DownloadResponse getNoContentResponse() { return new NotFoundResponse(); } + static DownloadResponse getJnlpErrorResponse(int jnlpErrorCode) { return new JnlpErrorResponse(jnlpErrorCode); } + + /** Factory method for file download responses */ + + static DownloadResponse getNotModifiedResponse() { + return new NotModifiedResponse(); + } + + static DownloadResponse getHeadRequestResponse(String mimeType, + String versionId, long lastModified, int contentLength) { + return new HeadRequestResponse(mimeType, versionId, lastModified, + contentLength); + } + + static DownloadResponse getFileDownloadResponse(byte[] content, String mimeType, long timestamp, String versionId) { + return new ByteArrayFileDownloadResponse(content, mimeType, versionId, timestamp); + } + + static DownloadResponse getFileDownloadResponse(URL resource, String mimeType, long timestamp, String versionId) { + return new ResourceFileDownloadResponse(resource, mimeType, versionId, timestamp); + } + + static DownloadResponse getFileDownloadResponse(File file, String mimeType, long timestamp, String versionId) { + return new DiskFileDownloadResponse(file, mimeType, versionId, timestamp); + } + + // + // Private classes implementing the various types + // + + static private class NotModifiedResponse extends DownloadResponse { + public void sendRespond(HttpServletResponse response) throws + IOException { + response.sendError(HttpServletResponse.SC_NOT_MODIFIED); + } + } + + static private class NotFoundResponse extends DownloadResponse { + public void sendRespond(HttpServletResponse response) throws IOException { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } + } + + static private class NoContentResponse extends DownloadResponse { + public void sendRespond(HttpServletResponse response) throws IOException { + response.sendError(HttpServletResponse.SC_NO_CONTENT); + } + } + + static private class HeadRequestResponse extends DownloadResponse { + private String _mimeType; + private String _versionId; + private long _lastModified; + private int _contentLength; + + HeadRequestResponse(String mimeType, String versionId, + long lastModified, int contentLength) { + _mimeType = mimeType; + _versionId = versionId; + _lastModified = lastModified; + _contentLength = contentLength; + } + + /** Post information to an HttpResponse */ + public void sendRespond(HttpServletResponse response) throws + IOException { + // Set header information + response.setContentType(_mimeType); + response.setContentLength(_contentLength); + if (_versionId != null) { + response.setHeader(HEADER_JNLP_VERSION, _versionId); + } + if (_lastModified != 0) { + response.setDateHeader(HEADER_LASTMOD, _lastModified); + } + response.sendError(HttpServletResponse.SC_OK); + } + } + + static public class JnlpErrorResponse extends DownloadResponse { + private String _message; + + public JnlpErrorResponse(int jnlpErrorCode) { + String msg = Integer.toString(jnlpErrorCode); + String dsc = "No description"; + try { + dsc = JnlpDownloadServlet.getResourceBundle().getString("servlet.jnlp.err." + msg); + } catch (MissingResourceException mre) { /* ignore */} + _message = msg + " " + dsc; + } + + public void sendRespond(HttpServletResponse response) throws IOException { + response.setContentType(JNLP_ERROR_MIMETYPE); + PrintWriter pw = response.getWriter(); + pw.println(_message); + }; + + public String toString() { return super.toString() + "[" + _message + "]"; } + } + + static private abstract class FileDownloadResponse extends DownloadResponse { + private String _mimeType; + private String _versionId; + private long _lastModified; + private String _fileName; + + FileDownloadResponse(String mimeType, String versionId, long lastModified) { + _mimeType = mimeType; + _versionId = versionId; + _lastModified = lastModified; + _fileName = null; + } + + FileDownloadResponse(String mimeType, String versionId, long lastModified, String fileName) { + _mimeType = mimeType; + _versionId = versionId; + _lastModified = lastModified; + _fileName = fileName; + } + + + /** Information about response */ + String getMimeType() { return _mimeType; } + String getVersionId() { return _versionId; } + long getLastModified() { return _lastModified; } + abstract int getContentLength() throws IOException; + abstract InputStream getContent() throws IOException; + + /** Post information to an HttpResponse */ + public void sendRespond(HttpServletResponse response) throws IOException { + // Set header information + response.setContentType(getMimeType()); + response.setContentLength(getContentLength()); + if (getVersionId() != null) response.setHeader(HEADER_JNLP_VERSION, getVersionId()); + if (getLastModified() != 0) response.setDateHeader(HEADER_LASTMOD, getLastModified()); + if (_fileName != null) { + + if (_fileName.endsWith(".pack.gz")) { + response.setHeader(CONTENT_ENCODING, PACK200_GZIP_ENCODING ); + } else if (_fileName.endsWith(".gz")) { + response.setHeader(CONTENT_ENCODING, GZIP_ENCODING ); + } else { + response.setHeader(CONTENT_ENCODING, null); + } + } + + // Send contents + InputStream in = getContent(); + OutputStream out = response.getOutputStream(); + try { + byte[] bytes = new byte[32 * 1024]; + int read; + while ((read = in.read(bytes)) != -1) { + out.write(bytes, 0, read); + } + } finally { + if (in != null) in.close(); + } + } + + protected String getArgString() { + long length = 0; + try { + length = getContentLength(); + } catch(IOException ioe) { /* ignore */ } + return "Mimetype=" + getMimeType() + + " VersionId=" + getVersionId() + + " Timestamp=" + new Date(getLastModified()) + + " Length=" + length; + } + } + + static private class ByteArrayFileDownloadResponse extends FileDownloadResponse { + private byte[] _content; + + ByteArrayFileDownloadResponse(byte[] content, String mimeType, String versionId, long lastModified) { + super(mimeType, versionId, lastModified); + _content = content; + } + + int getContentLength() { return _content.length; } + InputStream getContent() { return new ByteArrayInputStream(_content); } + public String toString() { return super.toString() + "[ " + getArgString() + "]"; } + } + + static private class ResourceFileDownloadResponse extends FileDownloadResponse { + URL _url; + + ResourceFileDownloadResponse(URL url, String mimeType, String versionId, long lastModified) { + super(mimeType, versionId, lastModified, url.toString()); + _url= url; + } + + int getContentLength() throws IOException { + return _url.openConnection().getContentLength(); + } + InputStream getContent() throws IOException { + return _url.openConnection().getInputStream(); + } + public String toString() { return super.toString() + "[ " + getArgString() + "]"; } + } + + static private class DiskFileDownloadResponse extends FileDownloadResponse { + private File _file; + + DiskFileDownloadResponse(File file, String mimeType, String versionId, long lastModified) { + super(mimeType, versionId, lastModified, file.getName()); + _file = file; + } + + int getContentLength() throws IOException { + return (int)_file.length(); + } + + InputStream getContent() throws IOException { + return new BufferedInputStream(new FileInputStream(_file)); + } + + public String toString() { return super.toString() + "[ " + getArgString() + "]"; } + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/ErrorResponseException.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/ErrorResponseException.java new file mode 100755 index 0000000000000000000000000000000000000000..e9dc09a194af2eca64a4e7604eb26f0852b7b016 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/ErrorResponseException.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; + +/** An exception that holds a DownloadResponse object. + * This exception can be thrown with the content describing + * the message that should be returned in the HTTP respond + */ +public class ErrorResponseException extends Exception { + private DownloadResponse _downloadResponse; + + public ErrorResponseException(DownloadResponse downloadResponse) { + _downloadResponse = downloadResponse; + } + + public DownloadResponse getDownloadResponse() { return _downloadResponse; } + + public String toString() { return _downloadResponse.toString(); } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JarDiffHandler.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JarDiffHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..06f2f975a739129eab46c4c9bf3a49412792aaad --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JarDiffHandler.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; +import java.io.*; +import java.util.*; +import jnlp.sample.jardiff.*; +import javax.servlet.*; +import javax.servlet.http.*; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +import jnlp.sample.util.VersionString; +import java.net.URL; +/* + * A class that generates and caches information about JarDiff files + * + */ +public class JarDiffHandler { + final private Logger log = Red5LoggerFactory.getLogger(JarDiffHandler.class, "screenshare"); + + // Default size of download buffer + private static final int BUF_SIZE = 32 * 1024; + + // Default JARDiff mime type + private static final String JARDIFF_MIMETYPE = "application/x-java-archive-diff"; + + /** List of all generated JARDiffs */ + private HashMap _jarDiffEntries = null; + + /** Reference to ServletContext and logger object */ + private ServletContext _servletContext = null; + private String _jarDiffMimeType = null; + + /* Contains information about a particular JARDiff entry */ + private static class JarDiffKey implements Comparable{ + private String _name; // Name of file + private String _fromVersionId; // From version + private String _toVersionId; // To version + private boolean _minimal; // True if this is a minimal jardiff + + /** Constructor used to generate a query object */ + public JarDiffKey(String name, String fromVersionId, String toVersionId, boolean minimal) { + _name = name; + _fromVersionId = fromVersionId; + _toVersionId = toVersionId; + _minimal = minimal; + } + + // Query methods + public String getName() { return _name; } + public String getFromVersionId() { return _fromVersionId; } + public String getToVersionId() { return _toVersionId; } + public boolean isMinimal() { return _minimal; } + + // Collection framework interface methods + + public int compareTo(Object o) { + // All non JarDiff entries are less + if (!(o instanceof JarDiffKey)) return -1; + JarDiffKey other = (JarDiffKey)o; + + int n = _name.compareTo(other.getName()); + if (n != 0) return n; + + n = _fromVersionId.compareTo(other.getFromVersionId()); + if (n != 0) return n; + + if (_minimal != other.isMinimal()) return -1; + + return _toVersionId.compareTo(other.getToVersionId()); + } + + public boolean equals(Object o) { + return compareTo(o) == 0; + } + + public int hashCode() { + return _name.hashCode() + + _fromVersionId.hashCode() + + _toVersionId.hashCode(); + } + } + + static private class JarDiffEntry { + private File _jardiffFile; // Location of JARDiff file + + public JarDiffEntry(File jarDiffFile) { + _jardiffFile = jarDiffFile; + } + + public File getJarDiffFile() { return _jardiffFile; } + } + + /** Initialize JarDiff handler */ + public JarDiffHandler(ServletContext servletContext) { + _jarDiffEntries = new HashMap(); + _servletContext = servletContext; + + _jarDiffMimeType = _servletContext.getMimeType("xyz.jardiff"); + if (_jarDiffMimeType == null) _jarDiffMimeType = JARDIFF_MIMETYPE; + } + + /** Returns a JarDiff for the given request */ + public synchronized DownloadResponse getJarDiffEntry(ResourceCatalog catalog, DownloadRequest dreq, JnlpResource res) { + if (dreq.getCurrentVersionId() == null) return null; + + // check whether the request is from javaws 1.0/1.0.1 + // do not generate minimal jardiff if it is from 1.0/1.0.1 + boolean doJarDiffWorkAround = isJavawsVersion(dreq, "1.0*"); + + // First do a lookup to find a match + JarDiffKey key = new JarDiffKey(res.getName(), + dreq.getCurrentVersionId(), + res.getReturnVersionId(), + !doJarDiffWorkAround); + + + JarDiffEntry entry = (JarDiffEntry)_jarDiffEntries.get(key); + // If entry is not found, then the querty has not been made. + if (entry == null) { + if (log.isInfoEnabled()) { + log.info("servlet.log.info.jardiff.gen", + res.getName(), + dreq.getCurrentVersionId(), + res.getReturnVersionId()); + } + File f = generateJarDiff(catalog, dreq, res, doJarDiffWorkAround); + if (f == null) { + log.warn("servlet.log.warning.jardiff.failed", + res.getName(), + dreq.getCurrentVersionId(), + res.getReturnVersionId()); + } + // Store entry in table + entry = new JarDiffEntry(f); + _jarDiffEntries.put(key, entry); + } + + + + // Check for no JarDiff to return + if (entry.getJarDiffFile() == null) { + return null; + } else { + return DownloadResponse.getFileDownloadResponse(entry.getJarDiffFile(), + _jarDiffMimeType, + entry.getJarDiffFile().lastModified(), + res.getReturnVersionId()); + } + } + + + public static boolean isJavawsVersion(DownloadRequest dreq, String version) { + String javawsAgent = "javaws"; + String jwsVer = dreq.getHttpRequest().getHeader("User-Agent"); + + + // check the request is coming from javaws + if (!jwsVer.startsWith("javaws-")) { + // this is the new style User-Agent string + // User-Agent: JNLP/1.0.1 javaws/1.4.2 (b28) J2SE/1.4.2 + StringTokenizer st = new StringTokenizer(jwsVer); + while (st.hasMoreTokens()) { + String verString = st.nextToken(); + int index = verString.indexOf(javawsAgent); + if (index != -1) { + verString = verString.substring(index + javawsAgent.length() + 1); + return VersionString.contains(version, verString); + } + } + return false; + } + + // extract the version id from the download request + int startIndex = jwsVer.indexOf("-"); + + if (startIndex == -1) { + return false; + } + + int endIndex = jwsVer.indexOf("/"); + + if (endIndex == -1 || endIndex < startIndex) { + return false; + } + + String verId = jwsVer.substring(startIndex + 1, endIndex); + + + // check whether the versionString contains the versionId + return VersionString.contains(version, verId); + + } + + /** Download resource to the given file */ + private boolean download(URL target, File file) { + + log.debug("JarDiffHandler: Doing download"); + + boolean ret = true; + boolean delete = false; + // use bufferedstream for better performance + BufferedInputStream in = null; + BufferedOutputStream out = null; + try { + in = new BufferedInputStream(target.openStream()); + out = new BufferedOutputStream(new FileOutputStream(file)); + int read = 0; + int totalRead = 0; + byte[] buf = new byte[BUF_SIZE]; + while ((read = in.read(buf)) != -1) { + out.write(buf, 0, read); + totalRead += read; + } + + log.debug("total read: " + totalRead); + log.debug("Wrote URL " + target.toString() + " to file " + file); + + } catch(IOException ioe) { + + log.debug("Got exception while downloading resource: " + ioe); + + ret = false; + + if (file != null) delete = true; + + } finally { + + try { + in.close(); + in = null; + } catch (IOException ioe) { + log.debug("Got exception while downloading resource: " + ioe); + } + + try { + out.close(); + out = null; + } catch (IOException ioe) { + log.debug("Got exception while downloading resource: " + ioe); + } + + if (delete) { + file.delete(); + } + + } + return ret; + } + + // fix for 4720897 + // if the jar file resides in a war file, download it to a temp dir + // so it can be used to generate jardiff + private String getRealPath(String path) throws IOException{ + + URL fileURL = _servletContext.getResource(path); + + File tempDir = (File)_servletContext.getAttribute("javax.servlet.context.tempdir"); + + // download file into temp dir + if (fileURL != null) { + File newFile = File.createTempFile("temp", ".jar", tempDir); + if (download(fileURL, newFile)) { + String filePath = newFile.getPath(); + return filePath; + } + } + return null; + } + + + private File generateJarDiff(ResourceCatalog catalog, DownloadRequest dreq, JnlpResource res, boolean doJarDiffWorkAround) { + boolean del_old = false; + boolean del_new = false; + + // Lookup up file for request version + DownloadRequest fromDreq = dreq.getFromDownloadRequest(); + try { + JnlpResource fromRes = catalog.lookupResource(fromDreq); + + /* Get file locations */ + String newFilePath = _servletContext.getRealPath(res.getPath()); + String oldFilePath = _servletContext.getRealPath(fromRes.getPath()); + + // fix for 4720897 + if (newFilePath == null) { + newFilePath = getRealPath(res.getPath()); + if (newFilePath != null) del_new = true; + } + + if (oldFilePath == null) { + oldFilePath = getRealPath(fromRes.getPath()); + if (oldFilePath != null) del_old = true; + } + + if (newFilePath == null || oldFilePath == null) { + return null; + } + + // Create temp. file to store JarDiff file in + File tempDir = (File)_servletContext.getAttribute("javax.servlet.context.tempdir"); + + // fix for 4653036: JarDiffHandler() should use javax.servlet.context.tempdir to store the jardiff + File outputFile = File.createTempFile("jnlp", ".jardiff", tempDir); + + log.debug("Generating Jardiff between " + oldFilePath + " and " + + newFilePath + " Store in " + outputFile); + + // Generate JarDiff + OutputStream os = new FileOutputStream(outputFile); + + JarDiff.createPatch(oldFilePath, newFilePath, os, !doJarDiffWorkAround); + os.close(); + + try { + + // Check that Jardiff is smaller, or return null + if (outputFile.length() >= (new File(newFilePath).length())) { + log.debug("JarDiff discarded - since it is bigger"); + return null; + } + + // Check that Jardiff is smaller than the packed version of + // the new file, if the file exists at all + File newFilePacked = new File(newFilePath + ".pack.gz"); + if (newFilePacked.exists()) { + log.debug("generated jardiff size: " + outputFile.length()); + log.debug("packed requesting file size: " + newFilePacked.length()); + if (outputFile.length() >= newFilePacked.length()) { + log.debug("JarDiff discarded - packed version of requesting file is smaller"); + return null; + } + } + + log.debug("JarDiff generation succeeded"); + return outputFile; + + } finally { + // delete the temporarily downloaded file + if (del_new) { + new File(newFilePath).delete(); + } + + if (del_old) { + new File(oldFilePath).delete(); + } + } + } catch(IOException ioe) { + log.debug("Failed to genereate jardiff", ioe); + return null; + } catch(ErrorResponseException ere) { + log.debug("Failed to genereate jardiff", ere); + return null; + } + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpDownloadServlet.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpDownloadServlet.java new file mode 100755 index 0000000000000000000000000000000000000000..ad276853c978e6f747220fe8bead562704b898ac --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpDownloadServlet.java @@ -0,0 +1,249 @@ + + +package jnlp.sample.servlet; + +import java.io.*; +import java.util.*; +import java.net.*; +import javax.servlet.*; +import javax.servlet.http.*; +import org.bigbluebutton.app.screenshare.IScreenShareApplication; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.WebApplicationContext; + +/** + * This Servlet class is an implementation of JNLP Specification's + * Download Protocols. + * + * All requests to this servlet is in the form of HTTP GET commands. + * The parameters that are needed are: + * <ul> + * <li><code>arch</code>, + * <li><code>os</code>, + * <li><code>locale</code>, + * <li><code>version-id</code> or <code>platform-version-id</code>, + * <li><code>current-version-id</code>, + * <li>code>known-platforms</code> + * </ul> + * <p> + * + * @version 1.8 01/23/03 + */ +public class JnlpDownloadServlet extends HttpServlet { + final private Logger log = Red5LoggerFactory.getLogger(JnlpDownloadServlet.class, "screenshare"); + + // Localization + private static ResourceBundle _resourceBundle = null; + + // Servlet configuration + private static final String PARAM_JNLP_EXTENSION = "jnlp-extension"; + private static final String PARAM_JAR_EXTENSION = "jar-extension"; + + private JnlpFileHandler _jnlpFileHandler = null; + private JarDiffHandler _jarDiffHandler = null; + private ResourceCatalog _resourceCatalog = null; + + /** Initialize servlet */ + public void init(ServletConfig config) throws ServletException { + super.init(config); + + // Get extension from Servlet configuration, or use default + JnlpResource.setDefaultExtensions( + config.getInitParameter(PARAM_JNLP_EXTENSION), + config.getInitParameter(PARAM_JAR_EXTENSION)); + + _jnlpFileHandler = new JnlpFileHandler(config.getServletContext()); + _jarDiffHandler = new JarDiffHandler(config.getServletContext()); + _resourceCatalog = new ResourceCatalog(config.getServletContext()); + } + + public static synchronized ResourceBundle getResourceBundle() { + if (_resourceBundle == null) { + _resourceBundle = ResourceBundle.getBundle("jnlp/sample/servlet/resources/strings"); + } + return _resourceBundle; + } + + + public void doHead(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + handleRequest(request, response, true); + } + + /** We handle get requests too - eventhough the spec. only requeres POST requests */ + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + handleRequest(request, response, false); + } + + private void handleRequest(HttpServletRequest request, + HttpServletResponse response, boolean isHead) throws IOException { + + String requestStr = request.getRequestURI(); + if (request.getQueryString() != null) requestStr += "?" + request.getQueryString().trim(); + + // Parse HTTP request + DownloadRequest dreq = new DownloadRequest(getServletContext(), request); + if (log.isInfoEnabled()) { + log.info("servlet.log.info.request", requestStr); + log.info("servlet.log.info.useragent", request.getHeader("User-Agent")); + } + if (log.isDebugEnabled()) { + log.debug(dreq.toString()); + } + + long ifModifiedSince = request.getDateHeader("If-Modified-Since"); + + // Check if it is a valid request + try { + // Check if the request is valid + validateRequest(dreq); + + // Decide what resource to return + JnlpResource jnlpres = locateResource(dreq); + log.debug("JnlpResource: " + jnlpres); + + + if (log.isInfoEnabled()) { + log.info("servlet.log.info.goodrequest", jnlpres.getPath()); + } + + DownloadResponse dres = null; + + if (isHead) { + + int cl = + jnlpres.getResource().openConnection().getContentLength(); + + // head request response + dres = DownloadResponse.getHeadRequestResponse( + jnlpres.getMimeType(), jnlpres.getVersionId(), + jnlpres.getLastModified(), cl); + + } else if (ifModifiedSince != -1 && + (ifModifiedSince / 1000) >= + (jnlpres.getLastModified() / 1000)) { + // We divide the value returned by getLastModified here by 1000 + // because if protocol is HTTP, last 3 digits will always be + // zero. However, if protocol is JNDI, that's not the case. + // so we divide the value by 1000 to remove the last 3 digits + // before comparison + + // return 304 not modified if possible + log.debug("return 304 Not modified"); + dres = DownloadResponse.getNotModifiedResponse(); + + } else { + + // Return selected resource + dres = constructResponse(jnlpres, dreq); + } + + dres.sendRespond(response); + + } catch(ErrorResponseException ere) { + if (log.isInfoEnabled()) { + log.info("servlet.log.info.badrequest", requestStr); + } + if (log.isDebugEnabled()) { + log.debug("Response: "+ ere.toString()); + } + // Return response from exception + ere.getDownloadResponse().sendRespond(response); + } catch(Throwable e) { + log.error("servlet.log.fatal.internalerror", e); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** Make sure that it is a valid request. This is also the place to implement the + * reverse IP lookup + */ + private void validateRequest(DownloadRequest dreq) throws ErrorResponseException { + String path = dreq.getPath(); + if (path.endsWith(ResourceCatalog.VERSION_XML_FILENAME) || + path.indexOf("__") != -1 ) { + throw new ErrorResponseException(DownloadResponse.getNoContentResponse()); + } + } + + /** Interprets the download request and convert it into a resource that is + * part of the Web Archive. + */ + private JnlpResource locateResource(DownloadRequest dreq) throws IOException, ErrorResponseException { + if (dreq.getVersion() == null) { + return handleBasicDownload(dreq); + } else { + return handleVersionRequest(dreq); + } + } + + private JnlpResource handleBasicDownload(DownloadRequest dreq) throws ErrorResponseException, IOException { + log.debug("Basic Protocol lookup"); + // Do not return directory names for basic protocol + if (dreq.getPath() == null || dreq.getPath().endsWith("/")) { + throw new ErrorResponseException(DownloadResponse.getNoContentResponse()); + } + // Lookup resource + JnlpResource jnlpres = new JnlpResource(getServletContext(), dreq.getPath()); + if (!jnlpres.exists()) { + throw new ErrorResponseException(DownloadResponse.getNoContentResponse()); + } + return jnlpres; + } + + private JnlpResource handleVersionRequest(DownloadRequest dreq) throws IOException, ErrorResponseException { + log.debug("Version-based/Extension based lookup"); + return _resourceCatalog.lookupResource(dreq); + } + + /** Given a DownloadPath and a DownloadRequest, it constructs the data stream to return + * to the requester + */ + private DownloadResponse constructResponse(JnlpResource jnlpres, DownloadRequest dreq) throws IOException { + String path = jnlpres.getPath(); + if (jnlpres.isJnlpFile()) { + // It is a JNLP file. It need to be macro-expanded, so it is handled differently + boolean supportQuery = JarDiffHandler.isJavawsVersion(dreq, "1.5+"); + log.debug("SupportQuery in Href: " + supportQuery); + + // only support query string in href for 1.5 and above + if (supportQuery) { + return _jnlpFileHandler.getJnlpFileEx(jnlpres, dreq); + } else { + return _jnlpFileHandler.getJnlpFile(jnlpres, dreq); + } + } + + // Check if a JARDiff can be returned + if (dreq.getCurrentVersionId() != null && jnlpres.isJarFile()) { + DownloadResponse response = _jarDiffHandler.getJarDiffEntry(_resourceCatalog, dreq, jnlpres); + if (response != null) { + log.info("servlet.log.info.jardiff.response"); + return response; + } + } + + // check and see if we can use pack resource + JnlpResource jr = new JnlpResource(getServletContext(), + jnlpres.getName(), + jnlpres.getVersionId(), + jnlpres.getOSList(), + jnlpres.getArchList(), + jnlpres.getLocaleList(), + jnlpres.getPath(), + jnlpres.getReturnVersionId(), + dreq.getEncoding()); + + log.debug("Real resource returned: " + jr); + + // Return WAR file resource + return DownloadResponse.getFileDownloadResponse(jr.getResource(), + jr.getMimeType(), + jr.getLastModified(), + jr.getReturnVersionId()); + } + + +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpFileHandler.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpFileHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..543866d9bbd3d3952c4fd978acdd893107374092 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpFileHandler.java @@ -0,0 +1,495 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; + +import java.util.*; +import java.util.regex.*; +import java.net.*; +import java.io.*; +import javax.servlet.*; +import javax.servlet.http.*; +import javax.xml.parsers.*; +import org.xml.sax.*; +import javax.xml.transform.*; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.bigbluebutton.app.screenshare.ScreenShareInfo; +import org.bigbluebutton.app.screenshare.ScreenShareInfoResponse; +import org.bigbluebutton.app.screenshare.server.servlet.JnlpConfigurator; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.WebApplicationContext; +import org.w3c.dom.*; + +/* The JNLP file handler implements a class that keeps + * track of JNLP files and their specializations + */ +public class JnlpFileHandler { + final private Logger log = Red5LoggerFactory.getLogger(JnlpFileHandler.class, "screenshare"); + + private static final String JNLP_MIME_TYPE = "application/x-java-jnlp-file"; + private static final String HEADER_LASTMOD = "Last-Modified"; + + private ServletContext _servletContext; + private HashMap<String, JnlpFileEntry> _jnlpFiles = null; + + private boolean hasConfigurator = false; + private JnlpConfigurator configurator = null; + + /** Initialize JnlpFileHandler for the specific ServletContext */ + public JnlpFileHandler(ServletContext servletContext) { + _servletContext = servletContext; + _jnlpFiles = new HashMap<String, JnlpFileEntry>(); + } + + private static class JnlpFileEntry { + // Response + DownloadResponse _response; + // Keeps track of cache is out of date + private long _lastModified; + + // Constructor + JnlpFileEntry(DownloadResponse response, long lastmodfied) { + _response = response; + _lastModified = lastmodfied; + } + + public DownloadResponse getResponse() { return _response; } + long getLastModified() { return _lastModified; } + } + + /* Main method to lookup an entry */ + public synchronized DownloadResponse getJnlpFile(JnlpResource jnlpres, DownloadRequest dreq) + throws IOException { + log.debug("In getJnlpFile"); + + String path = jnlpres.getPath(); + URL resource = jnlpres.getResource(); + long lastModified = jnlpres.getLastModified(); + + log.debug("lastModified: " + lastModified + " " + new Date(lastModified)); + if (lastModified == 0) { + log.warn("servlet.log.warning.nolastmodified", path); + } + + // fix for 4474854: use the request URL as key to look up jnlp file + // in hash map + String reqUrl = HttpUtils.getRequestURL(dreq.getHttpRequest()).toString(); + + // Read information from WAR file + long timeStamp = new java.util.Date().getTime();; + String mimeType = _servletContext.getMimeType(path); + if (mimeType == null) mimeType = JNLP_MIME_TYPE; + + StringBuffer jnlpFileTemplate = new StringBuffer(); + URLConnection conn = resource.openConnection(); + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + String line = br.readLine(); + if (line != null && line.startsWith("TS:")) { + timeStamp = parseTimeStamp(line.substring(3)); + log.debug("Timestamp: " + timeStamp + " " + new Date(timeStamp)); + if (timeStamp == 0) { + log.warn("servlet.log.warning.notimestamp", path); + timeStamp = lastModified; + } + line = br.readLine(); + } + while(line != null) { + jnlpFileTemplate.append(line); + line = br.readLine(); + } + + if (! hasConfigurator) { + configurator = getConfigurator(); + if (configurator != null) hasConfigurator = true; + } + + String jnlpFileContent = specializeJnlpTemplate(dreq.getHttpRequest(), path, jnlpFileTemplate.toString()); + + // Convert to bytes as a UTF-8 encoding + byte[] byteContent = jnlpFileContent.getBytes("UTF-8"); + + timeStamp = new java.util.Date().getTime(); + + // Create entry + DownloadResponse resp = DownloadResponse.getFileDownloadResponse(byteContent, + mimeType, + timeStamp, + jnlpres.getReturnVersionId()); + + log.debug("JNLP: mime=[" + mimeType + "] timestamp=[" + timeStamp + "] version=[" + jnlpres.getReturnVersionId() + "]"); + return resp; + } + + /* Main method to lookup an entry (NEW for JavaWebStart 1.5+) */ + public synchronized DownloadResponse getJnlpFileEx(JnlpResource jnlpres, DownloadRequest dreq) + throws IOException { + + log.debug("In getJnlpFileEx"); + + String path = jnlpres.getPath(); + URL resource = jnlpres.getResource(); + long lastModified = jnlpres.getLastModified(); + + + log.debug("lastModified: " + lastModified + " " + new Date(lastModified)); + if (lastModified == 0) { + log.warn("servlet.log.warning.nolastmodified", path); + } + + if (! hasConfigurator) { + configurator = getConfigurator(); + if (configurator != null) hasConfigurator = true; + } + + // fix for 4474854: use the request URL as key to look up jnlp file + // in hash map + String reqUrl = HttpUtils.getRequestURL(dreq.getHttpRequest()).toString(); + // SQE: To support query string, we changed the hash key from Request URL to (Request URL + query string) + if (dreq.getQuery() != null) + reqUrl += dreq.getQuery(); + + // Read information from WAR file + long timeStamp = lastModified; + String mimeType = _servletContext.getMimeType(path); + if (mimeType == null) mimeType = JNLP_MIME_TYPE; + + StringBuffer jnlpFileTemplate = new StringBuffer(); + URLConnection conn = resource.openConnection(); + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + String line = br.readLine(); + if (line != null && line.startsWith("TS:")) { + timeStamp = parseTimeStamp(line.substring(3)); + log.debug("Timestamp: " + timeStamp + " " + new Date(timeStamp)); + if (timeStamp == 0) { + log.warn("servlet.log.warning.notimestamp", path); + timeStamp = lastModified; + } + line = br.readLine(); + } + while(line != null) { + jnlpFileTemplate.append(line); + line = br.readLine(); + } + + String jnlpFileContent = specializeJnlpTemplate(dreq.getHttpRequest(), path, jnlpFileTemplate.toString()); + + /* SQE: We need to add query string back to href in jnlp file. We also need to handle JRE requirement for + * the test. We reconstruct the xml DOM object, modify the value, then regenerate the jnlpFileContent. + */ + String query = dreq.getQuery(); + String testJRE = dreq.getTestJRE(); + log.debug("Double check query string: " + query); + // For backward compatibility: Always check if the href value exists. + // Bug 4939273: We will retain the jnlp template structure and will NOT add href value. Above old + // approach to always check href value caused some test case not run. + if (query != null) { + byte [] cb = jnlpFileContent.getBytes("UTF-8"); + ByteArrayInputStream bis = new ByteArrayInputStream(cb); + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(bis); + if (document != null && document.getNodeType() == Node.DOCUMENT_NODE) { + boolean modified = false; + Element root = document.getDocumentElement(); + + if (root.hasAttribute("href") && query != null) { + String href = root.getAttribute("href"); + root.setAttribute("href", href + "?" + query); + modified = true; + } + // Update version value for j2se tag + if (testJRE != null) { + NodeList j2seNL = root.getElementsByTagName("j2se"); + if (j2seNL != null) { + Element j2se = (Element) j2seNL.item(0); + String ver = j2se.getAttribute("version"); + if (ver.length() > 0) { + j2se.setAttribute("version", testJRE); + modified = true; + } + } + } + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer transformer = tFactory.newTransformer(); + DOMSource source = new DOMSource(document); + StringWriter sw = new StringWriter(); + StreamResult result = new StreamResult(sw); + transformer.transform(source, result); + jnlpFileContent = sw.toString(); + log.debug("Converted jnlpFileContent: " + jnlpFileContent); + // Since we modified the file on the fly, we always update the timestamp value with current time + if (modified) { + timeStamp = new java.util.Date().getTime(); + log.debug("Last modified on the fly: " + timeStamp); + } + } + } catch (Exception e) { + log.debug(e.toString(), e); + } + } + + // Convert to bytes as a UTF-8 encoding + byte[] byteContent = jnlpFileContent.getBytes("UTF-8"); + + // Create entry + DownloadResponse resp = DownloadResponse.getFileDownloadResponse(byteContent, + mimeType, + timeStamp, + jnlpres.getReturnVersionId()); + + return resp; + } + + /* This method performs the following substituations + * $$name + * $$codebase + * $$context + */ + private String specializeJnlpTemplate(HttpServletRequest request, String respath, String jnlpTemplate) { + log.debug("Query string = [" + request.getQueryString().trim() + "]"); + + String errorMessage = "NO_ERRORS"; + String authParam = request.getParameter("authToken"); + String authToken = "unknown"; + if (authParam != null && authParam != "") { + authToken = authParam.trim(); + } else { + errorMessage = "MISSING_AUTHTOKEN"; + } + + String fullScreen = request.getParameter("fullScreen"); + if (fullScreen != null && fullScreen != "") { + fullScreen = fullScreen.trim(); + } else { + errorMessage = "MISSING_FULLSCREEN"; + } + + String meetingId = request.getParameter("meetingId"); + if (meetingId != null && meetingId != "") { + meetingId = meetingId.trim(); + } else { + errorMessage = "MISSING_MEETINGID"; + } + + ScreenShareInfo sInfo = configurator.getScreenShareInfo(meetingId, authToken); + String publishUrl = "unknown"; + String streamId = "unknown"; + if (sInfo == null) { + errorMessage = "ERROR_GETTING_INFO_USING_TOKEN"; + } else { + publishUrl = sInfo.publishUrl; + streamId = sInfo.streamId; + } + + String jnlpUrl = configurator.getJnlpUrl(); + + String codecOptions = configurator.getCodecOptions(); + log.debug("Codec Options = [" + codecOptions + "]"); + + String urlprefix = getUrlPrefix(request); + int idx = respath.lastIndexOf('/'); // + String name = respath.substring(idx + 1); // Exclude / + String codebase = respath.substring(0, idx + 1); // Include / + jnlpTemplate = substitute(jnlpTemplate, "$$name", name); + jnlpTemplate = substitute(jnlpTemplate, "$$jnlpUrl", jnlpUrl); + jnlpTemplate = substitute(jnlpTemplate, "$$serverUrl", jnlpUrl); + jnlpTemplate = substitute(jnlpTemplate, "$$publishUrl", publishUrl); + jnlpTemplate = substitute(jnlpTemplate, "$$fullScreen", fullScreen); + jnlpTemplate = substitute(jnlpTemplate, "$$meetingId", meetingId); + jnlpTemplate = substitute(jnlpTemplate, "$$streamId", streamId); + jnlpTemplate = substitute(jnlpTemplate, "$$codecOptions", codecOptions); + jnlpTemplate = substitute(jnlpTemplate, "$$errorMessage", errorMessage); + // fix for 5039951: Add $$hostname macro + jnlpTemplate = substitute(jnlpTemplate, "$$hostname", request.getServerName()); + jnlpTemplate = substitute(jnlpTemplate, "$$codebase", urlprefix + request.getContextPath() + codebase); + jnlpTemplate = substitute(jnlpTemplate, "$$context", urlprefix + request.getContextPath()); + // fix for 6256326: add $$site macro to sample jnlp servlet + jnlpTemplate = substitute(jnlpTemplate, "$$site", urlprefix); + + + log.debug(jnlpTemplate); + return jnlpTemplate; + } + + // This code is heavily inspired by the stuff in HttpUtils.getRequestURL + private String getUrlPrefix(HttpServletRequest req) { + StringBuffer url = new StringBuffer(); + String scheme = req.getScheme(); + int port = req.getServerPort(); + url.append(scheme); // http, https + url.append("://"); + url.append(req.getServerName()); + if ((scheme.equals("http") && port != 80) + || (scheme.equals("https") && port != 443)) { + url.append(':'); + url.append(req.getServerPort()); + } + return url.toString(); + } + + private String substitute(String target, String key, String value) { + int start = 0; + do { + int idx = target.indexOf(key, start); + if (idx == -1) return target; + target = target.substring(0, idx) + value + target.substring(idx + key.length()); + start = idx + value.length(); + } while(true); + } + + /** Parses a ISO 8601 Timestamp. The format of the timestamp is: + * + * YYYY-MM-DD hh:mm:ss or YYYYMMDDhhmmss + * + * Hours (hh) is in 24h format. ss are optional. Time are by default relative + * to the current timezone. Timezone information can be specified + * by: + * + * - Appending a 'Z', e.g., 2001-12-19 12:00Z + * - Appending +hh:mm, +hhmm, +hh, -hh:mm -hhmm, -hh to + * indicate that the locale timezone used is either the specified + * amound before or after GMT. For example, + * + * 12:00Z = 13:00+1:00 = 0700-0500 + * + * The method returns 0 if it cannot pass the string. Otherwise, it is + * the number of milliseconds size sometime in 1969. + */ + private long parseTimeStamp(String timestamp) { + int YYYY = 0; + int MM = 0; + int DD = 0; + int hh = 0; + int mm = 0; + int ss = 0; + + timestamp = timestamp.trim(); + try { + // Check what format is used + if (matchPattern("####-##-## ##:##", timestamp)) { + YYYY = getIntValue(timestamp, 0, 4); + MM = getIntValue(timestamp, 5, 7); + DD = getIntValue(timestamp, 8, 10); + hh = getIntValue(timestamp, 11, 13); + mm = getIntValue(timestamp, 14, 16); + timestamp = timestamp.substring(16); + if (matchPattern(":##", timestamp)) { + ss = getIntValue(timestamp, 1, 3); + timestamp = timestamp.substring(3); + } + } else if (matchPattern("############", timestamp)) { + YYYY = getIntValue(timestamp, 0, 4); + MM = getIntValue(timestamp, 4, 6); + DD = getIntValue(timestamp, 6, 8); + hh = getIntValue(timestamp, 8, 10); + mm = getIntValue(timestamp, 10, 12); + timestamp = timestamp.substring(12); + if (matchPattern("##", timestamp)) { + ss = getIntValue(timestamp, 0, 2); + timestamp = timestamp.substring(2); + } + } else { + // Unknown format + return 0; + } + } catch(NumberFormatException e) { + // Bad number + return 0; + } + + String timezone = null; + // Remove timezone information + timestamp = timestamp.trim(); + if (timestamp.equalsIgnoreCase("Z")) { + timezone ="GMT"; + } else if (timestamp.startsWith("+") || timestamp.startsWith("-")) { + timezone = "GMT" + timestamp; + } + + if (timezone == null) { + // Date is relative to current locale + Calendar cal = Calendar.getInstance(); + cal.set(YYYY, MM - 1, DD, hh, mm, ss); + return cal.getTime().getTime(); + } else { + // Date is relative to a timezone + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone(timezone)); + cal.set(YYYY, MM - 1, DD, hh, mm, ss); + return cal.getTime().getTime(); + } + } + + private int getIntValue(String key, int start, int end) { + return Integer.parseInt(key.substring(start, end)); + } + + private boolean matchPattern(String pattern, String key) { + // Key must be longer than pattern + if (key.length() < pattern.length()) return false; + for(int i = 0; i < pattern.length(); i++) { + char format = pattern.charAt(i); + char ch = key.charAt(i); + if (!((format == '#' && Character.isDigit(ch)) || (format == ch))) { + return false; + } + } + return true; + } + + private JnlpConfigurator getConfigurator() { + JnlpConfigurator jnlpConfigurator = null; + if (_servletContext != null) { + //Grab a reference to the application context + ApplicationContext appCtx = (ApplicationContext) _servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + + //Get the bean holding the parameter + jnlpConfigurator = (JnlpConfigurator) appCtx.getBean("jnlpConfigurator"); + if (jnlpConfigurator != null) { + log.debug("JnlpConfigurator initialized."); + } else { + log.error("Failed to initialize JnlpConfigurator."); + } + } else { + log.error("Failed to initialize JnlpConfigurator. Context is undefined."); + } + + return jnlpConfigurator; + } + +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpResource.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpResource.java new file mode 100755 index 0000000000000000000000000000000000000000..af585c140a00a2e0d1bf8559284145885d453ecf --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/JnlpResource.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; +import javax.servlet.ServletContext; +import java.net.URL; +import java.io.File; +import java.io.IOException; +import java.net.URLConnection; +import java.util.*; + +/** + * A JnlpResource encapsulate the information about a resource that is + * needed to process a JNLP Download Request. + * + * The pattern matching arguments are: name, version-id, os, arch, and locale. + * + * The outgoing arguments are: + * - path to resource in (WAR File) + * - product version-id (Version-id to return or null. Typically same as version-id above) + * - mime-type for content + * - lastModified date of WAR file resource + * + */ +public class JnlpResource { + private static final String JNLP_MIME_TYPE = "application/x-java-jnlp-file"; + private static final String JAR_MIME_TYPE = "application/x-java-archive"; + + private static final String JAR_MIME_TYPE_NEW = "application/java-archive"; + + // Default extension for the JNLP file + private static final String JNLP_EXTENSION = ".jnlp"; + private static final String JAR_EXTENSION = ".jar"; + + private static String _jnlpExtension = JNLP_EXTENSION; + private static String _jarExtension = JAR_EXTENSION; + + public static void setDefaultExtensions(String jnlpExtension, String jarExtension) { + if (jnlpExtension != null && jnlpExtension.length() > 0) { + if (!jnlpExtension.startsWith(".")) jnlpExtension = "." + jnlpExtension; + _jnlpExtension = jnlpExtension; + } + if (jarExtension != null && jarExtension.length() > 0) { + if (!jarExtension .startsWith(".")) jarExtension = "." + jarExtension ; + _jarExtension = jarExtension; + } + } + + /* Pattern matching arguments */ + private String _name; // Name of resource with path (this is the same as path for non-version based) + private String _versionId; // Version-id for resource, or null if none + private String[] _osList; // List of OSes for which resource should be returned + private String[] _archList; // List of architectures for which the resource should be returned + private String[] _localeList; // List of locales for which the resource should be returned + /* Information used for reply */ + private String _path; // Path to resource in WAR file (unique) + private URL _resource; // URL to resource in WAR file (unique - same as above really) + private long _lastModified; // Last modified in WAR file + private String _mimeType; // Mime-type for resource + private String _returnVersionId; // Version Id to return + private String _encoding; // Accept encoding + + public JnlpResource(ServletContext context, String path) { + this(context, null, null, null, null, null, path, null); + } + + public JnlpResource(ServletContext context, + String name, + String versionId, + String[] osList, + String[] archList, + String[] localeList, + String path, + String returnVersionId) { + this(context, name, versionId, osList, archList, localeList, path, + returnVersionId, null); + } + + public JnlpResource(ServletContext context, + String name, + String versionId, + String[] osList, + String[] archList, + String[] localeList, + String path, + String returnVersionId, + String encoding) { + // Matching arguments + _encoding = encoding; + _name = name; + _versionId = versionId; + _osList = osList; + _archList = archList; + _localeList = localeList; + + _returnVersionId = returnVersionId; + + /* Check for existance and get last modified timestamp */ + try { + String orig_path = path.trim(); + String search_path = orig_path; + _resource = context.getResource(orig_path); + _mimeType = getMimeType(context, orig_path); + if (_resource != null) { + + boolean found = false; + // pack200 compression + if (encoding != null && _mimeType != null && + (_mimeType.compareTo(JAR_MIME_TYPE) == 0 || _mimeType.compareTo(JAR_MIME_TYPE_NEW) == 0) && + encoding.toLowerCase().indexOf(DownloadResponse.PACK200_GZIP_ENCODING) > -1){ + search_path = orig_path + ".pack.gz"; + _resource = context.getResource(search_path); + // Get last modified time + if (_resource != null) { + _lastModified = getLastModified(context, _resource, search_path); + if (_lastModified != 0) { + _path = search_path; + found = true; + } else { + _resource = null; + } + } + } + + // gzip compression + if (found == false && encoding != null && + encoding.toLowerCase().indexOf(DownloadResponse.GZIP_ENCODING) > -1){ + search_path = orig_path + ".gz"; + _resource = context.getResource(search_path); + // Get last modified time + if (_resource != null) { + _lastModified = getLastModified(context, _resource, search_path); + if (_lastModified != 0) { + _path = search_path; + found = true; + } else { + _resource = null; + } + } + } + + if (found == false) { + // no compression + search_path = orig_path; + _resource = context.getResource(search_path); + // Get last modified time + if (_resource != null) { + _lastModified = getLastModified(context, _resource, search_path); + if (_lastModified != 0) { + _path = search_path; + found = true; + } else { + _resource = null; + } + } + } + } + } catch(IOException ioe) { + _resource = null; + } + } + + long getLastModified(ServletContext context, URL resource, String path) { + long lastModified = 0; + URLConnection conn; + try { + // Get last modified time + conn = resource.openConnection(); + lastModified = conn.getLastModified(); + } catch (Exception e) { + // do nothing + } + + if (lastModified == 0) { + // Arguably a bug in the JRE will not set the lastModified for file URLs, and + // always return 0. This is a workaround for that problem. + String filepath = context.getRealPath(path); + if (filepath != null) { + File f = new File(filepath); + if (f.exists()) { + lastModified = f.lastModified(); + } + } + } + return lastModified; + } + + /* Get resource specific attributes */ + public String getPath() { return _path; } + public URL getResource() { return _resource; } + public String getMimeType() { return _mimeType; } + public long getLastModified() { return _lastModified; } + public boolean exists() { return _resource != null; } + public boolean isJnlpFile() { return _path.endsWith(_jnlpExtension); } + public boolean isJarFile() { return _path.endsWith(_jarExtension); } + + /* Get JNLP version specific attributes */ + public String getName() { return _name; } + public String getVersionId() { return _versionId; } + public String[] getOSList() { return _osList; } + public String[] getArchList() { return _archList; } + public String[] getLocaleList() { return _localeList; } + public String getReturnVersionId() { return _returnVersionId; } + + private String getMimeType(ServletContext context, String path) { + String mimeType = context.getMimeType(path); + if (mimeType != null) return mimeType; + if (path.endsWith(_jnlpExtension)) return JNLP_MIME_TYPE; + if (path.endsWith(_jarExtension)) return JAR_MIME_TYPE; + return "application/unknown"; + } + + /** Print info about an entry */ + public String toString() { + return "JnlpResource[WAR Path: " + _path + + showEntry(" versionId=",_versionId) + + showEntry(" name=", _name) + + " lastModified=" + new Date(_lastModified) + + showEntry(" osList=", _osList) + + showEntry(" archList=", _archList) + + showEntry(" localeList=", _localeList) + "]" + + showEntry(" returnVersionId=", _returnVersionId) + "]"; + + } + + private String showEntry(String msg, String value) { + if (value == null) return ""; + return msg + value; + } + + private String showEntry(String msg, String[] value) { + if (value == null) return ""; + return msg + java.util.Arrays.asList(value).toString(); + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/Logger.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/Logger.java new file mode 100755 index 0000000000000000000000000000000000000000..cdb1d4d8cf6c891fb9ea33dca5d62a1e2ae8870c --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/Logger.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; +import java.text.MessageFormat; +import java.util.*; +import java.io.*; +import javax.servlet.*; + +/* A loging object used by the servlets */ +public class Logger { + // Logging levels + public final static int NONE = 0; + public static final String NONE_KEY = "NONE"; + public final static int FATAL = 1; + public static final String FATAL_KEY = "FATAL"; + public final static int WARNING = 2; + public static final String WARNING_KEY = "WARNING"; + public final static int INFORMATIONAL = 3; + public static final String INFORMATIONAL_KEY = "INFORMATIONAL"; + public final static int DEBUG = 4; + public static final String DEBUG_KEY = "DEBUG"; + + // Configuration parameters + private final static String LOG_LEVEL = "logLevel"; + private final static String LOG_PATH = "logPath"; + + private int _loggingLevel = FATAL; + private ServletContext _servletContext = null; + private String _logFile = null; + private String _servletName = null; + + // Localization + ResourceBundle _resources = null; + + + /** Initialize logging object. It reads the logLevel and pathLevel init parameters. + * Default is logging level FATAL, and logging using the ServletContext.log + */ + public Logger(ServletConfig config, ResourceBundle resources) { + _resources = resources; + _servletContext = config.getServletContext(); + _servletName = config.getServletName(); + _logFile = config.getInitParameter(LOG_PATH); + if (_logFile != null) { + _logFile = _logFile.trim(); + if (_logFile.length() == 0) _logFile = null; + } + String level = config.getInitParameter(LOG_LEVEL); + if (level != null) { + level = level.trim().toUpperCase(); + if (level.equals(NONE_KEY)) _loggingLevel = NONE; + if (level.equals(FATAL_KEY)) _loggingLevel = FATAL; + if (level.equals(WARNING_KEY)) _loggingLevel = WARNING; + if (level.equals(INFORMATIONAL_KEY)) _loggingLevel = INFORMATIONAL; + if (level.equals(DEBUG_KEY)) _loggingLevel = DEBUG; + } + } + + // Logging API. Fatal, Warning, and Informational are localized + public void addFatal(String key, Throwable throwable) { + logEvent(FATAL, getString(key), throwable); + } + + public void addWarning(String key, String arg) { + logL10N(WARNING, key, arg, (Throwable)null); + } + + public void addWarning(String key, String arg, Throwable t) { + logL10N(WARNING, key, arg, t); + } + + public void addWarning(String key, String arg1, String arg2) { + logL10N(WARNING, key, arg1, arg2); + } + + public void addWarning(String key, String arg1, String arg2, String arg3) { + logL10N(WARNING, key, arg1, arg2, arg3); + } + + public void addInformational(String key) { + logEvent(INFORMATIONAL, getString(key), (Throwable)null); + } + + public void addInformational(String key, String arg) { + logL10N(INFORMATIONAL, key, arg, (Throwable)null); + } + + public void addInformational(String key, String arg1, String arg2, String arg3) { + logL10N(INFORMATIONAL, key, arg1, arg2, arg3); + } + + // Debug messages are not localized + public void addDebug(String msg) { logEvent(DEBUG, msg, null); } + + public void addDebug(String msg, Throwable throwable) { + logEvent(DEBUG, msg, throwable); + } + + // Query to test for level + boolean isNoneLevel() { return _loggingLevel >= NONE; } + boolean isFatalevel() { return _loggingLevel >= FATAL; } + boolean isWarningLevel() { return _loggingLevel >= WARNING; } + boolean isInformationalLevel() { return _loggingLevel >= INFORMATIONAL; } + boolean isDebugLevel() { return _loggingLevel >= DEBUG; } + + // Returns a string from the resources + private String getString(String key) { + try { + return _resources.getString(key); + } catch (MissingResourceException mre) { + return "Missing resource for: " + key; + } + } + + private void logL10N(int level, String key, String arg, Throwable e) { + Object[] messageArguments = { arg }; + logEvent(level, applyPattern(key, messageArguments), e); + } + + private void logL10N(int level, String key, String arg1, String arg2) { + Object[] messageArguments = { arg1, arg2 }; + logEvent(level, applyPattern(key, messageArguments), null); + } + + private void logL10N(int level, String key, String arg1, String arg2, String arg3) { + Object[] messageArguments = { arg1, arg2, arg3 }; + logEvent(level, applyPattern(key, messageArguments), null); + } + + /** Helper function that applies the messageArguments to a message from the resource object */ + private String applyPattern(String key, Object[] messageArguments) { + String message = getString(key); + MessageFormat formatter = new MessageFormat(message); + String output = formatter.format(message, messageArguments); + return output; + } + + // The method that actually does the logging */ + private synchronized void logEvent(int level, String string, Throwable throwable) { + // Check if the event should be logged + if (level > _loggingLevel) return; + + if (_logFile != null) { + // No logfile specified, log using servlet context + PrintWriter pw = null; + try { + pw = new PrintWriter(new FileWriter(_logFile, true)); + pw.println(_servletName + "(" + level + "): " + string); + if (throwable != null) { + throwable.printStackTrace(pw); + } + pw.close(); + // Do a return here. An exception will cause a fall through to + // do _servletContex logging API + return; + } catch (IOException ioe) { + /* just ignore */ + } + } + + // Otherwise, write to servlet context log + if (throwable == null) { + _servletContext.log(string); + } else { + _servletContext.log(string, throwable); + } + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/ResourceCatalog.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/ResourceCatalog.java new file mode 100755 index 0000000000000000000000000000000000000000..978107a518859c4d463738a1d88aec6b963dc31f --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/ResourceCatalog.java @@ -0,0 +1,514 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.io.File; +import java.io.BufferedInputStream; +import javax.servlet.ServletContext; +import javax.xml.parsers.*; +import org.xml.sax.*; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import org.w3c.dom.*; +import jnlp.sample.util.VersionString; +import jnlp.sample.util.VersionID; + +public class ResourceCatalog { + final private Logger log = Red5LoggerFactory.getLogger(ResourceCatalog.class, "screenshare"); + + public static final String VERSION_XML_FILENAME = "version.xml"; + + private ServletContext _servletContext = null; + + private HashMap _entries; + + /** Class to contain the information we know + * about a specific directory + */ + static private class PathEntries { + /* Version-based entries at this particular path */ + private List _versionXmlList; + private List _directoryList; + private List _platformList; + /* Last time this entry was updated */ + private long _lastModified; // Last modified time of entry; + + public PathEntries(List versionXmlList, List directoryList, List platformList, long lastModified) { + _versionXmlList = versionXmlList; + _directoryList = directoryList; + _platformList = platformList; + _lastModified = lastModified; + } + + + public void setDirectoryList(List dirList) { + _directoryList = dirList; + } + + public List getVersionXmlList() { return _versionXmlList; } + public List getDirectoryList() { return _directoryList; } + public List getPlatformList() { return _platformList; } + + public long getLastModified() { return _lastModified; } + } + + public ResourceCatalog(ServletContext servletContext) { + _entries = new HashMap(); + _servletContext = servletContext; + } + + + public JnlpResource lookupResource(DownloadRequest dreq) throws ErrorResponseException { + // Split request up into path and name + String path = dreq.getPath(); + String name = null; + String dir = null; + int idx = path.lastIndexOf('/'); + if (idx == -1) { + name = path; + } else { + name = path.substring(idx + 1); // Exclude '/' + dir = path.substring(0, idx + 1); // Include '/' + } + + // Lookup up already parsed entries, and san directory for entries if neccesary + PathEntries pentries = (PathEntries)_entries.get(dir); + JnlpResource xmlVersionResPath = new JnlpResource(_servletContext, dir + VERSION_XML_FILENAME); + if (pentries == null || (xmlVersionResPath.exists() && xmlVersionResPath.getLastModified() > pentries.getLastModified())) { + log.info("servlet.log.scandir", dir); + List dirList = scanDirectory(dir, dreq); + // Scan XML file + List versionList = new ArrayList(); + List platformList = new ArrayList(); + parseVersionXML(versionList, platformList, dir, xmlVersionResPath); + pentries = new PathEntries(versionList, dirList, platformList, xmlVersionResPath.getLastModified()); + _entries.put(dir, pentries); + } + + // Search for a match + JnlpResource[] result = new JnlpResource[1]; + + if (dreq.isPlatformRequest()) { + int sts = findMatch(pentries.getPlatformList(), name, dreq, result); + if (sts != DownloadResponse.STS_00_OK) { + throw new ErrorResponseException(DownloadResponse.getJnlpErrorResponse(sts)); + } + } else { + // First lookup in versions.xml file + int sts1 = findMatch(pentries.getVersionXmlList(), name, dreq, result); + if (sts1 != DownloadResponse.STS_00_OK) { + // Then lookup in directory + int sts2 = findMatch(pentries.getDirectoryList(), name, dreq, result); + if (sts2 != DownloadResponse.STS_00_OK) { + + // fix for 4450104 + // try rescan and see if it helps + pentries.setDirectoryList(scanDirectory(dir, dreq)); + sts2 = findMatch(pentries.getDirectoryList(), name, dreq, result); + // try again after rescanning directory + if (sts2 != DownloadResponse.STS_00_OK) { + // Throw the most specific error code + throw new ErrorResponseException(DownloadResponse.getJnlpErrorResponse(Math.max(sts1, sts2))); + } + } + } + } + return result[0]; + } + + /** This method finds the best match, or return the best error code. The + * result parameter must be an array with room for one element. + * + * If a match is found, the method returns DownloadResponse.STS_00_OK + * If one or more entries matches on: name, version-id, os, arch, and locale, + * then the one with the highest version-id is set in the result[0] field. + * + * If a match is not found, it returns an error code, either: ERR_10_NO_RESOURCE, + * ERR_11_NO_VERSION, ERR_20_UNSUP_OS, ERR_21_UNSUP_ARCH, ERR_22_UNSUP_LOCALE, + * ERR_23_UNSUP_JRE. + * + */ + public int findMatch(List list, String name, DownloadRequest dreq, JnlpResource[] result) { + if (list == null) return DownloadResponse.ERR_10_NO_RESOURCE; + // Setup return values + VersionID bestVersionId = null; + int error = DownloadResponse.ERR_10_NO_RESOURCE; + VersionString vs = new VersionString(dreq.getVersion()); + // Iterate through entries + for(int i = 0; i < list.size(); i++) { + JnlpResource respath = (JnlpResource)list.get(i); + VersionID vid = new VersionID(respath.getVersionId()); + int sts = matchEntry(name, vs, dreq, respath, vid); + if (sts == DownloadResponse.STS_00_OK) { + if (result[0] == null || vid.isGreaterThan(bestVersionId)) { + result[0] = respath; + bestVersionId = vid; + } + } else { + error = Math.max(error, sts); + } + } + return (result[0] != null) ? DownloadResponse.STS_00_OK : error; + } + + public int matchEntry(String name, VersionString vs, DownloadRequest dreq, JnlpResource jnlpres, VersionID vid) { + if (!name.equals(jnlpres.getName())) { + return DownloadResponse.ERR_10_NO_RESOURCE; + } + if (!vs.contains(vid)) { + return DownloadResponse.ERR_11_NO_VERSION; + } + if (!prefixMatchLists(jnlpres.getOSList(), dreq.getOS())) { + return DownloadResponse.ERR_20_UNSUP_OS; + } + if (!prefixMatchLists(jnlpres.getArchList(), dreq.getArch())) { + return DownloadResponse.ERR_21_UNSUP_ARCH; + } + if (!prefixMatchLists(jnlpres.getLocaleList(), dreq.getLocale())) { + return DownloadResponse.ERR_22_UNSUP_LOCALE; + } + return DownloadResponse.STS_00_OK; + } + + + private static boolean prefixMatchStringList(String[] prefixList, String target) { + // No prefixes matches everything + if (prefixList == null) return true; + // No target, but a prefix list does not match anything + if (target == null) return false; + for(int i = 0; i < prefixList.length; i++) { + if (target.startsWith(prefixList[i])) return true; + } + return false; + } + + /* Return true if at least one of the strings in 'prefixes' are a prefix + * to at least one of the 'keys'. + */ + public boolean prefixMatchLists(String[] prefixes, String[] keys) { + // The prefixes are part of the server resources. If none is given, + // everything matches + if (prefixes == null) return true; + // If no os keyes was given, and the server resource is keyed of this, + // then return false. + if (keys == null) return false; + // Check for a match on a key + for(int i = 0; i < keys.length; i++) { + if (prefixMatchStringList(prefixes, keys[i])) return true; + } + return false; + } + + /** This method scans the directory pointed to by the + * given path and creates a list of ResourcePath elements + * that contains information about all the entries + * + * The version-based information is encoded in the file name + * given the following format: + * + * entry ::= <name> __ ( <options> ). <ext> + * options ::= <option> ( __ <options> )? + * option ::= V<version-id> + * | O<os> + * | A<arch> + * | L<locale> + * + */ + + + private String jnlpGetPath(DownloadRequest dreq) { + // fix for 4474021 + // try to manuually generate the filename + // extract file name + String path = dreq.getPath(); + String filename = path.substring(path.lastIndexOf("/") + 1); + path = path.substring(0, path.lastIndexOf("/") + 1); + String name = filename; + String ext = null; + + if (filename.lastIndexOf(".") != -1) { + ext = filename.substring(filename.lastIndexOf(".") + 1); + + filename = filename.substring(0, filename.lastIndexOf(".")); + + } + if (dreq.getVersion() != null) { + filename += "__V" + dreq.getVersion(); + } + + String[] temp = dreq.getOS(); + + if (temp != null) { + for (int i=0; i<temp.length; i++) { + filename += "__O" + temp[i]; + } + } + + temp = dreq.getArch(); + + if (temp != null) { + for (int i=0; i<temp.length; i++) { + filename += "__A" + temp[i]; + } + } + temp = dreq.getLocale(); + + if (temp != null) { + for (int i=0; i<temp.length; i++) { + filename += "__L" + temp[i]; + } + } + + if (ext != null) { + filename += "." + ext; + } + + path += filename; + + return path; + } + + public List scanDirectory(String dirPath, DownloadRequest dreq) { + ArrayList list = new ArrayList(); + + // fix for 4474021 + if (_servletContext.getRealPath(dirPath) == null) { + String path = jnlpGetPath(dreq); + + String name = dreq.getPath().substring(path.lastIndexOf("/") + 1); + + JnlpResource jnlpres = new JnlpResource(_servletContext, name, dreq.getVersion(), dreq.getOS(), dreq.getArch(), dreq.getLocale(), path, dreq.getVersion()); + + // the file does not exist + if (jnlpres.getResource() == null) return null; + + list.add(jnlpres); + return list; + } + File dir = new File(_servletContext.getRealPath(dirPath)); + log.debug("File directory: " + dir); + if (dir.exists() && dir.isDirectory()) { + File[] entries = dir.listFiles(); + for(int i = 0; i < entries.length; i++) { + JnlpResource jnlpres = parseFileEntry(dirPath, entries[i].getName()); + if (jnlpres != null) { + if (log.isDebugEnabled()) { + log.debug("Read file resource: " + jnlpres); + } + list.add(jnlpres); + } + } + } + return list; + } + + private JnlpResource parseFileEntry(String dir, String filename) { + int idx = filename .indexOf("__"); + if (idx == -1) return null; + + // Cut out name + String name = filename.substring(0, idx); + String rest = filename.substring(idx); + + // Cut out extension + idx = rest.lastIndexOf('.'); + String extension = ""; + if (idx != -1 ) { + extension = rest.substring(idx); + rest = rest .substring(0, idx); + } + + // Parse options + String versionId = null; + ArrayList osList = new ArrayList(); + ArrayList archList = new ArrayList(); + ArrayList localeList = new ArrayList(); + while(rest.length() > 0) { + /* Must start with __ at this point */ + if (!rest.startsWith("__")) return null; + rest = rest.substring(2); + // Get option and argument + char option = rest.charAt(0); + idx = rest.indexOf("__"); + String arg = null; + if (idx == -1) { + arg = rest.substring(1); + rest = ""; + } else { + arg = rest.substring(1, idx); + rest = rest.substring(idx); + } + switch(option) { + case 'V': versionId = arg; break; + case 'O': osList.add(arg); break; + case 'A': archList.add(arg); break; + case 'L': localeList.add(arg); break; + default: return null; // error + } + } + + return new JnlpResource(_servletContext, + name + extension, /* Resource name in URL request */ + versionId, + listToStrings(osList), + listToStrings(archList), + listToStrings(localeList), + dir + filename, /* Resource name in WAR file */ + versionId); + } + + private String[] listToStrings(List list) { + if (list.size() == 0) return null; + return (String[])list.toArray(new String[list.size()]); + } + + // Returns false if parsing failed + private void parseVersionXML(final List versionList, final List platformList, + final String dir, final JnlpResource versionRes) { + if (!versionRes.exists()) return; + + // Parse XML into a more understandable format + XMLNode root = null; + try { + DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); + Document doc = docBuilder.parse(new BufferedInputStream(versionRes.getResource().openStream())); + doc.getDocumentElement().normalize(); + + // Convert document into an XMLNode structure, since we already got utility methods + // to handle these. We should really use the data-binding stuff here - but that will come + // later + // + root = XMLParsing.convert(doc.getDocumentElement()); + } catch (SAXParseException err) { + log.warn("servlet.log.warning.xml.parsing", + versionRes.getPath(), + Integer.toString(err.getLineNumber()), + err.getMessage()); + return; + } catch (Throwable t) { + log.warn("servlet.log.warning.xml.reading", versionRes.getPath(), t); + return; + } + + // Check that root element is a <jnlp> tag + if (!root.getName().equals("jnlp-versions")) { + log.warn("servlet.log.warning.xml.missing-jnlp", versionRes.getPath()); + return; + } + + // Visit all <resource> elements + XMLParsing.visitElements(root, "<resource>", new XMLParsing.ElementVisitor() { + public void visitElement(XMLNode node) { + XMLNode pattern = XMLParsing.findElementPath(node, "<pattern>"); + if (pattern == null) { + log.warn("servlet.log.warning.xml.missing-pattern", versionRes.getPath()); + } else { + // Parse pattern + String name = XMLParsing.getElementContent(pattern , "<name>", ""); + String versionId = XMLParsing.getElementContent(pattern , "<version-id>"); + String[] os = XMLParsing.getMultiElementContent(pattern, "<os>"); + String[] arch = XMLParsing.getMultiElementContent(pattern, "<arch>"); + String[] locale = XMLParsing.getMultiElementContent(pattern, "<locale>"); + // Get return request + String file = XMLParsing.getElementContent(node, "<file>"); + if (versionId == null || file == null) { + log.warn("servlet.log.warning.xml.missing-elems", versionRes.getPath()); + } else { + JnlpResource res = new JnlpResource(_servletContext, + name, + versionId, + os, + arch, + locale, + dir + file, + versionId); + if (res.exists()) { + versionList.add(res); + if (log.isDebugEnabled()) { + log.debug("Read resource: " + res); + } + } else { + log.warn("servlet.log.warning.missing-file", file, versionRes.getPath()); + } + } + } + } + }); + + // Visit all <resource> elements + XMLParsing.visitElements(root, "<platform>", new XMLParsing.ElementVisitor() { + public void visitElement(XMLNode node) { + XMLNode pattern = XMLParsing.findElementPath(node, "<pattern>"); + if (pattern == null) { + log.warn("servlet.log.warning.xml.missing-pattern", versionRes.getPath()); + } else { + // Parse pattern + String name = XMLParsing.getElementContent(pattern , "<name>", ""); + String versionId = XMLParsing.getElementContent(pattern , "<version-id>"); + String[] os = XMLParsing.getMultiElementContent(pattern, "<os>"); + String[] arch = XMLParsing.getMultiElementContent(pattern, "<arch>"); + String[] locale = XMLParsing.getMultiElementContent(pattern, "<locale>"); + // Get return request + String file = XMLParsing.getElementContent(node, "<file>"); + String productId = XMLParsing.getElementContent(node, "<product-version-id>"); + + if (versionId == null || file == null || productId == null) { + log.warn("servlet.log.warning.xml.missing-elems2", versionRes.getPath()); + } else { + JnlpResource res = new JnlpResource(_servletContext, + name, + versionId, + os, + arch, + locale, + dir + file, + productId); + if (res.exists()) { + platformList.add(res); + if (log.isDebugEnabled()) { + log.debug("Read platform resource: " + res); + } + } else { + log.warn("servlet.log.warning.missing-file", file, versionRes.getPath()); + } + } + } + } + }); + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLAttribute.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLAttribute.java new file mode 100755 index 0000000000000000000000000000000000000000..76a1027620bfd8caf134d0f8a0e5cb3df8aabc21 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLAttribute.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; + +/** Class that contains information about a specific attribute + */ +public class XMLAttribute { + private String _name; + private String _value; + private XMLAttribute _next; + + public XMLAttribute(String name, String value) { + _name = name; + _value = value; + _next = null; + } + + public XMLAttribute(String name, String value, XMLAttribute next) { + _name = name; + _value = value; + _next = next; + } + + public String getName() { return _name; } + public String getValue() { return _value; } + public XMLAttribute getNext() { return _next; } + public void setNext(XMLAttribute next) { _next = next; } + + public boolean equals(Object o) { + if (o == null || !(o instanceof XMLAttribute)) return false; + XMLAttribute other = (XMLAttribute)o; + return + match(_name, other._name) && + match(_value, other._value) && + match(_next, other._next); + } + + private static boolean match(Object o1, Object o2) { + if (o1 == null) return (o2 == null); + return o1.equals(o2); + } + + public String toString() { + if (_next != null) { + return _name + "=\"" + _value + "\" " + _next.toString(); + } else { + return _name + "=\"" + _value + "\""; + } + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLNode.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLNode.java new file mode 100755 index 0000000000000000000000000000000000000000..ee5ca60051d47204b59af1641491b9e7a44ffcf9 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLNode.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** Class that contains information about an XML Node + */ +public class XMLNode { + private boolean _isElement; // Element/PCTEXT + private String _name; + private XMLAttribute _attr; + private XMLNode _parent; // Parent Node + private XMLNode _nested; // Nested XML tags + private XMLNode _next; // Following XML tag on the same level + + /** Creates a PCTEXT node */ + public XMLNode(String name) { + this(name, null, null, null); + _isElement = false; + } + + /** Creates a ELEMENT node */ + public XMLNode(String name, XMLAttribute attr) { + this(name, attr, null, null); + } + + /** Creates a ELEMENT node */ + public XMLNode(String name, XMLAttribute attr, XMLNode nested, XMLNode next) { + _isElement = true; + _name = name; + _attr = attr; + _nested = nested; + _next = next; + _parent = null; + } + + public String getName() { return _name; } + public XMLAttribute getAttributes() { return _attr; } + public XMLNode getNested() { return _nested; } + public XMLNode getNext() { return _next; } + public boolean isElement() { return _isElement; } + + public void setParent(XMLNode parent) { _parent = parent; } + public XMLNode getParent() { return _parent; } + + public void setNext(XMLNode next) { _next = next; } + public void setNested(XMLNode nested) { _nested = nested; } + + public boolean equals(Object o) { + if (o == null || !(o instanceof XMLNode)) return false; + XMLNode other = (XMLNode)o; + boolean result = + match(_name, other._name) && + match(_attr, other._attr) && + match(_nested, other._nested) && + match(_next, other._next); + return result; + } + + public String getAttribute(String name) { + XMLAttribute cur = _attr; + while(cur != null) { + if (name.equals(cur.getName())) return cur.getValue(); + cur = cur.getNext(); + } + return ""; + } + + private static boolean match(Object o1, Object o2) { + if (o1 == null) return (o2 == null); + return o1.equals(o2); + } + + public void printToStream(PrintWriter out) { + printToStream(out, 0); + } + + public void printToStream(PrintWriter out, int n) { + if (!isElement()) { + out.print(_name); + } else { + if (_nested == null) { + String attrString = (_attr == null) ? "" : (" " + _attr.toString()); + lineln(out, n, "<" + _name + attrString + "/>"); + } else { + String attrString = (_attr == null) ? "" : (" " + _attr.toString()); + lineln(out, n, "<" + _name + attrString + ">"); + _nested.printToStream(out, n + 1); + if (_nested.isElement()) { + lineln(out, n, "</" + _name + ">"); + } else { + out.print("</" + _name + ">"); + } + } + } + if (_next != null) { + _next.printToStream(out, n); + } + } + + private static void lineln(PrintWriter out, int indent, String s) { + out.println(""); + for(int i = 0; i < indent; i++) { + out.print(" "); + } + out.print(s); + } + + public String toString() { + StringWriter sw = new StringWriter(1000); + PrintWriter pw = new PrintWriter(sw); + printToStream(pw); + pw.close(); + return sw.toString(); + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLParsing.java b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLParsing.java new file mode 100755 index 0000000000000000000000000000000000000000..94571efda15f3877e97a434863e282a112d2e6d8 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/XMLParsing.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.servlet; + +import javax.xml.parsers.*; +import java.util.ArrayList; +import java.util.List; +import org.xml.sax.*; +import org.w3c.dom.*; + +/** Contains handy methods for looking up information + * stored in XMLNodes. + */ +public class XMLParsing { + + public static XMLNode convert(Node n) { + if (n == null) { + return null; + } else if (n instanceof Text) { + Text tn = (Text)n; + return new XMLNode(tn.getNodeValue()); + } else if (n instanceof Element) { + Element en = (Element)n; + + XMLAttribute xmlatts = null; + NamedNodeMap attributes = en.getAttributes(); + for(int i = attributes.getLength() - 1; i >= 0; i--) { + Attr ar = (Attr)attributes.item(i); + xmlatts = new XMLAttribute(ar.getName(), ar.getValue(), xmlatts); + } + + // Convert childern + XMLNode thisNode = new XMLNode(en.getNodeName(), xmlatts, null, null);; + XMLNode last = null; + Node nn = en.getFirstChild(); + while(nn != null) { + if (thisNode.getNested() == null) { + last = convert(nn); + thisNode.setNested(last); + } else { + XMLNode nnode = convert(nn); + last.setNext(nnode); + last = nnode; + } + last.setParent(thisNode); + nn = nn.getNextSibling(); + } + + return thisNode; + } + return null; + } + + /** Returns true if the path exists in the document, otherwise false */ + static public boolean isElementPath(XMLNode root, String path) { + return findElementPath(root, path) != null; + } + + + /** Returns a string describing the current location in the DOM */ + static public String getPathString(XMLNode e) { + return (e == null || !(e.isElement())) ? "" : getPathString(e.getParent()) + "<" + e.getName() + ">"; + } + + + /** Like getElementContents(...) but with a defaultValue of null */ + static public String getElementContent(XMLNode root, String path) { + return getElementContent(root, path, null); + } + + /** Like getElementContents(...) but with a defaultValue of null */ + static public String[] getMultiElementContent(XMLNode root, String path) { + final List list = new ArrayList(); + visitElements(root, path, new ElementVisitor() { + public void visitElement(XMLNode n) { + String value = getElementContent(n, ""); + if (value != null) list.add(value); + } + }); + if (list.size() == 0) return null; + return (String[])list.toArray(new String[list.size()]); + } + + /** Returns the value of the last element tag in the path, e.g., <..><tag>value</tag>. The DOM is assumes + * to be normalized. If no value is found, the defaultvalue is returned + */ + static public String getElementContent(XMLNode root, String path, String defaultvalue) { + XMLNode e = findElementPath(root, path); + if (e == null) return defaultvalue; + XMLNode n = e.getNested(); + if (n != null && !n.isElement()) return n.getName(); + return defaultvalue; + } + + /** Parses a path string of the form <tag1><tag2><tag3> and returns the specific Element + * node for that tag, or null if it does not exist. If multiple elements exists with same + * path the first is returned + */ + static public XMLNode findElementPath(XMLNode elem, String path) { + // End condition. Root null -> path does not exist + if (elem == null) return null; + // End condition. String empty, return current root + if (path == null || path.length() == 0) return elem; + + // Strip of first tag + int idx = path.indexOf('>'); + String head = path.substring(1, idx); + String tail = path.substring(idx + 1); + return findElementPath(findChildElement(elem, head), tail); + } + + /** Returns an child element with the current tag name or null. */ + static public XMLNode findChildElement(XMLNode elem, String tag) { + XMLNode n = elem.getNested(); + while(n != null) { + if (n.isElement() && n.getName().equals(tag)) return n; + n = n.getNext(); + } + return null; + } + + /** Iterator class */ + public abstract static class ElementVisitor { + abstract public void visitElement(XMLNode e); + } + + /** Visits all elements which matches the <path>. The iteration is only + * done on the last elment in the path. + */ + static public void visitElements(XMLNode root, String path, ElementVisitor ev) { + // Get last element in path + int idx = path.lastIndexOf('<'); + String head = path.substring(0, idx); + String tag = path.substring(idx + 1, path.length() - 1); + + XMLNode elem = findElementPath(root, head); + if (elem == null) return; + + // Iterate through all child nodes + XMLNode n = elem.getNested(); + while(n != null) { + if (n.isElement() && n.getName().equals(tag)) { + ev.visitElement(n); + } + n = n.getNext(); + } + } + + static public void visitChildrenElements(XMLNode elem, ElementVisitor ev) { + // Iterate through all child nodes + XMLNode n = elem.getNested(); + while(n != null) { + if (n.isElement()) ev.visitElement(n); + n = n.getNext(); + } + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/resources/strings.properties b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/resources/strings.properties new file mode 100755 index 0000000000000000000000000000000000000000..7b4f30d8086f29effa41e969aafebe7aec0212ed --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/servlet/resources/strings.properties @@ -0,0 +1,67 @@ +# +# Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# -Redistribution of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# -Redistribution in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# Neither the name of Oracle nor the names of contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# This software is provided "AS IS," without a warranty of any kind. ALL +# EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING +# ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE +# OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") +# AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE +# AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS +# DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST +# REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, +# INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY +# OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, +# EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +# +# You acknowledge that this software is not designed, licensed or intended +# for use in the design, construction, operation or maintenance of any +# nuclear facility. +# + +# Fatals +servlet.log.fatal.internalerror=Internal error: + +# Warnings +servlet.log.warning.nolastmodified=Last-modified read as 0 for {0} +servlet.log.warning.notimestamp=Timestamp read as 0 for {0}. Using Last-modified instead +servlet.log.warning.missing-file=Reference to non-existing file ({0}) in {1} +servlet.log.warning.xml.parsing=Error parsing {0} at line {1}: {2} +servlet.log.warning.xml.reading=Unexpected error reading {0}: +servlet.log.warning.xml.missing-jnlp=Missing <jnlp-versions> element in {0} +servlet.log.warning.xml.missing-pattern=Missing <pattern> element in {0} +servlet.log.warning.xml.missing-elems=Missing <version-id> or <file> attribute in {0} +servlet.log.warning.xml.missing-elems2=Missing <version-id>, <file>, or <product-version-id> attribute in {0} +servlet.log.warning.jardiff.failed=Failed to generate JarDiff for {0} {1}->{2} + +# Informational +servlet.log.info.request=Request: {0} +servlet.log.info.useragent=User-Agent: {0} +servlet.log.info.goodrequest=Resource returned: {0} +servlet.log.info.badrequest=Error code returned for request: {0} +servlet.log.scandir=Rescanning directory: {0} +servlet.log.info.jardiff.response=JarDiff returned for request +servlet.log.info.jardiff.gen=Generating JarDiff for {0} {1}->{2} + +# JNLP Error strings +servlet.jnlp.err.10 = Could not locate resource +servlet.jnlp.err.11 = Could not locate requested version +servlet.jnlp.err.20 = Unsupported operating system +servlet.jnlp.err.21 = Unsupported architecture +servlet.jnlp.err.22 = Unsupported locale +servlet.jnlp.err.23 = Unsupported JRE version +servlet.jnlp.err.99 = Unknown error + diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/util/VersionID.java b/bbb-screenshare/app/src/main/java/jnlp/sample/util/VersionID.java new file mode 100755 index 0000000000000000000000000000000000000000..e52c6778b3eab52c3b226cfe6aa9feb8e80849e8 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/util/VersionID.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.util; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * VersionID contains a JNLP version ID. + * + * The VersionID also contains a prefix indicator that can + * be used when stored with a VersionString + * + */ +public class VersionID implements Comparable { + private String[] _tuple; // Array of Integer or String objects + private boolean _usePrefixMatch; // star (*) prefix + private boolean _useGreaterThan; // plus (+) greather-than + private boolean _isCompound; // and (&) operator + private VersionID _rest; // remaining part after the & + + /** Creates a VersionID object */ + public VersionID(String str) { + _usePrefixMatch = false; + _useGreaterThan = false; + _isCompound = false; + if (str == null && str.length() == 0) { + _tuple = new String[0]; + return; + } + + // Check for compound + int amp = str.indexOf("&"); + if (amp >= 0) { + _isCompound = true; + VersionID firstPart = new VersionID(str.substring(0, amp)); + _rest = new VersionID(str.substring(amp+1)); + _tuple = firstPart._tuple; + _usePrefixMatch = firstPart._usePrefixMatch; + _useGreaterThan = firstPart._useGreaterThan; + } else { + // Check for postfix + if (str.endsWith("+")) { + _useGreaterThan = true; + str = str.substring(0, str.length() - 1); + } else if (str.endsWith("*")) { + _usePrefixMatch = true; + str = str.substring(0, str.length() - 1); + } + + ArrayList list = new ArrayList(); + int start = 0; + for(int i = 0; i < str.length(); i++) { + // Split at each separator character + if (".-_".indexOf(str.charAt(i)) != -1) { + if (start < i) { + String value = str.substring(start, i); + list.add(value); + } + start = i + 1; + } + } + if (start < str.length()) { + list.add(str.substring(start, str.length())); + } + _tuple = new String[list.size()]; + _tuple = (String[])list.toArray(_tuple); + } + } + + /** Returns true if no flags are set */ + public boolean isSimpleVersion() { + return !_useGreaterThan && !_usePrefixMatch && !_isCompound; + } + + /** Match 'this' versionID against vid. + * The _usePrefixMatch/_useGreaterThan flag is used to determine if a + * prefix match of an exact match should be performed + * if _isCompound, must match _rest also. + */ + public boolean match(VersionID vid) { + if (_isCompound) { + if (!_rest.match(vid)) { + return false; + } + } + return (_usePrefixMatch) ? this.isPrefixMatch(vid) : + (_useGreaterThan) ? vid.isGreaterThanOrEqual(this) : + matchTuple(vid); + } + + /** Compares if two version IDs are equal */ + public boolean equals(Object o) { + if (matchTuple(o)) { + VersionID ov = (VersionID) o; + if (_rest == null || _rest.equals(ov._rest)) { + if ((_useGreaterThan == ov._useGreaterThan) && + (_usePrefixMatch == ov._usePrefixMatch)) { + return true; + } + } + } + return false; + } + + /** Compares if two version IDs are equal */ + private boolean matchTuple(Object o) { + // Check for null and type + if (o == null || !(o instanceof VersionID)) return false; + VersionID vid = (VersionID)o; + + // Normalize arrays + String[] t1 = normalize(_tuple, vid._tuple.length); + String[] t2 = normalize(vid._tuple, _tuple.length); + + // Check contents + for(int i = 0; i < t1.length; i++) { + Object o1 = getValueAsObject(t1[i]); + Object o2 = getValueAsObject(t2[i]); + if (!o1.equals(o2)) return false; + } + return true; + } + + private Object getValueAsObject(String value) { + if (value.length() > 0 && value.charAt(0) != '-') { + try { return Integer.valueOf(value); + } catch(NumberFormatException nfe) { /* fall through */ } + } + return value; + } + + public boolean isGreaterThan(VersionID vid) { + return isGreaterThanOrEqualHelper(vid, false); + } + + public boolean isGreaterThanOrEqual(VersionID vid) { + return isGreaterThanOrEqualHelper(vid, true); + } + + /** Compares if 'this' is greater than vid */ + private boolean isGreaterThanOrEqualHelper(VersionID vid, + boolean allowEqual) { + + if (_isCompound) { + if (!_rest.isGreaterThanOrEqualHelper(vid, allowEqual)) { + return false; + } + } + // Normalize the two strings + String[] t1 = normalize(_tuple, vid._tuple.length); + String[] t2 = normalize(vid._tuple, _tuple.length); + + for(int i = 0; i < t1.length; i++) { + // Compare current element + Object e1 = getValueAsObject(t1[i]); + Object e2 = getValueAsObject(t2[i]); + if (e1.equals(e2)) { + // So far so good + } else { + if (e1 instanceof Integer && e2 instanceof Integer) { + return ((Integer)e1).intValue() > ((Integer)e2).intValue(); + } else { + String s1 = t1[i].toString(); + String s2 = t2[i].toString(); + return s1.compareTo(s2) > 0; + } + + } + } + // If we get here, they are equal + return allowEqual; + } + + /** Checks if 'this' is a prefix of vid */ + public boolean isPrefixMatch(VersionID vid) { + + if (_isCompound) { + if (!_rest.isPrefixMatch(vid)) { + return false; + } + } + // Make sure that vid is at least as long as the prefix + String[] t2 = normalize(vid._tuple, _tuple.length); + + for(int i = 0; i < _tuple.length; i++) { + Object e1 = _tuple[i]; + Object e2 = t2[i]; + if (e1.equals(e2)) { + // So far so good + } else { + // Not a prefix + return false; + } + } + return true; + } + + /** Normalize an array to a certain lengh */ + private String[] normalize(String[] list, int minlength) { + if (list.length < minlength) { + // Need to do padding + String[] newlist = new String[minlength]; + System.arraycopy(list, 0, newlist, 0, list.length); + Arrays.fill(newlist, list.length, newlist.length, "0"); + return newlist; + } else { + return list; + } + } + + public int compareTo(Object o) { + if (o == null || !(o instanceof VersionID)) return -1; + VersionID vid = (VersionID)o; + return equals(vid) ? 0 : (isGreaterThanOrEqual(vid) ? 1 : -1); + } + /** Show it as a string */ + public String toString() { + StringBuffer sb = new StringBuffer(); + for(int i = 0; i < _tuple.length -1; i++) { + sb.append(_tuple[i]); + sb.append('.'); + } + if (_tuple.length > 0 ) sb.append(_tuple[_tuple.length - 1]); + if (_usePrefixMatch) sb.append('+'); + return sb.toString(); + } +} diff --git a/bbb-screenshare/app/src/main/java/jnlp/sample/util/VersionString.java b/bbb-screenshare/app/src/main/java/jnlp/sample/util/VersionString.java new file mode 100755 index 0000000000000000000000000000000000000000..296093c4376a1acfadb812a4913b9c33231665fb --- /dev/null +++ b/bbb-screenshare/app/src/main/java/jnlp/sample/util/VersionString.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2006, 2010, Oracle and/or its affiliates. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * -Redistribution of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * -Redistribution in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of Oracle nor the names of contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any kind. ALL + * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING + * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") + * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE + * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST + * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, + * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY + * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, + * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed, licensed or intended + * for use in the design, construction, operation or maintenance of any + * nuclear facility. + */ + +package jnlp.sample.util; +import java.util.ArrayList; +import java.util.StringTokenizer; + +/* + * Utility class that knows to handle version strings + * A version string is of the form: + * + * (version-id ('+'?) ' ') * + * + */ +public class VersionString { + private ArrayList _versionIds; + + /** Constructs a VersionString object from string */ + public VersionString(String vs) { + _versionIds = new ArrayList(); + if (vs != null) { + StringTokenizer st = new StringTokenizer(vs, " ", false); + while(st.hasMoreElements()) { + // Note: The VersionID class takes care of a postfixed '+' + _versionIds.add(new VersionID(st.nextToken())); + } + } + } + + /** Check if this VersionString object contains the VersionID m */ + public boolean contains(VersionID m) { + for(int i = 0; i < _versionIds.size(); i++) { + VersionID vi = (VersionID)_versionIds.get(i); + boolean check = vi.match(m); + if (check) return true; + } + return false; + } + + /** Check if this VersionString object contains the VersionID m, given as a string */ + public boolean contains(String versionid) { + return contains(new VersionID(versionid)); + } + + /** Check if this VersionString object contains anything greater than m */ + public boolean containsGreaterThan(VersionID m) { + for(int i = 0; i < _versionIds.size(); i++) { + VersionID vi = (VersionID)_versionIds.get(i); + boolean check = vi.isGreaterThan(m); + if (check) return true; + } + return false; + } + + /** Check if this VersionString object contains anything greater than the VersionID m, given as a string */ + public boolean containsGreaterThan(String versionid) { + return containsGreaterThan(new VersionID(versionid)); + } + + /** Check if the versionString 'vs' contains the VersionID 'vi' */ + static public boolean contains(String vs, String vi) { + return (new VersionString(vs)).contains(vi); + } + + /** Pretty-print object */ + public String toString() { + StringBuffer sb = new StringBuffer(); + for(int i = 0; i < _versionIds.size(); i++) { + sb.append(_versionIds.get(i).toString()); + sb.append(' '); + } + return sb.toString(); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/Error.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/Error.java new file mode 100755 index 0000000000000000000000000000000000000000..31ac1b587ec4ce1f200efd45af98d7e9ea3daa5e --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/Error.java @@ -0,0 +1,10 @@ +package org.bigbluebutton.app.screenshare; + +public class Error { + + public final String reason; + + public Error(String reason) { + this.reason = reason; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/EventRecordingService.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/EventRecordingService.java new file mode 100755 index 0000000000000000000000000000000000000000..d534387e2e99c3a2a0c0b38bdb7511a69bf0a0d3 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/EventRecordingService.java @@ -0,0 +1,43 @@ +/** +* 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.app.screenshare; + + +import java.util.Map; + +import redis.clients.jedis.Jedis; + +public class EventRecordingService { + private static final String COLON = ":"; + + private final String host; + private final int port; + + public EventRecordingService(String host, int port) { + this.host = host; + this.port = port; + } + + public void record(String meetingId, Map<String, String> event) { + Jedis jedis = new Jedis(host, port); + Long msgid = jedis.incr("global:nextRecordedMsgId"); + jedis.hmset("recording:" + meetingId + COLON + msgid, event); + jedis.rpush("meeting:" + meetingId + COLON + "recordings", msgid.toString()); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/IScreenShareApplication.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/IScreenShareApplication.java new file mode 100755 index 0000000000000000000000000000000000000000..1ad412e2f860394783c3df9f1c6b4146b15c5511 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/IScreenShareApplication.java @@ -0,0 +1,17 @@ +package org.bigbluebutton.app.screenshare; + +public interface IScreenShareApplication { + + IsScreenSharingResponse isScreenSharing(String meetingId); + ScreenShareInfoResponse getScreenShareInfo(String meetingId, String token); + StartShareRequestResponse startShareRequest(String meetingId, String userId, Boolean record); + void stopShareRequest(String meetingId, String streamId); + void streamStarted(String meetingId, String streamId, String url); + void streamStopped(String meetingId, String streamId); + void sharingStarted(String meetingId, String streamId, Integer width, Integer height); + void sharingStopped(String meetingId, String streamId); + void updateShareStatus(String meetingId, String streamId, Integer seqNum); + Boolean isSharingStopped(String meetingId, String streamId); + Boolean recordStream(String meetingId, String streamId); + void userDisconnected(String meetingId, String userId); +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/IsScreenSharingResponse.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/IsScreenSharingResponse.java new file mode 100755 index 0000000000000000000000000000000000000000..2cab8dea76454735b89183d102ca432746c6d837 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/IsScreenSharingResponse.java @@ -0,0 +1,12 @@ +package org.bigbluebutton.app.screenshare; + +public class IsScreenSharingResponse { + + public final StreamInfo info; + public final Error error; + + public IsScreenSharingResponse(StreamInfo info, Error error) { + this.info = info; + this.error = error; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenShareInfo.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenShareInfo.java new file mode 100755 index 0000000000000000000000000000000000000000..294706eb766b9305861728b13d2afcf935e30d17 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenShareInfo.java @@ -0,0 +1,12 @@ +package org.bigbluebutton.app.screenshare; + +public class ScreenShareInfo { + + public final String streamId; + public final String publishUrl; + + public ScreenShareInfo(String publishUrl, String streamId) { + this.streamId = streamId; + this.publishUrl = publishUrl; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenShareInfoResponse.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenShareInfoResponse.java new file mode 100755 index 0000000000000000000000000000000000000000..5f97d545beb2bf9a1e4d7ca5ef8993f4aa0f2658 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenShareInfoResponse.java @@ -0,0 +1,12 @@ +package org.bigbluebutton.app.screenshare; + +public class ScreenShareInfoResponse { + + public final ScreenShareInfo info; + public final Error error; + + public ScreenShareInfoResponse(ScreenShareInfo info, Error error) { + this.info = info; + this.error = error; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenshareStreamListener.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenshareStreamListener.java new file mode 100755 index 0000000000000000000000000000000000000000..378e6dbfbf111ea130aaa34de530001e320f5663 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/ScreenshareStreamListener.java @@ -0,0 +1,99 @@ +/** +* 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.app.screenshare; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.mina.core.buffer.IoBuffer; +import org.red5.server.api.IConnection; +import org.red5.server.api.Red5; +import org.red5.server.api.stream.IBroadcastStream; +import org.red5.server.api.stream.IStreamListener; +import org.red5.server.api.stream.IStreamPacket; +import org.red5.server.net.rtmp.event.VideoData; + +/** + * Class to listen for the first video packet of the webcam. + * We need to listen for the first packet and send a startWebcamEvent. + * The reason is that when starting the webcam, sometimes Flash Player + * needs to prompt the user for permission to access the webcam. However, + * while waiting for the user to click OK to the prompt, Red5 has already + * called the startBroadcast method which we take as the start of the recording. + * When the user finally clicks OK, the packets then start to flow through. + * This introduces a delay of when we assume the start of the recording and + * the webcam actually publishes video packets. When we do the ingest and + * processing of the video and multiplex the audio, the video and audio will + * be un-synched by at least this amount of delay. + * @author Richard Alam + * + */ +public class ScreenshareStreamListener implements IStreamListener { + private EventRecordingService recordingService; + private volatile boolean firstPacketReceived = false; + private String recordingDir; + + public ScreenshareStreamListener(EventRecordingService s, String recordingDir) { + this.recordingService = s; + this.recordingDir = recordingDir; + } + + private Long genTimestamp() { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); + } + + @Override + public void packetReceived(IBroadcastStream stream, IStreamPacket packet) { + IoBuffer buf = packet.getData(); + if (buf != null) + buf.rewind(); + + if (buf == null || buf.remaining() == 0){ + return; + } + + if (packet instanceof VideoData) { + if (! firstPacketReceived) { + firstPacketReceived = true; + IConnection conn = Red5.getConnectionLocal(); + + String meetingId = conn.getScope().getName(); + + String filename = recordingDir; + if (!filename.endsWith("/")) { + filename.concat("/"); + } + + filename = filename.concat(meetingId).concat("/").concat(stream.getPublishedName()).concat(".flv"); + + Map<String, String> event = new HashMap<String, String>(); + event.put("module", "Deskshare"); + event.put("timestamp", new Long(System.currentTimeMillis()).toString()); + event.put("meetingId", meetingId); + event.put("file", filename); + event.put("stream", stream.getPublishedName()); + event.put("eventName", "DeskshareStartedEvent"); + + recordingService.record(conn.getScope().getName(), event); + } + } + } + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/StartShareRequestResponse.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/StartShareRequestResponse.java new file mode 100755 index 0000000000000000000000000000000000000000..bd1e58e5dc5b3264f30fa70bad84eb51bcbea5c4 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/StartShareRequestResponse.java @@ -0,0 +1,15 @@ +package org.bigbluebutton.app.screenshare; + +public class StartShareRequestResponse { + + public final String token; + public final String jnlp; + public final Error error; + + public StartShareRequestResponse(String token, String jnlp, Error error) { + this.token = token; + this.jnlp = jnlp; + + this.error = error; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/StreamInfo.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/StreamInfo.java new file mode 100755 index 0000000000000000000000000000000000000000..1c9334a96d7ba352024720798505024a364280fc --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/StreamInfo.java @@ -0,0 +1,19 @@ +package org.bigbluebutton.app.screenshare; + +public class StreamInfo { + + public final String streamId; + public final Boolean sharing; + public final int width; + public final int height; + public final String url; + + public StreamInfo(Boolean sharing, String streamId, + int width, int height, String url) { + this.sharing = sharing; + this.streamId = streamId; + this.width = width; + this.height = height; + this.url = url; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/EventMessageBusImp.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/EventMessageBusImp.java new file mode 100755 index 0000000000000000000000000000000000000000..9a53a2b2915e713249ab63ac4d6d866e55584199 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/EventMessageBusImp.java @@ -0,0 +1,67 @@ +package org.bigbluebutton.app.screenshare.events; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.Set; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +public class EventMessageBusImp implements IEventsMessageBus { + private static Logger log = Red5LoggerFactory.getLogger(EventMessageBusImp.class, "screenshare"); + + private BlockingQueue<IEvent> receivedMessages = new LinkedBlockingQueue<IEvent>(); + private volatile boolean processMessage = false; + private final Executor msgProcessorExec = Executors.newSingleThreadExecutor(); + private int maxThreshold = 1024; + private Set<IEventListener> listeners; + + public void send(IEvent msg) { + if (receivedMessages.size() > maxThreshold) { + log.warn("Queued number of events [{}] is greater than threshold [{}]", receivedMessages.size(), maxThreshold); + } + receivedMessages.add(msg); + } + + public void stop() { + processMessage = false; + } + + public void start() { + try { + processMessage = true; + + Runnable messageProcessor = new Runnable() { + public void run() { + while (processMessage) { + try { + IEvent msg = receivedMessages.take(); + processMessage(msg); + } catch (InterruptedException e) { + log.warn("Error while taking received message from queue."); + } + } + } + }; + msgProcessorExec.execute(messageProcessor); + } catch (Exception e) { + log.error("Error processing event: " + e.getMessage()); + } + } + + private void processMessage(final IEvent msg) { + for (IEventListener listener : listeners) { + listener.handleMessage(msg); + } + } + + public void setListeners(Set<IEventListener> listeners) { + this.listeners = listeners; + } + + public void setMaxThreshold(int threshold) { + maxThreshold = threshold; + } + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..b6bc34cbc2e4e18f9daa079a90484810b2dbbfb4 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEvent.java @@ -0,0 +1,5 @@ +package org.bigbluebutton.app.screenshare.events; + +public interface IEvent { + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEventListener.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEventListener.java new file mode 100755 index 0000000000000000000000000000000000000000..5036e43ea7e67c268f4b5927c74ba63e2f5cb4d7 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEventListener.java @@ -0,0 +1,5 @@ +package org.bigbluebutton.app.screenshare.events; + +public interface IEventListener { + void handleMessage(IEvent msg); +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEventsMessageBus.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEventsMessageBus.java new file mode 100755 index 0000000000000000000000000000000000000000..eea2cab91f8146c8ddd742660e9863777477687a --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/IEventsMessageBus.java @@ -0,0 +1,5 @@ +package org.bigbluebutton.app.screenshare.events; + +public interface IEventsMessageBus { + void send(IEvent msg); +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/ShareStartedEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/ShareStartedEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..530e8bfea2afcc8180d44ff3f05a29bb88935147 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/ShareStartedEvent.java @@ -0,0 +1,12 @@ +package org.bigbluebutton.app.screenshare.events; + +public class ShareStartedEvent implements IEvent { + + public final String meetingId; + public final String streamId; + + public ShareStartedEvent(String meetingId, String streamId) { + this.meetingId = meetingId; + this.streamId = streamId; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/ShareStoppedEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/ShareStoppedEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..929ae16d8b30cd6aa433fc36c38e30e633ce3db9 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/ShareStoppedEvent.java @@ -0,0 +1,12 @@ +package org.bigbluebutton.app.screenshare.events; + +public class ShareStoppedEvent implements IEvent { + + public final String meetingId; + public final String streamId; + + public ShareStoppedEvent(String meetingId, String streamId) { + this.meetingId = meetingId; + this.streamId = streamId; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamStartedEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamStartedEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..086178a8f29b806a97d6f243b64cde376f585c8c --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamStartedEvent.java @@ -0,0 +1,19 @@ +package org.bigbluebutton.app.screenshare.events; + +public class StreamStartedEvent implements IEvent { + + public final String meetingId; + public final String streamId; + public final int width; + public final int height; + public final String url; + + public StreamStartedEvent(String meetingId, String streamId, + int width, int height, String url) { + this.meetingId = meetingId; + this.streamId = streamId; + this.width = width; + this.height = height; + this.url = url; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamStoppedEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamStoppedEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..78869cfc0a64e7576cc7b79485659f17ed7e97cb --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamStoppedEvent.java @@ -0,0 +1,12 @@ +package org.bigbluebutton.app.screenshare.events; + +public class StreamStoppedEvent implements IEvent { + + public final String meetingId; + public final String streamId; + + public StreamStoppedEvent(String meetingId, String streamId) { + this.meetingId = meetingId; + this.streamId = streamId; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamUpdateEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamUpdateEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..75373b163c6e33a56a8d455ad0469acf7112224a --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/events/StreamUpdateEvent.java @@ -0,0 +1,14 @@ +package org.bigbluebutton.app.screenshare.events; + +public class StreamUpdateEvent implements IEvent { + + public final String meetingId; + public final String streamId; + public final Long date; + + public StreamUpdateEvent(String meetingId, String streamId, Long date) { + this.meetingId = meetingId; + this.streamId = streamId; + this.date = date; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MeetingMessageHandler.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MeetingMessageHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..e4bf6fd309e761a3ea532cd608f57ebb587432ed --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MeetingMessageHandler.java @@ -0,0 +1,35 @@ +package org.bigbluebutton.app.screenshare.messaging.redis; + +import java.util.HashMap; +import java.util.Map; + +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import com.google.gson.Gson; + + +public class MeetingMessageHandler implements MessageHandler { + private static Logger log = Red5LoggerFactory.getLogger(MeetingMessageHandler.class, "screenshare"); + + + @Override + public void handleMessage(String pattern, String channel, String message) { + + if (channel.equalsIgnoreCase(MessagingConstants.TO_MEETING_CHANNEL)) { + +// IMessage msg = MessageFromJsonConverter.convert(message); +// if (msg != null) { + +// } + } else if (channel.equalsIgnoreCase(MessagingConstants.TO_SYSTEM_CHANNEL)) { +// IMessage msg = MessageFromJsonConverter.convert(message); + +// if (msg != null) { +// +// } + } + } + + + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageDistributor.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageDistributor.java new file mode 100755 index 0000000000000000000000000000000000000000..794704bec8156ff8b50bf697df60fd8dea12eddb --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageDistributor.java @@ -0,0 +1,25 @@ +package org.bigbluebutton.app.screenshare.messaging.redis; + +import java.util.Set; + +public class MessageDistributor { + private ReceivedMessageHandler handler; + private Set<MessageHandler> listeners; + + public void setMessageListeners(Set<MessageHandler> listeners) { + this.listeners = listeners; + } + + public void setMessageHandler(ReceivedMessageHandler handler) { + this.handler = handler; + if (handler != null) { + handler.setMessageDistributor(this); + } + } + + public void notifyListeners(String pattern, String channel, String message) { + for (MessageHandler listener : listeners) { + listener.handleMessage(pattern, channel, message); + } + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageHandler.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..9b4cabcac78bbc5e4d79a9c58885e6ccf46498bf --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageHandler.java @@ -0,0 +1,23 @@ +/** +* 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.app.screenshare.messaging.redis; + +public interface MessageHandler { + void handleMessage(String pattern, String channel, String message); +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageReceiver.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageReceiver.java new file mode 100755 index 0000000000000000000000000000000000000000..a59df4c096472a43d3737d45c436b4282b286861 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageReceiver.java @@ -0,0 +1,88 @@ +package org.bigbluebutton.app.screenshare.messaging.redis; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPubSub; + +public class MessageReceiver { + private static Logger log = Red5LoggerFactory.getLogger(MessageReceiver.class, "bigbluebutton"); + + private ReceivedMessageHandler handler; + + private JedisPool redisPool; + private volatile boolean receiveMessage = false; + + private final Executor msgReceiverExec = Executors.newSingleThreadExecutor(); + + public void stop() { + receiveMessage = false; + } + + public void start() { + log.info("Ready to receive messages from Redis pubsub."); + try { + receiveMessage = true; + final Jedis jedis = redisPool.getResource(); + + Runnable messageReceiver = new Runnable() { + public void run() { + if (receiveMessage) { + jedis.psubscribe(new PubSubListener(), MessagingConstants.TO_BBB_APPS_PATTERN); + } + } + }; + msgReceiverExec.execute(messageReceiver); + } catch (Exception e) { + log.error("Error subscribing to channels: " + e.getMessage()); + } + } + + public void setRedisPool(JedisPool redisPool){ + this.redisPool = redisPool; + } + + public void setMessageHandler(ReceivedMessageHandler handler) { + this.handler = handler; + } + + private class PubSubListener extends JedisPubSub { + + public PubSubListener() { + super(); + } + + @Override + public void onMessage(String channel, String message) { + // Not used. + } + + @Override + public void onPMessage(String pattern, String channel, String message) { + handler.handleMessage(pattern, channel, message); + } + + @Override + public void onPSubscribe(String pattern, int subscribedChannels) { + log.debug("Subscribed to the pattern: " + pattern); + } + + @Override + public void onPUnsubscribe(String pattern, int subscribedChannels) { + // Not used. + } + + @Override + public void onSubscribe(String channel, int subscribedChannels) { + // Not used. + } + + @Override + public void onUnsubscribe(String channel, int subscribedChannels) { + // Not used. + } + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageSender.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageSender.java new file mode 100755 index 0000000000000000000000000000000000000000..18067a0a986a34ae5b7440e0c90decafeb8ef410 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageSender.java @@ -0,0 +1,74 @@ +package org.bigbluebutton.app.screenshare.messaging.redis; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +public class MessageSender { + private static Logger log = Red5LoggerFactory.getLogger(MessageSender.class, "bigbluebutton"); + + private JedisPool redisPool; + private volatile boolean sendMessage = false; + + private final Executor msgSenderExec = Executors.newSingleThreadExecutor(); + private final Executor runExec = Executors.newSingleThreadExecutor(); + private BlockingQueue<MessageToSend> messages = new LinkedBlockingQueue<MessageToSend>(); + + public void stop() { + sendMessage = false; + } + + public void start() { + log.info("Redis message publisher starting!"); + try { + sendMessage = true; + + Runnable messageSender = new Runnable() { + public void run() { + while (sendMessage) { + try { + MessageToSend msg = messages.take(); + publish(msg.getChannel(), msg.getMessage()); + } catch (InterruptedException e) { + log.warn("Failed to get message from queue."); + } + } + } + }; + msgSenderExec.execute(messageSender); + } catch (Exception e) { + log.error("Error subscribing to channels: " + e.getMessage()); + } + } + + public void send(String channel, String message) { + MessageToSend msg = new MessageToSend(channel, message); + messages.add(msg); + } + + private void publish(final String channel, final String message) { + Runnable task = new Runnable() { + public void run() { + Jedis jedis = redisPool.getResource(); + try { + jedis.publish(channel, message); + } catch(Exception e){ + log.warn("Cannot publish the message to redis", e); + } finally { + redisPool.returnResource(jedis); + } + } + }; + + runExec.execute(task); + } + + public void setRedisPool(JedisPool redisPool){ + this.redisPool = redisPool; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageToSend.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageToSend.java new file mode 100755 index 0000000000000000000000000000000000000000..45951b4dccdf449b9fc386b35faf5dac32dde813 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessageToSend.java @@ -0,0 +1,19 @@ +package org.bigbluebutton.app.screenshare.messaging.redis; + +public class MessageToSend { + private final String channel; + private final String message; + + public MessageToSend(String channel, String message) { + this.channel = channel; + this.message = message; + } + + public String getChannel() { + return channel; + } + + public String getMessage() { + return message; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessagingConstants.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessagingConstants.java new file mode 100755 index 0000000000000000000000000000000000000000..0a787e84d3788d7d562b198694078f545ce84e74 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/MessagingConstants.java @@ -0,0 +1,40 @@ +/** +* 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.app.screenshare.messaging.redis; + +public class MessagingConstants { + + public static final String FROM_BBB_APPS_CHANNEL = "bigbluebutton:from-bbb-apps"; + public static final String FROM_BBB_APPS_PATTERN = FROM_BBB_APPS_CHANNEL + ":*"; + public static final String FROM_SYSTEM_CHANNEL = FROM_BBB_APPS_CHANNEL + ":system"; + public static final String FROM_MEETING_CHANNEL = FROM_BBB_APPS_CHANNEL + ":meeting"; + + public static final String TO_BBB_APPS_CHANNEL = "bigbluebutton:to-bbb-apps"; + public static final String TO_BBB_APPS_PATTERN = TO_BBB_APPS_CHANNEL + ":*"; + public static final String TO_MEETING_CHANNEL = TO_BBB_APPS_CHANNEL + ":meeting"; + public static final String TO_SYSTEM_CHANNEL = TO_BBB_APPS_CHANNEL + ":system"; + + public static final String DESTROY_MEETING_REQUEST_EVENT = "DestroyMeetingRequestEvent"; + public static final String CREATE_MEETING_REQUEST_EVENT = "CreateMeetingRequestEvent"; + public static final String END_MEETING_REQUEST_EVENT = "EndMeetingRequestEvent"; + public static final String MEETING_STARTED_EVENT = "meeting_created_message"; + public static final String MEETING_ENDED_EVENT = "meeting_ended_event"; + public static final String MEETING_DESTROYED_EVENT = "meeting_destroyed_event"; + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/ReceivedMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/ReceivedMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..08d4b3996a6c06e70e4ad630cf8b10dd800604d7 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/ReceivedMessage.java @@ -0,0 +1,28 @@ +package org.bigbluebutton.app.screenshare.messaging.redis; + +public class ReceivedMessage { + + private final String pattern; + private final String channel; + private final String message; + + public ReceivedMessage(String pattern, String channel, String message) { + this.pattern = pattern; + this.channel = channel; + this.message = message; + } + + public String getPattern() { + return pattern; + } + + public String getChannel() { + return channel; + } + + public String getMessage() { + return message; + } + + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/ReceivedMessageHandler.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/ReceivedMessageHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..b713dd3c3f6096da4725640d2219f84811fb20c5 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/messaging/redis/ReceivedMessageHandler.java @@ -0,0 +1,72 @@ +package org.bigbluebutton.app.screenshare.messaging.redis; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +public class ReceivedMessageHandler { + private static Logger log = Red5LoggerFactory.getLogger(ReceivedMessageHandler.class, "bigbluebutton"); + + private BlockingQueue<ReceivedMessage> receivedMessages = new LinkedBlockingQueue<ReceivedMessage>(); + + private volatile boolean processMessage = false; + + private final Executor msgProcessorExec = Executors.newSingleThreadExecutor(); + private final Executor runExec = Executors.newSingleThreadExecutor(); + + private MessageDistributor handler; + + public void stop() { + processMessage = false; + } + + public void start() { + log.info("Ready to handle messages from Redis pubsub!"); + + try { + processMessage = true; + + Runnable messageProcessor = new Runnable() { + public void run() { + while (processMessage) { + try { + ReceivedMessage msg = receivedMessages.take(); + processMessage(msg); + } catch (InterruptedException e) { + log.warn("Error while taking received message from queue."); + } + } + } + }; + msgProcessorExec.execute(messageProcessor); + } catch (Exception e) { + log.error("Error subscribing to channels: " + e.getMessage()); + } + } + + private void processMessage(final ReceivedMessage msg) { + Runnable task = new Runnable() { + public void run() { + if (handler != null) { + handler.notifyListeners(msg.getPattern(), msg.getChannel(), msg.getMessage()); + } else { + log.info("No listeners interested in messages from Redis!"); + } + } + }; + runExec.execute(task); + } + + public void handleMessage(String pattern, String channel, String message) { + ReceivedMessage rm = new ReceivedMessage(pattern, channel, message); + receivedMessages.add(rm); + } + + public void setMessageDistributor(MessageDistributor h) { + this.handler = h; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/BroadcastClientMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/BroadcastClientMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..0800b43b004f96de6b2cd8da08bff024cc78b666 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/BroadcastClientMessage.java @@ -0,0 +1,46 @@ +/** +* 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.app.screenshare.red5; + +import java.util.Map; + +public class BroadcastClientMessage implements ClientMessage { + + private String meetingID; + private Map<String, Object> message; + private String messageName; + + public BroadcastClientMessage(String meetingID, String messageName, Map<String, Object> message) { + this.meetingID = meetingID; + this.message = message; + this.messageName = messageName; + } + + public String getMeetingID() { + return meetingID; + } + + public String getMessageName() { + return messageName; + } + + public Map<String, Object> getMessage() { + return message; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/ClientMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/ClientMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..501256a21dcccd22d633a78e8457a1b862a8b0fc --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/ClientMessage.java @@ -0,0 +1,24 @@ +/** +* 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.app.screenshare.red5; + + +public interface ClientMessage { + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/ConnectionInvokerService.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/ConnectionInvokerService.java new file mode 100755 index 0000000000000000000000000000000000000000..12f811f039ce2c3c4f907791de82e4917e444e92 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/ConnectionInvokerService.java @@ -0,0 +1,220 @@ +/** + * 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.app.screenshare.red5; + +import java.util.Set; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import org.red5.logging.Red5LoggerFactory; +import org.red5.server.api.IConnection; +import org.red5.server.api.scope.IScope; +import org.red5.server.api.scope.ScopeType; +import org.red5.server.api.service.ServiceUtils; +import org.red5.server.api.so.ISharedObject; +import org.red5.server.api.so.ISharedObjectService; +import org.red5.server.so.SharedObjectService; +import org.red5.server.util.ScopeUtils; +import org.slf4j.Logger; + +public class ConnectionInvokerService { + private static Logger log = Red5LoggerFactory.getLogger(ConnectionInvokerService.class, "screenshare"); + + private static final int NTHREADS = 1; + private static final Executor exec = Executors.newFixedThreadPool(NTHREADS); + private static final Executor runExec = Executors.newFixedThreadPool(NTHREADS); + + private BlockingQueue<ClientMessage> messages; + + private volatile boolean sendMessages = false; + private IScope bbbAppScope; + + public ConnectionInvokerService() { + messages = new LinkedBlockingQueue<ClientMessage>(); + } + + public void setAppScope(IScope scope) { + bbbAppScope = scope; + } + + public void start() { + sendMessages = true; + Runnable sender = new Runnable() { + public void run() { + while (sendMessages) { + ClientMessage message; + try { + message = messages.take(); + sendMessageToClient(message); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + }; + exec.execute(sender); + } + + public void stop() { + sendMessages = false; + } + + public void sendMessage(final ClientMessage message) { + messages.offer(message); + } + + private void sendMessageToClient(ClientMessage message) { + if (message instanceof BroadcastClientMessage) { + sendBroadcastMessage((BroadcastClientMessage) message); + } else if (message instanceof DirectClientMessage) { + sendDirectMessage((DirectClientMessage) message); + } else if (message instanceof SharedObjectClientMessage) { + sendSharedObjectMessage((SharedObjectClientMessage) message); + } else if (message instanceof DisconnectClientMessage) { + handlDisconnectClientMessage((DisconnectClientMessage) message); + } else if (message instanceof DisconnectAllClientsMessage) { + handleDisconnectAllClientsMessage((DisconnectAllClientsMessage) message); + } + } + + private void handleDisconnectAllClientsMessage(DisconnectAllClientsMessage msg) { + IScope meetingScope = getScope(msg.getMeetingId()); + if (meetingScope != null) { + Set<IConnection> conns = meetingScope.getClientConnections(); + + for (IConnection conn : conns) { + if (conn.isConnected()) { + String connId = (String) conn.getAttribute("INTERNAL_USER_ID"); + log.info("Disconnecting client=[{}] from meeting=[{}]", connId, msg.getMeetingId()); + conn.close(); + } + } + } + } + + private void handlDisconnectClientMessage(DisconnectClientMessage msg) { + IScope meetingScope = getScope(msg.getMeetingId()); + if (meetingScope != null) { + IConnection conn = getConnection(meetingScope, msg.getUserId()); + if (conn != null) { + if (conn.isConnected()) { + log.info("Disconnecting user=[{}] from meeting=[{}]", msg.getUserId(), msg.getMeetingId()); + conn.close(); + } + } + } + } + + private void sendSharedObjectMessage(SharedObjectClientMessage msg) { + System.out.println("*********** Request to send [" + msg.getMessageName() + "] using shared object."); + + IScope meetingScope = getScope(msg.getMeetingID()); + if (meetingScope != null) { + if (meetingScope.hasChildScope(ScopeType.SHARED_OBJECT, msg.getSharedObjectName())) { + ISharedObject so = getSharedObject(meetingScope, msg.getSharedObjectName()); + if (so != null) { + System.out.println("*********** Sending [" + msg.getMessageName() + "] using shared object."); + so.sendMessage(msg.getMessageName(), msg.getMessage()); + } else { + System.out.println("**** Cannot get SO for [" + msg.getSharedObjectName() + "]"); + } + } else { + System.out.println("**** No SO scope for [" + msg.getSharedObjectName() + "]"); + } + } else { + System.out.println("**** No Meeting scope for [" + msg.getMeetingID() + "]"); + } + } + + private void sendDirectMessage(final DirectClientMessage msg) { + Runnable sender = new Runnable() { + public void run() { + IScope meetingScope = getScope(msg.getMeetingID()); + if (meetingScope != null) { + log.debug("Found scope =[{}] for meeting=[{}]", meetingScope.getName(), msg.getMeetingID()); + IConnection conn = getConnection(meetingScope, msg.getUserID()); + if (conn != null) { + if (conn.isConnected()) { + List<Object> params = new ArrayList<Object>(); + params.add(msg.getMessageName()); + params.add(msg.getMessage()); + log.debug("Sending message=[{}] to meeting=[{}]", msg.getMessageName(), msg.getMeetingID()); + ServiceUtils.invokeOnConnection(conn, "onMessageFromServer", params.toArray()); + } else { + log.warn("Connection not connected for userid=[{}] in meeting=[{}]", msg.getUserID(), msg.getMeetingID()); + } + } else { + log.warn("No connection for userid=[{}] in meeting=[{}]", msg.getUserID(), msg.getMeetingID()); + } + } else { + log.error("Failed to find scope for meeting=[{}]", msg.getMeetingID()); + } + } + }; + runExec.execute(sender); + } + + private void sendBroadcastMessage(final BroadcastClientMessage msg) { + Runnable sender = new Runnable() { + public void run() { + IScope meetingScope = getScope(msg.getMeetingID()); + if (meetingScope != null) { + List<Object> params = new ArrayList<Object>(); + params.add(msg.getMessageName()); + params.add(msg.getMessage()); + ServiceUtils.invokeOnAllScopeConnections(meetingScope, "onMessageFromServer", params.toArray(), null); + } + } + }; + runExec.execute(sender); + } + + private IConnection getConnection(IScope scope, String userID) { + Set<IConnection> conns = scope.getClientConnections(); + for (IConnection conn : conns) { + String connID = (String) conn.getAttribute("USERID"); + if (connID != null && connID.equals(userID)) { + return conn; + } + } + + return null; + } + + public IScope getScope(String meetingID) { + if (bbbAppScope != null) { + return bbbAppScope.getContext().resolveScope("screenshare/" + meetingID); + } else { + log.error("BigBlueButton Scope not initialized. No messages are going to the Flash client!"); + } + + return null; + } + + private ISharedObject getSharedObject(IScope scope, String name) { + ISharedObjectService service = (ISharedObjectService) ScopeUtils.getScopeService(scope, ISharedObjectService.class, SharedObjectService.class, false); + return service.getSharedObject(scope, name); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DirectClientMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DirectClientMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..c84773194ef564dab2347ca2d36414ec35d344c2 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DirectClientMessage.java @@ -0,0 +1,61 @@ +/** +* 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.app.screenshare.red5; + +import java.util.Map; + +public class DirectClientMessage implements ClientMessage { + + private String meetingID; + private String userID; + private Map<String, Object> message; + private String messageName; + private String sharedObjectName; + + public DirectClientMessage(String meetingID, String userID, String messageName, Map<String, Object> message) { + this.meetingID = meetingID; + this.userID = userID; + this.message = message; + this.messageName = messageName; + } + + public void setSharedObjectName(String name) { + sharedObjectName = name; + } + + public String getSharedObjectName() { + return sharedObjectName; + } + + public String getMeetingID() { + return meetingID; + } + + public String getUserID() { + return userID; + } + + public String getMessageName() { + return messageName; + } + + public Map<String, Object> getMessage() { + return message; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DisconnectAllClientsMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DisconnectAllClientsMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..cabe2d3141e80f55644230bdf614aad721539f40 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DisconnectAllClientsMessage.java @@ -0,0 +1,14 @@ +package org.bigbluebutton.app.screenshare.red5; + +public class DisconnectAllClientsMessage implements ClientMessage { + + private final String meetingId; + + public DisconnectAllClientsMessage(String meetingId) { + this.meetingId = meetingId; + } + + public String getMeetingId() { + return meetingId; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DisconnectClientMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DisconnectClientMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..64352951631fd7b04de70ccf1a58667d8ca320e7 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/DisconnectClientMessage.java @@ -0,0 +1,20 @@ +package org.bigbluebutton.app.screenshare.red5; + +public class DisconnectClientMessage implements ClientMessage { + + private final String meetingId; + private final String userId; + + public DisconnectClientMessage(String meetingId, String userId) { + this.meetingId = meetingId; + this.userId = userId; + } + + public String getMeetingId() { + return meetingId; + } + + public String getUserId() { + return userId; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/EventListenerImp.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/EventListenerImp.java new file mode 100755 index 0000000000000000000000000000000000000000..08f675428da7cd8fb76dd9f96ecf4e7c25588ae9 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/EventListenerImp.java @@ -0,0 +1,91 @@ +package org.bigbluebutton.app.screenshare.red5; + +import java.util.HashMap; +import java.util.Map; + +import org.bigbluebutton.app.screenshare.events.IEvent; +import org.bigbluebutton.app.screenshare.events.IEventListener; +import org.bigbluebutton.app.screenshare.events.ShareStartedEvent; +import org.bigbluebutton.app.screenshare.events.ShareStoppedEvent; +import org.bigbluebutton.app.screenshare.events.StreamStartedEvent; +import org.bigbluebutton.app.screenshare.events.StreamStoppedEvent; + +import com.google.gson.Gson; + +public class EventListenerImp implements IEventListener { + private ConnectionInvokerService sender; + + @Override + public void handleMessage(IEvent event) { + if (event instanceof ShareStartedEvent) { + sendShareStartedEvent((ShareStartedEvent) event); + } else if (event instanceof ShareStoppedEvent) { + sendShareStoppedEvent((ShareStoppedEvent) event); + } else if (event instanceof StreamStartedEvent) { + sendStreamStartedEvent((StreamStartedEvent) event); + } else if (event instanceof StreamStoppedEvent) { + sendStreamStoppedEvent((StreamStoppedEvent) event); + } + + } + + private void sendShareStartedEvent(ShareStartedEvent event) { + Map<String, Object> data = new HashMap<String, Object>(); + data.put("meetingId", event.meetingId); + data.put("streamId", event.streamId); + + Map<String, Object> message = new HashMap<String, Object>(); + Gson gson = new Gson(); + message.put("msg", gson.toJson(data)); + + BroadcastClientMessage msg = new BroadcastClientMessage(event.meetingId, "screenShareStartedMessage", message); + sender.sendMessage(msg); + } + + private void sendShareStoppedEvent(ShareStoppedEvent event) { + Map<String, Object> data = new HashMap<String, Object>(); + data.put("meetingId", event.meetingId); + data.put("streamId", event.streamId); + + Map<String, Object> message = new HashMap<String, Object>(); + Gson gson = new Gson(); + message.put("msg", gson.toJson(data)); + + BroadcastClientMessage msg = new BroadcastClientMessage(event.meetingId, "screenShareStoppedMessage", message); + sender.sendMessage(msg); + } + + private void sendStreamStartedEvent(StreamStartedEvent event) { + Map<String, Object> data = new HashMap<String, Object>(); + data.put("meetingId", event.meetingId); + data.put("streamId", event.streamId); + data.put("width", event.width); + data.put("height", event.height); + data.put("url", event.url); + + Map<String, Object> message = new HashMap<String, Object>(); + Gson gson = new Gson(); + message.put("msg", gson.toJson(data)); + + BroadcastClientMessage msg = new BroadcastClientMessage(event.meetingId, "screenStreamStartedMessage", message); + sender.sendMessage(msg); + } + + private void sendStreamStoppedEvent(StreamStoppedEvent event) { + Map<String, Object> data = new HashMap<String, Object>(); + data.put("meetingId", event.meetingId); + data.put("streamId", event.streamId); + + Map<String, Object> message = new HashMap<String, Object>(); + Gson gson = new Gson(); + message.put("msg", gson.toJson(data)); + + BroadcastClientMessage msg = new BroadcastClientMessage(event.meetingId, "screenStreamStoppedMessage", message); + sender.sendMessage(msg); + } + + public void setMessageSender(ConnectionInvokerService sender) { + this.sender = sender; + } + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppAdapter.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppAdapter.java new file mode 100755 index 0000000000000000000000000000000000000000..ccbf050f2055d43979de51143577874676ed4c98 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppAdapter.java @@ -0,0 +1,274 @@ +/** + * 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.app.screenshare.red5; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.stream.IBroadcastStream; +import org.red5.server.api.stream.IServerStream; +import org.red5.server.api.stream.IStreamListener; +import org.red5.server.stream.ClientBroadcastStream; +import org.slf4j.Logger; + +import com.google.gson.Gson; + +import org.bigbluebutton.app.screenshare.EventRecordingService; +import org.bigbluebutton.app.screenshare.IScreenShareApplication; +import org.bigbluebutton.app.screenshare.ScreenshareStreamListener; + +public class Red5AppAdapter extends MultiThreadedApplicationAdapter { + private static Logger log = Red5LoggerFactory.getLogger(Red5AppAdapter.class, "screenshare"); + + private EventRecordingService recordingService; + private final Map<String, IStreamListener> streamListeners = new HashMap<String, IStreamListener>(); + + private IScreenShareApplication app; + private String streamBaseUrl; + private ConnectionInvokerService sender; + private String recordingDirectory; + + @Override + public boolean appStart(IScope app) { + super.appStart(app); + log.info("BBB Screenshare appStart"); + sender.setAppScope(app); + return true; + } + + @Override + public boolean appConnect(IConnection conn, Object[] params) { + log.info("BBB Screenshare appConnect"); + return super.appConnect(conn, params); + } + + @Override + public boolean roomConnect(IConnection conn, Object[] params) { + log.info("BBB Screenshare roomConnect"); + return super.roomConnect(conn, params); + } + + private String getConnectionType(String connType) { + if ("persistent".equals(connType.toLowerCase())) { + return "RTMP"; + } else if("polling".equals(connType.toLowerCase())) { + return "RTMPT"; + } else { + return connType.toUpperCase(); + } + } + + private String getUserId() { + String userid = (String) Red5.getConnectionLocal().getAttribute("USERID"); + if ((userid == null) || ("".equals(userid))) userid = "unknown-userid"; + return userid; + } + + private String getMeetingId() { + String meetingId = (String) Red5.getConnectionLocal().getAttribute("MEETING_ID"); + if ((meetingId == null) || ("".equals(meetingId))) meetingId = "unknown-meetingid"; + return meetingId; + } + + @Override + public void appDisconnect(IConnection conn) { + log.info("BBB Screenshare appDisconnect"); + + String connType = getConnectionType(Red5.getConnectionLocal().getType()); + String connId = Red5.getConnectionLocal().getSessionId(); + + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", getMeetingId()); + logData.put("userId", getUserId()); + logData.put("connType", connType); + logData.put("connId", connId); + logData.put("event", "user_leaving_bbb_screenshare"); + logData.put("description", "User leaving BBB Screenshare."); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + + log.info("User leaving bbb-screenshare: data={}", logStr); + + super.appDisconnect(conn); + } + + @Override + public void roomDisconnect(IConnection conn) { + log.info("BBB Screenshare roomDisconnect"); + + String connType = getConnectionType(Red5.getConnectionLocal().getType()); + String connId = Red5.getConnectionLocal().getSessionId(); + + String meetingId = conn.getScope().getName(); + String userId = getUserId(); + + app.userDisconnected(meetingId, userId); + + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", getMeetingId()); + logData.put("userId", userId); + logData.put("connType", connType); + logData.put("connId", connId); + logData.put("event", "user_leaving_bbb_screenshare"); + logData.put("description", "User leaving BBB Screenshare."); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + + log.info("User leaving bbb-screenshare: data={}", logStr); + + super.roomDisconnect(conn); + } + + @Override + public void streamPublishStart(IBroadcastStream stream) { + super.streamPublishStart(stream); + } + + @Override + public void streamBroadcastStart(IBroadcastStream stream) { + IConnection conn = Red5.getConnectionLocal(); + super.streamBroadcastStart(stream); + + log.info("streamBroadcastStart " + stream.getPublishedName() + "]"); + String streamId = stream.getPublishedName(); + Matcher matcher = STREAM_ID_PATTERN.matcher(stream.getPublishedName()); + if (matcher.matches()) { + String meetingId = matcher.group(1).trim(); + String url = streamBaseUrl + "/" + meetingId + "/" + streamId; + app.streamStarted(meetingId, streamId, url); + + boolean recordVideoStream = app.recordStream(meetingId, streamId); + if (recordVideoStream) { + recordStream(stream); + ScreenshareStreamListener listener = new ScreenshareStreamListener(recordingService, recordingDirectory); + stream.addStreamListener(listener); + streamListeners.put(conn.getScope().getName() + "-" + stream.getPublishedName(), listener); + } + } else { + log.error("Invalid streamid format [{}]", streamId); + } + } + + private Long genTimestamp() { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); + } + + private final Pattern STREAM_ID_PATTERN = Pattern.compile("(.*)-(.*)$"); + + @Override + public void streamBroadcastClose(IBroadcastStream stream) { + super.streamBroadcastClose(stream); + + log.info("streamBroadcastStop " + stream.getPublishedName() + "]"); + String streamId = stream.getPublishedName(); + Matcher matcher = STREAM_ID_PATTERN.matcher(stream.getPublishedName()); + if (matcher.matches()) { + String meetingId = matcher.group(1).trim(); + app.streamStopped(meetingId, streamId); + + boolean recordVideoStream = app.recordStream(meetingId, streamId); + if (recordVideoStream) { + IConnection conn = Red5.getConnectionLocal(); + String scopeName; + if (conn != null) { + scopeName = conn.getScope().getName(); + } else { + log.info("Connection local was null, using scope name from the stream: {}", stream); + scopeName = stream.getScope().getName(); + } + IStreamListener listener = streamListeners.remove(scopeName + "-" + stream.getPublishedName()); + if (listener != null) { + stream.removeStreamListener(listener); + } + + String filename = recordingDirectory; + if (!filename.endsWith("/")) { + filename.concat("/"); + } + + filename = filename.concat(meetingId).concat("/").concat(stream.getPublishedName()).concat(".flv"); + + long publishDuration = (System.currentTimeMillis() - stream.getCreationTime()) / 1000; + log.info("streamBroadcastClose " + stream.getPublishedName() + " " + System.currentTimeMillis() + " " + scopeName); + Map<String, String> event = new HashMap<String, String>(); + event.put("module", "Deskshare"); + event.put("timestamp", new Long(System.currentTimeMillis()).toString()); + event.put("meetingId", scopeName); + event.put("stream", stream.getPublishedName()); + event.put("file", filename); + event.put("duration", new Long(publishDuration).toString()); + event.put("eventName", "DeskshareStoppedEvent"); + recordingService.record(scopeName, event); + } + } else { + log.error("Invalid streamid format [{}]", streamId); + } + } + + /** + * A hook to record a stream. A file is written in webapps/video/streams/ + * @param stream + */ + private void recordStream(IBroadcastStream stream) { + IConnection conn = Red5.getConnectionLocal(); + long now = System.currentTimeMillis(); + String recordingStreamName = stream.getPublishedName(); // + "-" + now; /** Comment out for now...forgot why I added this - ralam */ + + try { + log.info("Recording stream " + recordingStreamName ); + ClientBroadcastStream cstream = (ClientBroadcastStream) this.getBroadcastStream(conn.getScope(), stream.getPublishedName()); + cstream.saveAs(recordingStreamName, false); + } catch(Exception e) { + log.error("ERROR while recording stream " + e.getMessage()); + e.printStackTrace(); + } + } + + + + public void setEventRecordingService(EventRecordingService s) { + recordingService = s; + } + + public void setStreamBaseUrl(String baseUrl) { + streamBaseUrl = baseUrl; + } + + public void setRecordingDirectory(String dir) { + recordingDirectory = dir; + } + + public void setApplication(IScreenShareApplication app) { + this.app = app; + } + + public void setMessageSender(ConnectionInvokerService sender) { + this.sender = sender; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppHandler.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..b25fd74529a0971a69712862892dd0a664f3279b --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppHandler.java @@ -0,0 +1,93 @@ +package org.bigbluebutton.app.screenshare.red5; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.bigbluebutton.app.screenshare.IScreenShareApplication; +import org.bigbluebutton.app.screenshare.IsScreenSharingResponse; +import org.bigbluebutton.app.screenshare.StartShareRequestResponse; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import com.google.gson.Gson; + +public class Red5AppHandler { + private static Logger log = Red5LoggerFactory.getLogger(Red5AppHandler.class, "screenshare"); + + private IScreenShareApplication app; + private ConnectionInvokerService sender; + + private final Pattern STREAM_ID_PATTERN = Pattern.compile("(.*)-(.*)$"); + + public void isScreenSharing(String meetingId, String userId) { + IsScreenSharingResponse resp = app.isScreenSharing(meetingId); + + Map<String, Object> data = new HashMap<String, Object>(); + data.put("sharing", resp.info.sharing); + + if (resp.info.sharing) { + data.put("streamId", resp.info.streamId); + data.put("width", resp.info.width); + data.put("height", resp.info.height); + data.put("url", resp.info.url); + } + + Map<String, Object> message = new HashMap<String, Object>(); + Gson gson = new Gson(); + message.put("msg", gson.toJson(data)); + + log.info("Sending isSharingScreenRequestResponse to client, meetingId=" + meetingId + " userid=" + userId); + DirectClientMessage msg = new DirectClientMessage(meetingId, userId, "isSharingScreenRequestResponse", message); + sender.sendMessage(msg); + } + + public void startShareRequest(String meetingId, String userId, Boolean record) { + StartShareRequestResponse resp = app.startShareRequest(meetingId, userId, record); + + Map<String, Object> data = new HashMap<String, Object>(); + + if (resp.error != null) { + data.put("error", resp.error.reason); + } else { + data.put("authToken", resp.token); + data.put("jnlp", resp.jnlp); + } + + Map<String, Object> message = new HashMap<String, Object>(); + Gson gson = new Gson(); + message.put("msg", gson.toJson(data)); + + log.info("Sending startShareRequestResponse to client, meetingId=" + meetingId + " userid=" + userId); + DirectClientMessage msg = new DirectClientMessage(meetingId, userId, "startShareRequestResponse", message); + sender.sendMessage(msg); + } + + public void stopShareRequest(String meetingId, String streamId) { + Matcher matcher = STREAM_ID_PATTERN.matcher(streamId); + if (matcher.matches()) { + app.stopShareRequest(meetingId, streamId); + } + + Map<String, Object> data = new HashMap<String, Object>(); + data.put("meetingId", meetingId); + data.put("streamId", streamId); + + Map<String, Object> message = new HashMap<String, Object>(); + Gson gson = new Gson(); + message.put("msg", gson.toJson(data)); + + log.info("Sending stopShareRequest to client, meetingId=" + meetingId + " streamId=" + streamId); + BroadcastClientMessage msg = new BroadcastClientMessage(meetingId, "stopViewingStream", message); + sender.sendMessage(msg); + } + + public void setApplication(IScreenShareApplication app) { + this.app = app; + } + + public void setMessageSender(ConnectionInvokerService sender) { + this.sender = sender; + } + + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppService.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppService.java new file mode 100755 index 0000000000000000000000000000000000000000..2cde72d2701c8199f685a29f5c779d13648f101c --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/Red5AppService.java @@ -0,0 +1,87 @@ +package org.bigbluebutton.app.screenshare.red5; + +import java.util.HashMap; +import java.util.Map; + +import org.red5.logging.Red5LoggerFactory; +import org.red5.server.api.Red5; +import org.slf4j.Logger; + +import com.google.gson.Gson; + + +public class Red5AppService { + private static Logger log = Red5LoggerFactory.getLogger(Red5AppService.class, "screenshare"); + + private Red5AppHandler handler; + + /** + * Called from the client to pass us the userId. + * + * We need to to this as we can't have params on the connect call + * as FFMeeg won't be able to connect. + * @param userId + */ + public void setUserId(Map<String, Object> msg) { + String meetingId = Red5.getConnectionLocal().getScope().getName(); + String userId = (String) msg.get("userId"); + Red5.getConnectionLocal().setAttribute("MEETING_ID", meetingId); + Red5.getConnectionLocal().setAttribute("USERID", userId); + + String connType = getConnectionType(Red5.getConnectionLocal().getType()); + String connId = Red5.getConnectionLocal().getSessionId(); + + Map<String, Object> logData = new HashMap<String, Object>(); + logData.put("meetingId", meetingId); + logData.put("userId", userId); + logData.put("connType", connType); + logData.put("connId", connId); + logData.put("event", "user_joining_bbb_screenshare"); + logData.put("description", "User joining BBB Screenshare."); + + Gson gson = new Gson(); + String logStr = gson.toJson(logData); + + log.info("User joining bbb-screenshare: data={}", logStr); + } + + private String getConnectionType(String connType) { + if ("persistent".equals(connType.toLowerCase())) { + return "RTMP"; + } else if("polling".equals(connType.toLowerCase())) { + return "RTMPT"; + } else { + return connType.toUpperCase(); + } + } + + public void isScreenSharing(Map<String, Object> msg) { + String meetingId = Red5.getConnectionLocal().getScope().getName(); + log.debug("Received check if publishing for meeting=[{}]", meetingId); + String userId = (String) Red5.getConnectionLocal().getAttribute("USERID"); + + handler.isScreenSharing(meetingId, userId); + } + + public void startShareRequest(Map<String, Object> msg) { + Boolean record = (Boolean) msg.get("record"); + String meetingId = Red5.getConnectionLocal().getScope().getName(); + log.debug("Received startShareRequest for meeting=[{}]", meetingId); + String userId = (String) Red5.getConnectionLocal().getAttribute("USERID"); + + handler.startShareRequest(meetingId, userId, record); + } + + public void stopShareRequest(Map<String, Object> msg) { + String meetingId = Red5.getConnectionLocal().getScope().getName(); + String streamId = (String) msg.get("streamId"); + log.debug("Received stopShareRequest for meeting=[{}]", meetingId); + + handler.stopShareRequest(meetingId, streamId); + } + + + public void setAppHandler(Red5AppHandler handler) { + this.handler = handler; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/SharedObjectClientMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/SharedObjectClientMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..054cfdbaf506f2c8c0318cfe67875b7b3c5a454c --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/red5/SharedObjectClientMessage.java @@ -0,0 +1,59 @@ +/** +* 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.app.screenshare.red5; + +import java.util.ArrayList; + +public class SharedObjectClientMessage implements ClientMessage { + public static final String BROADCAST = "broadcast"; + public static final String DIRECT = "direct"; + public static final String SHAREDOBJECT = "sharedobject"; + + private String meetingID; + private String sharedObjectName; + private ArrayList<Object> message; + private String messageName; + + public SharedObjectClientMessage(String meetingID, String sharedObjectName, String messageName, ArrayList<Object> message) { + this.meetingID = meetingID; + this.message = message; + this.sharedObjectName = sharedObjectName; + this.messageName = messageName; + } + + public void setSharedObjectName(String name) { + sharedObjectName = name; + } + + public String getSharedObjectName() { + return sharedObjectName; + } + + public String getMeetingID() { + return meetingID; + } + + public String getMessageName() { + return messageName; + } + + public ArrayList<Object> getMessage() { + return message; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureEndMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureEndMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..46b0b8ad9793ed25f5d903a247c6f2ca647a35aa --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureEndMessage.java @@ -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 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.app.screenshare.server.messages; + +public class CaptureEndMessage { + + private final String room; + private final int sequenceNum; + + public CaptureEndMessage(String room, int sequenceNum) { + this.room = room; + this.sequenceNum = sequenceNum; + } + + public String getRoom() { + return room; + } + + public int getSequenceNum() { + return sequenceNum; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureStartMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureStartMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..592c9b6eb350e1c13c4ce76c8851b099b3ccb74e --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureStartMessage.java @@ -0,0 +1,58 @@ +/** +* 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.app.screenshare.server.messages; + +import org.bigbluebutton.app.screenshare.server.session.Dimension; + +public class CaptureStartMessage { + + private final String room; + private final Dimension screenDim; + private final Dimension blockDim; + private final int sequenceNum; + private final boolean useSVC2; + + public CaptureStartMessage(String room, Dimension screen, Dimension block, int sequenceNum, boolean useSVC2) { + this.room = room; + screenDim = screen; + blockDim = block; + this.sequenceNum = sequenceNum; + this.useSVC2 = useSVC2; + } + + public Dimension getScreenDimension() { + return screenDim; + } + + public Dimension getBlockDimension() { + return blockDim; + } + + public String getRoom() { + return room; + } + + public int getSequenceNum() { + return sequenceNum; + } + + public boolean isUseSVC2() { + return useSVC2; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureUpdateMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureUpdateMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..1e4c704a28872eb9e0ede0ec0a2b41aa278fb3a8 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/CaptureUpdateMessage.java @@ -0,0 +1,55 @@ +/** +* 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.app.screenshare.server.messages; + +public class CaptureUpdateMessage { + private final String room; + private final int position; + private final byte[] videoData; + private final boolean isKeyFrame; + private final int sequenceNum; + + public CaptureUpdateMessage(String room, int position, byte[] videoData, boolean isKeyFrame, int sequenceNum) { + this.room = room; + this.position = position; + this.videoData = videoData; + this.isKeyFrame = isKeyFrame; + this.sequenceNum = sequenceNum; + } + + public String getRoom() { + return room; + } + + public int getPosition() { + return position; + } + + public byte[] getVideoData() { + return videoData; + } + + public boolean isKeyFrame() { + return isKeyFrame; + } + + public int getSequenceNum() { + return sequenceNum; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/MouseLocationMessage.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/MouseLocationMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..3a394d072eadbf9e5e8e2314def2c9873f2a6003 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/messages/MouseLocationMessage.java @@ -0,0 +1,46 @@ +/** +* 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.app.screenshare.server.messages; + +import java.awt.Point; + +public class MouseLocationMessage { + + private String room; + private Point loc; + private final int sequenceNum; + + public MouseLocationMessage(String room, Point loc, int sequenceNum) { + this.room = room; + this.loc = loc; + this.sequenceNum = sequenceNum; + } + + public String getRoom() { + return room; + } + + public Point getLoc() { + return loc; + } + + public int getSequenceNum() { + return sequenceNum; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/EventRecorder.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/EventRecorder.java new file mode 100755 index 0000000000000000000000000000000000000000..ec4452c66ea6600f878cfd91355bd3283aed6aa2 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/EventRecorder.java @@ -0,0 +1,59 @@ +/** +* 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.app.screenshare.server.recorder; + +import java.util.concurrent.TimeUnit; + +import org.bigbluebutton.app.screenshare.server.recorder.event.AbstractDeskshareRecordEvent; +import org.bigbluebutton.app.screenshare.server.recorder.event.RecordEvent; +import org.bigbluebutton.app.screenshare.server.recorder.event.RecordStartedEvent; +import org.bigbluebutton.app.screenshare.server.recorder.event.RecordStoppedEvent; + +import redis.clients.jedis.Jedis; + +public class EventRecorder implements RecordStatusListener { + private static final String COLON=":"; + private String host; + private int port; + + public EventRecorder(String host, int port){ + this.host = host; + this.port = port; + } + + private Long genTimestamp() { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime()); + } + + private void record(String session, RecordEvent message) { + Jedis jedis = new Jedis(host, port); + Long msgid = jedis.incr("global:nextRecordedMsgId"); + jedis.hmset("recording" + COLON + session + COLON + msgid, message.toMap()); + jedis.rpush("meeting" + COLON + session + COLON + "recordings", msgid.toString()); + } + + @Override + public void notify(RecordEvent event) { + if ((event instanceof RecordStoppedEvent) || (event instanceof RecordStartedEvent)) { + event.setTimestamp(genTimestamp()); + event.setMeetingId(((AbstractDeskshareRecordEvent)event).getSession()); + record(((AbstractDeskshareRecordEvent)event).getSession(), event); + } + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordStatusListener.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordStatusListener.java new file mode 100755 index 0000000000000000000000000000000000000000..517426e07c65e8fd646cd8aa46b4ff92b443de84 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordStatusListener.java @@ -0,0 +1,25 @@ +/** +* 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.app.screenshare.server.recorder; + +import org.bigbluebutton.app.screenshare.server.recorder.event.RecordEvent; + +public interface RecordStatusListener { + void notify(RecordEvent event); +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordStatusListeners.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordStatusListeners.java new file mode 100755 index 0000000000000000000000000000000000000000..f2f5b15c70ba88661f5af5f561d91f59be8542ca --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordStatusListeners.java @@ -0,0 +1,42 @@ +/** +* 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.app.screenshare.server.recorder; + +import java.util.HashSet; +import java.util.Set; + +import org.bigbluebutton.app.screenshare.server.recorder.event.RecordEvent; + +public class RecordStatusListeners { + private final Set<RecordStatusListener> listeners = new HashSet<RecordStatusListener>(); + + public void addListener(RecordStatusListener l) { + listeners.add(l); + } + + public void removeListener(RecordStatusListener l) { + listeners.remove(l); + } + + public void notifyListeners(RecordEvent event) { + for (RecordStatusListener listener: listeners) { + listener.notify(event); + } + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/Recorder.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/Recorder.java new file mode 100755 index 0000000000000000000000000000000000000000..b98edc86517f2ccad2b7a2fb519709e9e25ba1b9 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/Recorder.java @@ -0,0 +1,29 @@ +/** +* 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.app.screenshare.server.recorder; + +import org.apache.mina.core.buffer.IoBuffer; + +public interface Recorder { + public void record(IoBuffer frame); + public void start(); + public void stop(); + public void addListener(RecordStatusListener l); + public void removeListener(RecordStatusListener l); +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordingService.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordingService.java new file mode 100755 index 0000000000000000000000000000000000000000..3f464f4a0211e0b176d9931be93d1bedf404e69d --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/RecordingService.java @@ -0,0 +1,28 @@ +/** +* 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.app.screenshare.server.recorder; + +public interface RecordingService { + /** + * Get a recorder for a particular stream + * @param name the name of the stream + * @return the recorder for the stream + */ + Recorder getRecorderFor(String name); +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/AbstractDeskshareRecordEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/AbstractDeskshareRecordEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..74ff65fd82d6838133b99c23a08e11fb3b5a5cd2 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/AbstractDeskshareRecordEvent.java @@ -0,0 +1,33 @@ +/** +* 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.app.screenshare.server.recorder.event; + +public class AbstractDeskshareRecordEvent extends RecordEvent { + + private String session; + + public AbstractDeskshareRecordEvent(String session) { + setModule("Deskshare"); + this.session = session; + } + + public String getSession() { + return session; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordErrorEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordErrorEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..9fcdf4e09405eb495a49cc89f3a35aeae74f8f11 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordErrorEvent.java @@ -0,0 +1,37 @@ +/** +* 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.app.screenshare.server.recorder.event; + +public class RecordErrorEvent extends AbstractDeskshareRecordEvent { + + private String reason; + + public RecordErrorEvent(String session) { + super(session); + } + + public void setReason(String reason) { + this.reason = reason; + } + + public String getReason() { + return reason; + } + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..653552f51ac1a82c5a9341bb21ce244093ca2b82 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordEvent.java @@ -0,0 +1,81 @@ +/** +* 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.app.screenshare.server.recorder.event; + +import java.util.HashMap; + +/** + * Abstract class for all events that need to be recorded. + * @author Richard Alam + * + */ +public abstract class RecordEvent { + protected final HashMap<String, String> eventMap = new HashMap<String, String>(); + + protected final static String MODULE = "module"; + protected final static String TIMESTAMP = "timestamp"; + protected final static String MEETING = "meetingId"; + protected final static String EVENT = "eventName"; + + /** + * Set the module that generated the event. + * @param module + */ + public final void setModule(String module) { + eventMap.put(MODULE, module); + } + + /** + * Set the timestamp of the event. + * @param timestamp + */ + public final void setTimestamp(long timestamp) { + eventMap.put(TIMESTAMP, Long.toString(timestamp)); + } + + /** + * Set the meetingId for this particular event. + * @param meetingId + */ + public final void setMeetingId(String meetingId) { + eventMap.put(MEETING, meetingId); + } + + /** + * Set the name of the event. + * @param event + */ + public final void setEvent(String event) { + eventMap.put(EVENT, event); + } + + + /** + * Convert the event into a Map to be recorded. + * @return + */ + public final HashMap<String, String> toMap() { + return eventMap; + } + + @Override + public String toString() { + return eventMap.get(MODULE) + " " + eventMap.get(EVENT); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordStartedEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordStartedEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..41d6c26a44b727761872a2c89afec3be6015dab9 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordStartedEvent.java @@ -0,0 +1,31 @@ +/** +* 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.app.screenshare.server.recorder.event; + +public class RecordStartedEvent extends AbstractDeskshareRecordEvent { + + public RecordStartedEvent(String session) { + super(session); + setEvent("DeskshareStartedEvent"); + } + + public void setFile(String path) { + eventMap.put("file", path); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordStoppedEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordStoppedEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..18a856c2e5791a18cfaf1d87270ce42554dc2eec --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordStoppedEvent.java @@ -0,0 +1,31 @@ +/** +* 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.app.screenshare.server.recorder.event; + +public class RecordStoppedEvent extends AbstractDeskshareRecordEvent { + + public RecordStoppedEvent(String session) { + super(session); + setEvent("DeskshareStoppedEvent"); + } + + public void setFile(String path) { + eventMap.put("file", path); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordUpdateEvent.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordUpdateEvent.java new file mode 100755 index 0000000000000000000000000000000000000000..855af46cfa5c590a501c878d752fefe231cd905c --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/recorder/event/RecordUpdateEvent.java @@ -0,0 +1,25 @@ +/** +* 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.app.screenshare.server.recorder.event; + +public class RecordUpdateEvent extends AbstractDeskshareRecordEvent { + public RecordUpdateEvent(String session) { + super(session); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/servlet/HttpTunnelStreamController.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/servlet/HttpTunnelStreamController.java new file mode 100755 index 0000000000000000000000000000000000000000..0f6655b8bd1c12121a409992c0ab679b0d6dbed4 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/servlet/HttpTunnelStreamController.java @@ -0,0 +1,135 @@ +/** +* 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.app.screenshare.server.servlet; + +import java.util.*; +import java.awt.Point; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.bigbluebutton.app.screenshare.IScreenShareApplication; +import org.bigbluebutton.app.screenshare.server.session.Dimension; +import org.bigbluebutton.app.screenshare.server.session.ISessionManagerGateway; +import org.bigbluebutton.app.screenshare.server.socket.BlockStreamEventMessageHandler; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +public class HttpTunnelStreamController extends MultiActionController { + final private Logger log = Red5LoggerFactory.getLogger(HttpTunnelStreamController.class, "screenshare"); + + private boolean hasSessionManager = false; + private IScreenShareApplication screenShareApplication; + + public ModelAndView screenCaptureHandler(HttpServletRequest request, HttpServletResponse response) throws Exception { + String event = request.getParameterValues("event")[0]; + int captureRequest = Integer.parseInt(event); + + if (0 == captureRequest) { + handleCaptureStartRequest(request, response); + response.setStatus(HttpServletResponse.SC_OK); + } else if (1 == captureRequest) { + handleCaptureUpdateRequest(request, response); + response.setStatus(HttpServletResponse.SC_OK); + if (isSharingStopped(request, response)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } else if (2 == captureRequest) { + handleCaptureEndRequest(request, response); + response.setStatus(HttpServletResponse.SC_OK); + } else { + log.warn("Cannot handle screen capture event " + captureRequest); + response.setStatus(HttpServletResponse.SC_OK); + } + return null; + } + + private Boolean isSharingStopped(HttpServletRequest request, HttpServletResponse response) throws Exception { + String meetingId = request.getParameterValues("meetingId")[0]; + String streamId = request.getParameterValues("streamId")[0]; + boolean stopped = screenShareApplication.isSharingStopped(meetingId, streamId); + if (stopped) { + log.info("Screensharing for stream={} has stopped.", streamId); + } + + return stopped; + } + + private void handleCaptureStartRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String meetingId = request.getParameterValues("meetingId")[0]; + String streamId = request.getParameterValues("streamId")[0]; + String screenInfo = request.getParameterValues("screenInfo")[0]; + + String[] screen = screenInfo.split("x"); + + if (! hasSessionManager) { + screenShareApplication = getScreenShareApplication(); + hasSessionManager = true; + } + screenShareApplication.sharingStarted(meetingId, streamId, Integer.parseInt(screen[0]), Integer.parseInt(screen[1])); + } + + private void handleCaptureUpdateRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String meetingId = request.getParameterValues("meetingId")[0]; + String streamId = request.getParameterValues("streamId")[0]; + + log.debug("Received stream update message for meetingId={} streamId={}", meetingId, streamId); + + if (! hasSessionManager) { + screenShareApplication = getScreenShareApplication(); + hasSessionManager = true; + } + + screenShareApplication.updateShareStatus(meetingId, streamId, 0); + + } + + private void handleCaptureEndRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String meetingId = request.getParameterValues("meetingId")[0]; + String streamId = request.getParameterValues("streamId")[0]; + + if (! hasSessionManager) { + screenShareApplication = getScreenShareApplication(); + hasSessionManager = true; + } + System.out.println("HttpTunnel: Received Capture Enfd Event."); + screenShareApplication.sharingStopped(meetingId, streamId); + } + + private IScreenShareApplication getScreenShareApplication() { + //Get the servlet context + ServletContext ctx = getServletContext(); + //Grab a reference to the application context + ApplicationContext appCtx = (ApplicationContext) ctx.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + + //Get the bean holding the parameter + IScreenShareApplication manager = (IScreenShareApplication) appCtx.getBean("screenShareApplication"); + if (manager != null) { + log.debug("Got the IScreenShareApplication context: *****"); + } + return manager; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/servlet/JnlpConfigurator.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/servlet/JnlpConfigurator.java new file mode 100755 index 0000000000000000000000000000000000000000..755ba59cdc99638fb14fae5bcdf771dc190ccd9f --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/servlet/JnlpConfigurator.java @@ -0,0 +1,48 @@ +package org.bigbluebutton.app.screenshare.server.servlet; + +import org.bigbluebutton.app.screenshare.IScreenShareApplication; +import org.bigbluebutton.app.screenshare.ScreenShareInfo; +import org.bigbluebutton.app.screenshare.ScreenShareInfoResponse; + +public class JnlpConfigurator { + + private String jnlpUrl; + private IScreenShareApplication screenShareApplication; + private String streamBaseUrl; + private String codecOptions; + + + public String getJnlpUrl() { + return jnlpUrl; + } + + public void setJnlpUrl(String url) { + this.jnlpUrl = url; + } + + public void setStreamBaseUrl(String baseUrl) { + streamBaseUrl = baseUrl; + } + + public String getStreamBaseUrl() { + return streamBaseUrl; + } + + public void setCodecOptions(String codeOptions) { + this.codecOptions = codeOptions; + } + + public String getCodecOptions() { + return codecOptions; + } + + public ScreenShareInfo getScreenShareInfo(String meetingId, String token) { + ScreenShareInfoResponse resp = screenShareApplication.getScreenShareInfo(meetingId, token); + if (resp.error != null) return null; + else return resp.info; + } + + public void setApplication(IScreenShareApplication screenShareApplication) { + this.screenShareApplication = screenShareApplication; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/session/Dimension.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/session/Dimension.java new file mode 100755 index 0000000000000000000000000000000000000000..38e9236cb2fb74bcc7285969debabc3db360d690 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/session/Dimension.java @@ -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.app.screenshare.server.session; + +public final class Dimension { + + private final int width; + private final int height; + + public Dimension(int width, int height) { + this.width = width; + this.height = height; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/session/ISessionManagerGateway.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/session/ISessionManagerGateway.java new file mode 100755 index 0000000000000000000000000000000000000000..e33b21fe9cf442bfa5577c4e5d980b93bdd47d6b --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/session/ISessionManagerGateway.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.app.screenshare.server.session; + + +/** + * Interface between Java -> Scala + * @author Richard Alam + * + */ +public interface ISessionManagerGateway { + public void createSession(String streamId); + + public void removeSession(String streamId, int seqNum); + + public void updateSession(String streamId, int seqNum); + + public boolean isSharingStopped(String meetingId); +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/BlockStreamEventMessageHandler.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/BlockStreamEventMessageHandler.java new file mode 100755 index 0000000000000000000000000000000000000000..464c94c63f1e4e73c49050d1439cb89109f7d7c7 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/BlockStreamEventMessageHandler.java @@ -0,0 +1,118 @@ +/** +* 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.app.screenshare.server.socket; + +import org.apache.mina.core.future.CloseFuture; +import org.bigbluebutton.app.screenshare.IScreenShareApplication; +import org.bigbluebutton.app.screenshare.server.messages.CaptureEndMessage; +import org.bigbluebutton.app.screenshare.server.messages.CaptureStartMessage; +import org.bigbluebutton.app.screenshare.server.messages.CaptureUpdateMessage; +import org.bigbluebutton.app.screenshare.server.messages.MouseLocationMessage; +import org.apache.mina.core.service.IoHandlerAdapter; +import org.apache.mina.core.session.IdleStatus; +import org.apache.mina.core.session.IoSession; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +public class BlockStreamEventMessageHandler extends IoHandlerAdapter { + final private Logger log = Red5LoggerFactory.getLogger(BlockStreamEventMessageHandler.class, "screenshare"); + + private IScreenShareApplication app; + private static final String ROOM = "ROOM"; + + @Override + public void exceptionCaught( IoSession session, Throwable cause ) throws Exception { + log.warn(cause.toString() + " \n " + cause.getMessage()); + cause.printStackTrace(); + closeSession(session); + } + + private void closeSession(IoSession session) { + String room = (String)session.getAttribute(ROOM, null); + if (room != null) { + log.info("Closing session [" + room + "]. "); + } else { + log.info("Cannot determine session to close."); + } + CloseFuture future = session.close(true); + } + + @Override + public void messageReceived( IoSession session, Object message ) throws Exception + { + if (message instanceof CaptureStartMessage) { + System.out.println("Got CaptureStartBlockEvent"); + CaptureStartMessage event = (CaptureStartMessage) message; +// sessionManager.createSession(event.getRoom()); + } else if (message instanceof CaptureUpdateMessage) { +// System.out.println("Got CaptureUpdateBlockEvent"); + CaptureUpdateMessage event = (CaptureUpdateMessage) message; +// sessionManager.updateBlock(event.getRoom(), event.getSequenceNum()); + if (app.isSharingStopped(event.getRoom(), event.getRoom())) { + // The flash client told us to stop sharing. Force stopping by closing connection from applet. + // We're changing how to tell the applet to stop sharing as AS ExternalInterface to JS to Applet calls + // generates a popup dialog that users may or may not see causing the browser to hang. (ralam aug 24, 2014) + log.info("Sharing has stopped for meeting [" + event.getRoom() + "]. Closing connection."); + session.close(true); + } + } else if (message instanceof CaptureEndMessage) { + CaptureEndMessage event = (CaptureEndMessage) message; +// sessionManager.removeSession(event.getRoom(), event.getSequenceNum()); + } else if (message instanceof MouseLocationMessage) { + MouseLocationMessage event = (MouseLocationMessage) message; +// sessionManager.updateMouseLocation(event.getRoom(), event.getLoc(), event.getSequenceNum()); + } + } + + @Override + public void sessionIdle( IoSession session, IdleStatus status ) throws Exception + { + log.debug( "IDLE " + session.getIdleCount( status )); + super.sessionIdle(session, status); + } + + @Override + public void sessionCreated(IoSession session) throws Exception { + log.debug("Session Created"); + super.sessionCreated(session); + } + + @Override + public void sessionOpened(IoSession session) throws Exception { + log.debug("Session Opened."); + super.sessionOpened(session); + } + + @Override + public void sessionClosed(IoSession session) throws Exception { + log.debug("Session Closed."); + + String room = (String) session.getAttribute("ROOM"); + if (room != null) { + log.debug("Session Closed for room " + room); + app.sharingStopped(room, room); + } else { + log.warn("Closing session for a NULL room"); + } + } + + public void setApplication(IScreenShareApplication app) { + this.app = app; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/BlockStreamProtocolDecoder.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/BlockStreamProtocolDecoder.java new file mode 100755 index 0000000000000000000000000000000000000000..9b3e0d2bcd5e9ace1fe119cefda55148b46cafeb --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/BlockStreamProtocolDecoder.java @@ -0,0 +1,283 @@ +/** +* 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.app.screenshare.server.socket; + +import java.awt.Point; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.util.Arrays; + +import org.apache.mina.core.buffer.IoBuffer; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.CumulativeProtocolDecoder; +import org.apache.mina.filter.codec.ProtocolDecoderOutput; +import org.bigbluebutton.app.screenshare.server.messages.CaptureEndMessage; +import org.bigbluebutton.app.screenshare.server.messages.CaptureStartMessage; +import org.bigbluebutton.app.screenshare.server.messages.CaptureUpdateMessage; +import org.bigbluebutton.app.screenshare.server.messages.MouseLocationMessage; +import org.bigbluebutton.app.screenshare.server.session.Dimension; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + +public class BlockStreamProtocolDecoder extends CumulativeProtocolDecoder { + final private Logger log = Red5LoggerFactory.getLogger(BlockStreamProtocolDecoder.class, "screenshare"); + + private static final String ROOM = "ROOM"; + private static final byte[] POLICY_REQUEST = new byte[] {'<','p','o','l','i','c','y','-','f','i','l','e','-','r','e','q','u','e','s','t','/','>',0}; + private static final byte[] END_FRAME = new byte[] {'S', 'S', '-', 'E', 'N', 'D'}; + private static final byte[] HEADER = new byte[] {'B', 'B', 'B', '-', 'S', 'S'}; + private static final byte CAPTURE_START_EVENT = 0; + private static final byte CAPTURE_UPDATE_EVENT = 1; + private static final byte CAPTURE_END_EVENT = 2; + private static final byte MOUSE_LOCATION_EVENT = 3; + + protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { + // Remember the initial position. + int start = in.position(); + byte[] endFrame = new byte[END_FRAME.length]; + + // Now find the END FRAME delimeter in the buffer. + int curpos = 0; + while (in.remaining() >= END_FRAME.length) { + curpos = in.position(); + in.get(endFrame); + + if (Arrays.equals(endFrame, END_FRAME)) { + //log.debug("***** END FRAME {} = {}", endFrame, END_FRAME); + // Remember the current position and limit. + int position = in.position(); + int limit = in.limit(); + try { + in.position(start); + in.limit(position); + // The bytes between in.position() and in.limit() + // now contain a full frame. + parseFrame(session, in.slice(), out); + } finally { + // Set the position to point right after the + // detected END FRAME and set the limit to the old + // one. + in.position(position); + in.limit(limit); + } + return true; + } + + in.position(curpos+1); + } + + // Try to find a policy request, used by the BigBlueButton Client Checker + in.position(start); + if (tryToParsePolicyRequest(session, in, out)) { + return true; + } + + // Could not find END FRAME in the buffer. Reset the initial + // position to the one we recorded above. + in.position(start); + + return false; + } + + private boolean tryToParsePolicyRequest(IoSession session, IoBuffer in, ProtocolDecoderOutput out) { + byte[] message = new byte[POLICY_REQUEST.length]; + if (in.remaining() >= POLICY_REQUEST.length) { + in.get(message, 0, message.length); + if (Arrays.equals(message, POLICY_REQUEST)) { + log.debug("Sending cross domain policy to the user"); + IoBuffer buffer = IoBuffer.allocate(8); + buffer.setAutoExpand(true); + try { + buffer.putString("<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>", Charset.forName("UTF-8").newEncoder()); + buffer.put((byte) 0); + } catch (CharacterCodingException e) { + e.printStackTrace(); + return false; + } + buffer.flip(); + session.write(buffer); + return true; + } + } + + return false; + } + + private void parseFrame(IoSession session, IoBuffer in, ProtocolDecoderOutput out) { + //log.debug("Frame = {}", in.toString()); + try { + byte[] header = new byte[HEADER.length]; + + in.get(header, 0, HEADER.length); + + if (! Arrays.equals(header, HEADER)) { + log.info("Invalid header. Discarding. {}", header); + return; + } + + int messageLength = in.getInt(); + + if (in.remaining() < messageLength) { + log.info("Invalid length. Discarding. [{} < {}]", in.remaining(), messageLength); + return; + } + + decodeMessage(session, in, out); + + return; + } catch (Exception e) { + log.warn("Failed to parse frame. Discarding."); + } + } + + private void decodeMessage(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { + byte event = in.get(); + switch (event) { + case CAPTURE_START_EVENT: + log.info("Decoding CAPTURE_START_EVENT"); + decodeCaptureStartEvent(session, in, out); + break; + case CAPTURE_UPDATE_EVENT: + //log.info("Decoding CAPTURE_UPDATE_EVENT"); + decodeCaptureUpdateEvent(session, in, out); + break; + case CAPTURE_END_EVENT: + log.info("Got CAPTURE_END_EVENT event: " + event); + decodeCaptureEndEvent(session, in, out); + break; + case MOUSE_LOCATION_EVENT: + decodeMouseLocationEvent(session, in, out); + break; + default: + log.error("Unknown event: " + event); + throw new Exception("Unknown event: " + event); + } + } + + private void decodeMouseLocationEvent(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { + String room = decodeRoom(session, in); + if ("".equals(room)) { + log.warn("Empty meeting name in decoding mouse location."); + throw new Exception("Empty meeting name in decoding mouse location."); + } + + int seqNum = in.getInt(); + int mouseX = in.getInt(); + int mouseY = in.getInt(); + + /** Swallow end frame **/ + in.get(new byte[END_FRAME.length]); + + MouseLocationMessage event = new MouseLocationMessage(room, new Point(mouseX, mouseY), seqNum); + out.write(event); + } + + private void decodeCaptureEndEvent(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { + String room = decodeRoom(session, in); + if ("".equals(room)) { + log.warn("Empty meeting name in decoding capture end event."); + throw new Exception("Empty meeting name in decoding capture end event."); + } + + log.info("CaptureEndEvent for " + room); + int seqNum = in.getInt(); + + /** Swallow end frame **/ + in.get(new byte[END_FRAME.length]); + + CaptureEndMessage event = new CaptureEndMessage(room, seqNum); + out.write(event); + } + + private void decodeCaptureStartEvent(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { + String room = decodeRoom(session, in); + if ("".equals(room)) { + log.warn("Empty meeting name in decoding capture start event."); + throw new Exception("Empty meeting name in decoding capture start event."); + } + + session.setAttribute(ROOM, room); + int seqNum = in.getInt(); + + Dimension blockDim = decodeDimension(in); + Dimension screenDim = decodeDimension(in); + + boolean useSVC2 = (in.get() == 1); + + /** Swallow end frame **/ + in.get(new byte[END_FRAME.length]); + + log.info("CaptureStartEvent for " + room); + CaptureStartMessage event = new CaptureStartMessage(room, screenDim, blockDim, seqNum, useSVC2); + out.write(event); + } + + private Dimension decodeDimension(IoBuffer in) { + int width = in.getInt(); + int height = in.getInt(); + return new Dimension(width, height); + } + + private String decodeRoom(IoSession session, IoBuffer in) { + int roomLength = in.get(); +// System.out.println("Room length = " + roomLength); + String room = ""; + try { + room = in.getString(roomLength, Charset.forName( "UTF-8" ).newDecoder()); + if (session.containsAttribute(ROOM)) { + String attRoom = (String) session.getAttribute(ROOM); + if (!attRoom.equals(room)) { + log.warn(room + " is not the same as room in attribute [" + attRoom + "]"); + } + } + } catch (CharacterCodingException e) { + log.error(e.getMessage()); + } + + return room; + } + + private void decodeCaptureUpdateEvent(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { + String room = decodeRoom(session, in); + if ("".equals(room)) { + log.warn("Empty meeting name in decoding capture start event."); + throw new Exception("Empty meeting name in decoding capture start event."); + } + + int seqNum = in.getInt(); + int numBlocks = in.getShort(); + + String blocksStr = "Blocks changed "; + + for (int i = 0; i < numBlocks; i++) { + int position = in.getShort(); + blocksStr += " " + position; + + boolean isKeyFrame = (in.get() == 1) ? true : false; + int length = in.getInt(); + byte[] data = new byte[length]; + in.get(data, 0, length); + CaptureUpdateMessage event = new CaptureUpdateMessage(room, position, data, isKeyFrame, seqNum); + out.write(event); + } + + /** Swallow end frame **/ + in.get(new byte[END_FRAME.length]); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/DeskShareServer.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/DeskShareServer.java new file mode 100755 index 0000000000000000000000000000000000000000..ead02c9075fe1f7cecbbdd9c896fc94005c47ce5 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/DeskShareServer.java @@ -0,0 +1,67 @@ +/** +* 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.app.screenshare.server.socket; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import org.apache.mina.core.service.IoHandlerAdapter; +import org.apache.mina.core.session.IdleStatus; +import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.transport.socket.nio.NioSocketAcceptor; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; + + +public class DeskShareServer { + final private Logger log = Red5LoggerFactory.getLogger(DeskShareServer.class, "deskshare"); + + private int port = 1270; + + private IoHandlerAdapter screenCaptureHandler; + private NioSocketAcceptor acceptor; + + public void start() + { + acceptor = new NioSocketAcceptor(); + acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter(new ScreenCaptureProtocolCodecFactory())); + + acceptor.setHandler( screenCaptureHandler); + acceptor.getSessionConfig().setIdleTime( IdleStatus.BOTH_IDLE, 10 ); + acceptor.setReuseAddress(true); + try { + acceptor.bind( new InetSocketAddress(port) ); + } catch (IOException e) { + log.error("IOException while binding to port {}", port); + } + } + + public void setScreenCaptureHandler(IoHandlerAdapter screenCaptureHandler) { + this.screenCaptureHandler = screenCaptureHandler; + } + + public void stop() { + acceptor.unbind(); + acceptor.dispose(); + } + + public void setPort(int port) { + this.port = port; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/NullProtocolEncoder.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/NullProtocolEncoder.java new file mode 100755 index 0000000000000000000000000000000000000000..0d5c81271745407c9818cc72c3be0dc8c0f4c1bc --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/NullProtocolEncoder.java @@ -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 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.app.screenshare.server.socket; + +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.ProtocolEncoder; +import org.apache.mina.filter.codec.ProtocolEncoderOutput; + +public class NullProtocolEncoder implements ProtocolEncoder { + + public void dispose(IoSession in) throws Exception { + // TODO Auto-generated method stub + + } + + public void encode(IoSession session, Object message, ProtocolEncoderOutput out) + throws Exception { + // TODO Auto-generated method stub + + } + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/ScreenCaptureProtocolCodecFactory.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/ScreenCaptureProtocolCodecFactory.java new file mode 100755 index 0000000000000000000000000000000000000000..a8badc8b686ecda75acc605c1bb1cd060473b527 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/socket/ScreenCaptureProtocolCodecFactory.java @@ -0,0 +1,42 @@ +/** +* 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.app.screenshare.server.socket; + +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.ProtocolCodecFactory; +import org.apache.mina.filter.codec.ProtocolDecoder; +import org.apache.mina.filter.codec.ProtocolEncoder; + +public class ScreenCaptureProtocolCodecFactory implements ProtocolCodecFactory { + private ProtocolEncoder encoder; + private ProtocolDecoder decoder; + + public ScreenCaptureProtocolCodecFactory() { + encoder = new NullProtocolEncoder(); + decoder = new BlockStreamProtocolDecoder(); + } + + public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception { + return encoder; + } + + public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception { + return decoder; + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/util/StackTraceUtil.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/util/StackTraceUtil.java new file mode 100755 index 0000000000000000000000000000000000000000..deccbd60730c669ae4e4729fd8973356765318a0 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/server/util/StackTraceUtil.java @@ -0,0 +1,32 @@ +/** +* 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.app.screenshare.server.util; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +public final class StackTraceUtil { + public static String getStackTrace(Throwable aThrowable) { + final Writer result = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(result); + aThrowable.printStackTrace(printWriter); + return result.toString(); + } +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/IDataStore.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/IDataStore.java new file mode 100755 index 0000000000000000000000000000000000000000..0d2bd61f9f5008e91e36eb453a83c52829e6a557 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/IDataStore.java @@ -0,0 +1,5 @@ +package org.bigbluebutton.app.screenshare.store; + +public interface IDataStore { + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/redis/IScreenShareData.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/redis/IScreenShareData.java new file mode 100755 index 0000000000000000000000000000000000000000..da147d0a76400a50777f255d9c5eff15eaf2a987 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/redis/IScreenShareData.java @@ -0,0 +1,5 @@ +package org.bigbluebutton.app.screenshare.store.redis; + +public interface IScreenShareData { + +} diff --git a/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/redis/RedisDataStore.java b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/redis/RedisDataStore.java new file mode 100755 index 0000000000000000000000000000000000000000..e1900231febfe63631debd14a2c6a61702f4e220 --- /dev/null +++ b/bbb-screenshare/app/src/main/java/org/bigbluebutton/app/screenshare/store/redis/RedisDataStore.java @@ -0,0 +1,79 @@ +package org.bigbluebutton.app.screenshare.store.redis; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import org.red5.logging.Red5LoggerFactory; +import org.slf4j.Logger; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +public class RedisDataStore { + private static Logger log = Red5LoggerFactory.getLogger(RedisDataStore.class, "screenshare"); + + private JedisPool redisPool; + private volatile boolean sendMessage = false; + private int maxThreshold = 1024; + private final Executor msgSenderExec = Executors.newSingleThreadExecutor(); + private BlockingQueue<IScreenShareData> dataToStore = new LinkedBlockingQueue<IScreenShareData>(); + private final Executor runExec = Executors.newSingleThreadExecutor(); + + public void stop() { + sendMessage = false; + } + + public void start() { + try { + sendMessage = true; + + Runnable messageSender = new Runnable() { + public void run() { + while (sendMessage) { + try { + IScreenShareData data = dataToStore.take(); + storeData(data); + } catch (InterruptedException e) { + log.warn("Failed to get data from queue."); + } + } + } + }; + msgSenderExec.execute(messageSender); + } catch (Exception e) { + log.error("Error storing data into redis: " + e.getMessage()); + } + } + + public void store(IScreenShareData data) { + if (dataToStore.size() > maxThreshold) { + log.warn("Queued number of data [{}] is greater than threshold [{}]", dataToStore.size(), maxThreshold); + } + dataToStore.add(data); + } + + private void storeData(IScreenShareData data) { + Runnable task = new Runnable() { + public void run() { + Jedis jedis = redisPool.getResource(); + try { + // jedis.publish(channel, message); + } catch(Exception e){ + log.warn("Cannot publish the message to redis", e); + } finally { + redisPool.returnResource(jedis); + } + } + }; + + runExec.execute(task); + } + + public void setRedisPool(JedisPool redisPool){ + this.redisPool = redisPool; + } + + public void setMaxThreshold(int threshold) { + maxThreshold = threshold; + } +} diff --git a/bbb-screenshare/app/src/main/resources/logback-screenshare.xml b/bbb-screenshare/app/src/main/resources/logback-screenshare.xml new file mode 100755 index 0000000000000000000000000000000000000000..87637bd81f6a557d47751483b7ce829a9cb7465e --- /dev/null +++ b/bbb-screenshare/app/src/main/resources/logback-screenshare.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <appender name="screenshare" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <File>log/screenshare-slf.log</File> + + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <FileNamePattern>log/screenshare-slf.%d{yyyy-MM-dd}.log</FileNamePattern> + <!-- keep 30 days worth of history --> + <MaxHistory>30</MaxHistory> + </rollingPolicy> + + <encoder> + <charset>UTF-8</charset> + <pattern>%d{ISO8601} [%thread] %-5level %logger{35} - %msg%n</pattern> + </encoder> + </appender> + + <root> + <level value="DEBUG" /> + <appender-ref ref="screenshare" /> + </root> + + <!-- LEVEL CAN NOT BE DEBUG --> + <logger name="org.apache" level="INFO"></logger> +</configuration> diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/ScreenShareApplication.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/ScreenShareApplication.scala new file mode 100755 index 0000000000000000000000000000000000000000..a53495e3f71499c2c7b90c6376cad02881bfefb1 --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/ScreenShareApplication.scala @@ -0,0 +1,183 @@ +package org.bigbluebutton.app.screenshare + +import org.bigbluebutton.app.screenshare.events.IEventsMessageBus +import org.bigbluebutton.app.screenshare.server.sessions.ScreenshareSessionManager +import org.bigbluebutton.app.screenshare.server.sessions.messages._ +import org.bigbluebutton.app.screenshare.server.util.LogHelper + +class ScreenShareApplication(val bus: IEventsMessageBus, val jnlpFile: String, + val streamBaseUrl: String) + extends IScreenShareApplication with LogHelper { + + val sessionManager: ScreenshareSessionManager = new ScreenshareSessionManager(bus) + sessionManager.start + + val initError: Error = new Error("Uninitialized error.") + + def userDisconnected(meetingId: String, userId: String) { + if (logger.isDebugEnabled()) { + logger.debug("Received user disconnected on meeting=" + meetingId + + "] userid=[" + userId + "]") + } + + sessionManager ! new UserDisconnected(meetingId, userId) + } + + + def isScreenSharing(meetingId: String):IsScreenSharingResponse = { + if (logger.isDebugEnabled()) { + logger.debug("Received is screen sharing on meeting=" + meetingId + + "]") + } + + var response: IsScreenSharingResponse = new IsScreenSharingResponse(null, initError) + sessionManager !? (3000, IsScreenSharing(meetingId)) match { + case None => { + logger.info("Failed to get response to is screen sharing request on meeting=" + meetingId + "]") + val info = new StreamInfo(false, "none", 0, 0, "none") + response = new IsScreenSharingResponse(info, new Error("Timedout waiting for response")) + } + case Some(rep) => { + val reply = rep.asInstanceOf[IsScreenSharingReply] + val info = new StreamInfo(true, reply.streamId, reply.width, reply.height, reply.url) + response = new IsScreenSharingResponse(info, null) + } + } + + response + } + + def getScreenShareInfo(meetingId: String, token: String):ScreenShareInfoResponse = { + if (logger.isDebugEnabled()) { + logger.debug("Received get screen sharing info on token=" + token + + "]") + } + + var response: ScreenShareInfoResponse = new ScreenShareInfoResponse(null, initError) + sessionManager !? (3000, ScreenShareInfoRequest(meetingId, token)) match { + case None => { + logger.info("Failed to get response to get screen sharing info request on token=" + token + "]") + response = new ScreenShareInfoResponse(null, new Error("Timedout waiting for response.")) + } + case Some(rep) => { + val reply = rep.asInstanceOf[ScreenShareInfoRequestReply] + val publishUrl = streamBaseUrl + "/" + meetingId + "/" + reply.streamId + val info = new ScreenShareInfo(publishUrl, reply.streamId) + response = new ScreenShareInfoResponse(info, null) + } + } + + response + } + + def recordStream(meetingId: String, streamId: String):java.lang.Boolean = { + if (logger.isDebugEnabled()) { + logger.debug("Received record stream request on stream=" + streamId + "]") + } + + var record = false + + sessionManager !? (3000, IsStreamRecorded(meetingId, streamId)) match { + case None => { + logger.info("Failed to get response to record stream request on streamId=" + + streamId + "]") + record = false + } + case Some(rep) => { + val reply = rep.asInstanceOf[IsStreamRecordedReply] + record = reply.record + } + } + + record + } + + def startShareRequest(meetingId: String, userId: String, record: java.lang.Boolean): StartShareRequestResponse = { + if (logger.isDebugEnabled()) { + logger.debug("Received start share request on meeting=" + meetingId + + "for user=" + userId + "]") + } + + var response: StartShareRequestResponse = new StartShareRequestResponse(null, null, initError) + + sessionManager !? (3000, StartShareRequestMessage(meetingId, userId, record)) match { + case None => { + logger.info("Failed to get response to start share request on meeting=" + + meetingId + " for user=" + userId + "]") + response = new StartShareRequestResponse(null, null, new Error("Timedout waiting for response")) + } + case Some(rep) => { + val reply = rep.asInstanceOf[StartShareRequestReplyMessage] + response = new StartShareRequestResponse(reply.token, jnlpFile, null) + } + } + + response + } + + def stopShareRequest(meetingId: String, streamId: String) { + if (logger.isDebugEnabled()) { + logger.debug("Received stop share request on meeting=[" + meetingId + + "] for stream=[" + streamId + "]") + } + sessionManager ! new StopShareRequestMessage(meetingId, streamId) + } + + def streamStarted(meetingId: String, streamId: String, url: String) { + if (logger.isDebugEnabled()) { + logger.debug("Received stream started on meeting=[" + meetingId + + "] for stream=[" + streamId + "]") + } + sessionManager ! new StreamStartedMessage(meetingId, streamId, url) + } + + def streamStopped(meetingId: String, streamId: String) { + if (logger.isDebugEnabled()) { + logger.debug("Received stream stopped on meeting=[" + meetingId + + "] for stream=[" + streamId + "]") + } + sessionManager ! new StreamStoppedMessage(meetingId, streamId) + } + + def sharingStarted(meetingId: String, streamId: String, width: java.lang.Integer, height: java.lang.Integer) { + if (logger.isDebugEnabled()) { + logger.debug("Received share started on meeting=[" + meetingId + + "] for stream=[" + streamId + "] with region=[" + width + "x" + height + "]") + } + sessionManager ! new SharingStartedMessage(meetingId, streamId, width, height) + } + + def sharingStopped(meetingId: String, streamId: String) { + if (logger.isDebugEnabled()) { + logger.debug("Received sharing stopped on meeting=" + meetingId + + "for stream=" + streamId + "]") + } + sessionManager ! new SharingStoppedMessage(meetingId, streamId) + } + + def updateShareStatus(meetingId: String, streamId : String, seqNum: java.lang.Integer) { + if (logger.isDebugEnabled()) { + logger.debug("Received sharing status on meeting=" + meetingId + + "for stream=" + streamId + "]") + } + sessionManager ! new UpdateShareStatus(meetingId, streamId, seqNum) + } + + def isSharingStopped(meetingId: String, streamId: String): java.lang.Boolean = { + if (logger.isDebugEnabled()) { + logger.debug("Received sharing status on meeting=" + meetingId + + "for stream=" + streamId + "]") + } + + var stopped = false + sessionManager !? (3000, IsSharingStopped(meetingId, streamId)) match { + case None => stopped = true + case Some(rep) => { + val reply = rep.asInstanceOf[IsSharingStoppedReply] + stopped = reply.stopped + } + } + + stopped + } +} \ No newline at end of file diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/MeetingActor.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/MeetingActor.scala new file mode 100755 index 0000000000000000000000000000000000000000..bedcb3e5f4ff6a73f85d3a27afc0cb9dee7f45b8 --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/MeetingActor.scala @@ -0,0 +1,252 @@ +package org.bigbluebutton.app.screenshare.server.sessions + +import scala.actors.Actor +import scala.actors.Actor._ +import scala.collection.mutable.HashMap +import org.bigbluebutton.app.screenshare.events.IEventsMessageBus +import org.bigbluebutton.app.screenshare.server.util._ +import org.bigbluebutton.app.screenshare.server.sessions.messages._ + + +class MeetingActor(val sessionManager: ScreenshareSessionManager, + val bus: IEventsMessageBus, + val meetingId: String) extends Actor with LogHelper { + + private val sessions = new HashMap[String, ScreenshareSession] + + private var lastHasSessionCheck:Long = TimeUtil.getCurrentMonoTime + + private var activeSession:Option[ScreenshareSession] = None + private var stopped = false + + private val IS_MEETING_RUNNING = "IsMeetingRunning" + + def scheduleIsMeetingRunningCheck() { + val mainActor = self + actor { + Thread.sleep(60000) + mainActor ! IS_MEETING_RUNNING + } + } + + def act() = { + loop { + react { + case msg: StartShareRequestMessage => handleStartShareRequestMessage(msg) + case msg: StopShareRequestMessage => handleStopShareRequestMessage(msg) + case msg: StreamStartedMessage => handleStreamStartedMessage(msg) + case msg: StreamStoppedMessage => handleStreamStoppedMessage(msg) + case msg: SharingStartedMessage => handleSharingStartedMessage(msg) + case msg: SharingStoppedMessage => handleSharingStoppedMessage(msg) + case msg: IsSharingStopped => handleIsSharingStopped(msg) + case msg: IsScreenSharing => handleIsScreenSharing(msg) + case msg: IsStreamRecorded => handleIsStreamRecorded(msg) + case msg: UpdateShareStatus => handleUpdateShareStatus(msg) + case msg: UserDisconnected => handleUserDisconnected(msg) + case msg: ScreenShareInfoRequest => handleScreenShareInfoRequest(msg) + case IS_MEETING_RUNNING => handleIsMeetingRunning() + case msg: KeepAliveTimeout => handleKeepAliveTimeout(msg) + case m: Any => logger.warn("Session: Unknown message [{}]", m) + } + } + } + + private def findSessionByUser(userId: String):Option[ScreenshareSession] = { + sessions.values find (su => su.userId == userId) + } + + private def findSessionWithToken(token: String):Option[ScreenshareSession] = { + sessions.values find (su => su.token == token) + } + + private def handleUserDisconnected(msg: UserDisconnected) { + if (logger.isDebugEnabled()) { + logger.debug("Received UserDisconnected for meetingId=[" + msg.meetingId + "]") + } + + findSessionByUser(msg.userId) foreach (s => s forward msg) + } + + private def handleIsScreenSharing(msg: IsScreenSharing) { + if (logger.isDebugEnabled()) { + logger.debug("Received IsScreenSharing for meetingId=[" + msg.meetingId + "]") + } + + activeSession foreach (s => s forward msg) + } + + private def handleScreenShareInfoRequest(msg: ScreenShareInfoRequest) { + if (logger.isDebugEnabled()) { + logger.debug("Received ScreenShareInfoRequest for token=[" + msg.token + "]") + } + + findSessionWithToken(msg.token) foreach (s => s forward msg) + } + + private def handleIsStreamRecorded(msg: IsStreamRecorded) { + if (logger.isDebugEnabled()) { + logger.debug("Received IsStreamRecorded for streamId=[" + msg.streamId + "]") + } + + sessions.get(msg.streamId) match { + case Some(session) => { + session forward msg + } + case None => { + logger.info("IsStreamRecorded on a non-existing session=[" + msg.streamId + "]") + } + } + } + + private def handleUpdateShareStatus(msg: UpdateShareStatus) { + if (logger.isDebugEnabled()) { + logger.debug("Received UpdateShareStatus for streamId=[" + msg.streamId + "]") + } + + sessions.get(msg.streamId) match { + case Some(session) => { + session forward msg + } + case None => { + logger.info("Sharing stopped on a non-existing session=[" + msg.streamId + "]") + } + } + } + + private def handleSharingStoppedMessage(msg: SharingStoppedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received SharingStoppedMessage for streamId=[" + msg.streamId + "]") + } + + sessions.get(msg.streamId) match { + case Some(session) => { + session forward msg + + } + case None => { + logger.info("Sharing stopped on a non-existing session=[" + msg.streamId + "]") + } + } + } + + private def handleSharingStartedMessage(msg: SharingStartedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received SharingStartedMessage for streamId=[" + msg.streamId + "]") + } + + sessions.get(msg.streamId) match { + case Some(session) => { + session forward msg + } + case None => { + logger.info("Sharing started on a non-existing session=[" + msg.streamId + "]") + } + } + } + + private def handleStreamStoppedMessage(msg: StreamStoppedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received StreamStoppedMessage for streamId=[" + msg.streamId + "]") + } + + sessions.get(msg.streamId) match { + case Some(session) => { + session forward msg + activeSession = None + } + case None => { + logger.info("Stream stopped on a non-existing session=[" + msg.streamId + "]") + } + } + } + + private def handleStreamStartedMessage(msg: StreamStartedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received StreamStartedMessage for streamId=[" + msg.streamId + "]") + } + + sessions.get(msg.streamId) match { + case Some(session) => { + session forward msg + activeSession = Some(session) + } + case None => { + logger.info("Stream started on a non-existing session=[" + msg.streamId + "]") + } + } + } + + private def handleStopShareRequestMessage(msg: StopShareRequestMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received StopShareRequestMessage for streamId=[" + msg.streamId + "]") + } + sessions.get(msg.streamId) match { + case Some(session) => { + session forward msg + } + case None => { + logger.info("Stop share request on a non-existing session=[" + msg.streamId + "]") + } + } + } + + private def handleStartShareRequestMessage(msg: StartShareRequestMessage) { + val token = RandomStringGenerator.randomAlphanumericString(16) + val streamId = msg.meetingId + "-" + System.currentTimeMillis(); + + val session: ScreenshareSession = new ScreenshareSession(this, bus, + meetingId, streamId, token, + msg.record, msg.userId) + sessions += streamId -> session + session.start + + session forward msg + + } + + private def handleIsSharingStopped(msg: IsSharingStopped) { + sessions.get(msg.streamId) match { + case Some(session) => { + session forward msg + } + case None => { + logger.info("Stream stopped on a non-existing session=[" + msg.streamId + "]") + } + } + } + + private def handleStopSession() { + stopped = true + } + + private def handleStartSession() { + stopped = false + scheduleIsMeetingRunningCheck + } + + private def handleIsMeetingRunning() { + // If not sessions in the last 5 minutes, then assume meeting has ended. + if (sessions.isEmpty) { + if (TimeUtil.getCurrentMonoTime - lastHasSessionCheck > 300000) { + sessionManager ! MeetingHasEnded(meetingId) + } else { + scheduleIsMeetingRunningCheck + } + } else { + lastHasSessionCheck = TimeUtil.getCurrentMonoTime + scheduleIsMeetingRunningCheck + } + } + + private def handleKeepAliveTimeout(msg: KeepAliveTimeout) { + sessions.remove(msg.streamId) foreach { s => + if (activeSession != None) { + activeSession foreach { as => + if (as.streamId == s.streamId) activeSession = None + } + } + } + } + + +} \ No newline at end of file diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/ScreenshareSession.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/ScreenshareSession.scala new file mode 100755 index 0000000000000000000000000000000000000000..78c68e915ee8706c936b5976d7ee0e65233fbd3a --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/ScreenshareSession.scala @@ -0,0 +1,205 @@ +/** +* 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.app.screenshare.server.sessions + + +import scala.actors.Actor +import scala.actors.Actor._ +import net.lag.logging.Logger +import org.bigbluebutton.app.screenshare.server.util.LogHelper +import org.bigbluebutton.app.screenshare.server.util.TimeUtil +import org.bigbluebutton.app.screenshare.server.sessions.messages._ +import org.bigbluebutton.app.screenshare.events.IEventsMessageBus +import org.bigbluebutton.app.screenshare.events.ShareStartedEvent +import org.bigbluebutton.app.screenshare.events.ShareStoppedEvent +import org.bigbluebutton.app.screenshare.events.StreamStoppedEvent +import org.bigbluebutton.app.screenshare.events.StreamStartedEvent + + +case object StartSession +case object StopSession +case class KeepAliveTimeout(streamId: String) + +class ScreenshareSession(parent: MeetingActor, + bus: IEventsMessageBus, + val meetingId: String, + val streamId: String, + val token: String, + val recorded: Boolean, + val userId: String) extends Actor with LogHelper { + + private var timeOfLastKeepAliveUpdate:Long = TimeUtil.getCurrentMonoTime + private val KEEP_ALIVE_TIMEOUT = 60000 + + // if ffmpeg is still broadcasting + private var streamStopped = true + // if jws is still running + private var shareStopped = true + + // if the user has requested to stop sharing + private var stopShareRequested = false + + private var width: Int = 0 + private var height: Int = 0 + + private var streamUrl: String = "" + + private var timestamp = 0L; + private var lastUpdate:Long = System.currentTimeMillis(); + + private val IS_STREAM_ALIVE = "IsStreamAlive" + + def scheduleKeepAliveCheck() { + val mainActor = self + actor { + Thread.sleep(5000) + mainActor ! IS_STREAM_ALIVE + } + } + + def act() = { + loop { + react { + case msg: StartShareRequestMessage => handleStartShareRequestMessage(msg) + case msg: StopShareRequestMessage => handleStopShareRequestMessage(msg) + case msg: StreamStartedMessage => handleStreamStartedMessage(msg) + case msg: StreamStoppedMessage => handleStreamStoppedMessage(msg) + case msg: SharingStartedMessage => handleSharingStartedMessage(msg) + case msg: SharingStoppedMessage => handleSharingStoppedMessage(msg) + case msg: IsSharingStopped => handleIsSharingStopped(msg) + case msg: IsScreenSharing => handleIsScreenSharing(msg) + case msg: IsStreamRecorded => handleIsStreamRecorded(msg) + case msg: UpdateShareStatus => handleUpdateShareStatus(msg) + case msg: UserDisconnected => handleUserDisconnected(msg) + case msg: ScreenShareInfoRequest => handleScreenShareInfoRequest(msg) + case IS_STREAM_ALIVE => checkIfStreamIsAlive() + case m: Any => logger.warn("Session: Unknown message [%s]", m) + } + } + } + + private def handleUserDisconnected(msg: UserDisconnected) { + if (logger.isDebugEnabled()) { + logger.debug("Received UserDisconnected for streamId=[" + streamId + "]") + } + + stopShareRequested = true + } + + private def handleIsStreamRecorded(msg: IsStreamRecorded) { + if (logger.isDebugEnabled()) { + logger.debug("Received IsStreamRecorded for streamId=[" + msg.streamId + "]") + } + + reply(new IsStreamRecordedReply(recorded)) + } + + private def handleIsScreenSharing(msg: IsScreenSharing) { + if (logger.isDebugEnabled()) { + logger.debug("Received IsScreenSharing for meetingId=[" + msg.meetingId + "]") + } + + reply(new IsScreenSharingReply(true, streamId, width, height, streamUrl)) + } + + private def handleScreenShareInfoRequest(msg: ScreenShareInfoRequest) { + if (logger.isDebugEnabled()) { + logger.debug("Received ScreenShareInfoRequest for token=" + msg.token + " streamId=[" + streamId + "]") + } + + reply(new ScreenShareInfoRequestReply(msg.meetingId, streamId)) + } + + private def handleSharingStoppedMessage(msg: SharingStoppedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received SharingStoppedMessage for streamId=[" + msg.streamId + "]") + } + + shareStopped = true + width = 0 + height = 0 + bus.send(new ShareStoppedEvent(meetingId, streamId)) + } + + private def handleSharingStartedMessage(msg: SharingStartedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received SharingStartedMessagefor streamId=[" + msg.streamId + "]") + } + + stopShareRequested = false + shareStopped = false + width = msg.width + height = msg.height + bus.send(new ShareStartedEvent(meetingId, streamId)) + } + + private def handleStreamStoppedMessage(msg: StreamStoppedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received StreamStoppedMessage streamId=[" + msg.streamId + "]") + } + + streamStopped = true + bus.send(new StreamStoppedEvent(meetingId, streamId)) + } + + private def handleStreamStartedMessage(msg: StreamStartedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received StreamStartedMessage for streamId=[" + msg.streamId + "]") + } + + streamStopped = false + streamUrl = msg.url + bus.send(new StreamStartedEvent(meetingId, streamId, width, height, msg.url)) + } + + private def handleStopShareRequestMessage(msg: StopShareRequestMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received StopShareRequestMessage for streamId=[" + msg.streamId + "]") + } + + stopShareRequested = true + bus.send(new ShareStoppedEvent(meetingId, streamId)) + } + + private def handleStartShareRequestMessage(msg: StartShareRequestMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received StartShareRequestMessage for streamId=[" + msg.meetingId + "]") + } + + scheduleKeepAliveCheck() + reply(new StartShareRequestReplyMessage(token)) + } + + private def handleIsSharingStopped(msg: IsSharingStopped) { + reply(new IsSharingStoppedReply(stopShareRequested)) + } + + private def handleUpdateShareStatus(msg: UpdateShareStatus): Unit = { + timeOfLastKeepAliveUpdate = TimeUtil.getCurrentMonoTime + } + + private def checkIfStreamIsAlive() { + if (TimeUtil.getCurrentMonoTime - timeOfLastKeepAliveUpdate > KEEP_ALIVE_TIMEOUT) { + logger.warn("Did not received updates for more than 1 minute. Removing stream {}", streamId) + parent ! new KeepAliveTimeout(streamId) + } else { + scheduleKeepAliveCheck() + } + } +} diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/ScreenshareSessionManager.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/ScreenshareSessionManager.scala new file mode 100755 index 0000000000000000000000000000000000000000..aabca4089f58bbd7ecbc116fff7397bbe3f34ce9 --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/ScreenshareSessionManager.scala @@ -0,0 +1,207 @@ +/** + * 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.app.screenshare.server.sessions + +import scala.actors.Actor +import scala.actors.Actor._ +import net.lag.logging.Logger +import scala.collection.mutable.HashMap +import org.bigbluebutton.app.screenshare.events.IEventsMessageBus +import org.bigbluebutton.app.screenshare.server.sessions.messages._ +import org.bigbluebutton.app.screenshare.server.util.LogHelper + + +case class HasScreenShareSession(meetingId: String) +case class HasScreenShareSessionReply(meetingId: String, sharing: Boolean, streamId:Option[String]) +case class MeetingHasEnded(meetingId: String) + +class ScreenshareSessionManager(val bus: IEventsMessageBus) + extends Actor with LogHelper { + + private val meetings = new HashMap[String, MeetingActor] + + def act() = { + loop { + react { + case msg: StartShareRequestMessage => handleStartShareRequestMessage(msg) + case msg: StopShareRequestMessage => handleStopShareRequestMessage(msg) + case msg: StreamStartedMessage => handleStreamStartedMessage(msg) + case msg: StreamStoppedMessage => handleStreamStoppedMessage(msg) + case msg: SharingStartedMessage => handleSharingStartedMessage(msg) + case msg: SharingStoppedMessage => handleSharingStoppedMessage(msg) + case msg: IsStreamRecorded => handleIsStreamRecorded(msg) + case msg: IsSharingStopped => handleIsSharingStopped(msg) + case msg: IsScreenSharing => handleIsScreenSharing(msg) + case msg: ScreenShareInfoRequest => handleScreenShareInfoRequest(msg) + case msg: UpdateShareStatus => handleUpdateShareStatus(msg) + case msg: UserDisconnected => handleUserDisconnected(msg) + case msg: MeetingHasEnded => handleMeetingHasEnded(msg) + + case msg: Any => logger.warn("Unknown message " + msg) + } + } + } + + + private def handleUserDisconnected(msg: UserDisconnected) { + if (logger.isDebugEnabled()) { + logger.debug("Received UserDisconnected message for meeting=[" + msg.meetingId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleIsStreamRecorded(msg: IsStreamRecorded) { + if (logger.isDebugEnabled()) { + logger.debug("Received IsStreamRecorded message for meeting=[" + msg.meetingId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleIsScreenSharing(msg: IsScreenSharing) { + if (logger.isDebugEnabled()) { + logger.debug("Received IsScreenSharing message for meeting=[" + msg.meetingId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleMeetingHasEnded(msg: MeetingHasEnded) { + logger.info("Removing meeting [" + msg.meetingId + "]") + meetings -= msg.meetingId + } + + private def handleScreenShareInfoRequest(msg: ScreenShareInfoRequest) { + if (logger.isDebugEnabled()) { + logger.debug("Received ScreenShareInfoRequest message for meetingId=[" + msg.meetingId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleUpdateShareStatus(msg: UpdateShareStatus) { + if (logger.isDebugEnabled()) { + logger.debug("Received update share message for meeting=[" + msg.streamId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleSharingStoppedMessage(msg: SharingStoppedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received sharing stopped message for meeting=[" + msg.streamId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleSharingStartedMessage(msg: SharingStartedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received sharing started message for meeting=[" + msg.streamId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + + private def handleIsSharingStopped(msg: IsSharingStopped) { + meetings.get(msg.meetingId) foreach { s => s forward msg } + } + + private def handleStreamStoppedMessage(msg: StreamStoppedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received stream stopped message for meeting=[" + msg.streamId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleStreamStartedMessage(msg: StreamStartedMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received stream started message for meeting=[" + msg.meetingId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleStopShareRequestMessage(msg: StopShareRequestMessage) { + if (logger.isDebugEnabled()) { + logger.debug("Received stop share request message for meeting=[" + msg.meetingId + "]") + } + + meetings.get(msg.meetingId) foreach { meeting => + meeting forward msg + } + } + + private def handleStartShareRequestMessage(msg: StartShareRequestMessage): Unit = { + if (logger.isDebugEnabled()) { + logger.debug("Received start share request message for meeting=[" + msg.meetingId + "]") + } + + meetings.get(msg.meetingId) match { + case None => { + if (logger.isDebugEnabled()) { + logger.debug("Creating meeting=[" + msg.meetingId + "]") + } + + val meeting: MeetingActor = new MeetingActor(this, bus, msg.meetingId) + meetings += msg.meetingId -> meeting + meeting.start + meeting forward msg + } + case Some(meeting) => { + if (logger.isDebugEnabled()) { + logger.debug("Meeting already exists. meeting=[" + msg.meetingId + "]") + } + meeting forward msg + } + } + } + + private def removeSession(meetingId: String): Unit = { + logger.debug("SessionManager: Removing session " + meetingId); + meetings.get(meetingId) foreach { s => + s ! StopSession + val old:Int = meetings.size + meetings -= meetingId; + logger.debug("RemoveSession: Session length [%d,%d]", old, meetings.size) + } + } + +} diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/messages/IMessage.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/messages/IMessage.scala new file mode 100755 index 0000000000000000000000000000000000000000..0f90d01fb8c991cb80942c65add11f240b9e4cab --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/messages/IMessage.scala @@ -0,0 +1,36 @@ +package org.bigbluebutton.app.screenshare.server.sessions.messages + +case class StartShareRequestMessage(meetingId: String, userId: String, record: Boolean) + +case class StartShareRequestReplyMessage(token: String) + +case class StopShareRequestMessage(meetingId: String, streamId: String) + +case class StreamStartedMessage(meetingId: String, streamId: String, url: String) + +case class StreamStoppedMessage(meetingId: String, streamId: String) + +case class SharingStartedMessage(meetingId: String, streamId: String, width: Int, height: Int) + +case class SharingStoppedMessage(meetingId: String, streamId: String) + +case class IsStreamRecorded(meetingId: String, streamId: String) + +case class IsStreamRecordedReply(record: Boolean) + +case class IsSharingStopped(meetingId: String, streamId: String) + +case class IsSharingStoppedReply(stopped: Boolean) + +case class UpdateShareStatus(meetingId: String, streamId: String, sequence: Int) + +case class IsScreenSharing(meetingId: String) + +case class IsScreenSharingReply(sharing: Boolean, streamId: String, + width: Int, height: Int, url: String) + +case class ScreenShareInfoRequest(meetingId: String, token: String) + +case class ScreenShareInfoRequestReply(meetingId: String, streamId: String) + +case class UserDisconnected(meetingId: String, userId: String) \ No newline at end of file diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/messages/ShareScreenResponse.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/messages/ShareScreenResponse.scala new file mode 100755 index 0000000000000000000000000000000000000000..f81ae8589562a3d9b2cb43a90c2e5252c00c2245 --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/sessions/messages/ShareScreenResponse.scala @@ -0,0 +1,5 @@ +package org.bigbluebutton.app.screenshare.server.sessions.messages + +class ShareScreenResponse { + +} \ No newline at end of file diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/LogHelper.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/LogHelper.scala new file mode 100755 index 0000000000000000000000000000000000000000..41a602aeee11ed8cb1d7dac675b5cfaef5352b4e --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/LogHelper.scala @@ -0,0 +1,13 @@ +package org.bigbluebutton.app.screenshare.server.util + +import org.slf4j.Logger +import org.red5.logging.Red5LoggerFactory; + +/** + * LogHelper is a trait you can mix in to provide easy log4j logging + * for your scala classes. + **/ +trait LogHelper { + val loggerName = this.getClass.getName + lazy val logger = Red5LoggerFactory.getLogger(this.getClass, "screenshare") +} \ No newline at end of file diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/RandomStringGenerator.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/RandomStringGenerator.scala new file mode 100755 index 0000000000000000000000000000000000000000..cef85f6a9722136249342f14b6ad8e5fef50fad9 --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/RandomStringGenerator.scala @@ -0,0 +1,17 @@ +package org.bigbluebutton.app.screenshare.server.util + +object RandomStringGenerator { +// From: http://www.bindschaedler.com/2012/04/07/elegant-random-string-generation-in-scala/ + + // Random generator + val random = new scala.util.Random + + // Generate a random string of length n from the given alphabet + def randomString(alphabet: String)(n: Int): String = + Stream.continually(random.nextInt(alphabet.size)).map(alphabet).take(n).mkString + + // Generate a random alphabnumeric string of length n + def randomAlphanumericString(n: Int) = + randomString("abcdefghijklmnopqrstuvwxyz0123456789")(n) +} + diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/TimeUtil.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/TimeUtil.scala new file mode 100755 index 0000000000000000000000000000000000000000..14d08ff51a44cb4f69d4fee2caa9714086821233 --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/server/util/TimeUtil.scala @@ -0,0 +1,18 @@ +package org.bigbluebutton.app.screenshare.server.util + +import java.util.concurrent.TimeUnit + +object TimeUtil { + + def generateTimestamp():Long = { + TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + } + + def getCurrentMonoTime():Long = { + TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + } + + def getCurrentTime():Long = { + System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/store/redis/EventListenerImp.scala b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/store/redis/EventListenerImp.scala new file mode 100755 index 0000000000000000000000000000000000000000..3517942bbe23a365586f41c86ab230b3ecf67293 --- /dev/null +++ b/bbb-screenshare/app/src/main/scala/org/bigbluebutton/app/screenshare/store/redis/EventListenerImp.scala @@ -0,0 +1,11 @@ +package org.bigbluebutton.app.screenshare.store.redis + +import org.bigbluebutton.app.screenshare.events.IEventListener +import org.bigbluebutton.app.screenshare.events.IEvent + +class EventListenerImp extends IEventListener { + + def handleMessage(event: IEvent) { + + } +} \ No newline at end of file diff --git a/bbb-screenshare/app/src/main/webapp/WEB-INF/red5-web.properties b/bbb-screenshare/app/src/main/webapp/WEB-INF/red5-web.properties new file mode 100755 index 0000000000000000000000000000000000000000..71fa8ffbaae52a11f520db7bd40fac02cad3656d --- /dev/null +++ b/bbb-screenshare/app/src/main/webapp/WEB-INF/red5-web.properties @@ -0,0 +1,2 @@ +webapp.contextPath=/screenshare +webapp.virtualHosts=*, localhost, localhost:8088, 127.0.0.1:8088 diff --git a/bbb-screenshare/app/src/main/webapp/WEB-INF/red5-web.xml b/bbb-screenshare/app/src/main/webapp/WEB-INF/red5-web.xml new file mode 100755 index 0000000000000000000000000000000000000000..e5774867d0fd41262cfcdb0aa952ec7c7320c1ae --- /dev/null +++ b/bbb-screenshare/app/src/main/webapp/WEB-INF/red5-web.xml @@ -0,0 +1,110 @@ +<?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:lang="http://www.springframework.org/schema/lang" + xsi:schemaLocation="http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans-2.0.xsd + http://www.springframework.org/schema/lang + http://www.springframework.org/schema/lang/spring-lang-2.0.xsd"> + + <bean id="placeholderConfig" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> + <property name="locations"> + <list> + <value>/WEB-INF/red5-web.properties</value> + <value>/WEB-INF/screenshare.properties</value> + </list> + </property> + </bean> + + <bean id="web.context" class="org.red5.server.Context" autowire="byType"/> + + <bean id="web.scope" class="org.red5.server.scope.WebScope" init-method="register"> + <property name="server" ref="red5.server"/> + <property name="parent" ref="global.scope"/> + <property name="context" ref="web.context"/> + <property name="handler" ref="web.handler"/> + <property name="contextPath" value="${webapp.contextPath}"/> + <property name="virtualHosts" value="${webapp.virtualHosts}"/> + </bean> + + <bean id="web.handler" class="org.bigbluebutton.app.screenshare.red5.Red5AppAdapter"> + <property name="streamBaseUrl" value="${streamBaseUrl}"/> + <property name="eventRecordingService" ref="eventRecordingService"/> + <property name="recordingDirectory" value="${recordingDirectory}"/> + <property name="application" ref="screenShareApplication"/> + <property name="messageSender" ref="connectionInvokerService"/> + </bean> + + <bean id="screenshare.service" class="org.bigbluebutton.app.screenshare.red5.Red5AppService"> + <property name="appHandler" ref="red5AppHandler"/> + </bean> + + <bean id="red5AppHandler" class="org.bigbluebutton.app.screenshare.red5.Red5AppHandler"> + <property name="application" ref="screenShareApplication"/> + <property name="messageSender" ref="connectionInvokerService"/> + </bean> + + <!-- The IoHandler implementation --> + <bean id="screenCaptureHandler" class="org.bigbluebutton.app.screenshare.server.socket.BlockStreamEventMessageHandler"> + <property name="application" ref="screenShareApplication"/> + </bean> + + <bean id="screenShareApplication" class="org.bigbluebutton.app.screenshare.ScreenShareApplication"> + <constructor-arg index="0" ref="messageBus"/> + <constructor-arg index="1" value="${jnlpFile}"/> + <constructor-arg index="2" value="${streamBaseUrl}"/> + </bean> + + <bean id="eventListenerImp" class="org.bigbluebutton.app.screenshare.red5.EventListenerImp"> + <property name="messageSender" ref="connectionInvokerService"/> + </bean> + + <bean id="jnlpConfigurator" class="org.bigbluebutton.app.screenshare.server.servlet.JnlpConfigurator"> + <property name="jnlpUrl" value="${jnlpUrl}"/> + <property name="streamBaseUrl" value="${streamBaseUrl}"/> + <property name="codecOptions" value="${codecOptions}"/> + <property name="application" ref="screenShareApplication"/> + </bean> + + <bean id="messageBus" class="org.bigbluebutton.app.screenshare.events.EventMessageBusImp" + init-method="start" destroy-method="stop"> + <property name="listeners"> + <set> + <ref bean="eventListenerImp" /> + </set> + </property> + </bean> + + <bean id="connectionInvokerService" class="org.bigbluebutton.app.screenshare.red5.ConnectionInvokerService" + init-method="start" destroy-method="stop"> + </bean> + + <bean id="eventRecordingService" class="org.bigbluebutton.app.screenshare.EventRecordingService"> + <constructor-arg index="0" value="${redis.host}"/> + <constructor-arg index="1" value="${redis.port}"/> + </bean> + + <bean id="redisRecorder" class="org.bigbluebutton.app.screenshare.server.recorder.EventRecorder"> + <constructor-arg index="0" value="${redis.host}"/> + <constructor-arg index="1" value="${redis.port}"/> + </bean> +</beans> diff --git a/bbb-screenshare/app/src/main/webapp/WEB-INF/screenshare.conf b/bbb-screenshare/app/src/main/webapp/WEB-INF/screenshare.conf new file mode 100755 index 0000000000000000000000000000000000000000..f0f0e6c273c36fa8deb90739040bae5bcdc7d8d1 --- /dev/null +++ b/bbb-screenshare/app/src/main/webapp/WEB-INF/screenshare.conf @@ -0,0 +1,21 @@ +log { + filename = "/usr/share/red5/log/screenshare.log" + roll = "daily" + level = "info" + use_full_package_names = on + + silence_net_sf_cache { + node = "net.sf.ehcache" + level = "error" + } + + silence_sun_rmi { + node = "sun.rmi" + level = "error" + } + + silence_org_apache { + node = "org.apache" + level = "error" + } +} \ No newline at end of file diff --git a/bbb-screenshare/app/src/main/webapp/WEB-INF/screenshare.properties b/bbb-screenshare/app/src/main/webapp/WEB-INF/screenshare.properties new file mode 100755 index 0000000000000000000000000000000000000000..a61a86e3feeb19bb885431218a7b838d53b700ae --- /dev/null +++ b/bbb-screenshare/app/src/main/webapp/WEB-INF/screenshare.properties @@ -0,0 +1,29 @@ +# +# NOTE: default properties. +# +# NOTE!!!! NOTE!!!! NOTE!!!! NOTE!!!! NOTE!!!! +# When making changes that you don't want checked-in, do +# git update-index --assume-unchanged <file> +# +# To have git track the changes again +# git update-index --no-assume-unchanged <file> +# + +recordingDirectory=/usr/share/red5/webapps/screenshare/streams + +redis.host=127.0.0.1 +redis.port=6379 + + +streamBaseUrl=rtmp://192.168.23.36/screenshare +jnlpUrl=http://192.168.23.36/screenshare +jnlpFile=http://192.168.23.36/screenshare/screenshare.jnlp + +# NOTES: +# 1. GOP (group of pictures) is calculated as frameRate * keyFrameInterval +# 2. intra-refresh=1 doesn't work in Chrome. Late comers can't view the stream as +# the user missed the key frame +# 3. keyFrameInterval is in seconds +# 4. Make sure you encode & into & as it will break the JNLP XML +#codecOptions=crf=36&preset=veryfast&tune=animation,zerolatency&frameRate=12.0&keyFrameInterval=6 +codecOptions=crf=38&preset=veryfast&tune=zerolatency&frameRate=12.0&keyFrameInterval=6&intra-refresh=1 diff --git a/bbb-screenshare/app/src/main/webapp/WEB-INF/tunnel-servlet.xml b/bbb-screenshare/app/src/main/webapp/WEB-INF/tunnel-servlet.xml new file mode 100755 index 0000000000000000000000000000000000000000..0760dc00afb2c0458ab7bd3fc405c9ecb4ae7d18 --- /dev/null +++ b/bbb-screenshare/app/src/main/webapp/WEB-INF/tunnel-servlet.xml @@ -0,0 +1,44 @@ +<?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:lang="http://www.springframework.org/schema/lang" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd + http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.0.xsd"> + + <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/> + + <bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> + <property name="defaultHandler" ref="httpTunnelController"/> + </bean> + + <bean id="httpTunnelController" class="org.bigbluebutton.app.screenshare.server.servlet.HttpTunnelStreamController"> + <property name="methodNameResolver"> + <bean class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver"> + <property name="mappings"> + <props> + <prop key="/screenCapture">screenCaptureHandler</prop> + </props> + </property> + </bean> + </property> + </bean> +</beans> diff --git a/bbb-screenshare/app/src/main/webapp/WEB-INF/web.xml b/bbb-screenshare/app/src/main/webapp/WEB-INF/web.xml new file mode 100755 index 0000000000000000000000000000000000000000..35d88442837f2c1cecd72b6419095b3addd97e68 --- /dev/null +++ b/bbb-screenshare/app/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + +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/>. + +--> +<web-app + xmlns="http://java.sun.com/xml/ns/j2ee" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" + version="2.4"> + + <display-name>deskShare</display-name> + + <context-param> + <param-name>webAppRootKey</param-name> + <param-value>/screenshare</param-value> + </context-param> + + <listener> + <listener-class>org.red5.logging.ContextLoggingListener</listener-class> + </listener> + + <filter> + <filter-name>LoggerContextFilter</filter-name> + <filter-class>org.red5.logging.LoggerContextFilter</filter-class> + </filter> + + <filter-mapping> + <filter-name>LoggerContextFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <!--listener> + <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> + </listener--> + + <servlet> + <servlet-name>tunnel</servlet-name> + <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + + <!-- maps the sample dispatcher to *.form --> + <servlet-mapping> + <servlet-name>tunnel</servlet-name> + <url-pattern>/tunnel/*</url-pattern> + </servlet-mapping> + + <servlet> + <servlet-name>JnlpDownloadServlet</servlet-name> + <servlet-class>jnlp.sample.servlet.JnlpDownloadServlet</servlet-class> + </servlet> + + <servlet-mapping> + <servlet-name>JnlpDownloadServlet</servlet-name> + <url-pattern>*.jnlp</url-pattern> + </servlet-mapping> + + <servlet-mapping> + <servlet-name>JnlpDownloadServlet</servlet-name> + <url-pattern>*.jar</url-pattern> + </servlet-mapping> + + <security-constraint> + <web-resource-collection> + <web-resource-name>Forbidden</web-resource-name> + <url-pattern>/streams/*</url-pattern> + </web-resource-collection> + <auth-constraint/> + </security-constraint> + +</web-app> diff --git a/bbb-screenshare/app/src/main/webapp/streams/README b/bbb-screenshare/app/src/main/webapp/streams/README new file mode 100755 index 0000000000000000000000000000000000000000..7e41f04ba5c7ceb44ecd1f94474580e6f2c98e8a --- /dev/null +++ b/bbb-screenshare/app/src/main/webapp/streams/README @@ -0,0 +1 @@ +Do not delete the streams directory. This is where the screenshare recordings will be saved. \ No newline at end of file diff --git a/bbb-screenshare/app/src/test/java/README b/bbb-screenshare/app/src/test/java/README new file mode 100755 index 0000000000000000000000000000000000000000..28b05c6394926aa5dd12ccf8e7100907300e0261 --- /dev/null +++ b/bbb-screenshare/app/src/test/java/README @@ -0,0 +1 @@ + placeholder for tests diff --git a/bbb-screenshare/app/src/test/java/org/bigbluebutton/deskshare/server/recorder/FileRecorderTest.java b/bbb-screenshare/app/src/test/java/org/bigbluebutton/deskshare/server/recorder/FileRecorderTest.java new file mode 100755 index 0000000000000000000000000000000000000000..f5491111f761d49201584918ff900cdd51a8c3d3 --- /dev/null +++ b/bbb-screenshare/app/src/test/java/org/bigbluebutton/deskshare/server/recorder/FileRecorderTest.java @@ -0,0 +1,12 @@ +package org.bigbluebutton.deskshare.server.recorder; + +import org.testng.Assert; +import org.testng.annotations.Test; + +public class FileRecorderTest { + + @Test + public void testHello() { + Assert.assertEquals(true, true); + } +} diff --git a/bbb-screenshare/app/src/test/resources/testng.xml b/bbb-screenshare/app/src/test/resources/testng.xml new file mode 100755 index 0000000000000000000000000000000000000000..41fcfdd9e180c627aa928cb4502451d08050c6a8 --- /dev/null +++ b/bbb-screenshare/app/src/test/resources/testng.xml @@ -0,0 +1,14 @@ +<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" > +<suite name="BigBlueButton Desktop Sharing Test Suite"> + <test name="Conference tests"> + <!--groups> + <run> + <exclude name="broken"/> + </run> + </groups--> + + <packages> + <package name="org.bigbluebutton.deskshare.server.recorder"/> + </packages> + </test> +</suite> \ No newline at end of file diff --git a/bbb-screenshare/jws/README.md b/bbb-screenshare/jws/README.md new file mode 100755 index 0000000000000000000000000000000000000000..298cd0f33315127b396a1b0d79ed35383fcfa62e --- /dev/null +++ b/bbb-screenshare/jws/README.md @@ -0,0 +1 @@ +# bigbluebutton-screenshare diff --git a/bbb-screenshare/jws/native-libs/README.md b/bbb-screenshare/jws/native-libs/README.md new file mode 100755 index 0000000000000000000000000000000000000000..1023ffc06c3e26150d0e9825ad4ae3d7114fffad --- /dev/null +++ b/bbb-screenshare/jws/native-libs/README.md @@ -0,0 +1,15 @@ + +This directory contains the difference JavaCV jar files that we need for our web start application. + +To build each native library and sign each ffmpeg native libraries: + +1. cd to the platform directory you want to build (cd ffmpeg-win-x86) +2. type ```gradle jar``` to build the jar file +3. type ```ant sign-jar``` to sign the jar file with your certificate +4. copy the signed-jar to ```bbb-screenshare/app/jws/lib``` to be included when + deploying to red5 + +We have included unsigned jars for ```ffmpeg.jar```, ```javacpp.jar```, and ```javacv.jar``` in the +unsigned-jars directory. You can sign the jar files there with your certificate. + + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/.gitignore b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/.gitignore new file mode 100755 index 0000000000000000000000000000000000000000..24e6582bbcf2f11d21b3c52ed6bd4a5e29a8c303 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/.gitignore @@ -0,0 +1,5 @@ +build/ +src/ +workdir/ +*.p12 + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/build.gradle b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/build.gradle new file mode 100755 index 0000000000000000000000000000000000000000..7e20f10a8c068f27843e42380ed1327c8b6b64c0 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/build.gradle @@ -0,0 +1,10 @@ +apply plugin: 'java' +apply plugin: 'eclipse' + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +version = '0.0.1' +archivesBaseName = 'ffmpeg-linux-x86' + + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/build.xml b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/build.xml new file mode 100755 index 0000000000000000000000000000000000000000..3aa62722a7f196f483d288dffb98f092c21d036d --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/build.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<project name="ffmpeg-linux-x86-signing" basedir="."> + + <!-- Sign jar with Certificate using pkcs12 file --> + <target name="check-certificate"> + <input message="Enter cetificate filename:" addproperty="cert.name" /> + <input message="Enter cetificate password:" addproperty="cert.password" /> + <exec executable="/usr/bin/keytool" outputproperty="cert.info"> + <arg line="-list" /> + <arg line="-storetype pkcs12" /> + <arg line="-keystore ${cert.name}" /> + <arg line="-storepass ${cert.password}" /> + <arg line="-v" /> + </exec> + </target> + + <target name="get-alias-name" depends="check-certificate"> + <script language="javascript"> + <![CDATA[ + // getting the value + info = project.getProperty("cert.info"); + alias = (info.match(/Alias name:(.*)/)[0]).replace("Alias name: ",""); + project.setProperty("cert.alias",alias); + ]]> + </script> + </target> + + + <target name="sign-jar" depends="get-alias-name"> + <signjar jar="build/libs/ffmpeg-linux-x86-0.0.1.jar" + storetype="pkcs12" + keystore="${cert.name}" + storepass="${cert.password}" + alias="${cert.alias}" /> + </target> + + +</project> diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/ffmpeg-linux-x86.jar b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/ffmpeg-linux-x86.jar new file mode 100755 index 0000000000000000000000000000000000000000..668fc4bcd41f11495b3bc659e009939d9629c681 Binary files /dev/null and b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/ffmpeg-linux-x86.jar differ diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/sign-jar.sh b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/sign-jar.sh new file mode 100755 index 0000000000000000000000000000000000000000..291781c99a16d0dbd2672ba84e9476dd91194800 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86/sign-jar.sh @@ -0,0 +1,12 @@ +mkdir workdir +cp ffmpeg-linux-x86.jar workdir +rm -rf src +mkdir -p src/main/resources +cd workdir +jar xvf ffmpeg-linux-x86.jar +cp org/bytedeco/javacpp/linux-x86/* ../src/main/resources +cd .. +rm -rf workdir +gradle jar +ant sign-jar +cp build/libs/ffmpeg-linux-x86-0.0.1.jar ../../../app/jws/lib/ffmpeg-linux-x86.jar diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/.gitignore b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/.gitignore new file mode 100755 index 0000000000000000000000000000000000000000..24e6582bbcf2f11d21b3c52ed6bd4a5e29a8c303 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/.gitignore @@ -0,0 +1,5 @@ +build/ +src/ +workdir/ +*.p12 + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/build.gradle b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/build.gradle new file mode 100755 index 0000000000000000000000000000000000000000..6cc065a7dfd20e1ce798aba3ce3c45b4a40e49a1 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/build.gradle @@ -0,0 +1,10 @@ +apply plugin: 'java' +apply plugin: 'eclipse' + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +version = '0.0.1' +archivesBaseName = 'ffmpeg-linux-x86_64' + + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/build.xml b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/build.xml new file mode 100755 index 0000000000000000000000000000000000000000..ac9a22aaef132842657d420270db75b4a96c9c5a --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/build.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<project name="ffmpeg-linux-x86_64-signing" basedir="."> + + <!-- Sign jar with Certificate using pkcs12 file --> + <target name="check-certificate"> + <input message="Enter cetificate filename:" addproperty="cert.name" /> + <input message="Enter cetificate password:" addproperty="cert.password" /> + <exec executable="/usr/bin/keytool" outputproperty="cert.info"> + <arg line="-list" /> + <arg line="-storetype pkcs12" /> + <arg line="-keystore ${cert.name}" /> + <arg line="-storepass ${cert.password}" /> + <arg line="-v" /> + </exec> + </target> + + <target name="get-alias-name" depends="check-certificate"> + <script language="javascript"> + <![CDATA[ + // getting the value + info = project.getProperty("cert.info"); + alias = (info.match(/Alias name:(.*)/)[0]).replace("Alias name: ",""); + project.setProperty("cert.alias",alias); + ]]> + </script> + </target> + + + <target name="sign-jar" depends="get-alias-name"> + <signjar jar="build/libs/ffmpeg-linux-x86_64-0.0.1.jar" + storetype="pkcs12" + keystore="${cert.name}" + storepass="${cert.password}" + alias="${cert.alias}" /> + </target> + + +</project> diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/ffmpeg-linux-x86_64.jar b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/ffmpeg-linux-x86_64.jar new file mode 100755 index 0000000000000000000000000000000000000000..6241ebbcfb2c1f8cd55dc3c06a1ebaa7472a33f4 Binary files /dev/null and b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/ffmpeg-linux-x86_64.jar differ diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/sign-jar.sh b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/sign-jar.sh new file mode 100755 index 0000000000000000000000000000000000000000..cf31f4f5151eeb1f22199e5ba4236e145948823c --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-linux-x86_64/sign-jar.sh @@ -0,0 +1,12 @@ +mkdir workdir +cp ffmpeg-linux-x86_64.jar workdir +rm -rf src +mkdir -p src/main/resources +cd workdir +jar xvf ffmpeg-linux-x86_64.jar +cp org/bytedeco/javacpp/linux-x86_64/* ../src/main/resources +cd .. +rm -rf workdir +gradle jar +ant sign-jar +cp build/libs/ffmpeg-linux-x86_64-0.0.1.jar ../../../app/jws/lib/ffmpeg-linux-x86_64.jar diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/.gitignore b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/.gitignore new file mode 100755 index 0000000000000000000000000000000000000000..24e6582bbcf2f11d21b3c52ed6bd4a5e29a8c303 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/.gitignore @@ -0,0 +1,5 @@ +build/ +src/ +workdir/ +*.p12 + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/build.gradle b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/build.gradle new file mode 100755 index 0000000000000000000000000000000000000000..2243f4c691a847d3ddc6c50f9f7397e79243601e --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/build.gradle @@ -0,0 +1,10 @@ +apply plugin: 'java' +apply plugin: 'eclipse' + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +version = '0.0.1' +archivesBaseName = 'ffmpeg-macosx-x86_64' + + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/build.xml b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/build.xml new file mode 100755 index 0000000000000000000000000000000000000000..3b06a54dce2c3ea75bb4a8d8ed2b4f3ce863f6af --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/build.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<project name="ffmpeg-macosx-x86_64-signing" basedir="."> + + <!-- Sign jar with Certificate using pkcs12 file --> + <target name="check-certificate"> + <input message="Enter cetificate filename:" addproperty="cert.name" /> + <input message="Enter cetificate password:" addproperty="cert.password" /> + <exec executable="/usr/bin/keytool" outputproperty="cert.info"> + <arg line="-list" /> + <arg line="-storetype pkcs12" /> + <arg line="-keystore ${cert.name}" /> + <arg line="-storepass ${cert.password}" /> + <arg line="-v" /> + </exec> + </target> + + <target name="get-alias-name" depends="check-certificate"> + <script language="javascript"> + <![CDATA[ + // getting the value + info = project.getProperty("cert.info"); + alias = (info.match(/Alias name:(.*)/)[0]).replace("Alias name: ",""); + project.setProperty("cert.alias",alias); + ]]> + </script> + </target> + + + <target name="sign-jar" depends="get-alias-name"> + <signjar jar="build/libs/ffmpeg-macosx-x86_64-0.0.1.jar" + storetype="pkcs12" + keystore="${cert.name}" + storepass="${cert.password}" + alias="${cert.alias}" /> + </target> + + +</project> diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/ffmpeg-macosx-x86_64.jar b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/ffmpeg-macosx-x86_64.jar new file mode 100755 index 0000000000000000000000000000000000000000..334a1cac0ec23678ec55b37ee92c9e05064aaf4a Binary files /dev/null and b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/ffmpeg-macosx-x86_64.jar differ diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/sign-jar.sh b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/sign-jar.sh new file mode 100755 index 0000000000000000000000000000000000000000..70d3c5613ce92ff932453812b8b76c06df8d7178 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-macosx-x86_64/sign-jar.sh @@ -0,0 +1,12 @@ +mkdir workdir +cp ffmpeg-macosx-x86_64.jar workdir +rm -rf src +mkdir -p src/main/resources +cd workdir +jar xvf ffmpeg-macosx-x86_64.jar +cp org/bytedeco/javacpp/macosx-x86_64/* ../src/main/resources +cd .. +rm -rf workdir +gradle jar +ant sign-jar +cp build/libs/ffmpeg-macosx-x86_64-0.0.1.jar ../../../app/jws/lib/ffmpeg-macosx-x86_64.jar diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/.gitignore b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/.gitignore new file mode 100755 index 0000000000000000000000000000000000000000..24e6582bbcf2f11d21b3c52ed6bd4a5e29a8c303 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/.gitignore @@ -0,0 +1,5 @@ +build/ +src/ +workdir/ +*.p12 + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/build.gradle b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/build.gradle new file mode 100755 index 0000000000000000000000000000000000000000..3ab1f6fe39730f2fe49d536366cdf641e709af55 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/build.gradle @@ -0,0 +1,10 @@ +apply plugin: 'java' +apply plugin: 'eclipse' + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +version = '0.0.1' +archivesBaseName = 'ffmpeg-windows-x86' + + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/build.xml b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/build.xml new file mode 100755 index 0000000000000000000000000000000000000000..54ca846d98aca44b3e728625d20f0c04d3466aeb --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/build.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<project name="ffmpeg-win-x86-signing" basedir="."> + + <!-- Sign jar with Certificate using pkcs12 file --> + <target name="check-certificate"> + <input message="Enter cetificate filename:" addproperty="cert.name" /> + <input message="Enter cetificate password:" addproperty="cert.password" /> + <exec executable="/usr/bin/keytool" outputproperty="cert.info"> + <arg line="-list" /> + <arg line="-storetype pkcs12" /> + <arg line="-keystore ${cert.name}" /> + <arg line="-storepass ${cert.password}" /> + <arg line="-v" /> + </exec> + </target> + + <target name="get-alias-name" depends="check-certificate"> + <script language="javascript"> + <![CDATA[ + // getting the value + info = project.getProperty("cert.info"); + alias = (info.match(/Alias name:(.*)/)[0]).replace("Alias name: ",""); + project.setProperty("cert.alias",alias); + ]]> + </script> + </target> + + + <target name="sign-jar" depends="get-alias-name"> + <signjar jar="build/libs/ffmpeg-windows-x86-0.0.1.jar" + storetype="pkcs12" + keystore="${cert.name}" + storepass="${cert.password}" + alias="${cert.alias}" /> + </target> + + +</project> diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/ffmpeg-windows-x86.jar b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/ffmpeg-windows-x86.jar new file mode 100755 index 0000000000000000000000000000000000000000..ca60f25da40209bed487ea8231ae49099ad1d89a Binary files /dev/null and b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/ffmpeg-windows-x86.jar differ diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/sign-jar.sh b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/sign-jar.sh new file mode 100755 index 0000000000000000000000000000000000000000..6cb8f4c9d4ae092d65a5b37cfc3eddf6ff086cc5 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86/sign-jar.sh @@ -0,0 +1,15 @@ +mkdir workdir +cp ffmpeg-windows-x86.jar workdir/ffmpeg-windows-x86.jar +rm -rf src +mkdir -p src/main/resources +mkdir -p src/main/java +cd workdir +jar xvf ffmpeg-windows-x86.jar +cp org/bytedeco/javacpp/windows-x86/*.dll ../src/main/resources +cd .. +rm -rf workdir +gradle jar +ant sign-jar +cp build/libs/ffmpeg-windows-x86-0.0.1.jar ../../../app/jws/lib/ffmpeg-windows-x86.jar +rm -rf src + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/.gitignore b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/.gitignore new file mode 100755 index 0000000000000000000000000000000000000000..24e6582bbcf2f11d21b3c52ed6bd4a5e29a8c303 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/.gitignore @@ -0,0 +1,5 @@ +build/ +src/ +workdir/ +*.p12 + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/build.gradle b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/build.gradle new file mode 100755 index 0000000000000000000000000000000000000000..1d9791a055d075a6ecdd036a34ebec6119ec718f --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/build.gradle @@ -0,0 +1,10 @@ +apply plugin: 'java' +apply plugin: 'eclipse' + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +version = '0.0.1' +archivesBaseName = 'ffmpeg-windows-x86_64' + + diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/build.xml b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/build.xml new file mode 100755 index 0000000000000000000000000000000000000000..769a2130bd2528ef4742781fbe4fcc837d4c7e46 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/build.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" ?> +<project name="ffmpeg-win-x86-signing" basedir="."> + + <!-- Sign jar with Certificate using pkcs12 file --> + <target name="check-certificate"> + <input message="Enter cetificate filename:" addproperty="cert.name" /> + <input message="Enter cetificate password:" addproperty="cert.password" /> + <exec executable="/usr/bin/keytool" outputproperty="cert.info"> + <arg line="-list" /> + <arg line="-storetype pkcs12" /> + <arg line="-keystore ${cert.name}" /> + <arg line="-storepass ${cert.password}" /> + <arg line="-v" /> + </exec> + </target> + + <target name="get-alias-name" depends="check-certificate"> + <script language="javascript"> + <![CDATA[ + // getting the value + info = project.getProperty("cert.info"); + alias = (info.match(/Alias name:(.*)/)[0]).replace("Alias name: ",""); + project.setProperty("cert.alias",alias); + ]]> + </script> + </target> + + + <target name="sign-jar" depends="get-alias-name"> + <signjar jar="build/libs/ffmpeg-windows-x86_64-0.0.1.jar" + storetype="pkcs12" + keystore="${cert.name}" + storepass="${cert.password}" + alias="${cert.alias}" /> + </target> + + +</project> diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/ffmpeg-windows-x86_64.jar b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/ffmpeg-windows-x86_64.jar new file mode 100755 index 0000000000000000000000000000000000000000..413567d01e0f02cdd1c959b0cc522cd31a91894d Binary files /dev/null and b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/ffmpeg-windows-x86_64.jar differ diff --git a/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/sign-jar.sh b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/sign-jar.sh new file mode 100755 index 0000000000000000000000000000000000000000..ed16eb567dc37395fbfc2b0eff738dd89d162af2 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/ffmpeg-windows-x86_64/sign-jar.sh @@ -0,0 +1,15 @@ +mkdir workdir +cp ffmpeg-windows-x86_64.jar workdir/ffmpeg-windows-x86_64.jar +rm -rf src +mkdir -p src/main/resources +mkdir -p src/main/java +cd workdir +jar xvf ffmpeg-windows-x86_64.jar +cp org/bytedeco/javacpp/windows-x86_64/*.dll ../src/main/resources +cd .. +rm -rf workdir +gradle jar +ant sign-jar +cp build/libs/ffmpeg-windows-x86_64-0.0.1.jar ../../../app/jws/lib/ffmpeg-windows-x86_64.jar +rm -rf src + diff --git a/bbb-screenshare/jws/native-libs/signed-jars/ffmpeg.jar b/bbb-screenshare/jws/native-libs/signed-jars/ffmpeg.jar new file mode 100755 index 0000000000000000000000000000000000000000..578ebf0614960448557810f671844689995f19c3 Binary files /dev/null and b/bbb-screenshare/jws/native-libs/signed-jars/ffmpeg.jar differ diff --git a/bbb-screenshare/jws/native-libs/signed-jars/javacpp.jar b/bbb-screenshare/jws/native-libs/signed-jars/javacpp.jar new file mode 100755 index 0000000000000000000000000000000000000000..6ec49265efbf0584fa86edc67d3258348302a064 Binary files /dev/null and b/bbb-screenshare/jws/native-libs/signed-jars/javacpp.jar differ diff --git a/bbb-screenshare/jws/native-libs/signed-jars/javacv.jar b/bbb-screenshare/jws/native-libs/signed-jars/javacv.jar new file mode 100755 index 0000000000000000000000000000000000000000..8a560ec97649c69a7c0364d81fc53063cfa0246a Binary files /dev/null and b/bbb-screenshare/jws/native-libs/signed-jars/javacv.jar differ diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/README.md b/bbb-screenshare/jws/native-libs/unsigned-jars/README.md new file mode 100755 index 0000000000000000000000000000000000000000..562a962044c8d3df8fc9bbd904b4924d1d4b8d0b --- /dev/null +++ b/bbb-screenshare/jws/native-libs/unsigned-jars/README.md @@ -0,0 +1,11 @@ + +To sign the ffmpeg.jar and javacpp.jar, copy them to the ```workdir``` directory and +run "```ant sign-ffmpeg-jar```" or "```ant sign-javacpp-jar```". + +The resulting jar files will now be signed. To verify, run + +``` + jarsigner -verify ffmpeg.jar + jarsigner -verify javacpp.jar +``` + diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-2.8.1-1.2-SNAPSHOT.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-2.8.1-1.2-SNAPSHOT.jar new file mode 100755 index 0000000000000000000000000000000000000000..756da46cac0837de999394b94a7f588683c7ab27 Binary files /dev/null and b/bbb-screenshare/jws/native-libs/unsigned-jars/ffmpeg-2.8.1-1.2-SNAPSHOT.jar differ diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/javacpp-1.2-SNAPSHOT.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/javacpp-1.2-SNAPSHOT.jar new file mode 100755 index 0000000000000000000000000000000000000000..1d04b5276017c4daa3fcee22fb34e4610f1e069c Binary files /dev/null and b/bbb-screenshare/jws/native-libs/unsigned-jars/javacpp-1.2-SNAPSHOT.jar differ diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/javacv.jar b/bbb-screenshare/jws/native-libs/unsigned-jars/javacv.jar new file mode 100755 index 0000000000000000000000000000000000000000..1135b0afe7c5e43904b5354629ff6b9cfdad9052 Binary files /dev/null and b/bbb-screenshare/jws/native-libs/unsigned-jars/javacv.jar differ diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/sign-ffmpeg.sh b/bbb-screenshare/jws/native-libs/unsigned-jars/sign-ffmpeg.sh new file mode 100755 index 0000000000000000000000000000000000000000..c87144166d2ff90a85e60a661ce71af52c76d16f --- /dev/null +++ b/bbb-screenshare/jws/native-libs/unsigned-jars/sign-ffmpeg.sh @@ -0,0 +1,4 @@ +cp ffmpeg-2.8.1-1.2-SNAPSHOT.jar workdir/ffmpeg.jar +ant sign-ffmpeg-jar +cp workdir/ffmpeg.jar ../../../app/jws/lib/ + diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/sign-javacpp.sh b/bbb-screenshare/jws/native-libs/unsigned-jars/sign-javacpp.sh new file mode 100755 index 0000000000000000000000000000000000000000..36759bbf4f90c31df851680eda64afe7d401ccb0 --- /dev/null +++ b/bbb-screenshare/jws/native-libs/unsigned-jars/sign-javacpp.sh @@ -0,0 +1,4 @@ +cp javacpp-1.2-SNAPSHOT.jar workdir/javacpp.jar +ant sign-javacpp-jar +cp workdir/javacpp.jar ../../../app/jws/lib/ + diff --git a/bbb-screenshare/jws/native-libs/unsigned-jars/sign-javacv.sh b/bbb-screenshare/jws/native-libs/unsigned-jars/sign-javacv.sh new file mode 100755 index 0000000000000000000000000000000000000000..7ab5fe4f3bb0483a1aaafcaf73bf46279f8c4bfd --- /dev/null +++ b/bbb-screenshare/jws/native-libs/unsigned-jars/sign-javacv.sh @@ -0,0 +1,4 @@ +cp javacv.jar workdir +ant sign-javacv-jar +cp workdir/javacv.jar ../../../app/jws/lib/ + diff --git a/bbb-screenshare/jws/player/README.md b/bbb-screenshare/jws/player/README.md new file mode 100755 index 0000000000000000000000000000000000000000..29aafaafb3c18d838e20b9b3bfb9b0e32b44a569 --- /dev/null +++ b/bbb-screenshare/jws/player/README.md @@ -0,0 +1,28 @@ +# bbb-video-stream-html-client + +Some attempts to show a BigBlueButton video stream in an html with a standalone Flash player. + +Just open `index.html` and try it! + + + +## How to + +When you open `index.html` and you will see 3 inputs you need to fill: +* The name of your BBB server: use the name or IP only, without `http`, for example: `myserver.somewhere.com` or `192.168.0.1` +* The internal meeting ID used by Red5, that willbe similar to `183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1328884719999` +* The internal video stream ID used by Red5, that will be similar to `320x2401-1328884730777` + +The hard part is to find the internal IDs. +You need to either check the logs in your BBB server or include this +information in BBB's API. +In Mconf we included it in the API, see +http://code.google.com/p/mconf/wiki/MconfLiveApiChanges. +The changes are in this branch: https://github.com/mconf/bigbluebutton/tree/audio-video-on-api-2 + +## Important! + +In Flash Player's security model, Flash applications and SWF files on a local computer cannot access the network. +So first you need disable this security option for the files in your local folder. + +To do so, go to the [Flash Security Panel](http://www.macromedia.com/support/documentation/en/flashplayer/help/settings_manager04.html) and add your local folder to the list. diff --git a/bbb-screenshare/jws/player/actions.js b/bbb-screenshare/jws/player/actions.js new file mode 100755 index 0000000000000000000000000000000000000000..84aca308bb7628101f9f3c4cab88f5126748cad6 --- /dev/null +++ b/bbb-screenshare/jws/player/actions.js @@ -0,0 +1,84 @@ +$(document).ready(function() { + + setup_clear = function(wrapper, width, height) { + $(wrapper).empty(); + $(wrapper).css("width", width); + $(wrapper).css("height", height); + } + + setup_jwplayer = function(wrapper, ip, meeting, stream) { + var url = "rtmp://" + ip + "/live/" + meeting; + + // TODO: for now assuming we will never have bigger resolutions... + var width = 1920; // stream.substring(0, 3); + var height = 1080; // stream.substring(4, 7); + + setup_clear(wrapper, width, height); + + var el = $('<div></div>'); + el.attr("id", "mediaplayer"); + $(wrapper).append(el); + + jwplayer("mediaplayer").setup({ + flashplayer: 'jw-player/player.swf', + id: 'playerID', + width: width, + height: height, + file: stream, // ex: '320x2401-1328884730718' + streamer: url, // ex: 'rtmp://192.160.0.100/video/183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1328884719358' + autostart: 'true', + provider: 'rtmp', + duration: '0', + bufferlength: '1', // it's not working + //start: '0', + //live: 'true', + //repeat: 'none', + }); + }; + + setup_flowplayer = function(wrapper, ip, meeting, stream) { + var url = "rtmp://" + ip + "/live/" + meeting; + + // TODO: for now assuming we will never have bigger resolutions... + var width = 1920; //stream.substring(0, 3); + var height = 1080; // stream.substring(4, 7); + + setup_clear(wrapper, width, height); + + var el = $('<a></a>'); + el.addClass("flowplayer") + $(wrapper).append(el); + + $f("a.flowplayer", "flowplayer/flowplayer-3.2.7.swf", { + clip: { + url: stream, // ex: '320x2401-1328884730718' + provider: 'rtmp', + live: true, + bufferLength: 0, + autoPlay: true, + }, + plugins: { + rtmp: { + url: 'flowplayer/flowplayer.rtmp-3.2.3.swf', + netConnectionUrl: url // ex: 'rtmp://192.160.0.100/video/183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1328884719358' + } + } + }); + $("a.flowplayer").click(); + }; + + $("#submit").on("click", function(e) { + e.preventDefault(); + + var ip = $("#server_ip").val(); + var meeting = $("#meeting_id").val(); + var stream = $("#stream_id").val(); + + if ($('input:radio[name=player]:checked').val() == "flowplayer") { + setup_flowplayer("#wrapper", ip, meeting, stream); + } else { + setup_jwplayer("#wrapper", ip, meeting, stream); + } + }); + +}); diff --git a/bbb-screenshare/jws/player/flowplayer/LICENSE.txt b/bbb-screenshare/jws/player/flowplayer/LICENSE.txt new file mode 100755 index 0000000000000000000000000000000000000000..2a00962f22935d2719aea860183914ffb62a6486 --- /dev/null +++ b/bbb-screenshare/jws/player/flowplayer/LICENSE.txt @@ -0,0 +1,721 @@ +The Flowplayer Free version is released under the +GNU GENERAL PUBLIC LICENSE Version 3 (GPL). + +The GPL requires that you not remove the Flowplayer copyright notices +from the user interface. See section 5.d below. + +Commercial licenses are available. The commercial player version +does not require any Flowplayer notices or texts and also provides +some additional features. + +======================================================================== + +ADDITIONAL TERM per GPL Section 7 +If you convey this program (or any modifications of it) and assume +contractual liability for the program to recipients of it, you agree +to indemnify Flowplayer, Ltd. for any liability that those contractual +assumptions impose on Flowplayer, Ltd. + +Except as expressly provided herein, no trademark rights are granted in +any trademarks of Flowplayer, Ltd. Licensees are granted a limited, +non-exclusive right to use the mark Flowplayer and the Flowplayer logos +in connection with unmodified copies of the Program and the copyright +notices required by section 5.d of the GPL license. For the purposes +of this limited trademark license grant, customizing the Flowplayer +by skinning, scripting, or including PlugIns provided by Flowplayer, Ltd. +is not considered modifying the Program. + +Licensees that do modify the Program, taking advantage of the open-source +license, may not use the Flowplayer mark or Flowplayer logos and must +change the fullscreen notice (and the non-fullscreen notice, if that +option is enabled), the copyright notice in the dialog box, and the +notice on the Canvas as follows: + +the full screen (and non-fullscreen equivalent, if activated) notice +should read: "Based on Flowplayer source code"; in the context menu +(right-click menu), the link to "About Flowplayer free version #.#.#" +can remain. The copyright notice can remain, but must be supplemented with +an additional notice, stating that the licensee modified the Flowplayer. +A suitable notice might read "Flowplayer Source code modified by ModOrg 2009"; +for the canvas, the notice should read "Based on Flowplayer source code". +In addition, licensees that modify the Program must give the modified +Program a new name that is not confusingly similar to Flowplayer and +may not distribute it under the name Flowplayer. + +======================================================================== + + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. \ No newline at end of file diff --git a/bbb-screenshare/jws/player/flowplayer/README.txt b/bbb-screenshare/jws/player/flowplayer/README.txt new file mode 100755 index 0000000000000000000000000000000000000000..4609c6d7c70cb7774c443a7e81877f16b20b0297 --- /dev/null +++ b/bbb-screenshare/jws/player/flowplayer/README.txt @@ -0,0 +1,350 @@ +Version history: + +3.2.7 +----- + +- Loads the new controlbar plugin version 3.2.5. No other functional changes. + +3.2.6 +----- +- linkUrl should now work better with popup blockers: http://code.google.com/p/flowplayer-core/issues/detail?id=31 +- new linkWindow value "_popup" opens the linked page in a popup browser window +- added new onClipResized event +- Added new onUnload event, can be only listened in Flash and not triggered to JS +- API: Added new url property to plugin objects +Fixes: +- it was not possible to call play() in an onFinish listener +- fix to preserve the infoObject for custom netStream and netConnection clients in cases where the infoObject is a + primitive object without properties +- does not show the error dialog in the debugger player when showErrors: false +- fixed to correctly handle xx.ca subdomains when validating the license key +- a custom logo is now sized correctly according to the configured size +- does not show the buffer animation any more when the player receives the onBufferEmpty message from the netStream. + The animation was unnecessarily shown in some situations. +- fixed #155. added new urlEncoding property to Clip for url ncoding ut8 urls + +3.2.5 +----- +- added new scaling option 'crop' that resizes to fill all available space, cropping on top/bottom or left/right +- improvements to RSS file parsing +- Now displays a hand cursor when a linkUrl is used in clips + +3.2.4 +----- +- new flowplayer.js version, with Apple iDevice fixes + +3.2.3 +----- +- a new 'type' clip property exposed to JS +- changed the clip type property to better work as a read-write property. Now accepts 'video', 'audio', + 'image' and 'api' as configuration values. +- moved parallel rtmp connection mechanism from the RTMP plugin to Core so other plugins can use it (ie: securestreaming) +Fixes: +- fixed #112, wrong URL computation when using clip with relative URL on a page with a / after a # in its url +- fixed #111, wrong behavior of pre/post roll images with duration 0 +- fixed multiple license keys logic +Fixes: +- correct verification of license keys in *.ca domains +- fix to make playback to always reach end of video +- fixed resuming of live streams + +3.2.2 +----- +Fixes: +- Now recognizes following kind of urls as audio clips: 'mp3:audiostreamname' (ulrs with mp3 prefix and no extension) +- Now ignores the duration from metadata if we already got one. Fix required for pseudostreaming +- Fix to reuse buffered data when replaying a clip + +3.2.1 +--------- +- Support for RTMP redirects (tested with Wowza loadbalancing) +- Fixed video size when no size info available in clip metadata + +Fixes: +- Fix to correctly detect if the player SWF name contains a version number and if it does also use the version number +when it automatically loads the controls plugin. + +3.2.0 +----- +- canvas, controlbar and the content plugin backgound color and border color can be now given with rgb() and rgba() CSS style syntax +- Added onMouseOver() and onMouseOut() listener registration methods to the Flowplayer API +- enhancements to RSS playlist. Converted parsing to E4X, yahoo media and flowplayer namespace support. +- added feature to obtain bitrate and dimension information to a new clip custom property "bitrates" for future support for bitrate choosing. +- added getter for playerSwfName config +- if clip.url has the string "mp3:" in it, the clip.type will report 'audio' +- added setKeyboardShortcutsEnabled(), addKeyListener(), removeKeyListener() to FlowplayerBase +Fixes: +- onSeek() was not fired when seeking while paused and when using RTMP. An extra onStart was fired too. +- fireErrorExternal() was not working properly with an error PlayerEvent +- countPlugins() was throwing an error when a plugin was not found +- external swf files were not scaled properly +- the logo was unnecessary shown when going fullscreen if logo.displayTime was being used +- added a loadPluginWithConfig method to FlowplayerBase, accessible from javascript. Fixed double onload callback call. +- now handles cuepoint parameters injected using the Adobe Media Encoder +- showPlugin was not working when config.play was null +- handles 3-part duration values included in FLV metadata, like "500.123.123" +- player wasn't always reaching end of video +- fixed broken buffering: false +- fixed event dispatching when embedding flowplayer without flowplayer.js (=without playlist config field) +- fixed safari crashes when unloading player +- fixed scrubber behaviour with a playlist containing 2 images (or swf) in a row +- fixed errors in logs when using an RSS playlist +- fixed OverlayPlayButton that was showing even if it shouldn't on some cases +- fixed wrong behavior when onBeforeFinish was returning false within playlists +- /!\ Don't use the fadeIn / fadeOut controlbar's API while using autoHide. +- fixed play state button with images +- fixed splash image flickering + +3.1.5 +----- +Fixes: +- The player went to a locked state when resuming playback after a period that was long enought to send the +netConnection to an invalid state. Now when resuming playback on an invalid connection the clip starts again from +the beginning. This is only when using RTMP connections and does not affect progressive download playback. +- Custom netConnect and netStream events did not pass the info object to JS listeners + +3.1.4 +----- +Fixes: +- player did not initialize if the controlbar plugin was disabled and if the play button overlay was disabled with play: null +- works properly without cachebusting on IE +- RSS playlist parsing now respects the isDefault attribute used in mRSS media group items +- Fixed passing of connection arguments + +3.1.3 +----- +- enhancements to RSS playlist parsing: Now skips all media:content that have unsupported types. Now the type attribute +of the media:content element is mandatory and has to be present in the RSS file +- Possibility to pass a RSS file name with playFeed("playlist.rss") and setPlaylist("playlist.rss") calls. +- changes to the ConnectionProvider and URLResolver APIs +- Now automatically uses a plugin that is called 'rtmp' for all clips that have the rtmp-protocol in their URLs. +- Added possibility to specify all clip properties in an RSS playlist + +Fixes: +- the result of URL resolvers in now cached, and the resolvers will not be used again when a clip is replayed +- some style properties like 'backgroundGradient' had no effect in config +- video goes tiny on Firefox: http://flowplayer.org/forum/8/23226 +- RSS playlists: The 'type' attribute value 'audio/mp3' in the media:content element caused an error. +- Dispatches onMetadata() if an URL resolver changes the clip URL (changes to a different file) +- error codes and error message were not properly passed to onEvent JS listeners + +3.1.2 +----- +- The domain of the logo url must the same domain from where the player SWF is loaded from. +- Fullscreen can be toggled by doublclick on the video area. +Fixes: +- Player was not initialized correctly when instream playlists were used and the provider used in the instream clips was defined in the common clip. +- A separator in the Context Menu made the callbacks in the following menu items out of order. Related forum post: http://flowplayer.org/forum/8/22541 +- the width and height settings of a logo were ignored if the logo was a sWF file +- volume control and mute/unmute were not working after an instream clip had been played +- now possible to use RTMP for mp3 files +- Issue 12: cuepointMultiplier was undefined in the clip object set to JS event listeners +- Issue 14: onBeforeStop was unnecessarily fired when calling setPlaylist() and the player was not playing, + additionally onStop was never fired even if onBeforeStop was +- fixed screen vertical placement problems that reappeared with 3.1.1 +- The rotating animation now has the same size and position as it has after initialized + +3.1.1 +----- +- External configuration files +- Instream playback +- Added toggleFullscreen() the API +- Possibility to specify controls configuration in clips +- Seek target position is now sent in the onBeforeSeek event +Fixes: +- The screen size was initially too small on Firefox (Mac) +- Did not persist a zero volume value: http://www.flowplayer.org/forum/8/18413 + +3.1.0 +----- +New features: +- clip's can have urlResolvers and connectionProviders +- Added new configuration options 'connectionCallbacks' and 'streamCallbacks'. Both accept an Array of event names as a value. + When these events get fired on the connection or stream object, corresponding Clip events will be fired by the player. + This can be used for example when firing custom events from RTMP server apps +- Added new clip event types: 'onConnectionEvent' and 'onStreamEvent' these get fired when the predefined events happen on the connection and stream objects. +- Added Security.allowDomain() to allow loaded plugins to script the player +- Added addClip(clip, index) to the API, index is optional +- Possibility to view videos without metadata, using clip.metaData: false +- Now the player's preloader uses the rotating animation instead of a percent text to indicate the progress + of loading the player SWF. You can disable the aninamtion by setting buffering: false +- calling close() now does not send the onStop event +- Clip's custom properties are now present in the root of the clip argument in all clip events that are sent to JS. + +Bug fixes: +- The preloader sometimes failed to initialize the player +- Allow seeking while in buffering state: http://flowplayer.org/forum/8/16505 +- Replay of a RTMP stream was failing after the connection had expired +- Security error when clicking on the screen if there is an image in the playlist loaded from a foreign domain +- loadPlugin() was not working +- now fullscreen works with Flash versions older than 9.0.115, in versions that do not support hardware scaling +- replaying a RTMP stream with an image in front of the stream in the playlist was not working (video stayed hidden). Happened + because the server does not send metadata if replaying the same stream. +- the scrubber is disabled if the clip is not seekable in the first frame: http://flowplayer.org/forum/8/16526 + By default if the clip has one of following extensions (the typical flash video extensions) it is seekable + in the first frame: 'f4b', 'f4p', 'f4v', 'flv'. Added new clip property seekableOnBegin that can be used to override the default. + +3.0.6 +----- +- added possibility to associate a linkUrl and linkWindow to the canvas +Fixes: +- fix for entering fullscreen for Flash versions that don't support the hardware scaled fullscreen-mode +- when showing images the duration tracking starts only after the image has been completely loaded: http://flowplayer.org/forum/2/15301 +- fix for verifying license keys for domains that have more than 4 labels in them +- if plugin loading failis because of a IO error, the plugin will be discarded and the player initialization continues: + +3.0.4 +----- +- The "play" pseudo-plugin now supports fadeIn(), fadeOut(), showPlugin(), hidePlugin() and + additionally you can configure it like this: + // make only the play button invisible (buffering animation is still used) + play: { display: 'none' } + // disable the play button and the buffering animation + play: null + // disable the buffering animation + buffering: null +- Added possibility to seek when in the buffering state: http://flowplayer.org/forum/3/13896 +- Added copyright notices and other GPL required entries to the user interface + +Fixes: +- clip urls were not resolved correctly if the HTML page URL had a query string starting with a question mark (http://flowplayer.org/forum/8/14016#post-14016) +- Fixed context menu for with IE (commercial version) +- a cuepoint at time zero was fired several times +- screen is now arranged correctly even when only bottom or top is defined for it in the configuration +- Fixed context menu for with IE (commercial version) +- a cuepoint at time zero was fired several times +- screen is now arranged correctly even when only bottom or top is defined for it in the configuration +- Now possible to call play() in an onError handler: http://flowplayer.org/forum/8/12939 +- Does not throw an error if the player cannot persist the volume on the client computer: http://flowplayer.org/forum/8/13286#post-13495 +- Triggering fullscreen does not pause the player in IE +- The play button overlay no longer has a gap between it's pieces when a label is used: http://flowplayer.org/forum/8/14250 +- clip.update() JS call now resets the duration +- a label configured for the play button overlay did not work in the commercial version + +3.0.3 +----- +- fixed cuepoint firing: Does not skip cuepoints any more +- Plugins can now be loaded from a different domain to the flowplayer.swf +- Specifying a clip to play by just using the 'clip' node in the configuration was not working, a playlist definition was required. This is now fixed. +- Fixed: A playlist with different providers caused the onMetadata event to fire events with metadata from the previous clip in the playlist. Occurred when moving in the playlist with next() and prev() +- the opacity setting now works with the logo +- fadeOut() call to the "screen" plugin was sending the listenerId and pluginName arguments in wrong order +- stop(), pause(), resume(), close() no longer return the flowplayer object to JS +- changing the size of the screen in a onFullscreen listener now always works, there was a bug that caused this to fail occasionally +- fixed using arbitrary SWFs as plugins +- the API method setPlaylist() no longer starts playing if autoPlay: true, neither it starts buffering if autoBuffering: true +- the API method play() now accepts an array of clip objects as an argument, the playlist is replaced with the specified clips and playback starts from the 1st clip + +3.0.2 +----- +- setting play: null now works again +- pressing the play again button overlay does not open a linkUrl associated with a clip +- now displays a live feed even when the RTMP server does not send any metadata and the onStart method is not therefore dispatched +- added onMetaData clip event +- fixed 'orig' scaling: the player went to 'fit' scaling after coming back from fullscreen. This is now fixed and the original dimensions are preserved in non-fullscreen mode. +- cuepoint times are now given in milliseconds, the firing precision is 100 ms. All cuepoint times are rounded to the nearest 100 ms value (for example 1120 rounds to 1100) +- backgroundGradient was drawn over the background image in the canvas and in the content and controlbar plugins. Now it's drawn below the image. +- added cuepointMultiplier property to clips. This can be used to multiply the time values read from cuepoint metadata embedded into video files. +- the player's framerate was increased to 24 FPS, makes all animations smoother + +3.0.1 +----- +- Fixed negative cuepoints from common clip. Now these are properly propagated to the clips in playlist. +- buffering animation is now the same size as the play button overlay +- commercial version now supports license keys that allows the use of subdomains +- error messages are now automatically hidden after a 4 second delay. They are also hidden when a new clips + starts playing (when onBeforeBegin is fired) +- added possibility to disable the buffering animation like so: buffering: false +- pressing the play button overlay does not open a linkUrl associated with a clip +- license key verification failed if a port number was used in the URL (like in this url: http://mydomain.com:8080/video.html) +- added audio support, clip has a new "image" property +- workaround for missing "NetStream.Play.Start" notfication that was happending with Red5. Because of this issue the video was not shown. +- commercial version has the possibility to change the zIndex of the logo + +3.0.0 +----- +- Removed security errors that happened when loading images from foreign domains (domains other than the domain of the core SWF). + Using a backgroundImage on canvas, in the content plugin, and for the controls is also possible to be loaded + from a foreign domain - BUT backgroundRepeat cannot be used for foreign images. +- Now allows the embedding HTML to script the player even if the player is loaded from another domain. +- Added a 'live' property to Clips, used for live streams. +- A player embedded to a foreign domain now loads images, css files and other resources from the domain where the palyer SWF was loaded from. This is to generate shorter embed-codes. +- Added linkUrl and linkWindow properties to the logo, in commercial version you can set these to point to a linked page. The linked page gets opened + when the logo is clicked. Possible values for linkWindow: + * "_self" specifies the current frame in the current window. + * "_blank" specifies a new window. + * "_parent" specifies the parent of the current frame. + * "_top" specifies the top-level frame in the current window. +- Added linkUrl and linkWindow properties to clips. The linked page is opened when the video are is clicked and the corresponding clip has a linkUrl specified. +- Made the play button overlay and the "Play again" button slightly bigger. + +RC4 +--- +- Now shows a "Play again" button at the end of the video/playlist +- Commercial version shows a Flowplayer logo if invalidKey was supplied, but the otherwise the player works +- setting play: null in configuration will disable the play button overlay +- setting opacity for "play" also sets it for the buffering animation +- Fixed firing of cuepoints too early. Cuepoint firing is now based on stream time and does not rely on timers +- added onXMPData event listener +- Should not stop playback too early before the clip is really completed +- The START event is now delayed so that the metadata is available when the event is fired, METADATA event was removed, + new event BEGIN that is dispatched when the playback has been successfully started. Metadata is not normally + available when BEGIN is fired. + +RC3 +--- +- stopBuffering() now dispatches the onStop event first if the player is playing/paused/buffering at the time of calling it +- fixed detection of images based on file extensions +- fixed some issues with having images in the playlist +- made it possible to autoBuffer next video while showing an image (image without a duration) + +RC2 +--- +- fixed: setting the screen height in configuration did not have any effect + +RC1 +----- +- better error message if plugin loading fails, shows the URL used +- validates our redesigned multidomain license key correctly +- fix to prevent the play button going visible when the onBufferEmpty event occurs +- the commercial swf now correctly loads the controls using version information +- fixed: the play button overlay became invisible with long fadeOutSpeeds + +beta6 +----- +- removed the onFirstFramePause event +- playing a clip for the second time caused a doubled sound +- pausing on first frame did not work on some FLV files + +beta5 +----- +- logo only uses percentage scaling if it's a SWF file (there is ".swf" in it's url) +- context menu now correctly builds up from string entries in configuration +-always closes the previous connection before starting a new clip + +beta4 +----- +- now it's possible to load a plugin into the panel without specifying any position/dimensions + information, the plugin is placed to left: "50%", top: "50%" and using the plugin DisplayObject's width & height +- The Flowplayer API was not fully initialized when onLoad was invoked on Flash plugins + +beta3 +----- +- tweaking logo placement +- "play" did not show up after repeated pause/resume +- player now loads the latest controls SWF version, right now the latest SWF is called 'flowplayer.controls-3.0.0-beta2.swf' + +beta2 +----- +- fixed support for RTMP stream groups +- changed to loop through available fonts in order to find a suitable font also in IE +- Preloader was broken on IE: When the player SWf was in browser's cache it did not initialize properly +- Context menu now correctly handles menu items that are configured by their string labels only (not using json objects) +- fixed custom logo positioning (was moved to the left edge of screen in fullscreen) +- "play" now always follows the position and size of the screen +- video was stretched below the controls in fullscreen when autoHide: 'never' +- logo now takes 6.5% of the screen height, width is scaled so that the aspect ratio is preserved + +beta1 +----- +- First public beta release diff --git a/bbb-screenshare/jws/player/flowplayer/example/flowplayer-3.2.6.min.js b/bbb-screenshare/jws/player/flowplayer/example/flowplayer-3.2.6.min.js new file mode 100755 index 0000000000000000000000000000000000000000..500492e297789717ab506954af1f715cbb450785 --- /dev/null +++ b/bbb-screenshare/jws/player/flowplayer/example/flowplayer-3.2.6.min.js @@ -0,0 +1,24 @@ +/* + * flowplayer.js 3.2.6. The Flowplayer API + * + * Copyright 2009-2011 Flowplayer Oy + * + * This file is part of Flowplayer. + * + * Flowplayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Flowplayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Flowplayer. If not, see <http://www.gnu.org/licenses/>. + * + * Date: 2011-02-04 05:45:28 -0500 (Fri, 04 Feb 2011) + * Revision: 614 + */ +(function(){function g(o){console.log("$f.fireEvent",[].slice.call(o))}function k(q){if(!q||typeof q!="object"){return q}var o=new q.constructor();for(var p in q){if(q.hasOwnProperty(p)){o[p]=k(q[p])}}return o}function m(t,q){if(!t){return}var o,p=0,r=t.length;if(r===undefined){for(o in t){if(q.call(t[o],o,t[o])===false){break}}}else{for(var s=t[0];p<r&&q.call(s,p,s)!==false;s=t[++p]){}}return t}function c(o){return document.getElementById(o)}function i(q,p,o){if(typeof p!="object"){return q}if(q&&p){m(p,function(r,s){if(!o||typeof s!="function"){q[r]=s}})}return q}function n(s){var q=s.indexOf(".");if(q!=-1){var p=s.slice(0,q)||"*";var o=s.slice(q+1,s.length);var r=[];m(document.getElementsByTagName(p),function(){if(this.className&&this.className.indexOf(o)!=-1){r.push(this)}});return r}}function f(o){o=o||window.event;if(o.preventDefault){o.stopPropagation();o.preventDefault()}else{o.returnValue=false;o.cancelBubble=true}return false}function j(q,o,p){q[o]=q[o]||[];q[o].push(p)}function e(){return"_"+(""+Math.random()).slice(2,10)}var h=function(t,r,s){var q=this,p={},u={};q.index=r;if(typeof t=="string"){t={url:t}}i(this,t,true);m(("Begin*,Start,Pause*,Resume*,Seek*,Stop*,Finish*,LastSecond,Update,BufferFull,BufferEmpty,BufferStop").split(","),function(){var v="on"+this;if(v.indexOf("*")!=-1){v=v.slice(0,v.length-1);var w="onBefore"+v.slice(2);q[w]=function(x){j(u,w,x);return q}}q[v]=function(x){j(u,v,x);return q};if(r==-1){if(q[w]){s[w]=q[w]}if(q[v]){s[v]=q[v]}}});i(this,{onCuepoint:function(x,w){if(arguments.length==1){p.embedded=[null,x];return q}if(typeof x=="number"){x=[x]}var v=e();p[v]=[x,w];if(s.isLoaded()){s._api().fp_addCuepoints(x,r,v)}return q},update:function(w){i(q,w);if(s.isLoaded()){s._api().fp_updateClip(w,r)}var v=s.getConfig();var x=(r==-1)?v.clip:v.playlist[r];i(x,w,true)},_fireEvent:function(v,y,w,A){if(v=="onLoad"){m(p,function(B,C){if(C[0]){s._api().fp_addCuepoints(C[0],r,B)}});return false}A=A||q;if(v=="onCuepoint"){var z=p[y];if(z){return z[1].call(s,A,w)}}if(y&&"onBeforeBegin,onMetaData,onStart,onUpdate,onResume".indexOf(v)!=-1){i(A,y);if(y.metaData){if(!A.duration){A.duration=y.metaData.duration}else{A.fullDuration=y.metaData.duration}}}var x=true;m(u[v],function(){x=this.call(s,A,y,w)});return x}});if(t.onCuepoint){var o=t.onCuepoint;q.onCuepoint.apply(q,typeof o=="function"?[o]:o);delete t.onCuepoint}m(t,function(v,w){if(typeof w=="function"){j(u,v,w);delete t[v]}});if(r==-1){s.onCuepoint=this.onCuepoint}};var l=function(p,r,q,t){var o=this,s={},u=false;if(t){i(s,t)}m(r,function(v,w){if(typeof w=="function"){s[v]=w;delete r[v]}});i(this,{animate:function(y,z,x){if(!y){return o}if(typeof z=="function"){x=z;z=500}if(typeof y=="string"){var w=y;y={};y[w]=z;z=500}if(x){var v=e();s[v]=x}if(z===undefined){z=500}r=q._api().fp_animate(p,y,z,v);return o},css:function(w,x){if(x!==undefined){var v={};v[w]=x;w=v}r=q._api().fp_css(p,w);i(o,r);return o},show:function(){this.display="block";q._api().fp_showPlugin(p);return o},hide:function(){this.display="none";q._api().fp_hidePlugin(p);return o},toggle:function(){this.display=q._api().fp_togglePlugin(p);return o},fadeTo:function(y,x,w){if(typeof x=="function"){w=x;x=500}if(w){var v=e();s[v]=w}this.display=q._api().fp_fadeTo(p,y,x,v);this.opacity=y;return o},fadeIn:function(w,v){return o.fadeTo(1,w,v)},fadeOut:function(w,v){return o.fadeTo(0,w,v)},getName:function(){return p},getPlayer:function(){return q},_fireEvent:function(w,v,x){if(w=="onUpdate"){var z=q._api().fp_getPlugin(p);if(!z){return}i(o,z);delete o.methods;if(!u){m(z.methods,function(){var B=""+this;o[B]=function(){var C=[].slice.call(arguments);var D=q._api().fp_invoke(p,B,C);return D==="undefined"||D===undefined?o:D}});u=true}}var A=s[w];if(A){var y=A.apply(o,v);if(w.slice(0,1)=="_"){delete s[w]}return y}return o}})};function b(q,G,t){var w=this,v=null,D=false,u,s,F=[],y={},x={},E,r,p,C,o,A;i(w,{id:function(){return E},isLoaded:function(){return(v!==null&&v.fp_play!==undefined&&!D)},getParent:function(){return q},hide:function(H){if(H){q.style.height="0px"}if(w.isLoaded()){v.style.height="0px"}return w},show:function(){q.style.height=A+"px";if(w.isLoaded()){v.style.height=o+"px"}return w},isHidden:function(){return w.isLoaded()&&parseInt(v.style.height,10)===0},load:function(J){if(!w.isLoaded()&&w._fireEvent("onBeforeLoad")!==false){var H=function(){u=q.innerHTML;if(u&&!flashembed.isSupported(G.version)){q.innerHTML=""}if(J){J.cached=true;j(x,"onLoad",J)}flashembed(q,G,{config:t})};var I=0;m(a,function(){this.unload(function(K){if(++I==a.length){H()}})})}return w},unload:function(J){if(this.isFullscreen()&&/WebKit/i.test(navigator.userAgent)){if(J){J(false)}return w}if(u.replace(/\s/g,"")!==""){if(w._fireEvent("onBeforeUnload")===false){if(J){J(false)}return w}D=true;try{if(v){v.fp_close();w._fireEvent("onUnload")}}catch(H){}var I=function(){v=null;q.innerHTML=u;D=false;if(J){J(true)}};setTimeout(I,50)}else{if(J){J(false)}}return w},getClip:function(H){if(H===undefined){H=C}return F[H]},getCommonClip:function(){return s},getPlaylist:function(){return F},getPlugin:function(H){var J=y[H];if(!J&&w.isLoaded()){var I=w._api().fp_getPlugin(H);if(I){J=new l(H,I,w);y[H]=J}}return J},getScreen:function(){return w.getPlugin("screen")},getControls:function(){return w.getPlugin("controls")._fireEvent("onUpdate")},getLogo:function(){try{return w.getPlugin("logo")._fireEvent("onUpdate")}catch(H){}},getPlay:function(){return w.getPlugin("play")._fireEvent("onUpdate")},getConfig:function(H){return H?k(t):t},getFlashParams:function(){return G},loadPlugin:function(K,J,M,L){if(typeof M=="function"){L=M;M={}}var I=L?e():"_";w._api().fp_loadPlugin(K,J,M,I);var H={};H[I]=L;var N=new l(K,null,w,H);y[K]=N;return N},getState:function(){return w.isLoaded()?v.fp_getState():-1},play:function(I,H){var J=function(){if(I!==undefined){w._api().fp_play(I,H)}else{w._api().fp_play()}};if(w.isLoaded()){J()}else{if(D){setTimeout(function(){w.play(I,H)},50)}else{w.load(function(){J()})}}return w},getVersion:function(){var I="flowplayer.js 3.2.6";if(w.isLoaded()){var H=v.fp_getVersion();H.push(I);return H}return I},_api:function(){if(!w.isLoaded()){throw"Flowplayer "+w.id()+" not loaded when calling an API method"}return v},setClip:function(H){w.setPlaylist([H]);return w},getIndex:function(){return p},_swfHeight:function(){return v.clientHeight}});m(("Click*,Load*,Unload*,Keypress*,Volume*,Mute*,Unmute*,PlaylistReplace,ClipAdd,Fullscreen*,FullscreenExit,Error,MouseOver,MouseOut").split(","),function(){var H="on"+this;if(H.indexOf("*")!=-1){H=H.slice(0,H.length-1);var I="onBefore"+H.slice(2);w[I]=function(J){j(x,I,J);return w}}w[H]=function(J){j(x,H,J);return w}});m(("pause,resume,mute,unmute,stop,toggle,seek,getStatus,getVolume,setVolume,getTime,isPaused,isPlaying,startBuffering,stopBuffering,isFullscreen,toggleFullscreen,reset,close,setPlaylist,addClip,playFeed,setKeyboardShortcutsEnabled,isKeyboardShortcutsEnabled").split(","),function(){var H=this;w[H]=function(J,I){if(!w.isLoaded()){return w}var K=null;if(J!==undefined&&I!==undefined){K=v["fp_"+H](J,I)}else{K=(J===undefined)?v["fp_"+H]():v["fp_"+H](J)}return K==="undefined"||K===undefined?w:K}});w._fireEvent=function(Q){if(typeof Q=="string"){Q=[Q]}var R=Q[0],O=Q[1],M=Q[2],L=Q[3],K=0;if(t.debug){g(Q)}if(!w.isLoaded()&&R=="onLoad"&&O=="player"){v=v||c(r);o=w._swfHeight();m(F,function(){this._fireEvent("onLoad")});m(y,function(S,T){T._fireEvent("onUpdate")});s._fireEvent("onLoad")}if(R=="onLoad"&&O!="player"){return}if(R=="onError"){if(typeof O=="string"||(typeof O=="number"&&typeof M=="number")){O=M;M=L}}if(R=="onContextMenu"){m(t.contextMenu[O],function(S,T){T.call(w)});return}if(R=="onPluginEvent"||R=="onBeforePluginEvent"){var H=O.name||O;var I=y[H];if(I){I._fireEvent("onUpdate",O);return I._fireEvent(M,Q.slice(3))}return}if(R=="onPlaylistReplace"){F=[];var N=0;m(O,function(){F.push(new h(this,N++,w))})}if(R=="onClipAdd"){if(O.isInStream){return}O=new h(O,M,w);F.splice(M,0,O);for(K=M+1;K<F.length;K++){F[K].index++}}var P=true;if(typeof O=="number"&&O<F.length){C=O;var J=F[O];if(J){P=J._fireEvent(R,M,L)}if(!J||P!==false){P=s._fireEvent(R,M,L,J)}}m(x[R],function(){P=this.call(w,O,M);if(this.cached){x[R].splice(K,1)}if(P===false){return false}K++});return P};function B(){if($f(q)){$f(q).getParent().innerHTML="";p=$f(q).getIndex();a[p]=w}else{a.push(w);p=a.length-1}A=parseInt(q.style.height,10)||q.clientHeight;E=q.id||"fp"+e();r=G.id||E+"_api";G.id=r;t.playerId=E;if(typeof t=="string"){t={clip:{url:t}}}if(typeof t.clip=="string"){t.clip={url:t.clip}}t.clip=t.clip||{};if(q.getAttribute("href",2)&&!t.clip.url){t.clip.url=q.getAttribute("href",2)}s=new h(t.clip,-1,w);t.playlist=t.playlist||[t.clip];var I=0;m(t.playlist,function(){var K=this;if(typeof K=="object"&&K.length){K={url:""+K}}m(t.clip,function(L,M){if(M!==undefined&&K[L]===undefined&&typeof M!="function"){K[L]=M}});t.playlist[I]=K;K=new h(K,I,w);F.push(K);I++});m(t,function(K,L){if(typeof L=="function"){if(s[K]){s[K](L)}else{j(x,K,L)}delete t[K]}});m(t.plugins,function(K,L){if(L){y[K]=new l(K,L,w)}});if(!t.plugins||t.plugins.controls===undefined){y.controls=new l("controls",null,w)}y.canvas=new l("canvas",null,w);u=q.innerHTML;function J(L){var K=w.hasiPadSupport&&w.hasiPadSupport();if(/iPad|iPhone|iPod/i.test(navigator.userAgent)&&!/.flv$/i.test(F[0].url)&&!K){return true}if(!w.isLoaded()&&w._fireEvent("onBeforeClick")!==false){w.load()}return f(L)}function H(){if(u.replace(/\s/g,"")!==""){if(q.addEventListener){q.addEventListener("click",J,false)}else{if(q.attachEvent){q.attachEvent("onclick",J)}}}else{if(q.addEventListener){q.addEventListener("click",f,false)}w.load()}}setTimeout(H,0)}if(typeof q=="string"){var z=c(q);if(!z){throw"Flowplayer cannot access element: "+q}q=z;B()}else{B()}}var a=[];function d(o){this.length=o.length;this.each=function(p){m(o,p)};this.size=function(){return o.length}}window.flowplayer=window.$f=function(){var p=null;var o=arguments[0];if(!arguments.length){m(a,function(){if(this.isLoaded()){p=this;return false}});return p||a[0]}if(arguments.length==1){if(typeof o=="number"){return a[o]}else{if(o=="*"){return new d(a)}m(a,function(){if(this.id()==o.id||this.id()==o||this.getParent()==o){p=this;return false}});return p}}if(arguments.length>1){var t=arguments[1],q=(arguments.length==3)?arguments[2]:{};if(typeof t=="string"){t={src:t}}t=i({bgcolor:"#000000",version:[9,0],expressInstall:"http://static.flowplayer.org/swf/expressinstall.swf",cachebusting:false},t);if(typeof o=="string"){if(o.indexOf(".")!=-1){var s=[];m(n(o),function(){s.push(new b(this,k(t),k(q)))});return new d(s)}else{var r=c(o);return new b(r!==null?r:o,t,q)}}else{if(o){return new b(o,t,q)}}}return null};i(window.$f,{fireEvent:function(){var o=[].slice.call(arguments);var q=$f(o[0]);return q?q._fireEvent(o.slice(1)):null},addPlugin:function(o,p){b.prototype[o]=p;return $f},each:m,extend:i});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(q,p){if(!arguments.length||typeof arguments[0]=="number"){var o=[];this.each(function(){var r=$f(this);if(r){o.push(r)}});return arguments.length?o[arguments[0]]:new d(o)}return this.each(function(){$f(this,k(q),p?k(p):{})})}}})();(function(){var e=typeof jQuery=="function";var i={width:"100%",height:"100%",allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:null,onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(e){jQuery.tools=jQuery.tools||{};jQuery.tools.flashembed={version:"1.0.4",conf:i}}function j(){if(c.done){return false}var l=document;if(l&&l.getElementsByTagName&&l.getElementById&&l.body){clearInterval(c.timer);c.timer=null;for(var k=0;k<c.ready.length;k++){c.ready[k].call()}c.ready=null;c.done=true}}var c=e?jQuery:function(k){if(c.done){return k()}if(c.timer){c.ready.push(k)}else{c.ready=[k];c.timer=setInterval(j,13)}};function f(l,k){if(k){for(key in k){if(k.hasOwnProperty(key)){l[key]=k[key]}}}return l}function g(k){switch(h(k)){case"string":k=k.replace(new RegExp('(["\\\\])',"g"),"\\$1");k=k.replace(/^\s?(\d+)%/,"$1pct");return'"'+k+'"';case"array":return"["+b(k,function(n){return g(n)}).join(",")+"]";case"function":return'"function()"';case"object":var l=[];for(var m in k){if(k.hasOwnProperty(m)){l.push('"'+m+'":'+g(k[m]))}}return"{"+l.join(",")+"}"}return String(k).replace(/\s/g," ").replace(/\'/g,'"')}function h(l){if(l===null||l===undefined){return false}var k=typeof l;return(k=="object"&&l.push)?"array":k}if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function b(k,n){var m=[];for(var l in k){if(k.hasOwnProperty(l)){m[l]=n(k[l])}}return m}function a(r,t){var q=f({},r);var s=document.all;var n='<object width="'+q.width+'" height="'+q.height+'"';if(s&&!q.id){q.id="_"+(""+Math.random()).substring(9)}if(q.id){n+=' id="'+q.id+'"'}if(q.cachebusting){q.src+=((q.src.indexOf("?")!=-1?"&":"?")+Math.random())}if(q.w3c||!s){n+=' data="'+q.src+'" type="application/x-shockwave-flash"'}else{n+=' classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'}n+=">";if(q.w3c||s){n+='<param name="movie" value="'+q.src+'" />'}q.width=q.height=q.id=q.w3c=q.src=null;for(var l in q){if(q[l]!==null){n+='<param name="'+l+'" value="'+q[l]+'" />'}}var o="";if(t){for(var m in t){if(t[m]!==null){o+=m+"="+(typeof t[m]=="object"?g(t[m]):t[m])+"&"}}o=o.substring(0,o.length-1);n+='<param name="flashvars" value=\''+o+"' />"}n+="</object>";return n}function d(m,p,l){var k=flashembed.getVersion();f(this,{getContainer:function(){return m},getConf:function(){return p},getVersion:function(){return k},getFlashvars:function(){return l},getApi:function(){return m.firstChild},getHTML:function(){return a(p,l)}});var q=p.version;var r=p.expressInstall;var o=!q||flashembed.isSupported(q);if(o){p.onFail=p.version=p.expressInstall=null;m.innerHTML=a(p,l)}else{if(q&&r&&flashembed.isSupported([6,65])){f(p,{src:r});l={MMredirectURL:location.href,MMplayerType:"PlugIn",MMdoctitle:document.title};m.innerHTML=a(p,l)}else{if(m.innerHTML.replace(/\s/g,"")!==""){}else{m.innerHTML="<h2>Flash version "+q+" or greater is required</h2><h3>"+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+"</h3>"+(m.tagName=="A"?"<p>Click here to download latest version</p>":"<p>Download latest version from <a href='http://www.adobe.com/go/getflashplayer'>here</a></p>");if(m.tagName=="A"){m.onclick=function(){location.href="http://www.adobe.com/go/getflashplayer"}}}}}if(!o&&p.onFail){var n=p.onFail.call(this);if(typeof n=="string"){m.innerHTML=n}}if(document.all){window[p.id]=document.getElementById(p.id)}}window.flashembed=function(l,m,k){if(typeof l=="string"){var n=document.getElementById(l);if(n){l=n}else{c(function(){flashembed(l,m,k)});return}}if(!l){return}if(typeof m=="string"){m={src:m}}var o=f({},i);f(o,m);return new d(l,o,k)};f(window.flashembed,{getVersion:function(){var m=[0,0];if(navigator.plugins&&typeof navigator.plugins["Shockwave Flash"]=="object"){var l=navigator.plugins["Shockwave Flash"].description;if(typeof l!="undefined"){l=l.replace(/^.*\s+(\S+\s+\S+$)/,"$1");var n=parseInt(l.replace(/^(.*)\..*$/,"$1"),10);var r=/r/.test(l)?parseInt(l.replace(/^.*r(.*)$/,"$1"),10):0;m=[n,r]}}else{if(window.ActiveXObject){try{var p=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7")}catch(q){try{p=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");m=[6,0];p.AllowScriptAccess="always"}catch(k){if(m[0]==6){return m}}try{p=new ActiveXObject("ShockwaveFlash.ShockwaveFlash")}catch(o){}}if(typeof p=="object"){l=p.GetVariable("$version");if(typeof l!="undefined"){l=l.replace(/^\S+\s+(.*)$/,"$1").split(",");m=[parseInt(l[0],10),parseInt(l[2],10)]}}}}return m},isSupported:function(k){var m=flashembed.getVersion();var l=(m[0]>k[0])||(m[0]==k[0]&&m[1]>=k[1]);return l},domReady:c,asString:g,getHTML:a});if(e){jQuery.fn.flashembed=function(l,k){var m=null;this.each(function(){m=flashembed(this,l,k)});return l.api===false?this:m}}})(); \ No newline at end of file diff --git a/bbb-screenshare/jws/player/flowplayer/flowplayer-3.2.7.swf b/bbb-screenshare/jws/player/flowplayer/flowplayer-3.2.7.swf new file mode 100755 index 0000000000000000000000000000000000000000..20a41193f5ba6f63745632946e8ff6c1e52313ca Binary files /dev/null and b/bbb-screenshare/jws/player/flowplayer/flowplayer-3.2.7.swf differ diff --git a/bbb-screenshare/jws/player/flowplayer/flowplayer.controls-3.2.5.swf b/bbb-screenshare/jws/player/flowplayer/flowplayer.controls-3.2.5.swf new file mode 100755 index 0000000000000000000000000000000000000000..5507a531ddcc0baccaf1339c852ee1623dfd5b05 Binary files /dev/null and b/bbb-screenshare/jws/player/flowplayer/flowplayer.controls-3.2.5.swf differ diff --git a/bbb-screenshare/jws/player/flowplayer/flowplayer.rtmp-3.2.3.swf b/bbb-screenshare/jws/player/flowplayer/flowplayer.rtmp-3.2.3.swf new file mode 100755 index 0000000000000000000000000000000000000000..43f4c1961cf3be054481b068bed8b239dd9fa6cc Binary files /dev/null and b/bbb-screenshare/jws/player/flowplayer/flowplayer.rtmp-3.2.3.swf differ diff --git a/bbb-screenshare/jws/player/flowplayer/index.html b/bbb-screenshare/jws/player/flowplayer/index.html new file mode 100755 index 0000000000000000000000000000000000000000..6e9e51897f02ef5d8562b5ef9f8eb098f9bbe249 --- /dev/null +++ b/bbb-screenshare/jws/player/flowplayer/index.html @@ -0,0 +1,29 @@ +<html> + <head> + <script src="example/flowplayer-3.2.6.min.js"></script> + </head> + <body> + + <a class="rtmp" href="320x2401-1328884730718" style="display:block;width:320px;height:240px;" id="player">stream</a> + + <script language="JavaScript"> + $f("a.rtmp", "flowplayer-3.2.7.swf", { + clip: { + url: '320x2401-1328884730718', + provider: 'rtmp', + live: true, + bufferLength: 1, + autoPlay: true, + }, + plugins: { + rtmp: { + url: 'flowplayer.rtmp-3.2.3.swf', + netConnectionUrl: 'rtmp://143.54.12.217/video/183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1328884719358' + } + } + }); + </script> + + </body> +</html> + diff --git a/bbb-screenshare/jws/player/index.html b/bbb-screenshare/jws/player/index.html new file mode 100755 index 0000000000000000000000000000000000000000..23d46b1482620691b271fb5c1395bc1b8e23cc98 --- /dev/null +++ b/bbb-screenshare/jws/player/index.html @@ -0,0 +1,46 @@ +<html> + <head> + <title>bbb-video-stream-html-client</title> + + <link href="style.css" rel="stylesheet" type="text/css"> + <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> + <script type="text/javascript" src="actions.js"></script> + + <script type="text/javascript" src="jw-player/jwplayer.js"></script> + <script type="text/javascript" src="flowplayer/example/flowplayer-3.2.6.min.js"></script> + </head> + <body> + + <a href="https://github.com/daronco/bbb-video-stream-html-client"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png" alt="Fork me on GitHub"></a> + + <div id="menu"> + + <form action="#"> + <div class="field"> + <label class="label" for="server_ip">Server IP</label> + <input type="text" id="server_ip" name="server_ip" value="192.168.23.23" /> + </div> + <div class="field"> + <label class="label" for="meeting_id">Meeting ID</label> + <input type="text" id="meeting_id" name="meeting_id" value="foo" /> + </div> + <div class="field"> + <label class="label" for="stream_id">Stream ID</label> + <input type="text" id="stream_id" name="stream_id" value="room2" /> + </div> + <div class="field"> + <label class="label">Player</label> + <input type="radio" name="player" value="jw-player"> jw-player + <input type="radio" name="player" value="flowplayer" checked="checked"> flowplayer + <button type="button" id="submit">Submit</button> + </div> + </form> + + </div> + + <div id="content"> + <div id="wrapper">Your video will be played here...</div> + </div> + + </body> +</html> diff --git a/bbb-screenshare/jws/player/jw-player/index.html b/bbb-screenshare/jws/player/jw-player/index.html new file mode 100755 index 0000000000000000000000000000000000000000..cde33954e79b7cf906ffff744741cb9b736735f2 --- /dev/null +++ b/bbb-screenshare/jws/player/jw-player/index.html @@ -0,0 +1,48 @@ +<html> + <head> + <script type='text/javascript' src='jwplayer.js'></script> + <!--<script type="text/javascript" + src="http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js"> + </script>--> + </head> + <body> + + <div id='mediaplayer'></div> + + <script type="text/javascript"> + jwplayer('mediaplayer').setup({ + flashplayer: 'player.swf', + id: 'playerID', + width: '320', + height: '240', + file: '320x2401-1328884730718', + streamer: 'rtmp://143.54.12.217/video/183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1328884719358', + autostart: 'true', + provider: 'rtmp', + duration: '0', + bufferlength: '1', // it's not working + //start: '0', + //live: 'true', + //repeat: 'none', + }); + </script> + + <!-- using swfobject + <div id='container'>The player will be placed here</div> + <script type="text/javascript"> + var flashvars = { + file:'320x2401-1328884730718', + bufferlength: '1', + streamer: 'rtmp://143.54.12.217/video/183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1328884719358/', + autostart: 'true', + }; + swfobject.embedSWF('player.swf','container','320','240','9.0.115','false', flashvars, + {allowfullscreen:'true',allowscriptaccess:'always', wmode:'transparent'}, + {id:'jwplayer',name:'jwplayer'} + ); + </script> + --> + + </body> +</html> + diff --git a/bbb-screenshare/jws/player/jw-player/jwplayer.js b/bbb-screenshare/jws/player/jw-player/jwplayer.js new file mode 100755 index 0000000000000000000000000000000000000000..5e8a20d1c3d152e1e054109d037d34f8462c303a --- /dev/null +++ b/bbb-screenshare/jws/player/jw-player/jwplayer.js @@ -0,0 +1 @@ +if(typeof jwplayer=="undefined"){var jwplayer=function(a){if(jwplayer.api){return jwplayer.api.selectPlayer(a)}};var $jw=jwplayer;jwplayer.version="5.9.2118";jwplayer.vid=document.createElement("video");jwplayer.audio=document.createElement("audio");jwplayer.source=document.createElement("source");(function(b){b.utils=function(){};b.utils.typeOf=function(d){var c=typeof d;if(c==="object"){if(d){if(d instanceof Array){c="array"}}else{c="null"}}return c};b.utils.extend=function(){var c=b.utils.extend["arguments"];if(c.length>1){for(var e=1;e<c.length;e++){for(var d in c[e]){c[0][d]=c[e][d]}}return c[0]}return null};b.utils.clone=function(f){var c;var d=b.utils.clone["arguments"];if(d.length==1){switch(b.utils.typeOf(d[0])){case"object":c={};for(var e in d[0]){c[e]=b.utils.clone(d[0][e])}break;case"array":c=[];for(var e in d[0]){c[e]=b.utils.clone(d[0][e])}break;default:return d[0];break}}return c};b.utils.extension=function(c){if(!c){return""}c=c.substring(c.lastIndexOf("/")+1,c.length);c=c.split("?")[0];if(c.lastIndexOf(".")>-1){return c.substr(c.lastIndexOf(".")+1,c.length).toLowerCase()}return};b.utils.html=function(c,d){c.innerHTML=d};b.utils.wrap=function(c,d){if(c.parentNode){c.parentNode.replaceChild(d,c)}d.appendChild(c)};b.utils.ajax=function(g,f,c){var e;if(window.XMLHttpRequest){e=new XMLHttpRequest()}else{e=new ActiveXObject("Microsoft.XMLHTTP")}e.onreadystatechange=function(){if(e.readyState===4){if(e.status===200){if(f){if(!b.utils.exists(e.responseXML)){try{if(window.DOMParser){var h=(new DOMParser()).parseFromString(e.responseText,"text/xml");if(h){e=b.utils.extend({},e,{responseXML:h})}}else{h=new ActiveXObject("Microsoft.XMLDOM");h.async="false";h.loadXML(e.responseText);e=b.utils.extend({},e,{responseXML:h})}}catch(j){if(c){c(g)}}}f(e)}}else{if(c){c(g)}}}};try{e.open("GET",g,true);e.send(null)}catch(d){if(c){c(g)}}return e};b.utils.load=function(d,e,c){d.onreadystatechange=function(){if(d.readyState===4){if(d.status===200){if(e){e()}}else{if(c){c()}}}}};b.utils.find=function(d,c){return d.getElementsByTagName(c)};b.utils.append=function(c,d){c.appendChild(d)};b.utils.isIE=function(){return((!+"\v1")||(typeof window.ActiveXObject!="undefined"))};b.utils.userAgentMatch=function(d){var c=navigator.userAgent.toLowerCase();return(c.match(d)!==null)};b.utils.isIOS=function(){return b.utils.userAgentMatch(/iP(hone|ad|od)/i)};b.utils.isIPad=function(){return b.utils.userAgentMatch(/iPad/i)};b.utils.isIPod=function(){return b.utils.userAgentMatch(/iP(hone|od)/i)};b.utils.isAndroid=function(){return b.utils.userAgentMatch(/android/i)};b.utils.isLegacyAndroid=function(){return b.utils.userAgentMatch(/android 2.[012]/i)};b.utils.isBlackberry=function(){return b.utils.userAgentMatch(/blackberry/i)};b.utils.isMobile=function(){return b.utils.userAgentMatch(/(iP(hone|ad|od))|android/i)};b.utils.getFirstPlaylistItemFromConfig=function(c){var d={};var e;if(c.playlist&&c.playlist.length){e=c.playlist[0]}else{e=c}d.file=e.file;d.levels=e.levels;d.streamer=e.streamer;d.playlistfile=e.playlistfile;d.provider=e.provider;if(!d.provider){if(d.file&&(d.file.toLowerCase().indexOf("youtube.com")>-1||d.file.toLowerCase().indexOf("youtu.be")>-1)){d.provider="youtube"}if(d.streamer&&d.streamer.toLowerCase().indexOf("rtmp://")==0){d.provider="rtmp"}if(e.type){d.provider=e.type.toLowerCase()}}if(d.provider=="audio"){d.provider="sound"}return d};b.utils.getOuterHTML=function(c){if(c.outerHTML){return c.outerHTML}else{try{return new XMLSerializer().serializeToString(c)}catch(d){return""}}};b.utils.setOuterHTML=function(f,e){if(f.outerHTML){f.outerHTML=e}else{var g=document.createElement("div");g.innerHTML=e;var c=document.createRange();c.selectNodeContents(g);var d=c.extractContents();f.parentNode.insertBefore(d,f);f.parentNode.removeChild(f)}};b.utils.hasFlash=function(){if(typeof navigator.plugins!="undefined"&&typeof navigator.plugins["Shockwave Flash"]!="undefined"){return true}if(typeof window.ActiveXObject!="undefined"){try{new ActiveXObject("ShockwaveFlash.ShockwaveFlash");return true}catch(c){}}return false};b.utils.getPluginName=function(c){if(c.lastIndexOf("/")>=0){c=c.substring(c.lastIndexOf("/")+1,c.length)}if(c.lastIndexOf("-")>=0){c=c.substring(0,c.lastIndexOf("-"))}if(c.lastIndexOf(".swf")>=0){c=c.substring(0,c.lastIndexOf(".swf"))}if(c.lastIndexOf(".js")>=0){c=c.substring(0,c.lastIndexOf(".js"))}return c};b.utils.getPluginVersion=function(c){if(c.lastIndexOf("-")>=0){if(c.lastIndexOf(".js")>=0){return c.substring(c.lastIndexOf("-")+1,c.lastIndexOf(".js"))}else{if(c.lastIndexOf(".swf")>=0){return c.substring(c.lastIndexOf("-")+1,c.lastIndexOf(".swf"))}else{return c.substring(c.lastIndexOf("-")+1)}}}return""};b.utils.getAbsolutePath=function(j,h){if(!b.utils.exists(h)){h=document.location.href}if(!b.utils.exists(j)){return undefined}if(a(j)){return j}var k=h.substring(0,h.indexOf("://")+3);var g=h.substring(k.length,h.indexOf("/",k.length+1));var d;if(j.indexOf("/")===0){d=j.split("/")}else{var e=h.split("?")[0];e=e.substring(k.length+g.length+1,e.lastIndexOf("/"));d=e.split("/").concat(j.split("/"))}var c=[];for(var f=0;f<d.length;f++){if(!d[f]||!b.utils.exists(d[f])||d[f]=="."){continue}else{if(d[f]==".."){c.pop()}else{c.push(d[f])}}}return k+g+"/"+c.join("/")};function a(d){if(!b.utils.exists(d)){return}var e=d.indexOf("://");var c=d.indexOf("?");return(e>0&&(c<0||(c>e)))}b.utils.pluginPathType={ABSOLUTE:"ABSOLUTE",RELATIVE:"RELATIVE",CDN:"CDN"};b.utils.getPluginPathType=function(d){if(typeof d!="string"){return}d=d.split("?")[0];var e=d.indexOf("://");if(e>0){return b.utils.pluginPathType.ABSOLUTE}var c=d.indexOf("/");var f=b.utils.extension(d);if(e<0&&c<0&&(!f||!isNaN(f))){return b.utils.pluginPathType.CDN}return b.utils.pluginPathType.RELATIVE};b.utils.mapEmpty=function(c){for(var d in c){return false}return true};b.utils.mapLength=function(d){var c=0;for(var e in d){c++}return c};b.utils.log=function(d,c){if(typeof console!="undefined"&&typeof console.log!="undefined"){if(c){console.log(d,c)}else{console.log(d)}}};b.utils.css=function(d,g,c){if(b.utils.exists(d)){for(var e in g){try{if(typeof g[e]==="undefined"){continue}else{if(typeof g[e]=="number"&&!(e=="zIndex"||e=="opacity")){if(isNaN(g[e])){continue}if(e.match(/color/i)){g[e]="#"+b.utils.strings.pad(g[e].toString(16),6)}else{g[e]=Math.ceil(g[e])+"px"}}}d.style[e]=g[e]}catch(f){}}}};b.utils.isYouTube=function(c){return(c.indexOf("youtube.com")>-1||c.indexOf("youtu.be")>-1)};b.utils.transform=function(e,d,c,g,h){if(!b.utils.exists(d)){d=1}if(!b.utils.exists(c)){c=1}if(!b.utils.exists(g)){g=0}if(!b.utils.exists(h)){h=0}if(d==1&&c==1&&g==0&&h==0){e.style.webkitTransform="";e.style.MozTransform="";e.style.OTransform=""}else{var f="scale("+d+","+c+") translate("+g+"px,"+h+"px)";e.style.webkitTransform=f;e.style.MozTransform=f;e.style.OTransform=f}};b.utils.stretch=function(k,q,p,g,n,h){if(typeof p=="undefined"||typeof g=="undefined"||typeof n=="undefined"||typeof h=="undefined"){return}var d=p/n;var f=g/h;var m=0;var l=0;var e=false;var c={};if(q.parentElement){q.parentElement.style.overflow="hidden"}b.utils.transform(q);switch(k.toUpperCase()){case b.utils.stretching.NONE:c.width=n;c.height=h;c.top=(g-c.height)/2;c.left=(p-c.width)/2;break;case b.utils.stretching.UNIFORM:if(d>f){c.width=n*f;c.height=h*f;if(c.width/p>0.95){e=true;d=Math.ceil(100*p/c.width)/100;f=1;c.width=p}}else{c.width=n*d;c.height=h*d;if(c.height/g>0.95){e=true;d=1;f=Math.ceil(100*g/c.height)/100;c.height=g}}c.top=(g-c.height)/2;c.left=(p-c.width)/2;break;case b.utils.stretching.FILL:if(d>f){c.width=n*d;c.height=h*d}else{c.width=n*f;c.height=h*f}c.top=(g-c.height)/2;c.left=(p-c.width)/2;break;case b.utils.stretching.EXACTFIT:c.width=n;c.height=h;var o=Math.round((n/2)*(1-1/d));var j=Math.round((h/2)*(1-1/f));e=true;c.top=c.left=0;break;default:break}if(e){b.utils.transform(q,d,f,o,j)}b.utils.css(q,c)};b.utils.stretching={NONE:"NONE",FILL:"FILL",UNIFORM:"UNIFORM",EXACTFIT:"EXACTFIT"};b.utils.deepReplaceKeyName=function(k,e,c){switch(b.utils.typeOf(k)){case"array":for(var g=0;g<k.length;g++){k[g]=b.utils.deepReplaceKeyName(k[g],e,c)}break;case"object":for(var f in k){var j,h;if(e instanceof Array&&c instanceof Array){if(e.length!=c.length){continue}else{j=e;h=c}}else{j=[e];h=[c]}var d=f;for(var g=0;g<j.length;g++){d=d.replace(new RegExp(e[g],"g"),c[g])}k[d]=b.utils.deepReplaceKeyName(k[f],e,c);if(f!=d){delete k[f]}}break}return k};b.utils.isInArray=function(e,d){if(!(e)||!(e instanceof Array)){return false}for(var c=0;c<e.length;c++){if(d===e[c]){return true}}return false};b.utils.exists=function(c){switch(typeof(c)){case"string":return(c.length>0);break;case"object":return(c!==null);case"undefined":return false}return true};b.utils.empty=function(c){if(typeof c.hasChildNodes=="function"){while(c.hasChildNodes()){c.removeChild(c.firstChild)}}};b.utils.parseDimension=function(c){if(typeof c=="string"){if(c===""){return 0}else{if(c.lastIndexOf("%")>-1){return c}else{return parseInt(c.replace("px",""),10)}}}return c};b.utils.getDimensions=function(c){if(c&&c.style){return{x:b.utils.parseDimension(c.style.left),y:b.utils.parseDimension(c.style.top),width:b.utils.parseDimension(c.style.width),height:b.utils.parseDimension(c.style.height)}}else{return{}}};b.utils.getElementWidth=function(c){if(!c){return null}else{if(c==document.body){return b.utils.parentNode(c).clientWidth}else{if(c.clientWidth>0){return c.clientWidth}else{if(c.style){return b.utils.parseDimension(c.style.width)}else{return null}}}}};b.utils.getElementHeight=function(c){if(!c){return null}else{if(c==document.body){return b.utils.parentNode(c).clientHeight}else{if(c.clientHeight>0){return c.clientHeight}else{if(c.style){return b.utils.parseDimension(c.style.height)}else{return null}}}}};b.utils.timeFormat=function(c){str="00:00";if(c>0){str=Math.floor(c/60)<10?"0"+Math.floor(c/60)+":":Math.floor(c/60)+":";str+=Math.floor(c%60)<10?"0"+Math.floor(c%60):Math.floor(c%60)}return str};b.utils.useNativeFullscreen=function(){return(navigator&&navigator.vendor&&navigator.vendor.indexOf("Apple")==0)};b.utils.parentNode=function(c){if(!c){return docuemnt.body}else{if(c.parentNode){return c.parentNode}else{if(c.parentElement){return c.parentElement}else{return c}}}};b.utils.getBoundingClientRect=function(c){if(typeof c.getBoundingClientRect=="function"){return c.getBoundingClientRect()}else{return{left:c.offsetLeft+document.body.scrollLeft,top:c.offsetTop+document.body.scrollTop,width:c.offsetWidth,height:c.offsetHeight}}};b.utils.translateEventResponse=function(e,c){var g=b.utils.extend({},c);if(e==b.api.events.JWPLAYER_FULLSCREEN&&!g.fullscreen){g.fullscreen=g.message=="true"?true:false;delete g.message}else{if(typeof g.data=="object"){g=b.utils.extend(g,g.data);delete g.data}else{if(typeof g.metadata=="object"){b.utils.deepReplaceKeyName(g.metadata,["__dot__","__spc__","__dsh__"],["."," ","-"])}}}var d=["position","duration","offset"];for(var f in d){if(g[d[f]]){g[d[f]]=Math.round(g[d[f]]*1000)/1000}}return g};b.utils.saveCookie=function(c,d){document.cookie="jwplayer."+c+"="+d+"; path=/"};b.utils.getCookies=function(){var f={};var e=document.cookie.split("; ");for(var d=0;d<e.length;d++){var c=e[d].split("=");if(c[0].indexOf("jwplayer.")==0){f[c[0].substring(9,c[0].length)]=c[1]}}return f};b.utils.readCookie=function(c){return b.utils.getCookies()[c]}})(jwplayer);(function(a){a.events=function(){};a.events.COMPLETE="COMPLETE";a.events.ERROR="ERROR"})(jwplayer);(function(jwplayer){jwplayer.events.eventdispatcher=function(debug){var _debug=debug;var _listeners;var _globallisteners;this.resetEventListeners=function(){_listeners={};_globallisteners=[]};this.resetEventListeners();this.addEventListener=function(type,listener,count){try{if(!jwplayer.utils.exists(_listeners[type])){_listeners[type]=[]}if(typeof(listener)=="string"){eval("listener = "+listener)}_listeners[type].push({listener:listener,count:count})}catch(err){jwplayer.utils.log("error",err)}return false};this.removeEventListener=function(type,listener){if(!_listeners[type]){return}try{for(var listenerIndex=0;listenerIndex<_listeners[type].length;listenerIndex++){if(_listeners[type][listenerIndex].listener.toString()==listener.toString()){_listeners[type].splice(listenerIndex,1);break}}}catch(err){jwplayer.utils.log("error",err)}return false};this.addGlobalListener=function(listener,count){try{if(typeof(listener)=="string"){eval("listener = "+listener)}_globallisteners.push({listener:listener,count:count})}catch(err){jwplayer.utils.log("error",err)}return false};this.removeGlobalListener=function(listener){if(!listener){return}try{for(var globalListenerIndex=0;globalListenerIndex<_globallisteners.length;globalListenerIndex++){if(_globallisteners[globalListenerIndex].listener.toString()==listener.toString()){_globallisteners.splice(globalListenerIndex,1);break}}}catch(err){jwplayer.utils.log("error",err)}return false};this.sendEvent=function(type,data){if(!jwplayer.utils.exists(data)){data={}}if(_debug){jwplayer.utils.log(type,data)}if(typeof _listeners[type]!="undefined"){for(var listenerIndex=0;listenerIndex<_listeners[type].length;listenerIndex++){try{_listeners[type][listenerIndex].listener(data)}catch(err){jwplayer.utils.log("There was an error while handling a listener: "+err.toString(),_listeners[type][listenerIndex].listener)}if(_listeners[type][listenerIndex]){if(_listeners[type][listenerIndex].count===1){delete _listeners[type][listenerIndex]}else{if(_listeners[type][listenerIndex].count>0){_listeners[type][listenerIndex].count=_listeners[type][listenerIndex].count-1}}}}}for(var globalListenerIndex=0;globalListenerIndex<_globallisteners.length;globalListenerIndex++){try{_globallisteners[globalListenerIndex].listener(data)}catch(err){jwplayer.utils.log("There was an error while handling a listener: "+err.toString(),_globallisteners[globalListenerIndex].listener)}if(_globallisteners[globalListenerIndex]){if(_globallisteners[globalListenerIndex].count===1){delete _globallisteners[globalListenerIndex]}else{if(_globallisteners[globalListenerIndex].count>0){_globallisteners[globalListenerIndex].count=_globallisteners[globalListenerIndex].count-1}}}}}}})(jwplayer);(function(a){var b={};a.utils.animations=function(){};a.utils.animations.transform=function(c,d){c.style.webkitTransform=d;c.style.MozTransform=d;c.style.OTransform=d;c.style.msTransform=d};a.utils.animations.transformOrigin=function(c,d){c.style.webkitTransformOrigin=d;c.style.MozTransformOrigin=d;c.style.OTransformOrigin=d;c.style.msTransformOrigin=d};a.utils.animations.rotate=function(c,d){a.utils.animations.transform(c,["rotate(",d,"deg)"].join(""))};a.utils.cancelAnimation=function(c){delete b[c.id]};a.utils.fadeTo=function(m,f,e,j,h,d){if(b[m.id]!=d&&a.utils.exists(d)){return}if(m.style.opacity==f){return}var c=new Date().getTime();if(d>c){setTimeout(function(){a.utils.fadeTo(m,f,e,j,0,d)},d-c)}if(m.style.display=="none"){m.style.display="block"}if(!a.utils.exists(j)){j=m.style.opacity===""?1:m.style.opacity}if(m.style.opacity==f&&m.style.opacity!==""&&a.utils.exists(d)){if(f===0){m.style.display="none"}return}if(!a.utils.exists(d)){d=c;b[m.id]=d}if(!a.utils.exists(h)){h=0}var k=(e>0)?((c-d)/(e*1000)):0;k=k>1?1:k;var l=f-j;var g=j+(k*l);if(g>1){g=1}else{if(g<0){g=0}}m.style.opacity=g;if(h>0){b[m.id]=d+h*1000;a.utils.fadeTo(m,f,e,j,0,b[m.id]);return}setTimeout(function(){a.utils.fadeTo(m,f,e,j,0,d)},10)}})(jwplayer);(function(a){a.utils.arrays=function(){};a.utils.arrays.indexOf=function(c,d){for(var b=0;b<c.length;b++){if(c[b]==d){return b}}return -1};a.utils.arrays.remove=function(c,d){var b=a.utils.arrays.indexOf(c,d);if(b>-1){c.splice(b,1)}}})(jwplayer);(function(a){a.utils.extensionmap={"3gp":{html5:"video/3gpp",flash:"video"},"3gpp":{html5:"video/3gpp"},"3g2":{html5:"video/3gpp2",flash:"video"},"3gpp2":{html5:"video/3gpp2"},flv:{flash:"video"},f4a:{html5:"audio/mp4"},f4b:{html5:"audio/mp4",flash:"video"},f4v:{html5:"video/mp4",flash:"video"},mov:{html5:"video/quicktime",flash:"video"},m4a:{html5:"audio/mp4",flash:"video"},m4b:{html5:"audio/mp4"},m4p:{html5:"audio/mp4"},m4v:{html5:"video/mp4",flash:"video"},mp4:{html5:"video/mp4",flash:"video"},rbs:{flash:"sound"},aac:{html5:"audio/aac",flash:"video"},mp3:{html5:"audio/mp3",flash:"sound"},ogg:{html5:"audio/ogg"},oga:{html5:"audio/ogg"},ogv:{html5:"video/ogg"},webm:{html5:"video/webm"},m3u8:{html5:"audio/x-mpegurl"},gif:{flash:"image"},jpeg:{flash:"image"},jpg:{flash:"image"},swf:{flash:"image"},png:{flash:"image"},wav:{html5:"audio/x-wav"}}})(jwplayer);(function(e){e.utils.mediaparser=function(){};var g={element:{width:"width",height:"height",id:"id","class":"className",name:"name"},media:{src:"file",preload:"preload",autoplay:"autostart",loop:"repeat",controls:"controls"},source:{src:"file",type:"type",media:"media","data-jw-width":"width","data-jw-bitrate":"bitrate"},video:{poster:"image"}};var f={};e.utils.mediaparser.parseMedia=function(j){return d(j)};function c(k,j){if(!e.utils.exists(j)){j=g[k]}else{e.utils.extend(j,g[k])}return j}function d(n,j){if(f[n.tagName.toLowerCase()]&&!e.utils.exists(j)){return f[n.tagName.toLowerCase()](n)}else{j=c("element",j);var o={};for(var k in j){if(k!="length"){var m=n.getAttribute(k);if(e.utils.exists(m)){o[j[k]]=m}}}var l=n.style["#background-color"];if(l&&!(l=="transparent"||l=="rgba(0, 0, 0, 0)")){o.screencolor=l}return o}}function h(n,k){k=c("media",k);var l=[];var j=e.utils.selectors("source",n);for(var m in j){if(!isNaN(m)){l.push(a(j[m]))}}var o=d(n,k);if(e.utils.exists(o.file)){l[0]={file:o.file}}o.levels=l;return o}function a(l,k){k=c("source",k);var j=d(l,k);j.width=j.width?j.width:0;j.bitrate=j.bitrate?j.bitrate:0;return j}function b(l,k){k=c("video",k);var j=h(l,k);return j}f.media=h;f.audio=h;f.source=a;f.video=b})(jwplayer);(function(a){a.utils.loaderstatus={NEW:"NEW",LOADING:"LOADING",ERROR:"ERROR",COMPLETE:"COMPLETE"};a.utils.scriptloader=function(c){var d=a.utils.loaderstatus.NEW;var b=new a.events.eventdispatcher();a.utils.extend(this,b);this.load=function(){if(d==a.utils.loaderstatus.NEW){d=a.utils.loaderstatus.LOADING;var e=document.createElement("script");e.onload=function(f){d=a.utils.loaderstatus.COMPLETE;b.sendEvent(a.events.COMPLETE)};e.onerror=function(f){d=a.utils.loaderstatus.ERROR;b.sendEvent(a.events.ERROR)};e.onreadystatechange=function(){if(e.readyState=="loaded"||e.readyState=="complete"){d=a.utils.loaderstatus.COMPLETE;b.sendEvent(a.events.COMPLETE)}};document.getElementsByTagName("head")[0].appendChild(e);e.src=c}};this.getStatus=function(){return d}}})(jwplayer);(function(a){a.utils.selectors=function(b,e){if(!a.utils.exists(e)){e=document}b=a.utils.strings.trim(b);var c=b.charAt(0);if(c=="#"){return e.getElementById(b.substr(1))}else{if(c=="."){if(e.getElementsByClassName){return e.getElementsByClassName(b.substr(1))}else{return a.utils.selectors.getElementsByTagAndClass("*",b.substr(1))}}else{if(b.indexOf(".")>0){var d=b.split(".");return a.utils.selectors.getElementsByTagAndClass(d[0],d[1])}else{return e.getElementsByTagName(b)}}}return null};a.utils.selectors.getElementsByTagAndClass=function(e,h,g){var j=[];if(!a.utils.exists(g)){g=document}var f=g.getElementsByTagName(e);for(var d=0;d<f.length;d++){if(a.utils.exists(f[d].className)){var c=f[d].className.split(" ");for(var b=0;b<c.length;b++){if(c[b]==h){j.push(f[d])}}}}return j}})(jwplayer);(function(a){a.utils.strings=function(){};a.utils.strings.trim=function(b){return b.replace(/^\s*/,"").replace(/\s*$/,"")};a.utils.strings.pad=function(c,d,b){if(!b){b="0"}while(c.length<d){c=b+c}return c};a.utils.strings.serialize=function(b){if(b==null){return null}else{if(b=="true"){return true}else{if(b=="false"){return false}else{if(isNaN(Number(b))||b.length>5||b.length==0){return b}else{return Number(b)}}}}};a.utils.strings.seconds=function(d){d=d.replace(",",".");var b=d.split(":");var c=0;if(d.substr(-1)=="s"){c=Number(d.substr(0,d.length-1))}else{if(d.substr(-1)=="m"){c=Number(d.substr(0,d.length-1))*60}else{if(d.substr(-1)=="h"){c=Number(d.substr(0,d.length-1))*3600}else{if(b.length>1){c=Number(b[b.length-1]);c+=Number(b[b.length-2])*60;if(b.length==3){c+=Number(b[b.length-3])*3600}}else{c=Number(d)}}}}return c};a.utils.strings.xmlAttribute=function(b,c){for(var d=0;d<b.attributes.length;d++){if(b.attributes[d].name&&b.attributes[d].name.toLowerCase()==c.toLowerCase()){return b.attributes[d].value.toString()}}return""};a.utils.strings.jsonToString=function(f){var h=h||{};if(h&&h.stringify){return h.stringify(f)}var c=typeof(f);if(c!="object"||f===null){if(c=="string"){f='"'+f.replace(/"/g,'\\"')+'"'}else{return String(f)}}else{var g=[],b=(f&&f.constructor==Array);for(var d in f){var e=f[d];switch(typeof(e)){case"string":e='"'+e.replace(/"/g,'\\"')+'"';break;case"object":if(a.utils.exists(e)){e=a.utils.strings.jsonToString(e)}break}if(b){if(typeof(e)!="function"){g.push(String(e))}}else{if(typeof(e)!="function"){g.push('"'+d+'":'+String(e))}}}if(b){return"["+String(g)+"]"}else{return"{"+String(g)+"}"}}}})(jwplayer);(function(c){var d=new RegExp(/^(#|0x)[0-9a-fA-F]{3,6}/);c.utils.typechecker=function(g,f){f=!c.utils.exists(f)?b(g):f;return e(g,f)};function b(f){var g=["true","false","t","f"];if(g.toString().indexOf(f.toLowerCase().replace(" ",""))>=0){return"boolean"}else{if(d.test(f)){return"color"}else{if(!isNaN(parseInt(f,10))&&parseInt(f,10).toString().length==f.length){return"integer"}else{if(!isNaN(parseFloat(f))&&parseFloat(f).toString().length==f.length){return"float"}}}}return"string"}function e(g,f){if(!c.utils.exists(f)){return g}switch(f){case"color":if(g.length>0){return a(g)}return null;case"integer":return parseInt(g,10);case"float":return parseFloat(g);case"boolean":if(g.toLowerCase()=="true"){return true}else{if(g=="1"){return true}}return false}return g}function a(f){switch(f.toLowerCase()){case"blue":return parseInt("0000FF",16);case"green":return parseInt("00FF00",16);case"red":return parseInt("FF0000",16);case"cyan":return parseInt("00FFFF",16);case"magenta":return parseInt("FF00FF",16);case"yellow":return parseInt("FFFF00",16);case"black":return parseInt("000000",16);case"white":return parseInt("FFFFFF",16);default:f=f.replace(/(#|0x)?([0-9A-F]{3,6})$/gi,"$2");if(f.length==3){f=f.charAt(0)+f.charAt(0)+f.charAt(1)+f.charAt(1)+f.charAt(2)+f.charAt(2)}return parseInt(f,16)}return parseInt("000000",16)}})(jwplayer);(function(a){a.utils.parsers=function(){};a.utils.parsers.localName=function(b){if(!b){return""}else{if(b.localName){return b.localName}else{if(b.baseName){return b.baseName}else{return""}}}};a.utils.parsers.textContent=function(b){if(!b){return""}else{if(b.textContent){return b.textContent}else{if(b.text){return b.text}else{return""}}}}})(jwplayer);(function(a){a.utils.parsers.jwparser=function(){};a.utils.parsers.jwparser.PREFIX="jwplayer";a.utils.parsers.jwparser.parseEntry=function(c,d){for(var b=0;b<c.childNodes.length;b++){if(c.childNodes[b].prefix==a.utils.parsers.jwparser.PREFIX){d[a.utils.parsers.localName(c.childNodes[b])]=a.utils.strings.serialize(a.utils.parsers.textContent(c.childNodes[b]));if(a.utils.parsers.localName(c.childNodes[b])=="file"&&d.levels){delete d.levels}}if(!d.file&&String(d.link).toLowerCase().indexOf("youtube")>-1){d.file=d.link}}return d};a.utils.parsers.jwparser.getProvider=function(c){if(c.type){return c.type}else{if(c.file.indexOf("youtube.com/w")>-1||c.file.indexOf("youtube.com/v")>-1||c.file.indexOf("youtu.be/")>-1){return"youtube"}else{if(c.streamer&&c.streamer.indexOf("rtmp")==0){return"rtmp"}else{if(c.streamer&&c.streamer.indexOf("http")==0){return"http"}else{var b=a.utils.strings.extension(c.file);if(extensions.hasOwnProperty(b)){return extensions[b]}}}}}return""}})(jwplayer);(function(a){a.utils.parsers.mediaparser=function(){};a.utils.parsers.mediaparser.PREFIX="media";a.utils.parsers.mediaparser.parseGroup=function(d,f){var e=false;for(var c=0;c<d.childNodes.length;c++){if(d.childNodes[c].prefix==a.utils.parsers.mediaparser.PREFIX){if(!a.utils.parsers.localName(d.childNodes[c])){continue}switch(a.utils.parsers.localName(d.childNodes[c]).toLowerCase()){case"content":if(!e){f.file=a.utils.strings.xmlAttribute(d.childNodes[c],"url")}if(a.utils.strings.xmlAttribute(d.childNodes[c],"duration")){f.duration=a.utils.strings.seconds(a.utils.strings.xmlAttribute(d.childNodes[c],"duration"))}if(a.utils.strings.xmlAttribute(d.childNodes[c],"start")){f.start=a.utils.strings.seconds(a.utils.strings.xmlAttribute(d.childNodes[c],"start"))}if(d.childNodes[c].childNodes&&d.childNodes[c].childNodes.length>0){f=a.utils.parsers.mediaparser.parseGroup(d.childNodes[c],f)}if(a.utils.strings.xmlAttribute(d.childNodes[c],"width")||a.utils.strings.xmlAttribute(d.childNodes[c],"bitrate")||a.utils.strings.xmlAttribute(d.childNodes[c],"url")){if(!f.levels){f.levels=[]}f.levels.push({width:a.utils.strings.xmlAttribute(d.childNodes[c],"width"),bitrate:a.utils.strings.xmlAttribute(d.childNodes[c],"bitrate"),file:a.utils.strings.xmlAttribute(d.childNodes[c],"url")})}break;case"title":f.title=a.utils.parsers.textContent(d.childNodes[c]);break;case"description":f.description=a.utils.parsers.textContent(d.childNodes[c]);break;case"keywords":f.tags=a.utils.parsers.textContent(d.childNodes[c]);break;case"thumbnail":f.image=a.utils.strings.xmlAttribute(d.childNodes[c],"url");break;case"credit":f.author=a.utils.parsers.textContent(d.childNodes[c]);break;case"player":var b=d.childNodes[c].url;if(b.indexOf("youtube.com")>=0||b.indexOf("youtu.be")>=0){e=true;f.file=a.utils.strings.xmlAttribute(d.childNodes[c],"url")}break;case"group":a.utils.parsers.mediaparser.parseGroup(d.childNodes[c],f);break}}}return f}})(jwplayer);(function(b){b.utils.parsers.rssparser=function(){};b.utils.parsers.rssparser.parse=function(f){var c=[];for(var e=0;e<f.childNodes.length;e++){if(b.utils.parsers.localName(f.childNodes[e]).toLowerCase()=="channel"){for(var d=0;d<f.childNodes[e].childNodes.length;d++){if(b.utils.parsers.localName(f.childNodes[e].childNodes[d]).toLowerCase()=="item"){c.push(a(f.childNodes[e].childNodes[d]))}}}}return c};function a(d){var e={};for(var c=0;c<d.childNodes.length;c++){if(!b.utils.parsers.localName(d.childNodes[c])){continue}switch(b.utils.parsers.localName(d.childNodes[c]).toLowerCase()){case"enclosure":e.file=b.utils.strings.xmlAttribute(d.childNodes[c],"url");break;case"title":e.title=b.utils.parsers.textContent(d.childNodes[c]);break;case"pubdate":e.date=b.utils.parsers.textContent(d.childNodes[c]);break;case"description":e.description=b.utils.parsers.textContent(d.childNodes[c]);break;case"link":e.link=b.utils.parsers.textContent(d.childNodes[c]);break;case"category":if(e.tags){e.tags+=b.utils.parsers.textContent(d.childNodes[c])}else{e.tags=b.utils.parsers.textContent(d.childNodes[c])}break}}e=b.utils.parsers.mediaparser.parseGroup(d,e);e=b.utils.parsers.jwparser.parseEntry(d,e);return new b.html5.playlistitem(e)}})(jwplayer);(function(a){var c={};var b={};a.plugins=function(){};a.plugins.loadPlugins=function(e,d){b[e]=new a.plugins.pluginloader(new a.plugins.model(c),d);return b[e]};a.plugins.registerPlugin=function(h,f,e){var d=a.utils.getPluginName(h);if(c[d]){c[d].registerPlugin(h,f,e)}else{a.utils.log("A plugin ("+h+") was registered with the player that was not loaded. Please check your configuration.");for(var g in b){b[g].pluginFailed()}}}})(jwplayer);(function(a){a.plugins.model=function(b){this.addPlugin=function(c){var d=a.utils.getPluginName(c);if(!b[d]){b[d]=new a.plugins.plugin(c)}return b[d]}}})(jwplayer);(function(a){a.plugins.pluginmodes={FLASH:"FLASH",JAVASCRIPT:"JAVASCRIPT",HYBRID:"HYBRID"};a.plugins.plugin=function(b){var d="http://plugins.longtailvideo.com";var j=a.utils.loaderstatus.NEW;var k;var h;var l;var c=new a.events.eventdispatcher();a.utils.extend(this,c);function e(){switch(a.utils.getPluginPathType(b)){case a.utils.pluginPathType.ABSOLUTE:return b;case a.utils.pluginPathType.RELATIVE:return a.utils.getAbsolutePath(b,window.location.href);case a.utils.pluginPathType.CDN:var o=a.utils.getPluginName(b);var n=a.utils.getPluginVersion(b);var m=(window.location.href.indexOf("https://")==0)?d.replace("http://","https://secure"):d;return m+"/"+a.version.split(".")[0]+"/"+o+"/"+o+(n!==""?("-"+n):"")+".js"}}function g(m){l=setTimeout(function(){j=a.utils.loaderstatus.COMPLETE;c.sendEvent(a.events.COMPLETE)},1000)}function f(m){j=a.utils.loaderstatus.ERROR;c.sendEvent(a.events.ERROR)}this.load=function(){if(j==a.utils.loaderstatus.NEW){if(b.lastIndexOf(".swf")>0){k=b;j=a.utils.loaderstatus.COMPLETE;c.sendEvent(a.events.COMPLETE);return}j=a.utils.loaderstatus.LOADING;var m=new a.utils.scriptloader(e());m.addEventListener(a.events.COMPLETE,g);m.addEventListener(a.events.ERROR,f);m.load()}};this.registerPlugin=function(o,n,m){if(l){clearTimeout(l);l=undefined}if(n&&m){k=m;h=n}else{if(typeof n=="string"){k=n}else{if(typeof n=="function"){h=n}else{if(!n&&!m){k=o}}}}j=a.utils.loaderstatus.COMPLETE;c.sendEvent(a.events.COMPLETE)};this.getStatus=function(){return j};this.getPluginName=function(){return a.utils.getPluginName(b)};this.getFlashPath=function(){if(k){switch(a.utils.getPluginPathType(k)){case a.utils.pluginPathType.ABSOLUTE:return k;case a.utils.pluginPathType.RELATIVE:if(b.lastIndexOf(".swf")>0){return a.utils.getAbsolutePath(k,window.location.href)}return a.utils.getAbsolutePath(k,e());case a.utils.pluginPathType.CDN:if(k.indexOf("-")>-1){return k+"h"}return k+"-h"}}return null};this.getJS=function(){return h};this.getPluginmode=function(){if(typeof k!="undefined"&&typeof h!="undefined"){return a.plugins.pluginmodes.HYBRID}else{if(typeof k!="undefined"){return a.plugins.pluginmodes.FLASH}else{if(typeof h!="undefined"){return a.plugins.pluginmodes.JAVASCRIPT}}}};this.getNewInstance=function(n,m,o){return new h(n,m,o)};this.getURL=function(){return b}}})(jwplayer);(function(a){a.plugins.pluginloader=function(h,e){var g={};var k=a.utils.loaderstatus.NEW;var d=false;var b=false;var c=new a.events.eventdispatcher();a.utils.extend(this,c);function f(){if(!b){b=true;k=a.utils.loaderstatus.COMPLETE;c.sendEvent(a.events.COMPLETE)}}function j(){if(!b){var m=0;for(plugin in g){var l=g[plugin].getStatus();if(l==a.utils.loaderstatus.LOADING||l==a.utils.loaderstatus.NEW){m++}}if(m==0){f()}}}this.setupPlugins=function(n,l,s){var m={length:0,plugins:{}};var p={length:0,plugins:{}};for(var o in g){var q=g[o].getPluginName();if(g[o].getFlashPath()){m.plugins[g[o].getFlashPath()]=l.plugins[o];m.plugins[g[o].getFlashPath()].pluginmode=g[o].getPluginmode();m.length++}if(g[o].getJS()){var r=document.createElement("div");r.id=n.id+"_"+q;r.style.position="absolute";r.style.zIndex=p.length+10;p.plugins[q]=g[o].getNewInstance(n,l.plugins[o],r);p.length++;if(typeof p.plugins[q].resize!="undefined"){n.onReady(s(p.plugins[q],r,true));n.onResize(s(p.plugins[q],r))}}}n.plugins=p.plugins;return m};this.load=function(){k=a.utils.loaderstatus.LOADING;d=true;for(var l in e){if(a.utils.exists(l)){g[l]=h.addPlugin(l);g[l].addEventListener(a.events.COMPLETE,j);g[l].addEventListener(a.events.ERROR,j)}}for(l in g){g[l].load()}d=false;j()};this.pluginFailed=function(){f()};this.getStatus=function(){return k}}})(jwplayer);(function(b){var a=[];b.api=function(d){this.container=d;this.id=d.id;var m={};var s={};var p={};var c=[];var g=undefined;var k=false;var h=[];var q=undefined;var o=b.utils.getOuterHTML(d);var r={};var j={};this.getBuffer=function(){return this.callInternal("jwGetBuffer")};this.getContainer=function(){return this.container};function e(u,t){return function(z,v,w,x){if(u.renderingMode=="flash"||u.renderingMode=="html5"){var y;if(v){j[z]=v;y="jwplayer('"+u.id+"').callback('"+z+"')"}else{if(!v&&j[z]){delete j[z]}}g.jwDockSetButton(z,y,w,x)}return t}}this.getPlugin=function(t){var v=this;var u={};if(t=="dock"){return b.utils.extend(u,{setButton:e(v,u),show:function(){v.callInternal("jwDockShow");return u},hide:function(){v.callInternal("jwDockHide");return u},onShow:function(w){v.componentListener("dock",b.api.events.JWPLAYER_COMPONENT_SHOW,w);return u},onHide:function(w){v.componentListener("dock",b.api.events.JWPLAYER_COMPONENT_HIDE,w);return u}})}else{if(t=="controlbar"){return b.utils.extend(u,{show:function(){v.callInternal("jwControlbarShow");return u},hide:function(){v.callInternal("jwControlbarHide");return u},onShow:function(w){v.componentListener("controlbar",b.api.events.JWPLAYER_COMPONENT_SHOW,w);return u},onHide:function(w){v.componentListener("controlbar",b.api.events.JWPLAYER_COMPONENT_HIDE,w);return u}})}else{if(t=="display"){return b.utils.extend(u,{show:function(){v.callInternal("jwDisplayShow");return u},hide:function(){v.callInternal("jwDisplayHide");return u},onShow:function(w){v.componentListener("display",b.api.events.JWPLAYER_COMPONENT_SHOW,w);return u},onHide:function(w){v.componentListener("display",b.api.events.JWPLAYER_COMPONENT_HIDE,w);return u}})}else{return this.plugins[t]}}}};this.callback=function(t){if(j[t]){return j[t]()}};this.getDuration=function(){return this.callInternal("jwGetDuration")};this.getFullscreen=function(){return this.callInternal("jwGetFullscreen")};this.getHeight=function(){return this.callInternal("jwGetHeight")};this.getLockState=function(){return this.callInternal("jwGetLockState")};this.getMeta=function(){return this.getItemMeta()};this.getMute=function(){return this.callInternal("jwGetMute")};this.getPlaylist=function(){var u=this.callInternal("jwGetPlaylist");if(this.renderingMode=="flash"){b.utils.deepReplaceKeyName(u,["__dot__","__spc__","__dsh__"],["."," ","-"])}for(var t=0;t<u.length;t++){if(!b.utils.exists(u[t].index)){u[t].index=t}}return u};this.getPlaylistItem=function(t){if(!b.utils.exists(t)){t=this.getCurrentItem()}return this.getPlaylist()[t]};this.getPosition=function(){return this.callInternal("jwGetPosition")};this.getRenderingMode=function(){return this.renderingMode};this.getState=function(){return this.callInternal("jwGetState")};this.getVolume=function(){return this.callInternal("jwGetVolume")};this.getWidth=function(){return this.callInternal("jwGetWidth")};this.setFullscreen=function(t){if(!b.utils.exists(t)){this.callInternal("jwSetFullscreen",!this.callInternal("jwGetFullscreen"))}else{this.callInternal("jwSetFullscreen",t)}return this};this.setMute=function(t){if(!b.utils.exists(t)){this.callInternal("jwSetMute",!this.callInternal("jwGetMute"))}else{this.callInternal("jwSetMute",t)}return this};this.lock=function(){return this};this.unlock=function(){return this};this.load=function(t){this.callInternal("jwLoad",t);return this};this.playlistItem=function(t){this.callInternal("jwPlaylistItem",t);return this};this.playlistPrev=function(){this.callInternal("jwPlaylistPrev");return this};this.playlistNext=function(){this.callInternal("jwPlaylistNext");return this};this.resize=function(u,t){if(this.renderingMode=="html5"){g.jwResize(u,t)}else{this.container.width=u;this.container.height=t;var v=document.getElementById(this.id+"_wrapper");if(v){v.style.width=u+"px";v.style.height=t+"px"}}return this};this.play=function(t){if(typeof t=="undefined"){t=this.getState();if(t==b.api.events.state.PLAYING||t==b.api.events.state.BUFFERING){this.callInternal("jwPause")}else{this.callInternal("jwPlay")}}else{this.callInternal("jwPlay",t)}return this};this.pause=function(t){if(typeof t=="undefined"){t=this.getState();if(t==b.api.events.state.PLAYING||t==b.api.events.state.BUFFERING){this.callInternal("jwPause")}else{this.callInternal("jwPlay")}}else{this.callInternal("jwPause",t)}return this};this.stop=function(){this.callInternal("jwStop");return this};this.seek=function(t){this.callInternal("jwSeek",t);return this};this.setVolume=function(t){this.callInternal("jwSetVolume",t);return this};this.loadInstream=function(u,t){q=new b.api.instream(this,g,u,t);return q};this.onBufferChange=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_BUFFER,t)};this.onBufferFull=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_BUFFER_FULL,t)};this.onError=function(t){return this.eventListener(b.api.events.JWPLAYER_ERROR,t)};this.onFullscreen=function(t){return this.eventListener(b.api.events.JWPLAYER_FULLSCREEN,t)};this.onMeta=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_META,t)};this.onMute=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_MUTE,t)};this.onPlaylist=function(t){return this.eventListener(b.api.events.JWPLAYER_PLAYLIST_LOADED,t)};this.onPlaylistItem=function(t){return this.eventListener(b.api.events.JWPLAYER_PLAYLIST_ITEM,t)};this.onReady=function(t){return this.eventListener(b.api.events.API_READY,t)};this.onResize=function(t){return this.eventListener(b.api.events.JWPLAYER_RESIZE,t)};this.onComplete=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_COMPLETE,t)};this.onSeek=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_SEEK,t)};this.onTime=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_TIME,t)};this.onVolume=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_VOLUME,t)};this.onBeforePlay=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_BEFOREPLAY,t)};this.onBeforeComplete=function(t){return this.eventListener(b.api.events.JWPLAYER_MEDIA_BEFORECOMPLETE,t)};this.onBuffer=function(t){return this.stateListener(b.api.events.state.BUFFERING,t)};this.onPause=function(t){return this.stateListener(b.api.events.state.PAUSED,t)};this.onPlay=function(t){return this.stateListener(b.api.events.state.PLAYING,t)};this.onIdle=function(t){return this.stateListener(b.api.events.state.IDLE,t)};this.remove=function(){m={};h=[];if(b.utils.getOuterHTML(this.container)!=o){b.api.destroyPlayer(this.id,o)}};this.setup=function(u){if(b.embed){var t=this.id;this.remove();var v=b(t);v.config=u;return new b.embed(v)}return this};this.registerPlugin=function(v,u,t){b.plugins.registerPlugin(v,u,t)};this.setPlayer=function(t,u){g=t;this.renderingMode=u};this.stateListener=function(t,u){if(!s[t]){s[t]=[];this.eventListener(b.api.events.JWPLAYER_PLAYER_STATE,f(t))}s[t].push(u);return this};this.detachMedia=function(){if(this.renderingMode=="html5"){return this.callInternal("jwDetachMedia")}};this.attachMedia=function(){if(this.renderingMode=="html5"){return this.callInternal("jwAttachMedia")}};function f(t){return function(v){var u=v.newstate,x=v.oldstate;if(u==t){var w=s[u];if(w){for(var y=0;y<w.length;y++){if(typeof w[y]=="function"){w[y].call(this,{oldstate:x,newstate:u})}}}}}}this.componentListener=function(t,u,v){if(!p[t]){p[t]={}}if(!p[t][u]){p[t][u]=[];this.eventListener(u,l(t,u))}p[t][u].push(v);return this};function l(t,u){return function(w){if(t==w.component){var v=p[t][u];if(v){for(var x=0;x<v.length;x++){if(typeof v[x]=="function"){v[x].call(this,w)}}}}}}this.addInternalListener=function(t,u){t.jwAddEventListener(u,'function(dat) { jwplayer("'+this.id+'").dispatchEvent("'+u+'", dat); }')};this.eventListener=function(t,u){if(!m[t]){m[t]=[];if(g&&k){this.addInternalListener(g,t)}}m[t].push(u);return this};this.dispatchEvent=function(v){if(m[v]){var u=_utils.translateEventResponse(v,arguments[1]);for(var t=0;t<m[v].length;t++){if(typeof m[v][t]=="function"){m[v][t].call(this,u)}}}};this.dispatchInstreamEvent=function(t){if(q){q.dispatchEvent(t,arguments)}};this.callInternal=function(){if(k){var v=arguments[0],t=[];for(var u=1;u<arguments.length;u++){t.push(arguments[u])}if(typeof g!="undefined"&&typeof g[v]=="function"){if(t.length==2){return(g[v])(t[0],t[1])}else{if(t.length==1){return(g[v])(t[0])}else{return(g[v])()}}}return null}else{h.push(arguments)}};this.playerReady=function(u){k=true;if(!g){this.setPlayer(document.getElementById(u.id))}this.container=document.getElementById(this.id);for(var t in m){this.addInternalListener(g,t)}this.eventListener(b.api.events.JWPLAYER_PLAYLIST_ITEM,function(v){r={}});this.eventListener(b.api.events.JWPLAYER_MEDIA_META,function(v){b.utils.extend(r,v.metadata)});this.dispatchEvent(b.api.events.API_READY);while(h.length>0){this.callInternal.apply(this,h.shift())}};this.getItemMeta=function(){return r};this.getCurrentItem=function(){return this.callInternal("jwGetPlaylistIndex")};function n(v,x,w){var t=[];if(!x){x=0}if(!w){w=v.length-1}for(var u=x;u<=w;u++){t.push(v[u])}return t}return this};b.api.selectPlayer=function(d){var c;if(!b.utils.exists(d)){d=0}if(d.nodeType){c=d}else{if(typeof d=="string"){c=document.getElementById(d)}}if(c){var e=b.api.playerById(c.id);if(e){return e}else{return b.api.addPlayer(new b.api(c))}}else{if(typeof d=="number"){return b.getPlayers()[d]}}return null};b.api.events={API_READY:"jwplayerAPIReady",JWPLAYER_READY:"jwplayerReady",JWPLAYER_FULLSCREEN:"jwplayerFullscreen",JWPLAYER_RESIZE:"jwplayerResize",JWPLAYER_ERROR:"jwplayerError",JWPLAYER_MEDIA_BEFOREPLAY:"jwplayerMediaBeforePlay",JWPLAYER_MEDIA_BEFORECOMPLETE:"jwplayerMediaBeforeComplete",JWPLAYER_COMPONENT_SHOW:"jwplayerComponentShow",JWPLAYER_COMPONENT_HIDE:"jwplayerComponentHide",JWPLAYER_MEDIA_BUFFER:"jwplayerMediaBuffer",JWPLAYER_MEDIA_BUFFER_FULL:"jwplayerMediaBufferFull",JWPLAYER_MEDIA_ERROR:"jwplayerMediaError",JWPLAYER_MEDIA_LOADED:"jwplayerMediaLoaded",JWPLAYER_MEDIA_COMPLETE:"jwplayerMediaComplete",JWPLAYER_MEDIA_SEEK:"jwplayerMediaSeek",JWPLAYER_MEDIA_TIME:"jwplayerMediaTime",JWPLAYER_MEDIA_VOLUME:"jwplayerMediaVolume",JWPLAYER_MEDIA_META:"jwplayerMediaMeta",JWPLAYER_MEDIA_MUTE:"jwplayerMediaMute",JWPLAYER_PLAYER_STATE:"jwplayerPlayerState",JWPLAYER_PLAYLIST_LOADED:"jwplayerPlaylistLoaded",JWPLAYER_PLAYLIST_ITEM:"jwplayerPlaylistItem",JWPLAYER_INSTREAM_CLICK:"jwplayerInstreamClicked",JWPLAYER_INSTREAM_DESTROYED:"jwplayerInstreamDestroyed"};b.api.events.state={BUFFERING:"BUFFERING",IDLE:"IDLE",PAUSED:"PAUSED",PLAYING:"PLAYING"};b.api.playerById=function(d){for(var c=0;c<a.length;c++){if(a[c].id==d){return a[c]}}return null};b.api.addPlayer=function(c){for(var d=0;d<a.length;d++){if(a[d]==c){return c}}a.push(c);return c};b.api.destroyPlayer=function(g,d){var f=-1;for(var j=0;j<a.length;j++){if(a[j].id==g){f=j;continue}}if(f>=0){var c=document.getElementById(a[f].id);if(document.getElementById(a[f].id+"_wrapper")){c=document.getElementById(a[f].id+"_wrapper")}if(c){if(d){b.utils.setOuterHTML(c,d)}else{var h=document.createElement("div");var e=c.id;if(c.id.indexOf("_wrapper")==c.id.length-8){newID=c.id.substring(0,c.id.length-8)}h.setAttribute("id",e);c.parentNode.replaceChild(h,c)}}a.splice(f,1)}return null};b.getPlayers=function(){return a.slice(0)}})(jwplayer);var _userPlayerReady=(typeof playerReady=="function")?playerReady:undefined;playerReady=function(b){var a=jwplayer.api.playerById(b.id);if(a){a.playerReady(b)}else{jwplayer.api.selectPlayer(b.id).playerReady(b)}if(_userPlayerReady){_userPlayerReady.call(this,b)}};(function(a){a.api.instream=function(c,j,n,q){var h=c;var b=j;var g=n;var k=q;var e={};var p={};function f(){h.callInternal("jwLoadInstream",n,q)}function m(r,s){b.jwInstreamAddEventListener(s,'function(dat) { jwplayer("'+h.id+'").dispatchInstreamEvent("'+s+'", dat); }')}function d(r,s){if(!e[r]){e[r]=[];m(b,r)}e[r].push(s);return this}function o(r,s){if(!p[r]){p[r]=[];d(a.api.events.JWPLAYER_PLAYER_STATE,l(r))}p[r].push(s);return this}function l(r){return function(t){var s=t.newstate,v=t.oldstate;if(s==r){var u=p[s];if(u){for(var w=0;w<u.length;w++){if(typeof u[w]=="function"){u[w].call(this,{oldstate:v,newstate:s,type:t.type})}}}}}}this.dispatchEvent=function(u,t){if(e[u]){var s=_utils.translateEventResponse(u,t[1]);for(var r=0;r<e[u].length;r++){if(typeof e[u][r]=="function"){e[u][r].call(this,s)}}}};this.onError=function(r){return d(a.api.events.JWPLAYER_ERROR,r)};this.onFullscreen=function(r){return d(a.api.events.JWPLAYER_FULLSCREEN,r)};this.onMeta=function(r){return d(a.api.events.JWPLAYER_MEDIA_META,r)};this.onMute=function(r){return d(a.api.events.JWPLAYER_MEDIA_MUTE,r)};this.onComplete=function(r){return d(a.api.events.JWPLAYER_MEDIA_COMPLETE,r)};this.onSeek=function(r){return d(a.api.events.JWPLAYER_MEDIA_SEEK,r)};this.onTime=function(r){return d(a.api.events.JWPLAYER_MEDIA_TIME,r)};this.onVolume=function(r){return d(a.api.events.JWPLAYER_MEDIA_VOLUME,r)};this.onBuffer=function(r){return o(a.api.events.state.BUFFERING,r)};this.onPause=function(r){return o(a.api.events.state.PAUSED,r)};this.onPlay=function(r){return o(a.api.events.state.PLAYING,r)};this.onIdle=function(r){return o(a.api.events.state.IDLE,r)};this.onInstreamClick=function(r){return d(a.api.events.JWPLAYER_INSTREAM_CLICK,r)};this.onInstreamDestroyed=function(r){return d(a.api.events.JWPLAYER_INSTREAM_DESTROYED,r)};this.play=function(r){b.jwInstreamPlay(r)};this.pause=function(r){b.jwInstreamPause(r)};this.seek=function(r){b.jwInstreamSeek(r)};this.destroy=function(){b.jwInstreamDestroy()};this.getState=function(){return b.jwInstreamGetState()};this.getDuration=function(){return b.jwInstreamGetDuration()};this.getPosition=function(){return b.jwInstreamGetPosition()};f()}})(jwplayer);(function(a){var c=a.utils;a.embed=function(h){var k={width:400,height:300,components:{controlbar:{position:"over"}}};var g=c.mediaparser.parseMedia(h.container);var f=new a.embed.config(c.extend(k,g,h.config),this);var j=a.plugins.loadPlugins(h.id,f.plugins);function d(n,m){for(var l in m){if(typeof n[l]=="function"){(n[l]).call(n,m[l])}}}function e(){if(j.getStatus()==c.loaderstatus.COMPLETE){for(var n=0;n<f.modes.length;n++){if(f.modes[n].type&&a.embed[f.modes[n].type]){var p=f.modes[n].config;var t=f;if(p){t=c.extend(c.clone(f),p);var s=["file","levels","playlist"];for(var m=0;m<s.length;m++){var q=s[m];if(c.exists(p[q])){for(var l=0;l<s.length;l++){if(l!=m){var o=s[l];if(c.exists(t[o])&&!c.exists(p[o])){delete t[o]}}}}}}var r=new a.embed[f.modes[n].type](document.getElementById(h.id),f.modes[n],t,j,h);if(r.supportsConfig()){r.embed();d(h,f.events);return h}}}c.log("No suitable players found");new a.embed.logo(c.extend({hide:true},f.components.logo),"none",h.id)}}j.addEventListener(a.events.COMPLETE,e);j.addEventListener(a.events.ERROR,e);j.load();return h};function b(){if(!document.body){return setTimeout(b,15)}var d=c.selectors.getElementsByTagAndClass("video","jwplayer");for(var e=0;e<d.length;e++){var f=d[e];if(f.id==""){f.id="jwplayer_"+Math.round(Math.random()*100000)}a(f.id).setup({})}}b()})(jwplayer);(function(e){var k=e.utils;function h(m){var l=[{type:"flash",src:m?m:"/jwplayer/player.swf"},{type:"html5"},{type:"download"}];if(k.isAndroid()){l[0]=l.splice(1,1,l[0])[0]}return l}var a={players:"modes",autoplay:"autostart"};function b(o){var n=o.toLowerCase();var m=["left","right","top","bottom"];for(var l=0;l<m.length;l++){if(n==m[l]){return true}}return false}function c(m){var l=false;l=(m instanceof Array)||(typeof m=="object"&&!m.position&&!m.size);return l}function j(l){if(typeof l=="string"){if(parseInt(l).toString()==l||l.toLowerCase().indexOf("px")>-1){return parseInt(l)}}return l}var g=["playlist","dock","controlbar","logo","display"];function f(l){var o={};switch(k.typeOf(l.plugins)){case"object":for(var n in l.plugins){o[k.getPluginName(n)]=n}break;case"string":var p=l.plugins.split(",");for(var m=0;m<p.length;m++){o[k.getPluginName(p[m])]=p[m]}break}return o}function d(p,o,n,l){if(k.typeOf(p[o])!="object"){p[o]={}}var m=p[o][n];if(k.typeOf(m)!="object"){p[o][n]=m={}}if(l){if(o=="plugins"){var q=k.getPluginName(n);m[l]=p[q+"."+l];delete p[q+"."+l]}else{m[l]=p[n+"."+l];delete p[n+"."+l]}}}e.embed.deserialize=function(m){var n=f(m);for(var l in n){d(m,"plugins",n[l])}for(var q in m){if(q.indexOf(".")>-1){var p=q.split(".");var o=p[0];var q=p[1];if(k.isInArray(g,o)){d(m,"components",o,q)}else{if(n[o]){d(m,"plugins",n[o],q)}}}}return m};e.embed.config=function(l,v){var u=k.extend({},l);var s;if(c(u.playlist)){s=u.playlist;delete u.playlist}u=e.embed.deserialize(u);u.height=j(u.height);u.width=j(u.width);if(typeof u.plugins=="string"){var m=u.plugins.split(",");if(typeof u.plugins!="object"){u.plugins={}}for(var q=0;q<m.length;q++){var r=k.getPluginName(m[q]);if(typeof u[r]=="object"){u.plugins[m[q]]=u[r];delete u[r]}else{u.plugins[m[q]]={}}}}for(var t=0;t<g.length;t++){var p=g[t];if(k.exists(u[p])){if(typeof u[p]!="object"){if(!u.components[p]){u.components[p]={}}if(p=="logo"){u.components[p].file=u[p]}else{u.components[p].position=u[p]}delete u[p]}else{if(!u.components[p]){u.components[p]={}}k.extend(u.components[p],u[p]);delete u[p]}}if(typeof u[p+"size"]!="undefined"){if(!u.components[p]){u.components[p]={}}u.components[p].size=u[p+"size"];delete u[p+"size"]}}if(typeof u.icons!="undefined"){if(!u.components.display){u.components.display={}}u.components.display.icons=u.icons;delete u.icons}for(var o in a){if(u[o]){if(!u[a[o]]){u[a[o]]=u[o]}delete u[o]}}var n;if(u.flashplayer&&!u.modes){n=h(u.flashplayer);delete u.flashplayer}else{if(u.modes){if(typeof u.modes=="string"){n=h(u.modes)}else{if(u.modes instanceof Array){n=u.modes}else{if(typeof u.modes=="object"&&u.modes.type){n=[u.modes]}}}delete u.modes}else{n=h()}}u.modes=n;if(s){u.playlist=s}return u}})(jwplayer);(function(a){a.embed.download=function(c,g,b,d,f){this.embed=function(){var k=a.utils.extend({},b);var q={};var j=b.width?b.width:480;if(typeof j!="number"){j=parseInt(j,10)}var m=b.height?b.height:320;if(typeof m!="number"){m=parseInt(m,10)}var u,o,n;var s={};if(b.playlist&&b.playlist.length){s.file=b.playlist[0].file;o=b.playlist[0].image;s.levels=b.playlist[0].levels}else{s.file=b.file;o=b.image;s.levels=b.levels}if(s.file){u=s.file}else{if(s.levels&&s.levels.length){u=s.levels[0].file}}n=u?"pointer":"auto";var l={display:{style:{cursor:n,width:j,height:m,backgroundColor:"#000",position:"relative",textDecoration:"none",border:"none",display:"block"}},display_icon:{style:{cursor:n,position:"absolute",display:u?"block":"none",top:0,left:0,border:0,margin:0,padding:0,zIndex:3,width:50,height:50,backgroundImage:"url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAALdJREFUeNrs18ENgjAYhmFouDOCcQJGcARHgE10BDcgTOIosAGwQOuPwaQeuFRi2p/3Sb6EC5L3QCxZBgAAAOCorLW1zMn65TrlkH4NcV7QNcUQt7Gn7KIhxA+qNIR81spOGkL8oFJDyLJRdosqKDDkK+iX5+d7huzwM40xptMQMkjIOeRGo+VkEVvIPfTGIpKASfYIfT9iCHkHrBEzf4gcUQ56aEzuGK/mw0rHpy4AAACAf3kJMACBxjAQNRckhwAAAABJRU5ErkJggg==)"}},display_iconBackground:{style:{cursor:n,position:"absolute",display:u?"block":"none",top:((m-50)/2),left:((j-50)/2),border:0,width:50,height:50,margin:0,padding:0,zIndex:2,backgroundImage:"url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEpJREFUeNrszwENADAIA7DhX8ENoBMZ5KR10EryckCJiIiIiIiIiIiIiIiIiIiIiIh8GmkRERERERERERERERERERERERGRHSPAAPlXH1phYpYaAAAAAElFTkSuQmCC)"}},display_image:{style:{width:j,height:m,display:o?"block":"none",position:"absolute",cursor:n,left:0,top:0,margin:0,padding:0,textDecoration:"none",zIndex:1,border:"none"}}};var h=function(v,x,y){var w=document.createElement(v);if(y){w.id=y}else{w.id=c.id+"_jwplayer_"+x}a.utils.css(w,l[x].style);return w};q.display=h("a","display",c.id);if(u){q.display.setAttribute("href",a.utils.getAbsolutePath(u))}q.display_image=h("img","display_image");q.display_image.setAttribute("alt","Click to download...");if(o){q.display_image.setAttribute("src",a.utils.getAbsolutePath(o))}if(true){q.display_icon=h("div","display_icon");q.display_iconBackground=h("div","display_iconBackground");q.display.appendChild(q.display_image);q.display_iconBackground.appendChild(q.display_icon);q.display.appendChild(q.display_iconBackground)}_css=a.utils.css;_hide=function(v){_css(v,{display:"none"})};function r(v){_imageWidth=q.display_image.naturalWidth;_imageHeight=q.display_image.naturalHeight;t()}function t(){a.utils.stretch(a.utils.stretching.UNIFORM,q.display_image,j,m,_imageWidth,_imageHeight)}q.display_image.onerror=function(v){_hide(q.display_image)};q.display_image.onload=r;c.parentNode.replaceChild(q.display,c);var p=(b.plugins&&b.plugins.logo)?b.plugins.logo:{};q.display.appendChild(new a.embed.logo(b.components.logo,"download",c.id));f.container=document.getElementById(f.id);f.setPlayer(q.display,"download")};this.supportsConfig=function(){if(b){var j=a.utils.getFirstPlaylistItemFromConfig(b);if(typeof j.file=="undefined"&&typeof j.levels=="undefined"){return true}else{if(j.file){return e(j.file,j.provider,j.playlistfile)}else{if(j.levels&&j.levels.length){for(var h=0;h<j.levels.length;h++){if(j.levels[h].file&&e(j.levels[h].file,j.provider,j.playlistfile)){return true}}}}}}else{return true}};function e(j,l,h){if(h){return false}var k=["image","sound","youtube","http"];if(l&&(k.toString().indexOf(l)>-1)){return true}if(!l||(l&&l=="video")){var m=a.utils.extension(j);if(m&&a.utils.extensionmap[m]){return true}}return false}}})(jwplayer);(function(a){a.embed.flash=function(f,g,l,e,j){function m(o,n,p){var q=document.createElement("param");q.setAttribute("name",n);q.setAttribute("value",p);o.appendChild(q)}function k(o,p,n){return function(q){if(n){document.getElementById(j.id+"_wrapper").appendChild(p)}var s=document.getElementById(j.id).getPluginConfig("display");o.resize(s.width,s.height);var r={left:s.x,top:s.y};a.utils.css(p,r)}}function d(p){if(!p){return{}}var r={};for(var o in p){var n=p[o];for(var q in n){r[o+"."+q]=n[q]}}return r}function h(q,p){if(q[p]){var s=q[p];for(var o in s){var n=s[o];if(typeof n=="string"){if(!q[o]){q[o]=n}}else{for(var r in n){if(!q[o+"."+r]){q[o+"."+r]=n[r]}}}}delete q[p]}}function b(q){if(!q){return{}}var t={},s=[];for(var n in q){var p=a.utils.getPluginName(n);var o=q[n];s.push(n);for(var r in o){t[p+"."+r]=o[r]}}t.plugins=s.join(",");return t}function c(p){var n=p.netstreambasepath?"":"netstreambasepath="+encodeURIComponent(window.location.href.split("#")[0])+"&";for(var o in p){if(typeof(p[o])=="object"){n+=o+"="+encodeURIComponent("[[JSON]]"+a.utils.strings.jsonToString(p[o]))+"&"}else{n+=o+"="+encodeURIComponent(p[o])+"&"}}return n.substring(0,n.length-1)}this.embed=function(){l.id=j.id;var A;var r=a.utils.extend({},l);var o=r.width;var y=r.height;if(f.id+"_wrapper"==f.parentNode.id){A=document.getElementById(f.id+"_wrapper")}else{A=document.createElement("div");A.id=f.id+"_wrapper";a.utils.wrap(f,A);a.utils.css(A,{position:"relative",width:o,height:y})}var p=e.setupPlugins(j,r,k);if(p.length>0){a.utils.extend(r,b(p.plugins))}else{delete r.plugins}var s=["height","width","modes","events"];for(var v=0;v<s.length;v++){delete r[s[v]]}var q="opaque";if(r.wmode){q=r.wmode}h(r,"components");h(r,"providers");if(typeof r["dock.position"]!="undefined"){if(r["dock.position"].toString().toLowerCase()=="false"){r.dock=r["dock.position"];delete r["dock.position"]}}var x=a.utils.getCookies();for(var n in x){if(typeof(r[n])=="undefined"){r[n]=x[n]}}var z="#000000";var u;if(a.utils.isIE()){var w='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" bgcolor="'+z+'" width="100%" height="100%" id="'+f.id+'" name="'+f.id+'" tabindex=0"">';w+='<param name="movie" value="'+g.src+'">';w+='<param name="allowfullscreen" value="true">';w+='<param name="allowscriptaccess" value="always">';w+='<param name="seamlesstabbing" value="true">';w+='<param name="wmode" value="'+q+'">';w+='<param name="flashvars" value="'+c(r)+'">';w+="</object>";a.utils.setOuterHTML(f,w);u=document.getElementById(f.id)}else{var t=document.createElement("object");t.setAttribute("type","application/x-shockwave-flash");t.setAttribute("data",g.src);t.setAttribute("width","100%");t.setAttribute("height","100%");t.setAttribute("bgcolor","#000000");t.setAttribute("id",f.id);t.setAttribute("name",f.id);t.setAttribute("tabindex",0);m(t,"allowfullscreen","true");m(t,"allowscriptaccess","always");m(t,"seamlesstabbing","true");m(t,"wmode",q);m(t,"flashvars",c(r));f.parentNode.replaceChild(t,f);u=t}j.container=u;j.setPlayer(u,"flash")};this.supportsConfig=function(){if(a.utils.hasFlash()){if(l){var o=a.utils.getFirstPlaylistItemFromConfig(l);if(typeof o.file=="undefined"&&typeof o.levels=="undefined"){return true}else{if(o.file){return flashCanPlay(o.file,o.provider)}else{if(o.levels&&o.levels.length){for(var n=0;n<o.levels.length;n++){if(o.levels[n].file&&flashCanPlay(o.levels[n].file,o.provider)){return true}}}}}}else{return true}}return false};flashCanPlay=function(n,p){var o=["video","http","sound","image"];if(p&&(o.toString().indexOf(p)<0)){return true}var q=a.utils.extension(n);if(!q){return true}if(a.utils.exists(a.utils.extensionmap[q])&&!a.utils.exists(a.utils.extensionmap[q].flash)){return false}return true}}})(jwplayer);(function(a){a.embed.html5=function(c,g,b,d,f){function e(j,k,h){return function(l){var m=document.getElementById(c.id+"_displayarea");if(h){m.appendChild(k)}j.resize(m.clientWidth,m.clientHeight);k.left=m.style.left;k.top=m.style.top}}this.embed=function(){if(a.html5){d.setupPlugins(f,b,e);c.innerHTML="";var j=a.utils.extend({screencolor:"0x000000"},b);var h=["plugins","modes","events"];for(var k=0;k<h.length;k++){delete j[h[k]]}if(j.levels&&!j.sources){j.sources=b.levels}if(j.skin&&j.skin.toLowerCase().indexOf(".zip")>0){j.skin=j.skin.replace(/\.zip/i,".xml")}var l=new (a.html5(c)).setup(j);f.container=document.getElementById(f.id);f.setPlayer(l,"html5")}else{return null}};this.supportsConfig=function(){if(!!a.vid.canPlayType){if(b){var j=a.utils.getFirstPlaylistItemFromConfig(b);if(typeof j.file=="undefined"&&typeof j.levels=="undefined"){return true}else{if(j.file){return html5CanPlay(a.vid,j.file,j.provider,j.playlistfile)}else{if(j.levels&&j.levels.length){for(var h=0;h<j.levels.length;h++){if(j.levels[h].file&&html5CanPlay(a.vid,j.levels[h].file,j.provider,j.playlistfile)){return true}}}}}}else{return true}}return false};html5CanPlay=function(k,j,l,h){if(h){return false}if(l&&l=="youtube"){return true}if(l&&l!="video"&&l!="http"&&l!="sound"){return false}if(navigator.userAgent.match(/BlackBerry/i)!==null){return false}var m=a.utils.extension(j);if(!a.utils.exists(m)||!a.utils.exists(a.utils.extensionmap[m])){return true}if(!a.utils.exists(a.utils.extensionmap[m].html5)){return false}if(a.utils.isLegacyAndroid()&&m.match(/m4v|mp4/)){return true}return browserCanPlay(k,a.utils.extensionmap[m].html5)};browserCanPlay=function(j,h){if(!h){return true}if(j.canPlayType(h)){return true}else{if(h=="audio/mp3"&&navigator.userAgent.match(/safari/i)){return j.canPlayType("audio/mpeg")}else{return false}}}}})(jwplayer);(function(a){a.embed.logo=function(m,l,d){var j={prefix:"http://l.longtailvideo.com/"+l+"/",file:"logo.png",link:"http://www.longtailvideo.com/players/jw-flv-player/",linktarget:"_top",margin:8,out:0.5,over:1,timeout:5,hide:false,position:"bottom-left"};_css=a.utils.css;var b;var h;k();function k(){o();c();f()}function o(){if(j.prefix){var q=a.version.split(/\W/).splice(0,2).join("/");if(j.prefix.indexOf(q)<0){j.prefix+=q+"/"}}h=a.utils.extend({},j)}function p(){var s={border:"none",textDecoration:"none",position:"absolute",cursor:"pointer",zIndex:10};s.display=h.hide?"none":"block";var r=h.position.toLowerCase().split("-");for(var q in r){s[r[q]]=h.margin}return s}function c(){b=document.createElement("img");b.id=d+"_jwplayer_logo";b.style.display="none";b.onload=function(q){_css(b,p());e()};if(!h.file){return}if(h.file.indexOf("http://")===0){b.src=h.file}else{b.src=h.prefix+h.file}}if(!h.file){return}function f(){if(h.link){b.onmouseover=g;b.onmouseout=e;b.onclick=n}else{this.mouseEnabled=false}}function n(q){if(typeof q!="undefined"){q.preventDefault();q.stopPropagation()}if(h.link){window.open(h.link,h.linktarget)}return}function e(q){if(h.link){b.style.opacity=h.out}return}function g(q){if(h.hide){b.style.opacity=h.over}return}return b}})(jwplayer);(function(a){a.html5=function(b){var c=b;this.setup=function(d){a.utils.extend(this,new a.html5.api(c,d));return this};return this}})(jwplayer);(function(a){var c=a.utils;var b=c.css;a.html5.view=function(v,u,g){var A=v;var o=u;var C=g;var B;var h;var L;var w;var M;var s;var I;var t=false;var F,r;var x,f,e;function E(){B=document.createElement("div");B.id=o.id;B.className=o.className;_videowrapper=document.createElement("div");_videowrapper.id=B.id+"_video_wrapper";o.id=B.id+"_video";b(B,{position:"relative",height:C.height,width:C.width,padding:0,backgroundColor:N(),zIndex:0});function N(){if(A.skin.getComponentSettings("display")&&A.skin.getComponentSettings("display").backgroundcolor){return A.skin.getComponentSettings("display").backgroundcolor}return parseInt("000000",16)}b(o,{width:"100%",height:"100%",top:0,left:0,zIndex:1,margin:"auto",display:"block"});b(_videowrapper,{overflow:"hidden",position:"absolute",top:0,left:0,bottom:0,right:0});c.wrap(o,B);c.wrap(o,_videowrapper);w=document.createElement("div");w.id=B.id+"_displayarea";B.appendChild(w);_instreamArea=document.createElement("div");_instreamArea.id=B.id+"_instreamarea";b(_instreamArea,{overflow:"hidden",position:"absolute",top:0,left:0,bottom:0,right:0,zIndex:100,background:"000000",display:"none"});B.appendChild(_instreamArea)}function l(){for(var N=0;N<C.plugins.order.length;N++){var O=C.plugins.order[N];if(c.exists(C.plugins.object[O].getDisplayElement)){C.plugins.object[O].height=c.parseDimension(C.plugins.object[O].getDisplayElement().style.height);C.plugins.object[O].width=c.parseDimension(C.plugins.object[O].getDisplayElement().style.width);C.plugins.config[O].currentPosition=C.plugins.config[O].position}}z()}function n(N){if(f){return}if(C.getMedia()&&C.getMedia().hasChrome()){w.style.display="none"}else{switch(N.newstate){case N.newstate==a.api.events.state.PLAYING:w.style.display="none";break;default:w.style.display="block";break}}}function z(O){var Q=C.getMedia()?C.getMedia().getDisplayElement():null;if(c.exists(Q)){if(I!=Q){if(I&&I.parentNode){I.parentNode.replaceChild(Q,I)}I=Q}for(var N=0;N<C.plugins.order.length;N++){var P=C.plugins.order[N];if(c.exists(C.plugins.object[P].getDisplayElement)){C.plugins.config[P].currentPosition=C.plugins.config[P].position}}}k(C.width,C.height)}this.setup=function(){if(C&&C.getMedia()){o=C.getMedia().getDisplayElement()}E();l();A.jwAddEventListener(a.api.events.JWPLAYER_PLAYER_STATE,n);A.jwAddEventListener(a.api.events.JWPLAYER_MEDIA_LOADED,z);A.jwAddEventListener(a.api.events.JWPLAYER_MEDIA_META,function(O){D()});var N;if(c.exists(window.onresize)){N=window.onresize}window.onresize=function(O){if(c.exists(N)){try{N(O)}catch(Q){}}if(A.jwGetFullscreen()){if(!G()){var P=c.getBoundingClientRect(document.body);C.width=Math.abs(P.left)+Math.abs(P.right);C.height=window.innerHeight;k(C.width,C.height)}}else{k(C.width,C.height)}}};function j(N){switch(N.keyCode){case 27:if(A.jwGetFullscreen()){A.jwSetFullscreen(false)}break;case 32:if(A.jwGetState()!=a.api.events.state.IDLE&&A.jwGetState()!=a.api.events.state.PAUSED){A.jwPause()}else{A.jwPlay()}break}}function k(N,W){if(B.style.display=="none"){return}var Q=[].concat(C.plugins.order);Q.reverse();M=Q.length+2;if(G()){try{if(C.fullscreen&&!C.getMedia().getDisplayElement().webkitDisplayingFullscreen){C.fullscreen=false}}catch(T){}}if(!C.fullscreen){h=N;L=W;if(typeof N=="string"&&N.indexOf("%")>0){h=c.getElementWidth(c.parentNode(B))*parseInt(N.replace("%"),"")/100}else{h=N}if(typeof W=="string"&&W.indexOf("%")>0){L=c.getElementHeight(c.parentNode(B))*parseInt(W.replace("%"),"")/100}else{L=W}var R={top:0,bottom:0,left:0,right:0,width:h,height:L,position:"absolute"};b(w,R);var X={};var U;try{U=C.plugins.object.display.getDisplayElement()}catch(T){}if(U){X.width=c.parseDimension(U.style.width);X.height=c.parseDimension(U.style.height)}var V=c.extend({},R,X,{zIndex:_instreamArea.style.zIndex,display:_instreamArea.style.display});b(_instreamArea,V);b(B,{height:L,width:h});var S=q(y,Q);if(S.length>0){M+=S.length;var P=S.indexOf("playlist"),O=S.indexOf("controlbar");if(P>=0&&O>=0){S[P]=S.splice(O,1,S[P])[0]}q(m,S,true)}F=c.getElementWidth(w);r=c.getElementHeight(w)}else{if(!G()){q(K,Q,true)}}D()}function q(U,Q,R){var S=[];for(var P=0;P<Q.length;P++){var T=Q[P];if(c.exists(C.plugins.object[T].getDisplayElement)){if(C.plugins.config[T].currentPosition!=a.html5.view.positions.NONE){var N=U(T,M--);if(!N){S.push(T)}else{var O=N.width;var V=N.height;if(R){delete N.width;delete N.height}b(C.plugins.object[T].getDisplayElement(),N);C.plugins.object[T].resize(O,V)}}else{b(C.plugins.object[T].getDisplayElement(),{display:"none"})}}}return S}function y(O,P){if(c.exists(C.plugins.object[O].getDisplayElement)){if(C.plugins.config[O].position&&H(C.plugins.config[O].position)){if(!c.exists(C.plugins.object[O].getDisplayElement().parentNode)){B.appendChild(C.plugins.object[O].getDisplayElement())}var N=d(O);N.zIndex=P;return N}}return false}function m(N,O){if(!c.exists(C.plugins.object[N].getDisplayElement().parentNode)){w.appendChild(C.plugins.object[N].getDisplayElement())}return{position:"absolute",width:(c.getElementWidth(w)-c.parseDimension(w.style.left)-c.parseDimension(w.style.right)),height:(c.getElementHeight(w)-c.parseDimension(w.style.top)-c.parseDimension(w.style.bottom)),zIndex:O}}function K(N,O){return{position:"fixed",width:C.width,height:C.height,zIndex:O}}var D=this.resizeMedia=function(){w.style.position="absolute";var P=C.getMedia()?C.getMedia().getDisplayElement():e;if(!P){return}if(P&&P.tagName.toLowerCase()=="video"){if(!P.videoWidth||!P.videoHeight){return}P.style.position="absolute";c.fadeTo(P,1,0.25);if(P.parentNode){P.parentNode.style.left=w.style.left;P.parentNode.style.top=w.style.top}if(C.fullscreen&&A.jwGetStretching()==a.utils.stretching.EXACTFIT&&!c.isMobile()){var N=document.createElement("div");c.stretch(a.utils.stretching.UNIFORM,N,c.getElementWidth(w),c.getElementHeight(w),F,r);c.stretch(a.utils.stretching.EXACTFIT,P,c.parseDimension(N.style.width),c.parseDimension(N.style.height),P.videoWidth?P.videoWidth:400,P.videoHeight?P.videoHeight:300);b(P,{left:N.style.left,top:N.style.top})}else{c.stretch(A.jwGetStretching(),P,c.getElementWidth(w),c.getElementHeight(w),P.videoWidth?P.videoWidth:400,P.videoHeight?P.videoHeight:300)}}else{var O=C.plugins.object.display.getDisplayElement();if(O){C.getMedia().resize(c.parseDimension(O.style.width),c.parseDimension(O.style.height))}else{C.getMedia().resize(c.parseDimension(w.style.width),c.parseDimension(w.style.height))}}};var d=this.getComponentPosition=function(O){var P={position:"absolute",margin:0,padding:0,top:null};var N=C.plugins.config[O].currentPosition.toLowerCase();switch(N.toUpperCase()){case a.html5.view.positions.TOP:P.top=c.parseDimension(w.style.top);P.left=c.parseDimension(w.style.left);P.width=c.getElementWidth(w)-c.parseDimension(w.style.left)-c.parseDimension(w.style.right);P.height=C.plugins.object[O].height;w.style[N]=c.parseDimension(w.style[N])+C.plugins.object[O].height+"px";w.style.height=c.getElementHeight(w)-P.height+"px";break;case a.html5.view.positions.RIGHT:P.top=c.parseDimension(w.style.top);P.right=c.parseDimension(w.style.right);P.width=C.plugins.object[O].width;P.height=c.getElementHeight(w)-c.parseDimension(w.style.top)-c.parseDimension(w.style.bottom);w.style.width=c.getElementWidth(w)-P.width+"px";break;case a.html5.view.positions.BOTTOM:P.bottom=c.parseDimension(w.style.bottom);P.left=c.parseDimension(w.style.left);P.width=c.getElementWidth(w)-c.parseDimension(w.style.left)-c.parseDimension(w.style.right);P.height=C.plugins.object[O].height;w.style.height=c.getElementHeight(w)-P.height+"px";break;case a.html5.view.positions.LEFT:P.top=c.parseDimension(w.style.top);P.left=c.parseDimension(w.style.left);P.width=C.plugins.object[O].width;P.height=c.getElementHeight(w)-c.parseDimension(w.style.top)-c.parseDimension(w.style.bottom);w.style[N]=c.parseDimension(w.style[N])+C.plugins.object[O].width+"px";w.style.width=c.getElementWidth(w)-P.width+"px";break;default:break}return P};this.resize=k;var p;this.fullscreen=function(Q){var S;try{S=C.getMedia().getDisplayElement()}catch(R){}if(G()&&S&&S.webkitSupportsFullscreen){if(Q&&!S.webkitDisplayingFullscreen){try{c.transform(S);p=w.style.display;w.style.display="none";S.webkitEnterFullscreen()}catch(P){}}else{if(!Q){D();if(S.webkitDisplayingFullscreen){try{S.webkitExitFullscreen()}catch(P){}}w.style.display=p}}t=false}else{if(Q){document.onkeydown=j;clearInterval(s);var O=c.getBoundingClientRect(document.body);C.width=Math.abs(O.left)+Math.abs(O.right);C.height=window.innerHeight;var N={position:"fixed",width:"100%",height:"100%",top:0,left:0,zIndex:2147483000};b(B,N);N.zIndex=1;if(C.getMedia()&&C.getMedia().getDisplayElement()){b(C.getMedia().getDisplayElement(),N)}N.zIndex=2;b(w,N);t=true}else{document.onkeydown="";C.width=h;C.height=L;b(B,{position:"relative",height:C.height,width:C.width,zIndex:0});t=false}k(C.width,C.height)}};function H(N){return([a.html5.view.positions.TOP,a.html5.view.positions.RIGHT,a.html5.view.positions.BOTTOM,a.html5.view.positions.LEFT].toString().indexOf(N.toUpperCase())>-1)}function G(){if(A.jwGetState()!=a.api.events.state.IDLE&&!t&&(C.getMedia()&&C.getMedia().getDisplayElement()&&C.getMedia().getDisplayElement().webkitSupportsFullscreen)&&c.useNativeFullscreen()){return true}return false}this.setupInstream=function(N,O){c.css(_instreamArea,{display:"block",position:"absolute"});w.style.display="none";_instreamArea.appendChild(N);e=O;f=true};var J=this.destroyInstream=function(){_instreamArea.style.display="none";_instreamArea.innerHTML="";w.style.display="block";e=null;f=false;k(C.width,C.height)}};a.html5.view.positions={TOP:"TOP",RIGHT:"RIGHT",BOTTOM:"BOTTOM",LEFT:"LEFT",OVER:"OVER",NONE:"NONE"}})(jwplayer);(function(a){var b={backgroundcolor:"",margin:10,font:"Arial,sans-serif",fontsize:10,fontcolor:parseInt("000000",16),fontstyle:"normal",fontweight:"bold",buttoncolor:parseInt("ffffff",16),position:a.html5.view.positions.BOTTOM,idlehide:false,hideplaylistcontrols:false,forcenextprev:false,layout:{left:{position:"left",elements:[{name:"play",type:"button"},{name:"divider",type:"divider"},{name:"prev",type:"button"},{name:"divider",type:"divider"},{name:"next",type:"button"},{name:"divider",type:"divider"},{name:"elapsed",type:"text"}]},center:{position:"center",elements:[{name:"time",type:"slider"}]},right:{position:"right",elements:[{name:"duration",type:"text"},{name:"blank",type:"button"},{name:"divider",type:"divider"},{name:"mute",type:"button"},{name:"volume",type:"slider"},{name:"divider",type:"divider"},{name:"fullscreen",type:"button"}]}}};_utils=a.utils;_css=_utils.css;_hide=function(c){_css(c,{display:"none"})};_show=function(c){_css(c,{display:"block"})};a.html5.controlbar=function(m,X){window.controlbar=this;var l=m;var D=_utils.extend({},b,l.skin.getComponentSettings("controlbar"),X);if(D.position==a.html5.view.positions.NONE||typeof a.html5.view.positions[D.position]=="undefined"){return}if(_utils.mapLength(l.skin.getComponentLayout("controlbar"))>0){D.layout=l.skin.getComponentLayout("controlbar")}var af;var Q;var ae;var E;var w="none";var h;var k;var ag;var g;var f;var z;var R={};var q=false;var c={};var ab;var j=false;var p;var d;var U=false;var G=false;var H;var Z=new a.html5.eventdispatcher();_utils.extend(this,Z);function K(){if(!ab){ab=l.skin.getSkinElement("controlbar","background");if(!ab){ab={width:0,height:0,src:null}}}return ab}function O(){ae=0;E=0;Q=0;if(!q){var ao={height:K().height,backgroundColor:D.backgroundcolor};af=document.createElement("div");af.id=l.id+"_jwplayer_controlbar";_css(af,ao)}var an=(l.skin.getSkinElement("controlbar","capLeft"));var am=(l.skin.getSkinElement("controlbar","capRight"));if(an){y("capLeft","left",false,af)}ac("background",af,{position:"absolute",height:K().height,left:(an?an.width:0),zIndex:0},"img");if(K().src){R.background.src=K().src}ac("elements",af,{position:"relative",height:K().height,zIndex:1});if(am){y("capRight","right",false,af)}}this.getDisplayElement=function(){return af};this.resize=function(ao,am){S();_utils.cancelAnimation(af);f=ao;z=am;if(G!=l.jwGetFullscreen()){G=l.jwGetFullscreen();if(!G){Y()}d=undefined}var an=x();J({id:l.id,duration:ag,position:k});v({id:l.id,bufferPercent:g});return an};this.show=function(){if(j){j=false;_show(af);V()}};this.hide=function(){if(!j){j=true;_hide(af);ad()}};function r(){var an=["timeSlider","volumeSlider","timeSliderRail","volumeSliderRail"];for(var ao in an){var am=an[ao];if(typeof R[am]!="undefined"){c[am]=_utils.getBoundingClientRect(R[am])}}}var e;function Y(am){if(j){return}clearTimeout(p);if(D.position==a.html5.view.positions.OVER||l.jwGetFullscreen()){switch(l.jwGetState()){case a.api.events.state.PAUSED:case a.api.events.state.IDLE:if(af&&af.style.opacity<1&&(!D.idlehide||_utils.exists(am))){e=false;setTimeout(function(){if(!e){W()}},100)}if(D.idlehide){p=setTimeout(function(){A()},2000)}break;default:e=true;if(am){W()}p=setTimeout(function(){A()},2000);break}}else{W()}}function A(){if(!j){ad();if(af.style.opacity==1){_utils.cancelAnimation(af);_utils.fadeTo(af,0,0.1,1,0)}}}function W(){if(!j){V();if(af.style.opacity==0){_utils.cancelAnimation(af);_utils.fadeTo(af,1,0.1,0,0)}}}function I(am){return function(){if(U&&d!=am){d=am;Z.sendEvent(am,{component:"controlbar",boundingRect:P()})}}}var V=I(a.api.events.JWPLAYER_COMPONENT_SHOW);var ad=I(a.api.events.JWPLAYER_COMPONENT_HIDE);function P(){if(D.position==a.html5.view.positions.OVER||l.jwGetFullscreen()){return _utils.getDimensions(af)}else{return{x:0,y:0,width:0,height:0}}}function ac(aq,ap,ao,am){var an;if(!q){if(!am){am="div"}an=document.createElement(am);R[aq]=an;an.id=af.id+"_"+aq;ap.appendChild(an)}else{an=document.getElementById(af.id+"_"+aq)}if(_utils.exists(ao)){_css(an,ao)}return an}function N(){if(l.jwGetHeight()<=40){D.layout=_utils.clone(D.layout);for(var am=0;am<D.layout.left.elements.length;am++){if(D.layout.left.elements[am].name=="fullscreen"){D.layout.left.elements.splice(am,1)}}for(am=0;am<D.layout.right.elements.length;am++){if(D.layout.right.elements[am].name=="fullscreen"){D.layout.right.elements.splice(am,1)}}o()}al(D.layout.left);al(D.layout.center);al(D.layout.right)}function al(ap,am){var aq=ap.position=="right"?"right":"left";var ao=_utils.extend([],ap.elements);if(_utils.exists(am)){ao.reverse()}var ap=ac(ap.position+"Group",R.elements,{"float":"left",styleFloat:"left",cssFloat:"left",height:"100%"});for(var an=0;an<ao.length;an++){C(ao[an],aq,ap)}}function L(){return Q++}function C(aq,at,av){var ap,an,ao,am,aw;if(!av){av=R.elements}if(aq.type=="divider"){y("divider"+L(),at,true,av,undefined,aq.width,aq.element);return}switch(aq.name){case"play":y("playButton",at,false,av);y("pauseButton",at,true,av);T("playButton","jwPlay");T("pauseButton","jwPause");break;case"prev":y("prevButton",at,true,av);T("prevButton","jwPlaylistPrev");break;case"stop":y("stopButton",at,true,av);T("stopButton","jwStop");break;case"next":y("nextButton",at,true,av);T("nextButton","jwPlaylistNext");break;case"elapsed":y("elapsedText",at,true,av,null,null,l.skin.getSkinElement("controlbar","elapsedBackground"));break;case"time":an=!_utils.exists(l.skin.getSkinElement("controlbar","timeSliderCapLeft"))?0:l.skin.getSkinElement("controlbar","timeSliderCapLeft").width;ao=!_utils.exists(l.skin.getSkinElement("controlbar","timeSliderCapRight"))?0:l.skin.getSkinElement("controlbar","timeSliderCapRight").width;ap=at=="left"?an:ao;aw={height:K().height,position:"relative","float":"left",styleFloat:"left",cssFloat:"left"};var ar=ac("timeSlider",av,aw);y("timeSliderCapLeft",at,true,ar,"relative");y("timeSliderRail",at,false,ar,"relative");y("timeSliderBuffer",at,false,ar,"absolute");y("timeSliderProgress",at,false,ar,"absolute");y("timeSliderThumb",at,false,ar,"absolute");y("timeSliderCapRight",at,true,ar,"relative");aa("time");break;case"fullscreen":y("fullscreenButton",at,false,av);y("normalscreenButton",at,true,av);T("fullscreenButton","jwSetFullscreen",true);T("normalscreenButton","jwSetFullscreen",false);break;case"volume":an=!_utils.exists(l.skin.getSkinElement("controlbar","volumeSliderCapLeft"))?0:l.skin.getSkinElement("controlbar","volumeSliderCapLeft").width;ao=!_utils.exists(l.skin.getSkinElement("controlbar","volumeSliderCapRight"))?0:l.skin.getSkinElement("controlbar","volumeSliderCapRight").width;ap=at=="left"?an:ao;am=l.skin.getSkinElement("controlbar","volumeSliderRail").width+an+ao;aw={height:K().height,position:"relative",width:am,"float":"left",styleFloat:"left",cssFloat:"left"};var au=ac("volumeSlider",av,aw);y("volumeSliderCapLeft",at,false,au,"relative");y("volumeSliderRail",at,false,au,"relative");y("volumeSliderProgress",at,false,au,"absolute");y("volumeSliderThumb",at,false,au,"absolute");y("volumeSliderCapRight",at,false,au,"relative");aa("volume");break;case"mute":y("muteButton",at,false,av);y("unmuteButton",at,true,av);T("muteButton","jwSetMute",true);T("unmuteButton","jwSetMute",false);break;case"duration":y("durationText",at,true,av,null,null,l.skin.getSkinElement("controlbar","durationBackground"));break}}function y(ap,at,an,aw,aq,am,ao){if(_utils.exists(l.skin.getSkinElement("controlbar",ap))||ap.indexOf("Text")>0||ap.indexOf("divider")===0){var ar={height:"100%",position:aq?aq:"relative",display:"block","float":"left",styleFloat:"left",cssFloat:"left"};if((ap.indexOf("next")===0||ap.indexOf("prev")===0)&&(l.jwGetPlaylist().length<2||D.hideplaylistcontrols.toString()=="true")){if(D.forcenextprev.toString()!="true"){an=false;ar.display="none"}}var ax;if(ap.indexOf("Text")>0){ap.innerhtml="00:00";ar.font=D.fontsize+"px/"+(K().height+1)+"px "+D.font;ar.color=D.fontcolor;ar.textAlign="center";ar.fontWeight=D.fontweight;ar.fontStyle=D.fontstyle;ar.cursor="default";if(ao){ar.background="url("+ao.src+") no-repeat center";ar.backgroundSize="100% "+K().height+"px"}ar.padding="0 5px"}else{if(ap.indexOf("divider")===0){if(am){if(!isNaN(parseInt(am))){ax=parseInt(am)}}else{if(ao){var au=l.skin.getSkinElement("controlbar",ao);if(au){ar.background="url("+au.src+") repeat-x center left";ax=au.width}}else{ar.background="url("+l.skin.getSkinElement("controlbar","divider").src+") repeat-x center left";ax=l.skin.getSkinElement("controlbar","divider").width}}}else{ar.background="url("+l.skin.getSkinElement("controlbar",ap).src+") repeat-x center left";ax=l.skin.getSkinElement("controlbar",ap).width}}if(at=="left"){if(an){ae+=ax}}else{if(at=="right"){if(an){E+=ax}}}if(_utils.typeOf(aw)=="undefined"){aw=R.elements}ar.width=ax;if(q){_css(R[ap],ar)}else{var av=ac(ap,aw,ar);if(_utils.exists(l.skin.getSkinElement("controlbar",ap+"Over"))){av.onmouseover=function(ay){av.style.backgroundImage=["url(",l.skin.getSkinElement("controlbar",ap+"Over").src,")"].join("")};av.onmouseout=function(ay){av.style.backgroundImage=["url(",l.skin.getSkinElement("controlbar",ap).src,")"].join("")}}if(ap.indexOf("divider")==0){av.setAttribute("class","divider")}av.innerHTML=" "}}}function F(){l.jwAddEventListener(a.api.events.JWPLAYER_PLAYLIST_LOADED,B);l.jwAddEventListener(a.api.events.JWPLAYER_PLAYLIST_ITEM,t);l.jwAddEventListener(a.api.events.JWPLAYER_MEDIA_BUFFER,v);l.jwAddEventListener(a.api.events.JWPLAYER_PLAYER_STATE,s);l.jwAddEventListener(a.api.events.JWPLAYER_MEDIA_TIME,J);l.jwAddEventListener(a.api.events.JWPLAYER_MEDIA_MUTE,ak);l.jwAddEventListener(a.api.events.JWPLAYER_MEDIA_VOLUME,n);l.jwAddEventListener(a.api.events.JWPLAYER_MEDIA_COMPLETE,M)}function B(){if(!D.hideplaylistcontrols){if(l.jwGetPlaylist().length>1||D.forcenextprev.toString()=="true"){_show(R.nextButton);_show(R.prevButton)}else{_hide(R.nextButton);_hide(R.prevButton)}x();ah()}}function t(am){ag=l.jwGetPlaylist()[am.index].duration;J({id:l.id,duration:ag,position:0});v({id:l.id,bufferProgress:0})}function ah(){J({id:l.id,duration:l.jwGetDuration(),position:0});v({id:l.id,bufferProgress:0});ak({id:l.id,mute:l.jwGetMute()});s({id:l.id,newstate:a.api.events.state.IDLE});n({id:l.id,volume:l.jwGetVolume()})}function T(ao,ap,an){if(q){return}if(_utils.exists(l.skin.getSkinElement("controlbar",ao))){var am=R[ao];if(_utils.exists(am)){_css(am,{cursor:"pointer"});if(ap=="fullscreen"){am.onmouseup=function(aq){aq.stopPropagation();l.jwSetFullscreen(!l.jwGetFullscreen())}}else{am.onmouseup=function(aq){aq.stopPropagation();if(_utils.exists(an)){l[ap](an)}else{l[ap]()}}}}}}function aa(am){if(q){return}var an=R[am+"Slider"];_css(R.elements,{cursor:"pointer"});_css(an,{cursor:"pointer"});an.onmousedown=function(ao){w=am};an.onmouseup=function(ao){ao.stopPropagation();aj(ao.pageX)};an.onmousemove=function(ao){if(w=="time"){h=true;var ap=ao.pageX-c[am+"Slider"].left-window.pageXOffset;_css(R[w+"SliderThumb"],{left:ap})}}}function aj(an){h=false;var am;if(w=="time"){am=an-c.timeSliderRail.left+window.pageXOffset;var ap=am/c.timeSliderRail.width*ag;if(ap<0){ap=0}else{if(ap>ag){ap=ag-3}}if(l.jwGetState()==a.api.events.state.PAUSED||l.jwGetState()==a.api.events.state.IDLE){l.jwPlay()}l.jwSeek(ap)}else{if(w=="volume"){am=an-c.volumeSliderRail.left-window.pageXOffset;var ao=Math.round(am/c.volumeSliderRail.width*100);if(ao<10){ao=0}else{if(ao>100){ao=100}}if(l.jwGetMute()){l.jwSetMute(false)}l.jwSetVolume(ao)}}w="none"}function v(an){if(_utils.exists(an.bufferPercent)){g=an.bufferPercent}if(c.timeSliderRail){var ap=l.skin.getSkinElement("controlbar","timeSliderCapLeft");var ao=c.timeSliderRail.width;var am=isNaN(Math.round(ao*g/100))?0:Math.round(ao*g/100);_css(R.timeSliderBuffer,{width:am,left:ap?ap.width:0})}}function ak(am){if(am.mute){_hide(R.muteButton);_show(R.unmuteButton);_hide(R.volumeSliderProgress)}else{_show(R.muteButton);_hide(R.unmuteButton);_show(R.volumeSliderProgress)}}function s(am){if(am.newstate==a.api.events.state.BUFFERING||am.newstate==a.api.events.state.PLAYING){_show(R.pauseButton);_hide(R.playButton)}else{_hide(R.pauseButton);_show(R.playButton)}Y();if(am.newstate==a.api.events.state.IDLE){_hide(R.timeSliderBuffer);_hide(R.timeSliderProgress);_hide(R.timeSliderThumb);J({id:l.id,duration:l.jwGetDuration(),position:0})}else{_show(R.timeSliderBuffer);if(am.newstate!=a.api.events.state.BUFFERING){_show(R.timeSliderProgress);_show(R.timeSliderThumb)}}}function M(am){v({bufferPercent:0});J(_utils.extend(am,{position:0,duration:ag}))}function J(ap){if(_utils.exists(ap.position)){k=ap.position}if(_utils.exists(ap.duration)){ag=ap.duration}var an=(k===ag===0)?0:k/ag;var ar=c.timeSliderRail;if(ar){var am=isNaN(Math.round(ar.width*an))?0:Math.round(ar.width*an);var aq=l.skin.getSkinElement("controlbar","timeSliderCapLeft");var ao=am+(aq?aq.width:0);if(R.timeSliderProgress){_css(R.timeSliderProgress,{width:am,left:aq?aq.width:0});if(!h){if(R.timeSliderThumb){R.timeSliderThumb.style.left=ao+"px"}}}}if(R.durationText){R.durationText.innerHTML=_utils.timeFormat(ag)}if(R.elapsedText){R.elapsedText.innerHTML=_utils.timeFormat(k)}}function o(){var am=R.elements.childNodes;var ar,ap;for(var ao=0;ao<am.length;ao++){var aq=am[ao].childNodes;for(var an in aq){if(isNaN(parseInt(an,10))){continue}if(aq[an].id.indexOf(af.id+"_divider")===0&&ap&&ap.id.indexOf(af.id+"_divider")===0&&aq[an].style.backgroundImage==ap.style.backgroundImage){aq[an].style.display="none"}else{if(aq[an].id.indexOf(af.id+"_divider")===0&&ar&&ar.style.display!="none"){aq[an].style.display="block"}}if(aq[an].style.display!="none"){ap=aq[an]}ar=aq[an]}}}function ai(){if(l.jwGetFullscreen()){_show(R.normalscreenButton);_hide(R.fullscreenButton)}else{_hide(R.normalscreenButton);_show(R.fullscreenButton)}if(l.jwGetState()==a.api.events.state.BUFFERING||l.jwGetState()==a.api.events.state.PLAYING){_show(R.pauseButton);_hide(R.playButton)}else{_hide(R.pauseButton);_show(R.playButton)}if(l.jwGetMute()==true){_hide(R.muteButton);_show(R.unmuteButton);_hide(R.volumeSliderProgress)}else{_show(R.muteButton);_hide(R.unmuteButton);_show(R.volumeSliderProgress)}}function x(){o();ai();var ao={width:f};var aw={"float":"left",styleFloat:"left",cssFloat:"left"};if(D.position==a.html5.view.positions.OVER||l.jwGetFullscreen()){ao.left=D.margin;ao.width-=2*D.margin;ao.top=z-K().height-D.margin;ao.height=K().height}var aq=l.skin.getSkinElement("controlbar","capLeft");var au=l.skin.getSkinElement("controlbar","capRight");aw.width=ao.width-(aq?aq.width:0)-(au?au.width:0);var ap=_utils.getBoundingClientRect(R.leftGroup).width;var at=_utils.getBoundingClientRect(R.rightGroup).width;var ar=aw.width-ap-at-1;var an=ar;var am=l.skin.getSkinElement("controlbar","timeSliderCapLeft");var av=l.skin.getSkinElement("controlbar","timeSliderCapRight");if(_utils.exists(am)){an-=am.width}if(_utils.exists(av)){an-=av.width}R.timeSlider.style.width=ar+"px";R.timeSliderRail.style.width=an+"px";_css(af,ao);_css(R.elements,aw);_css(R.background,aw);r();return ao}function n(ar){if(_utils.exists(R.volumeSliderRail)){var ao=isNaN(ar.volume/100)?1:ar.volume/100;var ap=_utils.parseDimension(R.volumeSliderRail.style.width);var am=isNaN(Math.round(ap*ao))?0:Math.round(ap*ao);var at=_utils.parseDimension(R.volumeSliderRail.style.right);var an=(!_utils.exists(l.skin.getSkinElement("controlbar","volumeSliderCapLeft")))?0:l.skin.getSkinElement("controlbar","volumeSliderCapLeft").width;_css(R.volumeSliderProgress,{width:am,left:an});if(R.volumeSliderThumb){var aq=(am-Math.round(_utils.parseDimension(R.volumeSliderThumb.style.width)/2));aq=Math.min(Math.max(aq,0),ap-_utils.parseDimension(R.volumeSliderThumb.style.width));_css(R.volumeSliderThumb,{left:aq})}if(_utils.exists(R.volumeSliderCapLeft)){_css(R.volumeSliderCapLeft,{left:0})}}}function S(){try{var an=(l.id.indexOf("_instream")>0?l.id.replace("_instream",""):l.id);H=document.getElementById(an);H.addEventListener("mousemove",Y)}catch(am){_utils.log("Could not add mouse listeners to controlbar: "+am)}}function u(){O();N();r();q=true;F();D.idlehide=(D.idlehide.toString().toLowerCase()=="true");if(D.position==a.html5.view.positions.OVER&&D.idlehide){af.style.opacity=0;U=true}else{af.style.opacity=1;setTimeout((function(){U=true;V()}),1)}S();ah()}u();return this}})(jwplayer);(function(b){var a=["width","height","state","playlist","item","position","buffer","duration","volume","mute","fullscreen"];var c=b.utils;b.html5.controller=function(o,K,f,h){var n=o,m=f,j=h,y=K,M=true,G=-1,A=false,d=false,P,C=[],q=false;var D=(c.exists(m.config.debug)&&(m.config.debug.toString().toLowerCase()=="console")),N=new b.html5.eventdispatcher(y.id,D);c.extend(this,N);function L(T){if(q){N.sendEvent(T.type,T)}else{C.push(T)}}function s(T){if(!q){q=true;N.sendEvent(b.api.events.JWPLAYER_READY,T);if(b.utils.exists(window.playerReady)){playerReady(T)}if(b.utils.exists(window[f.config.playerReady])){window[f.config.playerReady](T)}while(C.length>0){var V=C.shift();N.sendEvent(V.type,V)}if(f.config.autostart&&!b.utils.isIOS()){O()}while(x.length>0){var U=x.shift();B(U.method,U.arguments)}}}m.addGlobalListener(L);m.addEventListener(b.api.events.JWPLAYER_MEDIA_BUFFER_FULL,function(){m.getMedia().play()});m.addEventListener(b.api.events.JWPLAYER_MEDIA_TIME,function(T){if(T.position>=m.playlist[m.item].start&&G>=0){m.playlist[m.item].start=G;G=-1}});m.addEventListener(b.api.events.JWPLAYER_MEDIA_COMPLETE,function(T){setTimeout(E,25)});m.addEventListener(b.api.events.JWPLAYER_PLAYLIST_LOADED,O);m.addEventListener(b.api.events.JWPLAYER_FULLSCREEN,p);function F(){try{P=F;if(!A){A=true;N.sendEvent(b.api.events.JWPLAYER_MEDIA_BEFOREPLAY);A=false;if(d){d=false;P=null;return}}v(m.item);if(m.playlist[m.item].levels[0].file.length>0){if(M||m.state==b.api.events.state.IDLE){m.getMedia().load(m.playlist[m.item]);M=false}else{if(m.state==b.api.events.state.PAUSED){m.getMedia().play()}}}return true}catch(T){N.sendEvent(b.api.events.JWPLAYER_ERROR,T);P=null}return false}function e(){try{if(m.playlist[m.item].levels[0].file.length>0){switch(m.state){case b.api.events.state.PLAYING:case b.api.events.state.BUFFERING:if(m.getMedia()){m.getMedia().pause()}break;default:if(A){d=true}}}return true}catch(T){N.sendEvent(b.api.events.JWPLAYER_ERROR,T)}return false}function z(T){try{if(m.playlist[m.item].levels[0].file.length>0){if(typeof T!="number"){T=parseFloat(T)}switch(m.state){case b.api.events.state.IDLE:if(G<0){G=m.playlist[m.item].start;m.playlist[m.item].start=T}if(!A){F()}break;case b.api.events.state.PLAYING:case b.api.events.state.PAUSED:case b.api.events.state.BUFFERING:m.seek(T);break}}return true}catch(U){N.sendEvent(b.api.events.JWPLAYER_ERROR,U)}return false}function w(T){P=null;if(!c.exists(T)){T=true}try{if((m.state!=b.api.events.state.IDLE||T)&&m.getMedia()){m.getMedia().stop(T)}if(A){d=true}return true}catch(U){N.sendEvent(b.api.events.JWPLAYER_ERROR,U)}return false}function k(){try{if(m.playlist[m.item].levels[0].file.length>0){if(m.config.shuffle){v(S())}else{if(m.item+1==m.playlist.length){v(0)}else{v(m.item+1)}}}if(m.state!=b.api.events.state.IDLE){var U=m.state;m.state=b.api.events.state.IDLE;N.sendEvent(b.api.events.JWPLAYER_PLAYER_STATE,{oldstate:U,newstate:b.api.events.state.IDLE})}F();return true}catch(T){N.sendEvent(b.api.events.JWPLAYER_ERROR,T)}return false}function I(){try{if(m.playlist[m.item].levels[0].file.length>0){if(m.config.shuffle){v(S())}else{if(m.item===0){v(m.playlist.length-1)}else{v(m.item-1)}}}if(m.state!=b.api.events.state.IDLE){var U=m.state;m.state=b.api.events.state.IDLE;N.sendEvent(b.api.events.JWPLAYER_PLAYER_STATE,{oldstate:U,newstate:b.api.events.state.IDLE})}F();return true}catch(T){N.sendEvent(b.api.events.JWPLAYER_ERROR,T)}return false}function S(){var T=null;if(m.playlist.length>1){while(!c.exists(T)){T=Math.floor(Math.random()*m.playlist.length);if(T==m.item){T=null}}}else{T=0}return T}function H(U){if(!m.playlist||!m.playlist[U]){return false}try{if(m.playlist[U].levels[0].file.length>0){var V=m.state;if(V!==b.api.events.state.IDLE){if(m.playlist[m.item]&&m.playlist[m.item].provider==m.playlist[U].provider){w(false)}else{w()}}v(U);F()}return true}catch(T){N.sendEvent(b.api.events.JWPLAYER_ERROR,T)}return false}function v(T){if(!m.playlist[T]){return}m.setActiveMediaProvider(m.playlist[T]);if(m.item!=T){m.item=T;M=true;N.sendEvent(b.api.events.JWPLAYER_PLAYLIST_ITEM,{index:T})}}function g(U){try{v(m.item);var V=m.getMedia();switch(typeof(U)){case"number":V.volume(U);break;case"string":V.volume(parseInt(U,10));break}m.setVolume(U);return true}catch(T){N.sendEvent(b.api.events.JWPLAYER_ERROR,T)}return false}function r(U){try{v(m.item);var V=m.getMedia();if(typeof U=="undefined"){V.mute(!m.mute);m.setMute(!m.mute)}else{if(U.toString().toLowerCase()=="true"){V.mute(true);m.setMute(true)}else{V.mute(false);m.setMute(false)}}return true}catch(T){N.sendEvent(b.api.events.JWPLAYER_ERROR,T)}return false}function J(U,T){try{m.width=U;m.height=T;j.resize(U,T);N.sendEvent(b.api.events.JWPLAYER_RESIZE,{width:m.width,height:m.height});return true}catch(V){N.sendEvent(b.api.events.JWPLAYER_ERROR,V)}return false}function u(U,V){try{if(typeof U=="undefined"){U=!m.fullscreen}if(typeof V=="undefined"){V=true}if(U!=m.fullscreen){m.fullscreen=(U.toString().toLowerCase()=="true");j.fullscreen(m.fullscreen);if(V){N.sendEvent(b.api.events.JWPLAYER_FULLSCREEN,{fullscreen:m.fullscreen})}N.sendEvent(b.api.events.JWPLAYER_RESIZE,{width:m.width,height:m.height})}return true}catch(T){N.sendEvent(b.api.events.JWPLAYER_ERROR,T)}return false}function R(T){try{w();if(A){d=false}m.loadPlaylist(T);if(m.playlist[m.item].provider){v(m.item);if(m.config.autostart.toString().toLowerCase()=="true"&&!c.isIOS()&&!A){F()}return true}else{return false}}catch(U){N.sendEvent(b.api.events.JWPLAYER_ERROR,U)}return false}function O(T){if(!c.isIOS()){v(m.item);if(m.config.autostart.toString().toLowerCase()=="true"&&!c.isIOS()){F()}}}function p(T){u(T.fullscreen,false)}function t(){try{return m.getMedia().detachMedia()}catch(T){return null}}function l(){try{var T=m.getMedia().attachMedia();if(typeof P=="function"){P()}}catch(U){return null}}b.html5.controller.repeatoptions={LIST:"LIST",ALWAYS:"ALWAYS",SINGLE:"SINGLE",NONE:"NONE"};function E(){if(m.state!=b.api.events.state.IDLE){return}P=E;switch(m.config.repeat.toUpperCase()){case b.html5.controller.repeatoptions.SINGLE:F();break;case b.html5.controller.repeatoptions.ALWAYS:if(m.item==m.playlist.length-1&&!m.config.shuffle){H(0)}else{k()}break;case b.html5.controller.repeatoptions.LIST:if(m.item==m.playlist.length-1&&!m.config.shuffle){w();v(0)}else{k()}break;default:w();break}}var x=[];function Q(T){return function(){if(q){B(T,arguments)}else{x.push({method:T,arguments:arguments})}}}function B(V,U){var T=[];for(i=0;i<U.length;i++){T.push(U[i])}V.apply(this,T)}this.play=Q(F);this.pause=Q(e);this.seek=Q(z);this.stop=Q(w);this.next=Q(k);this.prev=Q(I);this.item=Q(H);this.setVolume=Q(g);this.setMute=Q(r);this.resize=Q(J);this.setFullscreen=Q(u);this.load=Q(R);this.playerReady=s;this.detachMedia=t;this.attachMedia=l;this.beforePlay=function(){return A}}})(jwplayer);(function(a){a.html5.defaultSkin=function(){this.text='<?xml version="1.0" ?><skin author="LongTail Video" name="Five" version="1.1"><components><component name="controlbar"><settings><setting name="margin" value="20"/><setting name="fontsize" value="11"/><setting name="fontcolor" value="0x000000"/></settings><layout><group position="left"><button name="play"/><divider name="divider"/><button name="prev"/><divider name="divider"/><button name="next"/><divider name="divider"/><text name="elapsed"/></group><group position="center"><slider name="time"/></group><group position="right"><text name="duration"/><divider name="divider"/><button name="blank"/><divider name="divider"/><button name="mute"/><slider name="volume"/><divider name="divider"/><button name="fullscreen"/></group></layout><elements><element name="background" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAIAAABvFaqvAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAElJREFUOI3t1LERACAMQlFgGvcfxNIhHMK4gsUvUviOmgtNsiAZkBSEKxKEnCYkkQrJn/YwbUNiSDDYRZaQRDaShv+oX9GBZEIuK+8hXVLs+/YAAAAASUVORK5CYII="/><element name="blankButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAYCAYAAAAyJzegAAAAFElEQVQYV2P8//8/AzpgHBUc7oIAGZdH0RjKN8EAAAAASUVORK5CYII="/><element name="capLeft" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAYCAYAAAA7zJfaAAAAQElEQVQIWz3LsRGAMADDQJ0XB5bMINABZ9GENGrszxhjT2WLSqxEJG2JQrTMdV2q5LpOAvyRaVmsi7WdeZ/7+AAaOTq7BVrfOQAAAABJRU5ErkJggg=="/><element name="capRight" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAYCAYAAAA7zJfaAAAAQElEQVQIWz3LsRGAMADDQJ0XB5bMINABZ9GENGrszxhjT2WLSqxEJG2JQrTMdV2q5LpOAvyRaVmsi7WdeZ/7+AAaOTq7BVrfOQAAAABJRU5ErkJggg=="/><element name="divider" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAYCAIAAAC0rgCNAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADhJREFUCB0FwcENgEAAw7Aq+893g8APUILNOQcbFRktVGqUVFRkWNz3xTa2sUaLNUosKlRUvvf5AdbWOTtzmzyWAAAAAElFTkSuQmCC"/><element name="playButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAYCAYAAAAVibZIAAAANUlEQVR42u2RsQkAAAjD/NTTPaW6dXLrINJA1kBpGPMAjDWmOgp1HFQXx+b1KOefO4oxY57R73YnVYCQUCQAAAAASUVORK5CYII="/><element name="pauseButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAYCAYAAAAVibZIAAAAIUlEQVQ4jWNgGAWjYOiD/0gYG3/U0FFDB4Oho2AUDAYAAEwiL9HrpdMVAAAAAElFTkSuQmCC"/><element name="prevButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAYCAYAAAAVibZIAAAAQklEQVQ4y2NgGAWjYOiD/1AMA/JAfB5NjCJD/YH4PRaLyDa0H4lNNUP/DxlD59PCUBCIp3ZEwYA+NZLUKBgFgwEAAN+HLX9sB8u8AAAAAElFTkSuQmCC"/><element name="nextButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAYCAYAAAAVibZIAAAAQElEQVQ4y2NgGAWjYOiD/0B8Hojl0cT+U2ooCL8HYn9qGwrD/bQw9P+QMXQ+tSMqnpoRBUpS+tRMUqNgFAwGAADxZy1/mHvFnAAAAABJRU5ErkJggg=="/><element name="timeSliderRail" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAOElEQVRIDe3BwQkAIRADwAhhw/nU/kWwUK+KPITMABFh19Y+F0acY8CJvX9wYpXgRElwolSIiMf9ZWEDhtwurFsAAAAASUVORK5CYII="/><element name="timeSliderBuffer" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAN0lEQVRIDe3BwQkAMQwDMBcc55mRe9zi7RR+FCwBEWG39vcfGHFm4MTuhhMlwYlVBSdKhYh43AW/LQMKm1spzwAAAABJRU5ErkJggg=="/><element name="timeSliderProgress" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAIElEQVRIiWNgGAWjYBTQBfynMR61YCRYMApGwSigMQAAiVWPcbq6UkIAAAAASUVORK5CYII="/><element name="timeSliderThumb" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAYCAYAAAA/OUfnAAAAO0lEQVQYlWP4//8/Awwz0JgDBP/BeN6Cxf/hnI2btiI4u/fsQ3AOHjqK4Jw4eQbBOX/hEoKDYjSd/AMA4cS4mfLsorgAAAAASUVORK5CYII="/><element name="muteButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAYCAYAAADKx8xXAAAAJklEQVQ4y2NgGAUjDcwH4v/kaPxPikZkxcNVI9mBQ5XoGAWDFwAAsKAXKQQmfbUAAAAASUVORK5CYII="/><element name="unmuteButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAYCAYAAADKx8xXAAAAMklEQVQ4y2NgGAWDHPyntub5xBr6Hwv/Pzk2/yfVG/8psRFE25Oq8T+tQnsIaB4FVAcAi2YVysVY52AAAAAASUVORK5CYII="/><element name="volumeSliderRail" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYAgMAAACdGdVrAAAACVBMVEUAAACmpqampqbBXAu8AAAAAnRSTlMAgJsrThgAAAArSURBVAhbY2AgErBAyA4I2QEhOyBkB4TsYOhAoaCCUCUwDTDtMMNgRuMHAFB5FoGH5T0UAAAAAElFTkSuQmCC"/><element name="volumeSliderProgress" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYAgMAAACdGdVrAAAACVBMVEUAAAAAAAAAAACDY+nAAAAAAnRSTlMAgJsrThgAAAArSURBVAhbY2AgErBAyA4I2QEhOyBkB4TsYOhAoaCCUCUwDTDtMMNgRuMHAFB5FoGH5T0UAAAAAElFTkSuQmCC"/><element name="volumeSliderCapRight" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAYCAYAAAAyJzegAAAAFElEQVQYV2P8//8/AzpgHBUc7oIAGZdH0RjKN8EAAAAASUVORK5CYII="/><element name="fullscreenButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAQklEQVRIiWNgGAWjYMiD/0iYFDmSLbDHImdPLQtgBpEiR7Zl2NijAA5oEkT/0Whi5UiyAJ8BVMsHNMtoo2AUDAIAAGdcIN3IDNXoAAAAAElFTkSuQmCC"/><element name="normalscreenButton" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAP0lEQVRIx2NgGAWjYMiD/1RSQ5QB/wmIUWzJfzx8qhj+n4DYCAY0DyJ7PBbYU8sHMEvwiZFtODXUjIJRMJgBACpWIN2ZxdPTAAAAAElFTkSuQmCC"/></elements></component><component name="display"><elements><element name="background" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAQMAAAAk8RryAAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlOZpuml+rYAAAASSURBVBhXY2AYJuA/GBwY6jQAyDyoK8QcL4QAAAAASUVORK5CYII="/><element name="playIcon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAiUlEQVR42u3XSw2AMBREURwgAQlIQAISKgUpSEFKJeCg5b0E0kWBTVcD9ySTsL0Jn9IBAAAA+K2UUrBlW/Rr5ZDoIeeuoFkxJD9ss03aIXXQqB9SttoG7ZA6qNcOKdttiwcJh9RB+iFl4SshkRBuLR72+9cvH0SOKI2HRo7x/Fi1/uoCAAAAwLsD8ki99IlO2dQAAAAASUVORK5CYII="/><element name="muteIcon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAVUlEQVR42u3WMQrAIAxAUW/g/SdvGmvpoOBeSHgPsjj5QTANAACARCJilIhYM0tEvJM+Ik3Id9E957kQIb+F3OdCPC0hPkQriqWx9hp/x/QGAABQyAPLB22VGrpLDgAAAABJRU5ErkJggg=="/><element name="errorIcon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAA/0lEQVR42u2U0QmEMBAF7cASLMESUoIlpARLSCkpwRJSgiWkhOvAXD4WsgRkyaG5DbyB+Yvg8KITAAAAAAAYk+u61mwk15EjPtlEfihmqIiZR1Qx80ghjgdUuiHXGHSVsoag0x6x8DUoyjD5KovmEJ9NTDMRPIT0mtdIUkjlonuNohO+Ha99DTmkuGgKCTcvebAzx82ZoCWC3/3aIMWSRucaxcjORSFY4xpFdjYJGp1rFGcyCYZ/RVh6AUnfcNZ2zih3/mGj1jVCdiNDwyrq1rA/xMdeEXvDVdnYc1vDc3uPkDObXrlaxbNHSOohQhr/WOeLEWfWTgAAAAAAADzNF9sHJ7PJ57MlAAAAAElFTkSuQmCC"/><element name="bufferIcon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAACBklEQVR42u3Zv0sCYRzH8USTzOsHHEWGkC1HgaDgkktGDjUYtDQ01RDSljQ1BLU02+rk1NTm2NLq4Nx/0L/h9fnCd3j4cnZe1/U8xiO8h3uurufF0/3COd/3/0UWYiEWYiEWYiGJQ+J8xuPxKhXjEMZANinjIZhkGuVRNioE4wVURo4JkHm0xKWmhRAc1bh1EyCUw5BcBIjHiApKa4CErko6DEJwuRo6IRKzyJD8FJAyI3Zp2zRImiBcRhlfo5RtlxCcE3CcDNpGrhYIT2IhAJKilO0VRmzJ32fAMTpBTS0QMfGwlcuKMRftE0DJ0wCJdcOsCkBdXP3Mh9CEFUBTPS9mDZJBG6io4aqVzMdCokCw9H3kT6j/C/9iDdSeUMNC7DkyyxAs/Rk6Qss8FPWRZgdVtUH4DjxEn1zxh+/zj1wHlf4MQhNGrwqA6sY40U8JonRJwEQh+AO3AvCG6gHv4U7IY4krxkroWoAOkoQMGfCBrgIm+YBGqPENpIJ66CJg3x66Y0gnSUidAEEnNr9jjLiWMn5DiWP0OC/oAsCgkq43xBdGDMQr7YASP/vEkHvdl1+JOCcEV5sC4hGEOzTlPuKgd0b0xD4JkRcOgnRRTjdErkYhAsQVq6IdUuPJtmk7BCL3t/h88cx91pKQkI/pkDx6pmYTIjEoxiHsN1YWYiEWYiEWknhflZ5IErA5nr8AAAAASUVORK5CYII="/></elements></component><component name="dock"><settings><setting name="fontcolor" value="0xffffff"/></settings><elements><element name="button" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyAQMAAAAk8RryAAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlOZpuml+rYAAAASSURBVBhXY2AYJuA/GBwY6jQAyDyoK8QcL4QAAAAASUVORK5CYII="/></elements></component><component name="playlist"><settings><setting name="backgroundcolor" value="0xe8e8e8"/></settings><elements><element name="item" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAIAAAC1nk4lAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHBJREFUaN7t2MENwCAMBEEe9N8wSKYC/D8YV7CyJoRkVtVImxkZPQInMxoP0XiIxkM0HsGbjjSNBx544IEHHnjggUe/6UQeey0PIh7XTftGxKPj4eXCtLsHHh+ZxkO0Iw8PR55Ni8ZD9Hu/EAoP0dc5RRg9qeRjVF8AAAAASUVORK5CYII="/><element name="sliderCapTop" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAHCAYAAADnCQYGAAAAFUlEQVQokWP8//8/A7UB46ihI9hQAKt6FPPXhVGHAAAAAElFTkSuQmCC"/><element name="sliderRail" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAYAAABiS3YzAAAAKElEQVQ4y2P4//8/Az68bNmy/+iYkB6GUUNHDR01dNTQUUNHDaXcUABUDOKhcxnsSwAAAABJRU5ErkJggg=="/><element name="sliderThumb" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAYAAABiS3YzAAAAJUlEQVQ4T2P4//8/Ay4MBP9xYbz6Rg0dNXTU0FFDRw0dNZRyQwHH4NBa7GJsXAAAAABJRU5ErkJggg=="/><element name="sliderCapBottom" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAHCAYAAADnCQYGAAAAFUlEQVQokWP8//8/A7UB46ihI9hQAKt6FPPXhVGHAAAAAElFTkSuQmCC"/></elements></component></components></skin>';this.xml=null;if(window.DOMParser){parser=new DOMParser();this.xml=parser.parseFromString(this.text,"text/xml")}else{this.xml=new ActiveXObject("Microsoft.XMLDOM");this.xml.async="false";this.xml.loadXML(this.text)}return this}})(jwplayer);(function(a){_utils=a.utils;_css=_utils.css;_hide=function(b){_css(b,{display:"none"})};_show=function(b){_css(b,{display:"block"})};a.html5.display=function(k,K){var j={icons:true,showmute:false};var X=_utils.extend({},j,K);var h=k;var W={};var e;var w;var z;var T;var u;var M;var E;var N=!_utils.exists(h.skin.getComponentSettings("display").bufferrotation)?15:parseInt(h.skin.getComponentSettings("display").bufferrotation,10);var s=!_utils.exists(h.skin.getComponentSettings("display").bufferinterval)?100:parseInt(h.skin.getComponentSettings("display").bufferinterval,10);var D=-1;var v=a.api.events.state.IDLE;var O=true;var d;var C=false,V=true;var p="";var g=false;var o=false;var m;var y,R;var L=new a.html5.eventdispatcher();_utils.extend(this,L);var H={display:{style:{cursor:"pointer",top:0,left:0,overflow:"hidden"},click:n},display_icon:{style:{cursor:"pointer",position:"absolute",top:((h.skin.getSkinElement("display","background").height-h.skin.getSkinElement("display","playIcon").height)/2),left:((h.skin.getSkinElement("display","background").width-h.skin.getSkinElement("display","playIcon").width)/2),border:0,margin:0,padding:0,zIndex:3,display:"none"}},display_iconBackground:{style:{cursor:"pointer",position:"absolute",top:((w-h.skin.getSkinElement("display","background").height)/2),left:((e-h.skin.getSkinElement("display","background").width)/2),border:0,backgroundImage:(["url(",h.skin.getSkinElement("display","background").src,")"]).join(""),width:h.skin.getSkinElement("display","background").width,height:h.skin.getSkinElement("display","background").height,margin:0,padding:0,zIndex:2,display:"none"}},display_image:{style:{display:"none",width:e,height:w,position:"absolute",cursor:"pointer",left:0,top:0,margin:0,padding:0,textDecoration:"none",zIndex:1}},display_text:{style:{zIndex:4,position:"relative",opacity:0.8,backgroundColor:parseInt("000000",16),color:parseInt("ffffff",16),textAlign:"center",fontFamily:"Arial,sans-serif",padding:"0 5px",fontSize:14}}};h.jwAddEventListener(a.api.events.JWPLAYER_PLAYER_STATE,q);h.jwAddEventListener(a.api.events.JWPLAYER_MEDIA_MUTE,q);h.jwAddEventListener(a.api.events.JWPLAYER_PLAYLIST_LOADED,P);h.jwAddEventListener(a.api.events.JWPLAYER_PLAYLIST_ITEM,q);h.jwAddEventListener(a.api.events.JWPLAYER_ERROR,r);Q();function Q(){W.display=G("div","display");W.display_text=G("div","display_text");W.display.appendChild(W.display_text);W.display_image=G("img","display_image");W.display_image.onerror=function(Y){_hide(W.display_image)};W.display_image.onload=B;W.display_icon=G("div","display_icon");W.display_iconBackground=G("div","display_iconBackground");W.display.appendChild(W.display_image);W.display_iconBackground.appendChild(W.display_icon);W.display.appendChild(W.display_iconBackground);f();setTimeout((function(){o=true;if(X.icons.toString()=="true"){J()}}),1)}this.getDisplayElement=function(){return W.display};this.resize=function(Z,Y){if(h.jwGetFullscreen()&&_utils.useNativeFullscreen()){return}_css(W.display,{width:Z,height:Y});_css(W.display_text,{width:(Z-10),top:((Y-_utils.getBoundingClientRect(W.display_text).height)/2)});_css(W.display_iconBackground,{top:((Y-h.skin.getSkinElement("display","background").height)/2),left:((Z-h.skin.getSkinElement("display","background").width)/2)});if(e!=Z||w!=Y){e=Z;w=Y;d=undefined;J()}if(!h.jwGetFullscreen()){y=Z;R=Y}c();q({})};this.show=function(){if(g){g=false;t(h.jwGetState())}};this.hide=function(){if(!g){F();g=true}};function B(Y){z=W.display_image.naturalWidth;T=W.display_image.naturalHeight;c();if(h.jwGetState()==a.api.events.state.IDLE){_css(W.display_image,{display:"block",opacity:0});_utils.fadeTo(W.display_image,1,0.1)}C=false}function c(){if(h.jwGetFullscreen()&&h.jwGetStretching()==a.utils.stretching.EXACTFIT){var Y=document.createElement("div");_utils.stretch(a.utils.stretching.UNIFORM,Y,e,w,y,R);_utils.stretch(a.utils.stretching.EXACTFIT,W.display_image,_utils.parseDimension(Y.style.width),_utils.parseDimension(Y.style.height),z,T);_css(W.display_image,{left:Y.style.left,top:Y.style.top})}else{_utils.stretch(h.jwGetStretching(),W.display_image,e,w,z,T)}}function G(Y,aa){var Z=document.createElement(Y);Z.id=h.id+"_jwplayer_"+aa;_css(Z,H[aa].style);return Z}function f(){for(var Y in W){if(_utils.exists(H[Y].click)){W[Y].onclick=H[Y].click}}}function n(Y){if(typeof Y.preventDefault!="undefined"){Y.preventDefault()}else{Y.returnValue=false}if(typeof m=="function"){m(Y);return}else{if(h.jwGetState()!=a.api.events.state.PLAYING){h.jwPlay()}else{h.jwPause()}}}function U(Y){if(E){F();return}W.display_icon.style.backgroundImage=(["url(",h.skin.getSkinElement("display",Y).src,")"]).join("");_css(W.display_icon,{width:h.skin.getSkinElement("display",Y).width,height:h.skin.getSkinElement("display",Y).height,top:(h.skin.getSkinElement("display","background").height-h.skin.getSkinElement("display",Y).height)/2,left:(h.skin.getSkinElement("display","background").width-h.skin.getSkinElement("display",Y).width)/2});b();if(_utils.exists(h.skin.getSkinElement("display",Y+"Over"))){W.display_icon.onmouseover=function(Z){W.display_icon.style.backgroundImage=["url(",h.skin.getSkinElement("display",Y+"Over").src,")"].join("")};W.display_icon.onmouseout=function(Z){W.display_icon.style.backgroundImage=["url(",h.skin.getSkinElement("display",Y).src,")"].join("")}}else{W.display_icon.onmouseover=null;W.display_icon.onmouseout=null}}function F(){if(X.icons.toString()=="true"){_hide(W.display_icon);_hide(W.display_iconBackground);S()}}function b(){if(!g&&X.icons.toString()=="true"){_show(W.display_icon);_show(W.display_iconBackground);J()}}function r(Y){E=true;F();W.display_text.innerHTML=Y.message;_show(W.display_text);W.display_text.style.top=((w-_utils.getBoundingClientRect(W.display_text).height)/2)+"px"}function I(){V=false;W.display_image.style.display="none"}function P(){v=""}function q(Y){if((Y.type==a.api.events.JWPLAYER_PLAYER_STATE||Y.type==a.api.events.JWPLAYER_PLAYLIST_ITEM)&&E){E=false;_hide(W.display_text)}var Z=h.jwGetState();if(Z==v){return}v=Z;if(D>=0){clearTimeout(D)}if(O||h.jwGetState()==a.api.events.state.PLAYING||h.jwGetState()==a.api.events.state.PAUSED){t(h.jwGetState())}else{D=setTimeout(l(h.jwGetState()),500)}}function l(Y){return(function(){t(Y)})}function t(Y){if(_utils.exists(M)){clearInterval(M);M=null;_utils.animations.rotate(W.display_icon,0)}switch(Y){case a.api.events.state.BUFFERING:if(_utils.isIPod()){I();F()}else{if(h.jwGetPlaylist()[h.jwGetPlaylistIndex()].provider=="sound"){x()}u=0;M=setInterval(function(){u+=N;_utils.animations.rotate(W.display_icon,u%360)},s);U("bufferIcon");O=true}break;case a.api.events.state.PAUSED:if(!_utils.isIPod()){if(h.jwGetPlaylist()[h.jwGetPlaylistIndex()].provider!="sound"){_css(W.display_image,{background:"transparent no-repeat center center"})}U("playIcon");O=true}break;case a.api.events.state.IDLE:if(h.jwGetPlaylist()[h.jwGetPlaylistIndex()]&&h.jwGetPlaylist()[h.jwGetPlaylistIndex()].image){x()}else{I()}U("playIcon");O=true;break;default:if(h.jwGetPlaylist()[h.jwGetPlaylistIndex()]&&h.jwGetPlaylist()[h.jwGetPlaylistIndex()].provider=="sound"){if(_utils.isIPod()){I();O=false}else{x()}}else{I();O=false}if(h.jwGetMute()&&X.showmute){U("muteIcon")}else{F()}break}D=-1}function x(){if(h.jwGetPlaylist()[h.jwGetPlaylistIndex()]){var Y=h.jwGetPlaylist()[h.jwGetPlaylistIndex()].image;if(Y){if(Y!=p){p=Y;W.display_image.style.display="none";C=true;W.display_image.src=_utils.getAbsolutePath(Y)}else{if(!(C||V)){V=true;W.display_image.style.opacity=0;W.display_image.style.display="block";_utils.fadeTo(W.display_image,1,0.1)}}}}}function A(Y){return function(){if(!o){return}if(!g&&d!=Y){d=Y;L.sendEvent(Y,{component:"display",boundingRect:_utils.getDimensions(W.display_iconBackground)})}}}var J=A(a.api.events.JWPLAYER_COMPONENT_SHOW);var S=A(a.api.events.JWPLAYER_COMPONENT_HIDE);this.setAlternateClickHandler=function(Y){m=Y};this.revertAlternateClickHandler=function(){m=undefined};return this}})(jwplayer);(function(a){var c=a.utils;var b=c.css;a.html5.dock=function(w,D){function x(){return{align:a.html5.view.positions.RIGHT}}var n=c.extend({},x(),D);if(n.align=="FALSE"){return}var j={};var A=[];var k;var F;var f=false;var C=false;var g={x:0,y:0,width:0,height:0};var z;var o;var y;var m=new a.html5.eventdispatcher();c.extend(this,m);var r=document.createElement("div");r.id=w.id+"_jwplayer_dock";r.style.opacity=1;p();w.jwAddEventListener(a.api.events.JWPLAYER_PLAYER_STATE,q);this.getDisplayElement=function(){return r};this.setButton=function(K,H,I,J){if(!H&&j[K]){c.arrays.remove(A,K);r.removeChild(j[K].div);delete j[K]}else{if(H){if(!j[K]){j[K]={}}j[K].handler=H;j[K].outGraphic=I;j[K].overGraphic=J;if(!j[K].div){A.push(K);j[K].div=document.createElement("div");j[K].div.style.position="absolute";r.appendChild(j[K].div);j[K].div.appendChild(document.createElement("div"));j[K].div.childNodes[0].style.position="relative";j[K].div.childNodes[0].style.width="100%";j[K].div.childNodes[0].style.height="100%";j[K].div.childNodes[0].style.zIndex=10;j[K].div.childNodes[0].style.cursor="pointer";j[K].div.appendChild(document.createElement("img"));j[K].div.childNodes[1].style.position="absolute";j[K].div.childNodes[1].style.left=0;j[K].div.childNodes[1].style.top=0;if(w.skin.getSkinElement("dock","button")){j[K].div.childNodes[1].src=w.skin.getSkinElement("dock","button").src}j[K].div.childNodes[1].style.zIndex=9;j[K].div.childNodes[1].style.cursor="pointer";j[K].div.onmouseover=function(){if(j[K].overGraphic){j[K].div.childNodes[0].style.background=h(j[K].overGraphic)}if(w.skin.getSkinElement("dock","buttonOver")){j[K].div.childNodes[1].src=w.skin.getSkinElement("dock","buttonOver").src}};j[K].div.onmouseout=function(){if(j[K].outGraphic){j[K].div.childNodes[0].style.background=h(j[K].outGraphic)}if(w.skin.getSkinElement("dock","button")){j[K].div.childNodes[1].src=w.skin.getSkinElement("dock","button").src}};if(w.skin.getSkinElement("dock","button")){j[K].div.childNodes[1].src=w.skin.getSkinElement("dock","button").src}}if(j[K].outGraphic){j[K].div.childNodes[0].style.background=h(j[K].outGraphic)}else{if(j[K].overGraphic){j[K].div.childNodes[0].style.background=h(j[K].overGraphic)}}if(H){j[K].div.onclick=function(L){L.preventDefault();a(w.id).callback(K);if(j[K].overGraphic){j[K].div.childNodes[0].style.background=h(j[K].overGraphic)}if(w.skin.getSkinElement("dock","button")){j[K].div.childNodes[1].src=w.skin.getSkinElement("dock","button").src}}}}}l(k,F)};function h(H){return"url("+H+") no-repeat center center"}function t(H){}function l(H,T){p();if(A.length>0){var I=10;var S=I;var P=-1;var Q=w.skin.getSkinElement("dock","button").height;var O=w.skin.getSkinElement("dock","button").width;var M=H-O-I;var R,L;if(n.align==a.html5.view.positions.LEFT){P=1;M=I}for(var J=0;J<A.length;J++){var U=Math.floor(S/T);if((S+Q+I)>((U+1)*T)){S=((U+1)*T)+I;U=Math.floor(S/T)}var K=j[A[J]].div;K.style.top=(S%T)+"px";K.style.left=(M+(w.skin.getSkinElement("dock","button").width+I)*U*P)+"px";var N={x:c.parseDimension(K.style.left),y:c.parseDimension(K.style.top),width:O,height:Q};if(!R||(N.x<=R.x&&N.y<=R.y)){R=N}if(!L||(N.x>=L.x&&N.y>=L.y)){L=N}K.style.width=O+"px";K.style.height=Q+"px";S+=w.skin.getSkinElement("dock","button").height+I}g={x:R.x,y:R.y,width:L.x-R.x+L.width,height:R.y-L.y+L.height}}if(C!=w.jwGetFullscreen()||k!=H||F!=T){k=H;F=T;C=w.jwGetFullscreen();z=undefined;setTimeout(s,1)}}function d(H){return function(){if(!f&&z!=H&&A.length>0){z=H;m.sendEvent(H,{component:"dock",boundingRect:g})}}}function q(H){if(c.isMobile()){if(H.newstate==a.api.events.state.IDLE){v()}else{e()}}else{B()}}function B(H){if(f){return}clearTimeout(y);if(D.position==a.html5.view.positions.OVER||w.jwGetFullscreen()){switch(w.jwGetState()){case a.api.events.state.PAUSED:case a.api.events.state.IDLE:if(r&&r.style.opacity<1&&(!D.idlehide||c.exists(H))){E()}if(D.idlehide){y=setTimeout(function(){u()},2000)}break;default:if(c.exists(H)){E()}y=setTimeout(function(){u()},2000);break}}else{E()}}var s=d(a.api.events.JWPLAYER_COMPONENT_SHOW);var G=d(a.api.events.JWPLAYER_COMPONENT_HIDE);this.resize=l;var v=function(){b(r,{display:"block"});if(f){f=false;s()}};var e=function(){b(r,{display:"none"});if(!f){G();f=true}};function u(){if(!f){G();if(r.style.opacity==1){c.cancelAnimation(r);c.fadeTo(r,0,0.1,1,0)}}}function E(){if(!f){s();if(r.style.opacity==0){c.cancelAnimation(r);c.fadeTo(r,1,0.1,0,0)}}}function p(){try{o=document.getElementById(w.id);o.addEventListener("mousemove",B)}catch(H){c.log("Could not add mouse listeners to dock: "+H)}}this.hide=e;this.show=v;return this}})(jwplayer);(function(a){a.html5.eventdispatcher=function(d,b){var c=new a.events.eventdispatcher(b);a.utils.extend(this,c);this.sendEvent=function(e,f){if(!a.utils.exists(f)){f={}}a.utils.extend(f,{id:d,version:a.version,type:e});c.sendEvent(e,f)}}})(jwplayer);(function(a){var b=a.utils;a.html5.instream=function(y,m,x,z){var t={controlbarseekable:"always",controlbarpausable:true,controlbarstoppable:true,playlistclickable:true};var v,A,C=y,E=m,j=x,w=z,r,H,o,G,e,f,g,l,q,h=false,k,d,n=this;this.load=function(M,K){c();h=true;A=b.extend(t,K);v=a.html5.playlistitem(M);F();d=document.createElement("div");d.id=n.id+"_instream_container";w.detachMedia();r=g.getDisplayElement();f=E.playlist[E.item];e=C.jwGetState();if(e==a.api.events.state.BUFFERING||e==a.api.events.state.PLAYING){r.pause()}H=r.src?r.src:r.currentSrc;o=r.innerHTML;G=r.currentTime;q=new a.html5.display(n,b.extend({},E.plugins.config.display));q.setAlternateClickHandler(function(N){if(_fakemodel.state==a.api.events.state.PAUSED){n.jwInstreamPlay()}else{D(a.api.events.JWPLAYER_INSTREAM_CLICK,N)}});d.appendChild(q.getDisplayElement());if(!b.isMobile()){l=new a.html5.controlbar(n,b.extend({},E.plugins.config.controlbar,{}));if(E.plugins.config.controlbar.position==a.html5.view.positions.OVER){d.appendChild(l.getDisplayElement())}else{var L=E.plugins.object.controlbar.getDisplayElement().parentNode;L.appendChild(l.getDisplayElement())}}j.setupInstream(d,r);p();g.load(v)};this.jwInstreamDestroy=function(K){if(!h){return}h=false;if(e!=a.api.events.state.IDLE){g.load(f,false);g.stop(false)}else{g.stop(true)}g.detachMedia();j.destroyInstream();if(l){try{l.getDisplayElement().parentNode.removeChild(l.getDisplayElement())}catch(L){}}D(a.api.events.JWPLAYER_INSTREAM_DESTROYED,{reason:(K?"complete":"destroyed")},true);w.attachMedia();if(e==a.api.events.state.BUFFERING||e==a.api.events.state.PLAYING){r.play();if(E.playlist[E.item]==f){E.getMedia().seek(G)}}return};this.jwInstreamAddEventListener=function(K,L){k.addEventListener(K,L)};this.jwInstreamRemoveEventListener=function(K,L){k.removeEventListener(K,L)};this.jwInstreamPlay=function(){if(!h){return}g.play(true)};this.jwInstreamPause=function(){if(!h){return}g.pause(true)};this.jwInstreamSeek=function(K){if(!h){return}g.seek(K)};this.jwInstreamGetState=function(){if(!h){return undefined}return _fakemodel.state};this.jwInstreamGetPosition=function(){if(!h){return undefined}return _fakemodel.position};this.jwInstreamGetDuration=function(){if(!h){return undefined}return _fakemodel.duration};this.playlistClickable=function(){return(!h||A.playlistclickable.toString().toLowerCase()=="true")};function s(){_fakemodel=new a.html5.model(this,E.getMedia()?E.getMedia().getDisplayElement():E.container,E);k=new a.html5.eventdispatcher();C.jwAddEventListener(a.api.events.JWPLAYER_RESIZE,p);C.jwAddEventListener(a.api.events.JWPLAYER_FULLSCREEN,p)}function c(){_fakemodel.setMute(E.mute);_fakemodel.setVolume(E.volume)}function F(){if(!g){g=new a.html5.mediavideo(_fakemodel,E.getMedia()?E.getMedia().getDisplayElement():E.container);g.addGlobalListener(I);g.addEventListener(a.api.events.JWPLAYER_MEDIA_META,J);g.addEventListener(a.api.events.JWPLAYER_MEDIA_COMPLETE,u);g.addEventListener(a.api.events.JWPLAYER_MEDIA_BUFFER_FULL,B)}g.attachMedia()}function I(K){if(h){D(K.type,K)}}function B(K){if(h){g.play()}}function u(K){if(h){setTimeout(function(){n.jwInstreamDestroy(true)},10)}}function J(K){if(K.metadata.width&&K.metadata.height){j.resizeMedia()}}function D(K,L,M){if(h||M){k.sendEvent(K,L)}}function p(){var K=E.plugins.object.display.getDisplayElement().style;if(l){var L=E.plugins.object.controlbar.getDisplayElement().style;l.resize(b.parseDimension(K.width),b.parseDimension(L.height));_css(l.getDisplayElement(),b.extend({},L,{zIndex:1001,opacity:1}))}if(q){q.resize(b.parseDimension(K.width),b.parseDimension(K.height));_css(q.getDisplayElement(),b.extend({},K,{zIndex:1000}))}if(j){j.resizeMedia()}}this.jwPlay=function(K){if(A.controlbarpausable.toString().toLowerCase()=="true"){this.jwInstreamPlay()}};this.jwPause=function(K){if(A.controlbarpausable.toString().toLowerCase()=="true"){this.jwInstreamPause()}};this.jwStop=function(){if(A.controlbarstoppable.toString().toLowerCase()=="true"){this.jwInstreamDestroy();C.jwStop()}};this.jwSeek=function(K){switch(A.controlbarseekable.toLowerCase()){case"always":this.jwInstreamSeek(K);break;case"backwards":if(_fakemodel.position>K){this.jwInstreamSeek(K)}break}};this.jwGetPosition=function(){};this.jwGetDuration=function(){};this.jwGetWidth=C.jwGetWidth;this.jwGetHeight=C.jwGetHeight;this.jwGetFullscreen=C.jwGetFullscreen;this.jwSetFullscreen=C.jwSetFullscreen;this.jwGetVolume=function(){return E.volume};this.jwSetVolume=function(K){g.volume(K);C.jwSetVolume(K)};this.jwGetMute=function(){return E.mute};this.jwSetMute=function(K){g.mute(K);C.jwSetMute(K)};this.jwGetState=function(){return _fakemodel.state};this.jwGetPlaylist=function(){return[v]};this.jwGetPlaylistIndex=function(){return 0};this.jwGetStretching=function(){return E.config.stretching};this.jwAddEventListener=function(L,K){k.addEventListener(L,K)};this.jwRemoveEventListener=function(L,K){k.removeEventListener(L,K)};this.skin=C.skin;this.id=C.id+"_instream";s();return this}})(jwplayer);(function(a){var b={prefix:"http://l.longtailvideo.com/html5/",file:"logo.png",link:"http://www.longtailvideo.com/players/jw-flv-player/",linktarget:"_top",margin:8,out:0.5,over:1,timeout:5,hide:true,position:"bottom-left"};_css=a.utils.css;a.html5.logo=function(n,r){var q=n;var u;var d;var t;var h=false;g();function g(){o();q.jwAddEventListener(a.api.events.JWPLAYER_PLAYER_STATE,j);c();l()}function o(){if(b.prefix){var v=n.version.split(/\W/).splice(0,2).join("/");if(b.prefix.indexOf(v)<0){b.prefix+=v+"/"}}if(r.position==a.html5.view.positions.OVER){r.position=b.position}try{if(window.location.href.indexOf("https")==0){b.prefix=b.prefix.replace("http://l.longtailvideo.com","https://securel.longtailvideo.com")}}catch(w){}d=a.utils.extend({},b)}function c(){t=document.createElement("img");t.id=q.id+"_jwplayer_logo";t.style.display="none";t.onload=function(v){_css(t,k());p()};if(!d.file){return}if(d.file.indexOf("/")>=0){t.src=d.file}else{t.src=d.prefix+d.file}}if(!d.file){return}this.resize=function(w,v){};this.getDisplayElement=function(){return t};function l(){if(d.link){t.onmouseover=f;t.onmouseout=p;t.onclick=s}else{this.mouseEnabled=false}}function s(v){if(typeof v!="undefined"){v.stopPropagation()}if(!h){return}q.jwPause();q.jwSetFullscreen(false);if(d.link){window.open(d.link,d.linktarget)}return}function p(v){if(d.link&&h){t.style.opacity=d.out}return}function f(v){if(h){t.style.opacity=d.over}return}function k(){var x={textDecoration:"none",position:"absolute",cursor:"pointer"};x.display=(d.hide.toString()=="true"&&!h)?"none":"block";var w=d.position.toLowerCase().split("-");for(var v in w){x[w[v]]=parseInt(d.margin)}return x}function m(){if(d.hide.toString()=="true"){t.style.display="block";t.style.opacity=0;a.utils.fadeTo(t,d.out,0.1,parseFloat(t.style.opacity));u=setTimeout(function(){e()},d.timeout*1000)}h=true}function e(){h=false;if(d.hide.toString()=="true"){a.utils.fadeTo(t,0,0.1,parseFloat(t.style.opacity))}}function j(v){if(v.newstate==a.api.events.state.BUFFERING){clearTimeout(u);m()}}return this}})(jwplayer);(function(b){var d={ended:b.api.events.state.IDLE,playing:b.api.events.state.PLAYING,pause:b.api.events.state.PAUSED,buffering:b.api.events.state.BUFFERING};var e=b.utils;var a=e.isMobile();var c={};b.html5.mediavideo=function(h,F){var J={abort:y,canplay:p,canplaythrough:p,durationchange:u,emptied:y,ended:p,error:o,loadeddata:u,loadedmetadata:u,loadstart:p,pause:p,play:y,playing:p,progress:D,ratechange:y,seeked:p,seeking:p,stalled:p,suspend:p,timeupdate:N,volumechange:l,waiting:p,canshowcurrentframe:y,dataunavailable:y,empty:y,load:g,loadedfirstframe:y,webkitfullscreenchange:k};var K=new b.html5.eventdispatcher();e.extend(this,K);var j=h,B=F,m,f,C,T,E,M,L=false,t=false,x=false,I,G,Q;R();this.load=function(V,W){if(typeof W=="undefined"){W=true}if(!t){return}T=V;x=(T.duration>0);j.duration=T.duration;e.empty(m);Q=0;q(V.levels);if(V.levels&&V.levels.length>0){if(V.levels.length==1||e.isIOS()){m.src=V.levels[0].file}else{if(m.src){m.removeAttribute("src")}for(var U=0;U<V.levels.length;U++){var X=m.ownerDocument.createElement("source");X.src=V.levels[U].file;m.appendChild(X);Q++}}}else{m.src=V.file}m.style.display="block";m.style.opacity=1;m.volume=j.volume/100;m.muted=j.mute;if(a){P()}I=G=C=false;j.buffer=0;if(!e.exists(V.start)){V.start=0}M=(V.start>0)?V.start:-1;s(b.api.events.JWPLAYER_MEDIA_LOADED);if((!a&&V.levels.length==1)||!L){m.load()}L=false;if(W){w(b.api.events.state.BUFFERING);s(b.api.events.JWPLAYER_MEDIA_BUFFER,{bufferPercent:0});A()}if(m.videoWidth>0&&m.videoHeight>0){u()}};this.play=function(){if(!t){return}A();if(G){w(b.api.events.state.PLAYING)}else{w(b.api.events.state.BUFFERING)}m.play()};this.pause=function(){if(!t){return}m.pause();w(b.api.events.state.PAUSED)};this.seek=function(U){if(!t){return}if(!C&&m.readyState>0){if(!(j.duration<=0||isNaN(j.duration))&&!(j.position<=0||isNaN(j.position))){m.currentTime=U;m.play()}}else{M=U}};var z=this.stop=function(U){if(!t){return}if(!e.exists(U)){U=true}r();if(U){G=false;var V=navigator.userAgent;if(m.webkitSupportsFullscreen){try{m.webkitExitFullscreen()}catch(W){}}m.style.opacity=0;v();if(e.isIE()){m.src=""}else{m.removeAttribute("src")}e.empty(m);m.load();L=true}w(b.api.events.state.IDLE)};this.fullscreen=function(U){if(U===true){this.resize("100%","100%")}else{this.resize(j.config.width,j.config.height)}};this.resize=function(V,U){};this.volume=function(U){if(!a){m.volume=U/100;s(b.api.events.JWPLAYER_MEDIA_VOLUME,{volume:(U/100)})}};this.mute=function(U){if(!a){m.muted=U;s(b.api.events.JWPLAYER_MEDIA_MUTE,{mute:U})}};this.getDisplayElement=function(){return m};this.hasChrome=function(){return a&&(f==b.api.events.state.PLAYING)};this.detachMedia=function(){t=false;return this.getDisplayElement()};this.attachMedia=function(){t=true};function H(V,U){return function(W){if(e.exists(W.target.parentNode)){U(W)}}}function R(){f=b.api.events.state.IDLE;t=true;m=n();m.setAttribute("x-webkit-airplay","allow");if(B.parentNode){B.parentNode.replaceChild(m,B)}}function n(){var U;if(!c[j.id]){if(B.tagName.toLowerCase()=="video"){U=B}else{U=document.createElement("video")}c[j.id]=U;if(!U.id){U.id=B.id}for(var V in J){U.addEventListener(V,H(V,J[V]),true)}}return c[j.id]}function w(U){if(U==b.api.events.state.PAUSED&&f==b.api.events.state.IDLE){return}if(a){switch(U){case b.api.events.state.PLAYING:P();break;case b.api.events.state.BUFFERING:case b.api.events.state.PAUSED:v();break}}if(f!=U){var V=f;j.state=f=U;s(b.api.events.JWPLAYER_PLAYER_STATE,{oldstate:V,newstate:U})}}function y(U){}function l(U){var V=Math.round(m.volume*100);s(b.api.events.JWPLAYER_MEDIA_VOLUME,{volume:V},true);s(b.api.events.JWPLAYER_MEDIA_MUTE,{mute:m.muted},true)}function D(W){if(!t){return}var V;if(e.exists(W)&&W.lengthComputable&&W.total){V=W.loaded/W.total*100}else{if(e.exists(m.buffered)&&(m.buffered.length>0)){var U=m.buffered.length-1;if(U>=0){V=m.buffered.end(U)/m.duration*100}}}if(e.useNativeFullscreen()&&e.exists(m.webkitDisplayingFullscreen)){if(j.fullscreen!=m.webkitDisplayingFullscreen){s(b.api.events.JWPLAYER_FULLSCREEN,{fullscreen:m.webkitDisplayingFullscreen},true)}}if(G===false&&f==b.api.events.state.BUFFERING){s(b.api.events.JWPLAYER_MEDIA_BUFFER_FULL);G=true}if(!I){if(V==100){I=true}if(e.exists(V)&&(V>j.buffer)){j.buffer=Math.round(V);s(b.api.events.JWPLAYER_MEDIA_BUFFER,{bufferPercent:Math.round(V)})}}}function N(V){if(!t){return}if(e.exists(V)&&e.exists(V.target)){if(x>0){if(!isNaN(V.target.duration)&&(isNaN(j.duration)||j.duration<1)){if(V.target.duration==Infinity){j.duration=0}else{j.duration=Math.round(V.target.duration*10)/10}}}if(!C&&m.readyState>0){w(b.api.events.state.PLAYING)}if(f==b.api.events.state.PLAYING){if(m.readyState>0&&(M>-1||!C)){C=true;try{if(m.currentTime!=M&&M>-1){m.currentTime=M;M=-1}}catch(U){}m.volume=j.volume/100;m.muted=j.mute}j.position=j.duration>0?(Math.round(V.target.currentTime*10)/10):0;s(b.api.events.JWPLAYER_MEDIA_TIME,{position:j.position,duration:j.duration});if(j.position>=j.duration&&(j.position>0||j.duration>0)){O();return}}}D(V)}function g(U){}function p(U){if(!t){return}if(d[U.type]){if(U.type=="ended"){O()}else{w(d[U.type])}}}function u(V){if(!t){return}var U=Math.round(m.duration*10)/10;var W={height:m.videoHeight,width:m.videoWidth,duration:U};if(!x){if((j.duration<U||isNaN(j.duration))&&m.duration!=Infinity){j.duration=U}}s(b.api.events.JWPLAYER_MEDIA_META,{metadata:W})}function o(W){if(!t){return}if(f==b.api.events.state.IDLE){return}var V="There was an error: ";if((W.target.error&&W.target.tagName.toLowerCase()=="video")||W.target.parentNode.error&&W.target.parentNode.tagName.toLowerCase()=="video"){var U=!e.exists(W.target.error)?W.target.parentNode.error:W.target.error;switch(U.code){case U.MEDIA_ERR_ABORTED:e.log("User aborted the video playback.");return;case U.MEDIA_ERR_NETWORK:V="A network error caused the video download to fail part-way: ";break;case U.MEDIA_ERR_DECODE:V="The video playback was aborted due to a corruption problem or because the video used features your browser did not support: ";break;case U.MEDIA_ERR_SRC_NOT_SUPPORTED:V="The video could not be loaded, either because the server or network failed or because the format is not supported: ";break;default:V="An unknown error occurred: ";break}}else{if(W.target.tagName.toLowerCase()=="source"){Q--;if(Q>0){return}if(e.userAgentMatch(/firefox/i)){e.log("The video could not be loaded, either because the server or network failed or because the format is not supported.");z(false);return}else{V="The video could not be loaded, either because the server or network failed or because the format is not supported: "}}else{e.log("An unknown error occurred. Continuing...");return}}z(false);V+=S();_error=true;s(b.api.events.JWPLAYER_ERROR,{message:V});return}function S(){var W="";for(var V in T.levels){var U=T.levels[V];var X=B.ownerDocument.createElement("source");W+=b.utils.getAbsolutePath(U.file);if(V<(T.levels.length-1)){W+=", "}}return W}function A(){if(!e.exists(E)){E=setInterval(function(){D()},100)}}function r(){clearInterval(E);E=null}function O(){if(f==b.api.events.state.PLAYING){z(false);s(b.api.events.JWPLAYER_MEDIA_BEFORECOMPLETE);s(b.api.events.JWPLAYER_MEDIA_COMPLETE)}}function k(U){if(e.exists(m.webkitDisplayingFullscreen)){if(j.fullscreen&&!m.webkitDisplayingFullscreen){s(b.api.events.JWPLAYER_FULLSCREEN,{fullscreen:false},true)}}}function q(W){if(W.length>0&&e.userAgentMatch(/Safari/i)){var U=-1;for(var V=0;V<W.length;V++){switch(e.extension(W[V].file)){case"mp4":if(U<0){U=V}break;case"webm":W.splice(V,1);break}}if(U>0){var X=W.splice(U,1)[0];W.unshift(X)}}}function P(){setTimeout(function(){m.setAttribute("controls","controls")},100)}function v(){setTimeout(function(){m.removeAttribute("controls")},250)}function s(U,W,V){if(t||V){if(W){K.sendEvent(U,W)}else{K.sendEvent(U)}}}}})(jwplayer);(function(a){var c={ended:a.api.events.state.IDLE,playing:a.api.events.state.PLAYING,pause:a.api.events.state.PAUSED,buffering:a.api.events.state.BUFFERING};var b=a.utils.css;a.html5.mediayoutube=function(j,e){var f=new a.html5.eventdispatcher();a.utils.extend(this,f);var l=j;var h=document.getElementById(e.id);var g=a.api.events.state.IDLE;var n,m;function k(p){if(g!=p){var q=g;l.state=p;g=p;f.sendEvent(a.api.events.JWPLAYER_PLAYER_STATE,{oldstate:q,newstate:p})}}this.getDisplayElement=this.detachMedia=function(){return h};this.attachMedia=function(){};this.play=function(){if(g==a.api.events.state.IDLE){f.sendEvent(a.api.events.JWPLAYER_MEDIA_BUFFER,{bufferPercent:100});f.sendEvent(a.api.events.JWPLAYER_MEDIA_BUFFER_FULL);k(a.api.events.state.PLAYING)}else{if(g==a.api.events.state.PAUSED){k(a.api.events.state.PLAYING)}}};this.pause=function(){k(a.api.events.state.PAUSED)};this.seek=function(p){};this.stop=function(p){if(!_utils.exists(p)){p=true}l.position=0;k(a.api.events.state.IDLE);if(p){b(h,{display:"none"})}};this.volume=function(p){l.setVolume(p);f.sendEvent(a.api.events.JWPLAYER_MEDIA_VOLUME,{volume:Math.round(p)})};this.mute=function(p){h.muted=p;f.sendEvent(a.api.events.JWPLAYER_MEDIA_MUTE,{mute:p})};this.resize=function(q,p){if(q*p>0&&n){n.width=m.width=q;n.height=m.height=p}};this.fullscreen=function(p){if(p===true){this.resize("100%","100%")}else{this.resize(l.config.width,l.config.height)}};this.load=function(p){o(p);b(n,{display:"block"});k(a.api.events.state.BUFFERING);f.sendEvent(a.api.events.JWPLAYER_MEDIA_BUFFER,{bufferPercent:0});f.sendEvent(a.api.events.JWPLAYER_MEDIA_LOADED);this.play()};this.hasChrome=function(){return(g!=a.api.events.state.IDLE)};function o(v){var s=v.levels[0].file;s=["http://www.youtube.com/v/",d(s),"&hl=en_US&fs=1&autoplay=1"].join("");n=document.createElement("object");n.id=h.id;n.style.position="absolute";var u={movie:s,allowfullscreen:"true",allowscriptaccess:"always"};for(var p in u){var t=document.createElement("param");t.name=p;t.value=u[p];n.appendChild(t)}m=document.createElement("embed");n.appendChild(m);var q={src:s,type:"application/x-shockwave-flash",allowfullscreen:"true",allowscriptaccess:"always",width:n.width,height:n.height};for(var r in q){m.setAttribute(r,q[r])}n.appendChild(m);n.style.zIndex=2147483000;if(h!=n&&h.parentNode){h.parentNode.replaceChild(n,h)}h=n}function d(q){var p=q.split(/\?|\#\!/);var s="";for(var r=0;r<p.length;r++){if(p[r].substr(0,2)=="v="){s=p[r].substr(2)}}if(s==""){if(q.indexOf("/v/")>=0){s=q.substr(q.indexOf("/v/")+3)}else{if(q.indexOf("youtu.be")>=0){s=q.substr(q.indexOf("youtu.be/")+9)}else{s=q}}}if(s.indexOf("?")>-1){s=s.substr(0,s.indexOf("?"))}if(s.indexOf("&")>-1){s=s.substr(0,s.indexOf("&"))}return s}this.embed=m;return this}})(jwplayer);(function(jwplayer){var _configurableStateVariables=["width","height","start","duration","volume","mute","fullscreen","item","plugins","stretching"];var _utils=jwplayer.utils;jwplayer.html5.model=function(api,container,options){var _api=api;var _container=container;var _cookies=_utils.getCookies();var _model={id:_container.id,playlist:[],state:jwplayer.api.events.state.IDLE,position:0,buffer:0,container:_container,config:{width:480,height:320,item:-1,skin:undefined,file:undefined,image:undefined,start:0,duration:0,bufferlength:5,volume:_cookies.volume?_cookies.volume:90,mute:_cookies.mute&&_cookies.mute.toString().toLowerCase()=="true"?true:false,fullscreen:false,repeat:"",stretching:jwplayer.utils.stretching.UNIFORM,autostart:false,debug:undefined,screencolor:undefined}};var _media;var _eventDispatcher=new jwplayer.html5.eventdispatcher();var _components=["display","logo","controlbar","playlist","dock"];jwplayer.utils.extend(_model,_eventDispatcher);for(var option in options){if(typeof options[option]=="string"){var type=/color$/.test(option)?"color":null;options[option]=jwplayer.utils.typechecker(options[option],type)}var config=_model.config;var path=option.split(".");for(var edge in path){if(edge==path.length-1){config[path[edge]]=options[option]}else{if(!jwplayer.utils.exists(config[path[edge]])){config[path[edge]]={}}config=config[path[edge]]}}}for(var index in _configurableStateVariables){var configurableStateVariable=_configurableStateVariables[index];_model[configurableStateVariable]=_model.config[configurableStateVariable]}var pluginorder=_components.concat([]);if(jwplayer.utils.exists(_model.plugins)){if(typeof _model.plugins=="string"){var userplugins=_model.plugins.split(",");for(var userplugin in userplugins){if(typeof userplugins[userplugin]=="string"){pluginorder.push(userplugins[userplugin].replace(/^\s+|\s+$/g,""))}}}}if(jwplayer.utils.isMobile()){pluginorder=["display","logo","dock","playlist"];if(!jwplayer.utils.exists(_model.config.repeat)){_model.config.repeat="list"}}else{if(_model.config.chromeless){pluginorder=["logo","dock","playlist"];if(!jwplayer.utils.exists(_model.config.repeat)){_model.config.repeat="list"}}}_model.plugins={order:pluginorder,config:{},object:{}};if(typeof _model.config.components!="undefined"){for(var component in _model.config.components){_model.plugins.config[component]=_model.config.components[component]}}var playlistVisible=false;for(var pluginIndex in _model.plugins.order){var pluginName=_model.plugins.order[pluginIndex];var pluginConfig=!jwplayer.utils.exists(_model.plugins.config[pluginName])?{}:_model.plugins.config[pluginName];_model.plugins.config[pluginName]=!jwplayer.utils.exists(_model.plugins.config[pluginName])?pluginConfig:jwplayer.utils.extend(_model.plugins.config[pluginName],pluginConfig);if(!jwplayer.utils.exists(_model.plugins.config[pluginName].position)){if(pluginName=="playlist"){_model.plugins.config[pluginName].position=jwplayer.html5.view.positions.NONE}else{_model.plugins.config[pluginName].position=jwplayer.html5.view.positions.OVER}}else{if(pluginName=="playlist"){playlistVisible=true}_model.plugins.config[pluginName].position=_model.plugins.config[pluginName].position.toString().toUpperCase()}}if(_model.plugins.config.controlbar&&playlistVisible){_model.plugins.config.controlbar.hideplaylistcontrols=true}if(typeof _model.plugins.config.dock!="undefined"){if(typeof _model.plugins.config.dock!="object"){var position=_model.plugins.config.dock.toString().toUpperCase();_model.plugins.config.dock={position:position}}if(typeof _model.plugins.config.dock.position!="undefined"){_model.plugins.config.dock.align=_model.plugins.config.dock.position;_model.plugins.config.dock.position=jwplayer.html5.view.positions.OVER}if(typeof _model.plugins.config.dock.idlehide=="undefined"){try{_model.plugins.config.dock.idlehide=_model.plugins.config.controlbar.idlehide}catch(e){}}}function _loadExternal(playlistfile){var loader=new jwplayer.html5.playlistloader();loader.addEventListener(jwplayer.api.events.JWPLAYER_PLAYLIST_LOADED,function(evt){_model.playlist=new jwplayer.html5.playlist(evt);_loadComplete(true)});loader.addEventListener(jwplayer.api.events.JWPLAYER_ERROR,function(evt){_model.playlist=new jwplayer.html5.playlist({playlist:[]});_loadComplete(false)});loader.load(playlistfile)}function _loadComplete(){if(_model.config.shuffle){_model.item=_getShuffleItem()}else{if(_model.config.item>=_model.playlist.length){_model.config.item=_model.playlist.length-1}else{if(_model.config.item<0){_model.config.item=0}}_model.item=_model.config.item}_model.position=0;_model.duration=_model.playlist.length>0?_model.playlist[_model.item].duration:0;_eventDispatcher.sendEvent(jwplayer.api.events.JWPLAYER_PLAYLIST_LOADED,{playlist:_model.playlist});_eventDispatcher.sendEvent(jwplayer.api.events.JWPLAYER_PLAYLIST_ITEM,{index:_model.item})}_model.loadPlaylist=function(arg){var input;if(typeof arg=="string"){if(arg.indexOf("[")==0||arg.indexOf("{")=="0"){try{input=eval(arg)}catch(err){input=arg}}else{input=arg}}else{input=arg}var config;switch(jwplayer.utils.typeOf(input)){case"object":config=input;break;case"array":config={playlist:input};break;default:config={file:input};break}_model.playlist=new jwplayer.html5.playlist(config);_model.item=_model.config.item>=0?_model.config.item:0;if(!_model.playlist[0].provider&&_model.playlist[0].file){_loadExternal(_model.playlist[0].file)}else{_loadComplete()}};function _getShuffleItem(){var result=null;if(_model.playlist.length>1){while(!jwplayer.utils.exists(result)){result=Math.floor(Math.random()*_model.playlist.length);if(result==_model.item){result=null}}}else{result=0}return result}function forward(evt){switch(evt.type){case jwplayer.api.events.JWPLAYER_MEDIA_LOADED:_container=_media.getDisplayElement();break;case jwplayer.api.events.JWPLAYER_MEDIA_MUTE:this.mute=evt.mute;break;case jwplayer.api.events.JWPLAYER_MEDIA_VOLUME:this.volume=evt.volume;break}_eventDispatcher.sendEvent(evt.type,evt)}var _mediaProviders={};_model.setActiveMediaProvider=function(playlistItem){if(playlistItem.provider=="audio"){playlistItem.provider="sound"}var provider=playlistItem.provider;var current=_media?_media.getDisplayElement():null;if(provider=="sound"||provider=="http"||provider==""){provider="video"}if(!jwplayer.utils.exists(_mediaProviders[provider])){switch(provider){case"video":_media=new jwplayer.html5.mediavideo(_model,current?current:_container);break;case"youtube":_media=new jwplayer.html5.mediayoutube(_model,current?current:_container);break}if(!jwplayer.utils.exists(_media)){return false}_media.addGlobalListener(forward);_mediaProviders[provider]=_media}else{if(_media!=_mediaProviders[provider]){if(_media){_media.stop()}_media=_mediaProviders[provider]}}return true};_model.getMedia=function(){return _media};_model.seek=function(pos){_eventDispatcher.sendEvent(jwplayer.api.events.JWPLAYER_MEDIA_SEEK,{position:_model.position,offset:pos});return _media.seek(pos)};_model.setVolume=function(newVol){_utils.saveCookie("volume",newVol);_model.volume=newVol};_model.setMute=function(state){_utils.saveCookie("mute",state);_model.mute=state};_model.setupPlugins=function(){if(!jwplayer.utils.exists(_model.plugins)||!jwplayer.utils.exists(_model.plugins.order)||_model.plugins.order.length==0){jwplayer.utils.log("No plugins to set up");return _model}for(var i=0;i<_model.plugins.order.length;i++){try{var pluginName=_model.plugins.order[i];if(jwplayer.utils.exists(jwplayer.html5[pluginName])){if(pluginName=="playlist"){_model.plugins.object[pluginName]=new jwplayer.html5.playlistcomponent(_api,_model.plugins.config[pluginName])}else{_model.plugins.object[pluginName]=new jwplayer.html5[pluginName](_api,_model.plugins.config[pluginName])}}else{_model.plugins.order.splice(plugin,plugin+1)}if(typeof _model.plugins.object[pluginName].addGlobalListener=="function"){_model.plugins.object[pluginName].addGlobalListener(forward)}}catch(err){jwplayer.utils.log("Could not setup "+pluginName)}}};return _model}})(jwplayer);(function(a){a.html5.playlist=function(b){var d=[];if(b.playlist&&b.playlist instanceof Array&&b.playlist.length>0){for(var c in b.playlist){if(!isNaN(parseInt(c))){d.push(new a.html5.playlistitem(b.playlist[c]))}}}else{d.push(new a.html5.playlistitem(b))}return d}})(jwplayer);(function(a){var c={size:180,position:a.html5.view.positions.NONE,itemheight:60,thumbs:true,fontcolor:"#000000",overcolor:"",activecolor:"",backgroundcolor:"#f8f8f8",font:"_sans",fontsize:"",fontstyle:"",fontweight:""};var b={_sans:"Arial, Helvetica, sans-serif",_serif:"Times, Times New Roman, serif",_typewriter:"Courier New, Courier, monospace"};_utils=a.utils;_css=_utils.css;_hide=function(d){_css(d,{display:"none"})};_show=function(d){_css(d,{display:"block"})};a.html5.playlistcomponent=function(r,C){var x=r;var e=a.utils.extend({},c,x.skin.getComponentSettings("playlist"),C);if(e.position==a.html5.view.positions.NONE||typeof a.html5.view.positions[e.position]=="undefined"){return}var y;var l;var D;var d;var g;var f;var k=-1;var h={background:undefined,item:undefined,itemOver:undefined,itemImage:undefined,itemActive:undefined};this.getDisplayElement=function(){return y};this.resize=function(G,E){l=G;D=E;if(x.jwGetFullscreen()){_hide(y)}else{var F={display:"block",width:l,height:D};_css(y,F)}};this.show=function(){_show(y)};this.hide=function(){_hide(y)};function j(){y=document.createElement("div");y.id=x.id+"_jwplayer_playlistcomponent";y.style.overflow="hidden";switch(e.position){case a.html5.view.positions.RIGHT:case a.html5.view.positions.LEFT:y.style.width=e.size+"px";break;case a.html5.view.positions.TOP:case a.html5.view.positions.BOTTOM:y.style.height=e.size+"px";break}B();if(h.item){e.itemheight=h.item.height}y.style.backgroundColor="#C6C6C6";x.jwAddEventListener(a.api.events.JWPLAYER_PLAYLIST_LOADED,s);x.jwAddEventListener(a.api.events.JWPLAYER_PLAYLIST_ITEM,v);x.jwAddEventListener(a.api.events.JWPLAYER_PLAYER_STATE,m)}function p(){var E=document.createElement("ul");_css(E,{width:y.style.width,minWidth:y.style.width,height:y.style.height,backgroundColor:e.backgroundcolor,backgroundImage:h.background?"url("+h.background.src+")":"",color:e.fontcolor,listStyle:"none",margin:0,padding:0,fontFamily:b[e.font]?b[e.font]:b._sans,fontSize:(e.fontsize?e.fontsize:11)+"px",fontStyle:e.fontstyle,fontWeight:e.fontweight,overflowY:"auto"});return E}function z(E){return function(){var F=f.getElementsByClassName("item")[E];var G=e.fontcolor;var H=h.item?"url("+h.item.src+")":"";if(E==x.jwGetPlaylistIndex()){if(e.activecolor!==""){G=e.activecolor}if(h.itemActive){H="url("+h.itemActive.src+")"}}_css(F,{color:e.overcolor!==""?e.overcolor:G,backgroundImage:h.itemOver?"url("+h.itemOver.src+")":H})}}function o(E){return function(){var F=f.getElementsByClassName("item")[E];var G=e.fontcolor;var H=h.item?"url("+h.item.src+")":"";if(E==x.jwGetPlaylistIndex()){if(e.activecolor!==""){G=e.activecolor}if(h.itemActive){H="url("+h.itemActive.src+")"}}_css(F,{color:G,backgroundImage:H})}}function q(J){var Q=d[J];var P=document.createElement("li");P.className="item";_css(P,{height:e.itemheight,display:"block",cursor:"pointer",backgroundImage:h.item?"url("+h.item.src+")":"",backgroundSize:"100% "+e.itemheight+"px"});P.onmouseover=z(J);P.onmouseout=o(J);var K=document.createElement("div");var G=new Image();var L=0;var M=0;var N=0;if(w()&&(Q.image||Q["playlist.image"]||h.itemImage)){G.className="image";if(h.itemImage){L=(e.itemheight-h.itemImage.height)/2;M=h.itemImage.width;N=h.itemImage.height}else{M=e.itemheight*4/3;N=e.itemheight}_css(K,{height:N,width:M,"float":"left",styleFloat:"left",cssFloat:"left",margin:"0 5px 0 0",background:"black",overflow:"hidden",margin:L+"px",position:"relative"});_css(G,{position:"relative"});K.appendChild(G);G.onload=function(){a.utils.stretch(a.utils.stretching.FILL,G,M,N,this.naturalWidth,this.naturalHeight)};if(Q["playlist.image"]){G.src=Q["playlist.image"]}else{if(Q.image){G.src=Q.image}else{if(h.itemImage){G.src=h.itemImage.src}}}P.appendChild(K)}var F=l-M-L*2;if(D<e.itemheight*d.length){F-=15}var E=document.createElement("div");_css(E,{position:"relative",height:"100%",overflow:"hidden"});var H=document.createElement("span");if(Q.duration>0){H.className="duration";_css(H,{fontSize:(e.fontsize?e.fontsize:11)+"px",fontWeight:(e.fontweight?e.fontweight:"bold"),width:"40px",height:e.fontsize?e.fontsize+10:20,lineHeight:24,"float":"right",styleFloat:"right",cssFloat:"right"});H.innerHTML=_utils.timeFormat(Q.duration);E.appendChild(H)}var O=document.createElement("span");O.className="title";_css(O,{padding:"5px 5px 0 "+(L?0:"5px"),height:e.fontsize?e.fontsize+10:20,lineHeight:e.fontsize?e.fontsize+10:20,overflow:"hidden","float":"left",styleFloat:"left",cssFloat:"left",width:((Q.duration>0)?F-50:F)-10+"px",fontSize:(e.fontsize?e.fontsize:13)+"px",fontWeight:(e.fontweight?e.fontweight:"bold")});O.innerHTML=Q?Q.title:"";E.appendChild(O);if(Q.description){var I=document.createElement("span");I.className="description";_css(I,{display:"block","float":"left",styleFloat:"left",cssFloat:"left",margin:0,paddingLeft:O.style.paddingLeft,paddingRight:O.style.paddingRight,lineHeight:(e.fontsize?e.fontsize+4:16)+"px",overflow:"hidden",position:"relative"});I.innerHTML=Q.description;E.appendChild(I)}P.appendChild(E);return P}function s(F){y.innerHTML="";d=t();if(!d){return}items=[];f=p();for(var G=0;G<d.length;G++){var E=q(G);E.onclick=A(G);f.appendChild(E);items.push(E)}k=x.jwGetPlaylistIndex();o(k)();y.appendChild(f);if(_utils.isIOS()&&window.iScroll){f.style.height=e.itemheight*d.length+"px";var H=new iScroll(y.id)}}function t(){var F=x.jwGetPlaylist();var G=[];for(var E=0;E<F.length;E++){if(!F[E]["ova.hidden"]){G.push(F[E])}}return G}function A(E){return function(){x.jwPlaylistItem(E);x.jwPlay(true)}}function n(){f.scrollTop=x.jwGetPlaylistIndex()*e.itemheight}function w(){return e.thumbs.toString().toLowerCase()=="true"}function v(E){if(k>=0){o(k)();k=E.index}o(E.index)();n()}function m(){if(e.position==a.html5.view.positions.OVER){switch(x.jwGetState()){case a.api.events.state.IDLE:_show(y);break;default:_hide(y);break}}}function B(){for(var E in h){h[E]=u(E)}}function u(E){return x.skin.getSkinElement("playlist",E)}j();return this}})(jwplayer);(function(b){b.html5.playlistitem=function(d){var e={author:"",date:"",description:"",image:"",link:"",mediaid:"",tags:"",title:"",provider:"",file:"",streamer:"",duration:-1,start:0,currentLevel:-1,levels:[]};var c=b.utils.extend({},e,d);if(c.type){c.provider=c.type;delete c.type}if(c.levels.length===0){c.levels[0]=new b.html5.playlistitemlevel(c)}if(!c.provider){c.provider=a(c.levels[0])}else{c.provider=c.provider.toLowerCase()}return c};function a(e){if(b.utils.isYouTube(e.file)){return"youtube"}else{var f=b.utils.extension(e.file);var c;if(f&&b.utils.extensionmap[f]){if(f=="m3u8"){return"video"}c=b.utils.extensionmap[f].html5}else{if(e.type){c=e.type}}if(c){var d=c.split("/")[0];if(d=="audio"){return"sound"}else{if(d=="video"){return d}}}}return""}})(jwplayer);(function(a){a.html5.playlistitemlevel=function(b){var d={file:"",streamer:"",bitrate:0,width:0};for(var c in d){if(a.utils.exists(b[c])){d[c]=b[c]}}return d}})(jwplayer);(function(a){a.html5.playlistloader=function(){var c=new a.html5.eventdispatcher();a.utils.extend(this,c);this.load=function(e){a.utils.ajax(e,d,b)};function d(g){var f=[];try{var f=a.utils.parsers.rssparser.parse(g.responseXML.firstChild);c.sendEvent(a.api.events.JWPLAYER_PLAYLIST_LOADED,{playlist:new a.html5.playlist({playlist:f})})}catch(h){b("Could not parse the playlist")}}function b(e){c.sendEvent(a.api.events.JWPLAYER_ERROR,{message:e?e:"Could not load playlist an unknown reason."})}}})(jwplayer);(function(a){a.html5.skin=function(){var b={};var c=false;this.load=function(d,e){new a.html5.skinloader(d,function(f){c=true;b=f;e()},function(){new a.html5.skinloader("",function(f){c=true;b=f;e()})})};this.getSkinElement=function(d,e){if(c){try{return b[d].elements[e]}catch(f){a.utils.log("No such skin component / element: ",[d,e])}}return null};this.getComponentSettings=function(d){if(c&&b&&b[d]){return b[d].settings}return null};this.getComponentLayout=function(d){if(c){return b[d].layout}return null}}})(jwplayer);(function(a){a.html5.skinloader=function(f,p,k){var o={};var c=p;var l=k;var e=true;var j;var n=f;var s=false;function m(){if(typeof n!="string"||n===""){d(a.html5.defaultSkin().xml)}else{a.utils.ajax(a.utils.getAbsolutePath(n),function(t){try{if(a.utils.exists(t.responseXML)){d(t.responseXML);return}}catch(u){h()}d(a.html5.defaultSkin().xml)},function(t){d(a.html5.defaultSkin().xml)})}}function d(y){var E=y.getElementsByTagName("component");if(E.length===0){return}for(var H=0;H<E.length;H++){var C=E[H].getAttribute("name");var B={settings:{},elements:{},layout:{}};o[C]=B;var G=E[H].getElementsByTagName("elements")[0].getElementsByTagName("element");for(var F=0;F<G.length;F++){b(G[F],C)}var z=E[H].getElementsByTagName("settings")[0];if(z&&z.childNodes.length>0){var K=z.getElementsByTagName("setting");for(var P=0;P<K.length;P++){var Q=K[P].getAttribute("name");var I=K[P].getAttribute("value");var x=/color$/.test(Q)?"color":null;o[C].settings[Q]=a.utils.typechecker(I,x)}}var L=E[H].getElementsByTagName("layout")[0];if(L&&L.childNodes.length>0){var M=L.getElementsByTagName("group");for(var w=0;w<M.length;w++){var A=M[w];o[C].layout[A.getAttribute("position")]={elements:[]};for(var O=0;O<A.attributes.length;O++){var D=A.attributes[O];o[C].layout[A.getAttribute("position")][D.name]=D.value}var N=A.getElementsByTagName("*");for(var v=0;v<N.length;v++){var t=N[v];o[C].layout[A.getAttribute("position")].elements.push({type:t.tagName});for(var u=0;u<t.attributes.length;u++){var J=t.attributes[u];o[C].layout[A.getAttribute("position")].elements[v][J.name]=J.value}if(!a.utils.exists(o[C].layout[A.getAttribute("position")].elements[v].name)){o[C].layout[A.getAttribute("position")].elements[v].name=t.tagName}}}}e=false;r()}}function r(){clearInterval(j);if(!s){j=setInterval(function(){q()},100)}}function b(y,x){var w=new Image();var t=y.getAttribute("name");var v=y.getAttribute("src");var A;if(v.indexOf("data:image/png;base64,")===0){A=v}else{var u=a.utils.getAbsolutePath(n);var z=u.substr(0,u.lastIndexOf("/"));A=[z,x,v].join("/")}o[x].elements[t]={height:0,width:0,src:"",ready:false,image:w};w.onload=function(B){g(w,t,x)};w.onerror=function(B){s=true;r();l()};w.src=A}function h(){for(var u in o){var w=o[u];for(var t in w.elements){var x=w.elements[t];var v=x.image;v.onload=null;v.onerror=null;delete x.image;delete w.elements[t]}delete o[u]}}function q(){for(var t in o){if(t!="properties"){for(var u in o[t].elements){if(!o[t].elements[u].ready){return}}}}if(e===false){clearInterval(j);c(o)}}function g(t,v,u){if(o[u]&&o[u].elements[v]){o[u].elements[v].height=t.height;o[u].elements[v].width=t.width;o[u].elements[v].src=t.src;o[u].elements[v].ready=true;r()}else{a.utils.log("Loaded an image for a missing element: "+u+"."+v)}}m()}})(jwplayer);(function(a){a.html5.api=function(c,p){var n={};var g=document.createElement("div");c.parentNode.replaceChild(g,c);g.id=c.id;n.version=a.version;n.id=g.id;var m=new a.html5.model(n,g,p);var k=new a.html5.view(n,g,m);var l=new a.html5.controller(n,g,m,k);n.skin=new a.html5.skin();n.jwPlay=function(q){if(typeof q=="undefined"){f()}else{if(q.toString().toLowerCase()=="true"){l.play()}else{l.pause()}}};n.jwPause=function(q){if(typeof q=="undefined"){f()}else{if(q.toString().toLowerCase()=="true"){l.pause()}else{l.play()}}};function f(){if(m.state==a.api.events.state.PLAYING||m.state==a.api.events.state.BUFFERING){l.pause()}else{l.play()}}n.jwStop=l.stop;n.jwSeek=l.seek;n.jwPlaylistItem=function(q){if(d){if(d.playlistClickable()){d.jwInstreamDestroy();return l.item(q)}}else{return l.item(q)}};n.jwPlaylistNext=l.next;n.jwPlaylistPrev=l.prev;n.jwResize=l.resize;n.jwLoad=l.load;n.jwDetachMedia=l.detachMedia;n.jwAttachMedia=l.attachMedia;function j(q){return function(){return m[q]}}function e(q,s,r){return function(){var t=m.plugins.object[q];if(t&&t[s]&&typeof t[s]=="function"){t[s].apply(t,r)}}}n.jwGetPlaylistIndex=j("item");n.jwGetPosition=j("position");n.jwGetDuration=j("duration");n.jwGetBuffer=j("buffer");n.jwGetWidth=j("width");n.jwGetHeight=j("height");n.jwGetFullscreen=j("fullscreen");n.jwSetFullscreen=l.setFullscreen;n.jwGetVolume=j("volume");n.jwSetVolume=l.setVolume;n.jwGetMute=j("mute");n.jwSetMute=l.setMute;n.jwGetStretching=function(){return m.stretching.toUpperCase()};n.jwGetState=j("state");n.jwGetVersion=function(){return n.version};n.jwGetPlaylist=function(){return m.playlist};n.jwAddEventListener=l.addEventListener;n.jwRemoveEventListener=l.removeEventListener;n.jwSendEvent=l.sendEvent;n.jwDockSetButton=function(t,q,r,s){if(m.plugins.object.dock&&m.plugins.object.dock.setButton){m.plugins.object.dock.setButton(t,q,r,s)}};n.jwControlbarShow=e("controlbar","show");n.jwControlbarHide=e("controlbar","hide");n.jwDockShow=e("dock","show");n.jwDockHide=e("dock","hide");n.jwDisplayShow=e("display","show");n.jwDisplayHide=e("display","hide");var d;n.jwLoadInstream=function(r,q){if(!d){d=new a.html5.instream(n,m,k,l)}setTimeout(function(){d.load(r,q)},10)};n.jwInstreamDestroy=function(){if(d){d.jwInstreamDestroy()}};n.jwInstreamAddEventListener=o("jwInstreamAddEventListener");n.jwInstreamRemoveEventListener=o("jwInstreamRemoveEventListener");n.jwInstreamGetState=o("jwInstreamGetState");n.jwInstreamGetDuration=o("jwInstreamGetDuration");n.jwInstreamGetPosition=o("jwInstreamGetPosition");n.jwInstreamPlay=o("jwInstreamPlay");n.jwInstreamPause=o("jwInstreamPause");n.jwInstreamSeek=o("jwInstreamSeek");function o(q){return function(){if(d&&typeof d[q]=="function"){return d[q].apply(this,arguments)}else{_utils.log("Could not call instream method - instream API not initialized")}}}n.jwGetLevel=function(){};n.jwGetBandwidth=function(){};n.jwGetLockState=function(){};n.jwLock=function(){};n.jwUnlock=function(){};function b(){if(m.config.playlistfile){m.addEventListener(a.api.events.JWPLAYER_PLAYLIST_LOADED,h);m.loadPlaylist(m.config.playlistfile)}else{if(typeof m.config.playlist=="string"){m.addEventListener(a.api.events.JWPLAYER_PLAYLIST_LOADED,h);m.loadPlaylist(m.config.playlist)}else{m.loadPlaylist(m.config);setTimeout(h,25)}}}function h(q){m.removeEventListener(a.api.events.JWPLAYER_PLAYLIST_LOADED,h);m.setupPlugins();k.setup();var q={id:n.id,version:n.version};l.playerReady(q)}if(m.config.chromeless&&!a.utils.isIOS()){b()}else{n.skin.load(m.config.skin,b)}return n}})(jwplayer)}; \ No newline at end of file diff --git a/bbb-screenshare/jws/player/jw-player/license.txt b/bbb-screenshare/jws/player/jw-player/license.txt new file mode 100755 index 0000000000000000000000000000000000000000..3e8047b04f92e963d4631661ae9548df0a08ac6f --- /dev/null +++ b/bbb-screenshare/jws/player/jw-player/license.txt @@ -0,0 +1 @@ +This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/3.0/ or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. \ No newline at end of file diff --git a/bbb-screenshare/jws/player/jw-player/player.swf b/bbb-screenshare/jws/player/jw-player/player.swf new file mode 100755 index 0000000000000000000000000000000000000000..be4c9d958311e1019a5dd206ba1341c6f73dccb3 Binary files /dev/null and b/bbb-screenshare/jws/player/jw-player/player.swf differ diff --git a/bbb-screenshare/jws/player/screenshot.png b/bbb-screenshare/jws/player/screenshot.png new file mode 100755 index 0000000000000000000000000000000000000000..ab49abd5756bcd044fcaa5887cf4fc4aaf49a555 Binary files /dev/null and b/bbb-screenshare/jws/player/screenshot.png differ diff --git a/bbb-screenshare/jws/player/style.css b/bbb-screenshare/jws/player/style.css new file mode 100755 index 0000000000000000000000000000000000000000..da15278e751749190f31ffdfda3c9bae3c2866fa --- /dev/null +++ b/bbb-screenshare/jws/player/style.css @@ -0,0 +1,53 @@ +body { + margin: 0; + padding: 0; +} + +#menu { + border-bottom: 1px solid #bbb; + background: #eee; + padding: 20px; +} + +#menu form { + padding: 0; + margin: 0; +} + +.field { + clear: both; + display: block; + margin-bottom: 5px; +} + +.field2 { + clear: both; + display: block; + margin-bottom: 5px; + margin-left: 160px; +} + +.field .label { + width: 80px; + float: left; + text-align: right; + margin-right: 10px; + margin-top: 5px; +} + +.field input[type=text] { + width: 450px; +} + +.field button { + margin-left: 210px; +} + +#content { + padding: 30px; +} + +#wrapper { + border: 1px solid red; + padding: 5px; +} diff --git a/bbb-screenshare/jws/webstart/build.gradle b/bbb-screenshare/jws/webstart/build.gradle new file mode 100755 index 0000000000000000000000000000000000000000..f3273335b168e7f974920588c473a48452d0691f --- /dev/null +++ b/bbb-screenshare/jws/webstart/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'java' +apply plugin: 'eclipse' + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +version = '0.0.1' +archivesBaseName = 'javacv-screenshare' + +repositories { + flatDir name: 'localRepo', dirs: "./lib" +} + +dependencies { + compile ":javacpp:@jar" + compile ":ffmpeg:@jar" +} + +jar { + manifest.mainAttributes("Permissions": "all-permissions") + manifest.mainAttributes("Codebase": "*") + manifest.mainAttributes("Application-Name": "BigBlueButton Screenshare") + manifest.mainAttributes("Application-Library-Allowable-Codebase": "*") + manifest.mainAttributes("Caller-Allowable-Codebase": "*") + manifest.mainAttributes("Trusted-Only": "true") +} diff --git a/bbb-screenshare/jws/webstart/build.sh b/bbb-screenshare/jws/webstart/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..2be4ae9983076cf39cb935532122f6b308ba908e --- /dev/null +++ b/bbb-screenshare/jws/webstart/build.sh @@ -0,0 +1,5 @@ +cp ../../app/jws/lib/*.jar lib +gradle jar +ant sign-jar +cp build/libs/javacv-screenshare-0.0.1.jar ../../app/jws/lib/ + diff --git a/bbb-screenshare/jws/webstart/build.xml b/bbb-screenshare/jws/webstart/build.xml new file mode 100755 index 0000000000000000000000000000000000000000..6c1e3d55e48daf60090965ae984138b5be6576b2 --- /dev/null +++ b/bbb-screenshare/jws/webstart/build.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" ?> +<project name="bbb-deskshare-applet" basedir="."> + <!-- How to sign the applet. From Ant in Action book --> + + <target name="get-password" depends="init-security" description="Prompts for password for keystore"> + <input addproperty="keystore.password" >password for keystore:</input> + <echo level="verbose">password = ${keystore.password}</echo> + </target> + + <target name="init-security"> + <property name="keystore.dir" location="${user.home}/.secret" /> + <mkdir dir="${keystore.dir}" /> + <chmod file="${keystore.dir}" perm="700"/> + <property name="keystore" + location="${keystore.dir}/local.keystore" /> + <property file="${keystore.dir}/keystore.properties" /> + <property name="keystore.alias" value="code.signer"/> + </target> + + <target name="create-signing-key" depends="get-password"> + <genkey alias="${keystore.alias}" keystore="${keystore}" storepass="${keystore.password}" validity="366" > + <dname> + <param name="CN" value="BigBlueButton"/> + <param name="OU" value="BigBlueButton Project"/> + <param name="O" value="BigBlueButton"/> + <param name="C" value="CA"/> + </dname> + </genkey> + </target> + + <!-- Sign jar with Certificate using pkcs12 file --> + <target name="check-certificate"> + <input message="Enter cetificate filename:" addproperty="cert.name" /> + <input message="Enter cetificate password:" addproperty="cert.password" /> + <exec executable="/usr/bin/keytool" outputproperty="cert.info"> + <arg line="-list" /> + <arg line="-storetype pkcs12" /> + <arg line="-keystore ${cert.name}" /> + <arg line="-storepass ${cert.password}" /> + <arg line="-v" /> + </exec> + </target> + + <target name="get-alias-name" depends="check-certificate"> + <script language="javascript"> + <![CDATA[ + // getting the value + info = project.getProperty("cert.info"); + alias = (info.match(/Alias name:(.*)/)[0]).replace("Alias name: ",""); + project.setProperty("cert.alias",alias); + ]]> + </script> + </target> + + + <target name="sign-jar" depends="get-alias-name"> + <signjar jar="build/libs/javacv-screenshare-0.0.1.jar" + storetype="pkcs12" + keystore="${cert.name}" + storepass="${cert.password}" + alias="${cert.alias}" /> + </target> + +</project> diff --git a/bbb-screenshare/jws/webstart/src/main/java/com/myjavatools/web/ClientHttpRequest.java b/bbb-screenshare/jws/webstart/src/main/java/com/myjavatools/web/ClientHttpRequest.java new file mode 100755 index 0000000000000000000000000000000000000000..bc553e209a47a59794d83b6e77c474f0b30a7a26 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/com/myjavatools/web/ClientHttpRequest.java @@ -0,0 +1,492 @@ +package com.myjavatools.web; + +import java.net.URLConnection; +import java.net.URL; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.io.File; +import java.io.InputStream; +import java.util.Random; +import java.io.OutputStream; +import java.io.FileInputStream; +import java.util.Iterator; + +/** + * <p>Title: Client HTTP Request class</p> + * <p>Description: this class helps to send POST HTTP requests with various form data, + * including files. Cookies can be added to be included in the request.</p> + * + * @author Vlad Patryshev + * @version 1.0 + */ +public class ClientHttpRequest { + public URLConnection connection; + OutputStream os = null; + Map cookies = new HashMap(); + + protected void connect() throws IOException { + if (os == null) os = connection.getOutputStream(); + } + + protected void write(char c) throws IOException { + connect(); + os.write(c); + } + + protected void write(String s) throws IOException { + connect(); + os.write(s.getBytes()); + } + + protected void newline() throws IOException { + connect(); + write("\r\n"); + } + + protected void writeln(String s) throws IOException { + connect(); + write(s); + newline(); + } + + private static Random random = new Random(); + + protected static String randomString() { + return Long.toString(random.nextLong(), 36); + } + + String boundary = "---------------------------" + randomString() + randomString() + randomString(); + + private void boundary() throws IOException { + write("--"); + write(boundary); + } + + /** + * Creates a new multipart POST HTTP request on a freshly opened URLConnection + * + * @param connection an already open URL connection + * @throws IOException + */ + public ClientHttpRequest(URLConnection connection) throws IOException { + this.connection = connection; + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", + "multipart/form-data; boundary=" + boundary); + } + + /** + * Creates a new multipart POST HTTP request for a specified URL + * + * @param url the URL to send request to + * @throws IOException + */ + public ClientHttpRequest(URL url) throws IOException { + this(url.openConnection()); + } + + /** + * Creates a new multipart POST HTTP request for a specified URL string + * + * @param urlString the string representation of the URL to send request to + * @throws IOException + */ + public ClientHttpRequest(String urlString) throws IOException { + this(new URL(urlString)); + } + + + private void postCookies() { + StringBuffer cookieList = new StringBuffer(); + + for (Iterator i = cookies.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = (Map.Entry)(i.next()); + cookieList.append(entry.getKey().toString() + "=" + entry.getValue()); + + if (i.hasNext()) { + cookieList.append("; "); + } + } + if (cookieList.length() > 0) { + connection.setRequestProperty("Cookie", cookieList.toString()); + } + } + + /** + * adds a cookie to the requst + * @param name cookie name + * @param value cookie value + * @throws IOException + */ + public void setCookie(String name, String value) throws IOException { + cookies.put(name, value); + } + + /** + * adds cookies to the request + * @param cookies the cookie "name-to-value" map + * @throws IOException + */ + public void setCookies(Map cookies) throws IOException { + if (cookies == null) return; + this.cookies.putAll(cookies); + } + + /** + * adds cookies to the request + * @param cookies array of cookie names and values (cookies[2*i] is a name, cookies[2*i + 1] is a value) + * @throws IOException + */ + public void setCookies(String[] cookies) throws IOException { + if (cookies == null) return; + for (int i = 0; i < cookies.length - 1; i+=2) { + setCookie(cookies[i], cookies[i+1]); + } + } + + private void writeName(String name) throws IOException { + newline(); + write("Content-Disposition: form-data; name=\""); + write(name); + write('"'); + } + + /** + * adds a string parameter to the request + * @param name parameter name + * @param value parameter value + * @throws IOException + */ + public void setParameter(String name, String value) throws IOException { + boundary(); + writeName(name); + newline(); newline(); + writeln(value); + } + + private static void pipe(InputStream in, OutputStream out) throws IOException { + byte[] buf = new byte[500000]; + int nread; + int navailable; + int total = 0; + synchronized (in) { + while((nread = in.read(buf, 0, buf.length)) >= 0) { + out.write(buf, 0, nread); + total += nread; + } + } + out.flush(); + buf = null; + } + + /** + * adds a file parameter to the request + * @param name parameter name + * @param filename the name of the file + * @param is input stream to read the contents of the file from + * @throws IOException + */ + public void setParameter(String name, String filename, InputStream is) throws IOException { + boundary(); + writeName(name); + write("; filename=\""); + write(filename); + write('"'); + newline(); + write("Content-Type: "); + String type = connection.guessContentTypeFromName(filename); + if (type == null) type = "application/octet-stream"; + writeln(type); + newline(); + pipe(is, os); + newline(); + } + + /** + * adds a file parameter to the request + * @param name parameter name + * @param file the file to upload + * @throws IOException + */ + public void setParameter(String name, File file) throws IOException { + setParameter(name, file.getPath(), new FileInputStream(file)); + } + + /** + * adds a parameter to the request; if the parameter is a File, the file is uploaded, otherwise the string value of the parameter is passed in the request + * @param name parameter name + * @param object parameter value, a File or anything else that can be stringified + * @throws IOException + */ + public void setParameter(String name, Object object) throws IOException { + if (object instanceof File) { + setParameter(name, (File) object); + } else { + setParameter(name, object.toString()); + } + } + + /** + * adds parameters to the request + * @param parameters "name-to-value" map of parameters; if a value is a file, the file is uploaded, otherwise it is stringified and sent in the request + * @throws IOException + */ + public void setParameters(Map parameters) throws IOException { + if (parameters == null) return; + for (Iterator i = parameters.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = (Map.Entry)i.next(); + setParameter(entry.getKey().toString(), entry.getValue()); + } + } + + /** + * adds parameters to the request + * @param parameters array of parameter names and values (parameters[2*i] is a name, parameters[2*i + 1] is a value); if a value is a file, the file is uploaded, otherwise it is stringified and sent in the request + * @throws IOException + */ + public void setParameters(Object[] parameters) throws IOException { + if (parameters == null) return; + for (int i = 0; i < parameters.length - 1; i+=2) { + setParameter(parameters[i].toString(), parameters[i+1]); + } + } + + /** + * posts the requests to the server, with all the cookies and parameters that were added + * @return input stream with the server response + * @throws IOException + */ + public InputStream post() throws IOException { + boundary(); + writeln("--"); + os.close(); + return connection.getInputStream(); + } + + /** + * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with parameters that are passed in the argument + * @param parameters request parameters + * @return input stream with the server response + * @throws IOException + * @see setParameters + */ + public InputStream post(Map parameters) throws IOException { + setParameters(parameters); + return post(); + } + + /** + * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with parameters that are passed in the argument + * @param parameters request parameters + * @return input stream with the server response + * @throws IOException + * @see setParameters + */ + public InputStream post(Object[] parameters) throws IOException { + setParameters(parameters); + return post(); + } + + /** + * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with cookies and parameters that are passed in the arguments + * @param cookies request cookies + * @param parameters request parameters + * @return input stream with the server response + * @throws IOException + * @see setParameters + * @see setCookies + */ + public InputStream post(Map cookies, Map parameters) throws IOException { + setCookies(cookies); + setParameters(parameters); + return post(); + } + + /** + * posts the requests to the server, with all the cookies and parameters that were added before (if any), and with cookies and parameters that are passed in the arguments + * @param cookies request cookies + * @param parameters request parameters + * @return input stream with the server response + * @throws IOException + * @see setParameters + * @see setCookies + */ + public InputStream post(String[] cookies, Object[] parameters) throws IOException { + setCookies(cookies); + setParameters(parameters); + return post(); + } + + /** + * post the POST request to the server, with the specified parameter + * @param name parameter name + * @param value parameter value + * @return input stream with the server response + * @throws IOException + * @see setParameter + */ + public InputStream post(String name, Object value) throws IOException { + setParameter(name, value); + return post(); + } + + /** + * post the POST request to the server, with the specified parameters + * @param name1 first parameter name + * @param value1 first parameter value + * @param name2 second parameter name + * @param value2 second parameter value + * @return input stream with the server response + * @throws IOException + * @see setParameter + */ + public InputStream post(String name1, Object value1, String name2, Object value2) throws IOException { + setParameter(name1, value1); + return post(name2, value2); + } + + /** + * post the POST request to the server, with the specified parameters + * @param name1 first parameter name + * @param value1 first parameter value + * @param name2 second parameter name + * @param value2 second parameter value + * @param name3 third parameter name + * @param value3 third parameter value + * @return input stream with the server response + * @throws IOException + * @see setParameter + */ + public InputStream post(String name1, Object value1, String name2, Object value2, String name3, Object value3) throws IOException { + setParameter(name1, value1); + return post(name2, value2, name3, value3); + } + + /** + * post the POST request to the server, with the specified parameters + * @param name1 first parameter name + * @param value1 first parameter value + * @param name2 second parameter name + * @param value2 second parameter value + * @param name3 third parameter name + * @param value3 third parameter value + * @param name4 fourth parameter name + * @param value4 fourth parameter value + * @return input stream with the server response + * @throws IOException + * @see setParameter + */ + public InputStream post(String name1, Object value1, String name2, Object value2, String name3, Object value3, String name4, Object value4) throws IOException { + setParameter(name1, value1); + return post(name2, value2, name3, value3, name4, value4); + } + + /** + * posts a new request to specified URL, with parameters that are passed in the argument + * @param parameters request parameters + * @return input stream with the server response + * @throws IOException + * @see setParameters + */ + public static InputStream post(URL url, Map parameters) throws IOException { + return new ClientHttpRequest(url).post(parameters); + } + + /** + * posts a new request to specified URL, with parameters that are passed in the argument + * @param parameters request parameters + * @return input stream with the server response + * @throws IOException + * @see setParameters + */ + public static InputStream post(URL url, Object[] parameters) throws IOException { + return new ClientHttpRequest(url).post(parameters); + } + + /** + * posts a new request to specified URL, with cookies and parameters that are passed in the argument + * @param cookies request cookies + * @param parameters request parameters + * @return input stream with the server response + * @throws IOException + * @see setCookies + * @see setParameters + */ + public static InputStream post(URL url, Map cookies, Map parameters) throws IOException { + return new ClientHttpRequest(url).post(cookies, parameters); + } + + /** + * posts a new request to specified URL, with cookies and parameters that are passed in the argument + * @param cookies request cookies + * @param parameters request parameters + * @return input stream with the server response + * @throws IOException + * @see setCookies + * @see setParameters + */ + public static InputStream post(URL url, String[] cookies, Object[] parameters) throws IOException { + return new ClientHttpRequest(url).post(cookies, parameters); + } + + /** + * post the POST request specified URL, with the specified parameter + * @param name parameter name + * @param value parameter value + * @return input stream with the server response + * @throws IOException + * @see setParameter + */ + public static InputStream post(URL url, String name1, Object value1) throws IOException { + return new ClientHttpRequest(url).post(name1, value1); + } + + /** + * post the POST request to specified URL, with the specified parameters + * @param name1 first parameter name + * @param value1 first parameter value + * @param name2 second parameter name + * @param value2 second parameter value + * @return input stream with the server response + * @throws IOException + * @see setParameter + */ + public static InputStream post(URL url, String name1, Object value1, String name2, Object value2) throws IOException { + return new ClientHttpRequest(url).post(name1, value1, name2, value2); + } + + /** + * post the POST request to specified URL, with the specified parameters + * @param name1 first parameter name + * @param value1 first parameter value + * @param name2 second parameter name + * @param value2 second parameter value + * @param name3 third parameter name + * @param value3 third parameter value + * @return input stream with the server response + * @throws IOException + * @see setParameter + */ + public static InputStream post(URL url, String name1, Object value1, String name2, Object value2, String name3, Object value3) throws IOException { + return new ClientHttpRequest(url).post(name1, value1, name2, value2, name3, value3); + } + + /** + * post the POST request to specified URL, with the specified parameters + * @param name1 first parameter name + * @param value1 first parameter value + * @param name2 second parameter name + * @param value2 second parameter value + * @param name3 third parameter name + * @param value3 third parameter value + * @param name4 fourth parameter name + * @param value4 fourth parameter value + * @return input stream with the server response + * @throws IOException + * @see setParameter + */ + public static InputStream post(URL url, String name1, Object value1, String name2, Object value2, String name3, Object value3, String name4, Object value4) throws IOException { + return new ClientHttpRequest(url).post(name1, value1, name2, value2, name3, value3, name4, value4); + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/jargs/gnu/CmdLineParser.java b/bbb-screenshare/jws/webstart/src/main/java/jargs/gnu/CmdLineParser.java new file mode 100755 index 0000000000000000000000000000000000000000..cd429dd8156510d76da643411a13848a4eeb984d --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/jargs/gnu/CmdLineParser.java @@ -0,0 +1,525 @@ +package jargs.gnu; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Hashtable; +import java.util.Vector; +import java.util.Locale; + +/** + * Largely GNU-compatible command-line options parser. Has short (-v) and + * long-form (--verbose) option support, and also allows options with + * associated values (-d 2, --debug 2, --debug=2). Option processing + * can be explicitly terminated by the argument '--'. + * + * @author Steve Purcell + * @version $Revision$ + * @see jargs.examples.gnu.OptionTest + */ +public class CmdLineParser { + + /** + * Base class for exceptions that may be thrown when options are parsed + */ + public static abstract class OptionException extends Exception { + OptionException(String msg) { super(msg); } + } + + /** + * Thrown when the parsed command-line contains an option that is not + * recognised. <code>getMessage()</code> returns + * an error string suitable for reporting the error to the user (in + * English). + */ + public static class UnknownOptionException extends OptionException { + UnknownOptionException( String optionName ) { + this(optionName, "Unknown option '" + optionName + "'"); + } + + UnknownOptionException( String optionName, String msg ) { + super(msg); + this.optionName = optionName; + } + + /** + * @return the name of the option that was unknown (e.g. "-u") + */ + public String getOptionName() { return this.optionName; } + private String optionName = null; + } + + /** + * Thrown when the parsed commandline contains multiple concatenated + * short options, such as -abcd, where one is unknown. + * <code>getMessage()</code> returns an english human-readable error + * string. + * @author Vidar Holen + */ + public static class UnknownSuboptionException + extends UnknownOptionException { + private char suboption; + + UnknownSuboptionException( String option, char suboption ) { + super(option, "Illegal option: '"+suboption+"' in '"+option+"'"); + this.suboption=suboption; + } + public char getSuboption() { return suboption; } + } + + /** + * Thrown when the parsed commandline contains multiple concatenated + * short options, such as -abcd, where one or more requires a value. + * <code>getMessage()</code> returns an english human-readable error + * string. + * @author Vidar Holen + */ + public static class NotFlagException extends UnknownOptionException { + private char notflag; + + NotFlagException( String option, char unflaggish ) { + super(option, "Illegal option: '"+option+"', '"+ + unflaggish+"' requires a value"); + notflag=unflaggish; + } + + /** + * @return the first character which wasn't a boolean (e.g 'c') + */ + public char getOptionChar() { return notflag; } + } + + /** + * Thrown when an illegal or missing value is given by the user for + * an option that takes a value. <code>getMessage()</code> returns + * an error string suitable for reporting the error to the user (in + * English). + */ + public static class IllegalOptionValueException extends OptionException { + public IllegalOptionValueException( Option opt, String value ) { + super("Illegal value '" + value + "' for option " + + (opt.shortForm() != null ? "-" + opt.shortForm() + "/" : "") + + "--" + opt.longForm()); + this.option = opt; + this.value = value; + } + + /** + * @return the name of the option whose value was illegal (e.g. "-u") + */ + public Option getOption() { return this.option; } + + /** + * @return the illegal value + */ + public String getValue() { return this.value; } + private Option option; + private String value; + } + + /** + * Representation of a command-line option + */ + public static abstract class Option { + + protected Option( String longForm, boolean wantsValue ) { + this(null, longForm, wantsValue); + } + + protected Option( char shortForm, String longForm, + boolean wantsValue ) { + this(new String(new char[]{shortForm}), longForm, wantsValue); + } + + private Option( String shortForm, String longForm, boolean wantsValue ) { + if ( longForm == null ) + throw new IllegalArgumentException("Null longForm not allowed"); + this.shortForm = shortForm; + this.longForm = longForm; + this.wantsValue = wantsValue; + } + + public String shortForm() { return this.shortForm; } + + public String longForm() { return this.longForm; } + + /** + * Tells whether or not this option wants a value + */ + public boolean wantsValue() { return this.wantsValue; } + + public final Object getValue( String arg, Locale locale ) + throws IllegalOptionValueException { + if ( this.wantsValue ) { + if ( arg == null ) { + throw new IllegalOptionValueException(this, ""); + } + return this.parseValue(arg, locale); + } + else { + return Boolean.TRUE; + } + } + + /** + * Override to extract and convert an option value passed on the + * command-line + */ + protected Object parseValue( String arg, Locale locale ) + throws IllegalOptionValueException { + return null; + } + + private String shortForm = null; + private String longForm = null; + private boolean wantsValue = false; + + public static class BooleanOption extends Option { + public BooleanOption( char shortForm, String longForm ) { + super(shortForm, longForm, false); + } + public BooleanOption( String longForm ) { + super(longForm, false); + } + } + + /** + * An option that expects an integer value + */ + public static class IntegerOption extends Option { + public IntegerOption( char shortForm, String longForm ) { + super(shortForm, longForm, true); + } + public IntegerOption( String longForm ) { + super(longForm, true); + } + protected Object parseValue( String arg, Locale locale ) + throws IllegalOptionValueException { + try { + return new Integer(arg); + } + catch (NumberFormatException e) { + throw new IllegalOptionValueException(this, arg); + } + } + } + + /** + * An option that expects a long integer value + */ + public static class LongOption extends Option { + public LongOption( char shortForm, String longForm ) { + super(shortForm, longForm, true); + } + public LongOption( String longForm ) { + super(longForm, true); + } + protected Object parseValue( String arg, Locale locale ) + throws IllegalOptionValueException { + try { + return new Long(arg); + } + catch (NumberFormatException e) { + throw new IllegalOptionValueException(this, arg); + } + } + } + + /** + * An option that expects a floating-point value + */ + public static class DoubleOption extends Option { + public DoubleOption( char shortForm, String longForm ) { + super(shortForm, longForm, true); + } + public DoubleOption( String longForm ) { + super(longForm, true); + } + protected Object parseValue( String arg, Locale locale ) + throws IllegalOptionValueException { + try { + NumberFormat format = NumberFormat.getNumberInstance(locale); + Number num = (Number)format.parse(arg); + return new Double(num.doubleValue()); + } + catch (ParseException e) { + throw new IllegalOptionValueException(this, arg); + } + } + } + + /** + * An option that expects a string value + */ + public static class StringOption extends Option { + public StringOption( char shortForm, String longForm ) { + super(shortForm, longForm, true); + } + public StringOption( String longForm ) { + super(longForm, true); + } + protected Object parseValue( String arg, Locale locale ) { + return arg; + } + } + } + + /** + * Add the specified Option to the list of accepted options + */ + public final Option addOption( Option opt ) { + if ( opt.shortForm() != null ) + this.options.put("-" + opt.shortForm(), opt); + this.options.put("--" + opt.longForm(), opt); + return opt; + } + + /** + * Convenience method for adding a string option. + * @return the new Option + */ + public final Option addStringOption( char shortForm, String longForm ) { + return addOption(new Option.StringOption(shortForm, longForm)); + } + + /** + * Convenience method for adding a string option. + * @return the new Option + */ + public final Option addStringOption( String longForm ) { + return addOption(new Option.StringOption(longForm)); + } + + /** + * Convenience method for adding an integer option. + * @return the new Option + */ + public final Option addIntegerOption( char shortForm, String longForm ) { + return addOption(new Option.IntegerOption(shortForm, longForm)); + } + + /** + * Convenience method for adding an integer option. + * @return the new Option + */ + public final Option addIntegerOption( String longForm ) { + return addOption(new Option.IntegerOption(longForm)); + } + + /** + * Convenience method for adding a long integer option. + * @return the new Option + */ + public final Option addLongOption( char shortForm, String longForm ) { + return addOption(new Option.LongOption(shortForm, longForm)); + } + + /** + * Convenience method for adding a long integer option. + * @return the new Option + */ + public final Option addLongOption( String longForm ) { + return addOption(new Option.LongOption(longForm)); + } + + /** + * Convenience method for adding a double option. + * @return the new Option + */ + public final Option addDoubleOption( char shortForm, String longForm ) { + return addOption(new Option.DoubleOption(shortForm, longForm)); + } + + /** + * Convenience method for adding a double option. + * @return the new Option + */ + public final Option addDoubleOption( String longForm ) { + return addOption(new Option.DoubleOption(longForm)); + } + + /** + * Convenience method for adding a boolean option. + * @return the new Option + */ + public final Option addBooleanOption( char shortForm, String longForm ) { + return addOption(new Option.BooleanOption(shortForm, longForm)); + } + + /** + * Convenience method for adding a boolean option. + * @return the new Option + */ + public final Option addBooleanOption( String longForm ) { + return addOption(new Option.BooleanOption(longForm)); + } + + /** + * Equivalent to {@link #getOptionValue(Option, Object) getOptionValue(o, + * null)}. + */ + public final Object getOptionValue( Option o ) { + return getOptionValue(o, null); + } + + + /** + * @return the parsed value of the given Option, or the given default 'def' + * if the option was not set + */ + public final Object getOptionValue( Option o, Object def ) { + Vector v = (Vector)values.get(o.longForm()); + + if (v == null) { + return def; + } + else if (v.isEmpty()) { + return null; + } + else { + Object result = v.elementAt(0); + v.removeElementAt(0); + return result; + } + } + + + /** + * @return A Vector giving the parsed values of all the occurrences of the + * given Option, or an empty Vector if the option was not set. + */ + public final Vector getOptionValues( Option option ) { + Vector result = new Vector(); + + while (true) { + Object o = getOptionValue(option, null); + + if (o == null) { + return result; + } + else { + result.addElement(o); + } + } + } + + + /** + * @return the non-option arguments + */ + public final String[] getRemainingArgs() { + return this.remainingArgs; + } + + /** + * Extract the options and non-option arguments from the given + * list of command-line arguments. The default locale is used for + * parsing options whose values might be locale-specific. + */ + public final void parse( String[] argv ) + throws IllegalOptionValueException, UnknownOptionException { + + // It would be best if this method only threw OptionException, but for + // backwards compatibility with old user code we throw the two + // exceptions above instead. + + parse(argv, Locale.getDefault()); + } + + /** + * Extract the options and non-option arguments from the given + * list of command-line arguments. The specified locale is used for + * parsing options whose values might be locale-specific. + */ + public final void parse( String[] argv, Locale locale ) + throws IllegalOptionValueException, UnknownOptionException { + + // It would be best if this method only threw OptionException, but for + // backwards compatibility with old user code we throw the two + // exceptions above instead. + + Vector otherArgs = new Vector(); + int position = 0; + this.values = new Hashtable(10); + while ( position < argv.length ) { + String curArg = argv[position]; + if ( curArg.startsWith("-") ) { + if ( curArg.equals("--") ) { // end of options + position += 1; + break; + } + String valueArg = null; + if ( curArg.startsWith("--") ) { // handle --arg=value + int equalsPos = curArg.indexOf("="); + if ( equalsPos != -1 ) { + valueArg = curArg.substring(equalsPos+1); + curArg = curArg.substring(0,equalsPos); + } + } else if(curArg.length() > 2) { // handle -abcd + for(int i=1; i<curArg.length(); i++) { + Option opt=(Option)this.options.get + ("-"+curArg.charAt(i)); + if(opt==null) throw new + UnknownSuboptionException(curArg,curArg.charAt(i)); + if(opt.wantsValue()) throw new + NotFlagException(curArg,curArg.charAt(i)); + addValue(opt, opt.getValue(null,locale)); + + } + position++; + continue; + } + + Option opt = (Option)this.options.get(curArg); + if ( opt == null ) { + throw new UnknownOptionException(curArg); + } + Object value = null; + if ( opt.wantsValue() ) { + if ( valueArg == null ) { + position += 1; + if ( position < argv.length ) { + valueArg = argv[position]; + } + } + value = opt.getValue(valueArg, locale); + } + else { + value = opt.getValue(null, locale); + } + + addValue(opt, value); + + position += 1; + } + else { + otherArgs.addElement(curArg); + position += 1; + } + } + for ( ; position < argv.length; ++position ) { + otherArgs.addElement(argv[position]); + } + + this.remainingArgs = new String[otherArgs.size()]; + otherArgs.copyInto(remainingArgs); + } + + + private void addValue(Option opt, Object value) { + String lf = opt.longForm(); + + Vector v = (Vector)values.get(lf); + + if (v == null) { + v = new Vector(); + values.put(lf, v); + } + + v.addElement(value); + } + + + private String[] remainingArgs = null; + private Hashtable options = new Hashtable(10); + private Hashtable values = new Hashtable(10); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ClientListener.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ClientListener.java new file mode 100755 index 0000000000000000000000000000000000000000..aa216809a1b0c1775f1105c8999667e2b2238490 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ClientListener.java @@ -0,0 +1,23 @@ +/** + * 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.screenshare.client; + +public interface ClientListener { + public void onClientStop(ExitCode reason); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskShareApplet.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskShareApplet.java new file mode 100755 index 0000000000000000000000000000000000000000..f76e5c824a466b96b47a1c7a11f5807010f78683 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskShareApplet.java @@ -0,0 +1,253 @@ +/** + * 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.screenshare.client; + +import static org.bytedeco.javacpp.avformat.av_register_all; +import static org.bytedeco.javacpp.avformat.avformat_network_init; + +import javax.imageio.ImageIO; +import javax.swing.JApplet; +import javax.swing.JFrame; +import javax.swing.JOptionPane; + +import org.bigbluebutton.screenshare.client.javacv.BBBFFmpegFrameRecorder; +import org.bigbluebutton.screenshare.client.javacv.BBBFrameRecorder.Exception; +import org.bytedeco.javacpp.Loader; + +import java.io.IOException; +import java.net.URL; +import java.security.*; +import java.awt.Image; + +public class DeskShareApplet extends JApplet implements ClientListener { + public static final String NAME = "DESKSHAREAPPLET: "; + + private static final long serialVersionUID = 1L; + + String hostValue = "localhost"; + String minJreVersion = "1.7.0_51"; + Integer portValue = new Integer(9123); + String roomValue = "85115"; + Integer cWidthValue = new Integer(800); + Integer cHeightValue = new Integer(600); + Integer sWidthValue = new Integer(800); + Integer sHeightValue = new Integer(600); + Double scale = new Double(0.8); + Boolean qualityValue = false; + Boolean aspectRatioValue = false; + Integer xValue = new Integer(0); + Integer yValue = new Integer(0); + Boolean tunnelValue = true; + Boolean fullScreenValue = false; + String url = "rtmp://192.168.23.23/live/foo/room2"; + DeskshareClient client; + Image icon; + + public boolean isSharing = false; + private volatile boolean clientStarted = false; + private final static String VERSION_ERROR_MSG = "You have an unsupported Java version."; + + private static Exception loadingException = null; + + public static void tryLoad() throws Exception { + if (loadingException != null) { + throw loadingException; + } else { + try { + Loader.load(org.bytedeco.javacpp.avutil.class); + Loader.load(org.bytedeco.javacpp.swresample.class); + Loader.load(org.bytedeco.javacpp.avcodec.class); + Loader.load(org.bytedeco.javacpp.avformat.class); + Loader.load(org.bytedeco.javacpp.swscale.class); + + /* initialize libavcodec, and register all codecs and formats */ + av_register_all(); + avformat_network_init(); + } catch (Throwable t) { + if (t instanceof Exception) { + throw loadingException = (Exception)t; + } else { + throw loadingException = new Exception("Failed to load " + BBBFFmpegFrameRecorder.class, t); + } + } + } + } + + static { + try { + tryLoad(); + } catch (Exception ex) { } + } + + private class DestroyJob implements PrivilegedExceptionAction { + public Object run() throws Exception { + System.out.println("Desktop Sharing Applet Destroy"); + if (clientStarted) { + client.stop(); + } + return null; + } + } + + @Override + public void init() { + + System.out.println("Desktop Sharing Applet Initializing"); + + String javaVersion = getParameter("JavaVersion"); + if (javaVersion != null && javaVersion != "") minJreVersion = javaVersion; + + hostValue = getParameter("IP"); + String port = getParameter("PORT"); + if (port != null) portValue = Integer.parseInt(port); + roomValue = getParameter("ROOM"); + + String scaleValue = getParameter("SCALE"); + if (scaleValue != null) scale = Double.parseDouble(scaleValue); + + + String captureFullScreen = getParameter("FULL_SCREEN"); + if (captureFullScreen != null) fullScreenValue = Boolean.parseBoolean(captureFullScreen); + + String rtmpURL = getParameter("rtmpURL"); + if (rtmpURL != null) url = rtmpURL; + + String tunnel = getParameter("HTTP_TUNNEL"); + if (tunnel != null) tunnelValue = Boolean.parseBoolean(tunnel); + try { + URL iconUrl = new URL(getCodeBase(), "bbb.gif"); + icon = ImageIO.read(iconUrl); + } catch (IOException e) { + } + + displaySystemProperties(); + + + } + + + private void displaySystemProperties() { + System.out.println("Java temp dir : " + System.getProperty("java.io.tmpdir")); + System.out.println("Java name : " + System.getProperty("java.vm.name")); + System.out.println("OS name : " + System.getProperty("os.name")); + System.out.println("OS arch : " + System.getProperty("os.arch")); + System.out.println("JNA Path : " + System.getProperty("jna.library.path")); + } + + private String getJavaVersionRuntime() { + return System.getProperty("java.version"); + } + + /** + * Create the GUI and show it. For thread safety, + * this method should be invoked from the + * event-dispatching thread. + */ + private void createAndShowGUI(final String warning) { + JOptionPane.showMessageDialog(null, + warning, + "Java Version Error", + JOptionPane.ERROR_MESSAGE); + stop(); + } + + private void displayJavaWarning(final String warning) { + //Schedule a job for the event-dispatching thread: + //creating and showing this application's GUI. + javax.swing.SwingUtilities.invokeLater(new Runnable() { + public void run() { + createAndShowGUI(warning); + } + }); + } + + @Override + public void start() { + System.out.println("Desktop Sharing Applet Starting"); + super.start(); + String javaRuntimeVersion = getJavaVersionRuntime(); + System.out.println("**** JAVA VERSION = [" + javaRuntimeVersion + "]"); + + allowDesktopSharing(); + } + + + private void allowDesktopSharing() { + client = new DeskshareClient.NewBuilder().host(hostValue).port(portValue) + .meetingId(roomValue).captureWidth(cWidthValue) + .captureHeight(cHeightValue).scaleWidth(sWidthValue).scaleHeight(sHeightValue) + .quality(qualityValue).autoScale(scale) + .x(xValue).y(yValue).fullScreen(fullScreenValue).withURL(url) + .httpTunnel(tunnelValue).trayIcon(icon).enableTrayIconActions(false).build(); + client.addClientListener(this); + + clientStarted = true; + + client.start(); + } + + + @Override + public void destroy() { + /* We make this a privileged job. + * The privileges of the javascript code are 'anded' with the + * java privs. Sometimes (depending on jre version, browser, etc.) + * javascript will not have the privs to do some of the operations + * required for destroy, particularly network related activities, + * but java does. So we make sure here that we run only considering + * java privs, not javascript's. This should be 'security safe', since + * we are only shutting things down. + */ + try { + AccessController.doPrivileged( this.new DestroyJob() ); + } catch ( PrivilegedActionException e) { + System.out.println("Exception during Desktop Sharing Applet Stopping"+e.toString()); + UncheckedExceptions.spit((Exception) e.getException()); + } + super.destroy(); + } + + @Override + public void stop() { + System.out.println("Desktop Sharing Applet Stopping"); + if (clientStarted) { + client.stop(); + } + + super.stop(); + } + + public void onClientStop(ExitCode reason) { + // determine if client is disconnected _PTS_272_ + if ( ExitCode.CONNECTION_TO_DESKSHARE_SERVER_DROPPED == reason ){ + JFrame pframe = new JFrame("Desktop Sharing Disconneted"); + if ( null != pframe ){ + client.disconnected(); + JOptionPane.showMessageDialog(pframe, + "Disconnected. Reason: Lost connection to the server." + reason , + "Disconnected" ,JOptionPane.ERROR_MESSAGE ); + }else{ + System.out.println("Desktop sharing allocate memory failed."); + } + }else{ + client.stop(); + } + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareClient.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareClient.java new file mode 100755 index 0000000000000000000000000000000000000000..1b4c3cb217c0ff37e15490d8e8aa2f0d8eccecb0 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareClient.java @@ -0,0 +1,266 @@ +/** + * 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.screenshare.client; + +import java.awt.Image; +import java.awt.Toolkit; + +public class DeskshareClient { + public static final String NAME = "DESKSHARECLIENT: "; + + private ScreenShareInfo ssi; + private ClientListener listener; + private ScreenSharer screenSharer; + + public void addClientListener(ClientListener l) { + listener = l; + } + + public void start() { + if (ssi.fullScreen) { + System.out.println(NAME + "Sharing full screen."); + shareFullScreen(); + } else { + System.out.println(NAME + "Sharing region of screen."); + shareWithFrame(); + } + } + + private void shareWithFrame() { + screenSharer = new ScreenRegionSharer(ssi); + screenSharer.addClientListener(listener); + screenSharer.start(false); + } + + private void shareFullScreen() { + screenSharer = new ScreenRegionSharer(ssi); + screenSharer.addClientListener(listener); + screenSharer.start(true); + } + + public void disconnected(){ + System.out.println(NAME + "Disconneted"); + screenSharer.disconnected(); + } + + public void stop() { + System.out.println(NAME + "Stop"); + screenSharer.stop(); + } + + private DeskshareClient(ScreenShareInfo ssi) { + this.ssi = ssi; + } + + + /******************************************** + * Helper class + ********************************************/ + + /** + * Builds the Deskstop Sharing Client. + */ + public static class NewBuilder { + private String host = "localhost"; + private int port = 9123; + private String meetingId = "default-room"; + private String streamId = ""; + private String codecOptions = ""; + private int captureWidth = 0; + private int captureHeight = 0; + private int scaleWidth = 0; + private int scaleHeight = 0; + private boolean quality = false; + private double scale = 1; + private int x = -1; + private int y = -1; + private boolean httpTunnel = true; + private Image sysTrayIcon; + private boolean enableTrayActions = false; + private boolean fullScreen = false; + private String URL = "rtmp://192.168.23.23/live/foo/room2"; + + public NewBuilder host(String host) { + this.host = host; + return this; + } + + public NewBuilder port(int port) { + this.port = port; + return this; + } + + public NewBuilder meetingId(String meetingId) { + this.meetingId = meetingId; + return this; + } + + public NewBuilder streamId(String streamId) { + this.streamId = streamId; + return this; + } + + public NewBuilder codecOptions(String options) { + this.codecOptions = options; + return this; + } + + public NewBuilder captureWidth(int width) { + this.captureWidth = width; + return this; + } + + public NewBuilder captureHeight(int height) { + this.captureHeight = height; + return this; + } + + public NewBuilder scaleWidth(int width) { + this.scaleWidth = width; + return this; + } + + public NewBuilder scaleHeight(int height) { + this.scaleHeight = height; + return this; + } + + public NewBuilder quality(boolean quality) { + this.quality = quality; + return this; + } + + public NewBuilder autoScale(double scaleTo) { + this.scale = scaleTo; + return this; + } + + public NewBuilder x(int x) { + this.x = x; + return this; + } + + public NewBuilder y(int y) { + this.y = y; + return this; + } + + public NewBuilder httpTunnel(boolean httpTunnel) { + this.httpTunnel = httpTunnel; + return this; + } + + public NewBuilder fullScreen(boolean fullScreen) { + this.fullScreen = fullScreen; + return this; + } + + public NewBuilder withURL(String url) { + this.URL = url; + return this; + } + + public NewBuilder trayIcon(Image icon) { + this.sysTrayIcon = icon; + return this; + } + + public NewBuilder enableTrayIconActions(boolean enableActions) { + enableTrayActions = enableActions; + return this; + } + + public DeskshareClient build() { + if (fullScreen) { + System.out.println("Sharing full screen."); + setupFullScreen(); + } else { + System.out.println("Sharing region screen."); + setupCaptureRegion(); + } + + ScreenShareInfo ssi = new ScreenShareInfo(); + ssi.host = host; + ssi.port = port; + ssi.meetingId = meetingId; + ssi.streamId = streamId; + ssi.captureWidth = captureWidth; + ssi.captureHeight = captureHeight; + ssi.scaleWidth = scaleWidth; + ssi.scaleHeight = scaleHeight; + ssi.quality = quality; + ssi.scale = scale; + ssi.x = x; + ssi.y = y; + ssi.httpTunnel = httpTunnel; + ssi.fullScreen = fullScreen; + ssi.URL = URL; + ssi.codecOptions = codecOptions; + ssi.sysTrayIcon = sysTrayIcon; + ssi.enableTrayActions = enableTrayActions; + + System.out.println("ScreenShareInfo[captureWidth=" + captureWidth + ",captureHeight=" + captureHeight + "][" + x + "," + y +"]" + + "[scaleWidth=" + scaleWidth + ",scaleHeight=" + scaleHeight + "]"); + + return new DeskshareClient(ssi); + } + + private void setupCaptureRegion() { + if (captureWidth > 0 && captureHeight > 0) { + java.awt.Dimension fullScreenSize = Toolkit.getDefaultToolkit().getScreenSize(); + x = ((int) fullScreenSize.getWidth() - captureWidth) / 2; + y = ((int) fullScreenSize.getHeight() - captureHeight) / 2; + System.out.println("Info[" + captureWidth + "," + captureHeight + "][" + x + "," + y +"]" + + "[" + fullScreenSize.getWidth() + "," + fullScreenSize.getHeight() + "]"); + scaleWidth = captureWidth; + scaleHeight = captureHeight; + } + } + + private void setupFullScreen() { + java.awt.Dimension fullScreenSize = Toolkit.getDefaultToolkit().getScreenSize(); + captureWidth = (int) fullScreenSize.getWidth(); + captureHeight = (int) fullScreenSize.getHeight(); + + x = 0; + y = 0; + + if (scale > 0 && scale <= 0.8) { + scaleWidth = (int)(scale * (double)captureWidth); + scaleHeight = (int)(scale * (double)captureHeight); + } + + System.out.println("Check for scaling[" + captureWidth + "," + captureHeight +"][" + scaleWidth + "," + scaleHeight + "]"); + + if (scale == 1) { + scaleWidth = captureWidth; + scaleHeight = captureHeight; + } else { + if (scaleWidth > 1280) { + scaleWidth = 1280; + double ratio = (double)captureHeight/(double)captureWidth; + scaleHeight = (int)((double)scaleWidth * ratio); + System.out.println("Scaling[" + captureWidth + "," + captureHeight +"][" + scaleWidth + "," + scaleHeight + "]"); + } + } + + } + + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareMain.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareMain.java new file mode 100755 index 0000000000000000000000000000000000000000..9400f79c0ee9c0aa91314813a67b7211476bd740 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareMain.java @@ -0,0 +1,233 @@ +/** + * 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.screenshare.client; + +import jargs.gnu.CmdLineParser; +import jargs.gnu.CmdLineParser.Option; + +import java.awt.Image; +import java.awt.Toolkit; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import javax.imageio.ImageIO; +import javax.swing.JOptionPane; + +public class DeskshareMain implements ClientListener, LifeLineListener { + private final BlockingQueue<ExitCode> exitReasonQ = new LinkedBlockingQueue<ExitCode>(5); + + private List<String> optionHelpStrings = new ArrayList<String>(); + private static LifeLine lifeline; + private static DeskshareClient client; + + private Option addHelp(Option option, String helpString) { + optionHelpStrings.add(" -" + option.shortForm() + ", --" + option.longForm() + ": " + helpString); + return option; + } + + private void printUsage() { + System.err.println("usage: deskshare [options]"); + for (Iterator<String> i = optionHelpStrings.iterator(); i.hasNext(); ) { + System.err.println(i.next()); + } + } + + public static void main(String[] args) { + DeskshareMain dsMain = new DeskshareMain(); + CmdLineParser parser = new CmdLineParser(); + + CmdLineParser.Option host = dsMain.addHelp(parser.addStringOption('s', "server"), serverHelpText); + CmdLineParser.Option port = dsMain.addHelp(parser.addIntegerOption('p', "port"),"The port the application is listening"); + CmdLineParser.Option listenPort = dsMain.addHelp(parser.addIntegerOption('l', "listenPort"),"Port to listen for lifeline"); + CmdLineParser.Option room = dsMain.addHelp(parser.addStringOption('r', "room"),"Room"); + CmdLineParser.Option cWidth = dsMain.addHelp(parser.addIntegerOption('w', "captureWidth"),"Width of the screen capture"); + CmdLineParser.Option cHeight = dsMain.addHelp(parser.addIntegerOption('t', "captureHeight"),"Height of the screen capture"); + CmdLineParser.Option sWidth = dsMain.addHelp(parser.addIntegerOption('d', "scaleWidth"),"Scale capture width"); + CmdLineParser.Option sHeight = dsMain.addHelp(parser.addIntegerOption('g', "scaleHeight"),"Scale capture height"); + CmdLineParser.Option xCoord = dsMain.addHelp(parser.addIntegerOption('x', "x"),"Upper-left x coordinate of the screen capture"); + CmdLineParser.Option yCoord = dsMain.addHelp(parser.addIntegerOption('y', "y"),"Upper-left y coordinate of the screen capture"); + CmdLineParser.Option tryHttpTunnel = dsMain.addHelp(parser.addBooleanOption('n', "httptunnel"),"Http tunnel if direct connection fails"); + CmdLineParser.Option icon = dsMain.addHelp(parser.addStringOption('i', "icon"),"Path to system tray icon file"); + CmdLineParser.Option help = dsMain.addHelp(parser.addBooleanOption('h', "help"),"Show this help message"); + CmdLineParser.Option fullScreen = dsMain.addHelp(parser.addBooleanOption('f', "full-screen"),"Capture the full screen."); + + + try { + parser.parse(args); + } catch (CmdLineParser.OptionException e) { + System.err.println(e.getMessage()); + dsMain.printUsage(); + System.exit(2); + } + + if (Boolean.TRUE.equals(parser.getOptionValue(help))) { + dsMain.printUsage(); + System.exit(0); + } + + // Extract the values entered for the various options -- if the + // options were not specified, the corresponding values will be + // the default. + Integer portValue = (Integer)parser.getOptionValue(port, new Integer(9123)); + Integer listenPortValue = (Integer)parser.getOptionValue(listenPort, new Integer(9125)); + Integer cWidthValue = (Integer)parser.getOptionValue(cWidth, new Integer(801)); + Integer cHeightValue = (Integer)parser.getOptionValue(cHeight, new Integer(601)); + + Integer sWidthValue = (Integer)parser.getOptionValue(sWidth, new Integer(800)); + Integer sHeightValue = (Integer)parser.getOptionValue(sHeight, new Integer(600)); + + Integer xValue = (Integer)parser.getOptionValue(xCoord, new Integer(0)); + Integer yValue = (Integer)parser.getOptionValue(yCoord, new Integer(0)); + Boolean tunnelValue = (Boolean)parser.getOptionValue(tryHttpTunnel, new Boolean(false)); + String iconValue = (String)parser.getOptionValue(icon, ""); + + String url = null; + String meetingId = null; + String streamId = null; + String serverUrl = null; + Boolean captureFullScreen = false; + String codecOptions = null; + + if(args != null && args.length == 7) { + System.out.println("Using passed args: length=[" + args.length + "]"); + url = args[0]; + serverUrl = args[1]; + meetingId = args[2]; + streamId = args[3]; + captureFullScreen = Boolean.parseBoolean(args[4]); + + System.out.println("Using passed args: [" + url + "] meetingId=[" + meetingId + "] streamId=[" + streamId + "] captureFullScreen=" + captureFullScreen); + codecOptions = args[5]; + + String errorMessage = args[6]; + + if (! errorMessage.equalsIgnoreCase("NO_ERRORS")) { + dsMain.displayJavaWarning(errorMessage); + } else { + Image image = null; + if (iconValue.isEmpty()) { + try { + image = ImageIO.read(dsMain.getClass().getResourceAsStream("/images/bbb.gif")); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } else { + image = Toolkit.getDefaultToolkit().getImage("bbb.gif"); + } + + + dsMain.displaySystemProperties(); + + lifeline = new LifeLine(listenPortValue.intValue(), dsMain); + lifeline.listen(); + + client = new DeskshareClient.NewBuilder().host(serverUrl).port(portValue) + .meetingId(meetingId).streamId(streamId).captureWidth(cWidthValue) + .captureHeight(cHeightValue).scaleWidth(sWidthValue).scaleHeight(sHeightValue) + .quality(true).autoScale(0).codecOptions(codecOptions) + .x(xValue).y(yValue).fullScreen(captureFullScreen).withURL(url) + .httpTunnel(tunnelValue).trayIcon(image).enableTrayIconActions(true).build(); + + client.addClientListener(dsMain); + client.start(); + + try { + System.out.println("Waiting for trigger to Stop client."); + ExitCode reason = dsMain.exitReasonQ.take(); + System.out.println("Stopping Java Web Start."); + client.stop(); + lifeline.disconnect(); + System.exit(reason.getExitCode()); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + System.exit(500); + } + } + + } else { + System.out.println("Using default args: [" + url + "] width=[" + cWidthValue + "] height=[" + cHeightValue + "]"); + System.out.println("args null =[" + (args == null) + "] args.length=[" + args.length + "]"); + dsMain.displayJavaWarning("Invalid number of arguments."); + } + + } + + + /** + * Create the GUI and show it. For thread safety, + * this method should be invoked from the + * event-dispatching thread. + */ + private void createAndShowGUI(final String warning) { + JOptionPane.showMessageDialog(null, + warning, + "Java Version Error", + JOptionPane.ERROR_MESSAGE); + } + + private void displayJavaWarning(final String warning) { + //Schedule a job for the event-dispatching thread: + //creating and showing this application's GUI. + javax.swing.SwingUtilities.invokeLater(new Runnable() { + public void run() { + createAndShowGUI(warning); + } + }); + } + + private void displaySystemProperties() { + System.out.println("========== SYSTEM PROPERTIES ================ "); + System.out.println("Java temp dir : " + System.getProperty("java.io.tmpdir")); + System.out.println("Java name : " + System.getProperty("java.vm.name")); + System.out.println("OS name : " + System.getProperty("os.name")); + System.out.println("OS arch : " + System.getProperty("os.arch")); + System.out.println("JNA Path : " + System.getProperty("jna.library.path")); + System.out.println("========== END SYSTEM PROPERTIES ================ "); + } + + public void onClientStop(ExitCode reason) { + queueExitCode(reason); + } + + @Override + public void disconnected(ExitCode reason) { + queueExitCode(reason); + } + + private void queueExitCode(ExitCode reason) { + try { + // System.out.println("Trigger stop client ." + exitReasonQ.remainingCapacity()); + exitReasonQ.put(reason); + System.out.println("Triggered stop client."); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + client.stop(); + lifeline.disconnect(); + System.exit(reason.getExitCode()); + } + } + + private static final String serverHelpText = "\n\t The host or IP of the desktop sharing server. Default is localhost."; +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareShell.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareShell.java new file mode 100755 index 0000000000000000000000000000000000000000..4d79276be55b8c89f6492d9c8c0fc72cff79b66b --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareShell.java @@ -0,0 +1,47 @@ +/** + * 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.screenshare.client; + +import java.io.IOException; + +/* + * An application that calls the VBScript to test if the application is returning the exit codes. + */ +public class DeskshareShell { + + public static void main(String[] args) { + String COMMAND = "wscript deskshare.vbs -s 192.168.0.120 -r 6e87dfef-9f08-4f80-993f-c0ef5f7b999b"; + + Process process; + try { + process = Runtime.getRuntime().exec(COMMAND); + // Wait for the process to finish. + int exitValue = process.waitFor(); + System.out.println("Exit Value " + exitValue + " while for " + COMMAND); + if (exitValue != 0) { + System.out.println("Exit Value != 0 while for " + COMMAND); + } + } catch (IOException e) { + System.out.println("IOException while processing " + COMMAND); + } catch (InterruptedException e) { + System.out.println("InterruptedException while processing " + COMMAND); + } + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareSystemTray.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareSystemTray.java new file mode 100755 index 0000000000000000000000000000000000000000..f1dd6bab72fb72683f2377c2d4d7c757cdbd2181 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/DeskshareSystemTray.java @@ -0,0 +1,121 @@ +/** + * 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.screenshare.client; + +import java.awt.*; +import java.awt.event.*; + +public class DeskshareSystemTray { + private SystemTrayListener listener; + private TrayIcon trayIcon = null; + private SystemTray tray = null; + + public void addSystemTrayListener(SystemTrayListener l) { + listener = l; + } + + public void displayIconOnSystemTray(final Image image, final boolean enableActions) { + Runnable runner = new Runnable() { + public void run() { + if (SystemTray.isSupported()) { + tray = SystemTray.getSystemTray(); + + PopupMenu popup = new PopupMenu(); + trayIcon = new TrayIcon(image, "Sharing Desktop", popup); + + if (enableActions) { + MenuItem stopItem = new MenuItem("Stop Sharing"); + stopItem.addActionListener(new StopSharingListener( + trayIcon, "Stop Desktop Sharing", "Stop sharing your desktop", TrayIcon.MessageType.INFO)); + popup.add(stopItem); + } + + + try { + tray.add(trayIcon); + trayIcon.displayMessage("Sharing Desktop", "You are now sharing your desktop", TrayIcon.MessageType.INFO); + } catch (AWTException e) { + System.err.println("Can't add to tray"); + } + } else { + System.err.println("Tray unavailable"); + } + } + }; + EventQueue.invokeLater(runner); + } + + /***************************************************************************** + ; disconnectIconSystemTrayMessage + ;---------------------------------------------------------------------------- + ; DESCRIPTION + ; This routine is used to change icon system tray message string + ; to disconnect. + ; + ; RETURNS : N/A + ; + ; INTERFACE NOTES + ; + ; INPUT : N/A + ; + ; OUTPUT : N/A + ; + ; IMPLEMENTATION + ; + ; HISTORY + ; __date__ : PTS: + ; 2010.11.19 problem 272 + ; + ******************************************************************************/ + public void disconnectIconSystemTrayMessage(){ + trayIcon.setToolTip("Disconnected"); + trayIcon.displayMessage("Deskshare Disconnected" , + "You're disconnected from desktop sharing", + TrayIcon.MessageType.ERROR); + } // END FUNCTION disconnectIconSystemTrayMessage + + public void removeIconFromSystemTray() { + if (tray != null && trayIcon != null) { + tray.remove(trayIcon); + } + } + + class StopSharingListener implements ActionListener { + TrayIcon trayIcon; + String title; + String message; + TrayIcon.MessageType messageType; + + StopSharingListener(TrayIcon trayIcon, String title, + String message, TrayIcon.MessageType messageType) { + this.trayIcon = trayIcon; + this.title = title; + this.message = message; + this.messageType = messageType; + } + + public void actionPerformed(ActionEvent e) { + trayIcon.displayMessage(title, message, messageType); + if (listener != null) { + listener.onStopSharingSysTrayMenuClicked(); + } + } + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ExitCode.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ExitCode.java new file mode 100755 index 0000000000000000000000000000000000000000..c83c6f119aa140e70f99c61126472ab0d892c735 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ExitCode.java @@ -0,0 +1,37 @@ +/** + * 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.screenshare.client; + +public enum ExitCode { + NORMAL(0), + BAD_PARAMETERS(400), + CANNOT_CONNECT_TO_LIFELINE(401), + ERROR_ON_LIFELINE_CONNECTION(402), + CONNECTION_TO_DESKSHARE_SERVER_DROPPED(403), + CANNOT_BIND_TO_LIFELINE_PORT(404), + LIFELINE_CONNECTION_CLOSED(405), + INTERNAL_ERROR(500), + DESKSHARE_SERVICE_UNAVAILABLE(503); + + private final int exitValue; + + ExitCode(int code) { this.exitValue = code; } + + public int getExitCode() {return exitValue;} +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/LifeLine.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/LifeLine.java new file mode 100755 index 0000000000000000000000000000000000000000..c8e6aced7ec56e9da5e10c0c42ec9e888a90f8d5 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/LifeLine.java @@ -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.screenshare.client; + +import java.net.*; +import java.io.*; + +public class LifeLine { + + private int port; + private ServerSocket serverSocket = null; + private Socket clientSocket = null; + private boolean connected = false; + private PrintWriter out = null; + private BufferedReader in = null; + private LifeLineListener listener = null; + private Thread lifeLineThread; + private LifeLineServer lifeLineServer; + + public LifeLine(int port, LifeLineListener listener) { + this.port = port; + this.listener = listener; + } + + public void listen() { + + lifeLineServer = new LifeLineServer(); + lifeLineThread = new Thread(lifeLineServer, "LifeLineServer"); + lifeLineThread.start(); + } + + public void disconnect() { + lifeLineServer.close(); + } + + private void notifyListener(ExitCode reason) { + if (listener != null) listener.disconnected(reason); + } + + private class LifeLineServer implements Runnable { + @Override + public void run() { + try { + serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress("127.0.0.1", port)); + } catch (IOException e) { + System.err.println("Could not listen on port: " + port); + notifyListener(ExitCode.CANNOT_BIND_TO_LIFELINE_PORT); + } + + try { + System.out.println("Starting listener on [" + serverSocket.getInetAddress() + ":" + port + "]"); + clientSocket = serverSocket.accept(); + clientSocket.setKeepAlive(true); + out = new PrintWriter(clientSocket.getOutputStream(), true); + in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + String inputLine; + connected = true; + while ((inputLine = in.readLine()) != null) { + // do nothing + } + } catch (IOException e) { + System.out.println("IOException listener"); + System.err.println("Accept failed."); + notifyListener(ExitCode.ERROR_ON_LIFELINE_CONNECTION); + } + close(); + System.out.println("Stopped listener"); + notifyListener(ExitCode.LIFELINE_CONNECTION_CLOSED); + } + + public void close() { + try { + if (out != null) out.close(); + if (in != null) in.close(); + if (clientSocket != null) clientSocket.close(); + if (serverSocket != null) serverSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/LifeLineListener.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/LifeLineListener.java new file mode 100755 index 0000000000000000000000000000000000000000..9b7ab26130e1b4176b87d98d920194621ea25491 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/LifeLineListener.java @@ -0,0 +1,24 @@ +/** + * 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.screenshare.client; + +public interface LifeLineListener { + + public void disconnected(ExitCode reason); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCapture.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCapture.java new file mode 100755 index 0000000000000000000000000000000000000000..a148a8ef2a358a700311fc0b9479e5060fa3dd71 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCapture.java @@ -0,0 +1,159 @@ +/** + * 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.screenshare.client; + +import java.awt.AWTException; +import java.awt.Graphics2D; +import java.awt.HeadlessException; +import java.awt.Image; +import java.awt.MouseInfo; +import java.awt.Point; +import java.awt.PointerInfo; +import java.awt.Rectangle; +import java.awt.Robot; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import javax.imageio.ImageIO; + +/** + * The Capture class uses the java Robot class to capture the screen. + * @author Snap + * + */ +public class ScreenCapture { + private Robot robot; + private Rectangle screenBounds; + private int scaleWidth, scaleHeight, x, y, captureWidth, captureHeight; + private Point curMouseLocation = new Point(Integer.MIN_VALUE, Integer.MIN_VALUE); + + private Image cursor; + + public ScreenCapture(int x, int y, int captureWidth, int captureHeight, int scaleWidth, int scaleHeight) { + this.x = x; + this.y = y; + this.captureWidth = captureWidth; + this.captureHeight = captureHeight; + + try{ + robot = new Robot(); + }catch (AWTException e){ + System.out.println(e.getMessage()); + } + + this.screenBounds = new Rectangle(x, y, this.captureWidth, this.captureHeight); + this.scaleWidth = scaleWidth; + this.scaleHeight = scaleHeight; + + try { + cursor = ImageIO.read(getClass().getResourceAsStream("/images/Cursor.png")); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public BufferedImage takeSingleSnapshot() { + BufferedImage capturedImage = robot.createScreenCapture(this.screenBounds); + +// System.out.println("ScreenCapture snap: [cw=" + captureWidth + ",ch=" + captureHeight + "] at [x=" + x + ",y=" + y +"]" +// + "[sw==" + scaleWidth + ",sh=" + scaleHeight + "]"); + + BufferedImage currentScreenshot = new BufferedImage(capturedImage.getWidth(), capturedImage.getHeight(), BufferedImage.TYPE_3BYTE_BGR); + currentScreenshot.getGraphics().drawImage(capturedImage, 0, 0, null); + + Point mouseLoc = takeMouseLocation(); + int x = mouseLoc.x; + int y = mouseLoc.y; + + Graphics2D graphics2D = currentScreenshot.createGraphics(); + graphics2D.drawImage(cursor, x, y, 16, 16, null); // cursor.gif is 16x16 size. + + return currentScreenshot; + } + + public void setX(int x) { + this.x = x; + updateBounds(); + } + + public void setY(int y) { + this.y = y; + updateBounds(); + } + + public void setWidth(int width) { + this.captureWidth = width; + updateBounds(); + } + + public void setHeight(int height) { + this.captureHeight = height; + updateBounds(); + } + + public void updateBounds() { + this.screenBounds = new Rectangle(x, y, captureWidth, captureHeight); + } + + private Point getMouseLocation() { + PointerInfo pInfo; + Point pointerLocation = new Point(0,0); + + try { + pInfo = MouseInfo.getPointerInfo(); + } catch (HeadlessException e) { + pInfo = null; + } catch (SecurityException e) { + pInfo = null; + } + + if (pInfo == null) return pointerLocation; + + return pInfo.getLocation(); + } + + private Point calculatePointerLocation(Point p) { + // System.out.println("Mouse Tracker:: Image=[" + captureWidth + "," + captureHeight + "] scale=[" + scaleWidth + "," + scaleHeight + "]"); + + int mouseXInCapturedRegion = p.x - x; + int mouseYInCapturedRegion = p.y - y; + + double scaledMouseX = mouseXInCapturedRegion * (double)((double)scaleWidth / (double)captureWidth); + double scaledMouseY = mouseYInCapturedRegion * (double)((double)scaleHeight / (double)captureHeight); + + return new Point((int)scaledMouseX, (int)scaledMouseY); + } + + private Point takeMouseLocation() { + Point mouseLocation = getMouseLocation(); + if (isMouseInsideCapturedRegion(mouseLocation)) { + // System.out.println("Mouse is inside captured region [" + mouseLocation.x + "," + mouseLocation.y + "]"); + curMouseLocation = calculatePointerLocation(mouseLocation); + } + + return curMouseLocation; + } + + private boolean isMouseInsideCapturedRegion(Point p) { + return true; + // return ( ( (p.x > captureX) && (p.x < (captureX + captureWidth) ) ) + // && (p.y > captureY && p.y < captureY + captureHeight)); + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCaptureListener.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCaptureListener.java new file mode 100755 index 0000000000000000000000000000000000000000..8e41b4458d0aba4d630ad3f9d273c60851f62b13 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCaptureListener.java @@ -0,0 +1,26 @@ +/** + * 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.screenshare.client; + +import java.awt.image.BufferedImage; + +public interface ScreenCaptureListener { + + void onScreenCaptured(BufferedImage screen); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCaptureTaker.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCaptureTaker.java new file mode 100755 index 0000000000000000000000000000000000000000..8804940b245d59c773d980146d29d2b8823270d6 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenCaptureTaker.java @@ -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 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.screenshare.client; + +import java.awt.image.BufferedImage; + +public class ScreenCaptureTaker { + private ScreenCapture capture; + + public ScreenCaptureTaker(int x, int y, int captureWidth, int captureHeight, int scaleWidth, int scaleHeight){ + capture = new ScreenCapture(x, y, captureWidth, captureHeight, scaleWidth, scaleHeight); + } + + public void setCaptureCoordinates(int x, int y){ + capture.setX(x); + capture.setY(y); + } + + public BufferedImage captureScreen() { + // System.out.println("----- Taking screen capture -----"); + long start = System.currentTimeMillis(); + BufferedImage image = capture.takeSingleSnapshot(); + long end = System.currentTimeMillis(); + // System.out.println("Capture took " + (end - start) + " millis"); + return image; + } + + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenRegionSharer.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenRegionSharer.java new file mode 100755 index 0000000000000000000000000000000000000000..e7170da67b1a5d674fd61a3f2ba6a0053e034034 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenRegionSharer.java @@ -0,0 +1,120 @@ +/** + * 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.screenshare.client; + +import org.bigbluebutton.screenshare.client.frame.CaptureRegionFrame; +import org.bigbluebutton.screenshare.client.frame.CaptureRegionListener; +import org.bigbluebutton.screenshare.client.net.NetworkConnectionListener; +import org.bigbluebutton.screenshare.client.net.NetworkStreamSender; + +public class ScreenRegionSharer implements ScreenSharer, NetworkConnectionListener { + public static final String NAME = "SCREENREGIONSHARER: "; + + private final ScreenShareInfo ssi; + private ScreenSharerRunner sharer; + private CaptureRegionFrame frame; + private NetworkStreamSender signalChannel; + private DeskshareSystemTray tray = new DeskshareSystemTray(); + private ClientListener listener; + + public ScreenRegionSharer(ScreenShareInfo ssi) { + signalChannel = new NetworkStreamSender(ssi.host, ssi.meetingId, ssi.streamId); + signalChannel.addNetworkConnectionListener(this); + signalChannel.start(); + this.ssi = ssi; + sharer = new ScreenSharerRunner(ssi); + } + + public void start(boolean autoStart) { + CaptureRegionListener crl = new CaptureRegionListenerImp(this); + frame = new CaptureRegionFrame(crl, 5); + frame.setHeight(ssi.captureHeight); + frame.setWidth(ssi.captureWidth); + frame.setLocation(ssi.x, ssi.y); + System.out.println(NAME + "Launching Screen Capture Frame"); + frame.start(autoStart); + } + + public void addClientListener(ClientListener l) { + listener = l; + SystemTrayListener systrayListener = new SystemTrayListenerImp(listener); + tray.addSystemTrayListener(systrayListener); + tray.displayIconOnSystemTray(ssi.sysTrayIcon, ssi.enableTrayActions); + } + + public void disconnected(){ + frame.setVisible(false); + sharer.disconnectSharing(); + System.out.println(NAME + "Change system tray icon message"); + tray.disconnectIconSystemTrayMessage(); + System.out.println(NAME + "Desktop sharing disconneted"); + } + + public void stop() { + frame.setVisible(false); + sharer.stopSharing(); + signalChannel.stopSharing(); + tray.removeIconFromSystemTray(); + System.out.println(NAME + "Closing Screen Capture Frame"); + } + + @Override + public void networkConnectionException(ExitCode reason) { + if (listener != null) listener.onClientStop(reason); + } + + private class CaptureRegionListenerImp implements CaptureRegionListener { + private final ScreenRegionSharer srs; + + public CaptureRegionListenerImp(ScreenRegionSharer srs) { + this.srs = srs; + } + + @Override + public void onCaptureRegionMoved(int x, int y) { + ssi.x = x; + ssi.y = y; + if (sharer != null) + sharer.setCaptureCoordinates(x, y); + } + + @Override + public void onStartCapture(int x, int y, int width, int height) { + ssi.x = x; + ssi.y = y; + ssi.captureWidth = width; + ssi.captureHeight = height; + ssi.scaleWidth = width; + ssi.scaleHeight = height; + sharer.updateScreenShareInfo(x, y, width, height); + + signalChannel.startSharing(width, height); + sharer.addClientListener(listener); + + sharer.startSharing(); + } + + @Override + public void onStopCapture() { + srs.stop(); + } + } + + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenShareInfo.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenShareInfo.java new file mode 100755 index 0000000000000000000000000000000000000000..b07ebd5ba90eb59d7841e822fd5bb5f14b364e93 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenShareInfo.java @@ -0,0 +1,42 @@ +/** + * 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.screenshare.client; + +import java.awt.Image; + +public class ScreenShareInfo { + public String host; + public int port; + public String meetingId; + public String streamId; + public String codecOptions; + public int captureWidth; + public int captureHeight; + public int scaleWidth; + public int scaleHeight; + public boolean quality; + public double scale; + public int x; + public int y; + public boolean httpTunnel; + public boolean fullScreen; + public Image sysTrayIcon; + public boolean enableTrayActions; + public String URL; +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenSharer.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenSharer.java new file mode 100755 index 0000000000000000000000000000000000000000..db54a52d7e8e04465498c964a158ce61cf4dc3c9 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenSharer.java @@ -0,0 +1,26 @@ +/** + * 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.screenshare.client; + +public interface ScreenSharer { + void start(boolean autoStart); + void disconnected(); + void stop(); + void addClientListener(ClientListener l); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenSharerRunner.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenSharerRunner.java new file mode 100755 index 0000000000000000000000000000000000000000..a6266c981434a4cd8ba0166511fe66a8022c170e --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/ScreenSharerRunner.java @@ -0,0 +1,120 @@ +/** + * 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.screenshare.client; + +import java.awt.AWTException; +import java.io.IOException; +import org.bigbluebutton.screenshare.client.javacv.JavaCVScreenshare; + +public class ScreenSharerRunner { + public static final String NAME = "SCREENSHARERUNNER: "; + + boolean connected = false; + private boolean started = false; + private ScreenShareInfo ssi; + private int x, y, width, height; + private final JavaCVScreenshare jcs; + + public ScreenSharerRunner(ScreenShareInfo ssi) { + this.ssi = ssi; + +// System.out.println("ScreenSharerRunner[captureWidth=" + ssi.captureWidth + ",captureHeight=" + ssi.captureHeight + "][" + ssi.x + "," + ssi.y +"]" +// + "[scaleWidth=" + ssi.scaleWidth + ",scaleHeight=" + ssi.scaleHeight + "]"); + + jcs = new JavaCVScreenshare(ssi); + } + + public void updateScreenShareInfo(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public void startSharing() { +// printHeader(); + + try { + jcs.go(ssi.URL, x, y, width, height); + jcs.start(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (AWTException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (org.bigbluebutton.screenshare.client.javacv.BBBFrameRecorder.Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public void disconnectSharing(){ + System.out.println(NAME + "Disconneted"); + + jcs.stop(); + } // END FUNCTION disconnectSharing + + public void stopSharing() { + System.out.println(NAME + "Stopping"); + System.out.println(NAME + "Removing icon from system tray."); + + jcs.stop(); + } + + public void setCaptureCoordinates(int x, int y) { + jcs.setCaptureCoordinates(x, y); + } + + + public void addClientListener(ClientListener l) { + // NetworkConnectionListener netConnListener = new NetworkConnectionListenerImp(listener); + // if (sender != null) + // sender.addNetworkConnectionListener(netConnListener); + // else + // System.out.println(NAME + "ERROR - Cannot add listener to network connection."); + } + + private void printHeader() { + System.out.println("-----------------------------------------------------------------------"); + System.out.println(LICENSE_HEADER); + System.out.println("-----------------------------------------------------------------------\n\n"); + System.out.println("Desktop Sharing v0.9.0"); + System.out.println("Start"); + System.out.println("Connecting to " + ssi.host + ":" + ssi.port + " meetingId " + ssi.meetingId); + System.out.println("Sharing " + ssi.captureWidth + "x" + ssi.captureHeight + " at " + ssi.x + "," + ssi.y); + System.out.println("Scale to " + ssi.scaleWidth + "x" + ssi.scaleHeight + " with quality = " + ssi.quality); + // System.out.println("Http Tunnel: " + ssi.httpTunnel); + } + + private static final String LICENSE_HEADER = "This program is free software: you can redistribute it and/or modify\n" + + "it under the terms of the GNU Lesser General Public License as published by\n" + + "the Free Software Foundation, either version 3 of the License, or\n" + + "(at your option) any later version.\n\n" + + "This program is distributed in the hope that it will be useful,\n" + + "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + + "GNU General Public License for more details.\n\n" + + "You should have received a copy of the GNU Lesser General Public License\n" + + "along with this program. If not, see <http://www.gnu.org/licenses/>.\n\n" + + "Copyright 2010 BigBlueButton. All Rights Reserved.\n\n"; +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/SystemTrayListener.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/SystemTrayListener.java new file mode 100755 index 0000000000000000000000000000000000000000..c606296ea4ebeed94082d023ddadadaf6a73a056 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/SystemTrayListener.java @@ -0,0 +1,24 @@ +/** + * 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.screenshare.client; + +public interface SystemTrayListener { + + public void onStopSharingSysTrayMenuClicked(); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/SystemTrayListenerImp.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/SystemTrayListenerImp.java new file mode 100755 index 0000000000000000000000000000000000000000..fc094e9ab0f73a67b7ce289167bb092bb717d409 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/SystemTrayListenerImp.java @@ -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 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.screenshare.client; + +public class SystemTrayListenerImp implements SystemTrayListener { + + private final ClientListener listener; + + public SystemTrayListenerImp(ClientListener listener) { + this.listener = listener; + } + + @Override + public void onStopSharingSysTrayMenuClicked() { + listener.onClientStop(ExitCode.NORMAL); + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/UncheckedExceptions.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/UncheckedExceptions.java new file mode 100755 index 0000000000000000000000000000000000000000..449f1b9b086ba918f075cdb202b67d8f942d00c0 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/UncheckedExceptions.java @@ -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 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.screenshare.client; +class UncheckedExceptions { + private static Throwable throwable; + + private UncheckedExceptions() throws Throwable { + throw throwable; + } + + public static synchronized void spit(Throwable throwable) { + UncheckedExceptions.throwable = throwable; + try { + UncheckedExceptions.class.newInstance(); + } catch(InstantiationException e) { + } catch(IllegalAccessException e) { + } finally { + UncheckedExceptions.throwable = null; + } + } +} + diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/VersionCheckUtil.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/VersionCheckUtil.java new file mode 100755 index 0000000000000000000000000000000000000000..3586ef9faf2c87fac52663deb84a3c48869b7ccc --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/VersionCheckUtil.java @@ -0,0 +1,80 @@ +/** + * 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.screenshare.client; + +public class VersionCheckUtil { + + public static boolean validateMinJREVersion(String runtimeVersion, String minVersion){ + String[] requestedVersioning = minVersion.split("\\."); + String[] clientVersioning = runtimeVersion.split("\\."); + + if (requestedVersioning.length < 3 || clientVersioning.length < 3) + return false; + + // First major update + if (Integer.parseInt(clientVersioning[0]) > Integer.parseInt(requestedVersioning[0])) + return true; + else{ + // Checking Java version + if (Integer.parseInt(clientVersioning[1]) > Integer.parseInt(requestedVersioning[1])) + return true; + + // Checking update + else if (Integer.parseInt(clientVersioning[1]) == Integer.parseInt(requestedVersioning[1])){ + // non-GA or non-FCS release won't be supported + if(clientVersioning[2].indexOf("-") != -1) + return false; + + int rUpdatePart1 = 0; + int rUpdatePart2 = 0; + + int underbar = requestedVersioning[2].indexOf("_"); + if ( underbar == -1){ + rUpdatePart1 = Integer.parseInt(requestedVersioning[2]); + } else { + rUpdatePart1 = Integer.parseInt(requestedVersioning[2].substring(0, underbar)); + rUpdatePart2 = Integer.parseInt(requestedVersioning[2].substring(underbar + 1, requestedVersioning[2].length())); + } + + int cUpdatePart1 = 0; + int cUpdatePart2 = 0; + + underbar = clientVersioning[2].indexOf("_"); + if ( underbar == -1) { + cUpdatePart1 = Integer.parseInt(clientVersioning[2]); + } else { + cUpdatePart1 = Integer.parseInt(clientVersioning[2].substring(0, underbar)); + cUpdatePart2 = Integer.parseInt(clientVersioning[2].substring(underbar + 1, clientVersioning[2].length())); + } + + if (cUpdatePart1 > rUpdatePart1) + return true; + else if (cUpdatePart1 == rUpdatePart1) { + if (cUpdatePart2 > rUpdatePart2 || cUpdatePart2 == rUpdatePart2) + return true; + else + return false; + } else + return false; + } else + return false; + } + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/CaptureRegionFrame.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/CaptureRegionFrame.java new file mode 100755 index 0000000000000000000000000000000000000000..dc002f8c4543ab52616a6cf506b0784efb67bd8e --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/CaptureRegionFrame.java @@ -0,0 +1,164 @@ +/** + * 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.screenshare.client.frame; + +import java.awt.Button; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Graphics; +import java.awt.GridBagLayout; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.IOException; +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; + +public class CaptureRegionFrame { + private Button btnStartStop; + private CaptureRegionListener client; + private boolean capturing = false; + private WindowlessFrame frame; + private static final int RESIZE_BAR_SIZE = 40; + private static final int MOVE_BAR_SIZE = 60; + + private final String CLASS = "CaptureRegionFrame"; + + public CaptureRegionFrame(CaptureRegionListener client, int borderWidth) { + frame = new WindowlessFrame(borderWidth); + this.client = client; + frame.setCaptureRegionListener(client); + } + + public void setHeight(int h) { + frame.setHeight(h); + } + + public void setWidth(int w) { + frame.setWidth(w); + } + + public void setLocation(int x, int y) { + frame.setLocation(x, y); + } + + public void setVisible(boolean visible) { + frame.setVisible(visible); + } + + public void start(boolean autoStart) { + frame.setToolbar(createToolbar()); + frame.setResizeBar(createResizeBar()); + frame.setMoveBar(createMoveBar()); + setVisible(true); + if (autoStart) { + startCapture(); + } + } + + private JPanel createResizeBar(){ + final JPanel resizePanel = new JPanel(); + resizePanel.setPreferredSize(new Dimension(RESIZE_BAR_SIZE,RESIZE_BAR_SIZE)); + resizePanel.setBorder(BorderFactory.createLineBorder(Color.RED)); + resizePanel.setLayout(new GridBagLayout()); + BufferedImage resizeCursorImage = null; + + try { + // Image was taken from http://4.bp.blogspot.com/_fhb-4UuRH50/R1ZLryoIvJI/AAAAAAAAA6U/G3S-XYabULk/s1600/se-resize.gif + resizeCursorImage = ImageIO.read(getClass().getResourceAsStream("/images/resize-cursor.png")); + } catch (IOException e) { + e.printStackTrace(); + } + + JLabel resizePicLabel = new JLabel(new ImageIcon(resizeCursorImage)); + resizePanel.add(resizePicLabel); + return resizePanel; + } + + private JPanel createMoveBar() { + final CirclePanel movePanel = new CirclePanel(); + movePanel.setPreferredSize(new Dimension(MOVE_BAR_SIZE,MOVE_BAR_SIZE)); + movePanel.setLayout(new GridBagLayout()); + BufferedImage moveCursorImage = null; + + try { + // Image was taken from http://www.iconarchive.com/show/oxygen-icons-by-oxygen-icons.org/Actions-transform-move-icon.html + moveCursorImage = ImageIO.read(getClass().getResourceAsStream("/images/move-cursor.png")); + } catch (IOException e) { + e.printStackTrace(); + } + + JLabel movePicLabel = new JLabel(new ImageIcon(moveCursorImage)); + movePanel.add(movePicLabel); + return movePanel; + } + + // Wrap move panel in a circle + public class CirclePanel extends JPanel { + static final long serialVersionUID = 1L; + + @Override + protected void paintComponent(Graphics g) { + g.drawOval(0, 0, g.getClipBounds().width, g.getClipBounds().height); + } + } + + private JPanel createToolbar() { + final JPanel panel = new JPanel(); + panel.setBackground(Color.RED); + panel.setLayout(new FlowLayout()); + capturing = false; + btnStartStop = new Button("Start Sharing"); + btnStartStop.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + // if (capturing) { + // capturing = false; + // btnStartStop.setLabel("Start Capture"); + // stopCapture(); + // } else { + // capturing = true; + // btnStartStop.setLabel("Stop Capture"); + startCapture(); + // } + } + }); + panel.add(btnStartStop); + return panel; + } + + private void startCapture() { + System.out.println(CLASS + " - startCapture" ); + frame.changeBorderToBlue(); + System.out.println(CLASS + " - startCapture:: Change border to blue" ); + frame.removeResizeListeners(); + Rectangle rect = frame.getFramedRectangle(); + client.onStartCapture(rect.x, rect.y, frame.getWidth(), frame.getHeight()); + } + + private void stopCapture() { + frame.changeBorderToRed(); + client.onStopCapture(); + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/CaptureRegionListener.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/CaptureRegionListener.java new file mode 100755 index 0000000000000000000000000000000000000000..5a4784d8dbd8482bccdcbba45db20e8d70640836 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/CaptureRegionListener.java @@ -0,0 +1,26 @@ +/** + * 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.screenshare.client.frame; + +public interface CaptureRegionListener { + + void onStartCapture(int x, int y, int width, int height); + void onStopCapture(); + void onCaptureRegionMoved(int x, int y); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/Corner.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/Corner.java new file mode 100755 index 0000000000000000000000000000000000000000..5ebfa787b77be69ac48caae27139d09dd2510cf6 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/Corner.java @@ -0,0 +1,23 @@ +/** + * 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.screenshare.client.frame; + +enum Corner { + NORTHWEST, NORTHEAST, SOUTHEAST, SOUTHWEST +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/WindowlessFrame.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/WindowlessFrame.java new file mode 100755 index 0000000000000000000000000000000000000000..a52760bb624b258161a13cee156754127d83a974 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/frame/WindowlessFrame.java @@ -0,0 +1,889 @@ +/** + * 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.screenshare.client.frame; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.Serializable; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +class WindowlessFrame implements Serializable { + private static final long serialVersionUID = 1L; + + private CaptureRegionListener captureRegionListener; + private MouseAdapter resizingAdapter; + private MouseAdapter movingAdapter; + + private int BORDER_THICKNESS = 5; + + private static interface PropertyChanger { + void changeOn(Component component); + } + + private static interface LocationAndSizeUpdateable { + void updateLocationAndSize(); + } + + private static interface OffsetLocator { + int getLeftOffset(); + int getTopOffset(); + } + + private static class StaticOffsetLocator implements OffsetLocator { + private final int mLeftOffset; + private final int mTopOffset; + + public StaticOffsetLocator(int left, int top) { + mLeftOffset = left; + mTopOffset = top; + } + + @Override + public int getLeftOffset() { + return mLeftOffset; + } + + @Override + public int getTopOffset() { + return mTopOffset; + } + } + + private static final PropertyChanger REPAINTER = new PropertyChanger() { + @Override + public void changeOn(Component component) { + if (component instanceof LocationAndSizeUpdateable) { + ((LocationAndSizeUpdateable) component).updateLocationAndSize(); + } + component.repaint(); + } + }; + + + // properties that change during use + private Point mTopLeft = new Point(); + private Dimension mOverallSize = new Dimension(); + + // properties initialized during construction + private BasicStroke mBorderStroke = new BasicStroke(BORDER_THICKNESS, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0, new float[] { 12, 12 }, 0); + private final BasicStroke borderSolidStroke = new BasicStroke(BORDER_THICKNESS, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 0); + + private GradientPaint mGradient = new GradientPaint(0.0f, 0.0f, Color.red, 1.0f, 1.0f, Color.white, true); + private final GradientPaint blueGradient = new GradientPaint(0.0f, 0.0f, Color.blue, 1.0f, 1.0f, Color.blue, true); + private final GradientPaint redGradient = new GradientPaint(0.0f, 0.0f, Color.red, 1.0f, 1.0f, Color.white, true); + + private final int mBorderWidth; + private final JFrame mWindowFrame; + private final BarFrame mTopBorder; + private final BarFrame mRightBorder; + private final BarFrame mBottomBorder; + private final BarFrame mLeftBorder; + private ToolbarFrame mToolbarFrame; + private ResizeBarFrame mResizeBarFrame; + private MoveBarFrame mMoveBarFrame; + + private static final int MIN_WINDOW_SIZE = 200; + + private MultiScreen mScreen = new MultiScreen(); + + private class MultiScreen { + private int minX=0 ; //minimum of x position + private int totalWidth=0 ; // total screen resolution + private int curWidth=0 ; // primary screen width + private GraphicsEnvironment ge ; + private GraphicsDevice[] screenDevice ; + private boolean ismultiscreen=false ; + + private MultiScreen(){ + int i ; + + ge = GraphicsEnvironment.getLocalGraphicsEnvironment() ; + screenDevice = ge.getScreenDevices() ; + + if ( 1 < screenDevice.length ){ + // this is the case for multiple devices. + // set the flag to indicate multiple devices on the system. + ismultiscreen=true ; + for ( i=0; i<screenDevice.length; i++){ + GraphicsConfiguration[] gc = screenDevice[i].getConfigurations() ; + + // determine the minimum x position for the main screen + if ( gc[0].getBounds().x <= minX ){ + minX = gc[0].getBounds().x; + } + + // determine the total screen size + if ( gc[0].getBounds().x >= 0){ + totalWidth = totalWidth + gc[0].getBounds().width; + } + + } + }else{ + // this is the case for one screen only. + ismultiscreen = false ; + } + + // set the main screen width + curWidth = screenDevice[0].getConfigurations()[0].getBounds().width ; + + } // END FUNCTION MultiScreen + + public boolean isMultiScreen(){ + return ismultiscreen ; + } // END FUNCTION isMultiScreen + + } // END CLASS MultiScreen + + private class ToolbarFrame extends Window implements LocationAndSizeUpdateable { + private static final long serialVersionUID = 1L; + + private final OffsetLocator mOffsetLocator; + + public ToolbarFrame(JFrame frame, OffsetLocator ol, JPanel content) { + super(frame); + super.setAlwaysOnTop(true); + frame.setAlwaysOnTop(true); + mOffsetLocator = ol; + //setUndecorated(true); + add(content); + pack(); + } + + public void updateLocationAndSize() { + setLocation(getLocation()); + } + + @Override + public Point getLocation() { + return new Point(mTopLeft.x + mOffsetLocator.getLeftOffset(), mTopLeft.y + mOffsetLocator.getTopOffset()); + } + } + + private class MoveBarFrame extends Window implements LocationAndSizeUpdateable { + private static final long serialVersionUID = 1L; + + private final OffsetLocator mOffsetLocator; + private MouseAdapter moveMouseAdapter = null; + + public MoveBarFrame(JFrame frame, OffsetLocator ol, JPanel content) { + super(frame); + super.setAlwaysOnTop(true); + frame.setAlwaysOnTop(true); + mOffsetLocator = ol; + add(content); + pack(); + + moveMouseAdapter = createMoveBarMovingMouseListener(); + + changeMovingBarFrame(new PropertyChanger() { + @Override + public void changeOn(Component component) { + component.addMouseListener(moveMouseAdapter); + component.addMouseMotionListener(moveMouseAdapter); + } + }); + } + + private void changeMovingBarFrame(PropertyChanger pc) { + pc.changeOn(this); + } + + private MouseAdapter createMoveBarMovingMouseListener() { + return new FrameMovingMouseListener(false); + } + + @Override + public void updateLocationAndSize() { + setLocation(getLocation()); + } + + @Override + public Point getLocation() { + return new Point(mTopLeft.x + mOffsetLocator.getLeftOffset(), mTopLeft.y + mOffsetLocator.getTopOffset()); + } + } + + private class ResizeBarFrame extends Window implements LocationAndSizeUpdateable { + + private static final long serialVersionUID = 1L; + private final OffsetLocator mOffsetLocator; + private MouseAdapter resizeMouseAdapter = null; + + public ResizeBarFrame(JFrame frame, OffsetLocator ol, JPanel content) { + super(frame); + super.setAlwaysOnTop(true); + frame.setAlwaysOnTop(true); + setBackground(new Color(0, 255, 0, 0)); + mOffsetLocator = ol; + add(content); + pack(); + + resizeMouseAdapter = createResizeBarResizingMouseListener(); + + changeResizeBarFrame(new PropertyChanger() { + @Override + public void changeOn(Component component) { + component.addMouseListener(resizeMouseAdapter); + component.addMouseMotionListener(resizeMouseAdapter); + } + }); + } + + private MouseAdapter createResizeBarResizingMouseListener() { + return new FrameResizingMouseListener(false); + } + + public void updateLocationAndSize() { + setLocation(getLocation()); + } + + @Override + public Point getLocation() { + return new Point(mTopLeft.x + mOffsetLocator.getLeftOffset(), mTopLeft.y + mOffsetLocator.getTopOffset()); + } + + private void changeResizeBarFrame(PropertyChanger pc) { + pc.changeOn(this); + } + } + + private class FrameMovingMouseListener extends MouseAdapter { + + private AtomicBoolean mMoving = new AtomicBoolean(false); + private Point mActionOffset = null; + private Boolean isBorder = false; + + public FrameMovingMouseListener(Boolean isBorder) { + this.isBorder = isBorder; + } + + @Override + public void mouseDragged(MouseEvent e) { + int changeInX = e.getLocationOnScreen().x - mActionOffset.x - mTopLeft.x; + int changeInY = e.getLocationOnScreen().y - mActionOffset.y - mTopLeft.y; + Toolkit tk = Toolkit.getDefaultToolkit(); + Dimension d = tk.getScreenSize(); + + // check if multiscreen + if ( false == mScreen.isMultiScreen() ){ + // case one screen only + if (mTopLeft.x < 1 && changeInX < 0) { + mTopLeft.x = 0; + changeInX = 0; + } + if (mTopLeft.y < 1 && changeInY < 0) { + mTopLeft.y = 0; + changeInY = 0; + } + if (mTopLeft.x + mOverallSize.width > (d.width-6) && changeInX > 0) { + mTopLeft.x = d.width - mOverallSize.width-BORDER_THICKNESS; + changeInX = 0; + + } + if (mTopLeft.y + mOverallSize.height > (d.height-6) && changeInY > 0) { + mTopLeft.y = d.height - mOverallSize.height-BORDER_THICKNESS; + changeInY = 0; + } + }else{ + // case multiple screen + if (mTopLeft.x < mScreen.minX+1 && changeInX < 0) { + mTopLeft.x = mScreen.minX; + changeInX = 0; + } + if (mTopLeft.y < 1 && changeInY < 0) { + mTopLeft.y = 0; + changeInY = 0; + } + + if (mTopLeft.x + mOverallSize.width > (mScreen.totalWidth-6) && changeInX > 0) { + mTopLeft.x = mScreen.totalWidth - mOverallSize.width-BORDER_THICKNESS; + changeInX = 0; + } + if (mTopLeft.y + mOverallSize.height > (d.height-6) && changeInY > 0) { + mTopLeft.y = d.height - mOverallSize.height-5; + changeInY = 0; + } + } + if (mMoving.get() && !e.isConsumed()) { + WindowlessFrame.this.setLocation(changeInX + mTopLeft.x, changeInY + mTopLeft.y); + } + } + + @Override + public void mousePressed(MouseEvent e) { + final Point mouse = e.getLocationOnScreen(); + mActionOffset = new Point(mouse.x - mTopLeft.x, mouse.y - mTopLeft.y); + mMoving.set(true); + e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } + + @Override + public void mouseReleased(MouseEvent e) { + mMoving.set(false); + mActionOffset = null; + e.getComponent().setCursor(Cursor.getDefaultCursor()); + } + + @Override + public void mouseMoved(MouseEvent e) { + if (!isBorder) { + e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } + } + } + + private class FrameResizingMouseListener extends MouseAdapter { + + private static final int CORNER_SIZE = 150; + + private AtomicBoolean mResizing = new AtomicBoolean(false); + + private Point mActionOffset = null; + private Dimension mOriginalSize = null; + private Corner mCorner; + private Boolean isBorder = false; + + public FrameResizingMouseListener(Boolean isBorder) { + this.isBorder = isBorder; + } + + @Override + public void mouseDragged(MouseEvent e) { + int changeInX = e.getLocationOnScreen().x - mActionOffset.x - mTopLeft.x; + final int changeInY = e.getLocationOnScreen().y - mActionOffset.y - mTopLeft.y; + + Toolkit tk = Toolkit.getDefaultToolkit(); + Dimension d = tk.getScreenSize(); + + if (mResizing.get()) { + int newH = mOriginalSize.height; + int newW = mOriginalSize.width; + if (isBorder && Corner.SOUTHEAST != mCorner) { + // doing nothing + } else { + if (e.getLocationOnScreen().x < mTopLeft.x+BORDER_THICKNESS) { + newW = BORDER_THICKNESS; + } else { + newW += changeInX; + } + if (e.getLocationOnScreen().y < mTopLeft.y+BORDER_THICKNESS) { + newH = BORDER_THICKNESS; + } else { + newH += changeInY; + } + } + /*else if (mCorner == Corner.NORTHEAST) { + mTopLeft.y = mTopLeft.y + changeInY; + newH = mOverallSize.height + -changeInY; + newW += changeInX; + } else if (mCorner == Corner.NORTHWEST) { + mTopLeft.y = mTopLeft.y + changeInY; + newH = mOverallSize.height + -changeInY; + mTopLeft.x = mTopLeft.x + changeInX; + newW = mOverallSize.width + -changeInX; + } else if (mCorner == Corner.SOUTHWEST) { + newH += changeInY; + mTopLeft.x = mTopLeft.x + changeInX; + newW = mOverallSize.width + -changeInX; + }*/ + //System.out.println("orig size: " + mOriginalSize + ", newH: " + newH + ", newW: " + newW + ", X: " + changeInX + ", Y: " + changeInY); + + if (newH + mTopLeft.y > d.height-BORDER_THICKNESS){ + newH = d.height - mTopLeft.y-BORDER_THICKNESS; + } + + // check if multiple screen _PTS_644_ _PTS_647_ + if ( false == mScreen.isMultiScreen() ){ + // one screen only + if (newW + mTopLeft.x > d.width-BORDER_THICKNESS){ + newW = d.width - mTopLeft.x-BORDER_THICKNESS; + } + }else{ + int mWidth=0 ; + if ( mTopLeft.x > mScreen.curWidth ){ + mWidth = mScreen.totalWidth ; + }else{ + mWidth = d.width ; + } + if (newW + mTopLeft.x > mWidth-BORDER_THICKNESS && mTopLeft.x >= 0){ + newW = mWidth - mTopLeft.x-BORDER_THICKNESS; + }else if (mTopLeft.x<0 && mTopLeft.x + newW > -BORDER_THICKNESS){ + newW = - mTopLeft.x-BORDER_THICKNESS; + } + } + + // set minimum window size + if (newH < MIN_WINDOW_SIZE) { + newH = MIN_WINDOW_SIZE; + } + if (newW < MIN_WINDOW_SIZE) { + newW = MIN_WINDOW_SIZE; + } + +// if (newW % 2 != 0) newW -= 1; +// if (newH % 2 != 0) newH -= 1; + +// int newWidth = makeLengthEven(newW); +// int newHeight = makeLengthEven(newH); + + WindowlessFrame.this.setSize(newW, newH); + e.consume(); + } + } + + @Override + public void mousePressed(MouseEvent e) { + final Point mouse = e.getLocationOnScreen(); + mActionOffset = new Point(mouse.x - mTopLeft.x, mouse.y - mTopLeft.y); + mOriginalSize = new Dimension(mOverallSize); + + if (isBorder) { + mCorner = nearCorner(mouse); + if (mCorner != null ) { + mResizing.set(true); + } + } + else { + mResizing.set(true); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + mResizing.set(false); + mActionOffset = null; + mOriginalSize = null; + mCorner = null; + } + + private Corner nearCorner(Point mouse) { + if (isNearBottomRightCorner(mouse)) { + return Corner.SOUTHEAST; + } /* else if (isNearTopRightCorner(mouse)) { + return Corner.NORTHEAST; + } else if (isNearTopLeftCorner(mouse)) { + return Corner.NORTHWEST; + } else if (isNearBottomLeftCorner(mouse)) { + return Corner.SOUTHWEST; + } + */ + return null; + } + + private boolean isNearBottomRightCorner(Point mouse) { + int xToBotLeft = Math.abs(mTopLeft.x + (int) mOverallSize.getWidth() - mouse.x); + int yToBotLeft = Math.abs(mTopLeft.y + (int) mOverallSize.getHeight() - mouse.y);; + return (xToBotLeft < CORNER_SIZE && yToBotLeft < CORNER_SIZE); + } + + /* private boolean isNearTopRightCorner(Point mouse) { + int xToTopRight = Math.abs(mTopLeft.x + (int) mOverallSize.getWidth() - mouse.x); + int yToTopRight = Math.abs(mTopLeft.y - mouse.y); + return xToTopRight < CORNER_SIZE && yToTopRight < CORNER_SIZE; + } + + private boolean isNearBottomLeftCorner(Point mouse) { + int xToBottomLeft = Math.abs(mTopLeft.x - mouse.x); + int yToBottomLeft = Math.abs(mTopLeft.y + (int) mOverallSize.getHeight() - mouse.y); + return xToBottomLeft < CORNER_SIZE && yToBottomLeft < CORNER_SIZE; + } + + private boolean isNearTopLeftCorner(Point mouse) { + int xToTopLeft = Math.abs(mTopLeft.x - mouse.x); + int yToTopLeft = Math.abs(mTopLeft.y - mouse.y); + return xToTopLeft < CORNER_SIZE && yToTopLeft < CORNER_SIZE; + } + */ + + @Override + public void mouseMoved(MouseEvent e) { + final Point mouse = e.getLocationOnScreen(); + + /* + if (isNearTopLeftCorner(mouse)) { + e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR)); + } else if (isNearBottomLeftCorner(mouse)) { + e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR)); + } else if (isNearTopRightCorner(mouse)) { + e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR)); + } else + */ + if (isBorder) { + if (isNearBottomRightCorner(mouse)) { + e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR)); + } else { + e.getComponent().setCursor(Cursor.getDefaultCursor()); + } + } else { + e.getComponent().setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR)); + } + } + } + + private class BarFrame extends Window implements LocationAndSizeUpdateable { + private static final long serialVersionUID = 1L; + + private final OffsetLocator mOffsetLocator; + + public BarFrame(Frame frame, OffsetLocator offsetLocator) { + super(frame); + mOffsetLocator = offsetLocator; + //setUndecorated(true); + } + + @Override + public void paint(Graphics g) { + if (shouldPaintRectangle()) { + Graphics2D g2 = (Graphics2D) g; + g2.setStroke(mBorderStroke); + g2.setPaint(mGradient); + g2.drawRect(0, 0, getWidth(), getHeight()); + } else { + super.paint(g); + } + } + + protected boolean shouldPaintRectangle() { + return true; + } + + public void updateLocationAndSize() { + setSize(getWidth(), getHeight()); + setLocation(getLocation()); + } + + @Override + public Point getLocation() { + return new Point(mTopLeft.x + mOffsetLocator.getLeftOffset(), mTopLeft.y + mOffsetLocator.getTopOffset()); + } + + @Override + public int getHeight() { + return mBorderWidth; + } + + @Override + public int getWidth() { + return mOverallSize.width; + } + } + + private class HorizontalBarFrame extends BarFrame { + private static final long serialVersionUID = 1L; + + public HorizontalBarFrame(JFrame frame, OffsetLocator offsetLocator) { + super(frame, offsetLocator); + super.setAlwaysOnTop(true); + } + } + + private class VerticalBarFrame extends BarFrame { + private static final long serialVersionUID = 1L; + + public VerticalBarFrame(JFrame frame, OffsetLocator offsetLocator) { + super(frame, offsetLocator); + super.setAlwaysOnTop(true); + } + + @Override + public int getWidth() { + return mBorderWidth; + } + + @Override + public int getHeight() { + return mOverallSize.height; + } + } + + public WindowlessFrame(int borderWidth) { + mBorderWidth = borderWidth; + + mWindowFrame = new JFrame("Windowless Frame"); + //mWindowFrame.setAlwaysOnTop(true); + + mTopBorder = new HorizontalBarFrame(mWindowFrame, new StaticOffsetLocator(0, 0)); + mBottomBorder = new HorizontalBarFrame(mWindowFrame, new OffsetLocator() { + + @Override + public int getTopOffset() { + return mOverallSize.height; + } + + @Override + public int getLeftOffset() { + return 0; + } + }); + + mRightBorder = new VerticalBarFrame(mWindowFrame, new OffsetLocator() { + + @Override + public int getTopOffset() { + return 0; + } + + @Override + public int getLeftOffset() { + return mOverallSize.width; + } + }); + mLeftBorder = new VerticalBarFrame(mWindowFrame, new StaticOffsetLocator(0, 0)); + + movingAdapter = createMovingMouseListener(); + resizingAdapter = createResizingMouseListener(); + changeBarFrames(new PropertyChanger() { + @Override + public void changeOn(Component component) { + component.addMouseListener(resizingAdapter); + component.addMouseMotionListener(resizingAdapter); + component.addMouseListener(movingAdapter); + component.addMouseMotionListener(movingAdapter); + } + }, false); + } + + public final MouseAdapter createMovingMouseListener() { + return new FrameMovingMouseListener(true); + } + + public final MouseAdapter createResizingMouseListener() { + return new FrameResizingMouseListener(true); + } + + public void setToolbar(final JPanel toolbar) { + final OffsetLocator toolbarOffsetLocator = new OffsetLocator() { + @Override + public int getTopOffset() { + return (mOverallSize.height + mBorderWidth - toolbar.getHeight()) / 2; + } + + @Override + public int getLeftOffset() { + return (mOverallSize.width + mBorderWidth - toolbar.getWidth()) / 2; + } + }; + mToolbarFrame = new ToolbarFrame(mWindowFrame, toolbarOffsetLocator, toolbar); + } + + public void setResizeBar(final JPanel resizeBar) { + + final OffsetLocator resizeBarOffsetLocator = new OffsetLocator() { + @Override + public int getTopOffset() { + return (mOverallSize.height + mBorderWidth - resizeBar.getHeight()); + } + + @Override + public int getLeftOffset() { + return (mOverallSize.width + mBorderWidth - resizeBar.getWidth()); + } + }; + + mResizeBarFrame = new ResizeBarFrame(mWindowFrame, resizeBarOffsetLocator, resizeBar); + } + + public void setMoveBar(final JPanel moveBar) { + final OffsetLocator moveBarOffsetLocator = new OffsetLocator() { + @Override + public int getTopOffset() { + return (mOverallSize.height + mBorderWidth - (moveBar.getHeight()/2) - moveBar.getHeight())/2 - 50; + } + + @Override + public int getLeftOffset() { + return (mOverallSize.width + mBorderWidth - (moveBar.getWidth()))/2; + } + }; + mMoveBarFrame = new MoveBarFrame(mWindowFrame, moveBarOffsetLocator, moveBar); + } + + public final void setSize(int width, int height) { + setHeight(height); + setWidth(width); + repaint(); + } + + public final void setWidth(int width) { + int newWidth = width; + if (width % 2 != 0) { + // We need to make this an even number as H264 rejects odd length. + newWidth = makeLengthEven(width); +// System.out.println("Capture width is not even [" + width + "]. Changing to [" + newWidth + "]"); + } + mOverallSize.width = newWidth + BORDER_THICKNESS; + } + + public final void setHeight(int height) { + int newHeight = height; + if (height % 2 != 0) { + // We need to make this an even number as H264 rejects odd length. + newHeight = makeLengthEven(height); +// System.out.println("Capture height is not even [" + height + "]. Changing to [" + newHeight + "]"); + } + mOverallSize.height = newHeight + BORDER_THICKNESS; + } + + public final void setLocation(int x, int y) { + mTopLeft.x = x; + mTopLeft.y = y; + repaint(); + + if (captureRegionListener != null) { + Rectangle rect = getFramedRectangle(); + captureRegionListener.onCaptureRegionMoved(rect.x, rect.y); + } + } + + public final int getX(){ + return mTopLeft.x; + } + + public final int getY(){ + return mTopLeft.y; + } + + public final int getWidth(){ + return mOverallSize.width - mBorderWidth; + } + + public final int getHeight(){ + return mOverallSize.height - mBorderWidth; + } + + public final void centerOnScreen() { + Toolkit kit = mLeftBorder.getToolkit(); + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] gs = ge.getScreenDevices(); + Insets in = kit.getScreenInsets(gs[0].getDefaultConfiguration()); + + Dimension d = kit.getScreenSize(); + int maxWidth = (d.width - in.left - in.right); + int maxHeight = (d.height - in.top - in.bottom); + setLocation((int) (maxWidth - mOverallSize.width) / 2, (int) (maxHeight - mOverallSize.height) / 2); + } + + public final Rectangle getFramedRectangle() { + return new Rectangle(mTopLeft.x + mBorderWidth, mTopLeft.y + mBorderWidth, mOverallSize.width - mBorderWidth, mOverallSize.height - mBorderWidth); + } + + public final void setVisible(final boolean b) { + changeAll(new PropertyChanger() { + @Override + public void changeOn(Component component) { + component.setVisible(b); + } + }, true); + } + + private void changeBarFrames(PropertyChanger pc, boolean repaint) { + pc.changeOn(mTopBorder); + pc.changeOn(mRightBorder); + pc.changeOn(mBottomBorder); + pc.changeOn(mLeftBorder); + if (repaint) { + repaint(); + } + } + + private void changeAll(PropertyChanger pc, boolean repaint) { + if (mToolbarFrame != null) pc.changeOn(mToolbarFrame); + if (mResizeBarFrame != null) pc.changeOn(mResizeBarFrame); + if (mMoveBarFrame != null) pc.changeOn(mMoveBarFrame); + changeBarFrames(pc, repaint); + } + + public final void repaint() { + changeAll(REPAINTER, false); + } + + public void changeBorderToBlue() { + mBorderStroke = borderSolidStroke; + mGradient = blueGradient; + repaint(); + } + + public void changeBorderToRed() { + mGradient = redGradient; + repaint(); + } + + public static void main(String[] args) { + final WindowlessFrame wf = new WindowlessFrame(5); + wf.setHeight(300); + wf.setWidth(600); + wf.setLocation(100, 200); + wf.setVisible(true); + } + + public void setCaptureRegionListener(CaptureRegionListener listener){ + this.captureRegionListener = listener; + } + + public void removeResizeListeners() { + mRightBorder.removeMouseListener(resizingAdapter); + mRightBorder.removeMouseMotionListener(resizingAdapter); + mLeftBorder.removeMouseListener(resizingAdapter); + mLeftBorder.removeMouseMotionListener(resizingAdapter); + mTopBorder.removeMouseListener(resizingAdapter); + mTopBorder.removeMouseMotionListener(resizingAdapter); + mBottomBorder.removeMouseListener(resizingAdapter); + mBottomBorder.removeMouseMotionListener(resizingAdapter); + mResizeBarFrame.removeMouseListener(resizingAdapter); + mResizeBarFrame.removeMouseMotionListener(resizingAdapter); + mMoveBarFrame.removeMouseListener(movingAdapter); + mMoveBarFrame.removeMouseMotionListener(movingAdapter); + repaint(); + + System.out.println("Removing listeners......................"); + mToolbarFrame.setVisible(false); + mResizeBarFrame.setVisible(false); + mMoveBarFrame.setVisible(false); + } + + public void addResizeListeners() { + System.out.println("Adding listeners......................"); + mWindowFrame.add(mToolbarFrame); + } + + private int makeLengthEven(int length) { + if (length % 2 != 0) return length - 1; + return length; + } +} \ No newline at end of file diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/BBBFFmpegFrameRecorder.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/BBBFFmpegFrameRecorder.java new file mode 100755 index 0000000000000000000000000000000000000000..119d9e8627587ec7d22370c905fb2a4f15cfa785 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/BBBFFmpegFrameRecorder.java @@ -0,0 +1,668 @@ +package org.bigbluebutton.screenshare.client.javacv; + +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.awt.image.ComponentSampleModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferDouble; +import java.awt.image.DataBufferFloat; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.MultiPixelPackedSampleModel; +import java.awt.image.Raster; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.io.File; +import java.util.Map.Entry; +import org.bytedeco.javacpp.BytePointer; +import org.bytedeco.javacpp.DoublePointer; +import org.bytedeco.javacpp.Loader; +import org.bytedeco.javacpp.PointerPointer; +import static org.bytedeco.javacpp.avcodec.*; +import static org.bytedeco.javacpp.avformat.*; +import static org.bytedeco.javacpp.avutil.*; +import static org.bytedeco.javacpp.swresample.*; +import static org.bytedeco.javacpp.swscale.*; + +/** + * + * @author Samuel Audet + */ +public class BBBFFmpegFrameRecorder extends BBBFrameRecorder { + public static BBBFFmpegFrameRecorder createDefault(File f, int w, int h) throws Exception { return new BBBFFmpegFrameRecorder(f, w, h); } + public static BBBFFmpegFrameRecorder createDefault(String f, int w, int h) throws Exception { return new BBBFFmpegFrameRecorder(f, w, h); } + + private static Exception loadingException = null; + + public static void tryLoad() throws Exception { + if (loadingException != null) { + throw loadingException; + } else { + try { + Loader.load(org.bytedeco.javacpp.avutil.class); + Loader.load(org.bytedeco.javacpp.swresample.class); + Loader.load(org.bytedeco.javacpp.avcodec.class); + Loader.load(org.bytedeco.javacpp.avformat.class); + Loader.load(org.bytedeco.javacpp.swscale.class); + + /* initialize libavcodec, and register all codecs and formats */ + av_register_all(); + avformat_network_init(); + } catch (Throwable t) { + if (t instanceof Exception) { + throw loadingException = (Exception)t; + } else { + throw loadingException = new Exception("Failed to load " + BBBFFmpegFrameRecorder.class, t); + } + } + } + } + + static { + try { + tryLoad(); + } catch (Exception ex) { } + } + + public BBBFFmpegFrameRecorder(File file, int audioChannels) { + this(file, 0, 0, audioChannels); + } + + public BBBFFmpegFrameRecorder(String filename, int audioChannels) { + this(filename, 0, 0, audioChannels); + } + + public BBBFFmpegFrameRecorder(File file, int imageWidth, int imageHeight) { + this(file, imageWidth, imageHeight, 0); + } + + public BBBFFmpegFrameRecorder(String filename, int imageWidth, int imageHeight) { + this(filename, imageWidth, imageHeight, 0); + } + + public BBBFFmpegFrameRecorder(File file, int imageWidth, int imageHeight, int audioChannels) { + this(file.getAbsolutePath(), imageWidth, imageHeight, audioChannels); + } + + public BBBFFmpegFrameRecorder(String filename, int imageWidth, int imageHeight, int audioChannels) { + this.filename = filename; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + + this.pixelFormat = AV_PIX_FMT_NONE; + this.videoCodec = AV_CODEC_ID_NONE; + this.videoBitrate = 400000; + this.frameRate = 30; + + this.videoPacket = new AVPacket(); + } + + public void release() throws Exception { + synchronized (org.bytedeco.javacpp.avcodec.class) { + releaseUnsafe(); + } + } + + public void releaseUnsafe() throws Exception { + /* close each codec */ + if (avCodecContext != null) { + avcodec_close(avCodecContext); + avCodecContext = null; + } + + if (picture_buf != null) { + av_free(picture_buf); + picture_buf = null; + } + if (picture != null) { + av_frame_free(picture); + picture = null; + } + if (tmp_picture != null) { + av_frame_free(tmp_picture); + tmp_picture = null; + } + if (videoOutBuf != null) { + av_free(videoOutBuf); + videoOutBuf = null; + } + if (frame != null) { + av_frame_free(frame); + frame = null; + } + + videoStream = null; + + if (outFormatContext != null && !outFormatContext.isNull()) { + if ((outFormat.flags() & AVFMT_NOFILE) == 0) { + /* close the output file */ + avio_close(outFormatContext.pb()); + } + + /* free the streams */ + int nb_streams = outFormatContext.nb_streams(); + for(int i = 0; i < nb_streams; i++) { + av_free(outFormatContext.streams(i).codec()); + av_free(outFormatContext.streams(i)); + } + + /* free the stream */ + av_free(outFormatContext); + outFormatContext = null; + } + + if (img_convert_ctx != null) { + sws_freeContext(img_convert_ctx); + img_convert_ctx = null; + } + + if (samples_convert_ctx != null) { + swr_free(samples_convert_ctx); + samples_convert_ctx = null; + } + } + + @Override protected void finalize() throws Throwable { + super.finalize(); + release(); + } + + private String filename; + private AVFrame picture, tmp_picture; + private BytePointer picture_buf; + private BytePointer videoOutBuf; + private int video_outbuf_size; + private AVFrame frame; + private AVOutputFormat outFormat; + private AVFormatContext outFormatContext; + private AVCodec video_codec; + private AVCodecContext avCodecContext; + private AVStream videoStream; + private SwsContext img_convert_ctx; + private SwrContext samples_convert_ctx; + private AVPacket videoPacket; + private int[] got_video_packet; + + @Override public int getFrameNumber() { + return picture == null ? super.getFrameNumber() : (int)picture.pts(); + } + + @Override public void setFrameNumber(int frameNumber) { + if (picture == null) { super.setFrameNumber(frameNumber); } else { picture.pts(frameNumber); } + } + + // best guess for timestamp in microseconds... + @Override public long getTimestamp() { + return Math.round(getFrameNumber() * 1000000L / getFrameRate()); + } + + @Override public void setTimestamp(long timestamp) { + setFrameNumber((int)Math.round(timestamp * getFrameRate() / 1000000L)); + } + + public void start() throws Exception { + synchronized (org.bytedeco.javacpp.avcodec.class) { + startUnsafe(); + } + } + + public void startUnsafe() throws Exception { + int ret; + picture = null; + tmp_picture = null; + picture_buf = null; + frame = null; + videoOutBuf = null; + outFormatContext = null; + avCodecContext = null; + videoStream = null; + got_video_packet = new int[1]; + + /* auto detect the output format from the name. */ + String format_name = format == null || format.length() == 0 ? null : format; + if ((outFormat = av_guess_format(format_name, filename, null)) == null) { + int proto = filename.indexOf("://"); + if (proto > 0) { + format_name = filename.substring(0, proto); + } + if ((outFormat = av_guess_format(format_name, filename, null)) == null) { + throw new Exception("av_guess_format() error: Could not guess output format for \"" + filename + "\" and " + format + " format."); + } + } + format_name = outFormat.name().getString(); + + /* allocate the output media context */ + if ((outFormatContext = avformat_alloc_context()) == null) { + throw new Exception("avformat_alloc_context() error: Could not allocate format context"); + } + + outFormatContext.oformat(outFormat); + outFormatContext.filename().putString(filename); + + /* add the audio and video streams using the format codecs + and initialize the codecs */ + + if (imageWidth > 0 && imageHeight > 0) { + if (videoCodec != AV_CODEC_ID_NONE) { + outFormat.video_codec(videoCodec); + } else if ("flv".equals(format_name)) { + outFormat.video_codec(AV_CODEC_ID_FLV1); + } else if ("mp4".equals(format_name)) { + outFormat.video_codec(AV_CODEC_ID_MPEG4); + } else if ("3gp".equals(format_name)) { + outFormat.video_codec(AV_CODEC_ID_H263); + } else if ("avi".equals(format_name)) { + outFormat.video_codec(AV_CODEC_ID_HUFFYUV); + } + + /* find the video encoder */ + if ((video_codec = avcodec_find_encoder_by_name(videoCodecName)) == null && + (video_codec = avcodec_find_encoder(outFormat.video_codec())) == null) { + release(); + throw new Exception("avcodec_find_encoder() error: Video codec not found."); + } + + AVRational frame_rate = av_d2q(frameRate, 1001000); + AVRational supported_framerates = video_codec.supported_framerates(); + if (supported_framerates != null) { + int idx = av_find_nearest_q_idx(frame_rate, supported_framerates); + frame_rate = supported_framerates.position(idx); + } + + /* add a video output stream */ + if ((videoStream = avformat_new_stream(outFormatContext, video_codec)) == null) { + release(); + throw new Exception("avformat_new_stream() error: Could not allocate video stream."); + } + avCodecContext = videoStream.codec(); + avCodecContext.codec_id(outFormat.video_codec()); + avCodecContext.codec_type(AVMEDIA_TYPE_VIDEO); + + /* put sample parameters */ + avCodecContext.bit_rate(videoBitrate); + /* resolution must be a multiple of two, but round up to 16 as often required */ + avCodecContext.width((imageWidth + 15) / 16 * 16); + avCodecContext.height(imageHeight); + /* time base: this is the fundamental unit of time (in seconds) in terms + of which frame timestamps are represented. for fixed-fps content, + timebase should be 1/framerate and timestamp increments should be + identically 1. */ + avCodecContext.time_base(av_inv_q(frame_rate)); + videoStream.time_base(av_inv_q(frame_rate)); + if (gopSize >= 0) { + avCodecContext.gop_size(gopSize); /* emit one intra frame every gopSize frames at most */ + } + if (videoQuality >= 0) { + avCodecContext.flags(avCodecContext.flags() | CODEC_FLAG_QSCALE); + avCodecContext.global_quality((int)Math.round(FF_QP2LAMBDA * videoQuality)); + } + + if (pixelFormat != AV_PIX_FMT_NONE) { + avCodecContext.pix_fmt(pixelFormat); + } else if (avCodecContext.codec_id() == AV_CODEC_ID_RAWVIDEO || avCodecContext.codec_id() == AV_CODEC_ID_PNG || + avCodecContext.codec_id() == AV_CODEC_ID_HUFFYUV || avCodecContext.codec_id() == AV_CODEC_ID_FFV1) { + avCodecContext.pix_fmt(AV_PIX_FMT_RGB32); // appropriate for common lossless formats + } else { + avCodecContext.pix_fmt(AV_PIX_FMT_YUV420P); // lossy, but works with about everything + } + + if (avCodecContext.codec_id() == AV_CODEC_ID_MPEG2VIDEO) { + /* just for testing, we also add B frames */ + avCodecContext.max_b_frames(2); + } else if (avCodecContext.codec_id() == AV_CODEC_ID_MPEG1VIDEO) { + /* Needed to avoid using macroblocks in which some coeffs overflow. + This does not happen with normal video, it just happens here as + the motion of the chroma plane does not match the luma plane. */ + avCodecContext.mb_decision(2); + } else if (avCodecContext.codec_id() == AV_CODEC_ID_H263) { + // H.263 does not support any other resolution than the following + if (imageWidth <= 128 && imageHeight <= 96) { + avCodecContext.width(128).height(96); + } else if (imageWidth <= 176 && imageHeight <= 144) { + avCodecContext.width(176).height(144); + } else if (imageWidth <= 352 && imageHeight <= 288) { + avCodecContext.width(352).height(288); + } else if (imageWidth <= 704 && imageHeight <= 576) { + avCodecContext.width(704).height(576); + } else { + avCodecContext.width(1408).height(1152); + } + } else if (avCodecContext.codec_id() == AV_CODEC_ID_H264) { + // default to constrained baseline to produce content that plays back on anything, + // without any significant tradeoffs for most use cases + //video_c.profile(AVCodecContext.FF_PROFILE_H264_HIGH); + avCodecContext.profile(AVCodecContext.FF_PROFILE_H264_CONSTRAINED_BASELINE); + + } + + // some formats want stream headers to be separate + if ((outFormat.flags() & AVFMT_GLOBALHEADER) != 0) { + avCodecContext.flags(avCodecContext.flags() | CODEC_FLAG_GLOBAL_HEADER); + } + + if ((video_codec.capabilities() & CODEC_CAP_EXPERIMENTAL) != 0) { + avCodecContext.strict_std_compliance(AVCodecContext.FF_COMPLIANCE_EXPERIMENTAL); + } + } + + av_dump_format(outFormatContext, 0, filename, 1); + + /* now that all the parameters are set, we can open the audio and + video codecs and allocate the necessary encode buffers */ + if (videoStream != null) { + AVDictionary options = new AVDictionary(null); + if (videoQuality >= 0) { + av_dict_set(options, "crf", "" + videoQuality, 0); + } + for (Entry<String, String> e : videoOptions.entrySet()) { + av_dict_set(options, e.getKey(), e.getValue(), 0); + } + /* open the codec */ + if ((ret = avcodec_open2(avCodecContext, video_codec, options)) < 0) { + release(); + throw new Exception("avcodec_open2() error " + ret + ": Could not open video codec."); + } + av_dict_free(options); + + videoOutBuf = null; + if ((outFormat.flags() & AVFMT_RAWPICTURE) == 0) { + /* allocate output buffer */ + /* XXX: API change will be done */ + /* buffers passed into lav* can be allocated any way you prefer, + as long as they're aligned enough for the architecture, and + they're freed appropriately (such as using av_free for buffers + allocated with av_malloc) */ + video_outbuf_size = Math.max(256 * 1024, 8 * avCodecContext.width() * avCodecContext.height()); // a la ffmpeg.c + videoOutBuf = new BytePointer(av_malloc(video_outbuf_size)); + } + + /* allocate the encoded raw picture */ + if ((picture = av_frame_alloc()) == null) { + release(); + throw new Exception("av_frame_alloc() error: Could not allocate picture."); + } + picture.pts(0); // magic required by libx264 + + int size = avpicture_get_size(avCodecContext.pix_fmt(), avCodecContext.width(), avCodecContext.height()); + if ((picture_buf = new BytePointer(av_malloc(size))).isNull()) { + release(); + throw new Exception("av_malloc() error: Could not allocate picture buffer."); + } + + /* if the output format is not equal to the image format, then a temporary + picture is needed too. It is then converted to the required output format */ + if ((tmp_picture = av_frame_alloc()) == null) { + release(); + throw new Exception("av_frame_alloc() error: Could not allocate temporary picture."); + } + } + + /* open the output file, if needed */ + if ((outFormat.flags() & AVFMT_NOFILE) == 0) { + AVIOContext pb = new AVIOContext(null); + if ((ret = avio_open(pb, filename, AVIO_FLAG_WRITE)) < 0) { + release(); + throw new Exception("avio_open error() error " + ret + ": Could not open '" + filename + "'"); + } + outFormatContext.pb(pb); + } + + /* write the stream header, if any */ + avformat_write_header(outFormatContext, (PointerPointer)null); + } + + public void stop() throws Exception { + if (outFormatContext != null) { + try { + /* ralam TODO: flush all the buffers */ + // while (video_st != null && record((IplImage)null, AV_PIX_FMT_NONE)); + av_write_frame(outFormatContext, null); + + /* write the trailer, if any */ + av_write_trailer(outFormatContext); + } finally { + release(); + } + } + } + + public boolean record(BytePointer data, int width, int height, int pixelFormat) throws Exception { + if (videoStream == null) { + throw new Exception("No video output stream (Is imageWidth > 0 && imageHeight > 0 and has start() been called?)"); + } + int ret; + + if (data == null) { + /* no more frame to compress. The codec has a latency of a few + frames if using B frames, so we get the last frames by + passing the same picture again */ + } else { + // Should get the step programatically. 3 comes from RGB bytes x width (ralam jan 22, 2014) + int step = 3 * width; + + // System.out.println("PIXEL FORMAT=[" + pixelFormat + "] step=[" + step + "]"); + + if (avCodecContext.pix_fmt() != pixelFormat || avCodecContext.width() != width || avCodecContext.height() != height) { + // System.out.println("Converting picture: vcfmt=[" + avCodecContext.pix_fmt() + " pxfmt=[" + pixelFormat + "]" + + // "vcw=[" + avCodecContext.width() + "] width=[" + width + "] vch=[" + avCodecContext.height() + "] height=[" + height + "]"); + + /* convert to the codec pixel format if needed */ + img_convert_ctx = sws_getCachedContext(img_convert_ctx, width, height, pixelFormat, + avCodecContext.width(), avCodecContext.height(), avCodecContext.pix_fmt(), SWS_BILINEAR, + null, null, (DoublePointer)null); + if (img_convert_ctx == null) { + throw new Exception("sws_getCachedContext() error: Cannot initialize the conversion context."); + } + avpicture_fill(new AVPicture(tmp_picture), data, pixelFormat, width, height); + avpicture_fill(new AVPicture(picture), picture_buf, avCodecContext.pix_fmt(), avCodecContext.width(), avCodecContext.height()); + tmp_picture.linesize(0, step); + sws_scale(img_convert_ctx, new PointerPointer(tmp_picture), tmp_picture.linesize(), + 0, height, new PointerPointer(picture), picture.linesize()); + } else { + avpicture_fill(new AVPicture(picture), data, pixelFormat, width, height); + picture.linesize(0, step); + } + } + + if ((outFormat.flags() & AVFMT_RAWPICTURE) != 0) { + if (data == null) { + return false; + } + /* raw video case. The API may change slightly in the future for that? */ + av_init_packet(videoPacket); + videoPacket.flags(videoPacket.flags() | AV_PKT_FLAG_KEY); + videoPacket.stream_index(videoStream.index()); + videoPacket.data(new BytePointer(picture)); + videoPacket.size(Loader.sizeof(AVPicture.class)); + } else { + /* encode the image */ + av_init_packet(videoPacket); + videoPacket.data(videoOutBuf); + videoPacket.size(video_outbuf_size); + picture.quality(avCodecContext.global_quality()); + if ((ret = avcodec_encode_video2(avCodecContext, videoPacket, data == null ? null : picture, got_video_packet)) < 0) { + throw new Exception("avcodec_encode_video2() error " + ret + ": Could not encode video packet."); + } + picture.pts(picture.pts() + 1); // magic required by libx264 + + /* if zero size, it means the image was buffered */ + if (got_video_packet[0] != 0) { + if (videoPacket.pts() != AV_NOPTS_VALUE) { + videoPacket.pts(av_rescale_q(videoPacket.pts(), avCodecContext.time_base(), videoStream.time_base())); + } + if (videoPacket.dts() != AV_NOPTS_VALUE) { + videoPacket.dts(av_rescale_q(videoPacket.dts(), avCodecContext.time_base(), videoStream.time_base())); + } + videoPacket.stream_index(videoStream.index()); + } else { + return false; + } + } + + synchronized (outFormatContext) { + /* write the compressed frame in the media file */ + if ((ret = av_write_frame(outFormatContext, videoPacket)) < 0) { + throw new Exception("av_write_frame() error " + ret + " while writing video frame."); + } + } + return (videoPacket.flags() & AV_PKT_FLAG_KEY) == 1; + } + + + public void copyFrom(BufferedImage image, double gamma, boolean flipChannels) { + Rectangle r = new Rectangle(0, 0, image.getWidth(), image.getHeight()); + copyFrom(image, gamma, flipChannels, r); + } + + public void copyFrom(BufferedImage image, double gamma, boolean flipChannels, Rectangle roi) { + + // ByteBuffer out = getByteBuffer(roi == null ? 0 : roi.y*arrayStep() + roi.x); + SampleModel sm = image.getSampleModel(); + Raster r = image.getRaster(); + DataBuffer in = r.getDataBuffer(); + int x = -r.getSampleModelTranslateX(); + int y = -r.getSampleModelTranslateY(); + int step = sm.getWidth()*sm.getNumBands(); + int channels = sm.getNumBands(); + + if (sm instanceof ComponentSampleModel) { + step = ((ComponentSampleModel)sm).getScanlineStride(); + channels = ((ComponentSampleModel)sm).getPixelStride(); + } else if (sm instanceof SinglePixelPackedSampleModel) { + step = ((SinglePixelPackedSampleModel)sm).getScanlineStride(); + channels = 1; + } else if (sm instanceof MultiPixelPackedSampleModel) { + step = ((MultiPixelPackedSampleModel)sm).getScanlineStride(); + channels = ((MultiPixelPackedSampleModel)sm).getPixelBitStride()/8; // ?? + } + int start = y*step + x*channels; + + if (in instanceof DataBufferByte) { + System.out.println("DataBufferByte"); + // byte[] a = ((DataBufferByte)in).getData(); + // flipCopyWithGamma(ByteBuffer.wrap(a, start, a.length - start), step, out, arrayStep(), false, gamma, false, flipChannels ? channels : 0); + } else if (in instanceof DataBufferDouble) { + System.out.println("DataBufferDouble"); + // double[] a = ((DataBufferDouble)in).getData(); + // flipCopyWithGamma(DoubleBuffer.wrap(a, start, a.length - start), step, out.asDoubleBuffer(), arrayStep()/8, gamma, false, flipChannels ? channels : 0); + } else if (in instanceof DataBufferFloat) { + System.out.println("DataBufferFloat"); + // float[] a = ((DataBufferFloat)in).getData(); + // flipCopyWithGamma(FloatBuffer.wrap(a, start, a.length - start), step, out.asFloatBuffer(), arrayStep()/4, gamma, false, flipChannels ? channels : 0); + } else if (in instanceof DataBufferInt) { + System.out.println("DataBufferInt"); + // int[] a = ((DataBufferInt)in).getData(); + // flipCopyWithGamma(IntBuffer.wrap(a, start, a.length - start), step, out.asIntBuffer(), arrayStep()/4, gamma, false, flipChannels ? channels : 0); + } else if (in instanceof DataBufferShort) { + System.out.println("DataBufferShort"); + // short[] a = ((DataBufferShort)in).getData(); + // flipCopyWithGamma(ShortBuffer.wrap(a, start, a.length - start), step, out.asShortBuffer(), arrayStep()/2, true, gamma, false, flipChannels ? channels : 0); + } else if (in instanceof DataBufferUShort) { + System.out.println("DataBufferUShort"); + // short[] a = ((DataBufferUShort)in).getData(); + // flipCopyWithGamma(ShortBuffer.wrap(a, start, a.length - start), step, out.asShortBuffer(), arrayStep()/2, false, gamma, false, flipChannels ? channels : 0); + } else { + assert false; + } + + // if (bufferedImage == null && roi == null && + // image.getWidth() == arrayWidth() && image.getHeight() == arrayHeight()) { + // bufferedImage = image; + // } + + } + + /** + public static void flipCopyWithGamma(ByteBuffer srcBuf, int srcStep, + ByteBuffer dstBuf, int dstStep, boolean signed, double gamma, boolean flip, int channels) { + assert srcBuf != dstBuf; + int w = Math.min(srcStep, dstStep); + int srcLine = srcBuf.position(), dstLine = dstBuf.position(); + byte[] buffer = new byte[channels]; + while (srcLine < srcBuf.capacity() && dstLine < dstBuf.capacity()) { + if (flip) { + srcBuf.position(srcBuf.capacity() - srcLine - srcStep); + } else { + srcBuf.position(srcLine); + } + dstBuf.position(dstLine); + w = Math.min(Math.min(w, srcBuf.remaining()), dstBuf.remaining()); + if (signed) { + if (channels > 1) { + for (int x = 0; x < w; x+=channels) { + for (int z = 0; z < channels; z++) { + int in = srcBuf.get(); + byte out; + if (gamma == 1.0) { + out = (byte)in; + } else { + out = (byte)Math.round(Math.pow((double)in/Byte.MAX_VALUE, gamma)*Byte.MAX_VALUE); + } + buffer[z] = out; + } + for (int z = channels-1; z >= 0; z--) { + dstBuf.put(buffer[z]); + } + } + } else { + for (int x = 0; x < w; x++) { + int in = srcBuf.get(); + byte out; + if (gamma == 1.0) { + out = (byte)in; + } else { + out = (byte)Math.round(Math.pow((double)in/Byte.MAX_VALUE, gamma)*Byte.MAX_VALUE); + } + dstBuf.put(out); + } + } + } else { + if (channels > 1) { + for (int x = 0; x < w; x+=channels) { + for (int z = 0; z < channels; z++) { + byte out; + int in = srcBuf.get() & 0xFF; + if (gamma == 1.0) { + out = (byte)in; + } else if (gamma == 2.2) { + out = gamma22[in]; + } else if (gamma == 1/2.2) { + out = gamma22inv[in]; + } else { + out = (byte)Math.round(Math.pow((double)in/0xFF, gamma)*0xFF); + } + buffer[z] = out; + } + for (int z = channels-1; z >= 0; z--) { + dstBuf.put(buffer[z]); + } + } + } else { + for (int x = 0; x < w; x++) { + byte out; + int in = srcBuf.get() & 0xFF; + if (gamma == 1.0) { + out = (byte)in; + } else if (gamma == 2.2) { + out = gamma22[in]; + } else if (gamma == 1/2.2) { + out = gamma22inv[in]; + } else { + out = (byte)Math.round(Math.pow((double)in/0xFF, gamma)*0xFF); + } + dstBuf.put(out); + } + } + } + srcLine += srcStep; + dstLine += dstStep; + } +} + + **/ + + + +} \ No newline at end of file diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/BBBFrameRecorder.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/BBBFrameRecorder.java new file mode 100755 index 0000000000000000000000000000000000000000..09612e31d460f8efa2870a4a73d8b602c737e25f --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/BBBFrameRecorder.java @@ -0,0 +1,193 @@ +package org.bigbluebutton.screenshare.client.javacv; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +public abstract class BBBFrameRecorder { + + public static final List<String> list = new LinkedList<String>(Arrays.asList(new String[] { "FFmpeg", "OpenCV" })); + public static void init() { + for (String name : list) { + try { + Class<? extends BBBFrameRecorder> c = get(name); + c.getMethod("tryLoad").invoke(null); + } catch (Throwable t) { } + } + } + public static Class<? extends BBBFrameRecorder> getDefault() { + // select first frame recorder that can load.. + for (String name : list) { + try { + Class<? extends BBBFrameRecorder> c = get(name); + c.getMethod("tryLoad").invoke(null); + return c; + } catch (Throwable t) { } + } + return null; + } + public static Class<? extends BBBFrameRecorder> get(String className) throws Exception { + className = BBBFrameRecorder.class.getPackage().getName() + "." + className; + try { + return Class.forName(className).asSubclass(BBBFrameRecorder.class); + } catch (ClassNotFoundException e) { + String className2 = className + "FrameRecorder"; + try { + return Class.forName(className2).asSubclass(BBBFrameRecorder.class); + } catch (ClassNotFoundException ex) { + throw new Exception("Could not get FrameRecorder class for " + className + " or " + className2, e); + } + } + } + + public static BBBFrameRecorder create(Class<? extends BBBFrameRecorder> c, Class p, Object o, int w, int h) throws Exception { + Throwable cause = null; + try { + return c.getConstructor(p, int.class, int.class).newInstance(o, w, h); + } catch (InstantiationException ex) { + cause = ex; + } catch (IllegalAccessException ex) { + cause = ex; + } catch (IllegalArgumentException ex) { + cause = ex; + } catch (NoSuchMethodException ex) { + cause = ex; + } catch (InvocationTargetException ex) { + cause = ex.getCause(); + } + throw new Exception("Could not create new " + c.getSimpleName() + "(" + o + ", " + w + ", " + h + ")", cause); + } + + public static BBBFrameRecorder createDefault(File file, int width, int height) throws Exception { + return create(getDefault(), File.class, file, width, height); + } + public static BBBFrameRecorder createDefault(String filename, int width, int height) throws Exception { + return create(getDefault(), String.class, filename, width, height); + } + + public static BBBFrameRecorder create(String className, File file, int width, int height) throws Exception { + return create(get(className), File.class, file, width, height); + } + public static BBBFrameRecorder create(String className, String filename, int width, int height) throws Exception { + return create(get(className), String.class, filename, width, height); + } + + protected String format, videoCodecName; + protected int imageWidth, imageHeight, audioChannels; + protected int pixelFormat, videoCodec, videoBitrate, gopSize = -1; + protected double frameRate, videoQuality = -1; + protected HashMap<String, String> videoOptions = new HashMap<String, String>(); + protected int frameNumber = 0; + protected long timestamp = 0; + + public String getFormat() { + return format; + } + public void setFormat(String format) { + this.format = format; + } + + public String getVideoCodecName() { + return videoCodecName; + } + public void setVideoCodecName(String videoCodecName) { + this.videoCodecName = videoCodecName; + } + + public int getImageWidth() { + return imageWidth; + } + public void setImageWidth(int imageWidth) { + this.imageWidth = imageWidth; + } + + public int getImageHeight() { + return imageHeight; + } + public void setImageHeight(int imageHeight) { + this.imageHeight = imageHeight; + } + + public int getAudioChannels() { + return audioChannels; + } + public void setAudioChannels(int audioChannels) { + this.audioChannels = audioChannels; + } + + public int getPixelFormat() { + return pixelFormat; + } + public void setPixelFormat(int pixelFormat) { + this.pixelFormat = pixelFormat; + } + + public int getVideoCodec() { + return videoCodec; + } + public void setVideoCodec(int videoCodec) { + this.videoCodec = videoCodec; + } + + public int getVideoBitrate() { + return videoBitrate; + } + public void setVideoBitrate(int videoBitrate) { + this.videoBitrate = videoBitrate; + } + + public int getGopSize() { + return gopSize; + } + public void setGopSize(int gopSize) { + this.gopSize = gopSize; + } + + public double getFrameRate() { + return frameRate; + } + public void setFrameRate(double frameRate) { + this.frameRate = frameRate; + } + + public double getVideoQuality() { + return videoQuality; + } + public void setVideoQuality(double videoQuality) { + this.videoQuality = videoQuality; + } + + public String getVideoOption(String key) { + return videoOptions.get(key); + } + public void setVideoOption(String key, String value) { + videoOptions.put(key, value); + } + + public int getFrameNumber() { + return frameNumber; + } + public void setFrameNumber(int frameNumber) { + this.frameNumber = frameNumber; + } + + public long getTimestamp() { + return timestamp; + } + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public static class Exception extends java.lang.Exception { + public Exception(String message) { super(message); } + public Exception(String message, Throwable cause) { super(message, cause); } + } + + public abstract void start() throws Exception; + public abstract void stop() throws Exception; + + public abstract void release() throws Exception; +} \ No newline at end of file diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/JavaCVScreenshare.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/JavaCVScreenshare.java new file mode 100755 index 0000000000000000000000000000000000000000..3938102ff6d4c368a6b4d4b120dffb7df8dbaa20 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/javacv/JavaCVScreenshare.java @@ -0,0 +1,204 @@ +package org.bigbluebutton.screenshare.client.javacv; + +import java.awt.AWTException; +import java.awt.image.BufferedImage; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import org.bigbluebutton.screenshare.client.ScreenCaptureTaker; +import org.bigbluebutton.screenshare.client.ScreenShareInfo; +import org.bytedeco.javacpp.BytePointer; +import static org.bytedeco.javacpp.avcodec.*; +import static org.bytedeco.javacpp.avutil.*; + +public class JavaCVScreenshare { + + private volatile boolean startBroadcast = false; + private final Executor startBroadcastExec = Executors.newSingleThreadExecutor(); + private Runnable startBroadcastRunner; + private BBBFFmpegFrameRecorder recorder = null; + private Double defaultFrameRate = 12.0; + private Double frameRate = 12.0; + private int defaultKeyFrameInterval = 6; + + private long startTime; + private int frameNumber = 1; + + private ScreenShareInfo ssi; + private ScreenCaptureTaker captureTaker; + + + private final String FRAMERATE_KEY = "frameRate"; + private final String KEYFRAMEINTERVAL_KEY = "keyFrameInterval"; + + public JavaCVScreenshare(ScreenShareInfo ssi) { + this.ssi = ssi; + captureTaker = new ScreenCaptureTaker(ssi.x, ssi.y, ssi.captureWidth, ssi.captureHeight, ssi.scaleWidth, ssi.scaleHeight); + } + + public void setCaptureCoordinates(int x, int y){ + captureTaker.setCaptureCoordinates(x, y); + } + + private Map<String, String> splitToMap(String source, String entriesSeparator, String keyValueSeparator) { + System.out.println("CODEC_OPTS=" + source); + Map<String, String> map = new HashMap<String, String>(); + String[] entries = source.split(entriesSeparator); + for (String entry : entries) { + if (entry != "" && entry.contains(keyValueSeparator)) { + String[] keyValue = entry.split(keyValueSeparator); + System.out.println("OPTION: " + keyValue[0] + "=" + keyValue[1]); + map.put(keyValue[0], keyValue[1]); + } + } + return map; + } + + public void go(String URL, int x, int y, int width, int height) throws IOException, BBBFrameRecorder.Exception, + AWTException, InterruptedException { + + captureTaker = new ScreenCaptureTaker(x, y, width, height, width, height); + + System.out.println("Capturing w=[" + width + "] h=[" + height + "] at x=[" + x + "] y=[" + y + "]"); + + recorder = new BBBFFmpegFrameRecorder(URL, width, height); + recorder.setFormat("flv"); + + /// + // Flash SVC2 + //recorder.setVideoCodec(AV_CODEC_ID_FLASHSV2); + //recorder.setPixelFormat(AV_PIX_FMT_BGR24); + + // H264 + recorder.setVideoCodec(AV_CODEC_ID_H264); + recorder.setPixelFormat(AV_PIX_FMT_YUV420P); + + Map<String, String> codecOptions = splitToMap(ssi.codecOptions, "&", "="); + + frameRate = parseFrameRate(codecOptions.get(FRAMERATE_KEY)); + recorder.setFrameRate(frameRate); + + int keyFrameInterval = parseKeyFrameInterval(codecOptions.get(KEYFRAMEINTERVAL_KEY)); + int gopSize = frameRate.intValue() * keyFrameInterval; + recorder.setGopSize(gopSize); + + System.out.println("==== CODEC OPTIONS ====="); + for (Map.Entry<String, String> entry : codecOptions.entrySet()) { + System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); + if (entry.getKey().equals(FRAMERATE_KEY) || entry.getKey().equals(KEYFRAMEINTERVAL_KEY)) { + // ignore as we have handled this above + } else { + recorder.setVideoOption(entry.getKey(), entry.getValue()); + } + + } + System.out.println("==== END CODEC OPTIONS ====="); + + startTime = System.currentTimeMillis(); + + try { + recorder.start(); + } catch (Exception e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + } + + private Double parseFrameRate(String value) { + Double fr = defaultFrameRate; + + try { + fr = Double.parseDouble(value); + } catch (NumberFormatException e) { + fr = defaultFrameRate; + } + + return fr; + } + + private int parseKeyFrameInterval(String value) { + int fr = defaultKeyFrameInterval; + + try { + fr = Integer.parseInt(value); + } catch (NumberFormatException e) { + fr = defaultKeyFrameInterval; + } + + return fr; + } + + private void captureScreen() { + long now = System.currentTimeMillis(); + + BufferedImage currentScreenshot = captureTaker.captureScreen(); + DataBuffer in = currentScreenshot.getData().getDataBuffer(); + + byte[] a = ((DataBufferByte)in).getData();; + + ByteBuffer bbuffer = ByteBuffer.wrap(a); + + BytePointer bpointer = new BytePointer(bbuffer); + try { + recorder.record(bpointer, currentScreenshot.getWidth(), currentScreenshot.getHeight(), AV_PIX_FMT_BGR24); + } catch (Exception e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + + long sleepFramerate = (long) (1000 / frameRate); + long timestamp = now - startTime; + recorder.setTimestamp(timestamp * 1000); + + // System.out.println("i=[" + i + "] timestamp=[" + timestamp + "]"); + recorder.setFrameNumber(frameNumber); + +// System.out.println("[ENCODER] encoded image " + frameNumber + " in " + (System.currentTimeMillis() - now)); + frameNumber++; + + long execDuration = (System.currentTimeMillis() - now); + long sleepDuration = Math.max(sleepFramerate - execDuration, 0); + pause(sleepDuration); + + } + + private void pause(long dur) { + try{ + Thread.sleep(dur); + } catch (Exception e){ + System.out.println("Exception sleeping."); + } + } + + public void start() { + startBroadcast = true; + startBroadcastRunner = new Runnable() { + public void run() { + while (startBroadcast){ + captureScreen(); + } + System.out.println("Stopping screen capture."); + } + }; + startBroadcastExec.execute(startBroadcastRunner); + } + + public void stop() { + startBroadcast = false; + if (recorder != null) { + + try { + recorder.stop(); + recorder.release(); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/BlockStreamProtocolEncoder.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/BlockStreamProtocolEncoder.java new file mode 100755 index 0000000000000000000000000000000000000000..58a171e0602628d0d3f297f4bd24971797589805 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/BlockStreamProtocolEncoder.java @@ -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.screenshare.client.net; + +import java.awt.Point; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.Adler32; + +public class BlockStreamProtocolEncoder { + private static final byte[] END_FRAME = new byte[] {'D', 'S', '-', 'E', 'N', 'D'}; + private static final byte[] HEADER = new byte[] {'B', 'B', 'B', '-', 'D', 'S'}; + private static final byte CAPTURE_START_EVENT = 0; + private static final byte CAPTURE_UPDATE_EVENT = 1; + private static final byte CAPTURE_END_EVENT = 2; + private static final byte MOUSE_LOCATION_EVENT = 3; + private static final byte CORRUPT_PACKET_EVENT = 7; + + public static void encodeStartStreamMessage(String meetingId, int width, int height, + ByteArrayOutputStream data, int seqNum) throws IOException { + + data.write(CAPTURE_START_EVENT); + encodeMeetingId(data, meetingId); + encodeSequenceNumber(data, seqNum); + + data.write(intToBytes(width)); + data.write(intToBytes(height)); + + } + + + public static void encodeRoomAndSequenceNumber(String meetingId, int seqNum, ByteArrayOutputStream data) throws IOException{ + data.write(CAPTURE_UPDATE_EVENT); + encodeMeetingId(data, meetingId); + encodeSequenceNumber(data, seqNum); + } + + public static byte[] encodeHeaderAndLength(ByteArrayOutputStream data) throws IOException { + ByteArrayOutputStream header = new ByteArrayOutputStream(); + header.write(HEADER); + header.write(intToBytes(data.size())); + + return header.toByteArray(); + } + + public static void encodeMouseLocation(Point mouseLoc, String meetingId, ByteArrayOutputStream data, int seqNum) throws IOException { + data.write(MOUSE_LOCATION_EVENT); + encodeMeetingId(data, meetingId); + encodeSequenceNumber(data, seqNum); + data.write(intToBytes(mouseLoc.x)); + data.write(intToBytes(mouseLoc.y)); + } + + public static void encodeEndStreamMessage(String meetingId, ByteArrayOutputStream data, int seqNum) throws IOException { + data.write(CAPTURE_END_EVENT); + encodeMeetingId(data, meetingId); + encodeSequenceNumber(data, seqNum); + } + + public static void encodeDelimiter(ByteArrayOutputStream data) throws IOException { + data.write(END_FRAME); + } + + public static byte[] encodeChecksum(byte[] data) { + Adler32 checksum = new Adler32(); + checksum.reset(); + checksum.update(data); + return longToBytes(checksum.getValue()); + } + + private static byte[] longToBytes(long i) { + byte[] data = new byte[8]; + + data[0] = (byte)((i >> 56) & 0xff); + data[1] = (byte)((i >> 48) & 0xff); + data[2] = (byte)((i >> 40) & 0xff); + data[3] = (byte)((i >> 32) & 0xff); + data[4] = (byte)((i >> 24) & 0xff); + data[5] = (byte)((i >> 16) & 0xff); + data[6] = (byte)((i >> 8) & 0xff); + data[7] = (byte)(i & 0xff); + + return data; + } + + private static byte[] intToBytes(int i) { + byte[] data = new byte[4]; + data[0] = (byte)((i >> 24) & 0xff); + data[1] = (byte)((i >> 16) & 0xff); + data[2] = (byte)((i >> 8) & 0xff); + data[3] = (byte)(i & 0xff); + return data; + } + + private static void encodeMeetingId(ByteArrayOutputStream data, String meetingId) throws IOException { + data.write(meetingId.length()); + data.write(meetingId.getBytes()); + } + + private static void encodeSequenceNumber(ByteArrayOutputStream data, int seqNum) throws IOException { + data.write(intToBytes(seqNum)); + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/BlockStreamSender.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/BlockStreamSender.java new file mode 100755 index 0000000000000000000000000000000000000000..5b720681d80588cf5a8498d047ec17e5313ac5bc --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/BlockStreamSender.java @@ -0,0 +1,160 @@ +/** +* 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.screenshare.client.net; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +public class BlockStreamSender implements ScreenCaptureSender { + private static final int PORT = 9123; + + private Socket socket = null; + + private DataOutputStream outStream = null; + private String meetingId; + + private BlockingQueue<String> screenQ = new LinkedBlockingQueue<String>(500); + private final Executor exec = Executors.newSingleThreadExecutor(); + private Runnable capturedScreenSender; + private volatile boolean sendCapturedScreen = false; + + private ByteArrayOutputStream dataToSend; + + private static final byte[] HEADER = new byte[] {'B', 'B', 'B', '-', 'D', 'S'}; + private static final byte CAPTURE_START_EVENT = 0; + private static final byte CAPTURE_UPDATE_EVENT = 1; + private static final byte CAPTURE_END_EVENT = 2; + + public BlockStreamSender() { + dataToSend = new ByteArrayOutputStream(); + } + + public void connect(String host, String meetingId, int width, int height) throws ConnectionException { + this.meetingId = meetingId; + + System.out.println("Starting capturedScreenSender "); + try { + socket = new Socket(host, PORT); + outStream = new DataOutputStream(socket.getOutputStream()); + sendStartStreamMessage(meetingId, width, height); + outStream.flush(); + } catch (UnknownHostException e) { + e.printStackTrace(); + throw new ConnectionException("UnknownHostException: " + host); + } catch (IOException e) { + e.printStackTrace(); + throw new ConnectionException("IOException: " + host + ":" + PORT); + } + + sendCapturedScreen = true; + capturedScreenSender = new Runnable() { + public void run() { + while (sendCapturedScreen) { + try { + String block = screenQ.take(); + +// long now = System.currentTimeMillis(); +// if ((now - block.getTimestamp()) < 500) { + sendBlock(block); +// if (screenQ.size() == 500) screenQ.clear(); +// } else { +// System.out.println("Discarding stale block."); +// } + } catch (InterruptedException e) { + System.out.println("InterruptedExeption while taking event."); + } + } + } + }; + exec.execute(capturedScreenSender); + } + + private void sendStartStreamMessage(String meetingId, int width, int height) { + dataToSend.reset(); + + try { + dataToSend.write(CAPTURE_START_EVENT); + dataToSend.write(meetingId.length()); + dataToSend.write(meetingId.getBytes()); + dataToSend.write(intToByte(width)); + dataToSend.write(intToByte(height)); + sendToStream(dataToSend); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void sendBlock(String block) { + long start = System.currentTimeMillis(); + dataToSend.reset(); + try { + dataToSend.write(CAPTURE_UPDATE_EVENT); + dataToSend.write(meetingId.length()); + dataToSend.write(meetingId.getBytes()); + + sendToStream(dataToSend); + } catch (IOException e) { + e.printStackTrace(); + } + long end = System.currentTimeMillis(); + if ((end - start) > 200) { + System.out.println("Sending " + dataToSend.size() + " bytes took " + (end-start) + " ms."); + } + } + + private byte[] intToByte(int i) { + byte[] data = new byte[4]; + data[0] = (byte)((i >> 24) & 0xff); + data[1] = (byte)((i >> 16) & 0xff); + data[2] = (byte)((i >> 8) & 0xff); + data[3] = (byte)(i & 0xff); + return data; + } + + private void sendToStream(ByteArrayOutputStream data) throws IOException { +// System.out.println("Sending length " + data.size()); + outStream.write(HEADER); + outStream.writeInt(data.size()); + //outStream.write(data.toByteArray()); + data.writeTo(outStream); + } + + + public void disconnect() throws ConnectionException { + System.out.println("Closing connection."); + sendCapturedScreen = false; + + dataToSend.reset(); + try { + dataToSend.write(CAPTURE_END_EVENT); + dataToSend.write(meetingId.length()); + dataToSend.write(meetingId.getBytes()); + sendToStream(dataToSend); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/CaptureEvents.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/CaptureEvents.java new file mode 100755 index 0000000000000000000000000000000000000000..0f123b8e9abb3ff829a72cd361d4e302094b6900 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/CaptureEvents.java @@ -0,0 +1,52 @@ +/** +* +* 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.screenshare.client.net; + +public enum CaptureEvents { + /** + * WARNING: Must match corresponding values with deskshare-app on the server. + * org.bigbluebutton.deskshare.CaptureEvents + */ + CAPTURE_START(0), CAPTURE_UPDATE(1), CAPTURE_END(2), MOUSE_LOCATION_EVENT(3); + + private final int event; + + CaptureEvents(int event) { + this.event = event; + } + + public int getEvent() { + return event; + } + + @Override + public String toString() { + switch (event) { + case 0: + return "Capture Start Event"; + case 1: + return "Capture Update Event"; + case 2: + return "Capture End Event"; + } + + return "Unknown Capture Event"; + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ConnectionException.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ConnectionException.java new file mode 100755 index 0000000000000000000000000000000000000000..3b4bac17c0eb59c5e209bbc0cd35aacbe5692303 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ConnectionException.java @@ -0,0 +1,28 @@ +/** +* 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.screenshare.client.net; + +public class ConnectionException extends Exception { + + private static final long serialVersionUID = -8836714569259091334L; + + public ConnectionException(String message) { + super(message); + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/Message.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/Message.java new file mode 100755 index 0000000000000000000000000000000000000000..621e670efb01338029189ac2f015d848f40834d5 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/Message.java @@ -0,0 +1,26 @@ +/** +* 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.screenshare.client.net; + +public interface Message { + + public enum MessageType {UPDATE, STARTED, STOPPED, CANCELLED, POISON}; + + public MessageType getMessageType(); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkConnectionListener.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkConnectionListener.java new file mode 100755 index 0000000000000000000000000000000000000000..54d67ab3ca79b25303b4d2b598fb73a80e6395e0 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkConnectionListener.java @@ -0,0 +1,26 @@ +/** +* 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.screenshare.client.net; + +import org.bigbluebutton.screenshare.client.ExitCode; + +public interface NetworkConnectionListener { + + public void networkConnectionException(ExitCode reason); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkHttpStreamSender.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkHttpStreamSender.java new file mode 100755 index 0000000000000000000000000000000000000000..f47baa56586741777a8b762c830e7eb694cf7a5a --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkHttpStreamSender.java @@ -0,0 +1,235 @@ +/** + * 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.screenshare.client.net; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import org.bigbluebutton.screenshare.client.ExitCode; +import com.myjavatools.web.ClientHttpRequest; +import java.util.Date; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.text.SimpleDateFormat; + +public class NetworkHttpStreamSender { + private static final String SEQ_NUM = "sequenceNumber"; + private static final String MEETING_ID = "meetingId"; + private static final String STREAM_ID = "streamId"; + + private static final String EVENT = "event"; + private static final String SCREEN = "screenInfo"; + + private String host = "localhost"; + private static final String SCREEN_CAPTURE__URL = "/tunnel/screenCapture"; + private URL url; + private URLConnection conn; + private String meetingId; + private String streamId; + private NetworkStreamListener listener; + private final SequenceNumberGenerator seqNumGenerator; + + private ExecutorService executor; + private final BlockingQueue<Message> messages = new LinkedBlockingQueue<Message>(); + private volatile boolean sendMessages = false; + + public NetworkHttpStreamSender(String meetingId, String streamId, SequenceNumberGenerator seqNumGenerator) { + this.meetingId = meetingId; + this.streamId = streamId; + this.seqNumGenerator = seqNumGenerator; + + executor = Executors.newFixedThreadPool(1); + } + + public void addListener(NetworkStreamListener listener) { + this.listener = listener; + } + + private void notifyNetworkStreamListener(ExitCode reason) { + if (listener != null) listener.networkException(reason); + } + + public void connect(String host) throws ConnectionException { + this.host = host; + System.out.println("Starting NetworkHttpStreamSender to " + host); + openConnection(); + } + + public void send(Message message) { + messages.offer(message); + } + + public void start() { + sendMessages = true; + Runnable sender = new Runnable() { + public void run() { + while (sendMessages) { + Message message; + try { + message = messages.take(); + sendMessageToServer(message); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + } + }; + executor.execute(sender); + } + + private void sendMessageToServer(Message message) { + if (message.getMessageType() == Message.MessageType.UPDATE) { + sendUpdateMessage((ShareUpdateMessage) message); + } else if (message.getMessageType() == Message.MessageType.STARTED) { + sendStartStreamMessage((ShareStartedMessage)message); + } else if (message.getMessageType() == Message.MessageType.STOPPED) { + sendCaptureEndEvent(); + } + } + + public void stop() { + sendMessages = false; +} + + private void openConnection() throws ConnectionException { + /** + * Need to re-establish connection each time, otherwise, + * we get java.net.ProtocolException: Cannot write output after reading input. + * + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4382944 + * + */ + long start = System.currentTimeMillis(); + try { + url = new URL(host + SCREEN_CAPTURE__URL); + conn = url.openConnection(); + } catch (MalformedURLException e) { + e.printStackTrace(); + throw new ConnectionException("MalformedURLException " + url.toString()); + } catch (IOException e) { + e.printStackTrace(); + throw new ConnectionException("IOException while connecting to " + url.toString()); + } + long end = System.currentTimeMillis(); + System.out.println("Http Open connection took [" + (end-start) + " ms]"); + } + + private void sendStartStreamMessage(ShareStartedMessage message) { + try { + System.out.println("Http Open connection. In sendStartStreamMessage"); + openConnection(); + sendCaptureStartEvent(message.width, message.height); + } catch (ConnectionException e) { + e.printStackTrace(); + notifyNetworkStreamListener(ExitCode.DESKSHARE_SERVICE_UNAVAILABLE); + } + } + + private void sendCaptureStartEvent(int width, int height) throws ConnectionException { + ClientHttpRequest chr; + try { + System.out.println(getTimeStamp() + " - Sending Start Sharing Event."); + chr = new ClientHttpRequest(conn); + chr.setParameter(MEETING_ID, meetingId); + chr.setParameter(STREAM_ID, streamId); + chr.setParameter(SEQ_NUM, seqNumGenerator.getNext()); + String screenInfo = Integer.toString(width) + "x" + Integer.toString(height); + chr.setParameter(SCREEN, screenInfo); + chr.setParameter(EVENT, CaptureEvents.CAPTURE_START.getEvent()); + chr.post(); + } catch (IOException e) { + e.printStackTrace(); + throw new ConnectionException("IOException while sending capture start event."); + } + + } + + public void disconnect() throws ConnectionException { + try { + System.out.println("Http Open connection. In disconnect"); + openConnection(); + sendCaptureEndEvent(); + } catch (ConnectionException e) { + e.printStackTrace(); + notifyNetworkStreamListener(ExitCode.DESKSHARE_SERVICE_UNAVAILABLE); + throw e; + } finally { + + } + } + + private void sendCaptureEndEvent() { + ClientHttpRequest chr; + try { + System.out.println(getTimeStamp() + " - Sending End Sharing Event."); + chr = new ClientHttpRequest(conn); + chr.setParameter(MEETING_ID, meetingId); + chr.setParameter(STREAM_ID, streamId); + chr.setParameter(SEQ_NUM, seqNumGenerator.getNext()); + chr.setParameter(EVENT, CaptureEvents.CAPTURE_END.getEvent()); + chr.post(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void sendUpdateMessage(ShareUpdateMessage message) { + ClientHttpRequest chr; + + try { + + // Open a connection to the web server and create a request that has + // the room and event type. + System.out.println(getTimeStamp() + " - Sending Update Sharing Event."); + openConnection(); + chr = new ClientHttpRequest(conn); + chr.setParameter(MEETING_ID, meetingId); + chr.setParameter(STREAM_ID, streamId); + chr.setParameter(EVENT, CaptureEvents.CAPTURE_UPDATE.getEvent()); + + // Post the multi-part form to the server + chr.post(); + HttpURLConnection httpConnection = (HttpURLConnection) chr.connection; + int status = httpConnection.getResponseCode(); + + System.out.println("******* POST status = [" + status + "] ***************"); + + } catch (IOException e) { + notifyNetworkStreamListener(ExitCode.NORMAL); + } catch (ConnectionException e) { + System.out.println("ERROR: Failed to send block data."); + } + } + + + private String getTimeStamp() + { + SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSS");//dd/MM/yyyy + Date now = new Date(); + String strDate = sdfDate.format(now); + return strDate; + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkStreamListener.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkStreamListener.java new file mode 100755 index 0000000000000000000000000000000000000000..de81405e2afb306e47bfb27f67d1eefd486149b7 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkStreamListener.java @@ -0,0 +1,26 @@ +/** +* 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.screenshare.client.net; + +import org.bigbluebutton.screenshare.client.ExitCode; + +public interface NetworkStreamListener { + + public void networkException(ExitCode reason); +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkStreamSender.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkStreamSender.java new file mode 100755 index 0000000000000000000000000000000000000000..b705fa1f52016e8dc50be36d006471c408f3ed7f --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NetworkStreamSender.java @@ -0,0 +1,117 @@ +/** + * 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.screenshare.client.net; + +import java.util.Timer; +import java.util.TimerTask; +import org.bigbluebutton.screenshare.client.ExitCode; + +public class NetworkStreamSender implements NetworkStreamListener { + public static final String NAME = "NETWORKSTREAMSENDER: "; + + private final String meetingId; + private final String streamId; + private NetworkHttpStreamSender httpSenders; + private NetworkConnectionListener listener; + private final SequenceNumberGenerator seqNumGenerator = new SequenceNumberGenerator(); + private String host = "192.168.23.22"; + + private TimerTask timerTask = new UpdateTimerTask(); + private Timer timer = new Timer(); + + public NetworkStreamSender(String host, String meetingId, String streamId) { + this.meetingId = meetingId; + this.streamId = streamId; + this.host = host; + connect(); + } + + public void addNetworkConnectionListener(NetworkConnectionListener listener) { + this.listener = listener; + } + + private void notifyNetworkConnectionListener(ExitCode reason) { + if (listener != null) listener.networkConnectionException(reason); + } + + private boolean connect() { + httpSenders = new NetworkHttpStreamSender(meetingId, streamId, seqNumGenerator); + httpSenders.addListener(this); + try { + httpSenders.connect(host); + } catch (ConnectionException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return true; + } + + + public void stopSharing() { + System.out.println("Queueing ShareStoppedMessage"); + send(new ShareStoppedMessage(meetingId, streamId)); + } + + public void startSharing(int width, int height) { + System.out.println("Queueing ShareStartedMessage"); + send(new ShareStartedMessage(meetingId, streamId, width, height)); + } + + private void send(Message message) { + httpSenders.send(message); + } + + public void start() { + System.out.println(NAME + "Starting network sender."); + httpSenders.start(); + timer.scheduleAtFixedRate(timerTask, 0, 2 * 1000); + } + + public void stop() throws ConnectionException { + timer.cancel(); + + if (httpSenders != null) { + httpSenders.disconnect(); + httpSenders.stop(); + } + + } + + + @Override + public void networkException(ExitCode reason) { + try { + System.out.println(NAME + "Failed to use http tunneling. Stopping."); + stop(); + notifyNetworkConnectionListener(reason); + } catch (ConnectionException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + private class UpdateTimerTask extends TimerTask { + @Override + public void run() { + System.out.println("Queueing ShareUpdateMessage"); + send(new ShareUpdateMessage(meetingId, streamId)); + } + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NextBlockRetriever.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NextBlockRetriever.java new file mode 100755 index 0000000000000000000000000000000000000000..cc1d3be3524f880b6449da9896a33a4f45ae77a0 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/NextBlockRetriever.java @@ -0,0 +1,24 @@ +/** +* 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.screenshare.client.net; + +public interface NextBlockRetriever { + public void blockSent(int position); + public Message getNextMessageToSend() throws InterruptedException; +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/PoisonMessage.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/PoisonMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..c9fcf88fbb20eabc1dc40ea46cf69c2983f1735d --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/PoisonMessage.java @@ -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/>. +* +*/ +package org.bigbluebutton.screenshare.client.net; + +public class PoisonMessage implements Message { + + @Override + public MessageType getMessageType() { + return MessageType.POISON; + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ScreenCaptureSender.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ScreenCaptureSender.java new file mode 100755 index 0000000000000000000000000000000000000000..5c4dc082d8fa07bf6acb834b69f123413c4cbfc2 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ScreenCaptureSender.java @@ -0,0 +1,26 @@ +/** +* 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.screenshare.client.net; + +public interface ScreenCaptureSender { + + public void connect(String host, String meetingId, int width, int height) throws ConnectionException; + public void disconnect() throws ConnectionException; + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/SequenceNumberGenerator.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/SequenceNumberGenerator.java new file mode 100755 index 0000000000000000000000000000000000000000..895bd5efa5e146f0785663d89ea8b02f7cfdf49e --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/SequenceNumberGenerator.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.screenshare.client.net; + +import java.util.concurrent.atomic.AtomicInteger; + + +public class SequenceNumberGenerator { + + private final AtomicInteger sequenceNum; + + public SequenceNumberGenerator() { + sequenceNum = new AtomicInteger(0); + } + + public int getNext() { + return sequenceNum.incrementAndGet(); + } +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareStartedMessage.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareStartedMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..3385403177813ad773cfa413de5861a91e43ba11 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareStartedMessage.java @@ -0,0 +1,22 @@ +package org.bigbluebutton.screenshare.client.net; + +public class ShareStartedMessage implements Message { + + public final String meetingId; + public final String streamId; + public final int width; + public final int height; + + public ShareStartedMessage(String meetingId, String streamId, int width, int height) { + this.meetingId = meetingId; + this.streamId = streamId; + this.width = width; + this.height = height; + } + + @Override + public MessageType getMessageType() { + return Message.MessageType.STARTED; + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareStoppedMessage.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareStoppedMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..19418e83edbb890f767143c86082a35c67eb195b --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareStoppedMessage.java @@ -0,0 +1,18 @@ +package org.bigbluebutton.screenshare.client.net; + +public class ShareStoppedMessage implements Message { + + public final String meetingId; + public final String streamId; + + public ShareStoppedMessage(String meetingId, String streamId) { + this.meetingId = meetingId; + this.streamId = streamId; + } + + @Override + public MessageType getMessageType() { + return Message.MessageType.STOPPED; + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareUpdateMessage.java b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareUpdateMessage.java new file mode 100755 index 0000000000000000000000000000000000000000..cb390a67ebb347de4d804fe89d67ab6e3c38ec69 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/main/java/org/bigbluebutton/screenshare/client/net/ShareUpdateMessage.java @@ -0,0 +1,18 @@ +package org.bigbluebutton.screenshare.client.net; + +public class ShareUpdateMessage implements Message { + + public final String meetingId; + public final String streamId; + + public ShareUpdateMessage(String meetingId, String streamId) { + this.meetingId = meetingId; + this.streamId = streamId; + } + + @Override + public MessageType getMessageType() { + return Message.MessageType.UPDATE; + } + +} diff --git a/bbb-screenshare/jws/webstart/src/main/resources/images/Cursor.png b/bbb-screenshare/jws/webstart/src/main/resources/images/Cursor.png new file mode 100755 index 0000000000000000000000000000000000000000..6e29c43729b8711191d7120348e15bbcfa236dd0 Binary files /dev/null and b/bbb-screenshare/jws/webstart/src/main/resources/images/Cursor.png differ diff --git a/bbb-screenshare/jws/webstart/src/main/resources/images/bbb.gif b/bbb-screenshare/jws/webstart/src/main/resources/images/bbb.gif new file mode 100755 index 0000000000000000000000000000000000000000..b01a452375a5213065dc237f34f22d97c3f69473 Binary files /dev/null and b/bbb-screenshare/jws/webstart/src/main/resources/images/bbb.gif differ diff --git a/bbb-screenshare/jws/webstart/src/main/resources/images/move-cursor.png b/bbb-screenshare/jws/webstart/src/main/resources/images/move-cursor.png new file mode 100755 index 0000000000000000000000000000000000000000..4e64fc6e4ae7f6653495dd9bf22f5d09435343d0 Binary files /dev/null and b/bbb-screenshare/jws/webstart/src/main/resources/images/move-cursor.png differ diff --git a/bbb-screenshare/jws/webstart/src/main/resources/images/resize-cursor.png b/bbb-screenshare/jws/webstart/src/main/resources/images/resize-cursor.png new file mode 100755 index 0000000000000000000000000000000000000000..50d5f933c6eaf6a6470a16935d5ece83e74182c2 Binary files /dev/null and b/bbb-screenshare/jws/webstart/src/main/resources/images/resize-cursor.png differ diff --git a/bbb-screenshare/jws/webstart/src/test/resources/testng.xml b/bbb-screenshare/jws/webstart/src/test/resources/testng.xml new file mode 100755 index 0000000000000000000000000000000000000000..8a186d44ccc73481ead0fc211f8b3f641434cdf5 --- /dev/null +++ b/bbb-screenshare/jws/webstart/src/test/resources/testng.xml @@ -0,0 +1,18 @@ +<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" > +<suite name="BigBlueButton Test Suite"> + <test name="Conference tests"> + <groups> + <run> + <exclude name="broken"/> + <include name="unit"/> + </run> + </groups> + + <packages> + <package name="org.bigbluebutton.deskshare.client.encode"/> + <package name="org.bigbluebutton.deskshare.client.encode3"/> + <package name="org.bigbluebutton.deskshare.client.net"/> + <package name="org.bigbluebutton.deskshare.client"/> + </packages> + </test> +</suite> \ No newline at end of file