EmacsConf backstage: making it easier to do talk-specific actions

| emacs, emacsconf

During 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.

sat-open-video.png
Figure 1: Sample overlay file
2023-09-12_10-52-07.png
Figure 2: OBS scene with the overlay

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

buttons.png
Figure 3: Sample 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.

roles/obs/templates/play

#!/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.

2023-09-13_08-44-52.png
Figure 4: Backstage index

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.

roles/obs/templates/pad

#!/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.

roles/obs/templates/bbb

#!/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!

You can comment with Disqus or you can e-mail me at sacha@sachachua.com.