summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSacha Chua <sacha@sachachua.com>2026-01-02 10:52:40 -0500
committerSacha Chua <sacha@sachachua.com>2026-01-02 10:52:40 -0500
commitefffda29a9bb0f343418a6f5bf63d234c967e3d6 (patch)
tree88c0a9600f70527013692b59333c519f8dc0ccd8
parent6daeda036b6db86592f61a8fd49d5eb8082aa795 (diff)
downloademacsconf-el-efffda29a9bb0f343418a6f5bf63d234c967e3d6.tar.xz
emacsconf-el-efffda29a9bb0f343418a6f5bf63d234c967e3d6.zip
roughly move bbb code to emacsconf-bbb
-rw-r--r--emacsconf-bbb.el765
-rw-r--r--emacsconf-extract.el737
2 files changed, 770 insertions, 732 deletions
diff --git a/emacsconf-bbb.el b/emacsconf-bbb.el
index 4643dc1..9e0727d 100644
--- a/emacsconf-bbb.el
+++ b/emacsconf-bbb.el
@@ -1,3 +1,30 @@
+;;; emacsconf-bbb.el --- BigBlueButton -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022 Sacha Chua
+
+;; Author: Sacha Chua <sacha@sachachua.com>
+
+
+;; This program 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.
+
+;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; - set BBB_REC to the published presentation URL. If keeping the presentation recording private, don't set BBB_REC; set BBB_MEETING_ID to just the meeting ID.
+;; - Use the command from emacsconf-extract-raw-recordings-download-command to download raw data.
+
+;;; Code:
+
;;;###autoload
(defun emacsconf-bbb-status (talk)
(let ((states
@@ -124,3 +151,741 @@ Room.all.each { |x| puts x.friendly_id + " " + x.name }; nil
(spookfox-js-injection-eval-in-active-tab
"document.querySelector('button[data-rr-ui-event-key=\"settings\"]').click()" t)
(sleep-for 3)))
+
+(defvar emacsconf-extract-bbb-raw-dir (format "~/proj/emacsconf/%s/bbb/" emacsconf-year) "End with a \"/\".")
+(defvar emacsconf-extract-bbb-published-dir (format "~/proj/emacsconf/%s/bbb/published/" emacsconf-year) "End with a \"/\".")
+(defvar emacsconf-extract-bbb-path "/ssh:bbb@bbb.emacsverse.org:/var/bigbluebutton/")
+
+(defun emacsconf-extract-raw-recordings-download-command ()
+ "Copy the command for downloading raw recordings."
+ (interactive)
+ (let ((s (mapconcat (lambda (o) (if (plist-get o :bbb-meeting-id)
+ (format "rsync -avzue ssh %s %s\n"
+ (expand-file-name (plist-get o :bbb-meeting-id) emacsconf-extract-bbb-path)
+ emacsconf-extract-bbb-raw-dir)
+ ""))
+ (emacsconf-get-talk-info))))
+ (when (called-interactively-p 'any) (kill-new s))
+ s))
+
+(defun emacsconf-extract-download-published-recordings-command ()
+ "Copy the command for downloading published recordings from SOURCE to DEST."
+ (interactive)
+ (kill-new
+ (mapconcat (lambda (o) (if (plist-get o :bbb-meeting-id)
+ (replace-regexp-in-string
+ "/ssh:" ""
+ (format "rsync -avzue ssh %s %s # %s\n"
+ (expand-file-name
+ (plist-get o :bbb-meeting-id)
+ (expand-file-name "published/presentation"
+ emacsconf-extract-bbb-path
+ ))
+ emacsconf-extract-bbb-published-dir
+ (plist-get o :slug)))
+ ""))
+ (emacsconf-get-talk-info))))
+
+(defun emacsconf-extract-bbb-parse-events-dir (&optional dir)
+ (mapcar (lambda (file)
+ (emacsconf-extract-bbb-parse-events file))
+ (directory-files-recursively (or dir emacsconf-extract-bbb-raw-dir) "events.xml")))
+
+(defun emacsconf-extract-bbb-raw-events-file-name (talk)
+ (setq talk (emacsconf-resolve-talk talk))
+ (expand-file-name "events.xml" (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-raw-dir)))
+
+(defun emacsconf-extract-bbb-report (&optional event-xml-files)
+ (let* ((max 0)
+ (participant-count 0)
+ (meeting-count 0)
+ (max-meetings 0)
+ (max-participants 0)
+ meeting-participants
+ (meeting-events
+ (sort
+ (seq-mapcat
+ (lambda (file)
+ (let ((dom (xml-parse-file file))
+ participants talking meeting-events)
+ (mapc (lambda (o)
+ (pcase (dom-attr o 'eventname)
+ ("ParticipantJoinEvent"
+ (cl-pushnew (cons (dom-text (dom-by-tag o 'userId))
+ (dom-text (dom-by-tag o 'name)))
+ participants)
+ (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC)))
+ (dom-attr o 'eventname))
+ meeting-events))
+ ("ParticipantLeftEvent"
+ (when (string= (dom-attr o 'module) "PARTICIPANT")
+ (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC)))
+ (dom-attr o 'eventname))
+ meeting-events)))
+ ("ParticipantTalkingEvent"
+ (cl-pushnew (assoc-default (dom-text (dom-by-tag o 'participant)) participants) talking))
+ ((or
+ "CreatePresentationPodEvent"
+ "EndAndKickAllEvent")
+ (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC)))
+ (dom-attr o 'eventname))
+ meeting-events))))
+ (dom-search dom (lambda (o) (dom-attr o 'eventname))))
+ (cl-pushnew (list ;; :slug (plist-get talk :slug)
+ :participants participants
+ :talking talking)
+ meeting-participants)
+ meeting-events))
+ (or event-xml-files
+ (mapcar #'emacsconf-extract-bbb-raw-events-file-name
+ (seq-filter (lambda (talk) (plist-get talk :bbb-meeting-id))
+ (emacsconf-get-talk-info)))))
+ (lambda (a b) (< (car a) (car b))))))
+ (dolist (event meeting-events)
+ (pcase (cdr event)
+ ("CreatePresentationPodEvent" (cl-incf meeting-count) (when (> meeting-count max-meetings) (setq max-meetings meeting-count)))
+ ("ParticipantJoinEvent" (cl-incf participant-count) (when (> participant-count max-participants) (setq max-participants participant-count)))
+ ("ParticipantLeftEvent" (cl-decf participant-count))
+ ("EndAndKickAllEvent" (cl-decf meeting-count))))
+ `((,(length meeting-participants) "Number of meetings analyzed")
+ (,max-participants "Max number of simultaneous users")
+ (,max-meetings "Max number of simultaneous meetings")
+ (,(apply 'max (mapcar (lambda (o) (length (plist-get o :participants))) meeting-participants)) "Max number of people in one meeting")
+ (,(length (seq-uniq (seq-mapcat (lambda (o) (mapcar #'cdr (plist-get o :participants))) meeting-participants))) "Total unique users")
+ (,(length (seq-uniq (seq-mapcat (lambda (o) (plist-get o :talking)) meeting-participants))) "Total unique talking"))))
+
+(defun emacsconf-extract-bbb-dired-raw (talk)
+ (interactive (list (emacsconf-complete-talk-info)))
+ (setq talk (emacsconf-resolve-talk talk))
+ (dired (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-raw-dir)))
+
+(defun emacsconf-extract-bbb-dired-published (talk)
+ (interactive (list (emacsconf-complete-talk-info)))
+ (setq talk (emacsconf-resolve-talk talk))
+ (dired (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir)))
+
+
+(defun emacsconf-extract-bbb-parse-events (xml-file)
+ (let* ((dom (xml-parse-file xml-file))
+ (meeting-name (dom-attr (dom-by-tag dom 'metadata) 'meetingName))
+ (meeting-id (dom-attr dom 'meeting_id))
+ (conf-joined (dom-search dom (lambda (o) (and (string= (dom-tag o) "name") (string= (dom-text o) emacsconf-extract-conference-username)))))
+ (conf-joined-time
+ (and conf-joined
+ (string-to-number (dom-text (dom-by-tag (dom-parent dom conf-joined) 'timestampUTC)))))
+ recording-start
+ recording-stop
+ recording-spans
+ stream-start
+ chat
+ talking
+ participants
+ talking-starts
+ deskshare-info
+ (meeting-date (dom-text (dom-by-tag (dom-parent dom (car conf-joined)) 'date))))
+ (dolist (event (dom-by-tag dom 'event))
+ (let ((timestamp (string-to-number (dom-text (dom-by-tag event 'timestampUTC)))))
+ (pcase (dom-attr event 'eventname)
+ ("ParticipantJoinEvent"
+ (push (cons (dom-text (dom-by-tag event 'userId))
+ (dom-text (dom-by-tag event 'name)))
+ participants))
+ ("StartRecordingEvent"
+ (setq recording-start timestamp
+ recording-stop nil
+ recording-file
+ (file-name-nondirectory (dom-text (dom-by-tag event 'filename)))))
+ ("StopRecordingEvent"
+ (setq recording-stop timestamp)
+ (push (cons recording-start recording-stop) recording-spans))
+ ("PublicChatEvent"
+ ;; only include events in the public recording
+ (when (and recording-start
+ (null recording-stop)
+ (>= timestamp recording-start))
+ (push (list
+ timestamp
+ (dom-text (dom-by-tag event 'sender))
+ (with-temp-buffer
+ (insert
+ (replace-regexp-in-string
+ "&#39;" "'"
+ (replace-regexp-in-string "<.*?>" ""
+ (dom-text (dom-by-tag event 'message)))))
+ (mm-url-decode-entities)
+ (buffer-string)))
+ chat)))
+ ("ParticipantTalkingEvent"
+ (let* ((speaker (assoc-default
+ (dom-text (dom-by-tag event 'participant))
+ participants)))
+ (if (string= (dom-text (dom-by-tag event 'talking)) "true")
+ ;; started talking
+ (if (assoc-default speaker talking-starts)
+ (setcdr (assoc speaker talking-starts)
+ timestamp)
+ (push (cons speaker timestamp) talking-starts))
+ (when (and recording-start
+ (>= timestamp recording-start)
+ (assoc-default speaker talking-starts)
+ (or (null recording-stop)
+ (<= (assoc-default speaker talking-starts)
+ recording-stop)))
+ (push (list speaker
+ (- (max (assoc-default speaker talking-starts) recording-start)
+ recording-start)
+ (- (if recording-stop (min recording-stop timestamp) timestamp)
+ recording-start)
+ recording-file)
+ talking)))))
+ ("StartWebRTCDesktopShareEvent"
+ (setq deskshare-info (cons (dom-text (dom-by-tag event 'filename))
+ timestamp)))
+
+ )))
+ `((name . ,meeting-name)
+ (id . ,meeting-id)
+ (conf-joined . ,conf-joined-time)
+ (recording-start . ,recording-start)
+ (meeting-date . ,meeting-date)
+ (participants . ,participants)
+ (talking . ,(nreverse talking))
+ (chat . ,(nreverse chat)))))
+
+(defun emacsconf-extract-bbb-talking-report (meeting-xml)
+ (let ((data (emacsconf-extract-bbb-parse-events meeting-xml)))
+ (unless (string= "" (alist-get 'meeting-date data))
+ (format "- %s %s: %s\n"
+ (alist-get 'name data)
+ (format-time-string "%a %I:%M %p"
+ (date-to-time
+ (alist-get 'meeting-date data)))
+ (mapconcat
+ (lambda (person)
+ (format "%s (%s)"
+ (car person)
+ (/ (cdr person) 60000)))
+ (sort
+ (mapcar
+ (lambda (group)
+ (cons
+ (car group)
+ (apply '+ (mapcar (lambda (o) (- (elt o 2) (elt o 1))) (cdr group)))))
+ (seq-group-by 'car (alist-get 'talking data)))
+ :key 'cdr
+ :reverse t)
+ ", ")))))
+
+
+
+(defun emacsconf-extract-bbb-parse-events-for-talk (talk)
+ "Parse events TALK from raw recordings.
+This works with the events.xml from /var/bigbluebutton/raw.
+Files should be downloaded to `emacsconf-extract-bbb-raw-dir'."
+ (setq talk (emacsconf-resolve-talk talk))
+ (emacsconf-extract-bbb-parse-events (emacsconf-extract-bbb-raw-events-file-name talk)))
+
+(defun emacsconf-extract-bbb-format-chat ()
+ (mapconcat
+ (lambda (events)
+ (format "- %s (%s)\n%s"
+ (assoc-default 'name events)
+ (assoc-default 'id events)
+ (mapconcat
+ (lambda (message)
+ (format " - %s: %s\n"
+ (elt message 1)
+ (with-temp-buffer
+ (insert
+ (replace-regexp-in-string
+ "&#39;" "'"
+ (replace-regexp-in-string "<.*?>" ""
+ (elt message 2))))
+ (mm-url-decode-entities)
+ (buffer-string))))
+ (assoc-default 'chat events)
+ "")))
+ (emacsconf-extract-bbb-parse-events-dir)
+ ""))
+
+(defun emacsconf-extract-spookfox-update-bbb-rec ()
+ (interactive)
+ (let* ((data
+ (spookfox-js-injection-eval-in-active-tab
+ "row = [...document.querySelectorAll('.dropdown-toggle')].find((o) => o.textContent.match('Unlisted')).closest('tr'); [row.querySelector('#recording-text').getAttribute('title'), row.querySelector('a.btn-primary').getAttribute('href')]" t)
+ )
+ (slug
+ (when (and data (string-match "^\\([^(]+\\) (" (elt data 0)))
+ (split-string
+ (match-string 1 (elt data 0))
+ ", "))))
+ (when data
+ (if (> (length slug) 1)
+ (setq slug (completing-read "Talk: " slug))
+ (setq slug (car slug)))
+ (emacsconf-with-talk-heading slug
+ (if (org-entry-get (point) "BBB_REC")
+ (progn
+ (kill-new (elt data 1))
+ (error "%s already has BBB_REC?" slug))
+ (org-entry-put (point) "BBB_REC" (elt data 1))))
+ (message "Updated BBB_REC for %s to %s" slug (elt data 1))
+ (spookfox-js-injection-eval-in-active-tab
+ "row = [...document.querySelectorAll('.dropdown-toggle')].find((o) => o.textContent.match('Unlisted')).closest('tr'); row.querySelector('.button_to .dropdown-item .fa-globe').closest('button').click();" t))))
+
+(defun emacsconf-make-webcams-deskshare-spans (talk &optional start-ms stop-ms strategy)
+ (let* ((start-ms (or start-ms 0))
+ (source-dir (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir))
+ (secs (/ start-ms 1000.0))
+ (deskshare (xml-parse-file (expand-file-name "deskshare.xml" source-dir)))
+ (webcam-video (expand-file-name "video/webcams.webm" source-dir))
+ (deskshare-video (expand-file-name "deskshare/deskshare.webm" source-dir))
+ (stop-ms (or stop-ms (emacsconf-get-file-duration-ms deskshare-video)))
+ spans)
+ (mapc (lambda (o)
+ (unless (or (= secs (string-to-number (dom-attr o 'start_timestamp)))
+ (= (string-to-number (dom-attr o 'start_timestamp)) 0)
+ (> secs (/ stop-ms 1000.0)))
+ (setq spans (cons (list :source webcam-video
+ :start-ms (* secs 1000)
+ :stop-ms
+ (* 1000
+ (if (eq strategy 'test)
+ (+ secs 3)
+ (max secs (string-to-number (dom-attr o 'start_timestamp))))))
+ spans)))
+ (when (and (<= (string-to-number (dom-attr o 'start_timestamp))
+ (/ stop-ms 1000.0))
+ (>= (string-to-number (dom-attr o 'stop_timestamp))
+ (/ start-ms 1000.0)))
+ (setq spans (cons (list :source deskshare-video
+ :start-ms (max (* 1000 (string-to-number (dom-attr o 'start_timestamp)))
+ start-ms)
+ :stop-ms
+ (if (eq strategy 'test)
+ (* 1000 (+ (string-to-number (dom-attr o 'start_timestamp)) 3))
+ (min (* 1000 (string-to-number (dom-attr o 'stop_timestamp)))
+ stop-ms)))
+ spans))
+ (setq secs (string-to-number (dom-attr o 'stop_timestamp)))))
+ (dom-by-tag deskshare 'event))
+ (unless (>= (floor (* secs 1000)) stop-ms)
+ (setq spans (cons (list :source webcam-video
+ :start-ms (* 1000 secs)
+ :stop-ms (if (eq strategy 'test)
+ (* 1000 (+ secs 3))
+ stop-ms))
+ spans)))
+ (if (eq strategy 'test)
+ `((video ,@(nreverse spans))
+ (audio ,@(mapcar (lambda (o)
+ (list :source webcam-video
+ :start-ms (plist-get o :start-ms)
+ :stop-ms (plist-get o :stop-ms)))
+ (reverse spans))))
+ `((video ,@(nreverse spans))
+ (audio (:source ,webcam-video :start-ms ,start-ms :stop-ms ,stop-ms))))))
+
+(defun emacsconf-get-ffmpeg-to-splice-webcam-and-recording (talk &optional start-ms stop-ms info strategy)
+ "Return FFMPEG command for slicing.
+Strategies:
+- 'fast-cut-start-keyframe - find the keyframe before the start ms and cut from there, doing a fast copy.
+- 'start-keyframe-and-reencode - find the keyframe before the start ms and cut from there, reencoding.
+- 'cut-and-concat - seek to the keyframe before, slowly find the start-ms, reencode the snippet, and then do a fast copy of the remaining. May have encoding errors.
+- default: copy from start-ms to stop-ms, reencoding.
+"
+ (interactive (list (emacsconf-complete-talk-info)))
+ (setq talk (emacsconf-resolve-talk talk))
+ (let* ((slug (plist-get talk :slug))
+ (start-ms (or start-ms 0))
+ (source-dir (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir))
+ (video-slug (plist-get (seq-find (lambda (o) (string= (plist-get o :slug) slug)) info) :video-slug))
+ (output (expand-file-name (concat (plist-get talk :file-prefix) "--answers.webm") emacsconf-cache-dir))
+ (webcam-video (expand-file-name "video/webcams.webm" source-dir))
+ (deskshare-video (expand-file-name "deskshare/deskshare.webm" source-dir))
+ (webcam-duration (emacsconf-get-file-duration-ms webcam-video))
+ (stop-ms (or stop-ms webcam-duration))
+ (command
+ (if (file-exists-p deskshare-video)
+ ;; Has deskshare
+ (let* ((deskshare (xml-parse-file (expand-file-name "deskshare.xml" source-dir)))
+ (final-size (compile-media-max-dimensions
+ deskshare-video
+ webcam-video))
+ (duration (compile-media-get-file-duration-ms webcam-video))
+ (spans (emacsconf-make-webcams-deskshare-spans talk start-ms stop-ms strategy))
+ (compile-media-output-video-width (car final-size))
+ (compile-media-output-video-height (cdr final-size)))
+ (compile-media-get-command spans output))
+ ;; Just webcams
+ (if (and (= start-ms 0)
+ (= stop-ms webcam-duration))
+ (format "cp %s %s"
+ webcam-video
+ output)
+ (compile-media-get-command
+ (compile-media-split-tracks
+ (list (list :source webcam-video :start-ms start-ms :stop-ms stop-ms)))
+ output)))))
+ (when (called-interactively-p 'any)
+ (kill-new command))
+ command))
+
+(defun emacsconf-extract-chat (filename)
+ (when (file-exists-p filename)
+ (message "%s" filename)
+ (mapcar
+ (lambda (node)
+ (when (string= (dom-attr node 'target) "chat")
+ (let ((message
+ (replace-regexp-in-string
+ "\\[<u>\\([^<]+\\)?</u>\\](\"\\([^<]+\\)\")"
+ "<\\2>"
+ (condition-case nil
+ (html-to-markdown-string (dom-attr node 'message))
+ (error
+ (replace-regexp-in-string
+ "<a href=\"\\(.+?\\)\" rel=\"nofollow\"><u>\\(.+?\\)</u></a>"
+ "<\\2>"
+ (dom-attr node 'message)))))))
+ (list (string-to-number (dom-attr node 'in)) (dom-attr node 'name) message))))
+ (dom-by-tag (xml-parse-file filename) 'chattimeline))))
+
+(defvar emacsconf-extract-bbb-chat-use-wall-clock-time nil
+ "Non-nil means use wall clock time for logs.")
+(defun emacsconf-extract-chats ()
+ (interactive)
+ (mapc (lambda (o)
+ (let* ((playback-dir (expand-file-name (plist-get o :slug)
+ (expand-file-name "bbb-playbacks" emacsconf-cache-dir)))
+ (chat
+ (emacsconf-extract-extract-chat
+ (expand-file-name
+ "slides_new.xml"
+ playback-dir)))
+ metadata)
+ (when chat
+ (setq metadata (xml-parse-file (expand-file-name "metadata.xml"
+ playback-dir)))
+ (let ((recording-start (/ (string-to-number (dom-text
+ (dom-by-tag metadata 'start_time)))
+ 1000)))
+ (with-temp-file (expand-file-name (concat (plist-get o :file-prefix) "--extract.txt")
+ emacsconf-cache-dir)
+ (insert
+ (mapconcat
+ (lambda (line)
+ (format "`%s` _%s_ %s \n"
+ (if emacsconf-extract-bbb-chat-use-wall-clock-time
+ (format-time-string "%H:%M:%S"
+ (seconds-to-time
+ (+ recording-start
+ (elt line 0))))
+ (format-seconds "%h:%.2m:%.2s"
+ (elt line 0)))
+ (elt line 1)
+ (elt line 2)))
+ chat
+ "")))))))
+ (emacsconf-prepare-for-display (emacsconf-get-talk-info))))
+
+(defun emacsconf-extract-bbb-copy-files (&optional info)
+ (interactive)
+ (mapc
+ (lambda (o)
+ (let ((playback-dir (expand-file-name (plist-get o :slug)
+ (expand-file-name "bbb-playbacks" emacsconf-cache-dir))))
+ (mapc (lambda (file)
+ (when (and (file-exists-p (expand-file-name file playback-dir))
+ (not (file-exists-p (expand-file-name (concat (plist-get o :file-prefix) "--bbb-" file) emacsconf-cache-dir))))
+ (copy-file (expand-file-name file playback-dir)
+ (expand-file-name (concat (plist-get o :file-prefix) "--bbb-" file) emacsconf-cache-dir)
+ t)))
+ '("webcams.webm" "metadata.xml" "deskshare.webm" "deskshare.xml" "slides_new.xml" "webcams.opus"))))
+ (or info (emacsconf-prepare-for-display (emacsconf-get-talk-info)))))
+
+(defvar emacsconf-extract-dump-dir "/ssh:orga@res.emacsconf.org#46668:~/current/live0-streams/")
+
+(defun emacsconf-extract-wget-bbb (o)
+ (when (plist-get o :bbb-playback)
+ (let ((meeting-id (when (string-match "meetingId=\\(.+\\)"
+ (plist-get o :bbb-playback))
+ (match-string 1 (plist-get o :bbb-playback)))))
+ (concat "mkdir " (plist-get o :slug) "\n"
+ "cd " (plist-get o :slug) "\n"
+ (mapconcat
+ (lambda (file)
+ (concat
+ "wget https://bbb.emacsverse.org/presentation/"
+ meeting-id "/" file "\n"))
+ '("video/webcams.webm" "metadata.xml" "deskshare/deskshare.webm" "panzooms.xml" "cursor.xml" "deskshare.xml" "captions.json" "presentation_text.json" "slides_new.xml")
+ "")
+ "cd ..\n"))))
+
+(defun emacsconf-extract-bbb-events-xml (o)
+ "Copy the events.xml from the raw BBB directory copied from bbb@bbb.emacsverse.org."
+ (if (plist-get o :bbb-playback)
+ (let ((meeting-id (when (string-match "meetingId=\\(.+\\)"
+ (plist-get o :bbb-playback))
+ (match-string 1 (plist-get o :bbb-playback)))))
+ (format "scp ~/current/bbb-raw/%s/events.xml orga@media.emacsconf.org:~/backstage/%s--bbb-events.xml\n"
+ meeting-id
+ (plist-get o :file-prefix)))
+ ""))
+
+(defun emacsconf-extract-bbb-voice-events (file)
+ "Return a list of voice events.
+(:name participant :start-clock start-time :start-ms ... :stop-clock stop-time :stop-ms)."
+ (let ((dom (xml-parse-file file))
+ start-recording
+ stop-recording
+ start-ms stop-ms
+ participants results)
+ (setq start-recording
+ (date-to-time
+ (dom-text
+ (dom-by-tag
+ (dom-elements dom 'eventname "StartRecordingEvent")
+ 'date))))
+ (setq stop-recording
+ (date-to-time
+ (dom-text
+ (dom-by-tag
+ (or (dom-elements dom 'eventname "StopRecordingEvent")
+ (dom-elements dom 'eventname "EndAndKickAllEvent"))
+ 'date))))
+ (setq start-ms (* 1000 (time-to-seconds start-recording))
+ stop-ms (* 1000 (time-to-seconds stop-recording)))
+ ;; get the participant names and put them in an alist
+ (setq participants
+ (mapcar (lambda (o) (list
+ (dom-text (dom-by-tag o 'userId))
+ :name (dom-text (dom-by-tag o 'name))))
+ (seq-filter
+ (lambda (node) (string= (dom-attr node 'eventname)
+ "ParticipantJoinEvent"))
+ (dom-by-tag dom 'event))))
+ ;; get the voice events
+ (mapc (lambda (o)
+ (let ((participant (assoc-default
+ (dom-text (dom-by-tag o 'participant))
+ participants))
+ (time (date-to-time (dom-text (dom-by-tag o 'date))))
+ o-start o-stop)
+ (if (string= (dom-text (dom-by-tag o 'talking))
+ "true")
+ ;; start talking
+ (plist-put participant
+ ;; although maybe timestampUTC will be useful somehow
+ :start time)
+ ;; clamp it to start-recording and stop-recording
+ (when (and (time-less-p (plist-get participant :start)
+ stop-recording)
+ (time-less-p start-recording time))
+ (setq o-start
+ (- (max (* 1000 (time-to-seconds (plist-get participant :start)))
+ start-ms)
+ start-ms)
+ o-stop
+ (- (min (* 1000 (time-to-seconds time))
+ stop-ms)
+ start-ms))
+ (setq results
+ (cons (list
+ :name
+ (plist-get participant :name)
+ :start-ms
+ o-start
+ :stop-ms
+ o-stop
+ :start-clock
+ (plist-get participant :start)
+ :stop-clock
+ time
+ :duration-ms
+ (- o-stop o-start))
+ results))))))
+ (seq-filter
+ (lambda (node) (string= (dom-attr node 'eventname)
+ "ParticipantTalkingEvent"))
+ (dom-by-tag dom 'event)))
+ (nreverse results)))
+
+;; (emacsconf-extract-bbb-voice-events "~/proj/emacsconf/cache/emacsconf-2022-sqlite--using-sqlite-as-a-data-source-a-framework-and-an-example--andrew-hyatt--bbb-events.xml")
+;; Okay, now that we have voice events, what can we do with them?
+;; We can insert notes into the VTT for now to try to guess the speaker, when the speaker changes
+;; The audio is not split up by speaker, so the transcript is also not very split up
+;; Some speech-to-text systems can do speaker diarization, which also tries to identify speakers
+;; huh, is the StartRecordingEvent timestamp reliable? Am I misreading it?
+
+
+(defun emacsconf-private-qa (&optional info)
+ (seq-remove (lambda (o)
+ (or (null (emacsconf-talk-file o "--bbb-webcams.webm"))
+ (plist-get o :qa-public)))
+ (or info (emacsconf-get-talk-info))))
+;; sqlite detached localizing
+(defun emacsconf-extract-review-qa (talk)
+ (interactive (list (emacsconf-complete-talk-info (emacsconf-private-qa))))
+ (find-file (emacsconf-talk-file talk "--bbb-webcams.vtt")))
+
+(defun emacsconf-extract-publish-qa (talk &optional time)
+ (interactive (list (emacsconf-complete-talk-info (unless current-prefix-arg (emacsconf-private-qa)))
+ (when current-prefix-arg
+ (read-string "Time: "))))
+ (when (stringp talk) (setq talk (emacsconf-resolve-talk talk)))
+ (let ((buff (get-buffer-create "*ffmpeg*"))
+ (large-file-warning-threshold nil))
+ (cond
+ ((emacsconf-talk-file talk "--bbb-deskshare.webm")
+ (apply 'call-process "ffmpeg" nil buff nil
+ (append
+ (list
+ "-y"
+ "-i"
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--bbb-deskshare.webm")
+ emacsconf-cache-dir))
+ (when time (list "-to" time))
+ (list
+ "-i"
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--bbb-webcams.opus")
+ emacsconf-cache-dir))
+ (when time (list "-to" time))
+ (list
+ "-c"
+ "copy"
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--answers.webm")
+ emacsconf-cache-dir)))))
+ (time
+ (apply 'call-process "ffmpeg" nil buff nil
+ (append
+ (list
+ "-y"
+ "-i"
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--bbb-webcams.webm")
+ emacsconf-cache-dir))
+ (when time (list "-to" time))
+ (list
+ "-c"
+ "copy"
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--answers.webm")
+ emacsconf-cache-dir)))))
+ (t
+ (copy-file
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--bbb-webcams.webm")
+ emacsconf-cache-dir)
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--answers.webm")
+ emacsconf-cache-dir)
+ t)))
+ (call-process "ffmpeg" nil buff nil "-y" "-i"
+ (emacsconf-talk-file talk "--answers.webm")
+ "-c" "copy"
+ (emacsconf-talk-file talk "--answers.opus" t))
+ (dolist (suffix '("opus" "webm"))
+ (copy-file
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--answers." suffix)
+ emacsconf-cache-dir)
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--answers." suffix)
+ emacsconf-backstage-dir)
+ t)
+ (copy-file
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--answers." suffix)
+ emacsconf-backstage-dir)
+ (expand-file-name
+ (concat
+ (plist-get talk :file-prefix)
+ "--answers." suffix)
+ emacsconf-public-media-directory)
+ t))
+ (save-window-excursion
+ (emacsconf-go-to-talk talk)
+ (org-entry-put
+ (point)
+ "QA_PUBLIC" "t")
+ (unless (string-match "Q&A posted publicly." (or (org-entry-get (point) "QA_NOTE") ""))
+ (org-entry-put
+ (point)
+ "QA_NOTE"
+ (concat "Q&A posted publicly."
+ (emacsconf-surround " "
+ (org-entry-get (point) "QA_NOTE")
+ "" "")))))))
+
+(defun emacsconf-extract-waveform-published-webcam-video (talk)
+ (interactive (list (emacsconf-complete-talk-info)))
+ (setq talk (emacsconf-resolve-talk talk))
+ (waveform-show (expand-file-name "video/webcams.webm"
+ (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir))))
+
+(defun emacsconf-extract-chat (slug speaker)
+ (interactive (list
+ (emacsconf-complete-talk)
+ (completing-read "Speaker: "
+ (seq-uniq
+ (mapcar (lambda (node) (dom-attr node 'name))
+ (dom-by-tag (xml-parse-region (point-min) (point-max)) 'chattimeline)))
+ )))
+ (let ((text
+ (mapconcat (lambda (node)
+ (when (string= (dom-attr node 'target) "chat")
+ (let ((message
+ (replace-regexp-in-string
+ "\\(^[^ +]?\\): " ""
+ (replace-regexp-in-string "<a href=\"\\(.+?\\)\" rel=\"nofollow\"><u>\\(.+?\\)</u></a>"
+ "<\\1>" (dom-attr node 'message)))))
+ (if (string-match speaker (dom-attr node 'name))
+ (format "- %s: %s\n" speaker message)
+ (format "- %s\n" message)))))
+ (dom-by-tag (xml-parse-region (point-min) (point-max)) 'chattimeline)
+ "")))
+ (emacsconf-edit-wiki-page slug)
+ (if (re-search-forward "# Discussion" nil t)
+ (progn
+ (goto-char (match-end 0))
+ (insert "\n\n"))
+ (goto-char (point-max)))
+ (kill-new text)))
+
+
+
+
+;; TODO: Combine lines from same nick, or identify speakers with anon1/2/etc.
+(defun emacsconf-extract-chat-from-dired ()
+ (interactive)
+ (find-file (expand-file-name "slides_new.xml" (dired-get-file-for-visit)))
+ (call-interactively 'emacsconf-extract-chat))
+
+(defvar emacsconf-extract-conference-username "emacsconf" "Name of the streaming user.")
+
+(provide 'emacsconf-bbb)
+;;; emacsconf-bbb.el ends here
diff --git a/emacsconf-extract.el b/emacsconf-extract.el
index 3c2d21e..798d543 100644
--- a/emacsconf-extract.el
+++ b/emacsconf-extract.el
@@ -25,79 +25,8 @@
;;; Code:
-(defun emacsconf-extract-chat (filename)
- (when (file-exists-p filename)
- (message "%s" filename)
- (mapcar
- (lambda (node)
- (when (string= (dom-attr node 'target) "chat")
- (let ((message
- (replace-regexp-in-string
- "\\[<u>\\([^<]+\\)?</u>\\](\"\\([^<]+\\)\")"
- "<\\2>"
- (condition-case nil
- (html-to-markdown-string (dom-attr node 'message))
- (error
- (replace-regexp-in-string
- "<a href=\"\\(.+?\\)\" rel=\"nofollow\"><u>\\(.+?\\)</u></a>"
- "<\\2>"
- (dom-attr node 'message)))))))
- (list (string-to-number (dom-attr node 'in)) (dom-attr node 'name) message))))
- (dom-by-tag (xml-parse-file filename) 'chattimeline))))
-;; (emacsconf-extract-extract-chat (expand-file-name "bbb-playbacks/haskell/slides_new.xml" emacsconf-cache-dir))
-(defvar emacsconf-extract-bbb-chat-use-wall-clock-time nil
- "Non-nil means use wall clock time for logs.")
-(defun emacsconf-extract-chats ()
- (interactive)
- (mapc (lambda (o)
- (let* ((playback-dir (expand-file-name (plist-get o :slug)
- (expand-file-name "bbb-playbacks" emacsconf-cache-dir)))
- (chat
- (emacsconf-extract-extract-chat
- (expand-file-name
- "slides_new.xml"
- playback-dir)))
- metadata)
- (when chat
- (setq metadata (xml-parse-file (expand-file-name "metadata.xml"
- playback-dir)))
- (let ((recording-start (/ (string-to-number (dom-text
- (dom-by-tag metadata 'start_time)))
- 1000)))
- (with-temp-file (expand-file-name (concat (plist-get o :file-prefix) "--extract.txt")
- emacsconf-cache-dir)
- (insert
- (mapconcat
- (lambda (line)
- (format "`%s` _%s_ %s \n"
- (if emacsconf-extract-bbb-chat-use-wall-clock-time
- (format-time-string "%H:%M:%S"
- (seconds-to-time
- (+ recording-start
- (elt line 0))))
- (format-seconds "%h:%.2m:%.2s"
- (elt line 0)))
- (elt line 1)
- (elt line 2)))
- chat
- "")))))))
- (emacsconf-prepare-for-display (emacsconf-get-talk-info))))
-
-(defun emacsconf-extract-bbb-copy-files (&optional info)
- (interactive)
- (mapc
- (lambda (o)
- (let ((playback-dir (expand-file-name (plist-get o :slug)
- (expand-file-name "bbb-playbacks" emacsconf-cache-dir))))
- (mapc (lambda (file)
- (when (and (file-exists-p (expand-file-name file playback-dir))
- (not (file-exists-p (expand-file-name (concat (plist-get o :file-prefix) "--bbb-" file) emacsconf-cache-dir))))
- (copy-file (expand-file-name file playback-dir)
- (expand-file-name (concat (plist-get o :file-prefix) "--bbb-" file) emacsconf-cache-dir)
- t)))
- '("webcams.webm" "metadata.xml" "deskshare.webm" "deskshare.xml" "slides_new.xml" "webcams.opus"))))
- (or info (emacsconf-prepare-for-display (emacsconf-get-talk-info)))))
+;; (emacsconf-extract-extract-chat (expand-file-name "bbb-playbacks/haskell/slides_new.xml" emacsconf-cache-dir))
(defvar emacsconf-extract-dump-dir "/ssh:orga@res.emacsconf.org#46668:~/current/live0-streams/")
(defun emacsconf-extract-dump-time-from-filename (f)
@@ -315,116 +244,6 @@
(emacsconf-get-slug-from-string (file-name-base (buffer-file-name)))))))
(subed-set-subtitle-comment (concat "Q: " question)))
-(defun emacsconf-extract-wget-bbb (o)
- (when (plist-get o :bbb-playback)
- (let ((meeting-id (when (string-match "meetingId=\\(.+\\)"
- (plist-get o :bbb-playback))
- (match-string 1 (plist-get o :bbb-playback)))))
- (concat "mkdir " (plist-get o :slug) "\n"
- "cd " (plist-get o :slug) "\n"
- (mapconcat
- (lambda (file)
- (concat
- "wget https://bbb.emacsverse.org/presentation/"
- meeting-id "/" file "\n"))
- '("video/webcams.webm" "metadata.xml" "deskshare/deskshare.webm" "panzooms.xml" "cursor.xml" "deskshare.xml" "captions.json" "presentation_text.json" "slides_new.xml")
- "")
- "cd ..\n"))))
-
-(defun emacsconf-extract-bbb-events-xml (o)
- "Copy the events.xml from the raw BBB directory copied from bbb@bbb.emacsverse.org."
- (if (plist-get o :bbb-playback)
- (let ((meeting-id (when (string-match "meetingId=\\(.+\\)"
- (plist-get o :bbb-playback))
- (match-string 1 (plist-get o :bbb-playback)))))
- (format "scp ~/current/bbb-raw/%s/events.xml orga@media.emacsconf.org:~/backstage/%s--bbb-events.xml\n"
- meeting-id
- (plist-get o :file-prefix)))
- ""))
-
-(defun emacsconf-extract-bbb-voice-events (file)
- "Return a list of voice events.
-(:name participant :start-clock start-time :start-ms ... :stop-clock stop-time :stop-ms)."
- (let ((dom (xml-parse-file file))
- start-recording
- stop-recording
- start-ms stop-ms
- participants results)
- (setq start-recording
- (date-to-time
- (dom-text
- (dom-by-tag
- (dom-elements dom 'eventname "StartRecordingEvent")
- 'date))))
- (setq stop-recording
- (date-to-time
- (dom-text
- (dom-by-tag
- (or (dom-elements dom 'eventname "StopRecordingEvent")
- (dom-elements dom 'eventname "EndAndKickAllEvent"))
- 'date))))
- (setq start-ms (* 1000 (time-to-seconds start-recording))
- stop-ms (* 1000 (time-to-seconds stop-recording)))
- ;; get the participant names and put them in an alist
- (setq participants
- (mapcar (lambda (o) (list
- (dom-text (dom-by-tag o 'userId))
- :name (dom-text (dom-by-tag o 'name))))
- (seq-filter
- (lambda (node) (string= (dom-attr node 'eventname)
- "ParticipantJoinEvent"))
- (dom-by-tag dom 'event))))
- ;; get the voice events
- (mapc (lambda (o)
- (let ((participant (assoc-default
- (dom-text (dom-by-tag o 'participant))
- participants))
- (time (date-to-time (dom-text (dom-by-tag o 'date))))
- o-start o-stop)
- (if (string= (dom-text (dom-by-tag o 'talking))
- "true")
- ;; start talking
- (plist-put participant
- ;; although maybe timestampUTC will be useful somehow
- :start time)
- ;; clamp it to start-recording and stop-recording
- (when (and (time-less-p (plist-get participant :start)
- stop-recording)
- (time-less-p start-recording time))
- (setq o-start
- (- (max (* 1000 (time-to-seconds (plist-get participant :start)))
- start-ms)
- start-ms)
- o-stop
- (- (min (* 1000 (time-to-seconds time))
- stop-ms)
- start-ms))
- (setq results
- (cons (list
- :name
- (plist-get participant :name)
- :start-ms
- o-start
- :stop-ms
- o-stop
- :start-clock
- (plist-get participant :start)
- :stop-clock
- time
- :duration-ms
- (- o-stop o-start))
- results))))))
- (seq-filter
- (lambda (node) (string= (dom-attr node 'eventname)
- "ParticipantTalkingEvent"))
- (dom-by-tag dom 'event)))
- (nreverse results)))
-;; (emacsconf-extract-bbb-voice-events "~/proj/emacsconf/cache/emacsconf-2022-sqlite--using-sqlite-as-a-data-source-a-framework-and-an-example--andrew-hyatt--bbb-events.xml")
-;; Okay, now that we have voice events, what can we do with them?
-;; We can insert notes into the VTT for now to try to guess the speaker, when the speaker changes
-;; The audio is not split up by speaker, so the transcript is also not very split up
-;; Some speech-to-text systems can do speaker diarization, which also tries to identify speakers
-;; huh, is the StartRecordingEvent timestamp reliable? Am I misreading it?
(defvar emacsconf-extract-irc-speaker-nick nil "*Nick for the speaker.")
@@ -572,127 +391,7 @@
(replace-match (concat "- " (match-string 1) ": " (match-string 2)) t t)
(replace-match (concat "- " (match-string 2)) t t)))))
-(defun emacsconf-private-qa (&optional info)
- (seq-remove (lambda (o)
- (or (null (emacsconf-talk-file o "--bbb-webcams.webm"))
- (plist-get o :qa-public)))
- (or info (emacsconf-get-talk-info))))
-;; sqlite detached localizing
-(defun emacsconf-extract-review-qa (talk)
- (interactive (list (emacsconf-complete-talk-info (emacsconf-private-qa))))
- (find-file (emacsconf-talk-file talk "--bbb-webcams.vtt")))
-
-(defun emacsconf-extract-publish-qa (talk &optional time)
- (interactive (list (emacsconf-complete-talk-info (unless current-prefix-arg (emacsconf-private-qa)))
- (when current-prefix-arg
- (read-string "Time: "))))
- (when (stringp talk) (setq talk (emacsconf-resolve-talk talk)))
- (let ((buff (get-buffer-create "*ffmpeg*"))
- (large-file-warning-threshold nil))
- (cond
- ((emacsconf-talk-file talk "--bbb-deskshare.webm")
- (apply 'call-process "ffmpeg" nil buff nil
- (append
- (list
- "-y"
- "-i"
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--bbb-deskshare.webm")
- emacsconf-cache-dir))
- (when time (list "-to" time))
- (list
- "-i"
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--bbb-webcams.opus")
- emacsconf-cache-dir))
- (when time (list "-to" time))
- (list
- "-c"
- "copy"
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--answers.webm")
- emacsconf-cache-dir)))))
- (time
- (apply 'call-process "ffmpeg" nil buff nil
- (append
- (list
- "-y"
- "-i"
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--bbb-webcams.webm")
- emacsconf-cache-dir))
- (when time (list "-to" time))
- (list
- "-c"
- "copy"
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--answers.webm")
- emacsconf-cache-dir)))))
- (t
- (copy-file
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--bbb-webcams.webm")
- emacsconf-cache-dir)
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--answers.webm")
- emacsconf-cache-dir)
- t)))
- (call-process "ffmpeg" nil buff nil "-y" "-i"
- (emacsconf-talk-file talk "--answers.webm")
- "-c" "copy"
- (emacsconf-talk-file talk "--answers.opus" t))
- (dolist (suffix '("opus" "webm"))
- (copy-file
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--answers." suffix)
- emacsconf-cache-dir)
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--answers." suffix)
- emacsconf-backstage-dir)
- t)
- (copy-file
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--answers." suffix)
- emacsconf-backstage-dir)
- (expand-file-name
- (concat
- (plist-get talk :file-prefix)
- "--answers." suffix)
- emacsconf-public-media-directory)
- t))
- (save-window-excursion
- (emacsconf-go-to-talk talk)
- (org-entry-put
- (point)
- "QA_PUBLIC" "t")
- (unless (string-match "Q&A posted publicly." (or (org-entry-get (point) "QA_NOTE") ""))
- (org-entry-put
- (point)
- "QA_NOTE"
- (concat "Q&A posted publicly."
- (emacsconf-surround " "
- (org-entry-get (point) "QA_NOTE")
- "" "")))))))
+
;; (emacsconf-extract-publish-qa "workflows" "13:56.000") (emacsconf-extract-publish-qa "journalism") (emacsconf-extract-publish-qa "handwritten" "28:36.240")
;; (kill-new (mapconcat #'emacsconf-extract-bbb-events-xml (emacsconf-get-talk-info) ""))
;; (dolist (slug '("haskell" "hyperorg" "health" "jupyter" "workflows" "wayland" "mail" "meetups" "orgsuperlinks" "rde" "science"))
@@ -721,424 +420,12 @@ Would you like to help? See [[help_with_chapter_markers]] for more details. You
;; (mapc #'emacsconf-extract-add-help-index-qa (emacsconf-prepare-for-display (emacsconf-get-talk-info)))
;; (emacsconf-extract-download-published-recordings "bbb:/var/bigbluebutton/published/presentation/" "" ~/proj/emacsconf/2023/bbb-published/"")
-(defvar emacsconf-extract-bbb-raw-dir "~/proj/emacsconf/2023/bbb/" "End with a \"/\".")
-(defvar emacsconf-extract-bbb-published-dir "~/proj/emacsconf/2023/bbb-published/" "End with a \"/\".")
-(defvar emacsconf-extract-conference-username "emacsconf" "Name of the streaming user.")
-(defvar emacsconf-extract-bbb-path "/ssh:bbb@bbb.emacsverse.org:/var/bigbluebutton/")
-(defun emacsconf-extract-raw-recordings-download-command ()
- "Copy the command for downloading raw recordings."
- (interactive)
- (let ((s (mapconcat (lambda (o) (if (plist-get o :bbb-meeting-id)
- (format "rsync -avzue ssh %s %s\n"
- (expand-file-name (plist-get o :bbb-meeting-id) emacsconf-extract-bbb-path)
- emacsconf-extract-bbb-raw-dir)
- ""))
- (emacsconf-get-talk-info))))
- (when (called-interactively-p 'any) (kill-new s))
- s))
-
-(defun emacsconf-extract-download-published-recordings-command ()
- "Copy the command for downloading published recordings from SOURCE to DEST."
- (interactive)
- (kill-new
- (mapconcat (lambda (o) (if (plist-get o :bbb-meeting-id)
- (replace-regexp-in-string
- "/ssh:" ""
- (format "rsync -avzue ssh %s %s # %s\n"
- (expand-file-name
- (plist-get o :bbb-meeting-id)
- (expand-file-name "published/presentation"
- emacsconf-extract-bbb-path
- ))
- emacsconf-extract-bbb-published-dir
- (plist-get o :slug)))
- ""))
- (emacsconf-get-talk-info))))
-
-(defun emacsconf-extract-bbb-parse-events-dir (&optional dir)
- (mapcar (lambda (file)
- (emacsconf-extract-bbb-parse-events file))
- (directory-files-recursively (or dir emacsconf-extract-bbb-raw-dir) "events.xml")))
-
-(defun emacsconf-extract-bbb-raw-events-file-name (talk)
- (setq talk (emacsconf-resolve-talk talk))
- (expand-file-name "events.xml" (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-raw-dir)))
-
-(defun emacsconf-extract-bbb-report (&optional event-xml-files)
- (let* ((max 0)
- (participant-count 0)
- (meeting-count 0)
- (max-meetings 0)
- (max-participants 0)
- meeting-participants
- (meeting-events
- (sort
- (seq-mapcat
- (lambda (file)
- (let ((dom (xml-parse-file file))
- participants talking meeting-events)
- (mapc (lambda (o)
- (pcase (dom-attr o 'eventname)
- ("ParticipantJoinEvent"
- (cl-pushnew (cons (dom-text (dom-by-tag o 'userId))
- (dom-text (dom-by-tag o 'name)))
- participants)
- (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC)))
- (dom-attr o 'eventname))
- meeting-events))
- ("ParticipantLeftEvent"
- (when (string= (dom-attr o 'module) "PARTICIPANT")
- (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC)))
- (dom-attr o 'eventname))
- meeting-events)))
- ("ParticipantTalkingEvent"
- (cl-pushnew (assoc-default (dom-text (dom-by-tag o 'participant)) participants) talking))
- ((or
- "CreatePresentationPodEvent"
- "EndAndKickAllEvent")
- (push (cons (string-to-number (dom-text (dom-by-tag o 'timestampUTC)))
- (dom-attr o 'eventname))
- meeting-events))))
- (dom-search dom (lambda (o) (dom-attr o 'eventname))))
- (cl-pushnew (list ;; :slug (plist-get talk :slug)
- :participants participants
- :talking talking)
- meeting-participants)
- meeting-events))
- (or event-xml-files
- (mapcar #'emacsconf-extract-bbb-raw-events-file-name
- (seq-filter (lambda (talk) (plist-get talk :bbb-meeting-id))
- (emacsconf-get-talk-info)))))
- (lambda (a b) (< (car a) (car b))))))
- (dolist (event meeting-events)
- (pcase (cdr event)
- ("CreatePresentationPodEvent" (cl-incf meeting-count) (when (> meeting-count max-meetings) (setq max-meetings meeting-count)))
- ("ParticipantJoinEvent" (cl-incf participant-count) (when (> participant-count max-participants) (setq max-participants participant-count)))
- ("ParticipantLeftEvent" (cl-decf participant-count))
- ("EndAndKickAllEvent" (cl-decf meeting-count))))
- `((,(length meeting-participants) "Number of meetings analyzed")
- (,max-participants "Max number of simultaneous users")
- (,max-meetings "Max number of simultaneous meetings")
- (,(apply 'max (mapcar (lambda (o) (length (plist-get o :participants))) meeting-participants)) "Max number of people in one meeting")
- (,(length (seq-uniq (seq-mapcat (lambda (o) (mapcar #'cdr (plist-get o :participants))) meeting-participants))) "Total unique users")
- (,(length (seq-uniq (seq-mapcat (lambda (o) (plist-get o :talking)) meeting-participants))) "Total unique talking"))))
-
-(defun emacsconf-extract-bbb-dired-raw (talk)
- (interactive (list (emacsconf-complete-talk-info)))
- (setq talk (emacsconf-resolve-talk talk))
- (dired (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-raw-dir)))
-(defun emacsconf-extract-bbb-dired-published (talk)
- (interactive (list (emacsconf-complete-talk-info)))
- (setq talk (emacsconf-resolve-talk talk))
- (dired (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir)))
-(defun emacsconf-extract-waveform-published-webcam-video (talk)
- (interactive (list (emacsconf-complete-talk-info)))
- (setq talk (emacsconf-resolve-talk talk))
- (waveform-show (expand-file-name "video/webcams.webm"
- (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir))))
-
-
-(defun emacsconf-extract-bbb-parse-events (xml-file)
- (let* ((dom (xml-parse-file xml-file))
- (meeting-name (dom-attr (dom-by-tag dom 'metadata) 'meetingName))
- (meeting-id (dom-attr dom 'meeting_id))
- (conf-joined (dom-search dom (lambda (o) (and (string= (dom-tag o) "name") (string= (dom-text o) emacsconf-extract-conference-username)))))
- (conf-joined-time
- (and conf-joined
- (string-to-number (dom-text (dom-by-tag (dom-parent dom conf-joined) 'timestampUTC)))))
- recording-start
- recording-stop
- recording-spans
- stream-start
- chat
- talking
- participants
- talking-starts
- deskshare-info
- (meeting-date (dom-text (dom-by-tag (dom-parent dom (car conf-joined)) 'date))))
- (dolist (event (dom-by-tag dom 'event))
- (let ((timestamp (string-to-number (dom-text (dom-by-tag event 'timestampUTC)))))
- (pcase (dom-attr event 'eventname)
- ("ParticipantJoinEvent"
- (push (cons (dom-text (dom-by-tag event 'userId))
- (dom-text (dom-by-tag event 'name)))
- participants))
- ("StartRecordingEvent"
- (setq recording-start timestamp
- recording-stop nil
- recording-file
- (file-name-nondirectory (dom-text (dom-by-tag event 'filename)))))
- ("StopRecordingEvent"
- (setq recording-stop timestamp)
- (push (cons recording-start recording-stop) recording-spans))
- ("PublicChatEvent"
- ;; only include events in the public recording
- (when (and recording-start
- (null recording-stop)
- (>= timestamp recording-start))
- (push (list
- timestamp
- (dom-text (dom-by-tag event 'sender))
- (with-temp-buffer
- (insert
- (replace-regexp-in-string
- "&#39;" "'"
- (replace-regexp-in-string "<.*?>" ""
- (dom-text (dom-by-tag event 'message)))))
- (mm-url-decode-entities)
- (buffer-string)))
- chat)))
- ("ParticipantTalkingEvent"
- (let* ((speaker (assoc-default
- (dom-text (dom-by-tag event 'participant))
- participants)))
- (if (string= (dom-text (dom-by-tag event 'talking)) "true")
- ;; started talking
- (if (assoc-default speaker talking-starts)
- (setcdr (assoc speaker talking-starts)
- timestamp)
- (push (cons speaker timestamp) talking-starts))
- (when (and recording-start
- (>= timestamp recording-start)
- (assoc-default speaker talking-starts)
- (or (null recording-stop)
- (<= (assoc-default speaker talking-starts)
- recording-stop)))
- (push (list speaker
- (- (max (assoc-default speaker talking-starts) recording-start)
- recording-start)
- (- (if recording-stop (min recording-stop timestamp) timestamp)
- recording-start)
- recording-file)
- talking)))))
- ("StartWebRTCDesktopShareEvent"
- (setq deskshare-info (cons (dom-text (dom-by-tag event 'filename))
- timestamp)))
-
- )))
- `((name . ,meeting-name)
- (id . ,meeting-id)
- (conf-joined . ,conf-joined-time)
- (recording-start . ,recording-start)
- (meeting-date . ,meeting-date)
- (participants . ,participants)
- (talking . ,(nreverse talking))
- (chat . ,(nreverse chat)))))
-
-(defun emacsconf-extract-bbb-talking-report (meeting-xml)
- (let ((data (emacsconf-extract-bbb-parse-events meeting-xml)))
- (unless (string= "" (alist-get 'meeting-date data))
- (format "- %s %s: %s\n"
- (alist-get 'name data)
- (format-time-string "%a %I:%M %p"
- (date-to-time
- (alist-get 'meeting-date data)))
- (mapconcat
- (lambda (person)
- (format "%s (%s)"
- (car person)
- (/ (cdr person) 60000)))
- (sort
- (mapcar
- (lambda (group)
- (cons
- (car group)
- (apply '+ (mapcar (lambda (o) (- (elt o 2) (elt o 1))) (cdr group)))))
- (seq-group-by 'car (alist-get 'talking data)))
- :key 'cdr
- :reverse t)
- ", ")))))
-
-(defun emacsconf-extract-bbb-parse-events-for-talk (talk)
- "Parse events TALK from raw recordings.
-This works with the events.xml from /var/bigbluebutton/raw.
-Files should be downloaded to `emacsconf-extract-bbb-raw-dir'."
- (setq talk (emacsconf-resolve-talk talk))
- (emacsconf-extract-bbb-parse-events (emacsconf-extract-bbb-raw-events-file-name talk)))
-
-(defun emacsconf-extract-bbb-format-chat ()
- (mapconcat
- (lambda (events)
- (format "- %s (%s)\n%s"
- (assoc-default 'name events)
- (assoc-default 'id events)
- (mapconcat
- (lambda (message)
- (format " - %s: %s\n"
- (elt message 1)
- (with-temp-buffer
- (insert
- (replace-regexp-in-string
- "&#39;" "'"
- (replace-regexp-in-string "<.*?>" ""
- (elt message 2))))
- (mm-url-decode-entities)
- (buffer-string))))
- (assoc-default 'chat events)
- "")))
- (emacsconf-extract-bbb-parse-events-dir)
- ""))
-
-(defun emacsconf-extract-spookfox-update-bbb-rec ()
- (interactive)
- (let* ((data
- (spookfox-js-injection-eval-in-active-tab
- "row = [...document.querySelectorAll('.dropdown-toggle')].find((o) => o.textContent.match('Unlisted')).closest('tr'); [row.querySelector('#recording-text').getAttribute('title'), row.querySelector('a.btn-primary').getAttribute('href')]" t)
- )
- (slug
- (when (and data (string-match "^\\([^(]+\\) (" (elt data 0)))
- (split-string
- (match-string 1 (elt data 0))
- ", "))))
- (when data
- (if (> (length slug) 1)
- (setq slug (completing-read "Talk: " slug))
- (setq slug (car slug)))
- (emacsconf-with-talk-heading slug
- (if (org-entry-get (point) "BBB_REC")
- (progn
- (kill-new (elt data 1))
- (error "%s already has BBB_REC?" slug))
- (org-entry-put (point) "BBB_REC" (elt data 1))))
- (message "Updated BBB_REC for %s to %s" slug (elt data 1))
- (spookfox-js-injection-eval-in-active-tab
- "row = [...document.querySelectorAll('.dropdown-toggle')].find((o) => o.textContent.match('Unlisted')).closest('tr'); row.querySelector('.button_to .dropdown-item .fa-globe').closest('button').click();" t))))
-
-
-(defun emacsconf-extract-chat (slug speaker)
- (interactive (list
- (emacsconf-complete-talk)
- (completing-read "Speaker: "
- (seq-uniq
- (mapcar (lambda (node) (dom-attr node 'name))
- (dom-by-tag (xml-parse-region (point-min) (point-max)) 'chattimeline)))
- )))
- (let ((text
- (mapconcat (lambda (node)
- (when (string= (dom-attr node 'target) "chat")
- (let ((message
- (replace-regexp-in-string
- "\\(^[^ +]?\\): " ""
- (replace-regexp-in-string "<a href=\"\\(.+?\\)\" rel=\"nofollow\"><u>\\(.+?\\)</u></a>"
- "<\\1>" (dom-attr node 'message)))))
- (if (string-match speaker (dom-attr node 'name))
- (format "- %s: %s\n" speaker message)
- (format "- %s\n" message)))))
- (dom-by-tag (xml-parse-region (point-min) (point-max)) 'chattimeline)
- "")))
- (emacsconf-edit-wiki-page slug)
- (if (re-search-forward "# Discussion" nil t)
- (progn
- (goto-char (match-end 0))
- (insert "\n\n"))
- (goto-char (point-max)))
- (kill-new text)))
-;; TODO: Combine lines from same nick, or identify speakers with anon1/2/etc.
-(defun emacsconf-extract-chat-from-dired ()
- (interactive)
- (find-file (expand-file-name "slides_new.xml" (dired-get-file-for-visit)))
- (call-interactively 'emacsconf-extract-chat))
-
-(defun emacsconf-make-webcams-deskshare-spans (talk &optional start-ms stop-ms strategy)
- (let* ((start-ms (or start-ms 0))
- (source-dir (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir))
- (secs (/ start-ms 1000.0))
- (deskshare (xml-parse-file (expand-file-name "deskshare.xml" source-dir)))
- (webcam-video (expand-file-name "video/webcams.webm" source-dir))
- (deskshare-video (expand-file-name "deskshare/deskshare.webm" source-dir))
- (stop-ms (or stop-ms (emacsconf-get-file-duration-ms deskshare-video)))
- spans)
- (mapc (lambda (o)
- (unless (or (= secs (string-to-number (dom-attr o 'start_timestamp)))
- (= (string-to-number (dom-attr o 'start_timestamp)) 0)
- (> secs (/ stop-ms 1000.0)))
- (setq spans (cons (list :source webcam-video
- :start-ms (* secs 1000)
- :stop-ms
- (* 1000
- (if (eq strategy 'test)
- (+ secs 3)
- (max secs (string-to-number (dom-attr o 'start_timestamp))))))
- spans)))
- (when (and (<= (string-to-number (dom-attr o 'start_timestamp))
- (/ stop-ms 1000.0))
- (>= (string-to-number (dom-attr o 'stop_timestamp))
- (/ start-ms 1000.0)))
- (setq spans (cons (list :source deskshare-video
- :start-ms (max (* 1000 (string-to-number (dom-attr o 'start_timestamp)))
- start-ms)
- :stop-ms
- (if (eq strategy 'test)
- (* 1000 (+ (string-to-number (dom-attr o 'start_timestamp)) 3))
- (min (* 1000 (string-to-number (dom-attr o 'stop_timestamp)))
- stop-ms)))
- spans))
- (setq secs (string-to-number (dom-attr o 'stop_timestamp)))))
- (dom-by-tag deskshare 'event))
- (unless (>= (floor (* secs 1000)) stop-ms)
- (setq spans (cons (list :source webcam-video
- :start-ms (* 1000 secs)
- :stop-ms (if (eq strategy 'test)
- (* 1000 (+ secs 3))
- stop-ms))
- spans)))
- (if (eq strategy 'test)
- `((video ,@(nreverse spans))
- (audio ,@(mapcar (lambda (o)
- (list :source webcam-video
- :start-ms (plist-get o :start-ms)
- :stop-ms (plist-get o :stop-ms)))
- (reverse spans))))
- `((video ,@(nreverse spans))
- (audio (:source ,webcam-video :start-ms ,start-ms :stop-ms ,stop-ms))))))
-
-(defun emacsconf-get-ffmpeg-to-splice-webcam-and-recording (talk &optional start-ms stop-ms info strategy)
- "Return FFMPEG command for slicing.
-Strategies:
-- 'fast-cut-start-keyframe - find the keyframe before the start ms and cut from there, doing a fast copy.
-- 'start-keyframe-and-reencode - find the keyframe before the start ms and cut from there, reencoding.
-- 'cut-and-concat - seek to the keyframe before, slowly find the start-ms, reencode the snippet, and then do a fast copy of the remaining. May have encoding errors.
-- default: copy from start-ms to stop-ms, reencoding.
-"
- (interactive (list (emacsconf-complete-talk-info)))
- (setq talk (emacsconf-resolve-talk talk))
- (let* ((slug (plist-get talk :slug))
- (start-ms (or start-ms 0))
- (source-dir (expand-file-name (plist-get talk :bbb-meeting-id) emacsconf-extract-bbb-published-dir))
- (video-slug (plist-get (seq-find (lambda (o) (string= (plist-get o :slug) slug)) info) :video-slug))
- (output (expand-file-name (concat (plist-get talk :file-prefix) "--answers.webm") emacsconf-cache-dir))
- (webcam-video (expand-file-name "video/webcams.webm" source-dir))
- (deskshare-video (expand-file-name "deskshare/deskshare.webm" source-dir))
- (webcam-duration (emacsconf-get-file-duration-ms webcam-video))
- (stop-ms (or stop-ms webcam-duration))
- (command
- (if (file-exists-p deskshare-video)
- ;; Has deskshare
- (let* ((deskshare (xml-parse-file (expand-file-name "deskshare.xml" source-dir)))
- (final-size (compile-media-max-dimensions
- deskshare-video
- webcam-video))
- (duration (compile-media-get-file-duration-ms webcam-video))
- (spans (emacsconf-make-webcams-deskshare-spans talk start-ms stop-ms strategy))
- (compile-media-output-video-width (car final-size))
- (compile-media-output-video-height (cdr final-size)))
- (compile-media-get-command spans output))
- ;; Just webcams
- (if (and (= start-ms 0)
- (= stop-ms webcam-duration))
- (format "cp %s %s"
- webcam-video
- output)
- (compile-media-get-command
- (compile-media-split-tracks
- (list (list :source webcam-video :start-ms start-ms :stop-ms stop-ms)))
- output)))))
- (when (called-interactively-p 'any)
- (kill-new command))
- command))
+
+
+
;; (kill-new
;; (emacsconf-extract-replace-strings
@@ -1776,19 +1063,5 @@ Call with a prefix arg to store the URL as Q&A."
url)))))
-(defun emacsconf-extract-subed-copy-section-text ()
- (interactive)
- (save-excursion
- (subed-copy-region-text
- (unless (looking-at "^NOTE")
- (if (re-search-backward "^NOTE" nil t)
- (point)
- (point-min)))
- (progn
- (forward-line)
- (if (re-search-forward "^NOTE" nil t)
- (match-beginning 0)
- (point-max))))))
-
(provide 'emacsconf-extract)
;;; emacsconf-extract.el ends here