DEMO · WEB COMPONENT

An Avalon Item in One Element

Avalon Media System is the audiovisual repository software many university libraries run, and every public Avalon item publishes a IIIF Presentation 3 manifest. This page plays one — a production item from Indiana University's Media Collections Online — using the <iiif-transcript-player> web component: no Svelte in the page, no Avalon-specific code, no VTT parsing.

The recording is a nice piece of circularity: Chris Colvard, Avalon's community technical lead, giving a 2017 talk titled Beyond Images: Avalon and IIIF-AV — a talk about the very interoperability that lets this element render it.

This is a literate page: first what Avalon publishes (two excerpts from the real manifest), then what this page writes (four blocks — markup, registration, CSS, event wiring). Every host-page block below is the code this page actually runs; the player at the bottom is that code working.

What Avalon publishes

1 — The media is a Choice of HLS renditions

On the canvas, the painting annotation's body is not a single video file. It is a Choice — Presentation 3's "here are alternatives, pick one" construct — holding Avalon's quality ladder as four HLS streams:

"body": {
  "type": "Choice",
  "choiceHint": "user",
  "items": [
    {
      "id": "https://media.dlib.indiana.edu/master_files/ft848t943/auto.m3u8",
      "type": "Video",
      "format": "application/x-mpegURL",
      "height": 1080, "width": 1920, "duration": 3217.04,
      "label": { "none": ["auto"] }
    },
    { "...": "high.m3u8, medium.m3u8, low.m3u8 follow the same shape" }
  ]
}

The element resolves a Choice to its first Sound/Video member — here the auto stream, itself adaptive. The application/x-mpegURL format marks it as HLS, so the player attaches hls.js (Safari plays HLS natively). Playback runs off IU's streaming server, end to end.

2 — The transcript is a supplementing WebVTT annotation

Alongside the painting annotation, the canvas carries a second annotation whose motivation is supplementing — Presentation 3's word for content that accompanies the media rather than being the media. Its body is the WebVTT caption file Avalon already maintains for this item:

{
  "type": "Annotation",
  "motivation": "supplementing",
  "body": {
    "id": "https://media.dlib.indiana.edu/master_files/ft848t943/supplemental_files/2925/captions",
    "type": "Text",
    "format": "text/vtt",
    "language": "eng"
  },
  "target": "https://media.dlib.indiana.edu/media_objects/3r074z37q/manifest/canvas/ft848t943"
}

Nothing here was made for this player. Avalon generates this shape for every item that has captions; the annotation points at the same text/vtt file the repository serves its own player. When the element's annotations is "auto", it fetches this file, reduces each cue to plain text, and builds the interactive transcript — for this talk, nearly nine hundred click-to-seek, searchable segments.

What this page writes

3 — The markup: one element

<iiif-transcript-player
  id="player"
  manifest-url="https://media.dlib.indiana.edu/media_objects/3r074z37q/manifest.json"
  label="Beyond Images: Avalon and IIIF-AV (Indiana University)"
  crossorigin="anonymous"
></iiif-transcript-player>

manifest-url is the only required attribute. label sets the accessible name a screen reader announces for the player region — worth setting whenever a page could hold more than one player. crossorigin="anonymous" lets the closed-caption <track> load from the repository's domain — browsers require CORS mode for cross-origin text tracks, and an embedding page is by definition on a different origin than the repository. The transcript switch, annotations = "auto", is set as a property in the script below rather than an attribute, because its other accepted value is a full array of transcript segments — data an attribute string can't carry.

4 — Register, then listen

import { register } from "@umd-mith/iiif-timed-transcript/element";
register(); // defines <iiif-transcript-player>; until now it was an unknown element

const el = document.getElementById("player");

const EVENTS = [
  "iiif-player-ready", "iiif-player-error", "iiif-player-canvas-change",
  "iiif-player-play", "iiif-player-pause", "iiif-player-ended",
  "iiif-player-seeked", "iiif-player-rate-change",
];
for (const name of EVENTS) {
  el.addEventListener(name, (e) => logEvent(name, e.detail));
}

el.annotations = "auto"; // property, not attribute — see step 3

Order matters, and this order is safe: the element only wakes up when register() runs — before that the browser treats the tag as an unknown element and no network request happens. Listeners attached in the same script are therefore guaranteed to be in place before the manifest fetch can resolve, so iiif-player-ready is never missed. All events bubble and cross the shadow boundary; the five playback events (play, pause, ended, seeked, rate-change) exist for exactly the use this page puts them to: analytics without reaching inside the player. Watch them arrive in the log next to the live demo. On a page with no bundler — a CMS theme, a static site — the import line is replaced by one classic script tag for the bundled build, which registers the element as a side effect of loading; the rest is identical.

5 — Styling through the shadow boundary

The player renders in a shadow root, so page CSS cannot reach inside — deliberately. Three doors are provided, and this page uses all three: custom properties (theme tokens that inherit through the boundary), ::part() (named handles on the control bar, its buttons, the speed select, the seek slider, the transcript panel, and each segment), and :state() (the element tells the page whether it is loading, playing, or in error, and the page styles the frame accordingly):

/* tokens: theme colors inherit through the shadow boundary */
iiif-transcript-player {
  --iiif-player-accent: #c2694f;            /* this site's terracotta */
  --iiif-player-segment-active-bg: #fbeee8;
}

/* parts: named handles on internal pieces */
iiif-transcript-player::part(controls) { background: #2d2a26; border-radius: 8px; }
iiif-transcript-player::part(button)   { background: #c2694f; color: #fff; border-radius: 6px; }
iiif-transcript-player::part(speed)    { background: #3d3934; color: #fff; border-radius: 6px; }
iiif-transcript-player::part(progress) { accent-color: #c2694f; }

/* states: the element reports its condition; the page reacts */
iiif-transcript-player:state(loading) { outline: 4px solid #e0b089; }
iiif-transcript-player:state(playing) { outline: 4px solid #7ba05b; }
iiif-transcript-player:state(error)   { outline: 4px solid #b91c1c; }

While the manifest loads you'll see the sand-colored loading outline; press play and it turns green. The control bar below is this site's palette, not the player's defaults — restyled entirely from outside.

No Svelte in the page
Choice-wrapped HLS resolved
Transcript from the repository's own VTT
Styled and observed from outside

The code above, running

Streamed live from Indiana University — the video from IU's streaming server via HLS, the transcript from IU's caption file, both located through nothing but the manifest. The log on the right fills as the element's events reach the page.

EVENT LOG

    Source manifest: https://media.dlib.indiana.edu/media_objects/3r074z37q/manifest.json · item page on Media Collections Online

    This item is public and its manifest carries no IIIF Auth services. An access-restricted Avalon item would parse the same way but fail at the media request; the element reports that failure as iiif-player-error with source "auth" so a host page can send the viewer to the repository's own login. This page depends on IU's servers — if the demo is down, the manifest link above will say so quickly.