Embedding audio/video in org html export

Embed whatever you want through org mode export.

Despite the title, the strategies presented work to any export format, not just html. I use the lens of exporting some media in org mode to highlight some of org's features and some nifty integrations you should consider!

Let's talk about building speed in an org workflow for inserting some markup for media assets.


You're one of those or audio/visual/writing people1. You make content. And you use emacs. And you've heard about org mode, and you heard about generating static html files. Maybe you even make a static site using org mode like this one. All is well. You are in the right place.

You go along and discover some pretty things, some convenient things, and they taste exquisite. You start wondering about gathering more strength, more velocity, more followers, more brand deals, more estates, more politicians, more armies, more nukes, more moon colonies….

But first you want to embed <audio> in html. Or <video> for that matter. Or some custom tag. Ultimately you need it in html in some way or form. Well, you've got options:

Methods

  • use an #+EXPORT_HTML block.
    You write your html inside it, you export.

    This option works. It's a bit verbose to use each time. Even with the shorthand

    @@​html: my custom <audio><audio/> @@,

    this is a drag. Even if you make some autocomplete for the dang thing. Really the only thing that changes is the audio file usually.

    And if that's so…

  • use a macro that expands to content of an #+export_html block.
    It's reusable, it encapsulates what changes–probably the url or filename you use to serve the audio. You set it up once, and you have clean looking org documents. Begone, clutter!

    But you may not like the syntax of calling an inline macro:

    A podcast about deep sea exploration puns: {{{ my_macro("../audio/my-podcast.mp3" }}}
    

    And what's more, you have to make sure that macro is defined in every org doc you write so that you can use it. That's kind of a pain if you use the #+includes feature for this.

    But is there an easy mechanism making macros available without having to write extra markup in your document each time?

  • Global macros

    Ha! Of course there is: org-export-global-macros. I didn't go for this but it would work for me, and probably you. There aren't many downsides but you might want them on a per-project basis to make sure they don't clash. You can have a buffer local variable to set this for every org buffer of your project.

    Macros are certainly flexible, reusable, and great!

    But I didn't go that route. I used something more specific…something that hooks up to some other nice emacs mechanisms that we'll talk about…

  • Drag and drop your file

    Drop a file into an org buffer and boom, the link us automatically in your org markup.

    You can even customize drag and drop capability, which we don't explore right now2, and generate custom markup upon dropping.

    What you say, you don't use your mouse and you prefer mostly keyboard? Or maybe the file isn't local, and you dragging and dropping sounds like the aftermath of a hard drug to you…

  • Use a user-defined org-link, like [[audio:../audio/my-podcast.mp3]].
    Systemcrafters does it that way.

    Now just like any other org link, you can jump to it, specify its export behavior, and even its org-store-link functionality.

    Native ones exist:

    You can click on these in org mode! Or move point over and C-c C-o to open the links.

    If you want to create your own link for [[audio:../audio/my-podcast.mp3]]

    ;; For this example we only care about format being html
    ;; if you were exporting to say, md, or something else,
    ;; you'd have to handle that 
    ;; Also omitted is adding text for a11y 
    ;; https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio
    (defun jwow/audio-link-export (path desc format)
      (format "<audio controls src=\"%s\" >
    Your browser does not support the audio element.
    </audio>"
              path))
    
    (org-link-set-parameters "audio"
                             :follow #'org-link-open-as-file
                             :export #'jwow/audio-link-export)
    

    Now

    exports to this:

    This is great but it still seems I'm typing a ton of characters! All that "[[" and "audio" and the path of the file!

  • So let's autocomplete for the link, starting with the link type

    There's a lot of different methods you can customize for auto complete. You can use a various completion framework like completing read, helm, company, corfu, counsel, and others. Or make a quick expandable template using yasnippets. To keep this section focused, let's build on a link strategy.

    How do we complete for an audio link?

    C-c C-l in an org buffer:

    Here's the code that makes that possible:

    ;; I have a buffer local variable that defines my dir dynamically
    ;; I'm hard coding it for this demo for transparency, and so you can change it
    (defvar audio-dir
      "~/src/my-site/src/assets/media/embedding-audio-in-org-html-export/")
    
    ;; Also my post is an org file inside ~/my-site/src/content/, which is 
    ;; important for building a relative file link
    
    (defun jwow/org-audio-complete-link ()
      "Bring up autocomplete for audio files in a specific directory."
      (let* ((file (read-file-name "File: " audio-dir))
             (pwd-relative (file-relative-name file default-directory)))
        (concat "audio:" pwd-relative)))
    
    (org-link-set-parameters "audio"
                             :complete #'jwow/org-audio-complete-link)
    

    Sources say you can also complete for link abbreviations 3, 4, though I've had no luck with it. complete-symbol is what gets called, but I have 0 results with my cursor in front of [[. If I have [[a typed and then complete, I have an ispell error.

  • Completion for a non local resource

    For example, if you want to autocomplete for a youtube video by asking youtube for the video ids, you're going to hook into that corporation's custom api.

    We won't explore that here because every API is different, but it's entirely possible you want to embed someone else's content, and make your meta content commentary. Emacs is more than capable.

    Check out https://github.com/atheriel/helm-twitch for an example of completing for twitch streams using helm. (This was also featured in an emacs conf talk).

  • You've succeeded

    Months pass by, you gain quadrillions of followers. Like me, you become a guarded king secluded from his own dominion, cursed with an-all-too-outrageous-success with his interstellar, intertemporal blog.

    Then suddenly you get an email mentioning how your content has been banned/flagged/lost in the corporate dungeons of VROnlyTubelrSoundXCloudArtBookSyAIHub. It was your most liked piece of genius on the web! Oh no!

  • Renaming links

    You reupload material for your fans. But now the corporation gives you a new video id. Now you've got to replace all those links in org mode!

    You can't bear the tedium of doing it manually. So what do you do?

    If you program enough you probably have tools you go and use for this already. Since you're on org mode though, I'll point you to

    M-x project-query-replace-regexp

    I'd recommend tracking your project with version control then do this. You can one-shot all the replacements across all files by pressing "Y".

    Boom.

    An alternate way to avoid this problem is to use your own unique IDs to resources, and have your link type map to the video id5.

    But for now, just replace your links using a project-wide replace function. Keep it simple until you get those brand deals.

Let's stop for now

This could go on and on. There a lot of neat workflow tricks, things you can do with RSS, CI/CD, org-publish, defining an org export mode, etc.. See other articles on this blog for more! I'm sure someone has done something using the embark package that I haven't had time to test yet.

Conclusion

Org mode provides the speed-hungry with the most drugged up concoction of stimulants to bring your emacs fueled creator journey to the next, next, next, level.

At any point you could have stopped. Any of those solutions work to a quick workflow of inserting a simple link would work. It just depends on what you're looking for, and how you want to design your workflow.

I'd also love to know if you have any tips of your own, or anything you'd like to share. I'm also at this time looking for work in software development, or just being paid to eat chips. Please get in touch at [email protected].

Lastly, though I chose the path of org links, reach for macros when needed. If you need higher degrees of parameterization (you have more varying settings; for example at times wanting to use ogg format, but falling back to mp3, but sometimes there's no ogg or mp3, you can do that in the <audio> tag natively in html with more than one <source> tag. Your macro can take multiple parameters, where as in a link there isn't an obvious interface to do that).

Thanks for reading!


1

Hyper-optimization, or self-improvement, or rampant note-taking types will also fit in.

2

The manual notes the most important variable is dnd-protocol-alist, a list of (protocol . action) I think a great package to see how to customize drag and drop is org-download.

You can check buffer local variables in an org buffer. I have org--dnd-multi-local-file-handler, defined by org.el. This function eventually calls this function to give that menu to attach, insert, or insert file link.

(defun org--dnd-local-file-handler (url action &optional separator)
  "Handle file URL as per ACTION.
SEPARATOR is the string to insert after each link.  It may be nil
in which case, space is inserted."
  (unless separator
    (setq separator " "))
  (let ((method (if (eq org-yank-dnd-method 'ask)
                    (org--dnd-rmc
                     "What to do with file?"
                     '((?a "attach" attach)
                       (?o "open" open)
                       (?f "insert file: link" file-link)))
                  org-yank-dnd-method)))
    (pcase method
      (`attach (org--dnd-attach-file url action separator))
      (`open (dnd-open-local-file url action))
      (`file-link
       (let ((filename (dnd-get-local-file-name url)))
         (insert (org-link-make-string (concat "file:" filename)) separator))))))

Which should mean, you could actually make a new menu item.

If your function can't handle the request of what's being dropped, you can call org-download-dnd-fallback like org-download does (link).

3

See Completion. This is the generic completion in org mode which you can do in front of other elements too.

4

(or M-TAB? I don't know anyone who doesn't have that as actually Alt-TAB~/~Cmd-TAB for switching desktop windows) The point is, fire a command to trigger auto-complete. M-x pcomplete should work. But make sure you have org-link-abbrev-alist with some candidates or there's nothing to complete.

5

Then in the export function, look up some mapping for ID to video link/token string. That way when video tokens don't work, you just change the ID-to-video-link association in one place. Re-export and you're good to go!.