diff options
| author | Sacha Chua <sacha@sachachua.com> | 2026-01-02 10:52:40 -0500 |
|---|---|---|
| committer | Sacha Chua <sacha@sachachua.com> | 2026-01-02 10:52:40 -0500 |
| commit | efffda29a9bb0f343418a6f5bf63d234c967e3d6 (patch) | |
| tree | 88c0a9600f70527013692b59333c519f8dc0ccd8 | |
| parent | 6daeda036b6db86592f61a8fd49d5eb8082aa795 (diff) | |
| download | emacsconf-el-efffda29a9bb0f343418a6f5bf63d234c967e3d6.tar.xz emacsconf-el-efffda29a9bb0f343418a6f5bf63d234c967e3d6.zip | |
roughly move bbb code to emacsconf-bbb
| -rw-r--r-- | emacsconf-bbb.el | 765 | ||||
| -rw-r--r-- | emacsconf-extract.el | 737 |
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 + "'" "'" + (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 + "'" "'" + (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 - "'" "'" - (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 - "'" "'" - (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 |
