EmacsConf backstage: making it easier to do talk-specific actions
| emacs, emacsconfDuring an EmacsConf talk, we:
- copy the talk overlay images and use them in the streaming software (OBS)
- play videos
- a recorded introduction if it exists
- any extra videos we want to play
- the main talk
- and open up browser windows
- the BigBlueButton web conference room for a live Q&A session
- the talk's Etherpad collaborative document for questions
- the Internet Relay Chat channel, if that's where the speaker wants to handle questions
To minimize the work involved in copying and pasting filenames and URLs, I wanted to write scripts that could perform the right action given the talk ID. I automated most of it so that it could work from Emacs Lisp, and I also wrote shell scripts so that I (or someone else) could run the appropriate commands from the terminal.
The shell scripts are in the emacsconf-ansible repository and the Emacs Lisp functions are in emacsconf-stream.el.
Change the image overlay
We display the conference logo, talk title, and speaker name on the
screen while the video is playing. This is handled with an OBS scene
that includes whatever image is at ~/video.png
or ~/other.png
,
since that results in a nicer display than using text in OBS. I'll go
into how we make the overlay images in a different blog post. This
post focuses on including the right image, which is just a matter of
copying the right file over ~/video.png
.
This is copied by set-overlay.
FILE=$1 if [[ ! -f $FILE ]]; then LIST=(/data/emacsconf/assets/stream/emacsconf-[0-9][0-9][0-9][0-9]-$FILE*.webm) FILE="${LIST[0]}" BY_SLUG=1 fi shift SLUG=$(echo "$FILE" | perl -ne 'if (/^emacsconf-[0-9]*-(.*?)--/) { print $1; } else { print; }') if [[ -f /data/emacsconf/assets/overlays/$SLUG-other.png ]]; then echo "Found other overlay for $SLUG, copying" cp /data/emacsconf/assets/overlays/$SLUG-other.png ~/other.png else echo "Could not find /data/emacsconf/assets/overlays/$SLUG-other.png, please override ~/other.png manually" cp /data/emacsconf/assets/overlays/blank-other.png ~/other.png fi if [[ -f /data/emacsconf/assets/overlays/$SLUG-video.png ]]; then echo "Found video overlay for $SLUG, copying" cp /data/emacsconf/assets/overlays/$SLUG-video.png ~/video.png else echo "Could not find /data/emacsconf/assets/overlays/$SLUG-video.png, override ~/video.png manually" cp /data/emacsconf/assets/overlays/blank-video.png ~/video.png fi
set-overlay
is called by the Emacs Lisp function emacsconf-stream-set-overlay
:
emacsconf-stream-set-overlay: Reset the overlay for TALK, just in case.
(defun emacsconf-stream-set-overlay (talk) "Reset the overlay for TALK, just in case. With a prefix argument (\\[universal-argument]), clear the overlay." (interactive (list (if current-prefix-arg (emacsconf-complete-track) (emacsconf-complete-talk-info)))) (emacsconf-stream-track-ssh (emacsconf-get-track talk) "overlay" (if current-prefix-arg "blank" (plist-get talk :slug))))
Play the intro video or display the intro slide
We wanted to display the talk titles, speaker names, and URLs for both the previous talk and the next talk. We generated all the intro slides, and then as time permitted, we recorded introduction videos so that we could practise saying people's names instead of panicking during a full day. Actually generating the intro slide or video is a topic for another blog post. This post just focuses on playing the appropriate video or displaying the right image, which is handled by the intro script.
#!/bin/bash # # Kill the background music if playing if screen -list | grep -q background; then screen -S background -X quit fi # Update the overlay SLUG=$1 FILE=$1 if [[ ! -f $FILE ]]; then LIST=(/data/emacsconf/assets/stream/emacsconf--$FILE--*.webm) FILE="${LIST[0]}" BY_SLUG=1 else SLUG=$(echo "$FILE" | perl -ne 'if (/emacsconf-[0-9]*-(.*?)--/) { print $1; } else { print; }') fi shift overlay $SLUG if [[ -f /data/emacsconf/assets/intros/$SLUG.webm ]]; then mpv /data/emacsconf/assets/intros/$SLUG.webm else firefox /data/emacsconf/assets/in-between/$SLUG.png fi
This is easy to call from Emacs Lisp.
emacsconf-stream-play-intro: Play the recorded intro or display the in-between slide for TALK.
(defun emacsconf-stream-play-intro (talk) "Play the recorded intro or display the in-between slide for TALK." (interactive (list (emacsconf-complete-talk-info))) (setq talk (emacsconf-resolve-talk talk)) (emacsconf-stream-track-ssh talk "nohup" "intro" (plist-get talk :slug)))
Play just the video
Sometimes we might need to restart a video without playing the
introduction again. The ready-to-stream videos are all in one
directory following the naming convention
emacsconf-year-slug--title--speakers--main.webm
. We update the
--main.webm
video as we go through the process of reencoding the
video, normalizing sound, and adding captions. We can play the latest
video by doing a wildcard match based on the slug.
#!/bin/bash # Play intro if recorded, then play files # # Kill the background music if playing if screen -list | grep -q background; then screen -S background -X quit fi # Update the overlay FILE=$1 if [[ ! -f $FILE ]]; then LIST=(/data/emacsconf/assets/stream/emacsconf--$FILE*--main.webm) FILE="${LIST[0]}" BY_SLUG=1 fi shift SLUG=$(echo "$FILE" | perl -ne 'if (/emacsconf-[0-9]*-(.*?)--/) { print $1; } else { print; }') overlay $SLUG mpv $FILE $* &
emacsconf-stream-play-video: Play just the video for TALK.
(defun emacsconf-stream-play-video (talk) "Play just the video for TALK." (interactive (list (emacsconf-complete-talk-info))) (setq talk (emacsconf-resolve-talk talk)) (emacsconf-stream-track-ssh talk "nohup" "play" (plist-get talk :slug)))
Play the intro and then the video
The easiest way to go through a talk is to play the introduction and the video without further manual intervention. This shell script updates the overlay, plays the intro if available, and then continues with the video.
roles/obs/templates/play-with-intro
#!/bin/bash # Play intro if recorded, then play files # # Kill the background music if playing if screen -list | grep -q background; then screen -S background -X quit fi # Update the overlay FILE=$1 if [[ ! -f $FILE ]]; then LIST=(/data/emacsconf/assets/stream/emacsconf--$FILE*.webm) FILE="${LIST[0]}" BY_SLUG=1 fi shift SLUG=$(echo "$FILE" | perl -ne 'if (/emacsconf-[0-9]*-(.*?)--/) { print $1; } else { print; }') overlay $SLUG # Play the video if [[ -f /data/emacsconf/assets/intros/$SLUG.webm ]]; then intro $SLUG fi mpv $FILE $* &
Along the lines of minimizing manual work, this more complex Emacs Lisp function considers different combinations of intros and talks:
Recorded intro | Live intro | |
Recorded talk | automatically play both | show intro slide; remind host to play video |
Live talk | play intro; host joins BBB | join BBB room automatically |
emacsconf-stream-play-talk-on-change: Play the talk.
(defun emacsconf-stream-play-talk-on-change (talk) "Play the talk." (interactive (list (emacsconf-complete-talk-info))) (setq talk (emacsconf-resolve-talk talk)) (when (or (not (boundp 'org-state)) (string= org-state "PLAYING")) (if (plist-get talk :stream-files) (progn (emacsconf-stream-track-ssh talk "overlay" (plist-get talk :slug)) (emacsconf-stream-track-ssh talk (append (list "nohup" "mpv") (split-string-and-unquote (plist-get talk :stream-files)) (list "&")))) (emacsconf-stream-track-ssh talk (cons "nohup" (cond ((and (plist-get talk :recorded-intro) (plist-get talk :video-file)) ;; recorded intro and recorded talk (message "should automatically play intro and recording") (list "play-with-intro" (plist-get talk :slug))) ;; todo deal with stream files ((and (plist-get talk :recorded-intro) (null (plist-get talk :video-file))) ;; recorded intro and live talk; play the intro and join BBB (message "should automatically play intro; join %s" (plist-get talk :bbb-backstage)) (list "intro" (plist-get talk :slug))) ((and (null (plist-get talk :recorded-intro)) (plist-get talk :video-file)) ;; live intro and recorded talk, show slide and use Mumble; manually play talk (message "should show intro slide; play %s afterwards" (plist-get talk :slug)) (list "intro" (plist-get talk :slug))) ((and (null (plist-get talk :recorded-intro)) (null (plist-get talk :video-file))) ;; live intro and live talk, join the BBB (message "join %s for live intro and talk" (plist-get talk :bbb-backstage)) (list "bbb" (plist-get talk :slug)))))))))
Open the Etherpad
We used Etherpad collaborative documents to collect people's questions during the conference. I made an index page that linked to the Etherpads for the different talks so that I could open it in the browser used for streaming.
I also had an Emacs Lisp function that opened up the pad in the appropriate stream.
emacsconf-stream-open-pad: Open the Etherpad collaborative document for TALK.
(defun emacsconf-stream-open-pad (talk) "Open the Etherpad collaborative document for TALK." (interactive (list (emacsconf-complete-talk-info))) (setq talk (emacsconf-resolve-talk talk)) (emacsconf-stream-track-ssh talk "nohup" "firefox" (plist-get talk :pad-url)))
I think I'll add a shell script to make it more consistent, too.
#!/bin/bash # Display the Etherpad collaborative document # # Update the overlay SLUG=$1 overlay $SLUG firefox https://pad.emacsconf.org/-$SLUG
Open the Big Blue Button web conference
Most Q&A sessions are done live through a BigBlueButton web conference. We use redirects to make it easier to go to the talk URL. Backstage redirects are protected by a username and password which is shared with volunteers and saved in the browser used for streaming.
#!/bin/bash # Open the Big Blue Button room using the backstage link # # Kill the background music if playing if screen -list | grep -q background; then screen -S background -X quit fi # Update the overlay SLUG=$1 overlay $SLUG firefox https://media.emacsconf.org//backstage/current/room/$SLUG
Public redirect URLs start off with a refresh loop and then are
overwritten with a redirect to the actual page when the host is okay
with opening up the Q&A for general participation. This is done by
changing the TODO status of the talk from CLOSED_Q
to OPEN_Q
.
emacsconf-publish-bbb-redirect: Update the publicly-available redirect for TALK.
(defun emacsconf-publish-bbb-redirect (talk &optional status) "Update the publicly-available redirect for TALK." (interactive (list (emacsconf-complete-talk-info))) (let ((bbb-filename (expand-file-name (format "bbb-%s.html" (plist-get talk :slug)) emacsconf-publish-current-dir)) (bbb-redirect-url (concat "https://media.emacsconf.org/" emacsconf-year "/current/bbb-" (plist-get talk :slug) ".html")) (status (or status (emacsconf-bbb-status (if (boundp 'org-state) (append (list :status org-state) talk) talk))))) (with-temp-file bbb-filename (insert (emacsconf-replace-plist-in-string (append talk (list :base-url emacsconf-base-url :bbb-redirect-url bbb-redirect-url)) (pcase status ('open "<html><head><meta http-equiv=\"refresh\" content=\"0; URL=${bbb-room}\"></head><body> The live Q&A room for ${title} is now open. You should be redirected to <a href=\"${bbb-room}\">${bbb-room}</a> automatically, but if not, please visit the URL manually to join the Q&A.</body></html>") ('before "<html><head><meta http-equiv=\"refresh\" content=\"5; URL=${bbb-redirect-url}\"></head><body> The Q&A room for ${title} is not yet open. This page will refresh every 5 seconds until the BBB room is marked as open, or you can refresh it manually.</body></html>") ('after "<html><head><body> The Q&A room for ${title} has finished. You can find more information about the talk at <a href=\"${base-url}${url}\">${base-url}${url}</a>.</body></html>") (_ "<html><head><body> There is no live Q&A room for ${title}. You can find more information about the talk at <a href=\"${base-url}${url}\">${base-url}${url}</a>.</body></html>" )))))))
Open up the stream chat
The IRC chat is the same for the whole track instead of changing for each talk. Since we might close the window, it's useful to be able to quickly open it again.
emacsconf-stream-join-chat: Join the IRC chat for TALK.
(defun emacsconf-stream-join-chat (talk) "Join the IRC chat for TALK." (interactive (list (emacsconf-complete-talk-info))) (setq talk (emacsconf-resolve-talk talk)) (emacsconf-stream-track-ssh talk "nohup" "firefox" (plist-get talk :webchat-url)))
Set up for the right Q&A type
An Emacs Lisp function makes it easier to do the right thing depending on the type of Q&A planned for the talk.
emacsconf-stream-join-qa: Join the Q&A for TALK.
(defun emacsconf-stream-join-qa (talk) "Join the Q&A for TALK. This uses the BBB room if available, or the IRC channel if not." (interactive (list (emacsconf-complete-talk-info))) (if (and (null (plist-get talk :video-file)) (string-match "live" (plist-get talk :q-and-a))) (emacsconf-stream-track-ssh talk "nohup" "firefox" "-new-window" (plist-get talk :pad-url)) (emacsconf-stream-track-ssh talk "nohup" "firefox" "-new-window" (pcase (plist-get talk :q-and-a) ((or 'nil "" (rx "Mumble")) (plist-get talk :qa-slide-url)) ((rx "live") (plist-get talk :bbb-backstage)) ((rx "IRC") (plist-get talk :webchat-url)) ((rx "pad") (plist-get talk :pad-url)) (_ (plist-get talk :qa-slide-url))))))
Summary
Combining shell scripts (roles/obs/templates) and Emacs Lisp functions (emacsconf-stream.el) help us simplify the work of taking talk-specific actions that depend on the kind of talk or Q&A session. Using simple identifiers and consistent file name conventions means that we can refer to talks quickly and use wildcards in shell scripts.
We started the conference with me jumping around and running most of the commands, since I had hastily written them in the weeks leading up to the conference and I was the most familiar with them. As the conference went on, other organizers got the hang of the commands and took over running their streams. Yay!