@mango-iiif/av 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/annotations.d.ts +12 -0
  4. package/dist/annotations.js +15 -0
  5. package/dist/annotations.js.map +1 -0
  6. package/dist/chapters.d.ts +12 -0
  7. package/dist/chapters.js +14 -0
  8. package/dist/chapters.js.map +1 -0
  9. package/dist/chunk-3LLEY7MA.js +126 -0
  10. package/dist/chunk-3LLEY7MA.js.map +1 -0
  11. package/dist/chunk-CDLYT3KB.js +17 -0
  12. package/dist/chunk-CDLYT3KB.js.map +1 -0
  13. package/dist/chunk-GAVG7CPU.js +1 -0
  14. package/dist/chunk-GAVG7CPU.js.map +1 -0
  15. package/dist/chunk-GX7UU6V4.js +547 -0
  16. package/dist/chunk-GX7UU6V4.js.map +1 -0
  17. package/dist/chunk-IFTSCRCU.js +83 -0
  18. package/dist/chunk-IFTSCRCU.js.map +1 -0
  19. package/dist/chunk-JXUHKKIX.js +94 -0
  20. package/dist/chunk-JXUHKKIX.js.map +1 -0
  21. package/dist/chunk-ML77YRJL.js +221 -0
  22. package/dist/chunk-ML77YRJL.js.map +1 -0
  23. package/dist/chunk-NTHMZZYJ.js +38 -0
  24. package/dist/chunk-NTHMZZYJ.js.map +1 -0
  25. package/dist/chunk-PZ6GV6ZJ.js +163 -0
  26. package/dist/chunk-PZ6GV6ZJ.js.map +1 -0
  27. package/dist/chunk-R4OUWZ3R.js +1038 -0
  28. package/dist/chunk-R4OUWZ3R.js.map +1 -0
  29. package/dist/chunk-YH6DS7J6.js +74 -0
  30. package/dist/chunk-YH6DS7J6.js.map +1 -0
  31. package/dist/controller-DLy38ais.d.ts +356 -0
  32. package/dist/controller-element-BOKM9v2S.d.ts +15 -0
  33. package/dist/core.d.ts +28 -0
  34. package/dist/core.js +28 -0
  35. package/dist/core.js.map +1 -0
  36. package/dist/index.d.ts +25 -0
  37. package/dist/index.js +70 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/media.d.ts +28 -0
  40. package/dist/media.js +16 -0
  41. package/dist/media.js.map +1 -0
  42. package/dist/player.d.ts +27 -0
  43. package/dist/player.js +15 -0
  44. package/dist/player.js.map +1 -0
  45. package/dist/transcript-element.d.ts +12 -0
  46. package/dist/transcript-element.js +16 -0
  47. package/dist/transcript-element.js.map +1 -0
  48. package/docs/API.md +264 -0
  49. package/docs/FEATURE_PARITY.md +43 -0
  50. package/package.json +94 -0
@@ -0,0 +1,547 @@
1
+ import {
2
+ ensureMediaChrome
3
+ } from "./chunk-NTHMZZYJ.js";
4
+ import {
5
+ playFromInteraction
6
+ } from "./chunk-CDLYT3KB.js";
7
+ import {
8
+ loadTranscript
9
+ } from "./chunk-3LLEY7MA.js";
10
+ import {
11
+ AVPlayerController,
12
+ escapeHtml,
13
+ formatString,
14
+ formatTime,
15
+ resolveStrings
16
+ } from "./chunk-R4OUWZ3R.js";
17
+
18
+ // src/components/mango-av-media.ts
19
+ var HTMLElementBase = globalThis.HTMLElement ?? class {
20
+ };
21
+ var hlsElementRegistration;
22
+ var ensureHlsElement = () => {
23
+ hlsElementRegistration ??= import("hls-video-element");
24
+ return hlsElementRegistration;
25
+ };
26
+ var eventTypes = [
27
+ "av-loadstart",
28
+ "av-ready",
29
+ "av-error",
30
+ "av-canvaschange",
31
+ "av-sourcechange",
32
+ "av-play",
33
+ "av-pause",
34
+ "av-timeupdate",
35
+ "av-seek",
36
+ "av-ended",
37
+ "av-annotationschange",
38
+ "av-configchange",
39
+ "av-statechange"
40
+ ];
41
+ var styles = `
42
+ :host {
43
+ --mango-av-accent: #5b3fd6;
44
+ --mango-av-accent-contrast: #fff;
45
+ --mango-av-background: #fff;
46
+ --mango-av-surface: #f5f5f8;
47
+ --mango-av-text: #18181b;
48
+ --mango-av-muted: #62626a;
49
+ --mango-av-border: #d7d7de;
50
+ --mango-av-radius: .65rem;
51
+ --mango-av-font: ui-sans-serif, system-ui, sans-serif;
52
+ --mango-av-panel-width: 24rem;
53
+ --mango-av-player-aspect-ratio: 16 / 9;
54
+ --media-primary-color: var(--mango-av-accent-contrast);
55
+ --media-secondary-color: var(--mango-av-accent);
56
+ color: var(--mango-av-text);
57
+ display: block;
58
+ font-family: var(--mango-av-font);
59
+ }
60
+ *, *::before, *::after { box-sizing: border-box; }
61
+ button, input, select { font: inherit; }
62
+ button, select { cursor: pointer; }
63
+ .shell { background: var(--mango-av-background); border: 1px solid var(--mango-av-border); border-radius: var(--mango-av-radius); overflow: clip; }
64
+ .header { padding: 1rem 1.1rem; }
65
+ .header h2 { font-size: 1.15rem; margin: 0; }
66
+ .header p { color: var(--mango-av-muted); margin: .35rem 0 0; }
67
+ .layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(18rem, var(--mango-av-panel-width)); }
68
+ .player-column { background: #08080a; min-width: 0; position: relative; }
69
+ media-controller { display: block; width: 100%; }
70
+ media-controller:not([audio]) { aspect-ratio: var(--mango-av-player-aspect-ratio); }
71
+ video, hls-video { display: block; height: 100%; object-fit: contain; width: 100%; }
72
+ audio { width: 100%; }
73
+ media-control-bar { width: 100%; }
74
+ .audio-art { align-items: center; aspect-ratio: 16 / 7; background: linear-gradient(135deg, var(--mango-av-accent), #111); color: white; display: flex; justify-content: center; padding: 2rem; text-align: center; }
75
+ .audio-art img { height: 100%; inset: 0; object-fit: contain; position: absolute; width: 100%; }
76
+ .audio-art span { font-size: clamp(1.1rem, 3vw, 2rem); font-weight: 700; }
77
+ .player-toolbar { align-items: center; background: #151518; color: #fff; display: flex; flex-wrap: wrap; gap: .5rem; justify-content: space-between; padding: .55rem .7rem; }
78
+ .player-toolbar button, .player-toolbar select { background: #29292e; border: 1px solid #44444a; border-radius: .35rem; color: white; padding: .4rem .55rem; }
79
+ .player-toolbar .group { align-items: center; display: flex; gap: .45rem; }
80
+ .search-markers { background: #242429; height: .45rem; position: relative; }
81
+ .search-marker { background: #ffe24a; border: 0; border-radius: 50%; height: .7rem; padding: 0; position: absolute; top: 50%; transform: translate(-50%, -50%); width: .7rem; }
82
+ .resume { align-items: center; background: #fff8d7; color: #312900; display: flex; gap: .5rem; justify-content: center; padding: .6rem; }
83
+ .resume button { background: var(--mango-av-accent); border: 0; border-radius: .35rem; color: var(--mango-av-accent-contrast); padding: .35rem .65rem; }
84
+ .panel { border-left: 1px solid var(--mango-av-border); display: flex; flex-direction: column; min-height: 22rem; min-width: 0; }
85
+ .tabs { background: var(--mango-av-surface); display: flex; gap: .1rem; overflow-x: auto; padding: .35rem; }
86
+ .tab { background: transparent; border: 0; border-bottom: 3px solid transparent; color: var(--mango-av-muted); padding: .55rem .6rem; white-space: nowrap; }
87
+ .tab[aria-selected=true] { border-color: var(--mango-av-accent); color: var(--mango-av-text); font-weight: 650; }
88
+ .panel-content { flex: 1; max-height: 34rem; overflow: auto; padding: .85rem; }
89
+ .empty { color: var(--mango-av-muted); font-style: italic; }
90
+ .chapter-list, .annotation-list, .cue-list, .file-list { list-style: none; margin: 0; padding: 0; }
91
+ .chapter-list .chapter-list { border-left: 1px solid var(--mango-av-border); margin-left: .8rem; padding-left: .55rem; }
92
+ .chapter button, .annotation button, .cue button { background: transparent; border: 0; border-radius: .35rem; color: inherit; display: grid; gap: .15rem; padding: .5rem; text-align: left; width: 100%; }
93
+ .chapter button:hover, .annotation button:hover, .cue button:hover { background: var(--mango-av-surface); }
94
+ .chapter button.active, .annotation button.active, .cue button.active { background: color-mix(in srgb, var(--mango-av-accent) 14%, transparent); box-shadow: inset 3px 0 var(--mango-av-accent); }
95
+ .time { color: var(--mango-av-muted); font-size: .78rem; }
96
+ .panel-tools { display: flex; flex-wrap: wrap; gap: .45rem; margin-bottom: .75rem; }
97
+ .panel-tools input, .panel-tools select { border: 1px solid var(--mango-av-border); border-radius: .35rem; min-width: 0; padding: .45rem; }
98
+ .panel-tools input { flex: 1; }
99
+ .panel-tools a { align-content: center; color: var(--mango-av-accent); }
100
+ .cue mark { background: #ffe770; }
101
+ .speaker { font-weight: 700; }
102
+ .tags { display: flex; flex-wrap: wrap; gap: .25rem; }
103
+ .tag { background: var(--mango-av-surface); border-radius: 999px; font-size: .75rem; padding: .15rem .45rem; }
104
+ dl { margin: 0; }
105
+ dt { font-weight: 700; margin-top: .75rem; }
106
+ dd { color: var(--mango-av-muted); margin: .2rem 0 0; }
107
+ .file-list li { border-bottom: 1px solid var(--mango-av-border); padding: .55rem 0; }
108
+ a { color: var(--mango-av-accent); }
109
+ .error { background: #fff0f0; color: #8a1717; padding: 1rem; }
110
+ .sr-only { clip: rect(0,0,0,0); clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute; white-space: nowrap; width: 1px; }
111
+ @media (max-width: 760px) {
112
+ .layout { grid-template-columns: 1fr; }
113
+ .panel { border-left: 0; border-top: 1px solid var(--mango-av-border); }
114
+ }
115
+ `;
116
+ var metadataHtml = (entries, ui) => {
117
+ if (entries.length === 0) return `<p class="empty">${escapeHtml(ui.noMetadata)}</p>`;
118
+ return `<dl>${entries.map((entry) => `
119
+ <dt>${escapeHtml(entry.label)}</dt>
120
+ <dd>${entry.values.map((value) => escapeHtml(value.value)).join("<br>")}</dd>
121
+ `).join("")}</dl>`;
122
+ };
123
+ var chapterHtml = (chapters, canvasId, ui, level = 1) => {
124
+ if (chapters.length === 0 && level === 1) return `<p class="empty">${escapeHtml(ui.noChapters)}</p>`;
125
+ return `<ul class="chapter-list">${chapters.map((chapter) => {
126
+ const active = chapter.canvasId === canvasId;
127
+ const target = chapter.canvasId ? `data-canvas="${escapeHtml(chapter.canvasId)}"` : "";
128
+ const time = chapter.time ? `data-time="${chapter.time.start}"` : "";
129
+ return `<li class="chapter">
130
+ <button type="button" class="${active ? "active" : ""}" ${target} ${time} ${chapter.canvasId ? "" : "disabled"}>
131
+ <span>${escapeHtml(chapter.label)}</span>
132
+ ${chapter.time ? `<span class="time">${formatTime(chapter.time.start)}</span>` : ""}
133
+ </button>
134
+ ${chapterHtml(chapter.children, canvasId, ui, level + 1)}
135
+ </li>`;
136
+ }).join("")}</ul>`;
137
+ };
138
+ var highlight = (text, query) => {
139
+ const escaped = escapeHtml(text);
140
+ if (!query) return escaped;
141
+ const safeQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
142
+ return escaped.replace(new RegExp(`(${safeQuery})`, "gi"), "<mark>$1</mark>");
143
+ };
144
+ var MangoAVMediaElement = class extends HTMLElementBase {
145
+ static observedAttributes = ["manifest-url", "config"];
146
+ player;
147
+ #root;
148
+ #inputManifest;
149
+ #config = {};
150
+ #activePanel = "chapters";
151
+ #transcriptCues = [];
152
+ #transcriptIndex = 0;
153
+ #transcriptQuery = "";
154
+ #resumeTime;
155
+ #loadingToken = 0;
156
+ #resolvedSourceUrl;
157
+ #sourceToken = 0;
158
+ constructor() {
159
+ super();
160
+ this.#root = this.attachShadow({ mode: "open" });
161
+ this.player = new AVPlayerController();
162
+ for (const type of eventTypes) {
163
+ this.player.addEventListener(type, ((event) => {
164
+ if (type === "av-ready") {
165
+ if (this.player.source?.isHls) void ensureHlsElement();
166
+ this.#resolvedSourceUrl = void 0;
167
+ this.#resumeTime = this.player.getSavedPosition();
168
+ this.#activePanel = this.player.config.activePanel;
169
+ this.#render();
170
+ void this.#resolveSource();
171
+ void this.#loadActiveTranscript();
172
+ } else if (type === "av-canvaschange" || type === "av-sourcechange") {
173
+ if (this.player.source?.isHls) void ensureHlsElement();
174
+ this.#resolvedSourceUrl = void 0;
175
+ this.#transcriptCues = [];
176
+ this.#transcriptIndex = 0;
177
+ this.#render();
178
+ void this.#resolveSource();
179
+ void this.#loadActiveTranscript();
180
+ } else if (type === "av-configchange") {
181
+ this.#activePanel = this.player.config.activePanel;
182
+ void ensureMediaChrome(this.player.config.preferredLanguages[0]).then(() => this.#render());
183
+ } else if (type === "av-error") {
184
+ this.#render();
185
+ } else if (type === "av-annotationschange") {
186
+ if (this.#activePanel === "annotations") this.#renderPanelOnly();
187
+ } else if (type === "av-timeupdate") {
188
+ this.#syncTimedContent();
189
+ }
190
+ this.dispatchEvent(new CustomEvent(type, {
191
+ detail: event.detail,
192
+ bubbles: true,
193
+ composed: true
194
+ }));
195
+ }));
196
+ }
197
+ }
198
+ get manifest() {
199
+ return this.#inputManifest;
200
+ }
201
+ get controller() {
202
+ return this.player;
203
+ }
204
+ set manifest(value) {
205
+ this.#inputManifest = value;
206
+ if (this.isConnected && value !== void 0) void this.load(value);
207
+ }
208
+ get config() {
209
+ return this.#config;
210
+ }
211
+ set config(value) {
212
+ this.#config = value;
213
+ this.player.configure(value);
214
+ this.#activePanel = value.activePanel ?? this.player.config.activePanel;
215
+ if (this.isConnected) this.#render();
216
+ }
217
+ connectedCallback() {
218
+ this.#readConfigAttribute();
219
+ void ensureMediaChrome(this.player.config.preferredLanguages[0]);
220
+ this.#render();
221
+ const source = this.#inputManifest ?? this.getAttribute("manifest-url") ?? void 0;
222
+ if (source !== void 0) void this.load(source);
223
+ }
224
+ disconnectedCallback() {
225
+ this.player.destroy();
226
+ }
227
+ attributeChangedCallback(name, oldValue, newValue) {
228
+ if (!this.isConnected || oldValue === newValue) return;
229
+ if (name === "manifest-url" && newValue) {
230
+ this.#inputManifest = newValue;
231
+ void this.load(newValue);
232
+ }
233
+ if (name === "config") {
234
+ this.#readConfigAttribute();
235
+ this.#render();
236
+ }
237
+ }
238
+ async load(input) {
239
+ this.#inputManifest = input;
240
+ return await this.player.load(input);
241
+ }
242
+ play() {
243
+ return this.player.play();
244
+ }
245
+ pause() {
246
+ this.player.pause();
247
+ }
248
+ seekTo(time) {
249
+ this.player.seekTo(time);
250
+ }
251
+ selectCanvas(indexOrId, options) {
252
+ return this.player.selectCanvas(indexOrId, options);
253
+ }
254
+ #readConfigAttribute() {
255
+ const attribute = this.getAttribute("config");
256
+ if (!attribute) return;
257
+ try {
258
+ this.#config = JSON.parse(attribute);
259
+ this.player.configure(this.#config);
260
+ } catch (cause) {
261
+ const error = cause instanceof Error ? cause : new Error(String(cause));
262
+ this.dispatchEvent(new CustomEvent("av-error", { detail: { error }, bubbles: true, composed: true }));
263
+ }
264
+ }
265
+ #render() {
266
+ const state = this.player.state;
267
+ const manifest = state.manifest;
268
+ const canvas = this.player.canvas;
269
+ const source = this.player.source;
270
+ const ui = resolveStrings(this.player.config.strings);
271
+ this.#root.innerHTML = `<style>${styles}</style><div class="shell">
272
+ <slot name="header">
273
+ ${manifest ? `<header class="header"><h2>${escapeHtml(manifest.label)}</h2>${manifest.summary ? `<p>${escapeHtml(manifest.summary)}</p>` : ""}</header>` : ""}
274
+ </slot>
275
+ ${state.status === "error" ? `<div class="error" role="alert">${escapeHtml(state.error?.message ?? ui.unableToLoadPlayer)}</div>` : ""}
276
+ ${state.status === "loading" ? `<p class="header" role="status">${escapeHtml(ui.loadingManifest)}</p>` : ""}
277
+ ${manifest && canvas ? this.#renderReady(manifest, canvas, source?.kind ?? "video") : ""}
278
+ </div>`;
279
+ this.#bindInteractions();
280
+ const media = this.#root.querySelector("video, audio, hls-video");
281
+ if (media) this.player.attachMedia(media);
282
+ }
283
+ #renderReady(manifest, canvas, kind) {
284
+ const source = this.player.source;
285
+ const panels = this.player.config.panels;
286
+ const controls = this.player.config.controls;
287
+ const ui = resolveStrings(this.player.config.strings);
288
+ const panelTabs = [
289
+ panels.chapters ? ["chapters", ui.chapters] : void 0,
290
+ panels.transcript ? ["transcript", ui.transcript] : void 0,
291
+ panels.annotations ? ["annotations", ui.annotations] : void 0,
292
+ panels.metadata ? ["metadata", ui.metadata] : void 0,
293
+ panels.supplementalFiles ? ["files", ui.files] : void 0
294
+ ].filter((value) => Boolean(value));
295
+ const showPanel = this.#activePanel !== "none" && panelTabs.length > 0;
296
+ return `
297
+ ${this.#resumeTime !== void 0 ? `<div class="resume">${escapeHtml(formatString(ui.resumePrompt, { time: formatTime(this.#resumeTime) }))} <button type="button" data-action="resume">${escapeHtml(ui.resume)}</button><button type="button" data-action="dismiss-resume">${escapeHtml(ui.startOver)}</button></div>` : ""}
298
+ <div class="layout" ${showPanel ? "" : 'style="grid-template-columns: 1fr"'}>
299
+ <section class="player-column" aria-label="${escapeHtml(ui.mediaPlayer)}">
300
+ <slot name="before-player"></slot>
301
+ <media-controller ${kind === "audio" ? "audio" : ""} ${this.#mediaChromeLanguage()}>
302
+ ${kind === "audio" ? `<div class="audio-art"><span>${escapeHtml(canvas.label)}</span></div>` : ""}
303
+ ${source ? this.#mediaHtml(canvas, source.kind) : `<div class="audio-art"><span>${escapeHtml(ui.noPlayableMedia)}</span></div>`}
304
+ <slot name="player-overlay"></slot>
305
+ <slot name="controls">
306
+ <media-control-bar>
307
+ <media-play-button></media-play-button>
308
+ ${controls.seek ? `<media-seek-backward-button seekoffset="${controls.skipSeconds ?? 10}"></media-seek-backward-button><media-seek-forward-button seekoffset="${controls.skipSeconds ?? 10}"></media-seek-forward-button>` : ""}
309
+ <media-time-display showduration></media-time-display>
310
+ ${controls.seek ? "<media-time-range></media-time-range>" : ""}
311
+ ${controls.volume ? "<media-mute-button></media-mute-button><media-volume-range></media-volume-range>" : ""}
312
+ ${controls.captions && canvas.tracks.length > 0 ? "<media-captions-menu-button></media-captions-menu-button>" : ""}
313
+ ${controls.playbackRate ? "<media-playback-rate-menu-button></media-playback-rate-menu-button>" : ""}
314
+ ${controls.pictureInPicture && kind === "video" ? "<media-pip-button></media-pip-button>" : ""}
315
+ ${controls.fullscreen && kind === "video" ? "<media-fullscreen-button></media-fullscreen-button>" : ""}
316
+ </media-control-bar>
317
+ ${controls.captions && canvas.tracks.length > 0 ? "<media-captions-menu hidden></media-captions-menu>" : ""}
318
+ ${controls.playbackRate ? `<media-playback-rate-menu rates="${this.player.config.playbackRates.join(" ")}" hidden></media-playback-rate-menu>` : ""}
319
+ </slot>
320
+ </media-controller>
321
+ ${this.#searchMarkersHtml()}
322
+ <div class="player-toolbar">
323
+ <div class="group">
324
+ <button type="button" data-action="previous" ${canvas.index === 0 ? "disabled" : ""} aria-label="${escapeHtml(ui.previousSection)}">\u2190 ${escapeHtml(ui.previous)}</button>
325
+ <button type="button" data-action="next" ${canvas.index === manifest.canvases.length - 1 && !this.player.state.repeat ? "disabled" : ""} aria-label="${escapeHtml(ui.nextSection)}">${escapeHtml(ui.next)} \u2192</button>
326
+ </div>
327
+ <div class="group">
328
+ ${controls.sourceSelector && canvas.sources.length > 1 ? `<label><span class="sr-only">${escapeHtml(ui.mediaSource)}</span><select data-action="source">${canvas.sources.map((item, index) => `<option value="${index}" ${index === this.player.state.sourceIndex ? "selected" : ""}>${escapeHtml(item.label ?? item.format ?? formatString(ui.sourceNumber, { number: index + 1 }))}</option>`).join("")}</select></label>` : ""}
329
+ ${controls.download && canvas.rendering.length > 0 ? `<label><span class="sr-only">${escapeHtml(ui.downloadMediaFile)}</span><select data-action="download"><option value="">${escapeHtml(ui.download)}\u2026</option>${canvas.rendering.map((file) => `<option value="${escapeHtml(file.id)}" data-filename="${escapeHtml(file.filename ?? "")}">${escapeHtml(file.label)}</option>`).join("")}</select></label>` : ""}
330
+ <label><input type="checkbox" data-action="auto-advance" ${this.player.state.autoAdvance ? "checked" : ""}> ${escapeHtml(ui.autoAdvance)}</label>
331
+ </div>
332
+ </div>
333
+ <slot name="after-player"></slot>
334
+ </section>
335
+ ${showPanel ? `<aside class="panel">
336
+ <nav class="tabs" role="tablist" aria-label="${escapeHtml(ui.mediaInformation)}">${panelTabs.map(([id, tabLabel]) => `<button class="tab" type="button" role="tab" data-panel="${id}" aria-selected="${this.#activePanel === id}">${escapeHtml(tabLabel)}</button>`).join("")}</nav>
337
+ <div class="panel-content" role="tabpanel">${this.#panelHtml(manifest, canvas)}</div>
338
+ </aside>` : ""}
339
+ </div>`;
340
+ }
341
+ #mediaHtml(canvas, kind) {
342
+ const source = this.player.source;
343
+ if (!source) return "";
344
+ const tag = source.isHls ? "hls-video" : kind;
345
+ const sourceUrl = this.#resolvedSourceUrl ?? (this.player.config.resolveRequest ? "" : source.src);
346
+ const attributes = [
347
+ 'slot="media"',
348
+ sourceUrl ? `src="${escapeHtml(sourceUrl)}"` : "",
349
+ `preload="${this.player.config.preload}"`,
350
+ this.player.config.crossorigin ? `crossorigin="${this.player.config.crossorigin}"` : "",
351
+ kind === "video" && canvas.poster ? `poster="${escapeHtml(canvas.poster)}"` : "",
352
+ `aria-label="${escapeHtml(canvas.label)}"`,
353
+ `data-canvasindex="${canvas.index}"`
354
+ ].filter(Boolean).join(" ");
355
+ const tracks = canvas.tracks.map((track) => `<track src="${escapeHtml(track.src)}" kind="${track.kind}" label="${escapeHtml(track.label)}" ${track.language ? `srclang="${escapeHtml(track.language)}"` : ""} ${track.default ? "default" : ""}>`).join("");
356
+ return `<${tag} ${attributes}>${tracks}</${tag}>`;
357
+ }
358
+ #mediaChromeLanguage() {
359
+ const language = this.player.config.preferredLanguages.find((value) => value !== "none");
360
+ return language ? `lang="${escapeHtml(language)}"` : "";
361
+ }
362
+ #panelHtml(manifest, canvas) {
363
+ const ui = resolveStrings(this.player.config.strings);
364
+ switch (this.#activePanel) {
365
+ case "chapters":
366
+ return chapterHtml(manifest.chapters, canvas.id, ui);
367
+ case "transcript":
368
+ return this.#transcriptHtml(canvas);
369
+ case "annotations":
370
+ return this.#annotationsHtml(canvas.annotations);
371
+ case "metadata":
372
+ return `<h3>${escapeHtml(canvas.label)}</h3>${metadataHtml([...manifest.metadata, ...canvas.metadata], ui)}${manifest.requiredStatement ? metadataHtml([manifest.requiredStatement], ui) : ""}${manifest.rights ? `<p><strong>${escapeHtml(ui.rights)}:</strong> <a href="${escapeHtml(manifest.rights)}" target="_blank" rel="noreferrer">${escapeHtml(manifest.rights)}</a></p>` : ""}`;
373
+ case "files": {
374
+ const files = [...manifest.rendering, ...canvas.rendering];
375
+ return files.length > 0 ? `<ul class="file-list">${files.map((file) => `<li><a href="${escapeHtml(file.id)}" download="${escapeHtml(file.filename ?? "")}">${escapeHtml(file.label)}</a>${file.format ? `<div class="time">${escapeHtml(file.format)}</div>` : ""}</li>`).join("")}</ul>` : `<p class="empty">${escapeHtml(ui.noSupplementalFiles)}</p>`;
376
+ }
377
+ default:
378
+ return "";
379
+ }
380
+ }
381
+ #transcriptHtml(canvas) {
382
+ const ui = resolveStrings(this.player.config.strings);
383
+ if (canvas.transcripts.length === 0) return `<p class="empty">${escapeHtml(ui.noTranscript)}</p>`;
384
+ const resource = canvas.transcripts[this.#transcriptIndex];
385
+ const query = this.#transcriptQuery.trim();
386
+ const visible = query ? this.#transcriptCues.filter((cue) => cue.text.toLowerCase().includes(query.toLowerCase())) : this.#transcriptCues;
387
+ return `<div class="panel-tools">
388
+ ${canvas.transcripts.length > 1 ? `<select data-action="transcript" aria-label="${escapeHtml(ui.transcript)}">${canvas.transcripts.map((item, index) => `<option value="${index}" ${index === this.#transcriptIndex ? "selected" : ""}>${escapeHtml(item.label)}</option>`).join("")}</select>` : ""}
389
+ ${this.player.config.transcript.search ? `<input type="search" data-action="transcript-search" value="${escapeHtml(this.#transcriptQuery)}" placeholder="${escapeHtml(ui.searchTranscript)}" aria-label="${escapeHtml(ui.searchTranscript)}">` : ""}
390
+ ${this.player.config.transcript.download && resource?.src ? `<a href="${escapeHtml(resource.src)}" download>${escapeHtml(ui.download)}</a>` : ""}
391
+ </div>
392
+ ${this.#transcriptCues.length === 0 ? `<p class="empty">${escapeHtml(ui.loadingTranscript)}</p>` : `<ol class="cue-list">${visible.map((cue) => `<li class="cue" data-cue-id="${escapeHtml(cue.id)}" ${cue.start !== void 0 ? `data-start="${cue.start}" data-end="${cue.end ?? cue.start}"` : ""}><button type="button" ${cue.start === void 0 ? "disabled" : ""}>${cue.start !== void 0 ? `<span class="time">${formatTime(cue.start)}</span>` : ""}${cue.speaker ? `<span class="speaker">${escapeHtml(cue.speaker)}</span>` : ""}<span>${highlight(cue.text, query)}</span></button></li>`).join("")}</ol>`}`;
393
+ }
394
+ #annotationsHtml(annotations) {
395
+ const ui = resolveStrings(this.player.config.strings);
396
+ if (annotations.length === 0) return `<p class="empty">${escapeHtml(ui.noAnnotations)}</p>`;
397
+ return `<ol class="annotation-list">${annotations.map((annotation) => `<li class="annotation" data-start="${annotation.time?.start ?? ""}" data-end="${annotation.time?.end ?? annotation.time?.start ?? ""}"><button type="button" ${annotation.time ? "" : "disabled"}>${annotation.time ? `<span class="time">${formatTime(annotation.time.start)}</span>` : ""}<span>${escapeHtml(annotation.text)}</span>${annotation.tags.length > 0 ? `<span class="tags">${annotation.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}</span>` : ""}</button></li>`).join("")}</ol>`;
398
+ }
399
+ #searchMarkersHtml() {
400
+ const ui = resolveStrings(this.player.config.strings);
401
+ const query = this.#transcriptQuery.trim().toLowerCase();
402
+ const duration = this.player.state.duration || this.player.canvas?.duration || 0;
403
+ if (!query || !this.player.config.transcript.showSearchMarkers || duration <= 0) return "";
404
+ const matches = this.#transcriptCues.filter((cue) => cue.start !== void 0 && cue.text.toLowerCase().includes(query));
405
+ if (matches.length === 0) return "";
406
+ return `<div class="search-markers" aria-label="${escapeHtml(ui.transcriptSearchMatches)}">${matches.map((cue) => `<button type="button" class="search-marker" data-search-time="${cue.start}" style="left:${Math.min(100, Math.max(0, (cue.start ?? 0) / duration * 100))}%" aria-label="${escapeHtml(formatString(ui.searchMatchAt, { time: formatTime(cue.start ?? 0) }))}"></button>`).join("")}</div>`;
407
+ }
408
+ #bindInteractions() {
409
+ this.#root.querySelectorAll("[data-panel]").forEach((button) => button.addEventListener("click", () => {
410
+ this.#activePanel = button.dataset.panel;
411
+ this.#renderPanelOnly();
412
+ if (this.#activePanel === "transcript") void this.#loadActiveTranscript();
413
+ }));
414
+ this.#root.querySelector("[data-action=previous]")?.addEventListener("click", () => this.player.previous({ autoplay: this.player.state.status === "playing" }));
415
+ this.#root.querySelector("[data-action=next]")?.addEventListener("click", () => this.player.next({ autoplay: this.player.state.status === "playing" }));
416
+ this.#root.querySelector("[data-action=source]")?.addEventListener("change", (event) => this.player.selectSource(Number(event.target.value)));
417
+ this.#root.querySelector("[data-action=download]")?.addEventListener("change", (event) => {
418
+ const select = event.target;
419
+ if (!select.value) return;
420
+ const anchor = document.createElement("a");
421
+ anchor.href = select.value;
422
+ anchor.download = select.selectedOptions[0]?.dataset.filename ?? "";
423
+ anchor.click();
424
+ select.value = "";
425
+ });
426
+ this.#root.querySelector("[data-action=auto-advance]")?.addEventListener("change", (event) => this.player.setAutoAdvance(event.target.checked));
427
+ this.#root.querySelector("[data-action=resume]")?.addEventListener("click", () => {
428
+ if (this.#resumeTime !== void 0) this.player.seekTo(this.#resumeTime);
429
+ this.#resumeTime = void 0;
430
+ this.#render();
431
+ });
432
+ this.#root.querySelector("[data-action=dismiss-resume]")?.addEventListener("click", () => {
433
+ this.#resumeTime = void 0;
434
+ this.#render();
435
+ });
436
+ this.#bindPanelInteractions();
437
+ }
438
+ #bindPanelInteractions() {
439
+ this.#root.querySelectorAll("[data-canvas]").forEach((button) => button.addEventListener("click", () => {
440
+ const canvasId = button.dataset.canvas;
441
+ if (canvasId) this.player.selectCanvas(canvasId, { time: Number(button.dataset.time ?? 0), autoplay: true });
442
+ }));
443
+ this.#root.querySelector("[data-action=transcript]")?.addEventListener("change", (event) => {
444
+ this.#transcriptIndex = Number(event.target.value);
445
+ this.#transcriptCues = [];
446
+ this.#renderPanelOnly();
447
+ void this.#loadActiveTranscript();
448
+ });
449
+ this.#root.querySelector("[data-action=transcript-search]")?.addEventListener("input", (event) => {
450
+ this.#transcriptQuery = event.target.value;
451
+ const selection = event.target.selectionStart;
452
+ this.#renderPanelOnly();
453
+ const input = this.#root.querySelector("[data-action=transcript-search]");
454
+ input?.focus();
455
+ if (selection !== null) input?.setSelectionRange(selection, selection);
456
+ });
457
+ this.#root.querySelectorAll(".cue[data-start] button, .annotation[data-start] button").forEach((button) => button.addEventListener("click", () => {
458
+ const row = button.parentElement;
459
+ const start = Number(row?.dataset.start);
460
+ if (Number.isFinite(start)) {
461
+ this.player.seekTo(start);
462
+ playFromInteraction(this.player, this);
463
+ }
464
+ }));
465
+ this.#root.querySelectorAll("[data-search-time]").forEach((button) => button.addEventListener("click", () => {
466
+ const time = Number(button.dataset.searchTime);
467
+ if (Number.isFinite(time)) this.player.seekTo(time);
468
+ }));
469
+ }
470
+ #renderPanelOnly() {
471
+ const manifest = this.player.manifest;
472
+ const canvas = this.player.canvas;
473
+ if (!manifest || !canvas) return;
474
+ this.#root.querySelectorAll("[data-panel]").forEach((tab) => {
475
+ tab.setAttribute("aria-selected", String(tab.dataset.panel === this.#activePanel));
476
+ });
477
+ const content = this.#root.querySelector(".panel-content");
478
+ if (content) content.innerHTML = this.#panelHtml(manifest, canvas);
479
+ this.#root.querySelector(".search-markers")?.remove();
480
+ const toolbar = this.#root.querySelector(".player-toolbar");
481
+ const markers = this.#searchMarkersHtml();
482
+ if (toolbar && markers) toolbar.insertAdjacentHTML("beforebegin", markers);
483
+ this.#bindPanelInteractions();
484
+ }
485
+ async #loadActiveTranscript() {
486
+ const resource = this.player.canvas?.transcripts[this.#transcriptIndex];
487
+ if (!resource) return;
488
+ const token = ++this.#loadingToken;
489
+ try {
490
+ const transcriptOptions = {};
491
+ if (this.player.config.fetch) transcriptOptions.fetch = this.player.config.fetch;
492
+ if (this.player.config.requestInit) transcriptOptions.requestInit = this.player.config.requestInit;
493
+ if (this.player.config.transcript.showNotes !== void 0) transcriptOptions.showNotes = this.player.config.transcript.showNotes;
494
+ if (this.player.config.transcript.showMetadata !== void 0) transcriptOptions.showMetadata = this.player.config.transcript.showMetadata;
495
+ const cues = await loadTranscript(resource, transcriptOptions);
496
+ if (token !== this.#loadingToken) return;
497
+ this.#transcriptCues = cues;
498
+ if (this.#activePanel === "transcript") this.#renderPanelOnly();
499
+ } catch (cause) {
500
+ if (token !== this.#loadingToken) return;
501
+ const error = cause instanceof Error ? cause : new Error(String(cause));
502
+ this.dispatchEvent(new CustomEvent("av-error", { detail: { error }, bubbles: true, composed: true }));
503
+ this.#transcriptCues = [];
504
+ if (this.#activePanel === "transcript") this.#renderPanelOnly();
505
+ }
506
+ }
507
+ async #resolveSource() {
508
+ if (!this.player.config.resolveRequest) return;
509
+ const token = ++this.#sourceToken;
510
+ try {
511
+ const sourceUrl = await this.player.resolvedSourceUrl();
512
+ if (token !== this.#sourceToken || !sourceUrl) return;
513
+ this.#resolvedSourceUrl = sourceUrl;
514
+ const media = this.#root.querySelector("video, audio, hls-video");
515
+ if (media) media.setAttribute("src", sourceUrl);
516
+ } catch (cause) {
517
+ const error = cause instanceof Error ? cause : new Error(String(cause));
518
+ this.dispatchEvent(new CustomEvent("av-error", { detail: { error }, bubbles: true, composed: true }));
519
+ }
520
+ }
521
+ #syncTimedContent() {
522
+ const time = this.player.state.currentTime;
523
+ const rows = this.#root.querySelectorAll("[data-start][data-end]");
524
+ let active;
525
+ rows.forEach((row) => {
526
+ const start = Number(row.dataset.start);
527
+ const end = Number(row.dataset.end);
528
+ const isActive = Number.isFinite(start) && time >= start && (!Number.isFinite(end) || time < end);
529
+ row.querySelector("button")?.classList.toggle("active", isActive);
530
+ if (isActive) active = row;
531
+ });
532
+ if (active && this.player.config.transcript.autoScroll) {
533
+ active.scrollIntoView({ block: "nearest", behavior: "smooth" });
534
+ }
535
+ }
536
+ };
537
+ var defineMangoAVMedia = (tagName = "mango-av-media") => {
538
+ if (typeof customElements === "undefined") return MangoAVMediaElement;
539
+ if (!customElements.get(tagName)) customElements.define(tagName, MangoAVMediaElement);
540
+ return MangoAVMediaElement;
541
+ };
542
+
543
+ export {
544
+ MangoAVMediaElement,
545
+ defineMangoAVMedia
546
+ };
547
+ //# sourceMappingURL=chunk-GX7UU6V4.js.map