Universe

Attach speaker notes and embedded media (GIF / MP4 / WebM) to PDF slides so they can be presented with the presio.xyz viewer.

presio is framework-agnostic — it works with polylux, touying, or plain Typst pages. It only contributes two functions; layout and slide flow are up to your existing template.

Install

#import "@preview/presio:0.1.0": media, speaker-notes

Usage

Speaker notes

= Introduction

Hello world.

#speaker-notes[
  Remember to mention the funding agency before the next slide.
]

A JSON sidecar notes-slide-<N>.json is attached to the PDF. Multiple speaker-notes[…] calls on the same slide are concatenated into a single attachment, in source order.

Embedded media (local file)

Because of how Typst resolves file paths inside packages, the caller is responsible for read-ing the media bytes:

#media(
  read("figures/demo.gif", encoding: none),
  name: "demo.gif",
  placeholder: image("figures/demo.gif"),
  width: 60%,
)

The binary is embedded in the PDF via pdf.attach and a placement descriptor JSON is written alongside it. placeholder is optional content shown in the slide itself (Typst can render GIFs via image(...) — for MP4/WebM use a still image or omit placeholder to get a dark block).

Embedded media (URL)

#media(
  "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif",
  width: 40%,
  aspect-ratio: 1,
)

Nothing is attached to the PDF; the viewer fetches the URL at presentation time.

YouTube / Vimeo

URLs from YouTube (youtube.com/watch?v=…, youtu.be/…, youtube.com/embed/…, youtube.com/shorts/…, youtube-nocookie.com/embed/…) and Vimeo (vimeo.com/<id>, player.vimeo.com/video/<id>) are detected automatically and emitted with kind: "youtube" / kind: "vimeo" plus an extracted video_id the viewer can use to build an iframe embed.

#media("https://www.youtube.com/watch?v=dQw4w9WgXcQ", width: 60%, aspect-ratio: 16/9)
#media("https://vimeo.com/76979871", width: 60%, aspect-ratio: 16/9)

media parameters

Parameter Default Notes
source Either bytes (from read(..., encoding: none)) or a http(s):// URL string
name none Required when source is bytes. Used as the PDF attachment filename and to sniff the MIME type.
placeholder none Optional content rendered as the in-slide preview (e.g. image(...))
width auto Length, ratio of the container width, or auto (natural width of placeholder, else container width)
height auto Length, ratio, or auto (derived from placeholder / aspect-ratio / 16:9)
aspect-ratio none Width/height ratio, e.g. 16/9
autoplay true Forwarded to the viewer
loop true Forwarded to the viewer

Supported media types

The MIME is sniffed from the extension of name (bytes mode) or the URL (URL mode):

Extension MIME
.gif image/gif
.mp4 video/mp4
.webm video/webm

Sidecar JSON schemas

notes-slide-<N>.jsonnotes is always an array (one entry per speaker-notes call on that slide, in source order):

{ "slide": "3", "notes": [ "<typst content>", "<typst content>" ] }

media-slide-<N>-<id>.json

Common fields: kind, slide, id, mime, x_pt, y_pt, w_pt, h_pt, autoplay, loop. The remaining fields depend on kind:

kind Extra fields
file filename (PDF attachment name)
url url (direct media URL)
youtube url, video_id
vimeo url, video_id
{
  "kind": "file",
  "slide": 3,
  "id": "demo_gif",
  "mime": "image/gif",
  "x_pt": 72.0, "y_pt": 120.0, "w_pt": 432.0, "h_pt": 243.0,
  "autoplay": true,
  "loop": true,
  "filename": "media-demo_gif.gif"
}

License

MIT — see LICENSE.