diff --git a/bigbluebutton-html5/.travis.yml b/bigbluebutton-html5/.travis.yml new file mode 100644 index 0000000000000000000000000000000000000000..496996c7214158d95f3652a24f8a60cc12b4243f --- /dev/null +++ b/bigbluebutton-html5/.travis.yml @@ -0,0 +1,16 @@ +language: node_js + +install: + # - git clone git@github.com:browniecab/bigbluebutton-tests.git tests + - cd tests/puppeteer + - npm install + +script: + - cd tests/puppeteer + - node test-chat.js + - node test-draw.js + - node test-status.js + - node test-switch-slides.js + - node test-upload.js +# - node tests/puppeteer/test-hotkeys.js +# - node tests/puppeteer/test-hotkeys-mic-first.js \ No newline at end of file diff --git a/bigbluebutton-html5/Dockerfile.test b/bigbluebutton-html5/Dockerfile.test new file mode 100644 index 0000000000000000000000000000000000000000..99f79c12e6778e6073dc208de23fd04d19b905e7 --- /dev/null +++ b/bigbluebutton-html5/Dockerfile.test @@ -0,0 +1,72 @@ +FROM ubuntu:16.04 +MAINTAINER ffdixon@bigbluebutton.org + +ENV DEBIAN_FRONTEND noninteractive +RUN echo 'Acquire::http::Proxy "http://192.168.0.130:3142 ";' > /etc/apt/apt.conf.d/01proxy +RUN apt-get update && apt-get install -y wget software-properties-common + +RUN echo "deb http://ubuntu.bigbluebutton.org/xenial-200-dev bigbluebutton-xenial main " | tee /etc/apt/sources.list.d/bigbluebutton.list +RUN wget http://ubuntu.bigbluebutton.org/repo/bigbluebutton.asc -O- | apt-key add - +RUN add-apt-repository ppa:jonathonf/ffmpeg-4 -y +RUN apt-get update && apt-get -y dist-upgrade + +# -- Setup tomcat7 to run under docker +RUN apt-get install -y \ + haveged \ + net-tools \ + supervisor \ + sudo \ + tomcat7 + +RUN sed -i 's|securerandom.source=file:/dev/random|securerandom.source=file:/dev/urandom|g' /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/java.security +ADD mod/tomcat7 /etc/init.d/tomcat7 +RUN chmod +x /etc/init.d/tomcat7 + +RUN apt-get install -y language-pack-en +RUN update-locale LANG=en_US.UTF-8 + +# -- Install BigBlueButton +RUN echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | debconf-set-selections +RUN apt-get install -y bigbluebutton +RUN apt-get install -y bbb-demo + +# -- Install mongodb (for HTML5 client) +RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6 +RUN echo "deb [ arch=amd64,arm64 ] http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list +RUN sudo apt-get update && sudo apt-get install -y mongodb-org curl + +# -- Install nodejs (for HTML5 client) +RUN apt-get install -y apt-transport-https +RUN curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - +RUN echo 'deb http://deb.nodesource.com/node_8.x xenial main' > /etc/apt/sources.list.d/nodesource.list +RUN echo 'deb-src http://deb.nodesource.com/node_8.x xenial main' >> /etc/apt/sources.list.d/nodesource.list +RUN apt-get update && apt-get install -y nodejs + +# -- Install HTML5 client +RUN apt-get install -y bbb-html5 +RUN apt-get install -y coturn vim mlocate + +# -- Install Meteor +RUN curl https://install.meteor.com/ | sh +ENV METEOR_ALLOW_SUPERUSER true + +# -- Install supervisor to run all the BigBlueButton processes (replaces systemd) +RUN apt-get install -y supervisor +RUN mkdir -p /var/log/supervisor +ADD supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# -- Modify FreeSWITCH event_socket.conf.xml to listen to IPV4 +ADD mod/event_socket.conf.xml /opt/freeswitch/etc/freeswitch/autoload_configs +ADD mod/external.xml /opt/freeswitch/conf/sip_profiles/external.xml + +# -- Install latest HTML5 client from source +RUN supervisorctl stop bbb-html5 +ADD . /bigbluebutton-html5 +WORKDIR /bigbluebutton-html5 +RUN meteor npm install +WORKDIR / + +# -- Finish startup +ADD setup.sh /root/setup.sh +ENTRYPOINT ["/root/setup.sh"] +CMD [] diff --git a/bigbluebutton-html5/mod/event_socket.conf.xml b/bigbluebutton-html5/mod/event_socket.conf.xml new file mode 100644 index 0000000000000000000000000000000000000000..05e74d45ebcd09ca276972318fc2343892120485 --- /dev/null +++ b/bigbluebutton-html5/mod/event_socket.conf.xml @@ -0,0 +1,11 @@ +<configuration name="event_socket.conf" description="Socket Client"> + <settings> + <param name="nat-map" value="false"/> + <param name="listen-ip" value="127.0.0.1"/> + <param name="listen-port" value="8021"/> + <param name="password" value="ClueCon"/> + <!--<param name="apply-inbound-acl" value="loopback.auto"/>--> + <!--<param name="stop-on-bind-error" value="true"/>--> + </settings> +</configuration> + diff --git a/bigbluebutton-html5/mod/external.xml b/bigbluebutton-html5/mod/external.xml new file mode 100644 index 0000000000000000000000000000000000000000..f4b5c9463fb2026c3aecbcb87bf65f90bc42faab --- /dev/null +++ b/bigbluebutton-html5/mod/external.xml @@ -0,0 +1,113 @@ +<profile name="external"> + <!-- http://wiki.freeswitch.org/wiki/Sofia_Configuration_Files --> + <!-- This profile is only for outbound registrations to providers --> + <gateways> + <X-PRE-PROCESS cmd="include" data="external/*.xml"/> + </gateways> + + <aliases> + <!-- + <alias name="outbound"/> + <alias name="nat"/> + --> + </aliases> + + <domains> + <domain name="all" alias="false" parse="true"/> + </domains> + + <settings> + <param name="debug" value="0"/> + <!-- If you want FreeSWITCH to shutdown if this profile fails to load, uncomment the next line. --> + <!-- <param name="shutdown-on-fail" value="true"/> --> + <param name="sip-trace" value="no"/> + <param name="sip-capture" value="no"/> + <param name="rfc2833-pt" value="101"/> + <!-- RFC 5626 : Send reg-id and sip.instance --> + <!--<param name="enable-rfc-5626" value="true"/> --> + <param name="sip-port" value="$${external_sip_port}"/> + <param name="dialplan" value="XML"/> + <param name="context" value="public"/> + <param name="dtmf-duration" value="2000"/> + <param name="inbound-codec-prefs" value="$${global_codec_prefs}"/> + <param name="outbound-codec-prefs" value="$${outbound_codec_prefs}"/> + <param name="hold-music" value="$${hold_music}"/> + <param name="rtp-timer-name" value="soft"/> + <!--<param name="enable-100rel" value="true"/>--> + <!--<param name="disable-srv503" value="true"/>--> + <!-- This could be set to "passive" --> + <param name="local-network-acl" value="localnet.auto"/> + <param name="manage-presence" value="false"/> + + <!-- used to share presence info across sofia profiles + manage-presence needs to be set to passive on this profile + if you want it to behave as if it were the internal profile + for presence. + --> + <!-- Name of the db to use for this profile --> + <!--<param name="dbname" value="share_presence"/>--> + <!--<param name="presence-hosts" value="$${domain}"/>--> + <!--<param name="force-register-domain" value="$${domain}"/>--> + <!--all inbound reg will stored in the db using this domain --> + <!--<param name="force-register-db-domain" value="$${domain}"/>--> + <!-- ************************************************* --> + + <!--<param name="aggressive-nat-detection" value="true"/>--> + <param name="inbound-codec-negotiation" value="generous"/> + <param name="nonce-ttl" value="60"/> + <param name="auth-calls" value="false"/> + <param name="inbound-late-negotiation" value="true"/> + <param name="inbound-zrtp-passthru" value="true"/> <!-- (also enables late negotiation) --> + <!-- + DO NOT USE HOSTNAMES, ONLY IP ADDRESSES IN THESE SETTINGS! + <param name="rtp-ip" value="$${local_ip_v4}"/> + <param name="sip-ip" value="$${local_ip_v4}"/> + <param name="ext-rtp-ip" value="auto-nat"/> + <param name="ext-sip-ip" value="auto-nat"/> + --> + + <param name="rtp-ip" value="$${local_ip_v4}"/> + <param name="sip-ip" value="$${local_ip_v4}"/> + <param name="ext-rtp-ip" value="$${local_ip_v4}"/> + <param name="ext-sip-ip" value="$${local_ip_v4}"/> + + <param name="rtp-timeout-sec" value="300"/> + <param name="rtp-hold-timeout-sec" value="1800"/> + <!--<param name="enable-3pcc" value="true"/>--> + + <!-- TLS: disabled by default, set to "true" to enable --> + <param name="tls" value="$${external_ssl_enable}"/> + <!-- Set to true to not bind on the normal sip-port but only on the TLS port --> + <param name="tls-only" value="false"/> + <!-- additional bind parameters for TLS --> + <param name="tls-bind-params" value="transport=tls"/> + <!-- Port to listen on for TLS requests. (5081 will be used if unspecified) --> + <param name="tls-sip-port" value="$${external_tls_port}"/> + <!-- Location of the agent.pem and cafile.pem ssl certificates (needed for TLS server) --> + <!--<param name="tls-cert-dir" value=""/>--> + <!-- Optionally set the passphrase password used by openSSL to encrypt/decrypt TLS private key files --> + <param name="tls-passphrase" value=""/> + <!-- Verify the date on TLS certificates --> + <param name="tls-verify-date" value="true"/> + <!-- TLS verify policy, when registering/inviting gateways with other servers (outbound) or handling inbound registration/invite requests how should we verify their certificate --> + <!-- set to 'in' to only verify incoming connections, 'out' to only verify outgoing connections, 'all' to verify all connections, also 'in_subjects', 'out_subjects' and 'all_subjects' for subject validation. Multiple policies can be split with a '|' pipe --> + <param name="tls-verify-policy" value="none"/> + <!-- Certificate max verify depth to use for validating peer TLS certificates when the verify policy is not none --> + <param name="tls-verify-depth" value="2"/> + <!-- If the tls-verify-policy is set to subjects_all or subjects_in this sets which subjects are allowed, multiple subjects can be split with a '|' pipe --> + <param name="tls-verify-in-subjects" value=""/> + <!-- TLS version ("sslv23" (default), "tlsv1"). NOTE: Phones may not work with TLSv1 --> + <param name="tls-version" value="$${sip_tls_version}"/> + <param name="ws-binding" value=":5066"/> + <param name="apply-candidate-acl" value="webrtc-turn"/> + + <!-- enable rtcp on every channel also can be done per leg basis with rtcp_audio_interval_msec variable set to passthru to pass it across a call--> + <param name="rtcp-audio-interval-msec" value="5000"/> + <param name="rtcp-video-interval-msec" value="5000"/> + + <!-- Cut down in the join time --> + <param name="dtmf-type" value="info"/> + <param name="liberal-dtmf" value="true"/> + </settings> +</profile> + diff --git a/bigbluebutton-html5/mod/tomcat7 b/bigbluebutton-html5/mod/tomcat7 new file mode 100755 index 0000000000000000000000000000000000000000..1e4a45a6d33a6aeb5ad152ffaf00ffe3d8cbd5d8 --- /dev/null +++ b/bigbluebutton-html5/mod/tomcat7 @@ -0,0 +1,287 @@ +#!/bin/sh +# +# /etc/init.d/tomcat7 -- startup script for the Tomcat 6 servlet engine +# +# Written by Miquel van Smoorenburg <miquels@cistron.nl>. +# Modified for Debian GNU/Linux by Ian Murdock <imurdock@gnu.ai.mit.edu>. +# Modified for Tomcat by Stefan Gybas <sgybas@debian.org>. +# Modified for Tomcat6 by Thierry Carrez <thierry.carrez@ubuntu.com>. +# Modified for Tomcat7 by Ernesto Hernandez-Novich <emhn@itverx.com.ve>. +# Additional improvements by Jason Brittain <jason.brittain@mulesoft.com>. +# +### BEGIN INIT INFO +# Provides: tomcat7 +# Required-Start: $local_fs $remote_fs $network +# Required-Stop: $local_fs $remote_fs $network +# Should-Start: $named +# Should-Stop: $named +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start Tomcat. +# Description: Start the Tomcat servlet engine. +### END INIT INFO + +set -e + +PATH=/bin:/usr/bin:/sbin:/usr/sbin +NAME=tomcat7 +DESC="Tomcat servlet engine" +DEFAULT=/etc/default/$NAME +JVM_TMP=/tmp/tomcat7-$NAME-tmp + +if [ `id -u` -ne 0 ]; then + echo "You need root privileges to run this script" + exit 1 +fi + +# Make sure tomcat is started with system locale +if [ -r /etc/default/locale ]; then + . /etc/default/locale + export LANG +fi + +. /lib/lsb/init-functions + +if [ -r /etc/default/rcS ]; then + . /etc/default/rcS +fi + + +# The following variables can be overwritten in $DEFAULT + +# Run Tomcat 7 as this user ID and group ID +TOMCAT7_USER=tomcat7 +TOMCAT7_GROUP=tomcat7 + +# this is a work-around until there is a suitable runtime replacement +# for dpkg-architecture for arch:all packages +# this function sets the variable JDK_DIRS +find_jdks() +{ + for java_version in 9 8 7 6 + do + for jvmdir in /usr/lib/jvm/java-${java_version}-openjdk-* \ + /usr/lib/jvm/jdk-${java_version}-oracle-* \ + /usr/lib/jvm/jre-${java_version}-oracle-* + do + if [ -d "${jvmdir}" -a "${jvmdir}" != "/usr/lib/jvm/java-${java_version}-openjdk-common" ] + then + JDK_DIRS="${JDK_DIRS} ${jvmdir}" + fi + done + done + + # Add older non multi arch installations + JDK_DIRS="${JDK_DIRS} /usr/lib/jvm/java-6-openjdk /usr/lib/jvm/java-6-sun /usr/lib/jvm/java-7-oracle" +} + +# The first existing directory is used for JAVA_HOME (if JAVA_HOME is not +# defined in $DEFAULT) +JDK_DIRS="/usr/lib/jvm/default-java" +find_jdks + +# Look for the right JVM to use +for jdir in $JDK_DIRS; do + if [ -r "$jdir/bin/java" -a -z "${JAVA_HOME}" ]; then + JAVA_HOME="$jdir" + fi +done +export JAVA_HOME + +# Directory where the Tomcat 6 binary distribution resides +CATALINA_HOME=/usr/share/$NAME + +# Directory for per-instance configuration files and webapps +CATALINA_BASE=/var/lib/$NAME + +# Use the Java security manager? (yes/no) +TOMCAT7_SECURITY=no + +# Default Java options +# Set java.awt.headless=true if JAVA_OPTS is not set so the +# Xalan XSL transformer can work without X11 display on JDK 1.4+ +# It also looks like the default heap size of 64M is not enough for most cases +# so the maximum heap size is set to 128M +if [ -z "$JAVA_OPTS" ]; then + JAVA_OPTS="-Djava.awt.headless=true -Xmx128M" +fi + +# End of variables that can be overwritten in $DEFAULT + +# overwrite settings from default file +if [ -f "$DEFAULT" ]; then + . "$DEFAULT" +fi + +if [ ! -f "$CATALINA_HOME/bin/bootstrap.jar" ]; then + log_failure_msg "$NAME is not installed" + exit 1 +fi + +POLICY_CACHE="$CATALINA_BASE/work/catalina.policy" + +if [ -z "$CATALINA_TMPDIR" ]; then + CATALINA_TMPDIR="$JVM_TMP" +fi + +# Set the JSP compiler if set in the tomcat7.default file +if [ -n "$JSP_COMPILER" ]; then + JAVA_OPTS="$JAVA_OPTS -Dbuild.compiler=\"$JSP_COMPILER\"" +fi +JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom" + +SECURITY="" +if [ "$TOMCAT7_SECURITY" = "yes" ]; then + SECURITY="-security" +fi + +# Define other required variables +CATALINA_PID="/var/run/$NAME.pid" +CATALINA_SH="$CATALINA_HOME/bin/catalina.sh" + +# Look for Java Secure Sockets Extension (JSSE) JARs +if [ -z "${JSSE_HOME}" -a -r "${JAVA_HOME}/jre/lib/jsse.jar" ]; then + JSSE_HOME="${JAVA_HOME}/jre/" +fi + +catalina_sh() { + # Escape any double quotes in the value of JAVA_OPTS + JAVA_OPTS="$(echo $JAVA_OPTS | sed 's/\"/\\\"/g')" + + AUTHBIND_COMMAND="" + if [ "$AUTHBIND" = "yes" -a "$1" = "start" ]; then + AUTHBIND_COMMAND="/usr/bin/authbind --deep /bin/bash -c " + fi + + # Define the command to run Tomcat's catalina.sh as a daemon + # set -a tells sh to export assigned variables to spawned shells. + TOMCAT_SH="set -a; JAVA_HOME=\"$JAVA_HOME\"; source \"$DEFAULT\"; \ + CATALINA_HOME=\"$CATALINA_HOME\"; \ + CATALINA_BASE=\"$CATALINA_BASE\"; \ + JAVA_OPTS=\"$JAVA_OPTS\"; \ + CATALINA_PID=\"$CATALINA_PID\"; \ + CATALINA_TMPDIR=\"$CATALINA_TMPDIR\"; \ + LANG=\"$LANG\"; JSSE_HOME=\"$JSSE_HOME\"; \ + cd \"$CATALINA_BASE\"; \ + \"$CATALINA_SH\" $@" + + if [ "$AUTHBIND" = "yes" -a "$1" = "start" ]; then + TOMCAT_SH="'$TOMCAT_SH'" + fi + + # Run the catalina.sh script as a daemon + set +e + touch "$CATALINA_PID" "$CATALINA_BASE"/logs/catalina.out + chown $TOMCAT7_USER "$CATALINA_PID" "$CATALINA_BASE"/logs/catalina.out + start-stop-daemon --start -b -u "$TOMCAT7_USER" -g "$TOMCAT7_GROUP" \ + -c "$TOMCAT7_USER" -d "$CATALINA_TMPDIR" -p "$CATALINA_PID" \ + -x /bin/bash -- -c "$AUTHBIND_COMMAND $TOMCAT_SH" + status="$?" + set +a -e + return $status +} + +case "$1" in + start) + if [ -z "$JAVA_HOME" ]; then + log_failure_msg "no JDK or JRE found - please set JAVA_HOME" + exit 1 + fi + + if [ ! -d "$CATALINA_BASE/conf" ]; then + log_failure_msg "invalid CATALINA_BASE: $CATALINA_BASE" + exit 1 + fi + + log_daemon_msg "Starting $DESC" "$NAME" + if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \ + --user $TOMCAT7_USER --exec "$JAVA_HOME/bin/java" \ + >/dev/null; then + + # Regenerate POLICY_CACHE file + umask 022 + echo "// AUTO-GENERATED FILE from /etc/tomcat7/policy.d/" \ + > "$POLICY_CACHE" + echo "" >> "$POLICY_CACHE" + cat $CATALINA_BASE/conf/policy.d/*.policy \ + >> "$POLICY_CACHE" + + # Remove / recreate JVM_TMP directory + rm -rf "$JVM_TMP" + mkdir -p "$JVM_TMP" || { + log_failure_msg "could not create JVM temporary directory" + exit 1 + } + chown $TOMCAT7_USER "$JVM_TMP" + + catalina_sh start $SECURITY + sleep 5 + log_end_msg 0 + else + log_progress_msg "(already running)" + log_end_msg 0 + fi + ;; + stop) + log_daemon_msg "Stopping $DESC" "$NAME" + + set +e + if [ -f "$CATALINA_PID" ]; then + start-stop-daemon --stop --pidfile "$CATALINA_PID" \ + --user "$TOMCAT7_USER" \ + --retry=TERM/20/KILL/5 >/dev/null + if [ $? -eq 1 ]; then + log_progress_msg "$DESC is not running but pid file exists, cleaning up" + elif [ $? -eq 3 ]; then + PID="`cat $CATALINA_PID`" + log_failure_msg "Failed to stop $NAME (pid $PID)" + exit 1 + fi + rm -f "$CATALINA_PID" + rm -rf "$JVM_TMP" + else + log_progress_msg "(not running)" + fi + log_end_msg 0 + set -e + ;; + status) + set +e + start-stop-daemon --test --start --pidfile "$CATALINA_PID" \ + --user $TOMCAT7_USER --exec "$JAVA_HOME/bin/java" \ + >/dev/null 2>&1 + if [ "$?" = "0" ]; then + + if [ -f "$CATALINA_PID" ]; then + log_success_msg "$DESC is not running, but pid file exists." + exit 1 + else + log_success_msg "$DESC is not running." + exit 3 + fi + else + log_success_msg "$DESC is running with pid `cat $CATALINA_PID`" + fi + set -e + ;; + restart|force-reload) + if [ -f "$CATALINA_PID" ]; then + $0 stop + sleep 1 + fi + $0 start + ;; + try-restart) + if start-stop-daemon --test --start --pidfile "$CATALINA_PID" \ + --user $TOMCAT7_USER --exec "$JAVA_HOME/bin/java" \ + >/dev/null; then + $0 start + fi + ;; + *) + log_success_msg "Usage: $0 {start|stop|restart|try-restart|force-reload|status}" + exit 1 + ;; +esac + +exit 0 diff --git a/bigbluebutton-html5/restart.sh b/bigbluebutton-html5/restart.sh new file mode 100755 index 0000000000000000000000000000000000000000..b92334efd78042b2c32bb3b5e7074dd1aa11d2f0 --- /dev/null +++ b/bigbluebutton-html5/restart.sh @@ -0,0 +1,19 @@ +#!/bin/bash -x + +ID=`docker ps --format "{{.ID}}" --filter ancestor=bigbluebutton/b2` + +if [ "$ID" != "" ]; then + docker stop $ID +fi +docker build -t bigbluebutton/b2 . + +docker run -p 80:80/tcp -p 443:443/tcp -p 1935:1935/tcp -p 5066:5066/tcp -p 3478:3478/udp -p 3478:3478 -v /home/firstuser/dev/bigbluebutton/bigbluebutton-html5:/root/bigbluebutton-html5 --cap-add=NET_ADMIN bigbluebutton/b2 -h 192.168.0.130 > /dev/null + +cat << HERE + + docker exec -it `docker ps --format "{{.ID}}" --filter ancestor=bigbluebutton/b2` supervisorctl status + docker exec -it `docker ps --format "{{.ID}}" --filter ancestor=bigbluebutton/b2` /bin/bash + +HERE + +ID=`docker ps --format "{{.ID}}" --filter ancestor=bigbluebutton/b2` diff --git a/bigbluebutton-html5/setup.sh b/bigbluebutton-html5/setup.sh new file mode 100755 index 0000000000000000000000000000000000000000..9ab64272478cfc348bf557edb43fd6d8642584b6 --- /dev/null +++ b/bigbluebutton-html5/setup.sh @@ -0,0 +1,230 @@ +#!/bin/bash + +# +# BlueButton open source conferencing system - http://www.bigbluebutton.org/ +# +# Copyright (c) 2018 BigBlueButton Inc. +# +# 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/>. +# +set -x + +change_var_value () { + sed -i "s<^[[:blank:]#]*\(${2}\).*<\1=${3}<" $1 +} + +# docker run -p 80:80/tcp -p 443:443/tcp -p 1935:1935 -p 5066:5066 -p 3478:3478 -p 3478:3478/udp b2 -h 192.168.0.130 + +while getopts "eh:" opt; do + case $opt in + e) + env + exit + ;; + h) + HOST=$OPTARG + ;; + e) + SECRET=$OPTARG + ;; + :) + echo "Missing option argument for -$OPTARG" >&2; + exit 1 + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + cat<<HERE +Docker startup script for BigBlueButton. + + -h Hostname for BigBlueButton server + -s Shared secret + +HERE + exit 1 + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + exit 1 + ;; + esac +done + +apt-get install -y bbb-demo && /etc/init.d/tomcat7 start +while [ ! -f /var/lib/tomcat7/webapps/demo/bbb_api_conf.jsp ]; do sleep 1; done +sudo /etc/init.d/tomcat7 stop + + +# Setup the BigBlueButton configuration files +# +PROTOCOL_HTTP=http +PROTOCOL_RTMP=rtmp + +IP=$(echo "$(LANG=c ifconfig | awk -v RS="" '{gsub (/\n[ ]*inet /," ")}1' | grep ^et.* | grep addr: | head -n1 | sed 's/.*addr://g' | sed 's/ .*//g')$(LANG=c ifconfig | awk -v RS="" '{gsub (/\n[ ]*inet /," ")}1' | grep ^en.* | grep addr: | head -n1 | sed 's/.*addr://g' | sed 's/ .*//g')" | head -n1) + +xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_rtp_ip=")]/@data' --value "stun:coturn" /opt/freeswitch/conf/vars.xml +xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "external_sip_ip=")]/@data' --value "stun:coturn" /opt/freeswitch/conf/vars.xml +xmlstarlet edit --inplace --update '//X-PRE-PROCESS[@cmd="set" and starts-with(@data, "local_ip_v4=")]/@data' --value "${IP}" /opt/freeswitch/conf/vars.xml + +sed -i "s/proxy_pass .*/proxy_pass $PROTOCOL_HTTP:\/\/$IP:5066;/g" /etc/bigbluebutton/nginx/sip.nginx + +sed -i "s/http[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/$PROTOCOL_HTTP:\/\/$HOST\2/g" /var/www/bigbluebutton/client/conf/config.xml +sed -i "s/rtmp[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/$PROTOCOL_RTMP:\/\/$HOST\2/g" /var/www/bigbluebutton/client/conf/config.xml + +sed -i "s/server_name .*/server_name $HOST;/g" /etc/nginx/sites-available/bigbluebutton + +sed -i "s/bigbluebutton.web.serverURL=http[s]*:\/\/.*/bigbluebutton.web.serverURL=$PROTOCOL_HTTP:\/\/$HOST/g" \ + /var/lib/tomcat7/webapps/bigbluebutton/WEB-INF/classes/bigbluebutton.properties + +# Update Java screen share configuration +change_var_value /usr/share/red5/webapps/screenshare/WEB-INF/screenshare.properties streamBaseUrl rtmp://$HOST/screenshare +change_var_value /usr/share/red5/webapps/screenshare/WEB-INF/screenshare.properties jnlpUrl $PROTOCOL_HTTP://$HOST/screenshare +change_var_value /usr/share/red5/webapps/screenshare/WEB-INF/screenshare.properties jnlpFile $PROTOCOL_HTTP://$HOST/screenshare/screenshare.jnlp + +change_var_value /usr/share/red5/webapps/sip/WEB-INF/bigbluebutton-sip.properties bbb.sip.app.ip $IP +change_var_value /usr/share/red5/webapps/sip/WEB-INF/bigbluebutton-sip.properties freeswitch.ip $IP + +sed -i "s/bbbWebAPI[ ]*=[ ]*\"[^\"]*\"/bbbWebAPI=\"${PROTOCOL_HTTP}:\/\/$HOST\/bigbluebutton\/api\"/g" \ + /usr/share/bbb-apps-akka/conf/application.conf +sed -i "s/bbbWebHost[ ]*=[ ]*\"[^\"]*\"/bbbWebHost=\"$HOST\"/g" \ + /usr/share/bbb-apps-akka/conf/application.conf +sed -i "s/deskshareip[ ]*=[ ]*\"[^\"]*\"/deskshareip=\"$HOST\"/g" \ + /usr/share/bbb-apps-akka/conf/application.conf +sed -i "s/defaultPresentationURL[ ]*=[ ]*\"[^\"]*\"/defaultPresentationURL=\"${PROTOCOL_HTTP}:\/\/$HOST\/default.pdf\"/g" \ + /usr/share/bbb-apps-akka/conf/application.conf + +cat > /etc/kurento/modules/kurento/WebRtcEndpoint.conf.ini << HERE +; Only IP address are supported, not domain names for addresses +; You have to find a valid stun server. You can check if it works +; using this tool: +; http://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ +;stunServerAddress=64.233.177.127 +;stunServerPort=19302 + +turnURL=kurento:kurento@${HOST}:3478 + +;pemCertificate is deprecated. Please use pemCertificateRSA instead +;pemCertificate=<path> +;pemCertificateRSA=<path> +;pemCertificateECDSA=<path> +HERE + +TURN_SECRET=`openssl rand -hex 16` + +# Configure coturn to handle incoming UDP connections +cat > /etc/turnserver.conf << HERE +denied-peer-ip=0.0.0.0-255.255.255.255 +allowed-peer-ip=$IP +fingerprint +lt-cred-mech +use-auth-secret +static-auth-secret=$TURN_SECRET +user=user:password +log-file=/var/log/turn.log +HERE + +# Setup tomcat7 to share the TURN server information with clients (with matching secret) +cat > /var/lib/tomcat7/webapps/bigbluebutton/WEB-INF/spring/turn-stun-servers.xml << HERE +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + <bean id="turn0" class="org.bigbluebutton.web.services.turn.TurnServer"> + <constructor-arg index="0" value="$TURN_SECRET" /> + <constructor-arg index="1" value="turn:$HOST:3478" /> + <constructor-arg index="2" value="86400" /> + </bean> + <bean id="turn1" class="org.bigbluebutton.web.services.turn.TurnServer"> + <constructor-arg index="0" value="$TURN_SECRET" /> + <constructor-arg index="1" value="turn:$HOST:3478?transport=tcp" /> + <constructor-arg index="2" value="86400" /> + </bean> + <bean id="stunTurnService" class="org.bigbluebutton.web.services.turn.StunTurnService"> + <property name="stunServers"> + <set /> + </property> + <property name="turnServers"> + <set> + <ref bean="turn0" /> + <ref bean="turn1" /> + </set> + </property> + <property name="remoteIceCandidates"> + <set /> + </property> + </bean> +</beans> +HERE + +cat > /opt/freeswitch/conf/autoload_configs/acl.conf.xml << HERE +<configuration name="acl.conf" description="Network Lists"> + <network-lists> + <list name="domains" default="allow"> + <!-- domain= is special it scans the domain from the directory to build the ACL --> + <node type="allow" domain="\$\${domain}"/> + <!-- use cidr= if you wish to allow ip ranges to this domains acl. --> + <!-- <node type="allow" cidr="192.168.0.0/24"/> --> + </list> + + <list name="webrtc-turn" default="deny"> + <node type="allow" cidr="$IP/32"/> + </list> + + </network-lists> +</configuration> +HERE + + +# Ensure bbb-apps-akka has the latest shared secret from bbb-web +SECRET=$(cat /var/lib/tomcat7/webapps/bigbluebutton/WEB-INF/classes/bigbluebutton.properties | grep -v '#' | grep securitySalt | cut -d= -f2); +sed -i "s/sharedSecret[ ]*=[ ]*\"[^\"]*\"/sharedSecret=\"$SECRET\"/g" \ + /usr/share/bbb-apps-akka/conf/application.conf + +sed -i "s/BigBlueButtonURL = \"http[s]*:\/\/\([^\"\/]*\)\([\"\/]\)/BigBlueButtonURL = \"$PROTOCOL_HTTP:\/\/$HOST\2/g" \ + /var/lib/tomcat7/webapps/demo/bbb_api_conf.jsp + +sed -i "s/playback_host: .*/playback_host: $HOST/g" /usr/local/bigbluebutton/core/scripts/bigbluebutton.yml + +sed -i 's/daemonize no/daemonize yes/g' /etc/redis/redis.conf + +sed -i "s|\"wsUrl.*|\"wsUrl\": \"ws://$HOST/bbb-webrtc-sfu\",|g" \ + /usr/share/meteor/bundle/programs/server/assets/app/config/settings-production.json + +rm /usr/share/red5/log/sip.log + +# Add a sleep to each recording process so we can restart with supervisord +# (This works around the limitation that supervisord can't restart after intervals) +sed -i 's/BigBlueButton.logger.debug("rap-archive-worker done")/sleep 20; BigBlueButton.logger.debug("rap-archive-worker done")/g' /usr/local/bigbluebutton/core/scripts/rap-archive-worker.rb +sed -i 's/BigBlueButton.logger.debug("rap-process-worker done")/sleep 20; BigBlueButton.logger.debug("rap-process-worker done")/g' /usr/local/bigbluebutton/core/scripts/rap-process-worker.rb +sed -i 's/BigBlueButton.logger.debug("rap-sanity-worker done")/sleep 20 ; BigBlueButton.logger.debug("rap-sanity-worker done")/g' /usr/local/bigbluebutton/core/scripts/rap-sanity-worker.rb +sed -i 's/BigBlueButton.logger.debug("rap-publish-worker done")/sleep 20; BigBlueButton.logger.debug("rap-publish-worker done")/g' /usr/local/bigbluebutton/core/scripts/rap-publish-worker.rb + +# Start BigBlueButton! +# + +export NODE_ENV=production + +export DAEMON_LOG=/var/log/kurento-media-server +export GST_DEBUG="3,Kurento*:4,kms*:4" +export KURENTO_LOGS_PATH=$DAEMON_LOG + +cat << HERE + +BigBlueButton is now starting up at this address + + http://$HOST + +HERE + +updatedb +exec /usr/bin/supervisord > /var/log/supervisord.log + diff --git a/bigbluebutton-html5/supervisord.conf b/bigbluebutton-html5/supervisord.conf new file mode 100644 index 0000000000000000000000000000000000000000..953a8dda08dad7e644ef0c58cde36ca76d2de050 --- /dev/null +++ b/bigbluebutton-html5/supervisord.conf @@ -0,0 +1,112 @@ +[supervisord] +nodaemon=true + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[program:redis-server] +startsecs = 0 +autorestart = false +#user=redis +command=/usr/bin/redis-server /etc/redis/redis.conf +stdout_logfile=/var/log/redis/stdout.log +stderr_logfile=/var/log/redis/stderr.log + +[program:nginx] +startsecs = 0 +autorestart = false +command=/usr/sbin/nginx -g "daemon off;" + +[program:freeswitch] +startsecs = 0 +autorestart = false +user=freeswitch +group=daemon +directory=/opt/freeswitch +command=/opt/freeswitch/bin/freeswitch -nc -nf -core -nonat + +[program:bbb-apps-akka] +startsecs = 0 +autorestart = false +user=bigbluebutton +directory=/usr/share/bbb-apps-akka +command=/usr/share/bbb-apps-akka/bin/bbb-apps-akka + +[program:bbb-fsesl-akka] +startsecs = 0 +autorestart = false +user=bigbluebutton +directory=/usr/share/bbb-fsesl-akka +command=/usr/share/bbb-fsesl-akka/bin/bbb-fsesl-akka + +[program:red5] +startsecs = 0 +autorestart = false +user=red5 +directory=/usr/share/red5 +command=/usr/share/red5/red5.sh + +[program:rap-archive-worker] +command=/usr/local/bigbluebutton/core/scripts/rap-archive-worker.rb +directory=/usr/local/bigbluebutton/core/scripts +user=tomcat7 +autorestart=true + +[program:rap-process-worker] +command=/usr/local/bigbluebutton/core/scripts/rap-process-worker.rb +directory=/usr/local/bigbluebutton/core/scripts +user=tomcat7 +autorestart=true + +[program:rap-sanity-worker] +command=/usr/local/bigbluebutton/core/scripts/rap-sanity-worker.rb +directory=/usr/local/bigbluebutton/core/scripts +user=tomcat7 +autorestart=true + +[program:rap-publish-worker] +command=/usr/local/bigbluebutton/core/scripts/rap-publish-worker.rb +directory=/usr/local/bigbluebutton/core/scripts +user=tomcat7 +autorestart=true + +[program:mongod] +command=/usr/bin/mongod --quiet --config /etc/mongod.conf +stdout_logfile=/var/log/supervisor/%(program_name)s.log +stderr_logfile=/var/log/supervisor/%(program_name)s.log +user=mongodb +autorestart=true + +[program:bbb-webrtc-sfu] +command=/usr/bin/node server.js +directory=/usr/local/bigbluebutton/bbb-webrtc-sfu +user=bigbluebutton +group=bigbluebutton +autorestart=true + +[program:kurento-media-server] +command=/usr/bin/kurento-media-server +directory=/usr/share/meteor/bundle +user=kurento +group=kurento +autorestart=true + +[program:bbb-html5] +command=npm start +directory=/bigbluebutton-html5 +#user=meteor +#group=meteor +autorestart=true + +[program:tomcat7] +startsecs = 0 +autorestart = false +user=tomcat7 +command=/usr/lib/jvm/java-1.8.0-openjdk-amd64/jre/bin/java -Djava.util.logging.config.file=/var/lib/tomcat7/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.awt.headless=true -Xmx128m -XX:+UseConcMarkSweepGC -Xms256m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/bigbluebutton/diagnostics -Djava.endorsed.dirs=/usr/share/tomcat7/endorsed -classpath /usr/share/tomcat7/bin/bootstrap.jar:/usr/share/tomcat7/bin/tomcat-juli.jar -Dcatalina.base=/var/lib/tomcat7 -Dcatalina.home=/usr/share/tomcat7 -Djava.io.tmpdir=/tmp/tomcat7-tomcat7-tmp org.apache.catalina.startup.Bootstrap start + +[program:coturn] +startsecs = 0 +autorestart = false +user=turnserver +command=/usr/bin/turnserver -c /etc/turnserver.conf -u kurento:kurento + diff --git a/bigbluebutton-html5/test-html5.sh b/bigbluebutton-html5/test-html5.sh new file mode 100755 index 0000000000000000000000000000000000000000..0ed4a0acd9954996e0ff0613ea10e5281d4f6649 --- /dev/null +++ b/bigbluebutton-html5/test-html5.sh @@ -0,0 +1,26 @@ +# Change to HTML5 directory +cd $(dirname $0) +echo "Working directory: $PWD" + +# Build and run Docker image +docker build -t b2 . +docker=$(docker run -d -p 80:80/tcp -p 443:443/tcp -p 1935:1935 -p 5066:5066 -p 3478:3478 -p 3478:3478/udp b2 -h 10.130.218.149) +echo $docker + +# Check if HTML5 client is ready +cd tests/puppeteer +node test-html5-check.js +status=$? +echo $status + +# Run tests +if [ $status -eq 0 ]; then + node test-chat.js + node test-draw.js + node test-upload.js + node test-switch-slides.js + node test-status.js +fi + +# Stop Docker container +docker stop $docker diff --git a/bigbluebutton-html5/tests/puppeteer/.env_template b/bigbluebutton-html5/tests/puppeteer/.env_template new file mode 100644 index 0000000000000000000000000000000000000000..d8e887232b5e5c27cb92ec9c33f99f39e7c7b5af --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/.env_template @@ -0,0 +1,2 @@ +BBB_SERVER_URL="" +BBB_SHARED_SECRET="" \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/.gitignore b/bigbluebutton-html5/tests/puppeteer/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fc13734ed0d13b5411b3be4010853d63c9e961f3 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +screenshots/* +!screenshots/screenshots.txt +package-lock.json +.directory +.env \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/README.md b/bigbluebutton-html5/tests/puppeteer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4b51d976302d75ab8235de841649b5f30b9db624 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/README.md @@ -0,0 +1 @@ +# bigbluebutton-tests \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/elements.js b/bigbluebutton-html5/tests/puppeteer/elements.js new file mode 100644 index 0000000000000000000000000000000000000000..7a1fe15664df8eeb5f6350e16db5c6d167c5ac8c --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/elements.js @@ -0,0 +1,43 @@ +exports.audioDialog = '[aria-label="Modal"]'; +exports.closeAudio = '[aria-label="Close"]'; +exports.microphoneButton = 'button[aria-label="Microphone"]' +exports.listenButton = 'button[aria-label="Listen Only"]'; +exports.echoYes = 'button[aria-label="Echo is audible"]'; +exports.title = '._imports_ui_components_nav_bar__styles__presentationTitle'; +exports.alerts = '.toastify-content'; +exports.skipSlide = '#skipSlide'; +exports.statusIcon = '._imports_ui_components_user_avatar__styles__content'; + +exports.actions = 'button[aria-label="Actions"]'; +exports.options = 'button[aria-label="Options"]'; +exports.userList = 'button[aria-label="Users and Messages Toggle"]'; +exports.uploadPresentation = '._imports_ui_components_dropdown__styles__top-left > div:nth-child(1) > ul:nth-child(1) > li:nth-child(1)'; +exports.joinAudio = 'button[aria-label="Join Audio"]'; +exports.leaveAudio = 'button[aria-label="Leave Audio"]'; +exports.videoMenu = 'button[aria-label="Open video menu dropdown"]'; +exports.screenShare = 'button[aria-label="Share your screen"]'; + +exports.chatButton = 'div._imports_ui_components_user_list_chat_list_item__styles__chatName'; +exports.chatBox = '#message-input'; +exports.sendButton = '[aria-label="Send Message"]'; +exports.chatMessages = '#chat-messages'; + +exports.whiteboard = 'svg._imports_ui_components_presentation__styles__svgStyles'; +exports.toolbox = '._imports_ui_components_whiteboard_whiteboard_toolbar__styles__toolbarContainer'; +exports.tools = 'button[aria-label="Tools"]'; +exports.pencil = 'button[aria-label="Pencil"]'; +exports.rectangle = 'button[aria-label="Rectangle"]'; + +exports.presentationToolbarWrapper = '#presentationToolbarWrapper'; +exports.nextSlide = 'button[aria-label="Next slide"]'; +exports.prevSlide = 'button[aria-label="Previous slide"]'; + +exports.fileUpload = 'input[type="file"]'; +exports.start = 'button[aria-label="Start"]'; +exports.cancel = 'button[aria-label="Cancel]'; + +exports.firstUser = 'div._imports_ui_components_user_list_user_list_content__styles__participantsList:nth-child(1)'; +exports.setStatus = '._imports_ui_components_user_list_user_list_content_user_participants_user_list_item_user_dropdown__styles__usertListItemWithMenu > div:nth-child(2) > div:nth-child(1) > ul:nth-child(1) > li:nth-child(1)'; +exports.away = '._imports_ui_components_user_list_user_list_content_user_participants_user_list_item_user_dropdown__styles__usertListItemWithMenu > div:nth-child(2) > div:nth-child(1) > ul:nth-child(1) > li:nth-child(3)'; +exports.applaud = 'li._imports_ui_components_dropdown_list__styles__item:nth-child(9)'; +exports.clearStatus = '._imports_ui_components_user_list_user_list_content_user_participants_user_list_item_user_dropdown__styles__usertListItemWithMenu > div:nth-child(2) > div:nth-child(1) > ul:nth-child(1) > li:nth-child(2)'; \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/helper.js b/bigbluebutton-html5/tests/puppeteer/helper.js new file mode 100644 index 0000000000000000000000000000000000000000..4ec65b4cc550af458d41431fc07956acb64163af --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/helper.js @@ -0,0 +1,49 @@ +require('dotenv').config(); +const sha1 = require('sha1'); +const axios = require('axios'); + +const params = require('./params'); +const e = require('./elements'); + +function getRandomInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; +} + +async function createMeeting(params) +{ + var meetingID = "random-" + getRandomInt(1000000, 10000000).toString(); + var mp = params.moderatorPW; + var ap = params.attendeePW; + var query = `name=${meetingID}&meetingID=${meetingID}&attendeePW=${ap}&moderatorPW=${mp}&joinViaHtml5=true` + + `&record=false&allowStartStopRecording=true&autoStartRecording=false&welcome=${params.welcome}`; + var apicall = "create" + query + params.secret; + var checksum = sha1(apicall); + var url = params.server + "/create?" + query + "&checksum=" + checksum; + var response = await axios.get(url); + return meetingID; +} + +function getJoinURL(meetingID, params, moderator) +{ + var pw = moderator ? params.moderatorPW : params.attendeePW; + var query = `fullName=${params.fullName}&joinViaHtml5=true&meetingID=${meetingID}&password=${pw}`; + var apicall = "join" + query + params.secret; + var checksum = sha1(apicall); + var url = params.server + "/join?" + query + "&checksum=" + checksum; + return url; +} + +function sleep(time) +{ + return new Promise((resolve) => + { + setTimeout(resolve, time); + }); +} + +exports.getRandomInt = getRandomInt; +exports.createMeeting = createMeeting; +exports.getJoinURL = getJoinURL; +exports.sleep = sleep; \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/package.json b/bigbluebutton-html5/tests/puppeteer/package.json new file mode 100644 index 0000000000000000000000000000000000000000..96b7c4162cc5f0dd5657d8217f6bfe68b5b24a2f --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/package.json @@ -0,0 +1,17 @@ +{ + "name": "bigbluebutton-tests", + "version": "1.0.0", + "description": "", + "main": "app.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^0.18.0", + "dotenv": "^6.0.0", + "puppeteer": "^1.7.0", + "sha1": "^1.1.1" + } +} diff --git a/bigbluebutton-html5/tests/puppeteer/page.js b/bigbluebutton-html5/tests/puppeteer/page.js new file mode 100644 index 0000000000000000000000000000000000000000..7471b0c6fe97ccdb77dda70214d6a13550399d8c --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/page.js @@ -0,0 +1,142 @@ +const puppeteer = require('puppeteer'); +const helper = require('./helper'); +const params = require('./params'); +const e = require('./elements'); + +class Page +{ + // Initializes the page + async init(args) + { + this.browser = await puppeteer.launch(args); + this.page = await this.browser.newPage(); + } + + // Navigates to a page + async goto(page) + { + this.page.goto(page); + } + + // Run the test for the page + async test() + { + } + + // Closes the page + async close() + { + await this.browser.close(); + } + + // Gets the DOM elements being tested, as strings + async getTestElements() + { + } + + // Get the default arguments for creating a page + static getArgs() + { + return {headless: true, args: ["--no-sandbox", "--use-fake-ui-for-media-stream"]}; + } + + // Creates a BigBlueButton meeting + async createBBBMeeting() + { + var meetingID = await helper.createMeeting(params); + await this.joinBBBMeeting(meetingID); + return meetingID; + } + + // Navigates the page to join a BigBlueButton meeting + async joinBBBMeeting(meetingID) + { + var joinURL = helper.getJoinURL(meetingID, params, true); + await this.goto(joinURL); + } + + // Joins a BigBlueButton as a listener + async joinAudioListenOnly() + { + await this.page.waitFor(e.listenButton); + await this.page.click(e.listenButton); + await this.elementRemoved(e.audioDialog); + console.log("Joined meeting as listener"); + } + + // Joins a BigBlueButton meeting with a microphone + async joinAudioMicrophone() + { + await this.page.waitFor(e.microphoneButton); + await this.page.click(e.microphoneButton); + await this.page.waitFor(e.echoYes); + await helper.sleep(500); // Echo test confirmation sometimes fails without this + await this.page.click(e.echoYes); + await this.elementRemoved(e.audioDialog); + console.log("Joined meeting with microphone"); + } + + // Joins a BigBlueButton meeting without audio + async joinWithoutAudio() + { + await this.page.waitFor(e.closeAudio); + await this.page.click(e.closeAudio); + await this.elementRemoved(e.audioDialog); + console.log("Joined meeting without audio"); + } + + // Returns a Promise that resolves when an element does not exist/is removed from the DOM + elementRemoved(element) + { + return this.page.waitFor((element) => {return !document.querySelector(element);}, {}, element); + } + + // Presses a hotkey (Ctrl, Alt and Shift can be held down while pressing the key) + async hotkey(key, ctrl, alt, shift) + { + if(ctrl) {await this.page.keyboard.down('Control');} + if(alt) {await this.page.keyboard.down('Alt');} + if(shift) {await this.page.keyboard.down('Shift');} + + await this.page.keyboard.press(key); + + if(ctrl) {await this.page.keyboard.up('Control');} + if(alt) {await this.page.keyboard.up('Alt');} + if(shift) {await this.page.keyboard.up('Shift');} + } + + // Presses the Tab key a set number of times + async tab(count) + { + for(var i = 0; i < count; i++) + { + await this.page.keyboard.press('Tab'); + } + } + + // Presses the Enter key + async enter() + { + await this.page.keyboard.press('Enter'); + } + + // Presses the Down Arrow key a set number of times + async down(count) + { + for(var i = 0; i < count; i++) + { + await this.page.keyboard.press('ArrowDown'); + } + } + + // Presses the up arrow key a set number of times + async up(count) + { + for(var i = 0; i < count; i++) + { + await this.page.keyboard.press('ArrowUp'); + } + } +}; + +module.exports = exports = Page; \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/params.js b/bigbluebutton-html5/tests/puppeteer/params.js new file mode 100644 index 0000000000000000000000000000000000000000..984336d7043b1cad0b68ca3c8996d68664dfc2a8 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/params.js @@ -0,0 +1,9 @@ +module.exports = exports = +{ + server: process.env.BBB_SERVER_URL, + secret: process.env.BBB_SHARED_SECRET, + welcome: "%3Cbr%3EWelcome+to+%3Cb%3E%25%25CONFNAME%25%25%3C%2Fb%3E%21", + fullName: "User1", + moderatorPW: "mp", + attendeePW: "ap" +}; \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/screenshots/screenshots.txt b/bigbluebutton-html5/tests/puppeteer/screenshots/screenshots.txt new file mode 100644 index 0000000000000000000000000000000000000000..1928861f4008bfa55dbab4cf9a8b10df464ac3ae --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/screenshots/screenshots.txt @@ -0,0 +1 @@ +This is where screenshots from the BigBlueButton tests will be saved. diff --git a/bigbluebutton-html5/tests/puppeteer/test-chat.js b/bigbluebutton-html5/tests/puppeteer/test-chat.js new file mode 100644 index 0000000000000000000000000000000000000000..e3499cadb558f1d3168f066f6d612a6ed2fb2933 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/test-chat.js @@ -0,0 +1,72 @@ +// Test: Sending a chat message + +const Page = require('./page'); +const helper = require('./helper'); +const e = require('./elements'); + +class ChatTestPage extends Page +{ + async test() + { + await this.createBBBMeeting(); + await this.joinWithoutAudio(); + + await this.page.waitFor(e.chatButton); + await this.page.click(e.chatButton); + await this.page.waitFor(e.chatBox); + await this.page.waitFor(e.chatMessages); + + var messages0 = await this.getTestElements(); + + await this.page.type(e.chatBox, "Hello world!"); + await this.page.click(e.sendButton); + await helper.sleep(500); + + await this.page.screenshot({path: "screenshots/test-chat.png"}); + + var messages1 = await this.getTestElements(); + + console.log("\nChat messages before posting:"); + console.log(JSON.stringify(messages0, null, 2)); + console.log("\nChat messages after posting:"); + console.log(JSON.stringify(messages1, null, 2)); + } + + async getTestElements() + { + var messages = await this.page.evaluate((chat) => + { + var messages = []; + var children = document.querySelector(chat).childNodes; + for(var i = 0; i < children.length; i++) + { + var content = children[i].childNodes[0].childNodes[1]; + if(content) + { + content = content.childNodes; + messages.push({name: content[0].innerText, message: content[1].innerText}); + } + } + console.log(messages); + return messages; + }, e.chatMessages); + + return messages; + } +}; + +var test = new ChatTestPage(); +(async() => +{ + try + { + await test.init(Page.getArgs()); + await test.test(); + await test.close(); + } + catch(e) + { + console.log(e); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/test-draw.js b/bigbluebutton-html5/tests/puppeteer/test-draw.js new file mode 100644 index 0000000000000000000000000000000000000000..11030b5e6533298b9eed4ae819932e4a85af83a4 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/test-draw.js @@ -0,0 +1,60 @@ +// Test: Drawing a box + +const Page = require('./page'); +const helper = require('./helper'); +const e = require('./elements'); + +class DrawTestPage extends Page +{ + async test() + { + await this.createBBBMeeting(); + await this.joinWithoutAudio(); + + await this.page.waitFor(e.tools); + await this.page.click(e.tools); + await this.page.waitFor(e.rectangle); + await this.page.click(e.rectangle); + await this.page.waitFor(e.whiteboard); + + var shapes0 = await this.getTestElements(); + + var wb = await this.page.$(e.whiteboard); + var wbBox = await wb.boundingBox(); + await this.page.mouse.move(wbBox.x + 0.3 * wbBox.width, wbBox.y + 0.3 * wbBox.height); + await this.page.mouse.down(); + await this.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height); + await this.page.mouse.up(); + + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-draw.png"}) + var shapes1 = await this.getTestElements(); + + console.log("\nShapes before drawing box:"); + console.log(shapes0); + console.log("\nShapes after drawing box:"); + console.log(shapes1); + } + + async getTestElements() + { + var shapes = await this.page.evaluate(() =>{ return document.querySelector("svg g[clip-path]").children[1].outerHTML; }); + return shapes; + } +}; + +var test = new DrawTestPage(); +(async() => +{ + try + { + await test.init(Page.getArgs()); + await test.test(); + await test.close(); + } + catch(e) + { + console.log(e); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/test-hotkeys-mic-first.js b/bigbluebutton-html5/tests/puppeteer/test-hotkeys-mic-first.js new file mode 100644 index 0000000000000000000000000000000000000000..d41b64b9928b21263c573f4516e87b783ab78acc --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/test-hotkeys-mic-first.js @@ -0,0 +1,84 @@ +// Test: Hotkeys when a user first joins a meeting with a microphone: Leaving audio, rejoining as Listen Only, then rejoining with microphone + +const Page = require('./page'); +const helper = require('./helper'); +const e = require('./elements'); + +class HotkeysMicFirstTestPage extends Page +{ + constructor() + { + super(); + this.tabCounts = + { + audioNoMic: 12, + audioMic: 13 + } + } + + async test() + { + await this.createBBBMeeting(); + await this.joinAudioMicrophone(); + await this.page.screenshot({path: "screenshots/test-hotkeys-mic-first-0.png"}); + + await this.page.waitFor(e.whiteboard); + await this.page.waitFor(e.options); + await this.page.waitFor(e.userList); + await this.page.waitFor(e.toolbox); + await this.page.waitFor(e.leaveAudio); + await this.page.waitFor(e.chatButton); + await this.page.waitFor(e.firstUser); + await this.page.waitFor(e.screenShare); + await this.page.waitFor(e.videoMenu); + await this.page.waitFor(e.actions); + await this.page.waitFor(e.nextSlide); + await this.page.waitFor(e.prevSlide); + + // Leave/Join Audio as Listen Only + await this.elementRemoved(e.alerts); + await this.page.click(e.title); + await this.tab(this.tabCounts.audioMic); + await this.enter(); + await this.enter(); + await this.page.waitFor(e.listenButton); + await this.tab(3); + await this.enter(); + await this.elementRemoved(e.audioDialog); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-mic-first-1.png"}); + + // Leave/Join Audio with Microphone + await this.elementRemoved(e.alerts); + await this.page.click(e.title); + await this.tab(this.tabCounts.audioNoMic); + await this.enter(); + await this.enter(); + await this.page.waitFor(e.microphoneButton); + await this.tab(2); + await this.enter(); + await this.page.waitFor(e.echoYes); + await helper.sleep(500); // Echo test confirmation sometimes fails without this + await this.tab(1); + await this.enter(); + await this.elementRemoved(e.audioDialog); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-mic-first-2.png"}); + } +}; + +var test = new HotkeysMicFirstTestPage(); +(async() => +{ + try + { + await test.init(Page.getArgs()); + await test.test(); + await test.close(); + } + catch(e) + { + console.log(e); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/test-hotkeys.js b/bigbluebutton-html5/tests/puppeteer/test-hotkeys.js new file mode 100644 index 0000000000000000000000000000000000000000..c172aee5fd8e1d67278a8446abe1a6d69ae44858 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/test-hotkeys.js @@ -0,0 +1,180 @@ +// Test: Hotkeys: Options, User List, Leave/Join Audio, Mute/Unmute, Toggle Public Chat, Actions Menu, Status Menu + +const Page = require('./page'); +const helper = require('./helper'); +const e = require('./elements'); + +class HotkeysTestPage extends Page +{ + constructor() + { + super(); + this.tabCounts = + { + options: 1, + actions: 11, + audioNoMic: 12, + audioMic: 13, + mute: 12, + chat: 15, + closeChat: 17, // Only when chat is open + status: 16, + userList: 18 + } + } + + async test() + { + await this.createBBBMeeting(); + await this.joinAudioListenOnly(); + + await this.page.waitFor(e.whiteboard); + await this.page.waitFor(e.options); + await this.page.waitFor(e.userList); + await this.page.waitFor(e.toolbox); + await this.page.waitFor(e.leaveAudio); + await this.page.waitFor(e.chatButton); + await this.page.waitFor(e.firstUser); + await this.page.waitFor(e.screenShare); + await this.page.waitFor(e.videoMenu); + await this.page.waitFor(e.actions); + await this.page.waitFor(e.nextSlide); + await this.page.waitFor(e.prevSlide); + + await this.elementRemoved(e.alerts); + + // Options + await this.page.click(e.title); + await this.tab(this.tabCounts.options); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-options-0.png"}); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-options-1.png"}); + + // User List + await this.page.click(e.title); + await this.tab(this.tabCounts.userList); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-userlist-0.png"}); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-userlist-1.png"}); + + // Toggle Public Chat + await this.elementRemoved(e.alerts); + await this.page.click(e.title); + await this.tab(this.tabCounts.chat); + await this.up(1); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-chat-0.png"}); + await this.page.click(e.title); + await this.tab(this.tabCounts.closeChat); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-chat-1.png"}); + + // Open Actions Menu + await this.page.click(e.title); + await this.tab(this.tabCounts.actions); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-actions-0.png"}); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-actions-1.png"}); + + // Open Status Menu + await this.elementRemoved(e.alerts); + await this.page.click(e.title); + await this.tab(this.tabCounts.status); + await this.up(1); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-status-0.png"}); + await this.tab(1); + await this.enter(); + await this.tab(1); + await this.down(7); //Applaud status + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-status-1.png"}); + + // Leave/Join Audio + await this.page.click(e.title); + await this.tab(this.tabCounts.audioNoMic); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-audio-0.png"}); + await this.enter(); + await this.page.waitFor(e.microphoneButton); + await this.tab(2); + await this.enter(); + await this.page.waitFor(e.echoYes); + await helper.sleep(500); // Echo test confirmation sometimes fails without this + await this.tab(1); + await this.enter(); + await this.elementRemoved(e.audioDialog); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-audio-1.png"}); + + // Mute/Unmute + await this.elementRemoved(e.alerts); + await this.page.click(e.title); + await this.tab(this.tabCounts.mute); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-mute-0.png"}); + await this.enter(); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-hotkeys-mute-1.png"}); + } + + async tab(count) + { + for(var i = 0; i < count; i++) + { + await this.page.keyboard.press('Tab'); + } + } + + async enter() + { + await this.page.keyboard.press('Enter'); + } + + async down(count) + { + for(var i = 0; i < count; i++) + { + await this.page.keyboard.press('ArrowDown'); + } + } + + async up(count) + { + for(var i = 0; i < count; i++) + { + await this.page.keyboard.press('ArrowUp'); + } + } +}; + +var test = new HotkeysTestPage(); +(async() => +{ + try + { + await test.init(Page.getArgs()); + await test.test(); + await test.close(); + } + catch(e) + { + console.log(e); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/test-html5-check.js b/bigbluebutton-html5/tests/puppeteer/test-html5-check.js new file mode 100644 index 0000000000000000000000000000000000000000..d758d81b7830143c796bcea196122e1a77ef43a4 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/test-html5-check.js @@ -0,0 +1,48 @@ +require('dotenv').config(); +const axios = require('axios'); +const url = require('url'); +const helper = require('./helper'); + +(async() => +{ + var bbb = url.parse(process.env.BBB_SERVER_URL) + var check = bbb.protocol + "//" + bbb.hostname + "/html5client/check"; + const maxRetries = 20; + const delay = 10000; + var retryCount = 0; + while(true) + { + try + { + var response = await axios.get(check); + var status = response.data.html5clientStatus + console.log(response.data); + if(status === 'running') + { + break; + } + else if (retryCount < maxRetries) + { + retryCount++; + } + else + { + process.exit(1); + } + } + catch(e) + { + console.log(e.message); + if (retryCount < maxRetries) + { + retryCount++; + } + else + { + process.exit(1); + } + } + console.log("Retrying (attempt " + (retryCount + 1) + "/" + maxRetries + ")..."); + await helper.sleep(delay); + } +})(); \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/test-status.js b/bigbluebutton-html5/tests/puppeteer/test-status.js new file mode 100644 index 0000000000000000000000000000000000000000..faad989bac984915b5b9b44c3f8d95c107118a1f --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/test-status.js @@ -0,0 +1,67 @@ +// Test: Setting/changing/clearing a user's status + +const Page = require('./page'); +const helper = require('./helper'); +const e = require('./elements'); + +class StatusTestPage extends Page +{ + async test() + { + await this.createBBBMeeting(); + await this.joinWithoutAudio(); + await this.page.screenshot({path: "screenshots/test-status-0.png"}); + var status0 = await this.getTestElements(); + + await this.page.click(e.firstUser); + await this.page.click(e.setStatus); + await this.page.click(e.applaud); + await helper.sleep(100); + await this.page.screenshot({path: "screenshots/test-status-1.png"}); + var status1 = await this.getTestElements(); + + await this.page.click(e.firstUser); + await this.page.click(e.setStatus); + await this.page.click(e.away); + await helper.sleep(100); + await this.page.screenshot({path: "screenshots/test-status-2.png"}); + var status2 = await this.getTestElements(); + + await this.page.click(e.firstUser); + await this.page.click(e.clearStatus); + await helper.sleep(100); + await this.page.screenshot({path: "screenshots/test-status-3.png"}); + var status3 = await this.getTestElements(); + + console.log("\nStatus at start of meeting:"); + console.log(status0); + console.log("\nStatus after status set (applaud):"); + console.log(status1); + console.log("\nStatus after status change (away):"); + console.log(status2); + console.log("\nStatus after status clear:"); + console.log(status3); + } + + async getTestElements() + { + var status = await this.page.evaluate((statusIcon) => {return document.querySelector(statusIcon).innerHTML;}, e.statusIcon); + return status; + } +}; + +var test = new StatusTestPage(); +(async() => +{ + try + { + await test.init(Page.getArgs()); + await test.test(); + await test.close(); + } + catch(e) + { + console.log(e); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/test-switch-slides.js b/bigbluebutton-html5/tests/puppeteer/test-switch-slides.js new file mode 100644 index 0000000000000000000000000000000000000000..360942475b989d180aac1fe4940c340e320a93f9 --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/test-switch-slides.js @@ -0,0 +1,57 @@ +// Test: Switching slides + +const Page = require('./page'); +const helper = require('./helper'); +const e = require('./elements'); + +class SlideSwitchTestPage extends Page +{ + async test() + { + await this.createBBBMeeting(); + await this.joinWithoutAudio(); + + await this.page.waitFor(e.whiteboard); + await this.page.waitFor(e.presentationToolbarWrapper); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-switch-slides-0.png"}); + var svg0 = await this.getTestElements(); + await this.page.click(e.nextSlide); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-switch-slides-1.png"}); + var svg1 = await this.getTestElements(); + await this.page.click(e.prevSlide); + await helper.sleep(500); + await this.page.screenshot({path: "screenshots/test-switch-slides-2.png"}); + var svg2 = await this.getTestElements(); + + console.log("\nStarting slide:"); + console.log(svg0); + console.log("\nAfter next slide:"); + console.log(svg1); + console.log("\nAfter previous slide:"); + console.log(svg2); + } + + async getTestElements() + { + var svg = await this.page.evaluate(() =>{ return document.querySelector("svg g g g").outerHTML; }); + return svg; + } +}; + +var test = new SlideSwitchTestPage(); +(async() => +{ + try + { + await test.init(Page.getArgs()); + await test.test(); + await test.close(); + } + catch(e) + { + console.log(e); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/test-upload.js b/bigbluebutton-html5/tests/puppeteer/test-upload.js new file mode 100644 index 0000000000000000000000000000000000000000..91927dd58e1d5b24de7a4f9354d62fd67e6a5cbe --- /dev/null +++ b/bigbluebutton-html5/tests/puppeteer/test-upload.js @@ -0,0 +1,65 @@ +// Test: Uploading an image + +const Page = require('./page'); +const helper = require('./helper'); +const e = require('./elements'); + +class UploadTestPage extends Page +{ + async test() + { + await this.createBBBMeeting(); + await this.joinWithoutAudio(); + + await this.page.waitFor(e.actions); + await this.page.waitFor(e.whiteboard); + await this.page.waitFor(e.skipSlide); + await this.page.click(e.actions); + await this.page.waitFor(e.uploadPresentation); + + var slides0 = await this.getTestElements(); + + await this.page.click(e.uploadPresentation); + await this.page.waitFor(e.fileUpload); + var fileUpload = await this.page.$(e.fileUpload); + await fileUpload.uploadFile(__dirname + "/upload-test.png"); + await this.page.waitFor(e.start); + await this.page.click(e.start); + await this.elementRemoved(e.start); + + await helper.sleep(1000); + await this.page.screenshot({path: "screenshots/test-upload.png"}); + var slides1 = await this.getTestElements(); + + console.log("\nSlides before presentation upload:"); + console.log(slides0.slideList); + console.log(slides0.svg); + console.log("\nSlides after presentation upload:"); + console.log(slides1.slideList); + console.log(slides1.svg); + } + + async getTestElements() + { + var slides = {}; + slides.svg = await this.page.evaluate(() =>{ return document.querySelector("svg g g g").outerHTML; }); + slides.slideList = await this.page.evaluate((skipSlide) =>{ return document.querySelector(skipSlide).innerHTML; }, e.skipSlide); + return slides; + } +}; + +var test = new UploadTestPage(); +(async() => +{ + try + { + await test.init(Page.getArgs()); + await test.test(); + await test.close(); + } + catch(e) + { + console.log(e); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/bigbluebutton-html5/tests/puppeteer/upload-test.png b/bigbluebutton-html5/tests/puppeteer/upload-test.png new file mode 100644 index 0000000000000000000000000000000000000000..89849fb8408ed62d8b2195f51beb14ef3b4a0846 Binary files /dev/null and b/bigbluebutton-html5/tests/puppeteer/upload-test.png differ