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.
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.
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.
<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.
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.
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.
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.
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.