diff --git a/Dockerfile b/Dockerfile index cf5a6275eb4ef2798212039c0995ddd1759d54f6..399ec89666a0abad6c18f7a53dcde1ab17ba8f92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,6 +49,9 @@ ENV BBB_SHOW_CHAT false ENV BBB_ENABLE_CHAT false ENV BBB_REDIS_HOST redis ENV BBB_REDIS_CHANNEL chat +RUN DEBIAN_FRONTEND="noninteractive" apt-get -y install tzdata +ENV TZ Europe/Vienna +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY stream.py ./ COPY chat.py ./ diff --git a/README.md b/README.md index 136b96db7459b2254e6e2eec97e3c916fefb5b5d..3d774ba55a229915b68f47e093268c74c53b1ab2 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You need to set some environment variables to run the container. * BBB_URL - URL to BBB including http/https e.g. https://your_BigBlueButton_server/bigbluebutton/api * BBB_MEETING_ID - ID of the BBB Meeting (You can get the ID via an API call: https://your_bbb_server/bigbluebutton/api/getMeetings?checksum=<checksum>) * BBB_SECRET - Secret of your BBB installation (You can get the secret with: bbb-conf --secret) -* BBB_STREAM_URL - Stream URL to your streaming server including rtmp. (e.g. rtmp://media_server_url/stream/stream_key) +* BBB_STREAM_URL - URL of your streaming server including rtmp. Leave out to disable streaming. (e.g. rtmp://media_server_url/stream/stream_key) #### Optional settings * BBB_AS_MODERATOR - if set to "true" the meeting will be joined as moderator @@ -26,8 +26,13 @@ You need to set some environment variables to run the container. * BBB_ATTENDEE_PASSWORD - attendee password (optional - has to be set to the attendee password of moodle/greenlight or any other frontend to allow joining via their links) * BBB_MODERATOR_PASSWORD - moderator password (optional - has to be set to the moderator password of moodle/greenlight or any other frontend to allow joining via their links) * BBB_MEETING_TITLE - meeting title (optional - only works if the meeting is started by the liveStreaming) +* BBB_DOWNLOAD_MEETING= - download / save BigBlueButton meeting in lossless mkv format +* BBB_INTRO= - play intro file (can be a local file in videodata folder e.g. /video/intro.mp4 or a url of a mediastream e.g. https://my.intro.stream) +* BBB_BEGIN_INTRO_AT=04:40 - begin the intro at position (optional, e.g. 00:00:05) +* BBB_END_INTRO_AT= - end intro after (optional, e.g. 01:00:00 - after one hour) * BBB_USER_NAME - the username to join the meeting. (Default: Live) * BBB_SHOW_CHAT - shows the chat on the left side of the window (Default: false) +* TZ - Timezone (Default: Europe/Vienna) #### Chat settings * BBB_ENABLE_CHAT - Enable Chat feedback channel @@ -42,8 +47,6 @@ You need to set some environment variables to run the container. * docker-compose down ## Known Limitations -* You must extract and provide the meetingID, which is not visible within the room. -* the streamer cannot join meetings, that where not started yet * the streamer does not reconnect, if the connection to BigBlueButton gets lost * when using breakoutrooms, the streamer will show the popup of the invitation and not be able to get back to the conference @@ -61,15 +64,15 @@ Similar to the GDPR and the CCPA, other local privacy law principles also may Always be transparent and act upon privacy rights and federal law Regulations. ## License - BigBlueButton-liveStreaming is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - BigBlueButton-liveStreaming is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with BigBlueButton-liveStreaming. If not, see [GNU website](https://www.gnu.org/licenses) +BigBlueButton-liveStreaming is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +BigBlueButton-liveStreaming is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with BigBlueButton-liveStreaming. If not, see [GNU website](https://www.gnu.org/licenses) diff --git a/chat.py b/chat.py index 2e908e8ced51915c69a8ecba288c83320cd1199f..e081fec3106c1ea478eb0c2e7a17ca3f6648e59f 100644 --- a/chat.py +++ b/chat.py @@ -21,6 +21,10 @@ 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("-S","--startMeeting", help="start the meeting if not running",action="store_true") +parser.add_argument("-A","--attendeePassword", help="attendee password (required to create meetings)") +parser.add_argument("-M","--moderatorPassword", help="moderator password (required to create a meeting)") +parser.add_argument("-T","--meetingTitle", help="meeting title (required to create a meeting)") 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") @@ -51,6 +55,13 @@ def bbb_browser(): global browser logging.info('Open BBB for chat!!') + if args.startMeeting is True: + try: + logging.info("create_meeting...") + create_meeting() + except exception.bbbexception.BBBException as ERR: + logging.info(ERR) + browser.get(get_join_url()) element = EC.presence_of_element_located((By.CSS_SELECTOR, '[aria-label="Listen only"]')) @@ -74,6 +85,16 @@ def chat_handler(message): browser.find_elements_by_css_selector('[aria-label="Send message"]')[0].click() logging.info(message['data']) +def create_meeting(): + create_params = {} + if args.moderatorPassword: + create_params['moderatorPW'] = args.moderatorPassword + if args.attendeePassword: + create_params['attendeePW'] = args.attendeePassword + if args.meetingTitle: + create_params['name'] = args.meetingTitle + return bbb.create_meeting(args.id, params=create_params) + def get_join_url(): minfo = bbb.get_meeting_info(args.id) if args.moderator: diff --git a/examples/docker-compose.yml.example b/examples/docker-compose.yml.example index a564f8fff7fdf38a1109d74725479f32001a67aa..9a65879c83575cd5010baf5d8853b71ad7d4ae8a 100644 --- a/examples/docker-compose.yml.example +++ b/examples/docker-compose.yml.example @@ -18,5 +18,18 @@ services: - BBB_MODERATOR_PASSWORD=JjeQYksarqLQ # meeting title (optional): - BBB_MEETING_TITLE=liveStreaming Test + # download / save BigBlueButton meeting + - BBB_DOWNLOAD_MEETING=false + # play intro file (can be a local file in videodata folder e.g. /video/intro.mp4 or a url of a mediastream e.g. https://my.intro.stream) + - BBB_INTRO=false + # begin the intro at position (optional, e.g. 00:00:05) + - BBB_BEGIN_INTRO_AT=04:40 + # end intro after (optional, e.g. 01:00:00 - after one hour) + - BBB_END_INTRO_AT= # Media server url: - BBB_STREAM_URL=rtmp://media_server_url/stream/stream_key + # Timezone (default: Europe/Vienna): + - TZ=Europe/Vienna + + volumes: + - ./videodata:/video diff --git a/examples/get_meetings.py b/examples/get_meetings.py index 60045754d49ab803515dc86a74be4a2f9148fe06..75c36088ab46ccc17c9899c53d0fa23d34dae720 100755 --- a/examples/get_meetings.py +++ b/examples/get_meetings.py @@ -1,6 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +### to use this script you will need to ### +# apt install pip3 +# pip3 install bigbluebutton_api_python +# pip3 install pyyaml + import argparse, sys, os, logging, yaml, urllib, json from bigbluebutton_api_python import BigBlueButton @@ -8,7 +13,7 @@ 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("-c","--config", help="path to config file in yaml format e.g. your docker-compose.yml", default="../docker-compose.yml") +parser.add_argument("-c","--config", help="path to config file in yaml format e.g. your docker-compose.yml", default="./docker-compose.yml") parser.add_argument("-p","--secret", help="Big Blue Button Secret") args = parser.parse_args() @@ -41,10 +46,17 @@ if not args.server and not args.secret: sys.exit() bbb = BigBlueButton(args.server,args.secret) -meetings = get_meetings() -for meeting in meetings['xml']['meetings']: - print(meetings['xml']['meetings'][meeting]['meetingName']) - print("ID: {}".format(meetings['xml']['meetings'][meeting]['meetingID'])) - print("ATTENDEE_PASSWORD: {}".format(meetings['xml']['meetings'][meeting]['attendeePW'])) - print("MODERATOR_PASSWORD: {}".format(meetings['xml']['meetings'][meeting]['moderatorPW'])) +meetingsXML = get_meetings() +rawMeetings = meetingsXML['xml']['meetings']['meeting'] +meetings = [] +if isinstance(rawMeetings, list): + meetings = rawMeetings +else: + meetings.append(rawMeetings) + +for meeting in meetings: + print(meeting['meetingName']) + print("ID: {}".format(meeting['meetingID'])) + print("ATTENDEE_PASSWORD: {}".format(meeting['attendeePW'])) + print("MODERATOR_PASSWORD: {}".format(meeting['moderatorPW'])) print("") diff --git a/startStream.sh b/startStream.sh index 51b5ecc49ca7f19a510c2ea4437c9af82edd4da7..361f44458aa88e7c2b7ee3873b60a72701188c20 100644 --- a/startStream.sh +++ b/startStream.sh @@ -6,12 +6,42 @@ then JOIN_AS_MODERATOR="-m"; fi +STREAM_MEETING=""; +if [ "${BBB_STREAM_URL}" != "" ] +then + STREAM_MEETING="-l -t ${BBB_STREAM_URL}"; +fi + +DOWNLOAD_MEETING=""; +if [ "${BBB_DOWNLOAD_MEETING}" = "true" ] +then + DOWNLOAD_MEETING="-d"; +fi + SHOW_CHAT=""; if [ "${BBB_SHOW_CHAT}" = "true" ] then SHOW_CHAT="-c"; fi +INTRO=""; +if [ "${BBB_INTRO}" != "" ] +then + INTRO="-I ${BBB_INTRO}"; +fi + +BEGIN_INTRO=""; +if [ "${BBB_BEGIN_INTRO_AT}" != "" ] +then + BEGIN_INTRO="-B ${BBB_BEGIN_INTRO_AT}"; +fi + +END_INTRO=""; +if [ "${BBB_END_INTRO_AT}" != "" ] +then + END_INTRO="-E ${BBB_END_INTRO_AT}"; +fi + START_MEETING=""; if [ "${BBB_START_MEETING}" != "" ] then @@ -42,4 +72,4 @@ then 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} ${SHOW_CHAT} $START_MEETING $ATTENDEE_PASSWORD $MODERATOR_PASSWORD -T "$MEETING_TITLE" $JOIN_AS_MODERATOR; +xvfb-run -n 122 --server-args="-screen 0 1920x1080x24" python3 stream.py -s ${BBB_URL} -p ${BBB_SECRET} -i ${BBB_MEETING_ID} -u ${BBB_USER_NAME} ${SHOW_CHAT} $START_MEETING $ATTENDEE_PASSWORD $MODERATOR_PASSWORD -T "$MEETING_TITLE" $STREAM_MEETING $INTRO $BEGIN_INTRO $END_INTRO $JOIN_AS_MODERATOR $DOWNLOAD_MEETING; diff --git a/stream.py b/stream.py index b2e7db7ca2323929daceb5efa15e6131f741dd22..019c9364a2b946f33fd6750ed57c2d5664aecb5e 100644 --- a/stream.py +++ b/stream.py @@ -12,6 +12,9 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By +from datetime import datetime + +downloadProcess = None browser = None selelnium_timeout = 30 connect_timeout = 5 @@ -22,6 +25,11 @@ 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("-I","--intro", help="Intro file to play before streaming") +parser.add_argument("-B","--beginIntroAt", help="begin intro at position (e.g. 00:01:05)") +parser.add_argument("-E","--endIntroAt", help="End intro at position (e.g. 01:00:04)") +parser.add_argument("-l","--stream", help="live stream a BigBlueButton meeting",action="store_true") +parser.add_argument("-d","--download", help="download / save a BigBlueButton meeting",action="store_true") parser.add_argument("-m","--moderator", help="Join the meeting as moderator",action="store_true") parser.add_argument("-S","--startMeeting", help="start the meeting if not running",action="store_true") parser.add_argument("-A","--attendeePassword", help="attendee password (required to create meetings)") @@ -101,24 +109,57 @@ def get_join_url(): pwd = minfo.get_meetinginfo().get_attendeepw() return bbb.get_join_meeting_url(args.user,args.id, pwd) -def watch(): - while True: - time.sleep(60) +def stream_intro(): + audio_options = '-f alsa -i pulse -ac 2 -c:a aac -b:a 160k -ar 44100' + video_options = '-c:v libx264 -x264-params "nal-hrd=cbr" -profile:v high -level:v 4.2 -vf format=yuv420p -b:v 4000k -maxrate 4000k -minrate 2000k -bufsize 8000k -g 60 -preset ultrafast' + introBegin = "" + if args.beginIntroAt: + introBegin = "-ss %s"%(args.beginIntroAt) + introEnd = "" + if args.endIntroAt: + introEnd = "-to %s"%(args.endIntroAt) + ffmpeg_stream = 'ffmpeg -re %s %s -thread_queue_size 1024 -i %s -thread_queue_size 1024 %s -threads 0 %s -f flv "%s"' % ( introBegin, introEnd, args.intro, audio_options, video_options, args.target) + ffmpeg_args = shlex.split(ffmpeg_stream) + logging.info("streaming intro...") + p = subprocess.call(ffmpeg_args) def stream(): audio_options = '-f alsa -i pulse -ac 2 -c:a aac -b:a 160k -ar 44100' #video_options = ' -c:v libvpx-vp9 -b:v 2000k -crf 33 -quality realtime -speed 5' video_options = '-c:v libx264 -x264-params "nal-hrd=cbr" -profile:v high -level:v 4.2 -vf format=yuv420p -b:v 4000k -maxrate 4000k -minrate 2000k -bufsize 8000k -g 60 -preset ultrafast -tune zerolatency' - ffmpeg_stream = 'ffmpeg -thread_queue_size 1024 -f x11grab -draw_mouse 0 -s 1920x1080 -i :%d %s -threads 0 %s -f flv -flvflags no_duration_filesize "%s"' % ( 122, audio_options, video_options, args.target) + ffmpeg_stream = 'ffmpeg -thread_queue_size 1024 -f x11grab -draw_mouse 0 -s 1920x1080 -i :%d -thread_queue_size 1024 %s -threads 0 %s -f flv -flvflags no_duration_filesize "%s"' % ( 122, audio_options, video_options, args.target) ffmpeg_args = shlex.split(ffmpeg_stream) - p = subprocess.Popen(ffmpeg_args) + logging.info("streaming meeting...") + p = subprocess.call(ffmpeg_args) + +def download(): + downloadFile = "/video/meeting-%s.mkv" % fileTimeStamp + audio_options = '-f alsa -i pulse -ac 2' + video_options = '-c:v libx264rgb -crf 0 -preset ultrafast' + ffmpeg_stream = 'ffmpeg -thread_queue_size 1024 -f x11grab -draw_mouse 0 -s 1920x1080 -i :%d -thread_queue_size 1024 %s %s %s' % ( 122, audio_options, video_options, downloadFile) + ffmpeg_args = shlex.split(ffmpeg_stream) + logging.info("saving meeting as %s" % downloadFile) + return subprocess.Popen(ffmpeg_args) if args.startMeeting is False: while bbb.is_meeting_running(args.id).is_meeting_running() != True: logging.info("Meeting isn't running. We will try again in %d seconds!" % connect_timeout) time.sleep(connect_timeout) + +# current date and time +now = datetime.now() +fileTimeStamp = now.strftime("%Y%m%d%H%M%S") + set_up() -bbb_browser() -stream() -watch() -browser.quit() \ No newline at end of file +if args.stream and args.intro: + stream_intro() +if args.stream or args.download: + bbb_browser() +if args.download: + downloadProcess = download() +if args.stream: + stream() +if downloadProcess: + downloadProcess.communicate(input=None) +if browser: + browser.quit()