<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/rss.xsl" type="text/xsl"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
	<title>Sacha Chua - category - ffmpeg</title>
	<atom:link href="https://sachachua.com/blog/category/ffmpeg/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/ffmpeg" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/ffmpeg/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
	<lastBuildDate>Tue, 14 Apr 2026 18:44:16 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js</title>
		<link>https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 11 Jan 2024 17:25:40 GMT</pubDate>
    <category>emacs</category>
<category>drawing</category>
<category>org</category>
<category>ffmpeg</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/</guid>
		<description><![CDATA[<div id="text-table-of-contents" role="doc-toc">
<ul>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-types">Getting the sketches: PDFs are not all the same</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-paths-in-order">Animation style 1: displaying paths in order</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-identify-paths">Identifying paths from Supernote sketches</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-sorting">Sorting and animating the paths by IDs</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-highlights">Animation style 2: Building up a map with temporary highlights</a></li>
<li><a href="https://sachachua.com/blog/feed/index.xml#svg-animation-next">Ideas for next steps</a></li>
</ul>
</div>

<div class="tldr" id="org2916c84">
<p>
tldr (2167 words): I can make animating presentation maps easier by
writing my own functions for the Emacs text editor. In this post, I
show how I can animate an SVG element by element. I can also add IDs
to the path and use CSS to build up an SVG with temporary highlighting
in a Reveal.js presentation.
</p>

</div>

<p>
</p><div class="sketch-full"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2024-01-10-01%20Animating%20SVG%20topic%20maps%20with%20Inkscape%2C%20Emacs%2C%20FFmpeg%2C%20and%20Reveal.js%20%23drawing%20%23emacs%20%23video%20%23workflow.png" data-src="https://sketches.sachachua.com/static/2024-01-10-01%20Animating%20SVG%20topic%20maps%20with%20Inkscape%2C%20Emacs%2C%20FFmpeg%2C%20and%20Reveal.js%20%23drawing%20%23emacs%20%23video%20%23workflow.png" data-title="2024-01-10-01 Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js #drawing #emacs #video #workflow.png" data-w="2808" data-h="3744"><picture>
      <img src="https://sketches.sachachua.com/static/2024-01-10-01%20Animating%20SVG%20topic%20maps%20with%20Inkscape%2C%20Emacs%2C%20FFmpeg%2C%20and%20Reveal.js%20%23drawing%20%23emacs%20%23video%20%23workflow.png" width="2808" height="3744" alt="2024-01-10-01 Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js #drawing #emacs #video #workflow.png" loading="lazy" style="max-height: 90vw; height: auto; width: auto" decoding="async">
      <figcaption>2024-01-10-01 Animating SVG topic maps with Inkscape, Emacs, FFmpeg, and Reveal.js #drawing #emacs #video #workflow.png</figcaption>
    </picture></a></div>
<p></p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Text from the sketch</strong></summary>
<ul class="org-ul">
<li>PNG: Inkscape: trace</li>
<li>Supernote (e-ink)</li>
<li>iPad: Adobe Fresco</li>
</ul>

<p>
Convert PDF to SVG with Inkscape (Cairo option) or pdftocairo)
</p>

<ul class="org-ul">
<li>PNG / Supernote PDF: Combined shapes. Process
<ol class="org-ol">
<li>Break apart, fracture overlaps</li>
<li>Recombine</li>
<li>Set IDs</li>
<li>Sort paths -&gt; Animation style 1</li>
</ol></li>

<li>Adobe Fresco: individual elements in order; landscape feels natural</li>
</ul>

<p>
Animation styles
</p>

<ul class="org-ul">
<li>Animation style 1: Display elements one after another</li>
<li>Animation style 2: Display elements one after another, and also show/hide highlights
<ul class="org-ul">
<li>Table: slide ID, IDs to add, temporary highlights -&gt; Reveal.js: CSS with transitions</li>
</ul></li>
</ul>

<p>
Ideas for next steps:
</p>

<ul class="org-ul">
<li>Explore graphviz &amp; other diagramming tools</li>
<li>Frame-by-frame SVGs
<ul class="org-ul">
<li>on include</li>
<li>write to files</li>
</ul></li>
<li>FFmpeg crossfade</li>
<li>Recording Reveal.js presentations</li>
<li>Use OCR results?</li>
</ul>


</details>

<p>
I often have a hard time organizing my thoughts into a linear
sequence. Sketches are nice because they let me jump around and still
show the connections between ideas. For presentations, I'd like to
walk people through these sketches by highlighting different areas.
For example, I might highlight the current topic or show the previous
topics that are connected to the current one. Of course, this is
something Emacs can help with. Before we dive into it, here are quick
previews of the kinds of animation I'm talking about:
</p>


<figure id="org9c4a1c6">
<img src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/animation-loop.gif" alt="animation-loop.gif">

<figcaption><span class="figure-number">Figure 1: </span>Animation style 1: based on drawing order</figcaption>
</figure>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_11.57.51.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_11.57.51.webm">Download the video</a></video><figcaption>Animation style 2: building up a map with temporary highlights</figcaption></figure>
<p></p>
<div id="outline-container-svg-types" class="outline-2">
<h3 id="svg-types">Getting the sketches: PDFs are not all the same</h3>
<div class="outline-text-2" id="text-svg-types">
<p>
Let's start with getting the sketches. I usually export my sketches as
PNGs from my <a href="https://supernote.com/">Supernote</a> A5X. But if I know that I'm going to animate a
sketch, I can export it as a PDF. I've recently been experimenting
with Adobe Fresco on the iPad, which can also export to PDF. The PDF I
get from Fresco is easier to animate, but I prefer to draw on the
Supernote because it's an e-ink device (and because the kiddo usually
uses the iPad).
</p>

<p>
If I start with a PNG, I could use Inkscape to trace the PNG and turn
it into an SVG. I think Inkscape uses autotrace behind the scenes. I
don't usually put my highlights on a separate layer, so autotrace will
make odd shapes.
</p>

<p>
It's a lot easier if you start off with vector graphics in the first
place. I can export a vector PDF from the SuperNote A5X and either
import it into Inkscape using the Cairo option or use the command-line
pdftocairo tool.
</p>

<p>
I've been looking into using Adobe Fresco, which is a free app
available for the iPad. Fresco's PDF export can be converted to an SVG
using Inkscape or PDF to Cairo. What I like about the output of this
app is that it gives me individual elements as their own paths and
they're listed in order of drawing. This makes it really easy to
animate by just going through the paths in order.
</p>
</div>
</div>
<div id="outline-container-svg-paths-in-order" class="outline-2">
<h3 id="svg-paths-in-order">Animation style 1: displaying paths in order</h3>
<div class="outline-text-2" id="text-svg-paths-in-order">
<p>
Here's a sample SVG file that pdfcairo creates from an Adobe Fresco
PDF export:
</p>


<div class="org-src-container">
<pre class="src src-sh">pdftocairo -svg ~/Downloads/subed-audio.pdf ~/Downloads/subed-audio.svg
</pre>
</div>


<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Sample SVG</strong></summary>

<figure id="org6cff3aa">
<img src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/subed-audio.svg" alt="subed-audio.svg" class="org-svg">

</figure>


</details>

<p>
Adobe Fresco also includes built-in time-lapse, but since I often like
to move things around or tidy things up, it's easier to just work with
the final image, export it as a PDF, and convert it to an SVG.
</p>

<p>
I can make a very simple animation by setting the opacity of all the
paths to 0, then looping through the elements to set the opacity back
to 1 and write that version of the SVG to a separate file.
From <a href="https://sachachua.com/dotemacs#how-can-i-generate-png-frames-that-step-through-the-highlights">how-can-i-generate-png-frames-that-step-through-the-highlights</a>:
</p>

<p>
</p><details><summary>my-animate-svg-paths: Add one path at a time. Save the resulting SVGs to OUTPUT-DIR.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-animate-svg-paths</span> (filename output-dir)
  <span class="org-doc">"Add one path at a time. Save the resulting SVGs to OUTPUT-DIR."</span>
  (<span class="org-keyword">unless</span> (file-directory-p output-dir)
    (make-directory output-dir t))
  (<span class="org-keyword">let*</span> ((dom (xml-parse-file filename))
         (paths (seq-filter (<span class="org-keyword">lambda</span> (e) (<span class="org-keyword">dom-attr</span> e <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>))
                            (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>)))
         (total (length paths))
         (frame-num (length paths))
         result)
    (<span class="org-keyword">dolist</span> (elem paths)
      (dom-set-attribute elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>
                         (concat
                          (<span class="org-keyword">dom-attr</span> elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>)
                          <span class="org-string">";mix-blend-mode:darken"</span>)))
    (<span class="org-keyword">with-temp-file</span> (expand-file-name (format <span class="org-string">"frame-%03d.svg"</span> (1+ frame-num)) output-dir)
      (xml-print dom))
    (<span class="org-keyword">dolist</span> (elem paths)
      (dom-set-attribute elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>
                         (concat
                          (<span class="org-keyword">dom-attr</span> elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>)
                          <span class="org-string">";fill-opacity:0"</span>)))
    (<span class="org-keyword">dolist</span> (elem paths)
      (<span class="org-keyword">with-temp-file</span> (expand-file-name
                       (format <span class="org-string">"frame-%03d.svg"</span>
                               (- total frame-num))
                       output-dir)
        (message <span class="org-string">"%03d"</span> frame-num)
        (dom-set-attribute elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>
                           (concat (<span class="org-keyword">dom-attr</span> elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>)
                                   <span class="org-string">";fill-opacity:1"</span>))
        (<span class="org-keyword">push</span> (list (format <span class="org-string">"frame-%03d.svg"</span>
                            (1+ (- total frame-num)))
                    (<span class="org-keyword">dom-attr</span> elem <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>))
              result)
        (<span class="org-keyword">setq</span> frame-num (1- frame-num))
        (xml-print dom)))
    (reverse result)))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#svg-animating-paths-in-order">Context</a></div>
<p></p>

<p>
Here's how I call it:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(my-animate-svg-paths <span class="org-string">"~/Downloads/subed-audio.svg"</span> <span class="org-string">"/tmp/subed-audio/frames"</span> t)
</pre>
</div>


<p>
Then I can use FFmpeg to combine all of those frames into a video:
</p>


<div class="org-src-container">
<pre class="src src-sh">ffmpeg -i frame-%03d.svg -vf palettegen -y palette.png
ffmpeg -framerate 30 -i frame-%03d.svg -i palette.png -lavfi <span class="org-string">"paletteuse"</span> -loop 0 -y animation-loop.gif
</pre>
</div>



<figure id="orgc3765a8">
<img src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/animation-loop.gif" alt="animation-loop.gif">

<figcaption><span class="figure-number">Figure 2: </span>Animating SVG paths based on drawing order</figcaption>
</figure>

<p>
Neither Supernote nor Adobe Fresco give me the original stroke
information. These are filled shapes, so I can't animate something
drawing it. But having different elements appear in sequence is fine
for my purposes. If you happen to know how to get stroke information
out of Supernote .note files or of an iPad app that exports nice
single-line SVGs that have stroke direction, I would love to hear
about it.
</p>
</div>
</div>
<div id="outline-container-svg-identify-paths" class="outline-2">
<h3 id="svg-identify-paths">Identifying paths from Supernote sketches</h3>
<div class="outline-text-2" id="text-svg-identify-paths">
<p>
When I export a PDF from Supernote and convert it to an SVG, each
color is a combined shape with all the elements. If I want to animate
parts of the image, I have to break it up and recombine selected
elements (Inkscape's Ctrl-k shortcut) so that the holes in shapes are
properly handled. This is a bit of a tedious process and it usually
ends up with elements in a pretty random order. Since I have to
reorder elements by hand, I don't really want to animate the sketch
letter-by-letter. Instead, I combine them into larger chunks like
topics or paragraphs.
</p>

<p>
The following code takes the PDF, converts it to an SVG, recolours
highlights, and then breaks up paths into elements:
</p>

<p>
</p><details><summary>my-sketch-convert-pdf-and-break-up-paths: Convert PDF to SVG and break up paths.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-sketch-convert-pdf-and-break-up-paths</span> (pdf-file <span class="org-type">&amp;optional</span> rotate)
  <span class="org-doc">"Convert PDF to SVG and break up paths."</span>
  (<span class="org-keyword">interactive</span> (list (read-file-name
                      (format <span class="org-string">"PDF (%s): "</span>
                              (my-latest-file <span class="org-string">"~/Dropbox/Supernote/EXPORT/"</span> <span class="org-string">"pdf"</span>))
                      <span class="org-string">"~/Dropbox/Supernote/EXPORT/"</span>
                      (my-latest-file <span class="org-string">"~/Dropbox/Supernote/EXPORT/"</span> <span class="org-string">"pdf"</span>)
                      t
                      nil
                      (<span class="org-keyword">lambda</span> (s) (string-match <span class="org-string">"pdf"</span> s)))))
  (<span class="org-keyword">unless</span> (file-exists-p (concat (file-name-sans-extension pdf-file) <span class="org-string">".svg"</span>))
    (call-process <span class="org-string">"pdftocairo"</span> nil nil nil <span class="org-string">"-svg"</span> (expand-file-name pdf-file)
                  (expand-file-name (concat (file-name-sans-extension pdf-file) <span class="org-string">".svg"</span>))))
  (<span class="org-keyword">let</span> ((dom (xml-parse-file (expand-file-name (concat (file-name-sans-extension pdf-file) <span class="org-string">".svg"</span>))))
        highlights)
    (<span class="org-keyword">setq</span> highlights (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">g</span> <span class="org-highlight-quoted-quote">'</span>((id . <span class="org-string">"highlights"</span>))))
    (dom-append-child dom highlights)
    (<span class="org-keyword">dolist</span> (path (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>))
      <span class="org-comment-delimiter">;;  </span><span class="org-comment">recolor and move</span>
      (<span class="org-keyword">unless</span> (string-match (regexp-quote <span class="org-string">"rgb(0%,0%,0%)"</span>) (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>) <span class="org-string">""</span>))
        (dom-remove-node dom path)
        (dom-append-child highlights path)
        (dom-set-attribute
         path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>
         (replace-regexp-in-string
          (regexp-quote <span class="org-string">"rgb(78.822327%,78.822327%,78.822327%)"</span>)
          <span class="org-string">"#f6f396"</span>
          (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>) <span class="org-string">""</span>))))
      (<span class="org-keyword">let</span> ((parent (dom-parent dom path)))
        <span class="org-comment-delimiter">;; </span><span class="org-comment">break apart</span>
        (<span class="org-keyword">when</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>)
          (<span class="org-keyword">dolist</span> (part (split-string (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>) <span class="org-string">"M "</span> t <span class="org-string">" +"</span>))
            (dom-append-child
             parent
             (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span> <span class="org-highlight-quoted-quote">`</span>((style . ,(<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">style</span>))
                               (d . ,(concat <span class="org-string">"M "</span> part))))))
          (dom-remove-node dom path))))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">remove the use</span>
    (<span class="org-keyword">dolist</span> (use (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">use</span>))
      (dom-remove-node dom use))
    (<span class="org-keyword">dolist</span> (use (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">image</span>))
      (dom-remove-node dom use))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">move the first g down</span>
    (<span class="org-keyword">let</span> ((g (car (dom-by-id dom <span class="org-string">"surface1"</span>))))
      (<span class="org-keyword">setf</span> (cddar dom)
            (seq-remove (<span class="org-keyword">lambda</span> (o)
                          (<span class="org-keyword">and</span> (listp o) (string= (<span class="org-keyword">dom-attr</span> o <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"surface1"</span>)))
                        (dom-children dom)))
      (dom-append-child dom g)
      (<span class="org-keyword">when</span> rotate
        (<span class="org-keyword">let*</span> ((old-width (<span class="org-keyword">dom-attr</span> dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">width</span>))
               (old-height (<span class="org-keyword">dom-attr</span> dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">height</span>))
               (view-box (mapcar <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">string-to-number</span> (split-string (<span class="org-keyword">dom-attr</span> dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">viewBox</span>))))
               (rotate (format <span class="org-string">"rotate(90) translate(0 %s)"</span> (- (elt view-box 3)))))
          (dom-set-attribute dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">width</span> old-height)
          (dom-set-attribute dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">height</span> old-width)
          (dom-set-attribute dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">viewBox</span> (format <span class="org-string">"0 0 %d %d"</span> (elt view-box 3) (elt view-box 2)))
          (dom-set-attribute highlights <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">transform</span> rotate)
          (dom-set-attribute g <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">transform</span> rotate))))
    (<span class="org-keyword">with-temp-file</span> (expand-file-name (concat (file-name-sans-extension pdf-file) <span class="org-string">"-split.svg"</span>))
      (svg-print (car dom)))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#how-can-i-generate-png-frames-that-step-through-the-highlights">Context</a></div>
<p></p>


<figure id="orgc788582">
<img src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2023-10-split.svg" alt="2023-10-split.svg" class="org-svg">

<figcaption><span class="figure-number">Figure 3: </span>Image after splitting up into elements</figcaption>
</figure>

<p>
You can see how the spaces inside letters like "o" end up being black.
Selecting and combining those paths fixes that.
</p>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-10_21.16.48.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-10_21.16.48.webm">Download the video</a></video><figcaption>Combining paths in Inkscape</figcaption></figure>
<p></p>

<p>
If there were shapes that were touching, then I need to draw lines and
fracture the shapes in order to break them apart.
</p>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-10_21.19.17.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-10_21.19.17.webm">Download the video</a></video><figcaption>Fracturing shapes and checking the highlights</figcaption></figure>
<p></p>

<p>
The end result should be an SVG with the different chunks that I might
want to animate, but I need to identify the paths first. You can
assign object IDs in Inkscape, but this is a bit of an annoying
process since I haven't figured out a keyboard-friendly way to set
object IDs. I usually find it easier to just set up an Autokey
shortcut (or AutoHotkey in Windows) to click on the ID text box so
that I can type something in.
</p>

<details><summary>Autokey script for clicking</summary>
<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> time
<span class="org-variable-name">x</span>, <span class="org-variable-name">y</span> <span class="org-operator">=</span> mouse.get_location()
<span class="org-comment-delimiter"># </span><span class="org-comment">Use the coordinates of the ID text field on your screen; xev can help</span>
mouse.click_absolute(3152, 639, 1)
time.sleep(1)
keyboard.send_keys(<span class="org-string">"&lt;ctrl&gt;+a"</span>)
mouse.move_cursor(x, y)
</pre>
</div>

</details>

<p>
Then I can select each element, press the shortcut key, and type an ID
into the textbox. I might use "t-&#x2026;" to indicate the text for a map
section, "h-&#x2026;" to indicate a highlight, and arrows by specifying
their start and end.
</p>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_06.01.49.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_06.01.49.webm">Download the video</a></video><figcaption>Setting IDs in Inkscape</figcaption></figure>
<p></p>

<p>
To simplify things, I wrote a function in Emacs that will go through
the different groups that I've made, show each path in a different
color and with a reasonable guess at a bounding box, and prompt me for
an ID. This way, I can quickly assign IDs to all of the paths. The
completion is mostly there to make sure I don't accidentally reuse an
ID, although it can try to combine paths if I specify the ID. It saves
the paths after each change so that I can start and stop as needed.
Identifying paths in Emacs is usually much nicer than identifying them
in Inkscape.
</p>

<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_07.33.50.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_07.33.50.webm">Download the video</a></video><figcaption>Identifying paths inside Emacs</figcaption></figure>
<p></p>

<p>
</p><details><summary>my-svg-identify-paths: Prompt for IDs for each path in FILENAME.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-svg-identify-paths</span> (filename)
  <span class="org-doc">"Prompt for IDs for each path in FILENAME."</span>
  (<span class="org-keyword">interactive</span> (list (read-file-name <span class="org-string">"SVG: "</span> nil nil
                                     (<span class="org-keyword">lambda</span> (f) (string-match <span class="org-string">"\\.svg$"</span> f)))))
  (<span class="org-keyword">let*</span> ((dom (car (xml-parse-file filename)))
         (paths (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>))
         (vertico-count 3)
         (ids (seq-keep (<span class="org-keyword">lambda</span> (path)
                          (<span class="org-keyword">unless</span> (string-match <span class="org-string">"path[0-9]+"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"path0"</span>))
                            (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>)))
                        paths))
         (edges (window-inside-pixel-edges (get-buffer-window)))
         id)
    (my-svg-display <span class="org-string">"*image*"</span> dom nil t)
    (<span class="org-keyword">dolist</span> (path paths)
      (<span class="org-keyword">when</span> (string-match <span class="org-string">"path[0-9]+"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"path0"</span>))
        <span class="org-comment-delimiter">;; </span><span class="org-comment">display the image with an outline</span>
        (<span class="org-keyword">unwind-protect</span>
            (<span class="org-keyword">progn</span>
              (my-svg-display <span class="org-string">"*image*"</span> dom (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) t)
              (<span class="org-keyword">setq</span> id (completing-read
                        (format <span class="org-string">"ID (%s): "</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>))
                        ids))
              <span class="org-comment-delimiter">;; </span><span class="org-comment">already exists, merge with existing element</span>
              (<span class="org-keyword">if-let</span> ((old (dom-by-id dom id)))
                  (<span class="org-keyword">progn</span>
                    (dom-set-attribute
                     old
                     <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>
                     (concat (<span class="org-keyword">dom-attr</span> (dom-by-id dom id) <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>)
                             <span class="org-string">" "</span>
                             <span class="org-comment-delimiter">;; </span><span class="org-comment">change relative to absolute</span>
                             (replace-regexp-in-string <span class="org-string">"^m"</span> <span class="org-string">"M"</span>
                                                       (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">d</span>))))
                    (dom-remove-node dom path)
                    (<span class="org-keyword">setq</span> id nil))
                (dom-set-attribute path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span> id)
                (add-to-list <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">ids</span> id))))
        <span class="org-comment-delimiter">;; </span><span class="org-comment">save the image just in case we get interrupted halfway through</span>
        (<span class="org-keyword">with-temp-file</span> filename
          (svg-print dom))))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#svg-identifying-paths">Context</a></div>
<p></p>
</div>
</div>
<div id="outline-container-svg-sorting" class="outline-2">
<h3 id="svg-sorting">Sorting and animating the paths by IDs</h3>
<div class="outline-text-2" id="text-svg-sorting">
<p>
Then I can animate SVGs by specifying the IDs. I can reorder the paths
in the SVG itself so that I can animate it group by group, like the
way that the Adobe Fresco SVGs were animated element by element.
</p>

<details><summary>Reordering paths</summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp" id="org2c2010e">(my-svg-reorder-paths <span class="org-string">"~/proj/2023-12-audio-workflow/map.svg"</span>
                      <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"t-start"</span> <span class="org-string">"h-audio"</span> <span class="org-string">"h-capture"</span> <span class="org-string">"t-but"</span> <span class="org-string">"t-mic"</span> <span class="org-string">"h-mic"</span>
                        <span class="org-string">"t-reviewing"</span> <span class="org-string">"h-reviewing"</span>
                        <span class="org-string">"t-words"</span> <span class="org-string">"h-words"</span> <span class="org-string">"t-workflow"</span> <span class="org-string">"h-workflow"</span>
                        <span class="org-string">"t-lapel"</span> <span class="org-string">"h-lapel"</span>
                        <span class="org-string">"mic-recorder"</span> <span class="org-string">"t-recorder"</span> <span class="org-string">"h-recorder"</span>
                        <span class="org-string">"t-syncthing"</span> <span class="org-string">"h-sync"</span>
                        <span class="org-string">"t-keywords"</span> <span class="org-string">"h-keywords"</span> <span class="org-string">"t-keyword-types"</span>
                        <span class="org-string">"t-lines"</span> <span class="org-string">"h-lines"</span>
                        <span class="org-string">"t-align"</span> <span class="org-string">"h-align"</span>
                        <span class="org-string">"arrow"</span>
                        <span class="org-string">"t-org"</span> <span class="org-string">"h-org"</span> <span class="org-string">"t-todo"</span> <span class="org-string">"h-todo"</span> <span class="org-string">"h-linked"</span>
                        <span class="org-string">"t-jump"</span> <span class="org-string">"h-jump"</span>
                        <span class="org-string">"t-waveform"</span> <span class="org-string">"h-waveform"</span>
                        <span class="org-string">"t-someday"</span>
                        <span class="org-string">"h-sections"</span>
                        <span class="org-string">"t-speech-recognition"</span> <span class="org-string">"h-speech-recognition"</span>
                        <span class="org-string">"t-ai"</span> <span class="org-string">"h-ai"</span>
                        <span class="org-string">"t-summary"</span>
                        <span class="org-string">"extra"</span>)
                      <span class="org-string">"~/proj/2023-12-audio-workflow/map-output.svg"</span>)
(my-animate-svg-paths <span class="org-string">"~/proj/2023-12-audio-workflow/map-output.svg"</span>
                      <span class="org-string">"~/proj/2023-12-audio-workflow/frames/"</span>)
</pre>
</div>

</details>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Table of filenames after reordering paths and animating the image</strong></summary>
<table>


<colgroup>
<col class="org-left">

<col class="org-left">
</colgroup>
<tbody>
<tr>
<td class="org-left">frame-001.svg</td>
<td class="org-left">t-start</td>
</tr>

<tr>
<td class="org-left">frame-002.svg</td>
<td class="org-left">h-audio</td>
</tr>

<tr>
<td class="org-left">frame-003.svg</td>
<td class="org-left">h-capture</td>
</tr>

<tr>
<td class="org-left">frame-004.svg</td>
<td class="org-left">t-but</td>
</tr>

<tr>
<td class="org-left">frame-005.svg</td>
<td class="org-left">t-mic</td>
</tr>

<tr>
<td class="org-left">frame-006.svg</td>
<td class="org-left">h-mic</td>
</tr>

<tr>
<td class="org-left">frame-007.svg</td>
<td class="org-left">t-reviewing</td>
</tr>

<tr>
<td class="org-left">frame-008.svg</td>
<td class="org-left">h-reviewing</td>
</tr>

<tr>
<td class="org-left">frame-009.svg</td>
<td class="org-left">t-words</td>
</tr>

<tr>
<td class="org-left">frame-010.svg</td>
<td class="org-left">h-words</td>
</tr>

<tr>
<td class="org-left">frame-011.svg</td>
<td class="org-left">t-workflow</td>
</tr>

<tr>
<td class="org-left">frame-012.svg</td>
<td class="org-left">h-workflow</td>
</tr>

<tr>
<td class="org-left">frame-013.svg</td>
<td class="org-left">t-lapel</td>
</tr>

<tr>
<td class="org-left">frame-014.svg</td>
<td class="org-left">h-lapel</td>
</tr>

<tr>
<td class="org-left">frame-015.svg</td>
<td class="org-left">mic-recorder</td>
</tr>

<tr>
<td class="org-left">frame-016.svg</td>
<td class="org-left">t-recorder</td>
</tr>

<tr>
<td class="org-left">frame-017.svg</td>
<td class="org-left">h-recorder</td>
</tr>

<tr>
<td class="org-left">frame-018.svg</td>
<td class="org-left">t-syncthing</td>
</tr>

<tr>
<td class="org-left">frame-019.svg</td>
<td class="org-left">h-sync</td>
</tr>

<tr>
<td class="org-left">frame-020.svg</td>
<td class="org-left">t-keywords</td>
</tr>

<tr>
<td class="org-left">frame-021.svg</td>
<td class="org-left">h-keywords</td>
</tr>

<tr>
<td class="org-left">frame-022.svg</td>
<td class="org-left">t-keyword-types</td>
</tr>

<tr>
<td class="org-left">frame-023.svg</td>
<td class="org-left">t-lines</td>
</tr>

<tr>
<td class="org-left">frame-024.svg</td>
<td class="org-left">h-lines</td>
</tr>

<tr>
<td class="org-left">frame-025.svg</td>
<td class="org-left">t-align</td>
</tr>

<tr>
<td class="org-left">frame-026.svg</td>
<td class="org-left">h-align</td>
</tr>

<tr>
<td class="org-left">frame-027.svg</td>
<td class="org-left">arrow</td>
</tr>

<tr>
<td class="org-left">frame-028.svg</td>
<td class="org-left">t-org</td>
</tr>

<tr>
<td class="org-left">frame-029.svg</td>
<td class="org-left">h-org</td>
</tr>

<tr>
<td class="org-left">frame-030.svg</td>
<td class="org-left">t-todo</td>
</tr>

<tr>
<td class="org-left">frame-031.svg</td>
<td class="org-left">h-todo</td>
</tr>

<tr>
<td class="org-left">frame-032.svg</td>
<td class="org-left">h-linked</td>
</tr>

<tr>
<td class="org-left">frame-033.svg</td>
<td class="org-left">t-jump</td>
</tr>

<tr>
<td class="org-left">frame-034.svg</td>
<td class="org-left">h-jump</td>
</tr>

<tr>
<td class="org-left">frame-035.svg</td>
<td class="org-left">t-waveform</td>
</tr>

<tr>
<td class="org-left">frame-036.svg</td>
<td class="org-left">h-waveform</td>
</tr>

<tr>
<td class="org-left">frame-037.svg</td>
<td class="org-left">t-someday</td>
</tr>

<tr>
<td class="org-left">frame-038.svg</td>
<td class="org-left">h-sections</td>
</tr>

<tr>
<td class="org-left">frame-039.svg</td>
<td class="org-left">t-speech-recognition</td>
</tr>

<tr>
<td class="org-left">frame-040.svg</td>
<td class="org-left">h-speech-recognition</td>
</tr>

<tr>
<td class="org-left">frame-041.svg</td>
<td class="org-left">t-ai</td>
</tr>

<tr>
<td class="org-left">frame-042.svg</td>
<td class="org-left">h-ai</td>
</tr>

<tr>
<td class="org-left">frame-043.svg</td>
<td class="org-left">t-summary</td>
</tr>

<tr>
<td class="org-left">frame-044.svg</td>
<td class="org-left">extra</td>
</tr>
</tbody>
</table>


</details>

<p>
The table of filenames makes it easy to use specific frames as part of
a presentation or video.
</p>

<p>
Here is the result as a video:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let</span> ((compile-media-output-video-width 1280)
      (compile-media-output-video-height 720))
  (my-ffmpeg-animate-images
   (directory-files <span class="org-string">"~/proj/2023-12-audio-workflow/frames/"</span> t <span class="org-string">"\\.svg$"</span>)
   (expand-file-name <span class="org-string">"~/proj/2023-12-audio-workflow/frames/animation.webm"</span>)
   4))
</pre>
</div>


<p>
</p><figure><video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/animation.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/animation.webm">Download the video</a></video><figcaption>Animation of SVG by paths</figcaption></figure>
<p></p>

<p>
The way it works is that the <code>my-svg-reorder-paths</code> function removes
and readds elements following the list of IDs specified, so
everything's ready to go for step-by-step animation. Here's the code:
</p>

<p>
</p><details><summary>my-svg-reorder-paths: Sort paths in FILENAME.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-svg-reorder-paths</span> (filename <span class="org-type">&amp;optional</span> ids output-filename)
  <span class="org-doc">"Sort paths in FILENAME."</span>
  (<span class="org-keyword">interactive</span> (list (read-file-name <span class="org-string">"SVG: "</span> nil nil (<span class="org-keyword">lambda</span> (f) (string-match <span class="org-string">"\\.svg$"</span> f)))
                     nil (read-file-name <span class="org-string">"Output: "</span>)))
  (<span class="org-keyword">let*</span> ((dom (car (xml-parse-file filename)))
         (paths (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>))
         (parent (dom-parent dom (car paths)))
         (ids-left
          (nreverse (seq-keep (<span class="org-keyword">lambda</span> (path)
                                (<span class="org-keyword">unless</span> (string-match <span class="org-string">"path[0-9]+"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"path0"</span>))
                                  (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>)))
                              paths)))
         list)
    (<span class="org-keyword">when</span> (called-interactively-p)
      (<span class="org-keyword">while</span> ids-left
        (my-svg-display <span class="org-string">"*image*"</span> dom (car ids-left))
        (<span class="org-keyword">let</span> ((current (completing-read
                        (format <span class="org-string">"ID (%s): "</span>
                                (car ids-left))
                        ids-left nil nil nil nil (car ids-left)))
              node)
          (add-to-list <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">ids</span> current)
          (<span class="org-keyword">setq</span> ids-left (seq-remove (<span class="org-keyword">lambda</span> (o) (string= o current)) ids-left)))))
    (<span class="org-keyword">if</span> ids <span class="org-comment-delimiter">;; </span><span class="org-comment">reorganize under the first path's parent</span>
        (<span class="org-keyword">progn</span>
          (<span class="org-keyword">dolist</span> (id ids)
            (<span class="org-keyword">if-let</span> ((node (car (dom-by-id dom id))))
                (<span class="org-keyword">progn</span>
                  (dom-remove-node dom node)
                  (dom-append-child parent node))
              (message <span class="org-string">"Could not find %s"</span> id)))
          (<span class="org-keyword">with-temp-file</span> (<span class="org-keyword">or</span> output-filename filename)
            (svg-print dom))))
    (nreverse (seq-keep (<span class="org-keyword">lambda</span> (path)
                          (<span class="org-keyword">unless</span> (string-match <span class="org-string">"path[0-9]+"</span> (<span class="org-keyword">or</span> (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>) <span class="org-string">"path0"</span>))
                            (<span class="org-keyword">dom-attr</span> path <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">id</span>)))
                        (dom-by-tag dom <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">path</span>)))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#svg-sorting-paths">Context</a></div>
<p></p>
</div>
</div>
<div id="outline-container-svg-highlights" class="outline-2">
<h3 id="svg-highlights">Animation style 2: Building up a map with temporary highlights</h3>
<div class="outline-text-2" id="text-svg-highlights">
<p>
I can also use CSS rules to transition between opacity values for more
complex animations. For my <a href="https://emacsconf.org/2023/talks/emacsconf">EmacsConf 2023 presentation</a>, I wanted to
make <a href="https://sachachua.com/proj/emacsconf-2023-emacsconf/#/sec-title-slide">a self-paced, narrated presentation</a> so that people could follow
hyperlinks, read the source code, and explore. I wanted to include a
<a href="https://sachachua.com/proj/emacsconf-2023-emacsconf/#/slide-props-map">map</a> so that I could try to make sense of everything. For this map, I
wanted to highlight the previous sections that were connected to the
topic for the current section.
</p>

<p>
I used a custom Org link to include the full contents of the SVG
instead of just including it with an img tag.
</p>


<div class="org-src-container">
<pre class="src src-org"><span class="org-org-meta-line">#+ATTR_HTML: :class r-stretch</span>
<span class="org-org-link">my-include:~/proj/emacsconf-2023-emacsconf/map.svg?wrap=export html</span>
</pre>
</div>


<p>
</p><details><summary>my-include-export: Export PATH to FORMAT using the specified wrap parameter.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-include-export</span> (path _ format _)
  <span class="org-doc">"Export PATH to FORMAT using the specified wrap parameter."</span>
  (<span class="org-keyword">let</span> (params body start end)
    (<span class="org-keyword">when</span> (string-match <span class="org-string">"^</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.*+?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(?:</span></span><span class="org-string">::</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">|</span></span><span class="org-string">\\?</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">.*+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">"</span> path)
      (<span class="org-keyword">setq</span> params (<span class="org-keyword">save-match-data</span> (org-protocol-convert-query-to-plist (match-string 2 path)))
            path (match-string 1 path)))
    (<span class="org-keyword">with-temp-buffer</span>
      (insert-file-contents-literally path)
      (<span class="org-keyword">when</span> (string-match <span class="org-string">"\\.org$"</span> path)
        (org-mode))
      (<span class="org-keyword">if</span> (plist-get params <span class="org-builtin">:name</span>)
          (<span class="org-keyword">when</span> (org-babel-find-named-block (plist-get params <span class="org-builtin">:name</span>))
            (goto-char (org-babel-find-named-block (plist-get params <span class="org-builtin">:name</span>)))
            (<span class="org-keyword">let</span> ((block (org-element-context)))
              (<span class="org-keyword">setq</span> start (org-element-begin block)
                    end (org-element-end block))))
        (goto-char (point-min))
        (<span class="org-keyword">when</span> (plist-get params <span class="org-builtin">:from-regexp</span>)
          (re-search-forward (url-unhex-string (plist-get params <span class="org-builtin">:from-regexp</span>)))
          (goto-char (match-beginning 0)))
        (<span class="org-keyword">setq</span> start (point))
        (<span class="org-keyword">setq</span> end (point-max))
        (<span class="org-keyword">when</span> (plist-get params <span class="org-builtin">:to-regexp</span>)
          (re-search-forward (url-unhex-string (plist-get params <span class="org-builtin">:to-regexp</span>)))
          (<span class="org-keyword">setq</span> end (match-beginning 0))))
      (<span class="org-keyword">setq</span> body (buffer-substring start end)))
    (<span class="org-keyword">with-temp-buffer</span>
      (<span class="org-keyword">when</span> (plist-get params <span class="org-builtin">:wrap</span>)
        (<span class="org-keyword">let*</span> ((wrap (plist-get params <span class="org-builtin">:wrap</span>))
               block args)
          (<span class="org-keyword">when</span> (string-match <span class="org-string">"\\&lt;</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">\\S-+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string"> +.*</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">?"</span> wrap)
            (<span class="org-keyword">setq</span> block (match-string 1 wrap))
            (<span class="org-keyword">setq</span> args (match-string 2 wrap))
            (<span class="org-keyword">setq</span> body (format <span class="org-string">"#+BEGIN_%s%s\n%s\n#+END_%s\n"</span>
                               block (<span class="org-keyword">or</span> args <span class="org-string">""</span>)
                               body
                               block)))))
      (<span class="org-keyword">when</span> (plist-get params <span class="org-builtin">:summary</span>)
        (<span class="org-keyword">setq</span> body (format <span class="org-string">"#+begin_my_details %s\n%s\n#+end_my_details\n"</span>
                           (plist-get params <span class="org-builtin">:summary</span>)
                           body)))
      (insert body)
      (org-export-as format nil nil t))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#org-mode-including-portions-of-files-between-two-regular-expressions">Context</a></div>
<p></p>

<p>
I wanted to be able to specify the entire sequence using a table in
the Org Mode source for my presentation. Each row had the slide ID, a
list of highlights in the form <code>prev1,prev2;current</code>, and a
comma-separated list of elements to add to the full-opacity view.
</p>

<div class="bordered" id="org9d17b2b">
<table id="org67db231">


<colgroup>
<col class="org-left">

<col class="org-left">

<col class="org-left">
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Slide</th>
<th scope="col" class="org-left">Highlight</th>
<th scope="col" class="org-left">Additional elements</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">props-map</td>
<td class="org-left">h-email;h-properties</td>
<td class="org-left">t-email,email-properties,t-properties</td>
</tr>

<tr>
<td class="org-left">file-prefixes</td>
<td class="org-left">h-properties;h-filename</td>
<td class="org-left">t-filename,properties-filename</td>
</tr>

<tr>
<td class="org-left">renaming</td>
<td class="org-left">h-filename;h-renaming</td>
<td class="org-left">t-renaming,filename-renaming</td>
</tr>

<tr>
<td class="org-left">shell-scripts</td>
<td class="org-left">h-renaming;h-shell-scripts</td>
<td class="org-left">renaming-shell-scripts,t-shell-scripts</td>
</tr>

<tr>
<td class="org-left">availability</td>
<td class="org-left">h-properties;h-timezone</td>
<td class="org-left">t-timezone,properties-timezone</td>
</tr>

<tr>
<td class="org-left">schedule</td>
<td class="org-left">h-timezone;h-schedule</td>
<td class="org-left">t-schedule,timezone-schedule</td>
</tr>

<tr>
<td class="org-left">emailing-speakers</td>
<td class="org-left">h-timezone,h-mail-merge;h-emailing-speakers</td>
<td class="org-left">schedule-emailing-speakers,t-emailing-speakers</td>
</tr>

<tr>
<td class="org-left">template</td>
<td class="org-left">h-properties;h-template</td>
<td class="org-left">t-template,properties-template</td>
</tr>

<tr>
<td class="org-left">wiki</td>
<td class="org-left">h-template;h-wiki</td>
<td class="org-left">t-wiki,template-wiki,schedule-wiki</td>
</tr>

<tr>
<td class="org-left">pad</td>
<td class="org-left">h-template;h-pad</td>
<td class="org-left">template-pad,t-pad</td>
</tr>

<tr>
<td class="org-left">mail-merge</td>
<td class="org-left">h-template;h-mail-merge</td>
<td class="org-left">t-mail-merge,template-mail-merge,schedule-mail-merge,emailing-speakers-mail-merge</td>
</tr>

<tr>
<td class="org-left">bbb</td>
<td class="org-left">h-bbb</td>
<td class="org-left">t-bbb</td>
</tr>

<tr>
<td class="org-left">checkin</td>
<td class="org-left">h-mail-merge;h-checkin</td>
<td class="org-left">t-checkin,bbb-checkin</td>
</tr>

<tr>
<td class="org-left">redirect</td>
<td class="org-left">h-bbb;h-redirect</td>
<td class="org-left">t-redirect,bbb-redirect</td>
</tr>

<tr>
<td class="org-left">shortcuts</td>
<td class="org-left">h-email;h-shortcuts</td>
<td class="org-left">t-shortcuts,email-shortcuts</td>
</tr>

<tr>
<td class="org-left">logbook</td>
<td class="org-left">h-shortcuts;h-logbook</td>
<td class="org-left">shortcuts-logbook,t-logbook</td>
</tr>

<tr>
<td class="org-left">captions</td>
<td class="org-left">h-captions</td>
<td class="org-left">t-captions,captions-wiki</td>
</tr>

<tr>
<td class="org-left">tramp</td>
<td class="org-left">h-captions;h-tramp</td>
<td class="org-left">t-tramp,captions-tramp</td>
</tr>

<tr>
<td class="org-left">crontab</td>
<td class="org-left">h-tramp;h-crontab</td>
<td class="org-left">tramp-crontab,bbb-crontab,t-crontab</td>
</tr>

<tr>
<td class="org-left">transitions</td>
<td class="org-left">h-crontab;h-transitions</td>
<td class="org-left">shell-scripts-transitions,t-transitions,shortcuts-transitions,transitions-crontab</td>
</tr>

<tr>
<td class="org-left">irc</td>
<td class="org-left">h-transitions;h-irc</td>
<td class="org-left">t-irc,transitions-irc</td>
</tr>
</tbody>
</table>

</div>

<p>
Reveal.js adds a "current" class to the slide, so I can use that as a
trigger for the transition. I have a bit of Emacs Lisp code that
generates some very messy CSS, in which I specify the ID of the slide,
followed by all of the elements that need their opacity set to 1, and
also specifying the highlights that will be shown in an animated way.
</p>

<p>
</p><details><summary>my-reveal-svg-progression-css: Make the CSS.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-reveal-svg-progression-css</span> (map-progression <span class="org-type">&amp;optional</span> highlight-duration)
  <span class="org-doc">"Make the CSS.</span>
<span class="org-doc">map-progression should be a list of lists with the following format:</span>
<span class="org-doc">((\"slide-id\" \"prev1,prev2;cur1\" \"id-to-add1,id-to-add2\") ...)."</span>
  (<span class="org-keyword">setq</span> highlight-duration (<span class="org-keyword">or</span> highlight-duration 2))
  (<span class="org-keyword">let</span> (full)
    (format
     <span class="org-string">"&lt;style&gt;%s&lt;/style&gt;"</span>
     (mapconcat
      (<span class="org-keyword">lambda</span> (slide)
        (<span class="org-keyword">setq</span> full (append (split-string (elt slide 2) <span class="org-string">","</span>) full))
        (format <span class="org-string">"#slide-%s.present path { opacity: 0.2 }</span>
<span class="org-string">%s { opacity: 1 !important }</span>
<span class="org-string">%s"</span>
                (car slide)
                (mapconcat (<span class="org-keyword">lambda</span> (id) (format <span class="org-string">"#slide-%s.present #%s"</span> (car slide) id))
                           full
                           <span class="org-string">", "</span>)
                (my-reveal-svg-highlight-different-colors slide)))
      map-progression
      <span class="org-string">"\n"</span>))))
</pre></div></details><div><a href="https://sachachua.com/dotemacs#reveal-js-sketch-animation">Context</a></div>
I call it from my Org file like this:
<p></p>


<div class="org-src-container">
<pre class="src src-org"><span class="org-org-meta-line">#+NAME: progression-css</span>
<span class="org-org-block-begin-line">#+begin_src emacs-lisp :exports code :var map-progression=progression :var highlight-duration=2 :results silent</span>
<span class="org-org-block">(my-reveal-svg-progression-css map-progression highlight-duration)</span>
<span class="org-org-block-end-line">#+end_src</span>
</pre>
</div>


<p>
Here's an excerpt showing the kind of code it makes:
</p>

<pre class="example" id="orgd2b216e">
&lt;style&gt;#slide-props-map.present path { opacity: 0.2 }
#slide-props-map.present #t-email, #slide-props-map.present #email-properties, #slide-props-map.present #t-properties { opacity: 1 !important }
#slide-props-map.present #h-email { fill: #c6c6c6; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.0s }#slide-props-map.present #h-properties { fill: #f6f396; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.5s }
#slide-file-prefixes.present path { opacity: 0.2 }
#slide-file-prefixes.present #t-filename, #slide-file-prefixes.present #properties-filename, #slide-file-prefixes.present #t-email, #slide-file-prefixes.present #email-properties, #slide-file-prefixes.present #t-properties { opacity: 1 !important }
#slide-file-prefixes.present #h-properties { fill: #c6c6c6; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.0s }#slide-file-prefixes.present #h-filename { fill: #f6f396; opacity: 1 !important; transition: fill 0.5s; transition-delay: 0.5s }
...&lt;/style&gt;
</pre>

<p>
Since it's automatically generated, I don't have to worry about it
once I've gotten it to work. It's all hidden in a
results drawer. So this CSS highlights specific parts of the SVG with
a transition, and the highlight changes over the course of a second or
two. It highlights the previous names and then the current one. The
topics I'd already discussed would be in black, and the topics that I
had yet to discuss would be in very light gray. This could give people
a sense of the progress through the presentation.
</p>

<p>
</p><details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Code for making the CSS</strong></summary>

<div class="org-src-container">
<pre class="src src-emacs-lisp" id="org1ab8883">(<span class="org-keyword">defun</span> <span class="org-function-name">my-reveal-svg-animation</span> (slide)
  (string-join
   (seq-map-indexed
    (<span class="org-keyword">lambda</span> (step-ids i)
      (format <span class="org-string">"%s { fill: #f6f396; transition: fill %ds; transition-delay: %ds }"</span>
              (mapconcat
               (<span class="org-keyword">lambda</span> (id) (format <span class="org-string">"#slide-%s.present #%s"</span> (car slide) id))
               (split-string step-ids <span class="org-string">","</span>)
               <span class="org-string">", "</span>)
              highlight-duration
              (* i highlight-duration)))
    (split-string (elt slide 1) <span class="org-string">";"</span>))
   <span class="org-string">"\n"</span>))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-reveal-svg-highlight-different-colors</span> (slide)
  (<span class="org-keyword">let*</span> ((colors <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"#f6f396"</span> <span class="org-string">"#c6c6c6"</span>)) <span class="org-comment-delimiter">; </span><span class="org-comment">reverse</span>
         (steps (split-string (elt slide 1) <span class="org-string">";"</span>))
         (step-length 0.5))
    (string-join
     (seq-map-indexed
      (<span class="org-keyword">lambda</span> (step-ids i)
        (format <span class="org-string">"%s { fill: %s; opacity: 1 !important; transition: fill %.1fs; transition-delay: %.1fs }"</span>
                (mapconcat
                 (<span class="org-keyword">lambda</span> (id) (format <span class="org-string">"#slide-%s.present #%s"</span> (car slide) id))
                 (split-string step-ids <span class="org-string">","</span>)
                 <span class="org-string">", "</span>)
                (elt colors (- (length steps) i 1))
                step-length
                (* i 0.5)))
      steps))))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-reveal-svg-progression-css</span> (map-progression <span class="org-type">&amp;optional</span> highlight-duration)
  <span class="org-doc">"Make the CSS.</span>
<span class="org-doc">map-progression should be a list of lists with the following format:</span>
<span class="org-doc">((\"slide-id\" \"prev1,prev2;cur1\" \"id-to-add1,id-to-add2\") ...)."</span>
  (<span class="org-keyword">setq</span> highlight-duration (<span class="org-keyword">or</span> highlight-duration 2))
  (<span class="org-keyword">let</span> (full)
    (format
     <span class="org-string">"&lt;style&gt;%s&lt;/style&gt;"</span>
     (mapconcat
      (<span class="org-keyword">lambda</span> (slide)
        (<span class="org-keyword">setq</span> full (append (split-string (elt slide 2) <span class="org-string">","</span>) full))
        (format <span class="org-string">"#slide-%s.present path { opacity: 0.2 }</span>
<span class="org-string">%s { opacity: 1 !important }</span>
<span class="org-string">%s"</span>
                (car slide)
                (mapconcat (<span class="org-keyword">lambda</span> (id) (format <span class="org-string">"#slide-%s.present #%s"</span> (car slide) id))
                           full
                           <span class="org-string">", "</span>)
                (my-reveal-svg-highlight-different-colors slide)))
      map-progression
      <span class="org-string">"\n"</span>))))
</pre>
</div>




</details>

<p></p>

<p>
As a result, as I go through my presentation, the image appears to
build up incrementally, which is the effect that I was going for.
I can test this by exporting only my map slides:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">save-excursion</span>
  (goto-char (org-babel-find-named-block <span class="org-string">"progression-css"</span>))
  (org-babel-execute-src-block))
(<span class="org-keyword">let</span> ((org-tags-exclude-from-inheritance <span class="org-string">"map"</span>)
      (org-export-select-tags <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"map"</span>)))
   (oer-reveal-export-to-html))
</pre>
</div>


<p>
<video controls="1"><source src="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_11.57.51.webm" type="video/webm"><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/2024-01-11_11.57.51.webm">Download the video</a></video>
</p>
</div>
</div>
<div id="outline-container-svg-animation-next" class="outline-2">
<h3 id="svg-animation-next">Ideas for next steps</h3>
<div class="outline-text-2" id="text-svg-animation-next">
<ul class="org-ul">
<li>Graphviz, mermaid-js, and other diagramming tools can make SVGs. I
should be able to adapt my code to animate those diagrams by adding
other elements in addition to <code>path</code>. Then I'll be able to make
diagrams even more easily.</li>

<li>Since SVGs can contain CSS, I could make an SVG equivalent of the
CSS rules I used for the presentation, maybe calling a function with
a Lisp expression that specifies the operations (ex:
<code>("frame-001.svg" "h-foo" opacity 1)</code>). Then I could write frames to
SVGs.</li>

<li>FFmpeg has a crossfade filter. With a little bit of figuring out, I
should be able to make the same kind of animation in a webm form
that I can include in my regular videos instead of using Reveal.js
and CSS transitions.</li>

<li>I've also been thinking about automating the recording of my
Reveal.js presentations. For my EmacsConf talk, I opened my
presentation, started the recording with the system audio and the
screen, and then let it autoplay the presentation. I checked on it
periodically to avoid the screensaver/energy saving things from
kicking in and so that I could stop the recording when it's
finished. If I want to make this take less work, one option is to
use ffmpeg's "-t" argument to specify the expected duration of the
presentation so that I don't have to manually stop it. I'm also
thinking about using Puppeteer to open the presentation, check when
it's fully loaded, and start the process to record it - maybe even
polling to see whether it's finished. I haven't gotten around to it
yet. Anyhow, those are some ideas to explore next time.</li>

<li>As for animation, I'm still curious about the possibility of
finding a way to access the raw stroke information if it's even
available from my Supernote A5X (difficult because it's a
proprietary data format) or finding an app for the iPad that exports
single line SVGs that use stroke information instead of fill. That
would only be if I wanted to do those even fancier animations that
look like the whole thing is being drawn for you. I was trying to
figure out if I could green screen the Adobe Fresco timelapse videos
so that even if I have a pre-sketch to figure out spacing and remind
me what to draw, I can just export the finished elements. But
there's too much anti-aliasing and I haven't figured out how to do
it cleanly yet. Maybe some other day.</li>

<li>I use Google Cloud Vision's text detection engine to convert my
handwriting to text. It can give me bounding polygons for words or
paragraphs. I might be able to figure out which curves are entirely
within a word's bounding polygon and combine those automatically.</li>

<li>It would be pretty cool if I could combine the words recognized by
Google Cloud Vision with the word-level timestamps from speech
recognition so that I could get word-synced sketchnote animations
with maybe a little manual intervention.</li>
</ul>

<p>
Anyway, those are some workflows for animating sketches with Inkscape
and Emacs. Yay Emacs!
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/01/animating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js/index.org">View org source for this post</a></div>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F01%2Fanimating-svg-topic-maps-with-inkscape-emacs-ffmpeg-and-reveal-js%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Quick notes on livestreaming to YouTube with FFmpeg on a Lenovo X230T</title>
		<link>https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 04 Jan 2024 15:57:25 GMT</pubDate>
    <category>video</category>
<category>youtube</category>
<category>streaming</category>
<category>ffmpeg</category>
<category>yay-emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/</guid>
		<description><![CDATA[<div class="update" id="org78e2751">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2024-01-05 Fri]</span></span>: Updated scripts
</p>

</div>

<p>
</p><div class="sketch-full"><a class="photoswipe" href="https://sketches.sachachua.com/filename/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" data-src="https://sketches.sachachua.com/static/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" data-title="2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png" data-w="2808" data-h="3744"><picture>
      <img src="https://sketches.sachachua.com/static/2024-01-03-03%20Quick%20thoughts%20on%20livestreaming%20%23sharing%20%23video%20%23streaming%20%23community.png" width="2808" height="3744" alt="2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png" loading="lazy" style="max-height: 90vw; height: auto; width: auto" decoding="async">
      <figcaption>2024-01-03-03 Quick thoughts on livestreaming #sharing #video #streaming #community.png</figcaption>
    </picture></a></div>
<p></p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Text from the sketch</strong></summary>
<p>
Quick thoughts on livestreaming
</p>

<p>
Why:
</p>

<ul class="org-ul">
<li>work out loud</li>
<li>share tips</li>
<li>share more</li>
<li>spark conversations</li>
<li>(also get questions about things)</li>
</ul>

<p>
Doable with ffmpeg on my X230T:
</p>

<ul class="org-ul">
<li>streaming from my laptop</li>
<li>lapel mic + system audio,</li>
<li>second screen for monitoring</li>
</ul>

<p>
Ideas for next time:
</p>

<ul class="org-ul">
<li>Overall notes in Emacs with outline, org-timer timestamped notes; capture to this file</li>
<li>Elisp to start/stop the stream → find old code</li>
<li>Use the Yeti? Better sound</li>
<li>tee to a local recording</li>
<li>grab screenshot from SuperNote mirror?</li>
</ul>

<p>
Live streaming info density:
</p>

<ul class="org-ul">
<li>High: Emacs News review, package/workflow demo</li>
<li>Narrating a blog post to make it a video</li>
<li>Categorizing Emacs News, exploring packages</li>
<li>Low: Figuring things out</li>
</ul>

<p>
YouTube can do closed captions for livestreams, although accuracy is
low. Videos take a while to be ready to download.
</p>


</details>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud">Experimenting with working out loud</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-experimenting-with-working-out-loud">
<p>
I wanted to write <a href="https://emacsconf.org/2023/report">a report on EmacsConf 2023</a> so that we could share it
with speakers, volunteers, participants, donors, related organizations
like the Free Software Foundation, and other communities. I
experimented with livestreaming via YouTube while I worked on the
conference highlights.
</p>

<p>
It's a little over an hour long and probably very boring, but it was
nice of people to drop by and say hello.
</p>

<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/KXKA-3JFb14?si=2V5XQmQSUte0ZWyF" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>

<p>
The main parts are:
</p>

<ul class="org-ul">
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h0m00s">0:00</a>: reading through other conference reports for inspiration</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h6m54s">6:54</a>: writing an overview of the talks</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h13m10s">13:10</a>: adding quotes for specific talks</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h25m00s">25:00</a>: writing about the overall conference</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h32m00s">32:00</a>: squeezing in more highlights</li>
<li><a href="https://www.youtube.com/watch?v=KXKA-3JFb14&t=0h49m00s">49:00</a>: fiddling with the formatting and the export</li>
</ul>

<p>
It mostly worked out, aside from a brief moment of "uhhh, I'm
looking at our private conf.org file on stream". Fortunately, the
e-mail addresses that were showed were the public ones.
</p>
</div>
</div>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details">Technical details</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-technical-details">
<p>
Setup:
</p>

<ul class="org-ul">
<li><p>
I set up environment variables and screen resolution:
</p>

<div class="org-src-container">
<pre class="src src-sh" id="org8d1d9a1">  <span class="org-comment-delimiter"># </span><span class="org-comment">From pacmd list-sources | egrep '^\s+name'</span>
  <span class="org-variable-name">LAPEL</span>=alsa_input.usb-Jieli_Technology_USB_Composite_Device_433035383239312E-00.mono-fallback <span class="org-comment-delimiter">#</span>
  <span class="org-variable-name">YETI</span>=alsa_input.usb-Blue_Microphones_Yeti_Stereo_Microphone_REV8-00.analog-stereo
  <span class="org-variable-name">SYSTEM</span>=alsa_output.pci-0000_00_1b.0.analog-stereo.monitor
  <span class="org-comment-delimiter"># </span><span class="org-comment">MIC=$LAPEL</span>
  <span class="org-comment-delimiter"># </span><span class="org-comment">AUDIO_WEIGHTS="1 1"</span>
  <span class="org-variable-name">MIC</span>=$<span class="org-variable-name">YETI</span>
  <span class="org-variable-name">AUDIO_WEIGHTS</span>=<span class="org-string">"0.5 0.5"</span>
  <span class="org-variable-name">OFFSET</span>=+1920,430
  <span class="org-variable-name">SIZE</span>=1280x720
  <span class="org-variable-name">SCREEN</span>=LVDS-1  <span class="org-comment-delimiter"># </span><span class="org-comment">from xrandr</span>
  xrandr &#45;&#45;output $<span class="org-variable-name">SCREEN</span> &#45;&#45;mode 1280x720
</pre>
</div>
</li>
<li>I switch to a larger size and a light theme. I also turn <a href="https://github.com/minad/consult">consult</a> previews off to minimize the risk of leaking data through buffer previews.
<details open=""><summary>my-emacsconf-prepare-for-screenshots: Set the resolution, change to a light theme, and make the text bigger.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-emacsconf-prepare-for-screenshots</span> ()
  (<span class="org-keyword">interactive</span>)
  (shell-command <span class="org-string">"xrandr &#45;&#45;output LVDS-1 &#45;&#45;mode 1280x720"</span>)
  (modus-themes-load-theme <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">modus-operandi</span>)
  (my-hl-sexp-update-overlay)
  (set-face-attribute <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">default</span> nil <span class="org-builtin">:height</span> 170)
  (keycast-mode))
</pre></div></details></li>
</ul>

<p>
Testing:
</p>


<div class="org-src-container">
<pre class="src src-sh">ffmpeg -f x11grab -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -y /tmp/test.png; display /tmp/test.png
ffmpeg -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-variable-name">amix</span>=<span class="org-variable-name">inputs</span>=2:<span class="org-variable-name">weights</span>=$<span class="org-variable-name">AUDIO_WEIGHTS</span>:<span class="org-variable-name">duration</span>=longest:<span class="org-variable-name">normalize</span>=0 -y /tmp/test.mp3; mpv /tmp/test.mp3
<span class="org-variable-name">DATE</span>=$(date <span class="org-string">"+%Y-%m-%d-%H-%M-%S"</span>)
ffmpeg -f x11grab -framerate 30 -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-string">"amix=inputs=2:weights=$AUDIO_WEIGHTS:duration=longest:normalize=0"</span> -c:v libx264 -preset fast -maxrate 690k -bufsize 2000k -g 60 -vf <span class="org-variable-name">format</span>=yuv420p -c:a aac -b:a 96k -y -flags +global_header <span class="org-string">"/home/sacha/recordings/$DATE.flv"</span> -f flv
</pre>
</div>


<p>
Streaming:
</p>


<div class="org-src-container">
<pre class="src src-sh"><span class="org-variable-name">DATE</span>=$(date <span class="org-string">"+%Y-%m-%d-%H-%M-%S"</span>)
ffmpeg -f x11grab -framerate 30 -video_size $<span class="org-variable-name">SIZE</span> -i :0.0$<span class="org-variable-name">OFFSET</span> -f pulse -i $<span class="org-variable-name">MIC</span> -f pulse -i $<span class="org-variable-name">SYSTEM</span> -filter_complex <span class="org-string">"amix=inputs=2:weights=$AUDIO_WEIGHTS:duration=longest:normalize=0[audio]"</span> -c:v libx264 -preset fast -maxrate 690k -bufsize 2000k -g 60 -vf <span class="org-variable-name">format</span>=yuv420p -c:a aac -b:a 96k -y -f tee -map 0:v -map <span class="org-string">'[audio]'</span> -flags +global_header  <span class="org-string">"/home/sacha/recordings/$DATE.flv|[f=flv]rtmp://a.rtmp.youtube.com/live2/$YOUTUBE_KEY"</span>
</pre>
</div>


<p>
To restore my previous setup:
</p>

<p>
</p><details open=""><summary>my-emacsconf-back-to-normal: Go back to a more regular setup.</summary><div class="org-src-container"><pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-emacsconf-back-to-normal</span> ()
  (<span class="org-keyword">interactive</span>)
  (shell-command <span class="org-string">"xrandr &#45;&#45;output LVDS-1 &#45;&#45;mode 1366x768"</span>)
  (modus-themes-load-theme <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">modus-vivendi</span>)
  (my-hl-sexp-update-overlay)
  (set-face-attribute <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">default</span> nil <span class="org-builtin">:height</span> 115)
  (keycast-mode -1))
</pre></div></details>
<p></p>
</div>
</div>
<div id="outline-container-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps" class="outline-2">
<h3 id="quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps">Ideas for next steps</h3>
<div class="outline-text-2" id="text-quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t-ideas-for-next-steps">
<p>
I can think of a few workflow tweaks that might be fun:
</p>

<ul class="org-ul">
<li>a stream notes buffer on the right side of the screen for context
information, timestamped notes to make editing/review easier (maybe
using <code>org-timer</code>), etc. I experimented with some streaming-related
code in my config, so I can dust that off and see what that's like.
I also want to have an <code>org-capture</code> template for it so that I can add
notes from anywhere.</li>
<li>a quick way to <a href="https://sachachua.com/blog/2024/01/using-puppeteer-to-grab-an-image-from-the-supernote-s-screen-mirror/">add a screenshot from my Supernote</a> to my Org files</li>
</ul>

<p>
I think I'll try going through an informal presentation or Emacs News as my next livestream experiment, since that's probably higher information density.
</p>
</div>
</div>
<div><a href="https://sachachua.com/blog/2024/01/quick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t/index.org">View org source for this post</a></div>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2024%2F01%2Fquick-notes-on-livestreaming-to-youtube-with-ffmpeg-on-a-lenovo-x230t%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Figuring out how to use ffmpeg to mask a chroma-keyed video based on the differences between images</title>
		<link>https://sachachua.com/blog/2022/12/figuring-out-how-to-use-ffmpeg-to-mask-a-chroma-keyed-video/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 25 Dec 2022 09:06:30 GMT</pubDate>
    <category>linux</category>
<category>geek</category>
<category>ffmpeg</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2022/12/figuring-out-how-to-use-ffmpeg-to-mask-a-chroma-keyed-video/</guid>
		<description><![CDATA[<p>
A- is really into Santa and Christmas because of the books she's read.
Last year, she wanted to set up the GoPro to capture footage during
Christmas Eve. I helped her set it up for a timelapse video. After she
went to bed, we gradually positioned the presents. I extracted the
frames from the video, removed the ones that caught us moving around,
and then used <a href="https://krita.org/en/">Krita</a>'s new animation features to animate sparkles so
that the presents magically appeared. She mentioned the sparkles a
number of times during her deliberations about whether Santa exists or
not.
</p>

<p>
This year, I want to see if I can use green-screen videos like <a href="https://www.youtube.com/watch?v=xgVtyx9XJhI">this
reversed-spin sparkle</a> or this other <a href="https://youtube.com/watch?v=uyxpPKZQSzU">sparkle video</a>. I'm going to take
a series of images, with each image adding one more gift. Then I'm
going to make a mask in Krita with white covering the gift and a
transparent background for the rest of the image. Then I'll use
chroma-key to drop out the green screen of the sparkle video and mask
it in so that the sparkles only happen within the boundaries of the
gift that was added. I also want to fade one image into the other, and
I want the sparkles to fade out as the gift appears.
</p>

<div id="outline-container-org0b393d7" class="outline-2">
<h3 id="org0b393d7">Figuring things out</h3>
<div class="outline-text-2" id="text-org0b393d7">
<p>
I didn't know how to do any of that yet with ffmpeg, so here's how I
started figuring things out. First, I wanted to see how to <a href="https://ffmpeg.org/ffmpeg-filters.html#fade">fade</a>
test.jpg into test2.jpg over 4 seconds.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -loop 1 -i test.jpg -loop 1 -i test2.jpg -filter_complex <span class="org-string">"[1:v]fade=t=in:d=4:alpha=1[fadein];[0:v][fadein]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 -shortest test.webm
</pre>
</div>

<p>
Here's another way using the <a href="https://ffmpeg.org/ffmpeg-filters.html#blend">blend</a> filter:
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -loop 1 -i test.jpg -loop 1 -i test2.jpg -filter_complex <span class="org-string">"[1:v][0:v]blend=all_expr='A*(if(gte(T,4),1,T/4))+B*(1-(if(gte(T,4),1,T/4)))"</span> -t 4 -r 1 test.webm
</pre>
</div>

<p>
Then I looked into <a href="https://ffmpeg.org/ffmpeg-filters.html#chromakey">chromakey</a>ing in the other video. I used balloons
instead of sparkles just in case she happened to look at my screen.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -i test.webm -i balloons.mp4 -filter_complex <span class="org-string">"[1:v]chromakey=0x00ff00:0.1:0.2[ckout];[0:v][ckout]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -shortest -r 1 overlaid.webm
</pre>
</div>

<p>
I experimented with the <a href="https://ffmpeg.org/ffmpeg-filters.html#alphamerge">alphamerge</a> filter.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -i test.jpg -i test2.jpg -i mask.png -filter_complex <span class="org-string">"[1:v][2:v]alphamerge[a];[0:v][a]overlay[out]"</span> -map <span class="org-string">"[out]"</span> masked.jpg
</pre>
</div>

<p>
Okay! That overlaid test.jpg with a masked part of test2.jpg. How about alphamerging in a video? First, I need a mask video&#x2026;
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -y -loop 1 -i mask.png  -r 1 -t 4  mask.webm
</pre>
</div>

<p>
Then I can combine that:
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i mask.webm -filter_complex <span class="org-string">"[1:v][2:v]alphamerge[masked];[0:v][masked]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 alphamerged.webm
</pre>
</div>

<p>
Great, let's figure out how to combine chroma-key and alphamerge video. The naive approach doesn't work, probably because they're both messing with the alpha layer.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i mask.webm -filter_complex <span class="org-string">"[1:v]chromakey=0x00ff00:0.1:0.2[ckout];[ckout][2:v]alphamerge[masked];[0:v][masked]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 masked.webm
</pre>
</div>

<p>
So I probably need to blend the chromakey and the mask. Let's see if I can extract the chromakey alpha.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i mask.webm -filter_complex <span class="org-string">"[1:v]chromakey=0x00ff00:0.1:0.2,format=rgba,alphaextract[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4
chroma-alpha.webm
</pre>
</div>

<p>
Now let's blend it with the mask.webm.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i mask.webm -filter_complex <span class="org-string">"[1:v]chromakey=0x00ff00:0.1:0.2,format=rgba,alphaextract[ckalpha];[ckalpha][2:v]blend=all_mode=and[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 masked-alpha.webm
</pre>
</div>

<p>
Then let's use it as the alpha:
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i test.webm -i balloons.mp4 -i masked-alpha.webm -filter_complex <span class="org-string">"[2:v]format=rgba[mask];[1:v][mask]alphamerge[masked];[0:v][masked]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 1 -t 4 alphamerged.webm
</pre>
</div>

<p>
Okay, that worked! Now how do I combine everything into one command? Hmm&#x2026;
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -loop 1 -i test.jpg -t 4 -loop 1 -i test2.jpg -t 4 -i balloons.mp4 -loop 1 -i mask.png -t 4 -filter_complex <span class="org-string">"[1:v][0:v]blend=all_expr='A*(if(gte(T,4),1,T/4))+B*(1-(if(gte(T,4),1,T/4)))'[fade];[2:v]chromakey=0x00ff00:0.1:0.2,format=rgba,alphaextract[ckalpha];[ckalpha][3:v]blend=all_mode=and,format=rgba[maskedalpha];[2:v][maskedalpha]alphamerge[masked];[fade][masked]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 5 -t 4 alphamerged.webm
</pre>
</div>

<p>
Then I wanted to fade the masked video out by the end.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -loop 1 -i test.jpg -t 4 -loop 1 -i test2.jpg -t 4 -i balloons.mp4 -loop 1 -i mask.png -t 4 -filter_complex <span class="org-string">"[1:v][0:v]blend=all_expr='A*(if(gte(T,4),1,T/4))+B*(1-(if(gte(T,4),1,T/4)))'[fade];[2:v]chromakey=0x00ff00:0.1:0.2,format=rgba,alphaextract[ckalpha];[ckalpha][3:v]blend=all_mode=and,format=rgba[maskedalpha];[2:v][maskedalpha]alphamerge[masked];[masked]fade=type=out:st=2:d=1:alpha=1[maskedfade];[fade][maskedfade]overlay[out]"</span> -map <span class="org-string">"[out]"</span> -r 10 -t 4 alphamerged.webm
</pre>
</div>
</div>
</div>
<div id="outline-container-org0e23e45" class="outline-2">
<h3 id="org0e23e45">Making the video</h3>
<div class="outline-text-2" id="text-org0e23e45">
<p>
When A- finally went to bed, we arranged the presents, using the GoPro to take a picture at each step of the way. I cropped and resized the images, using Krita to figure out the cropping rectangle and offset.
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="org-keyword">for</span> FILE<span class="org-keyword"> in</span> *.JPG; <span class="org-keyword">do</span> convert $<span class="org-variable-name">FILE</span> -crop 1558x876+473+842 -resize 1280x720 cropped/$<span class="org-variable-name">FILE</span>; <span class="org-keyword">done</span>
</pre>
</div>

<p>
I used ImageMagick to calculate the masks automatically.
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="org-variable-name">files</span>=(*.JPG)
<span class="org-variable-name">i</span>=0
<span class="org-variable-name">j</span>=1
<span class="org-variable-name">len</span>=<span class="org-string">"${#files[@]}"</span>
<span class="org-keyword">while</span> [ <span class="org-string">"$j"</span> -lt $<span class="org-variable-name">len</span> ]; <span class="org-keyword">do</span>
  compare -fuzz 15% cropped/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">i</span>]} cropped/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">j</span>]} -compose Src -highlight-color White -lowlight-color Black masks/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">j</span>]}
  convert -morphology Open Disk -morphology Close Disk -blur 20x5 masks/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">j</span>]} processed-masks/${<span class="org-variable-name">files</span>[$<span class="org-variable-name">j</span>]}
  <span class="org-variable-name">i</span>=$((i+1))
  <span class="org-variable-name">j</span>=$((j+1))
<span class="org-keyword">done</span>
</pre>
</div>

<p>
Then I faded the images together to make a video.
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> ffmpeg
<span class="org-keyword">import</span> glob
<span class="org-variable-name">files</span> = glob.glob(<span class="org-string">"images/cropped/*.JPG"</span>)
files.sort()
<span class="org-variable-name">fps</span> = 15
<span class="org-variable-name">crf</span> = 32
<span class="org-variable-name">out</span> = ffmpeg.<span class="org-builtin">input</span>(files[0], loop=1, r=fps)
<span class="org-variable-name">duration</span> = 3
<span class="org-keyword">for</span> i <span class="org-keyword">in</span> <span class="org-builtin">range</span>(1, <span class="org-builtin">len</span>(files)):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">out</span> = ffmpeg.<span class="org-builtin">filter</span>([out, ffmpeg.<span class="org-builtin">input</span>(files[i], loop=1, r=fps).<span class="org-builtin">filter</span>(<span class="org-string">'fade'</span>, t=<span class="org-string">'in'</span>, d=duration, st=i*duration, alpha=1)], <span class="org-string">'overlay'</span>)
<span class="org-variable-name">args</span> = out.output(<span class="org-string">'images.webm'</span>, t=<span class="org-builtin">len</span>(files) * duration, r=fps, y=<span class="org-constant">None</span>, crf=crf).<span class="org-builtin">compile</span>()
<span class="org-builtin">print</span>(<span class="org-string">' '</span>.join(f<span class="org-string">'"</span>{item}<span class="org-string">"'</span> <span class="org-keyword">for</span> item <span class="org-keyword">in</span> args))
</pre>
</div>

<p>
"ffmpeg" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2317.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2318.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2319.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2320.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2321.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2322.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2323.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2324.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2325.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2326.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2327.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2328.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2329.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2330.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2331.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2332.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2333.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2334.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2335.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2336.JPG" "-loop" "1" "-r" "15" "-i" "images/cropped/GOPR2337.JPG" "-filter_complex" "[1]fade=alpha=1:d=3:st=3:t=in[s0];[0][s0]overlay[s1];[2]fade=alpha=1:d=3:st=6:t=in[s2];[s1][s2]overlay[s3];[3]fade=alpha=1:d=3:st=9:t=in[s4];[s3][s4]overlay[s5];[4]fade=alpha=1:d=3:st=12:t=in[s6];[s5][s6]overlay[s7];[5]fade=alpha=1:d=3:st=15:t=in[s8];[s7][s8]overlay[s9];[6]fade=alpha=1:d=3:st=18:t=in[s10];[s9][s10]overlay[s11];[7]fade=alpha=1:d=3:st=21:t=in[s12];[s11][s12]overlay[s13];[8]fade=alpha=1:d=3:st=24:t=in[s14];[s13][s14]overlay[s15];[9]fade=alpha=1:d=3:st=27:t=in[s16];[s15][s16]overlay[s17];[10]fade=alpha=1:d=3:st=30:t=in[s18];[s17][s18]overlay[s19];[11]fade=alpha=1:d=3:st=33:t=in[s20];[s19][s20]overlay[s21];[12]fade=alpha=1:d=3:st=36:t=in[s22];[s21][s22]overlay[s23];[13]fade=alpha=1:d=3:st=39:t=in[s24];[s23][s24]overlay[s25];[14]fade=alpha=1:d=3:st=42:t=in[s26];[s25][s26]overlay[s27];[15]fade=alpha=1:d=3:st=45:t=in[s28];[s27][s28]overlay[s29];[16]fade=alpha=1:d=3:st=48:t=in[s30];[s29][s30]overlay[s31];[17]fade=alpha=1:d=3:st=51:t=in[s32];[s31][s32]overlay[s33];[18]fade=alpha=1:d=3:st=54:t=in[s34];[s33][s34]overlay[s35];[19]fade=alpha=1:d=3:st=57:t=in[s36];[s35][s36]overlay[s37];[20]fade=alpha=1:d=3:st=60:t=in[s38];[s37][s38]overlay[s39]" "-map" "[s39]" "-crf" "32" "-r" "15" "-t" "63" "-y" "images.webm"
</p>

<p>
Next, I faded the masks together. These ones faded in and out so that only one mask was active at a time.
</p>

<div class="org-src-container">
<pre class="src src-python"><span class="org-keyword">import</span> ffmpeg
<span class="org-keyword">import</span> glob
<span class="org-variable-name">files</span> = glob.glob(<span class="org-string">"images/processed-masks/*.JPG"</span>)
files.sort()
<span class="org-variable-name">files</span> = files[:-2]  <span class="org-comment-delimiter"># </span><span class="org-comment">Omit the last two, where I'm just turning off the lights</span>
<span class="org-variable-name">fps</span> = 15
<span class="org-variable-name">crf</span> = 32
<span class="org-variable-name">out</span> = ffmpeg.<span class="org-builtin">input</span>(<span class="org-string">'color=black:s=1280x720'</span>, f=<span class="org-string">'lavfi'</span>, r=fps)
<span class="org-variable-name">duration</span> = 3
<span class="org-keyword">for</span> i <span class="org-keyword">in</span> <span class="org-builtin">range</span>(0, <span class="org-builtin">len</span>(files)):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">out</span> = ffmpeg.<span class="org-builtin">filter</span>([out, ffmpeg.<span class="org-builtin">input</span>(files[i], loop=1, r=fps).<span class="org-builtin">filter</span>(<span class="org-string">'fade'</span>, t=<span class="org-string">'in'</span>, d=1, st=(i + 1)*duration, alpha=1).<span class="org-builtin">filter</span>(<span class="org-string">'fade'</span>, t=<span class="org-string">'out'</span>, st=(i + 2)*duration - 1)], <span class="org-string">'overlay'</span>)
<span class="org-variable-name">args</span> = out.output(<span class="org-string">'processed-masks.webm'</span>, t=<span class="org-builtin">len</span>(files) * duration, r=fps, y=<span class="org-constant">None</span>, crf=crf).<span class="org-builtin">compile</span>()
<span class="org-builtin">print</span>(<span class="org-string">' '</span>.join(f<span class="org-string">'"</span>{item}<span class="org-string">"'</span> <span class="org-keyword">for</span> item <span class="org-keyword">in</span> args))
</pre>
</div>

<p>
"ffmpeg" "-f" "lavfi" "-r" "15" "-i" "color=<span style="color:black;">s=1280x720</span>" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2318.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2319.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2320.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2321.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2322.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2323.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2324.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2325.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2326.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2327.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2328.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2329.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2330.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2331.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2332.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2333.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2334.JPG" "-loop" "1" "-r" "15" "-i" "images/processed-masks/GOPR2335.JPG" "-filter_complex" "[1]fade=alpha=1:d=1:st=3:t=in[s0];[s0]fade=st=5:t=out[s1];[0][s1]overlay[s2];[2]fade=alpha=1:d=1:st=6:t=in[s3];[s3]fade=st=8:t=out[s4];[s2][s4]overlay[s5];[3]fade=alpha=1:d=1:st=9:t=in[s6];[s6]fade=st=11:t=out[s7];[s5][s7]overlay[s8];[4]fade=alpha=1:d=1:st=12:t=in[s9];[s9]fade=st=14:t=out[s10];[s8][s10]overlay[s11];[5]fade=alpha=1:d=1:st=15:t=in[s12];[s12]fade=st=17:t=out[s13];[s11][s13]overlay[s14];[6]fade=alpha=1:d=1:st=18:t=in[s15];[s15]fade=st=20:t=out[s16];[s14][s16]overlay[s17];[7]fade=alpha=1:d=1:st=21:t=in[s18];[s18]fade=st=23:t=out[s19];[s17][s19]overlay[s20];[8]fade=alpha=1:d=1:st=24:t=in[s21];[s21]fade=st=26:t=out[s22];[s20][s22]overlay[s23];[9]fade=alpha=1:d=1:st=27:t=in[s24];[s24]fade=st=29:t=out[s25];[s23][s25]overlay[s26];[10]fade=alpha=1:d=1:st=30:t=in[s27];[s27]fade=st=32:t=out[s28];[s26][s28]overlay[s29];[11]fade=alpha=1:d=1:st=33:t=in[s30];[s30]fade=st=35:t=out[s31];[s29][s31]overlay[s32];[12]fade=alpha=1:d=1:st=36:t=in[s33];[s33]fade=st=38:t=out[s34];[s32][s34]overlay[s35];[13]fade=alpha=1:d=1:st=39:t=in[s36];[s36]fade=st=41:t=out[s37];[s35][s37]overlay[s38];[14]fade=alpha=1:d=1:st=42:t=in[s39];[s39]fade=st=44:t=out[s40];[s38][s40]overlay[s41];[15]fade=alpha=1:d=1:st=45:t=in[s42];[s42]fade=st=47:t=out[s43];[s41][s43]overlay[s44];[16]fade=alpha=1:d=1:st=48:t=in[s45];[s45]fade=st=50:t=out[s46];[s44][s46]overlay[s47];[17]fade=alpha=1:d=1:st=51:t=in[s48];[s48]fade=st=53:t=out[s49];[s47][s49]overlay[s50];[18]fade=alpha=1:d=1:st=54:t=in[s51];[s51]fade=st=56:t=out[s52];[s50][s52]overlay[s53]" "-map" "[s53]" "-crf" "32" "-r" "15" "-t" "54" "-y" "processed-masks.webm"
</p>

<p>
I ended up using <a href="https://www.youtube.com/watch?v=cn1zf9nFUsI">this particle glitter video</a> because the gifts were small, so I wanted a video that was dense with sparkly things. I also wanted the sparkles to be more concentrated on the area where the gifts were, so I resized it and positioned it.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -f lavfi -i <span class="org-variable-name">color</span>=black:<span class="org-variable-name">s</span>=1280x720 -i sparkles4.webm -ss 13 -filter_complex <span class="org-string">"[1:v]scale=700:392[sparkles];[0:v][sparkles]overlay=x=582:y=194,setpts=(PTS-STARTPTS)*1.05[out]"</span> -map <span class="org-string">"[out]"</span> -r 15 -t 53 -shortest sparkles-trimmed.webm
ffmpeg -y -stream_loop 2 -i sparkles-trimmed.webm -t 57 sparkles-looped.webm              
</pre>
</div>

<p>
Lastly, I combined the videos with the sparkles.
</p>

<div class="org-src-container">
<pre class="src src-sh">ffmpeg -loglevel 32 -y -i images.webm -i sparkles-looped.webm -i processed-masks.webm -filter_complex <span class="org-string">"[1:v]chromakey=0x0a9d06:0.1:0.2,format=rgba,alphaextract[ckalpha];[ckalpha][2:v]blend=all_mode=and,format=rgba[maskedalpha];[1:v][maskedalpha]alphamerge[masked];[masked]fade=t=out:st=57:d=1:alpha=1[maskedfaded];[0:v][maskedfaded]overlay[combined];[combined]tpad=start_mode=clone:start_duration=4:stop_mode=clone:stop_duration=4[out]"</span> -map <span class="org-string">"[out]"</span> -r 15 -crf 32 output.webm
</pre>
</div>

<p>
After many iterations and a very late night, I got (roughly) the video I wanted, which I'm not posting here because of reasons. But it worked, yay! Now I don't have to manually place stars frame-by-frame in Krita, and I can just have all that magic happen semi-automatically.
</p>
</div>
</div>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2022%2F12%2Ffiguring-out-how-to-use-ffmpeg-to-mask-a-chroma-keyed-video%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Converting our VTT files to TTML</title>
		<link>https://sachachua.com/blog/2022/11/converting-our-vtt-files-to-ttml/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 17 Nov 2022 18:52:28 GMT</pubDate>
    <category>subed</category>
<category>emacsconf</category>
<category>geek</category>
<category>ffmpeg</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2022/11/converting-our-vtt-files-to-ttml/</guid>
		<description><![CDATA[<p>
I wanted to convert our VTT files to TTML files so that we might be
able to use them for training <a href="https://github.com/readbeyond/lachesis">lachesis</a> for transcript segmentation. I
downloaded the VTT files from EmacsConf 2021 to a directory and copied
the edited captions from the EmacsConf 2022 backstage area (using
<code>head -1 ${FILE} | grep -q "captioned"</code> to distinguish them from the
automatic ones). I installed the <a href="https://github.com/sandflow/ttconv">ttconv</a> python package. Then I used
the following command to convert the TTML files:
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="org-keyword">for</span> FILE<span class="org-keyword"> in</span> *.vtt; <span class="org-keyword">do</span>
    <span class="org-variable-name">BASE</span>=$(<span class="org-sh-quoted-exec">basename -s .vtt "$FILE"</span>);
    ffmpeg -y -i $<span class="org-variable-name">FILE</span> $<span class="org-variable-name">BASE</span>.srt; tt convert -i $<span class="org-variable-name">BASE</span>.srt -o $<span class="org-variable-name">BASE</span>.ttml
<span class="org-keyword">done</span>           
</pre>
</div>

<p>
I haven't gotten around to installing whanever I need in order to get
lachesis to work under Python 2.7, since it hasn't been updated for
Python 3. It'll probably be a low-priority project anyway, as
EmacsConf is fast approaching. Anyway, I thought I'd stash this in my
blog somewhere in case I need to make TTML files again!
</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2022%2F11%2Fconverting-our-vtt-files-to-ttml%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Re-encoding the EmacsConf videos with FFmpeg and GNU Parallel</title>
		<link>https://sachachua.com/blog/2021/12/re-encoding-the-emacsconf-videos-with-ffmpeg-and-gnu-parallel/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 23 Dec 2021 07:30:12 GMT</pubDate>
    <category>geek</category>
<category>linux</category>
<category>emacsconf</category>
<category>ffmpeg</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2021/12/re-encoding-the-emacsconf-videos-with-ffmpeg-and-gnu-parallel/</guid>
		<description><![CDATA[<p>
It turns out that using <code>-crf 56</code> compressed the EmacsConf a little
too aggressively, losing too much information in the video. We wanted
to reencode everything, maybe going back to the default value of <code>-crf
32</code>. My laptop would have taken a long time to do all of those videos.
Fortunately, one of the other volunteers shared a VM on a machine with
12 cores, and I had access to a few other systems. It was a good
opportunity to learn how to use <a href="https://www.gnu.org/software/parallel/">GNU Parallel</a> to send jobs to different
machines and retrieve the results.
</p>

<p>
First, I updated the compression script, <code>compress-video-low.sh</code>:
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="org-variable-name">Q</span>=$<span class="org-variable-name">1</span>
<span class="org-variable-name">WIDTH</span>=1280
<span class="org-variable-name">HEIGHT</span>=720
<span class="org-variable-name">AUDIO_RATE</span>=48000
<span class="org-variable-name">VIDEO_FILTER</span>=<span class="org-string">"scale=w=${WIDTH}:h=${HEIGHT}:force_original_aspect_ratio=1,pad=${WIDTH}:${HEIGHT}:(ow-iw)/2:(oh-ih)/2,fps=25,colorspace=all=bt709:iall=bt601-6-625:fast=1"</span>
<span class="org-variable-name">FILE</span>=$<span class="org-variable-name">2</span>
<span class="org-variable-name">SUFFIX</span>=$<span class="org-variable-name">Q</span>
<span class="org-builtin">shift</span>
<span class="org-builtin">shift</span>
ffmpeg -y -i <span class="org-string">"$FILE"</span>  -pixel_format yuv420p -vf $<span class="org-variable-name">VIDEO_FILTER</span> -colorspace 1 -color_primaries 1 -color_trc 1 -c:v libvpx-vp9 -b:v 0 -crf $<span class="org-variable-name">Q</span> -aq-mode 2 -tile-columns 0 -tile-rows 0 -frame-parallel 0 -cpu-used 8 -auto-alt-ref 1 -lag-in-frames 25 -g 240 -pass 1 -f webm -an -threads 8 /dev/null &amp;&amp;
<span class="org-keyword">if</span> [[ $<span class="org-variable-name">FILE</span> =~ <span class="org-string">"webm"</span> ]]; <span class="org-keyword">then</span>
    ffmpeg -y -i <span class="org-string">"$FILE"</span> $<span class="org-variable-name">*</span>  -pixel_format yuv420p -vf $<span class="org-variable-name">VIDEO_FILTER</span> -colorspace 1 -color_primaries 1 -color_trc 1 -c:v libvpx-vp9 -b:v 0 -crf $<span class="org-variable-name">Q</span> -tile-columns 2 -tile-rows 2 -frame-parallel 0 -cpu-used -5 -auto-alt-ref 1 -lag-in-frames 25 -pass 2 -g 240 -ac 2 -threads 8 -c:a copy <span class="org-string">"${FILE%.*}&#45;&#45;compressed$SUFFIX.webm"</span>
<span class="org-keyword">else</span>
    ffmpeg -y -i <span class="org-string">"$FILE"</span> $<span class="org-variable-name">*</span>  -pixel_format yuv420p -vf $<span class="org-variable-name">VIDEO_FILTER</span> -colorspace 1 -color_primaries 1 -color_trc 1 -c:v libvpx-vp9 -b:v 0 -crf $<span class="org-variable-name">Q</span> -tile-columns 2 -tile-rows 2 -frame-parallel 0 -cpu-used -5 -auto-alt-ref 1 -lag-in-frames 25 -pass 2 -g 240 -ac 2 -threads 8 -c:a libvorbis <span class="org-string">"${FILE%.*}&#45;&#45;compressed$SUFFIX.webm"</span>
<span class="org-keyword">fi</span>
</pre>
</div>

<p>
I made an <code>originals.txt</code> file with all the original filenames. It looked like this:
</p>

<pre class="example" id="orge717a1c">
emacsconf-2020-frownies&#45;&#45;the-true-frownies-are-the-friends-we-made-along-the-way-an-anecdote-of-emacs-s-malleability&#45;&#45;case-duckworth.mkv
emacsconf-2021-montessori&#45;&#45;emacs-and-montessori-philosophy&#45;&#45;grant-shangreaux.webm
emacsconf-2021-pattern&#45;&#45;emacs-as-design-pattern-learning&#45;&#45;greta-goetz.mp4
...
</pre>

<p>
I set up a <code>~/.parallel/emacsconf</code> profile with something like this so
that I could use three computers and my laptop, sending one job each
and displaying progress:
</p>

<p>
<code>&#45;&#45;sshlogin computer1 &#45;&#45;sshlogin computer2 &#45;&#45;sshlogin computer3 &#45;&#45;sshlogin : -j 1 &#45;&#45;progress &#45;&#45;verbose &#45;&#45;joblog parallel.log</code>
</p>

<p>
I already had SSH key-based authentication set up so that I could connect to the three remote computers.
</p>

<p>
Then I spread the jobs over four computers with the following command:
</p>

<div class="org-src-container">
<pre class="src src-sh">cat originals.txt | parallel -J emacsconf <span class="org-sh-escaped-newline">\</span>
                             &#45;&#45;transferfile {} <span class="org-sh-escaped-newline">\</span>
                             &#45;&#45;return <span class="org-string">'{=$_ =~ s/\..*?$/&#45;&#45;compressed32.webm/=}'</span> <span class="org-sh-escaped-newline">\</span>
                             &#45;&#45;cleanup <span class="org-sh-escaped-newline">\</span>
                             &#45;&#45;basefile compress-video-low.sh <span class="org-sh-escaped-newline">\</span>
                             bash compress-video-low.sh 32 {}
</pre>
</div>

<p>
It copied each file over to the computer it was assigned to, processed
the file, and then copied the file back.
</p>

<p>
It was also helpful to occasionally do <code>echo 'killall -9 ffmpeg' |
parallel -J emacsconf -j 1 &#45;&#45;onall</code> if I cancelled a run.
</p>

<p>
It still took a long time, but less than it would have if any one
computer had to crunch through everything on its own.
</p>

<p>
This was much better than my previous way of doing things, which
involved copying the files over, running ffmpeg commands, copying the
files back, and getting somewhat confused about which directory I was
in and which file I assigned where and what to do about
incompletely-encoded files.
</p>

<p>
I sometimes ran into problems with incompletely-encoded files because
I'd cancelled the FFmpeg process. Even though <code>ffprobe</code> said the files
were long, they were missing a large chunk of video at the end. I
added a <code>compile-media-verify-video-frames</code> function to
<a href="https://github.com/sachac/compile-media/blob/main/compile-media.el">compile-media.el</a> so that I could get the last few seconds of frames,
compare them against the duration, and report an error if there was a
big gap.
</p>

<p>
Then I changed <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-publish.el">emacsconf-publish.el</a> to use the new filenames, and I
regenerated all the pages. For EmacsConf 2020, I used some Emacs Lisp
to update the files. I'm not particularly fond of wrangling video files (lots of waiting, high chance of error), but I'm glad I got the computers to work together.
</p>

<p>You can <a href="https://sachachua.com/blog/2021/12/re-encoding-the-emacsconf-videos-with-ffmpeg-and-gnu-parallel/#comment">view 2 comments</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2021%2F12%2Fre-encoding-the-emacsconf-videos-with-ffmpeg-and-gnu-parallel%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Update on Emacs Conf 2015 videos; Org Mode tables and time calculations</title>
		<link>https://sachachua.com/blog/2015/09/update-on-emacs-conf-2015-videos-org-mode-tables-and-time-calculations/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Fri, 25 Sep 2015 21:47:00 GMT</pubDate>
    <category>emacs</category>
<category>geek</category>
<category>org</category>
<category>ffmpeg</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=28407</guid>
		<description><![CDATA[<p>I spent the day cutting up the rest of the videos from the Emacs Conference 2015 Twitch.tv stream into individual talks. I&#8217;d already cut the set of talks before lunch, but there were quite a few more after. As it turned out, keeping the video data in <code>.ts</code> format instead of converting it to <code>.mp4</code> is actually better for Youtube processing.</p>
<p>Since Camtasia Studio and Movie Maker were both having problems with the large videos, I used VLC to play the video and find the timestamps at which I needed to cut the segments. I made an Org Mode table with the start and end times, and then I used the <code>;T</code> flag in a table function to get the duration. A little bit of Emacs Lisp code later, and I had my ffmpeg commands. Here&#8217;s the source from my Org file:</p>
<div class="org-src-container">
<pre class="src src-org"><span class="org-org-meta-line">#+NAME: emacsconf-c.ts</span>
<span class="org-org-table">| Notes                                            |      Start |        End | Duration |</span>
<span class="org-org-table">|&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;+&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;+&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;+&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;|</span>
<span class="org-org-table">| Emacs configuration                              | 4:02:25.37 | 4:27:09.30 | 00:24:44 |</span>
<span class="org-org-table">| Hearing from Emacs Beginners                     |    4:27:27 |    5:01:00 | 00:33:33 |</span>
<span class="org-org-table">| Lightning talk: Emacs Club                       | 5:03:19.30 | 5:19:37.83 | 00:16:18 |</span>
<span class="org-org-table">| Starting an Emacs Meetup - Harry Schwartz part 1 | 5:31:52.03 |    6:01:20 | 00:29:28 |</span>
<span class="org-org-meta-line">#+TBLFM: $4=$3-$2;T</span>

<span class="org-org-meta-line">#+NAME: emacsconf-a.ts</span>
<span class="org-org-table">| Notes                                                    |   Start |     End | Duration |</span>
<span class="org-org-table">|&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;+&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;-+&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;-+&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;&#45;|</span>
<span class="org-org-table">| Starting an Emacs Meetup - Harry Schwartz part 2         |  0:0:00 | 0:20:04 | 00:20:04 |</span>
<span class="org-org-table">| Literate Devops - Howard Abrams                          | 1:28:20 | 2:08:15 | 00:39:55 |</span>
<span class="org-org-table">| Lightning talk: Wanderlust and other mail clients        | 2:15:04 | 2:26:55 | 00:11:51 |</span>
<span class="org-org-table">| Making Emacs a Better Tool for Scholars - Erik Hetzner   | 2:27:00 | 2:57:38 | 00:30:38 |</span>
<span class="org-org-table">| Wrapping up and going forward                            | 2:58:09 | 2:59:44 | 00:01:35 |</span>
<span class="org-org-table">| Lightning talk: Collaborative coding with tmux and tmate | 3:00:20 | 3:05:53 | 00:05:33 |</span>
<span class="org-org-table">| Lightning talk: Cask and Pellet                          | 3:05:56 | 3:09:04 | 00:03:08 |</span>
<span class="org-org-table">| Lightning talk: File sharing with Git and save hooks     | 3:09:34 | 3:17:50 | 00:08:16 |</span>
<span class="org-org-table">| Lightning talk: Calc                                     | 3:18:42 | 3:33:20 | 00:14:38 |</span>
<span class="org-org-table">| Lightning talk: Magit                                    | 3:35:15 | 3:49:42 | 00:14:27 |</span>
<span class="org-org-table">| Lightning talk: gist.el                                  | 3:53:50 | 4:01:58 | 00:08:08 |</span>
<span class="org-org-table">| Lightning talk: Go                                       | 4:02:45 | 4:16:37 | 00:13:52 |</span>
<span class="org-org-table">| Question: Emacs Lisp backtraces                          | 4:16:50 | 4:20:09 | 00:03:19 |</span>
<span class="org-org-meta-line">#+TBLFM: $4=$3-$2;T</span>

<span class="org-org-block-begin-line">#+begin_src emacs-lisp :var data=emacsconf-a.ts :var data2=emacsconf-c.ts :colnames t :results output</span>
(<span class="org-keyword">let</span> ((format-str <span class="org-string">"ffmpeg -i %s -ss %s -t %s -c:v copy -c:a copy \"EmacsConf 2015 - %s.ts\"\n"</span>))
  (mapc (<span class="org-keyword">lambda</span> (file)
    (mapc (<span class="org-keyword">lambda</span> (row) 
      (princ (format format-str (car file) (elt row 1) (elt row 3) (my/convert-sketch-title-to-filename (elt row 0))))) 
     (cdr file)))
    `((<span class="org-string">"emacsconf-c.ts"</span> . ,data2)
      (<span class="org-string">"emacsconf-a.ts"</span> . ,data))))
<span class="org-org-block-end-line">#+end_src</span>
</pre>
</div>
<p>and the output:</p>
<pre class="example">ffmpeg -i emacsconf-c.ts -ss 4:02:25.37 -t 00:24:44 -c:v copy -c:a copy "EmacsConf 2015 - Emacs configuration.ts"
ffmpeg -i emacsconf-c.ts -ss 4:27:27 -t 00:33:33 -c:v copy -c:a copy "EmacsConf 2015 - Hearing from Emacs Beginners.ts"
ffmpeg -i emacsconf-c.ts -ss 5:03:19.30 -t 00:16:18 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - Emacs Club.ts"
ffmpeg -i emacsconf-c.ts -ss 5:31:52.03 -t 00:29:28 -c:v copy -c:a copy "EmacsConf 2015 - Starting an Emacs Meetup - Harry Schwartz part 1.ts"
ffmpeg -i emacsconf-a.ts -ss 0:0:00 -t 00:20:04 -c:v copy -c:a copy "EmacsConf 2015 - Starting an Emacs Meetup - Harry Schwartz part 2.ts"
ffmpeg -i emacsconf-a.ts -ss 1:28:20 -t 00:39:55 -c:v copy -c:a copy "EmacsConf 2015 - Literate Devops - Howard Abrams.ts"
ffmpeg -i emacsconf-a.ts -ss 2:15:04 -t 00:11:51 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - Wanderlust and other mail clients.ts"
ffmpeg -i emacsconf-a.ts -ss 2:27:00 -t 00:30:38 -c:v copy -c:a copy "EmacsConf 2015 - Making Emacs a Better Tool for Scholars - Erik Hetzner.ts"
ffmpeg -i emacsconf-a.ts -ss 2:58:09 -t 00:01:35 -c:v copy -c:a copy "EmacsConf 2015 - Wrapping up and going forward.ts"
ffmpeg -i emacsconf-a.ts -ss 3:00:20 -t 00:05:33 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - Collaborative coding with tmux and tmate.ts"
ffmpeg -i emacsconf-a.ts -ss 3:05:56 -t 00:03:08 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - Cask and Pellet.ts"
ffmpeg -i emacsconf-a.ts -ss 3:09:34 -t 00:08:16 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - File sharing with Git and save hooks.ts"
ffmpeg -i emacsconf-a.ts -ss 3:18:42 -t 00:14:38 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - Calc.ts"
ffmpeg -i emacsconf-a.ts -ss 3:35:15 -t 00:14:27 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - Magit.ts"
ffmpeg -i emacsconf-a.ts -ss 3:53:50 -t 00:08:08 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - gist.el.ts"
ffmpeg -i emacsconf-a.ts -ss 4:02:45 -t 00:13:52 -c:v copy -c:a copy "EmacsConf 2015 - Lightning talk - Go.ts"
ffmpeg -i emacsconf-a.ts -ss 4:16:50 -t 00:03:19 -c:v copy -c:a copy "EmacsConf 2015 - Question - Emacs Lisp backtraces.ts"
</pre>
<p><iframe loading="lazy" src="https://www.youtube.com/embed/QaTlXpZ03kc?list=PLomc4HLgvuCUoPVpUJSkpsje4333Guxeo" width="640" height="360" frameborder="0" allowfullscreen="allowfullscreen"></iframe></p>
<p>You can watch the <a href="https://www.youtube.com/playlist?list=PLomc4HLgvuCUoPVpUJSkpsje4333Guxeo">Emacs Conference 2015 playlist</a> on YouTube. At some point, each talk will probably have individual wiki pages and IRC logs at <a href="http://emacsconf2015.org/">http://emacsconf2015.org/</a> . =) Enjoy!</p>
<p>Related tech notes: <a href="https://sachachua.com/blog/2015/09/emacs-conf-video-tech-notes-jit-si-twitch-tv-livestreamer-ffmpeg/">Emacs Conf video tech notes: jit.si, twitch.tv, livestreamer, ffmpeg</a></p>

<p>You can <a href="https://sachachua.com/blog/2015/09/update-on-emacs-conf-2015-videos-org-mode-tables-and-time-calculations/#comment">view 10 comments</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2015%2F09%2Fupdate-on-emacs-conf-2015-videos-org-mode-tables-and-time-calculations%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Emacs Conf video tech notes: jit.si, twitch.tv, livestreamer, ffmpeg</title>
		<link>https://sachachua.com/blog/2015/09/emacs-conf-video-tech-notes-jit-si-twitch-tv-livestreamer-ffmpeg/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 06 Sep 2015 12:00:00 GMT</pubDate>
    <category>emacs</category>
<category>geek</category>
<category>ffmpeg</category>
<category>streaming</category>
		<guid isPermaLink="false">https://sachachua.com/blog/?p=28395</guid>
		<description><![CDATA[<p>Last week&#8217;s Emacs Conf was fantastic. There were lots of people at the in-person event in San Francisco, and people could also watch the stream through twitch.tv and ask questions through IRC. There were remote speakers and in-person speakers, and that mix even worked for the impromptu lightning talks sprinkled throughout the day.</p>
<p>This is how the tech worked:</p>
<ul class="org-ul">
<li>Before the conference started, the organizers set up a laptop for streaming on <a href="http://twitch.tv/emacsconf">twitch.tv/emacsconf</a>. This was hooked up to the main display (a large television with speakers). They also configured the account to record and archive videos. In the free account, recorded videos are available for 14 days.</li>
<li>Remote speakers were brought in using the <a href="http://jitsi.org/">Jitsi</a> open source video conferencing system, using the public servers at <a href="http://meet.jit.si/">meet.jit.si</a>. This was on the same computer that did the twitch.tv streaming, so people watching the stream could see whatever was shared through Jitsi. Organizers read out questions from the in-person audience and from the IRC channel. The audio from Jitsi wasn&#8217;t directly available through twitch.tv, though. Instead, the audio came in as a recording from the laptop&#8217;s microphone.</li>
<li>Local speakers either used the streaming laptop to go to a specific webpage they wanted to talk about, or joined the Jitsi web conference using Google Chrome or Chromium so that they could share their screen. The organizers muted the second Jitsi client to avoid audio feedback loops.</li>
</ul>
<p>That worked out really well. There were more than a hundred remote viewers. As one of them, I can definitely rate the experience as surprisingly smooth.</p>
<p>All that&#8217;s left now is to figure out how to make a more lasting archive of the Emacs Conf videos. As it turns out, twitch.tv or online tools don&#8217;t make it easy to download stream recordings that are longer than three hours. Fortunately, <code>livestreamer</code> can handle the job. Here&#8217;s what I did to download the timestream data from one of the recordings of EmacsConf:</p>
<div class="org-src-container">
<pre class="src src-sh">livestreamer -o emacsconf-1.ts &#45;&#45;hls-segment-threads 4 http://www.twitch.tv/emacsconf/v/13421774 best
ffmpeg -i emacsconf-1.ts -acodec copy -absf aac_adtstoasc -vcodec copy emacsconf-1.mp4
</pre>
</div>
<p>I normally use Camtasia Studio to edit videos, but for some reason, it kept flaking out on me today. After the umpteenth crash, I decided to keep things simple by using <code>ffmpeg</code> to extract the relevant part of the video. To extract a segment, you can use <code>-ss</code> to specify the start time and <code>t</code> to specify the duration. Here&#8217;s a sample command:</p>
<div class="org-src-container">
<pre class="src src-sh">ffmpeg -i emacsconf-1.mp4 -ss 1:18:06.11 -t 0:03:32.29 -c:v copy -c:a copy emacsconf-engine-mode.mp4
</pre>
</div>
<p>Your version of ffmpeg might have a <code>-to</code> option, which would let you specify the end time instead of using <code>-t</code> to specify duration.</p>
<p>I&#8217;m coordinating with the other organizers to see if there&#8217;s a better way to process the videos, so that&#8217;s why we haven&#8217;t released them publicly yet. (Soon!) It would be nice to improve the audio, especially for some of the talks, and maybe it would be good to add overlays or zoom in as well. The on-site organizers captured backup videos and screen recordings, too, so we might want to edit some of those clips into the streamed recording. One of the organizers has access to better video editing tools, so we&#8217;ll try that out.</p>
<p>Anyway, those were the commands that helped me get started with command-line conversion and editing of Twitch.tv recorded videos. Hope they come in handy for other people too.</p>
<p>For more info about EmacsConf 2015, check out <a href="http://emacsconf2015.org/">http://emacsconf2015.org/</a>. There&#8217;ll probably be an announcement there once the videos are up. =)</p>
<p>Hat tip to <a href="https://www.reddit.com/r/Twitch/comments/36qtwg/is_there_any_way_of_currently_download_a_past_vod/">Reddit</a> and <a href="http://superuser.com/questions/377343/cut-part-from-video-file-from-start-position-to-end-position-with-ffmpeg">superuser.com</a> for tips.</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2015%2F09%2Femacs-conf-video-tech-notes-jit-si-twitch-tv-livestreamer-ffmpeg%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item>
	</channel>
</rss>