From 9ecec3b12f87dd79e5f283c26ef8616bea712452 Mon Sep 17 00:00:00 2001 From: Michael Koscher <michael.koscher@aau.at> Date: Fri, 8 May 2020 14:23:16 +0200 Subject: [PATCH] add Chat functionality and smaller improvments --- .DS_Store | Bin 0 -> 8196 bytes .gitignore | 4 + Dockerfile | 5 + README.md | 11 ++- chat.py | 92 ++++++++++++++++++ examples/.DS_Store | Bin 0 -> 6148 bytes examples/.env.chat_example | 16 +++ .env.example => examples/.env.example | 0 examples/docker-compose.yml.chat_example | 30 ++++++ .../docker-compose.yml.example | 0 examples/example_player_and_chat.php | 58 +++++++++++ examples/sendChatMessage.php | 8 ++ startStream.sh | 10 +- stream.py | 30 +++--- 14 files changed, 242 insertions(+), 22 deletions(-) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 chat.py create mode 100644 examples/.DS_Store create mode 100644 examples/.env.chat_example rename .env.example => examples/.env.example (100%) create mode 100644 examples/docker-compose.yml.chat_example rename docker-compose.yml.example => examples/docker-compose.yml.example (100%) create mode 100644 examples/example_player_and_chat.php create mode 100644 examples/sendChatMessage.php diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..040b436ffbc376e7a8800ad7f0a1384559a3737a GIT binary patch literal 8196 zcmZQzU|@7AO)+F(kYHe7;9!8z0^AH(0Z1N%F(jFwBElf^7#IW?Jah7slXCKt7(g7T z<R~>70;3@?8UmvsFd71*Auy;yfDz&v4jlD9M0S)M4S~@R7?B|WDjyUeZF>d>DBS>| zK~fBi3=H5d03!nf3oL*bp-q2A1_qE;kQ$Iy5Dn7GzzAZ2%>Zj<V1#OA1b0Ii7#JA@ zz(z4bv@<Y*Z3c;hwKFh+ZDwF#glK1Agxbsq?V&J2v@<Y*ZD(L$1ltZaa+DYifzc2^ z3jt8yorOV<A(bJIp$xVE&M3~nz@YIT48XcT>e3l98A{O9wK6a;a)6cn2dm>^&|?6r zW5{GkXUJp7XDDJwMbq;cO^*yi5*Wra6f=~7)H5V9<T7M3<T0c(=rJTS<TIo&q%!C- zl!HynW+-MTU`S+0#%3<V3?#R(GbA%)FeEaRFz7K9FjS(tg25Wihe8Y~4EYSn4A~5+ z3`GpOV0&{J3K;SkiWyQF^cX4`av5^a%vgzLh6tJ&ppXUms1j@r$VVWz6f<O?n#0(N zW)2sF3zA#X7%~}hu=*A`hBz2f87jabR{&DYP>gB^!!rg3hR;Z$gP}gDytn{5f^#yd zASbi9#K7P>BNH<VD;qlp2NwqyFIQ}EMt*s4Nn%N9u~TAEG>8|FSdx(hWryVF=fK&C zNnx3(<?#X{&iQ#IiJ5t+MIhzDnJKABiA6EtnRzMs<xcsfc`3zUbD$C&9Go1S@d6Un z)h5QqhB^wyCbc>W)#l~~ItnJnX0^4P9O9~mww?*Ol~vU>wRJPWi4I;fFz`e8aB>!S zj0-Jba?%Zhlk;;6;Hp65U}$P`^Icq^fx$6HVaAUAMu!}+nU$NLLX^6K48nGn1sCPz z<maV>t4Ib0VNjxFhzDngLWWX?OfV}KoLx&8iW&46N*F58GbA`!BWD6mh9Cx421f=L z244nO20eych7`1f0ZPQkI{3iZFp;5%p@bnADMRWpU?yBpF@da?6Rfw0Ar+huv1B`N zQitn+RN$laKT=8@WsZgb9YTN^Vuu8%|L@AcfNT68qH2^J4S~@R7?vTx$l?<0;skAR zVDllUuMO%?CqN}ZeQ;2BoDtM7hv)+-0d?)cwK*fGF@;bI(h8CTcf}bQ7(iM_2Lhns MGfItyz(9lm07t<(8UO$Q literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..adf7a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +*.yml +*.code-workspace +*.iml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index acc2b8b..3bb946e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,13 @@ RUN apt-get update && \ ENV BBB_AS_MODERATOR false ENV BBB_USER_NAME Live +ENV BBB_CHAT_NAME Chat +ENV BBB_ENABLE_CHAT false +ENV BBB_REDIS_HOST redis +ENV BBB_REDIS_CHANNEL chat COPY stream.py ./ +COPY chat.py ./ COPY startStream.sh ./ COPY docker-entrypoint.sh ./ diff --git a/README.md b/README.md index b2ad981..d726e0d 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,20 @@ You need to set some environment variables to run the container. #### Optional * BBB_AS_MODERATOR - if set to "true" the meeting will be joined as moderator -* BBB_USER_NAME - the Username to join the meeting. (Default: Live) +* BBB_USER_NAME - the username to join the meeting. (Default: Live) + +### Chat options +* BBB_ENABLE_CHAT - Enable Chat +* BBB_REDIS_HOST - Set REDIS host (Default: redis) +* BBB_REDIS_CHANNEL - Set REDIS channel (Default: chat) +* BBB_CHAT_NAME - the username to join the meeting for chatting. (Default: Chat) ### Starting liveStreaming * wget -O docker-compose.yml https://github.com/aau-zid/BigBlueButton-liveStreaming/raw/master/docker-compose.yml.example * (change configuration) * docker-compose up -d -* docker-compose down +* docker-compose down ## Known Limitations * You must extract and provide the meetingID, which is not visible within the room. -* Some unnecessary html elements are not hidden yet. * ffmpeg settings to be improved diff --git a/chat.py b/chat.py new file mode 100644 index 0000000..e5ee705 --- /dev/null +++ b/chat.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys, argparse, time, logging, os, redis +from bigbluebutton_api_python import BigBlueButton + +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By + +browser = None +selelnium_timeout = 30 +connect_timeout = 5 + +logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) + +parser = argparse.ArgumentParser() +parser.add_argument("-s","--server", help="Big Blue Button Server URL") +parser.add_argument("-p","--secret", help="Big Blue Button Secret") +parser.add_argument("-i","--id", help="Big Blue Button Meeting ID") +parser.add_argument("-m","--moderator", help="Join the meeting as moderator",action="store_true") +parser.add_argument("-u","--user", help="Name to join the meeting",default="Live") +parser.add_argument("-r","--redis", help="Redis hostname",default="redis") +parser.add_argument("-c","--channel", help="Redis channel",default="chat") +args = parser.parse_args() + +bbb = BigBlueButton(args.server,args.secret) + +def set_up(): + global browser + + options = Options() + options.add_argument('--disable-infobars') + options.add_argument('--no-sandbox') + options.add_argument('--kiosk') + options.add_argument('--window-size=1920,1080') + options.add_argument('--window-position=0,0') + options.add_experimental_option("excludeSwitches", ['enable-automation']) + options.add_argument('--incognito') + options.add_argument('--shm-size=1gb') + options.add_argument('--disable-dev-shm-usage') + options.add_argument('--start-fullscreen') + + logging.info('Starting browser to chat!!') + + browser = webdriver.Chrome(executable_path='./chromedriver',options=options) + +def bbb_browser(): + global browser + + logging.info('Open BBB for chat!!') + browser.get(get_join_url()) + + element = EC.presence_of_element_located((By.CSS_SELECTOR, '[aria-label="Listen only"]')) + WebDriverWait(browser, selelnium_timeout).until(element) + browser.find_elements_by_css_selector('[aria-label="Listen only"]')[0].click() + + element = EC.invisibility_of_element((By.CSS_SELECTOR, '.ReactModal__Overlay')) + WebDriverWait(browser, selelnium_timeout).until(element) + + redis_r = redis.Redis(host=args.redis,charset="utf-8", decode_responses=True) + redis_s = redis_r.pubsub() + redis_s.psubscribe(**{args.channel:chat_handler}) + thread = redis_s.run_in_thread(sleep_time=0.001) + +def chat_handler(message): + global browser + browser.find_element_by_id('message-input').send_keys(message['data']) + browser.find_elements_by_css_selector('[aria-label="Send message"]')[0].click() + logging.info(message['data']) + +def get_join_url(): + minfo = bbb.get_meeting_info(args.id) + if args.moderator: + pwd = minfo.get_meetinginfo().get_moderatorpw() + else: + pwd = minfo.get_meetinginfo().get_attendeepw() + return bbb.get_join_meeting_url(args.user,args.id,pwd) + +def chat(): + while True: + time.sleep(60) + + +while bbb.is_meeting_running(args.id).is_meeting_running() != True: + time.sleep(connect_timeout) +set_up() +bbb_browser() +chat() +browser.quit() \ No newline at end of file diff --git a/examples/.DS_Store b/examples/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..061191090bf0ca855938b42988da20a9784a760d GIT binary patch literal 6148 zcmZQzU|@7AO)+F(5MW?n;9!8zOq>i@0Z1N%F(jFwB5WY@z-H(%Br{|%Br=pR#6#sq zslgorptuuc&|^qt$YUsDK#spuh6;v6hFpdMh8%`e&z$_^q@4UDXi{0pz`&sK9}K|W z<3Tb3pT7GH3=9v!+W!Cl&%nSS&yd29&ydWJ&5+7a#GuQN%#aVZF`uEBA(cUop^_n& zA%|F3F^Z$PN{UF+@wwO<%}i+q6x9sz;P6Rgs08~Uo*|JTk0FI29-a^M7z!9N7z&WX z59WUu&A`AQ4E8Tbr!zGF_%ft26f+bvBr>FfLjuDT1_s7fG`E4HVw4ySfzc2c4FMuT zfDxjGgBwDRlA|Fo8Umvs01p9B`Je!4+cP*o=>`Z5l44+FU;uXk7#SE?V44`g{Qw4# z97rpO25AM+Agv6HAQspRuvP{}s8&XBHw2^))FlDYVC@WyV4FdFuyzJUu+0n%j1cV% zj8K~yp*<8vh;{}>h;{}>u<bC{jnbnbFd71|5MYKd1VHt_D+2?r{y#+3C^;GeqaiRX zLx7RRCD_FYTq$GsAE>Sc)u#ziX;2*us*V{!^)f;XTop4x1{5Wr!l3FNq!mPit71k5 U29Va#h5#&pM(NQI7=RD}04h$0)Bpeg literal 0 HcmV?d00001 diff --git a/examples/.env.chat_example b/examples/.env.chat_example new file mode 100644 index 0000000..bc844e9 --- /dev/null +++ b/examples/.env.chat_example @@ -0,0 +1,16 @@ +# BigBlueButton Server url: +BBB_URL=https://your_BigBlueButton_server/bigbluebutton/api +# BigBlueButton secret: +BBB_SECRET=your_secret +# BigBlueButton meetingID: +BBB_MEETING_ID=your_meetingID +# Media server url: +BBB_STREAM_URL=rtmp://media_server_url/stream/stream_key +# Enable chat functionality +BBB_ENABLE_CHAT=true +# Set REDIS host (default: 'redis') +BBB_REDIS_HOST=redis +# Set REDIS channel to subscribe (default: 'chat') +BBB_REDIS_CHANNEL=chat +# Username for the chat (default: 'Chat') +BBB_CHAT_NAME=Chat \ No newline at end of file diff --git a/.env.example b/examples/.env.example similarity index 100% rename from .env.example rename to examples/.env.example diff --git a/examples/docker-compose.yml.chat_example b/examples/docker-compose.yml.chat_example new file mode 100644 index 0000000..db09b67 --- /dev/null +++ b/examples/docker-compose.yml.chat_example @@ -0,0 +1,30 @@ +version: "3.3" +services: + redis: + image: redis + networks: + - app-tier + bbb-streamer: + image: aauzid/bigbluebutton-livestreaming + environment: + # BigBlueButton Server url: + - BBB_URL=https://your_BigBlueButton_server/bigbluebutton/api + # BigBlueButton secret: + - BBB_SECRET=your_secret + # BigBlueButton meetingID: + - BBB_MEETING_ID=your_meetingID + # Media server url: + - BBB_STREAM_URL=rtmp://media_server_url/stream/stream_key + # Enable chat functionality + -BBB_ENABLE_CHAT=true + # Set REDIS host (default: 'redis') + -BBB_REDIS_HOST=redis + # Set REDIS channel to subscribe (default: 'chat') + -BBB_REDIS_CHANNEL=chat + # Username for the chat (default: 'Chat') + -BBB_CHAT_NAME=Chat + networks: + - app-tier +networks: + app-tier: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml.example b/examples/docker-compose.yml.example similarity index 100% rename from docker-compose.yml.example rename to examples/docker-compose.yml.example diff --git a/examples/example_player_and_chat.php b/examples/example_player_and_chat.php new file mode 100644 index 0000000..33a922f --- /dev/null +++ b/examples/example_player_and_chat.php @@ -0,0 +1,58 @@ +<head> + <link href="https://vjs.zencdn.net/7.6.5/video-js.css" rel="stylesheet"> + <script + src="https://code.jquery.com/jquery-3.5.1.min.js" + integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" + crossorigin="anonymous"></script> + + <!-- If you'd like to support IE8 (for Video.js versions prior to v7) --> + + +</head> + +<body> + + <h1>Live Stream</h1> + <div style="float:left; width:49%"> + <video id='my-video-live' class="video-js vjs-default-skin" width='760' height='400'> + <source src="LIVE_STREAM_URL"> + <p class='vjs-no-js'> + To view this video please enable JavaScript, and consider upgrading to a web browser that + <a href='http://videojs.com/html5-video-support/' target='_blank'>supports HTML5 video</a> + </p> + </video> + </div> + <div id="container" style=" float:right; width:49%"> + <form method="post" action="" id="contactform"> + <div class="form-group"> + <h2 >Send Question</h2> + <textarea name="message" rows="15" cols="60" class="form-control" id="message"></textarea> + </div> + <button type="submit" class="btn btn-primary send-message">Submit</button> + </form> + </div> + + <script src='https://vjs.zencdn.net/7.6.6/video.js'></script> + <script type="application/javascript"> + + + $(document).ready(function () { + $('.send-message').click(function (e) { + e.preventDefault(); + var message = $('#message').val(); + $.ajax + ({ + type: "POST", + url: "sendChatMessage.php", + data: { "message": message }, + success: function (data) { + $('#contactform')[0].reset(); + } + }); + }); + }); + + + </script> + +</body> \ No newline at end of file diff --git a/examples/sendChatMessage.php b/examples/sendChatMessage.php new file mode 100644 index 0000000..2059bcb --- /dev/null +++ b/examples/sendChatMessage.php @@ -0,0 +1,8 @@ +<?php + +if(!empty($_POST)) { + $message = $_POST['message']; + $redis = new Redis(); + $redis->pconnect('REDIS_HOST'); // REDIS_HOST hast to be the same as in BBB_REDIS_HOST + $redis->publish('REDIS_CHANNEL', $message); // REDIS_CHANNEL hast to be the same as in BBB_REDIS_CHANNEL + echo "Message published\n"; \ No newline at end of file diff --git a/startStream.sh b/startStream.sh index 6c80ee4..7bd8c89 100644 --- a/startStream.sh +++ b/startStream.sh @@ -4,6 +4,12 @@ JOIN_AS_MODERATOR=""; if [ "${BBB_AS_MODERATOR}" = "true" ] then JOIN_AS_MODERATOR="-m"; -fi +fi -xvfb-run -n 122 --server-args="-screen 0 1280x720x24" python3 stream.py -s ${BBB_URL} -p ${BBB_SECRET} -i ${BBB_MEETING_ID} -t ${BBB_STREAM_URL} -u ${BBB_USER_NAME} $JOIN_AS_MODERATOR; +if [ "${BBB_ENABLE_CHAT}" = "true" ] +then + xvfb-run -n 133 --server-args="-screen 0 1920x1080x24" python3 chat.py -s ${BBB_URL} -p ${BBB_SECRET} -i ${BBB_MEETING_ID} -r ${BBB_REDIS_HOST} -u ${BBB_CHAT_NAME} -c ${BBB_REDIS_CHANNEL} $JOIN_AS_MODERATOR & + sleep 10 +fi + +xvfb-run -n 122 --server-args="-screen 0 1920x1080x24" python3 stream.py -s ${BBB_URL} -p ${BBB_SECRET} -i ${BBB_MEETING_ID} -t ${BBB_STREAM_URL} -u ${BBB_USER_NAME} $JOIN_AS_MODERATOR; diff --git a/stream.py b/stream.py index 9ea2e27..1bea469 100644 --- a/stream.py +++ b/stream.py @@ -13,7 +13,8 @@ from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By browser = None -timeout = 5 +selelnium_timeout = 30 +connect_timeout = 5 logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) @@ -32,21 +33,14 @@ def set_up(): global browser options = Options() - #options.set_headless(headless=True) - #options.add_argument("--headless") - #options.add_argument('--enable-logging=stderr') - #options.add_argument('--disable-gpu') - #options.add_argument('--enable-usermedia-screen-capturing') - #options.add_argument('--allow-http-screen-capture') - #options.add_argument('--auto-select-desktop-capture-source=bbbrecorder') options.add_argument('--disable-infobars') options.add_argument('--no-sandbox') options.add_argument('--kiosk') - options.add_argument('--window-size=1280,720') + options.add_argument('--window-size=1920,1080') options.add_argument('--window-position=0,0') options.add_experimental_option("excludeSwitches", ['enable-automation']); - #options.add_argument('--shm-size=1gb') - #options.add_argument('--disable-dev-shm-usage') + options.add_argument('--shm-size=1gb') + options.add_argument('--disable-dev-shm-usage') options.add_argument('--start-fullscreen') logging.info('Starting browser!!') @@ -59,13 +53,15 @@ def bbb_browser(): logging.info('Open BBB and hide elements!!') browser.get(get_join_url()) element = EC.presence_of_element_located((By.CSS_SELECTOR, '[aria-label="Listen only"]')) - WebDriverWait(browser, timeout).until(element) + WebDriverWait(browser, selelnium_timeout).until(element) browser.find_elements_by_css_selector('[aria-label="Listen only"]')[0].click() element = EC.invisibility_of_element((By.CSS_SELECTOR, '.ReactModal__Overlay')) - WebDriverWait(browser, timeout).until(element) + WebDriverWait(browser, selelnium_timeout).until(element) browser.find_elements_by_id('chat-toggle-button')[0].click() browser.find_elements_by_css_selector('button[aria-label="Users and messages toggle"]')[0].click() + browser.execute_script("document.querySelector('[aria-label=\"Users and messages toggle\"]').style.display='none';") + browser.execute_script("document.querySelector('[aria-label=\"Options\"]').style.display='none';") def get_join_url(): minfo = bbb.get_meeting_info(args.id) @@ -80,16 +76,16 @@ def watch(): time.sleep(60) def stream(): - logging.info('Starting Stream with cmd: ffmpeg -fflags +igndts -f x11grab -s 1280x720 -r 24 -draw_mouse 0 -i :%d -f alsa -i pulse -ac 2 -preset ultrafaset -crf 0 -pix_fmt yuv420p -s 1280x720 -c:a aac -b:a 160k -ar 44100 -threads 0 -f flv "%s"' % (122, args.target)) - ffmpeg_stream = 'ffmpeg -fflags +igndts -f x11grab -s 1280x720 -r 24 -draw_mouse 0 -i :%d -f alsa -i pulse -ac 2 -preset ultrafaset -pix_fmt yuv420p -s 1280x720 -c:a aac -b:a 160k -ar 44100 -threads 0 -f flv "%s"' % (122, args.target) + logging.info('Starting Stream with cmd: ffmpeg -fflags +igndts -f x11grab -s 1920x1080 -r 24 -draw_mouse 0 -i :%d -f alsa -i pulse -ac 2 -preset ultrafaset -crf 0 -pix_fmt yuv420p -s 1920x1080 -c:a aac -b:a 160k -ar 44100 -threads 0 -f flv "%s"' % (122, args.target)) + ffmpeg_stream = 'ffmpeg -fflags +igndts -f x11grab -s 1920x1080 -r 24 -draw_mouse 0 -i :%d -f alsa -i pulse -ac 2 -preset ultrafaset -pix_fmt yuv420p -s 1920x1080 -c:a aac -b:a 160k -ar 44100 -threads 0 -f flv "%s"' % (122, args.target) ffmpeg_args = shlex.split(ffmpeg_stream) p = subprocess.Popen(ffmpeg_args) while bbb.is_meeting_running(args.id).is_meeting_running() != True: - logging.info("Meeting isn't running. We will try again in %d seconds!" % timeout) - time.sleep(timeout) + logging.info("Meeting isn't running. We will try again in %d seconds!" % connect_timeout) + time.sleep(connect_timeout) set_up() bbb_browser() stream() -- GitLab