diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..040b436ffbc376e7a8800ad7f0a1384559a3737a Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..adf7a64ae918f5bbd153c9596531ce9b0f913fd2 --- /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 acc2b8b3df4faccf2a9b79a19cb601c2ec1d1ed3..3bb946e4b547853873a52b6cae3b4fc9706ae978 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 b2ad981e0fa2e9e2ab9d2fa44d1ae2077a022a43..d726e0d1ac1302e2b076a286bf5b9de4a4ffe2cc 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 0000000000000000000000000000000000000000..e5ee705e528163a82f4f32ef0e8220be3c74dfff --- /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 Binary files /dev/null and b/examples/.DS_Store differ diff --git a/examples/.env.chat_example b/examples/.env.chat_example new file mode 100644 index 0000000000000000000000000000000000000000..bc844e9c41fcd7529a76b5e5347e8ac7c23c4064 --- /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 0000000000000000000000000000000000000000..db09b67e8c1d36ad05c930274b473b6ade6ed690 --- /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 0000000000000000000000000000000000000000..33a922f4b10c7d89956de0a090b959d0c076bf65 --- /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 0000000000000000000000000000000000000000..2059bcbb0c9215d3838e2b0ef0085b39ec9ded1a --- /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 6c80ee45245a30ed4f7f9194f2d4642f53964d56..7bd8c89ff4610a6cb85eac5415ea4f47cee03e23 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 9ea2e277619ce3a35c2fda4b9d10a856f797f84d..1bea46976f65a1ed6b0ec0e586e9c189e74767a7 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()