Make chapter markers and video time hyperlinks easier to note while I livestream
| org, emacsI want to make it easier to add chapter markers to my YouTube video descriptions and hyperlinks to specific times in videos in my blog posts.
Capture timestamps
Using wall-clock time via Org Mode timestamps like makes more sense to me than using video offsets because they're independent of any editing I might do.
C-u C-c C-! (org-timestamp-inactive) creates a timestamp with a time. I probably do often enough that I should create a Yasnippet for it:
# -*- mode: snippet -*-
# name: insert time
# key: zt
# --
`(format-time-string "[%Y-%m-%d %a %H:%M]")`
I also have Org capture templates, like this:
(with-eval-after-load 'org-capture
(add-to-list
'org-capture-templates
`("l" "Timestamp" item
(file+headline ,sacha-stream-inbox-file "Timestamps")
"- %U %i%?")))
I've been experimenting with a custom Org Mode link type "stream:" which:
- displays the text in a larger font with a QR code for easier copying
- sends the text to the YouTube chat via socialstream.ninja
- adds a timestamped note using the org-capture template above
Here is an example of that link in action. It's the (Log) link that I clicked on.
Let's extract that clip
(compile-media-sync
'((combined (:source
"/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs.mp4"
:original-start-ms "51:09"
:original-stop-ms "51:16"))
(combined (:source
"/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs-link-overlay.png"
:output-start-ms "0:03"
:output-stop-ms "0:04"))
(combined (:source
"/home/sacha/proj/yay-emacs/ye16-sacha-and-prot-talk-emacs-qr-chat-overlay.png"
:output-start-ms "0:05"
:output-stop-ms "0:06")))
"/home/sacha/proj/yay-emacs/ye16.1-stream-show-string-and-calculate-offset.mp4")
I used it in YE16: Sacha and Prot talk Emacs. It was handy to have a link that I could click on instead of trying to remember a keyboard shortcut and type text. For example, these are the timestamps that were filed under org-capture:
- Getting more out of livestreams
- Announcing livestreams
- Processing the recordings
- Non-packaged code
Here's a short function for getting those times:
(defun sacha-org-time-at-point ()
"Return Emacs time object for timestamp at point."
(org-timestamp-to-time (org-timestamp-from-string (org-element-property :raw-value (org-element-context)))))
Next, I wanted to turn those timestamps into a hh:mm:ss offset into the streamed video.
Calculate an Org timestamp's offset into a YouTube stream
I post my YouTube videos under a brand account so that just in case I lose access to my main sacha@sachachua.com Google account, I still have access via my @gmail.com account. To enable YouTube API access to my channel, I needed to get my brand account's email address and set it up as a test user.
- Go to https://myaccount.google.com/brandaccounts.
- Select the account.
- Click on View general account info
- Copy the
...@pages.plusgoogle.comemail address there. - Go to https://console.cloud.google.com/
- Enable the YouTube data API for my project.
- Download the credentials.json.
- Go to Data Access - Audience
- Set the User type to External
- Add my brand account as one of the Test users.
Log in at the command line:
gcloud auth application-default login \ --client-id-file=credentials.json \ --scopes="https://www.googleapis.com/auth/youtube"
Then the following code calculates the offset of the timestamp at point based on the livestream that contains it.
(defun sacha-google-youtube-stream-offset (time)
"Return the offset from the start of the stream.
When called interactively, copy it."
(interactive (list (sacha-org-time-at-point)))
(when (and (stringp time)
(string-match org-element--timestamp-regexp time))
(setq time (org-timestamp-to-time (org-timestamp-from-string (match-string 0 time)))))
(let ((result
(emacstv-format-seconds (sacha-google-youtube-live-seconds-offset-from-start-of-stream
time))))
(when (called-interactively-p 'any)
(kill-new result)
(message "%s" result))
result))
(defvar sacha-google-access-token nil "Cached access token.")
(defun sacha-google-access-token ()
"Return Google access token."
(or sacha-google-access-token
(setq sacha-google-access-token
(string-trim (shell-command-to-string "gcloud auth application-default print-access-token")))))
(defvar sacha-google-youtube-live-broadcasts nil "Cache.")
(defvar sacha-google-youtube-stream-offset-seconds 10 "Number of seconds to offset.")
(defun sacha-google-youtube-live-broadcasts ()
"Return the list of broadcasts."
(or sacha-google-youtube-live-broadcasts
(setq sacha-google-youtube-live-broadcasts
(request-response-data
(request "https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&mine=true&maxResults=10"
:headers `(("Authorization" . ,(format "Bearer %s" (sacha-google-access-token))))
:sync t
:parser #'json-read)))))
(defun sacha-google-youtube-live-get-broadcast-at-time (time)
"Return the broadcast encompassing TIME."
(seq-find (lambda (o)
(and
(alist-get 'actualStartTime (alist-get 'snippet o))
(alist-get 'actualEndTime (alist-get 'snippet o))
(time-less-p (date-to-time (alist-get 'actualStartTime (alist-get 'snippet o))) time)
(time-less-p time (date-to-time (alist-get 'actualEndTime (alist-get 'snippet o))))))
(alist-get 'items (sacha-google-youtube-live-broadcasts))))
(defun sacha-google-youtube-live-seconds-offset-from-start-of-stream (wall-time)
"Return number of seconds for WALL-TIME from the start of the stream that contains it.
Offset by `sacha-google-youtube-stream-offset-seconds'."
(+ sacha-google-youtube-stream-offset-seconds
(time-to-seconds
(time-subtract
wall-time
(date-to-time
(alist-get 'actualStartTime
(alist-get 'snippet
(sacha-google-youtube-live-get-broadcast-at-time wall-time))))))))
;; (memoize 'sacha-google-youtube-live-broadcasts)
For example:
(mapcar
(lambda (o)
(list (concat
"vtime:"
(sacha-google-youtube-stream-offset
o))
o))
timestamps)
| 19:09 | Getting more out of livestreams |
| 37:09 | Announcing livestreams |
| 45:09 | Processing the recordings |
| 51:09 | Non-packaged code |
It's not exact, but it gets me in the right neighbourhood. Then I can use the MPV player to figure out a better timestamp if I want, and I can use my custom vtime Org link time to make those clickable when people have Javascript enabled. See YE16: Sacha and Prot talk Emacs for examples.
It could be nice to log seconds someday for even finer timestamps. Still, this is handy already!