@mango-iiif/av 0.3.0 → 1.0.0
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.
- package/CHANGELOG.md +36 -0
- package/README.md +25 -13
- package/dist/annotations.d.ts +2 -2
- package/dist/annotations.js +4 -4
- package/dist/captions.d.ts +23 -0
- package/dist/captions.js +16 -0
- package/dist/captions.js.map +1 -0
- package/dist/chapters.d.ts +2 -2
- package/dist/chapters.js +3 -3
- package/dist/{chunk-4NOWGMI5.js → chunk-53ZN4Y4L.js} +28 -66
- package/dist/chunk-53ZN4Y4L.js.map +1 -0
- package/dist/chunk-AWLDINQR.js +73 -0
- package/dist/chunk-AWLDINQR.js.map +1 -0
- package/dist/chunk-BB7RE7EO.js +407 -0
- package/dist/chunk-BB7RE7EO.js.map +1 -0
- package/dist/chunk-BMMQFCD3.js +86 -0
- package/dist/chunk-BMMQFCD3.js.map +1 -0
- package/dist/chunk-IZYEGSGT.js +148 -0
- package/dist/chunk-IZYEGSGT.js.map +1 -0
- package/dist/chunk-JXZR5DL2.js +160 -0
- package/dist/chunk-JXZR5DL2.js.map +1 -0
- package/dist/{chunk-CIAQZ2EB.js → chunk-LMZIFVHN.js} +8 -4
- package/dist/chunk-LMZIFVHN.js.map +1 -0
- package/dist/chunk-TQNEVWUH.js +39 -0
- package/dist/chunk-TQNEVWUH.js.map +1 -0
- package/dist/chunk-XXM4G3K5.js +296 -0
- package/dist/chunk-XXM4G3K5.js.map +1 -0
- package/dist/chunk-YVI7J43Q.js +29 -0
- package/dist/chunk-YVI7J43Q.js.map +1 -0
- package/dist/chunk-Z2OF3RKT.js +152 -0
- package/dist/chunk-Z2OF3RKT.js.map +1 -0
- package/dist/{controller-SkwGkVE8.d.ts → controller-Ag3nFtwP.d.ts} +16 -24
- package/dist/{controller-element-CkXIA6w2.d.ts → controller-element-Cn5cxKnD.d.ts} +4 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/index.d.ts +6 -3
- package/dist/index.js +21 -14
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +3 -3
- package/dist/media.js +11 -5
- package/dist/media.js.map +1 -1
- package/dist/player.d.ts +8 -2
- package/dist/player.js +3 -5
- package/dist/player.js.map +1 -1
- package/dist/ramp-2XKALEGA.js +98 -0
- package/dist/ramp-2XKALEGA.js.map +1 -0
- package/dist/transcript-element.d.ts +2 -2
- package/dist/transcript-element.js +6 -5
- package/dist/transcript-element.js.map +1 -1
- package/docs/API.md +70 -32
- package/package.json +18 -11
- package/dist/chunk-4NOWGMI5.js.map +0 -1
- package/dist/chunk-4V6FME6B.js +0 -643
- package/dist/chunk-4V6FME6B.js.map +0 -1
- package/dist/chunk-C4H46HDS.js +0 -95
- package/dist/chunk-C4H46HDS.js.map +0 -1
- package/dist/chunk-CDLYT3KB.js +0 -17
- package/dist/chunk-CDLYT3KB.js.map +0 -1
- package/dist/chunk-CIAQZ2EB.js.map +0 -1
- package/dist/chunk-EHIKFRFV.js +0 -75
- package/dist/chunk-EHIKFRFV.js.map +0 -1
- package/dist/chunk-ELGGWWDS.js +0 -257
- package/dist/chunk-ELGGWWDS.js.map +0 -1
- package/dist/chunk-FVYEV2LO.js +0 -163
- package/dist/chunk-FVYEV2LO.js.map +0 -1
- package/dist/chunk-YX32ZQPR.js +0 -83
- package/dist/chunk-YX32ZQPR.js.map +0 -1
- package/dist/chunk-YX6L6CUF.js +0 -355
- package/dist/chunk-YX6L6CUF.js.map +0 -1
package/dist/chunk-ELGGWWDS.js
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
escapeHtml
|
|
3
|
-
} from "./chunk-4NOWGMI5.js";
|
|
4
|
-
|
|
5
|
-
// src/components/media-chrome.ts
|
|
6
|
-
var registration;
|
|
7
|
-
var languageRegistrations = /* @__PURE__ */ new Map();
|
|
8
|
-
var loadLanguage = (language) => {
|
|
9
|
-
const normalized = language.toLowerCase();
|
|
10
|
-
const code = normalized === "zh-cn" || normalized === "zh-tw" ? normalized : normalized.split("-")[0] ?? normalized;
|
|
11
|
-
if (languageRegistrations.has(code)) return languageRegistrations.get(code);
|
|
12
|
-
const pending = (() => {
|
|
13
|
-
switch (code) {
|
|
14
|
-
case "de":
|
|
15
|
-
return import("media-chrome/lang/de");
|
|
16
|
-
case "es":
|
|
17
|
-
return import("media-chrome/lang/es");
|
|
18
|
-
case "fr":
|
|
19
|
-
return import("media-chrome/lang/fr");
|
|
20
|
-
case "pt":
|
|
21
|
-
return import("media-chrome/lang/pt");
|
|
22
|
-
case "zh-cn":
|
|
23
|
-
return import("media-chrome/lang/zh-CN");
|
|
24
|
-
case "zh-tw":
|
|
25
|
-
return import("media-chrome/lang/zh-TW");
|
|
26
|
-
default:
|
|
27
|
-
return void 0;
|
|
28
|
-
}
|
|
29
|
-
})();
|
|
30
|
-
if (pending) languageRegistrations.set(code, pending);
|
|
31
|
-
return pending;
|
|
32
|
-
};
|
|
33
|
-
var ensureMediaChrome = async (language) => {
|
|
34
|
-
if (language) await loadLanguage(language);
|
|
35
|
-
registration ??= import("media-chrome");
|
|
36
|
-
await registration;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// src/components/audio-art.ts
|
|
40
|
-
var audioArtTranscriptHtml = (label, title, cues, enabled) => {
|
|
41
|
-
if (!enabled || cues.length === 0) return title ? `<span class="audio-art-title">${escapeHtml(title)}</span>` : "";
|
|
42
|
-
return `<div class="audio-art-transcript" aria-live="polite" aria-label="${escapeHtml(label)}">
|
|
43
|
-
${cues.map((cue) => `<div class="audio-art-cue" data-audio-cue data-start="${cue.start ?? ""}" data-end="${cue.end ?? cue.start ?? ""}">${cue.speaker ? `<span class="audio-art-speaker">${escapeHtml(cue.speaker)}</span>` : ""}<span>${escapeHtml(cue.text)}</span></div>`).join("")}
|
|
44
|
-
</div>`;
|
|
45
|
-
};
|
|
46
|
-
var resolveAudioVisualizerMode = (value) => value === true ? "pulse" : value || void 0;
|
|
47
|
-
var audioArtContentHtml = (label, title, cues, transcriptEnabled, visualizerMode) => `<div class="audio-art-text">${audioArtTranscriptHtml(label, title, cues, transcriptEnabled)}</div>${visualizerMode === "waveform" || visualizerMode === "spectrum" ? `<canvas class="audio-art-visualizer-canvas" aria-hidden="true"></canvas>` : ""}`;
|
|
48
|
-
var syncAudioArtTranscript = (root, time, instant = false) => {
|
|
49
|
-
const cues = [...root.querySelectorAll("[data-audio-cue]")];
|
|
50
|
-
if (cues.length === 0) return;
|
|
51
|
-
const transcript = root.querySelector(".audio-art-transcript");
|
|
52
|
-
if (instant && transcript) {
|
|
53
|
-
transcript.classList.add("seeking");
|
|
54
|
-
requestAnimationFrame(() => transcript.classList.remove("seeking"));
|
|
55
|
-
}
|
|
56
|
-
let activeIndex = cues.findIndex((cue) => {
|
|
57
|
-
const start = Number(cue.dataset.start);
|
|
58
|
-
const end = Number(cue.dataset.end);
|
|
59
|
-
return Number.isFinite(start) && time >= start && (!Number.isFinite(end) || time < end);
|
|
60
|
-
});
|
|
61
|
-
if (activeIndex < 0) {
|
|
62
|
-
for (let index = cues.length - 1; index >= 0; index -= 1) {
|
|
63
|
-
const cue = cues[index];
|
|
64
|
-
if (cue && Number.isFinite(Number(cue.dataset.start)) && time >= Number(cue.dataset.start)) {
|
|
65
|
-
activeIndex = index;
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (activeIndex < 0) activeIndex = 0;
|
|
71
|
-
cues.forEach((cue, index) => {
|
|
72
|
-
const offset = index - activeIndex;
|
|
73
|
-
cue.classList.toggle("active", offset === 0);
|
|
74
|
-
cue.classList.toggle("adjacent", Math.abs(offset) === 1);
|
|
75
|
-
cue.toggleAttribute("aria-hidden", offset !== 0);
|
|
76
|
-
cue.style.opacity = offset === 0 ? "1" : Math.abs(offset) === 1 ? ".2" : "0";
|
|
77
|
-
cue.style.transform = `translateY(calc(-50% + ${offset * 4.25}rem))`;
|
|
78
|
-
cue.style.visibility = Math.abs(offset) <= 1 ? "visible" : "hidden";
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
var AudioArtVisualizer = class {
|
|
82
|
-
#token = 0;
|
|
83
|
-
#context;
|
|
84
|
-
#source;
|
|
85
|
-
#analyzer;
|
|
86
|
-
#frame = 0;
|
|
87
|
-
#level = 0;
|
|
88
|
-
#media;
|
|
89
|
-
#resume;
|
|
90
|
-
connect(media, art, mode) {
|
|
91
|
-
this.disconnect();
|
|
92
|
-
const token = ++this.#token;
|
|
93
|
-
if (typeof AudioContext === "undefined") return;
|
|
94
|
-
try {
|
|
95
|
-
if (token !== this.#token || !media.isConnected || !art.isConnected) return;
|
|
96
|
-
const context = new AudioContext();
|
|
97
|
-
this.#context = context;
|
|
98
|
-
const source = context.createMediaElementSource(media);
|
|
99
|
-
this.#source = source;
|
|
100
|
-
const analyzer = context.createAnalyser();
|
|
101
|
-
this.#analyzer = analyzer;
|
|
102
|
-
analyzer.fftSize = 512;
|
|
103
|
-
analyzer.smoothingTimeConstant = 0.95;
|
|
104
|
-
source.connect(context.destination);
|
|
105
|
-
source.connect(analyzer);
|
|
106
|
-
this.#media = media;
|
|
107
|
-
const samples = new Uint8Array(analyzer.fftSize);
|
|
108
|
-
const frequencies = new Uint8Array(analyzer.frequencyBinCount);
|
|
109
|
-
const canvas = art.querySelector(".audio-art-visualizer-canvas");
|
|
110
|
-
const canvasContext = canvas?.getContext("2d");
|
|
111
|
-
const reducedMotion = typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
112
|
-
const resizeCanvas = () => {
|
|
113
|
-
if (!canvas || !canvasContext) return;
|
|
114
|
-
const scale = Math.min(2, devicePixelRatio || 1);
|
|
115
|
-
const width = Math.max(1, Math.round(art.clientWidth * scale));
|
|
116
|
-
const height = Math.max(1, Math.round(art.clientHeight * scale));
|
|
117
|
-
if (canvas.width !== width || canvas.height !== height) {
|
|
118
|
-
canvas.width = width;
|
|
119
|
-
canvas.height = height;
|
|
120
|
-
}
|
|
121
|
-
canvasContext.setTransform(scale, 0, 0, scale, 0, 0);
|
|
122
|
-
};
|
|
123
|
-
const drawWaveform = (strength) => {
|
|
124
|
-
if (!canvas || !canvasContext) return;
|
|
125
|
-
resizeCanvas();
|
|
126
|
-
const width = art.clientWidth;
|
|
127
|
-
const height = art.clientHeight;
|
|
128
|
-
canvasContext.clearRect(0, 0, width, height);
|
|
129
|
-
if (strength === 0) return;
|
|
130
|
-
const accent = getComputedStyle(art).getPropertyValue("--mango-av-accent").trim() || "#5b3fd6";
|
|
131
|
-
canvasContext.globalAlpha = Math.min(0.7, 0.18 + strength / 100);
|
|
132
|
-
canvasContext.strokeStyle = accent;
|
|
133
|
-
canvasContext.lineWidth = 1.5;
|
|
134
|
-
canvasContext.beginPath();
|
|
135
|
-
const amplitude = reducedMotion ? 0 : Math.min(height * 0.16, 5 + this.#level * height * 0.12);
|
|
136
|
-
for (let index = 0; index < samples.length; index += 1) {
|
|
137
|
-
const x = index / (samples.length - 1) * width;
|
|
138
|
-
const sample = (samples[index] - 128) / 128;
|
|
139
|
-
const y = height * 0.72 + sample * amplitude;
|
|
140
|
-
if (index === 0) canvasContext.moveTo(x, y);
|
|
141
|
-
else canvasContext.lineTo(x, y);
|
|
142
|
-
}
|
|
143
|
-
canvasContext.stroke();
|
|
144
|
-
canvasContext.globalAlpha = 1;
|
|
145
|
-
};
|
|
146
|
-
const drawSpectrum = (strength) => {
|
|
147
|
-
if (!canvas || !canvasContext) return;
|
|
148
|
-
resizeCanvas();
|
|
149
|
-
const width = art.clientWidth;
|
|
150
|
-
const height = art.clientHeight;
|
|
151
|
-
canvasContext.clearRect(0, 0, width, height);
|
|
152
|
-
if (strength === 0) return;
|
|
153
|
-
analyzer.getByteFrequencyData(frequencies);
|
|
154
|
-
const accent = getComputedStyle(art).getPropertyValue("--mango-av-accent").trim() || "#5b3fd6";
|
|
155
|
-
const barCount = 24;
|
|
156
|
-
const gap = 3;
|
|
157
|
-
const barWidth = Math.max(1, (width - gap * (barCount - 1)) / barCount);
|
|
158
|
-
canvasContext.fillStyle = accent;
|
|
159
|
-
canvasContext.globalAlpha = reducedMotion ? 0.2 : Math.min(0.55, 0.15 + strength / 120);
|
|
160
|
-
for (let index = 0; index < barCount; index += 1) {
|
|
161
|
-
const frequencyIndex = Math.floor((index / barCount) ** 1.7 * frequencies.length * 0.7);
|
|
162
|
-
const energy = reducedMotion ? 0.08 : (frequencies[frequencyIndex] ?? 0) / 255;
|
|
163
|
-
const barHeight = Math.max(1, energy * height * 0.32);
|
|
164
|
-
canvasContext.fillRect(index * (barWidth + gap), height - barHeight, barWidth, barHeight);
|
|
165
|
-
}
|
|
166
|
-
canvasContext.globalAlpha = 1;
|
|
167
|
-
};
|
|
168
|
-
const draw = () => {
|
|
169
|
-
if (token !== this.#token) return;
|
|
170
|
-
analyzer.getByteTimeDomainData(samples);
|
|
171
|
-
let sum = 0;
|
|
172
|
-
for (const sample of samples) {
|
|
173
|
-
const normalized = (sample - 128) / 128;
|
|
174
|
-
sum += normalized * normalized;
|
|
175
|
-
}
|
|
176
|
-
const rms = Math.sqrt(sum / samples.length);
|
|
177
|
-
const target = media.paused || rms < 0.012 ? 0 : Math.min(1, (rms - 0.012) * 5);
|
|
178
|
-
this.#level += (target - this.#level) * (target > this.#level ? 0.1 : 0.04);
|
|
179
|
-
if (target === 0 && this.#level < 0.01) this.#level = 0;
|
|
180
|
-
const strength = reducedMotion ? this.#level * 18 : this.#level === 0 ? 0 : Math.min(52, 12 + this.#level * 40);
|
|
181
|
-
art.style.setProperty("--mango-av-wave-rise", `${(8 - (reducedMotion ? 0 : this.#level * 6)).toFixed(2)}%`);
|
|
182
|
-
art.style.setProperty("--mango-av-wave-strength", `${strength.toFixed(1)}%`);
|
|
183
|
-
art.style.setProperty("--mango-av-wave-mid-strength", `${(strength * 0.45).toFixed(1)}%`);
|
|
184
|
-
art.style.setProperty("--mango-av-wave-edge-strength", `${(strength * 0.18).toFixed(1)}%`);
|
|
185
|
-
if (mode === "waveform") drawWaveform(strength);
|
|
186
|
-
else if (mode === "spectrum") drawSpectrum(strength);
|
|
187
|
-
this.#frame = requestAnimationFrame(draw);
|
|
188
|
-
};
|
|
189
|
-
draw();
|
|
190
|
-
const resume = () => void context.resume();
|
|
191
|
-
this.#resume = resume;
|
|
192
|
-
media.addEventListener("play", resume, { once: true });
|
|
193
|
-
if (!media.paused) resume();
|
|
194
|
-
} catch {
|
|
195
|
-
this.disconnect();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
disconnect() {
|
|
199
|
-
this.#token += 1;
|
|
200
|
-
if (typeof cancelAnimationFrame !== "undefined") cancelAnimationFrame(this.#frame);
|
|
201
|
-
this.#frame = 0;
|
|
202
|
-
this.#level = 0;
|
|
203
|
-
if (this.#media && this.#resume) this.#media.removeEventListener("play", this.#resume);
|
|
204
|
-
try {
|
|
205
|
-
this.#source?.disconnect();
|
|
206
|
-
} catch {
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
this.#analyzer?.disconnect();
|
|
210
|
-
} catch {
|
|
211
|
-
}
|
|
212
|
-
if (this.#context && this.#context.state !== "closed") void this.#context.close();
|
|
213
|
-
this.#analyzer = void 0;
|
|
214
|
-
this.#source = void 0;
|
|
215
|
-
this.#context = void 0;
|
|
216
|
-
this.#media = void 0;
|
|
217
|
-
this.#resume = void 0;
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// src/components/media-policy.ts
|
|
222
|
-
var isCrossOriginHttp = (sourceUrl) => {
|
|
223
|
-
try {
|
|
224
|
-
const url = new URL(sourceUrl, document.baseURI);
|
|
225
|
-
return (url.protocol === "http:" || url.protocol === "https:") && url.origin !== location.origin;
|
|
226
|
-
} catch {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
var resolveAudioMediaPolicy = (sourceUrl, configuredCrossOrigin, crossOriginExplicit, visualizerMode, fallback) => {
|
|
231
|
-
const crossOrigin = isCrossOriginHttp(sourceUrl);
|
|
232
|
-
const implicitCors = Boolean(visualizerMode) && crossOrigin && !crossOriginExplicit && !fallback;
|
|
233
|
-
const selectedCrossOrigin = implicitCors ? "anonymous" : configuredCrossOrigin || void 0;
|
|
234
|
-
const visualize = Boolean(visualizerMode) && !fallback && !(crossOrigin && !selectedCrossOrigin);
|
|
235
|
-
return {
|
|
236
|
-
...selectedCrossOrigin ? { crossOrigin: selectedCrossOrigin } : {},
|
|
237
|
-
implicitCors,
|
|
238
|
-
visualize
|
|
239
|
-
};
|
|
240
|
-
};
|
|
241
|
-
var applyMediaSource = (media, sourceUrl, crossOrigin) => {
|
|
242
|
-
if (crossOrigin) media.crossOrigin = crossOrigin;
|
|
243
|
-
else media.removeAttribute("crossorigin");
|
|
244
|
-
media.src = sourceUrl;
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
export {
|
|
248
|
-
ensureMediaChrome,
|
|
249
|
-
audioArtTranscriptHtml,
|
|
250
|
-
resolveAudioVisualizerMode,
|
|
251
|
-
audioArtContentHtml,
|
|
252
|
-
syncAudioArtTranscript,
|
|
253
|
-
AudioArtVisualizer,
|
|
254
|
-
resolveAudioMediaPolicy,
|
|
255
|
-
applyMediaSource
|
|
256
|
-
};
|
|
257
|
-
//# sourceMappingURL=chunk-ELGGWWDS.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/media-chrome.ts","../src/components/audio-art.ts","../src/components/media-policy.ts"],"sourcesContent":["let registration: Promise<unknown> | undefined;\nconst languageRegistrations = new Map<string, Promise<unknown>>();\n\nconst loadLanguage = (language: string): Promise<unknown> | undefined => {\n const normalized = language.toLowerCase();\n const code = normalized === 'zh-cn' || normalized === 'zh-tw'\n ? normalized\n : normalized.split('-')[0] ?? normalized;\n if (languageRegistrations.has(code)) return languageRegistrations.get(code);\n const pending = (() => {\n switch (code) {\n case 'de': return import('media-chrome/lang/de');\n case 'es': return import('media-chrome/lang/es');\n case 'fr': return import('media-chrome/lang/fr');\n case 'pt': return import('media-chrome/lang/pt');\n case 'zh-cn': return import('media-chrome/lang/zh-CN');\n case 'zh-tw': return import('media-chrome/lang/zh-TW');\n default: return undefined;\n }\n })();\n if (pending) languageRegistrations.set(code, pending);\n return pending;\n};\n\nexport const ensureMediaChrome = async (language?: string): Promise<void> => {\n if (language) await loadLanguage(language);\n registration ??= import('media-chrome');\n await registration;\n};\n","import type { AVAudioVisualizerMode, TranscriptCue } from '../types';\nimport { escapeHtml } from '../utils';\n\nexport const audioArtTranscriptHtml = (label: string, title: string, cues: TranscriptCue[], enabled: boolean): string => {\n if (!enabled || cues.length === 0) return title ? `<span class=\"audio-art-title\">${escapeHtml(title)}</span>` : '';\n return `<div class=\"audio-art-transcript\" aria-live=\"polite\" aria-label=\"${escapeHtml(label)}\">\n ${cues.map((cue) => `<div class=\"audio-art-cue\" data-audio-cue data-start=\"${cue.start ?? ''}\" data-end=\"${cue.end ?? cue.start ?? ''}\">${cue.speaker ? `<span class=\"audio-art-speaker\">${escapeHtml(cue.speaker)}</span>` : ''}<span>${escapeHtml(cue.text)}</span></div>`).join('')}\n </div>`;\n};\n\nexport const resolveAudioVisualizerMode = (\n value: boolean | AVAudioVisualizerMode | undefined,\n): AVAudioVisualizerMode | undefined => value === true ? 'pulse' : value || undefined;\n\nexport const audioArtContentHtml = (\n label: string,\n title: string,\n cues: TranscriptCue[],\n transcriptEnabled: boolean,\n visualizerMode?: AVAudioVisualizerMode,\n): string => `<div class=\"audio-art-text\">${audioArtTranscriptHtml(label, title, cues, transcriptEnabled)}</div>${visualizerMode === 'waveform' || visualizerMode === 'spectrum' ? `<canvas class=\"audio-art-visualizer-canvas\" aria-hidden=\"true\"></canvas>` : ''}`;\n\nexport const syncAudioArtTranscript = (root: ParentNode, time: number, instant = false): void => {\n const cues = [...root.querySelectorAll<HTMLElement>('[data-audio-cue]')];\n if (cues.length === 0) return;\n const transcript = root.querySelector<HTMLElement>('.audio-art-transcript');\n if (instant && transcript) {\n transcript.classList.add('seeking');\n requestAnimationFrame(() => transcript.classList.remove('seeking'));\n }\n\n let activeIndex = cues.findIndex((cue) => {\n const start = Number(cue.dataset.start);\n const end = Number(cue.dataset.end);\n return Number.isFinite(start) && time >= start && (!Number.isFinite(end) || time < end);\n });\n if (activeIndex < 0) {\n for (let index = cues.length - 1; index >= 0; index -= 1) {\n const cue = cues[index];\n if (cue && Number.isFinite(Number(cue.dataset.start)) && time >= Number(cue.dataset.start)) {\n activeIndex = index;\n break;\n }\n }\n }\n if (activeIndex < 0) activeIndex = 0;\n\n cues.forEach((cue, index) => {\n const offset = index - activeIndex;\n cue.classList.toggle('active', offset === 0);\n cue.classList.toggle('adjacent', Math.abs(offset) === 1);\n cue.toggleAttribute('aria-hidden', offset !== 0);\n cue.style.opacity = offset === 0 ? '1' : Math.abs(offset) === 1 ? '.2' : '0';\n cue.style.transform = `translateY(calc(-50% + ${offset * 4.25}rem))`;\n cue.style.visibility = Math.abs(offset) <= 1 ? 'visible' : 'hidden';\n });\n};\n\nexport class AudioArtVisualizer {\n #token = 0;\n #context: AudioContext | undefined;\n #source: MediaElementAudioSourceNode | undefined;\n #analyzer: AnalyserNode | undefined;\n #frame = 0;\n #level = 0;\n #media: HTMLMediaElement | undefined;\n #resume: (() => void) | undefined;\n\n connect(media: HTMLMediaElement, art: HTMLElement, mode: AVAudioVisualizerMode): void {\n this.disconnect();\n const token = ++this.#token;\n if (typeof AudioContext === 'undefined') return;\n\n try {\n if (token !== this.#token || !media.isConnected || !art.isConnected) return;\n const context = new AudioContext();\n this.#context = context;\n const source = context.createMediaElementSource(media);\n this.#source = source;\n const analyzer = context.createAnalyser();\n this.#analyzer = analyzer;\n analyzer.fftSize = 512;\n analyzer.smoothingTimeConstant = .95;\n source.connect(context.destination);\n source.connect(analyzer);\n this.#media = media;\n const samples = new Uint8Array(analyzer.fftSize);\n const frequencies = new Uint8Array(analyzer.frequencyBinCount);\n const canvas = art.querySelector<HTMLCanvasElement>('.audio-art-visualizer-canvas');\n const canvasContext = canvas?.getContext('2d');\n const reducedMotion = typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n const resizeCanvas = () => {\n if (!canvas || !canvasContext) return;\n const scale = Math.min(2, devicePixelRatio || 1);\n const width = Math.max(1, Math.round(art.clientWidth * scale));\n const height = Math.max(1, Math.round(art.clientHeight * scale));\n if (canvas.width !== width || canvas.height !== height) {\n canvas.width = width;\n canvas.height = height;\n }\n canvasContext.setTransform(scale, 0, 0, scale, 0, 0);\n };\n const drawWaveform = (strength: number) => {\n if (!canvas || !canvasContext) return;\n resizeCanvas();\n const width = art.clientWidth;\n const height = art.clientHeight;\n canvasContext.clearRect(0, 0, width, height);\n if (strength === 0) return;\n const accent = getComputedStyle(art).getPropertyValue('--mango-av-accent').trim() || '#5b3fd6';\n canvasContext.globalAlpha = Math.min(.7, .18 + strength / 100);\n canvasContext.strokeStyle = accent;\n canvasContext.lineWidth = 1.5;\n canvasContext.beginPath();\n const amplitude = reducedMotion ? 0 : Math.min(height * .16, 5 + this.#level * height * .12);\n for (let index = 0; index < samples.length; index += 1) {\n const x = (index / (samples.length - 1)) * width;\n const sample = (samples[index]! - 128) / 128;\n const y = height * .72 + sample * amplitude;\n if (index === 0) canvasContext.moveTo(x, y);\n else canvasContext.lineTo(x, y);\n }\n canvasContext.stroke();\n canvasContext.globalAlpha = 1;\n };\n const drawSpectrum = (strength: number) => {\n if (!canvas || !canvasContext) return;\n resizeCanvas();\n const width = art.clientWidth;\n const height = art.clientHeight;\n canvasContext.clearRect(0, 0, width, height);\n if (strength === 0) return;\n analyzer.getByteFrequencyData(frequencies);\n const accent = getComputedStyle(art).getPropertyValue('--mango-av-accent').trim() || '#5b3fd6';\n const barCount = 24;\n const gap = 3;\n const barWidth = Math.max(1, (width - gap * (barCount - 1)) / barCount);\n canvasContext.fillStyle = accent;\n canvasContext.globalAlpha = reducedMotion ? .2 : Math.min(.55, .15 + strength / 120);\n for (let index = 0; index < barCount; index += 1) {\n const frequencyIndex = Math.floor((index / barCount) ** 1.7 * frequencies.length * .7);\n const energy = reducedMotion ? .08 : (frequencies[frequencyIndex] ?? 0) / 255;\n const barHeight = Math.max(1, energy * height * .32);\n canvasContext.fillRect(index * (barWidth + gap), height - barHeight, barWidth, barHeight);\n }\n canvasContext.globalAlpha = 1;\n };\n const draw = () => {\n if (token !== this.#token) return;\n analyzer.getByteTimeDomainData(samples);\n let sum = 0;\n for (const sample of samples) {\n const normalized = (sample - 128) / 128;\n sum += normalized * normalized;\n }\n const rms = Math.sqrt(sum / samples.length);\n const target = media.paused || rms < .012 ? 0 : Math.min(1, (rms - .012) * 5);\n this.#level += (target - this.#level) * (target > this.#level ? .1 : .04);\n if (target === 0 && this.#level < .01) this.#level = 0;\n const strength = reducedMotion\n ? this.#level * 18\n : this.#level === 0 ? 0 : Math.min(52, 12 + this.#level * 40);\n art.style.setProperty('--mango-av-wave-rise', `${(8 - (reducedMotion ? 0 : this.#level * 6)).toFixed(2)}%`);\n art.style.setProperty('--mango-av-wave-strength', `${strength.toFixed(1)}%`);\n art.style.setProperty('--mango-av-wave-mid-strength', `${(strength * .45).toFixed(1)}%`);\n art.style.setProperty('--mango-av-wave-edge-strength', `${(strength * .18).toFixed(1)}%`);\n if (mode === 'waveform') drawWaveform(strength);\n else if (mode === 'spectrum') drawSpectrum(strength);\n this.#frame = requestAnimationFrame(draw);\n };\n draw();\n const resume = () => void context.resume();\n this.#resume = resume;\n media.addEventListener('play', resume, { once: true });\n if (!media.paused) resume();\n } catch {\n this.disconnect();\n }\n }\n\n disconnect(): void {\n this.#token += 1;\n if (typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(this.#frame);\n this.#frame = 0;\n this.#level = 0;\n if (this.#media && this.#resume) this.#media.removeEventListener('play', this.#resume);\n try { this.#source?.disconnect(); } catch { /* Already disconnected. */ }\n try { this.#analyzer?.disconnect(); } catch { /* Already disconnected. */ }\n if (this.#context && this.#context.state !== 'closed') void this.#context.close();\n this.#analyzer = undefined;\n this.#source = undefined;\n this.#context = undefined;\n this.#media = undefined;\n this.#resume = undefined;\n }\n}\n","import type { AVAudioVisualizerMode } from '../types';\n\nexport interface AudioMediaPolicy {\n crossOrigin?: 'anonymous' | 'use-credentials';\n implicitCors: boolean;\n visualize: boolean;\n}\n\nconst isCrossOriginHttp = (sourceUrl: string): boolean => {\n try {\n const url = new URL(sourceUrl, document.baseURI);\n return (url.protocol === 'http:' || url.protocol === 'https:') && url.origin !== location.origin;\n } catch {\n return false;\n }\n};\n\nexport const resolveAudioMediaPolicy = (\n sourceUrl: string,\n configuredCrossOrigin: 'anonymous' | 'use-credentials' | '',\n crossOriginExplicit: boolean,\n visualizerMode: AVAudioVisualizerMode | undefined,\n fallback: boolean,\n): AudioMediaPolicy => {\n const crossOrigin = isCrossOriginHttp(sourceUrl);\n const implicitCors = Boolean(visualizerMode) && crossOrigin && !crossOriginExplicit && !fallback;\n const selectedCrossOrigin = implicitCors ? 'anonymous' : configuredCrossOrigin || undefined;\n // Explicit no-CORS loading is respected, but a cross-origin element loaded that\n // way cannot safely be connected to Web Audio without risking silent output.\n const visualize = Boolean(visualizerMode) && !fallback && !(crossOrigin && !selectedCrossOrigin);\n return {\n ...(selectedCrossOrigin ? { crossOrigin: selectedCrossOrigin } : {}),\n implicitCors,\n visualize,\n };\n};\n\nexport const applyMediaSource = (\n media: HTMLMediaElement,\n sourceUrl: string,\n crossOrigin?: 'anonymous' | 'use-credentials',\n): void => {\n if (crossOrigin) media.crossOrigin = crossOrigin;\n else media.removeAttribute('crossorigin');\n media.src = sourceUrl;\n};\n"],"mappings":"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","names":[]}
|
package/dist/chunk-FVYEV2LO.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
playFromInteraction
|
|
3
|
-
} from "./chunk-CDLYT3KB.js";
|
|
4
|
-
import {
|
|
5
|
-
AVControllerElement,
|
|
6
|
-
componentTheme
|
|
7
|
-
} from "./chunk-C4H46HDS.js";
|
|
8
|
-
import {
|
|
9
|
-
loadTranscript
|
|
10
|
-
} from "./chunk-CIAQZ2EB.js";
|
|
11
|
-
import {
|
|
12
|
-
escapeHtml,
|
|
13
|
-
formatTime,
|
|
14
|
-
resolveStrings
|
|
15
|
-
} from "./chunk-4NOWGMI5.js";
|
|
16
|
-
|
|
17
|
-
// src/components/mango-av-transcript.ts
|
|
18
|
-
var styles = `${componentTheme}
|
|
19
|
-
.transcript { background: var(--mango-av-background); border: 1px solid var(--mango-av-border); border-radius: var(--mango-av-radius); padding: .75rem; }
|
|
20
|
-
h2 { font-size: 1rem; margin: 0 0 .5rem; }
|
|
21
|
-
.tools { display: flex; flex-wrap: wrap; gap: .45rem; margin-bottom: .75rem; }
|
|
22
|
-
.tools input, .tools select { border: 1px solid var(--mango-av-border); border-radius: .35rem; min-width: 0; padding: .45rem; }
|
|
23
|
-
.tools input { flex: 1; }
|
|
24
|
-
.tools a { align-content: center; color: var(--mango-av-accent); }
|
|
25
|
-
ol { list-style: none; margin: 0; max-height: var(--mango-av-transcript-height, 34rem); overflow: auto; padding: 0; }
|
|
26
|
-
button { background: transparent; border: 0; border-radius: .35rem; color: inherit; display: grid; gap: .2rem; padding: .5rem; text-align: left; width: 100%; }
|
|
27
|
-
button:hover { background: var(--mango-av-surface); }
|
|
28
|
-
button.active { background: color-mix(in srgb, var(--mango-av-accent) 14%, transparent); box-shadow: inset 3px 0 var(--mango-av-accent); }
|
|
29
|
-
mark { background: #ffe770; }
|
|
30
|
-
.speaker { font-weight: 700; }
|
|
31
|
-
`;
|
|
32
|
-
var highlight = (text, query) => {
|
|
33
|
-
const escaped = escapeHtml(text);
|
|
34
|
-
if (!query) return escaped;
|
|
35
|
-
const safeQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
-
return escaped.replace(new RegExp(`(${safeQuery})`, "gi"), "<mark>$1</mark>");
|
|
37
|
-
};
|
|
38
|
-
var MangoAVTranscriptElement = class extends AVControllerElement {
|
|
39
|
-
#root;
|
|
40
|
-
#cues = [];
|
|
41
|
-
#transcriptIndex = 0;
|
|
42
|
-
#query = "";
|
|
43
|
-
#loadToken = 0;
|
|
44
|
-
constructor() {
|
|
45
|
-
super();
|
|
46
|
-
this.#root = this.attachShadow({ mode: "open" });
|
|
47
|
-
}
|
|
48
|
-
controllerChanged() {
|
|
49
|
-
this.#cues = [];
|
|
50
|
-
this.#transcriptIndex = 0;
|
|
51
|
-
this.#render();
|
|
52
|
-
void this.#loadTranscript();
|
|
53
|
-
}
|
|
54
|
-
handleControllerEvent(type, _event) {
|
|
55
|
-
if (type === "av-ready" || type === "av-canvaschange") {
|
|
56
|
-
this.#cues = [];
|
|
57
|
-
this.#transcriptIndex = 0;
|
|
58
|
-
this.#render();
|
|
59
|
-
void this.#loadTranscript();
|
|
60
|
-
} else if (type === "av-configchange") {
|
|
61
|
-
this.#render();
|
|
62
|
-
} else if (type === "av-timeupdate" || type === "av-seek" || type === "av-statechange") {
|
|
63
|
-
this.#sync();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
#render() {
|
|
67
|
-
const canvas = this.controller.canvas;
|
|
68
|
-
const transcripts = canvas?.transcripts ?? [];
|
|
69
|
-
const resource = transcripts[this.#transcriptIndex];
|
|
70
|
-
const query = this.#query.trim();
|
|
71
|
-
const ui = resolveStrings(this.controller.config.strings);
|
|
72
|
-
const cues = query ? this.#cues.filter((cue) => cue.text.toLowerCase().includes(query.toLowerCase())) : this.#cues;
|
|
73
|
-
this.#root.innerHTML = `<style>${styles}</style><section class="transcript" aria-label="${escapeHtml(ui.transcript)}">
|
|
74
|
-
<slot name="heading"><h2>${escapeHtml(ui.transcript)}</h2></slot>
|
|
75
|
-
${!canvas ? `<p class="empty">${escapeHtml(ui.noManifest)}</p>` : transcripts.length === 0 ? `<p class="empty">${escapeHtml(ui.noTranscript)}</p>` : `
|
|
76
|
-
<div class="tools">
|
|
77
|
-
${transcripts.length > 1 ? `<select data-action="transcript" aria-label="${escapeHtml(ui.transcript)}">${transcripts.map((item, index) => `<option value="${index}" ${index === this.#transcriptIndex ? "selected" : ""}>${escapeHtml(item.label)}</option>`).join("")}</select>` : ""}
|
|
78
|
-
${this.controller.config.transcript.search ? `<input type="search" data-action="search" value="${escapeHtml(this.#query)}" placeholder="${escapeHtml(ui.searchTranscript)}" aria-label="${escapeHtml(ui.searchTranscript)}">` : ""}
|
|
79
|
-
${this.controller.config.transcript.download && resource?.src ? `<a href="${escapeHtml(resource.src)}" download>${escapeHtml(ui.download)}</a>` : ""}
|
|
80
|
-
</div>
|
|
81
|
-
${this.#cues.length === 0 ? `<p class="empty">${escapeHtml(ui.loadingTranscript)}</p>` : `<ol>${cues.map((cue) => `<li data-cue-id="${escapeHtml(cue.id)}" ${cue.start !== void 0 ? `data-start="${cue.start}" data-end="${cue.end ?? cue.start}"` : ""}>
|
|
82
|
-
<button type="button" ${cue.start === void 0 ? "disabled" : ""}>
|
|
83
|
-
${cue.start !== void 0 ? `<span class="time">${formatTime(cue.start)}</span>` : ""}
|
|
84
|
-
${cue.speaker ? `<span class="speaker">${escapeHtml(cue.speaker)}</span>` : ""}
|
|
85
|
-
<span>${highlight(cue.text, query)}</span>
|
|
86
|
-
</button>
|
|
87
|
-
</li>`).join("")}</ol>`}
|
|
88
|
-
`}
|
|
89
|
-
</section>`;
|
|
90
|
-
this.#bind();
|
|
91
|
-
this.#sync();
|
|
92
|
-
}
|
|
93
|
-
#bind() {
|
|
94
|
-
this.#root.querySelector("[data-action=transcript]")?.addEventListener("change", (event) => {
|
|
95
|
-
this.#transcriptIndex = Number(event.target.value);
|
|
96
|
-
this.#cues = [];
|
|
97
|
-
this.#render();
|
|
98
|
-
void this.#loadTranscript();
|
|
99
|
-
});
|
|
100
|
-
this.#root.querySelector("[data-action=search]")?.addEventListener("input", (event) => {
|
|
101
|
-
const input = event.target;
|
|
102
|
-
const selection = input.selectionStart;
|
|
103
|
-
this.#query = input.value;
|
|
104
|
-
this.#render();
|
|
105
|
-
const next = this.#root.querySelector("[data-action=search]");
|
|
106
|
-
next?.focus();
|
|
107
|
-
if (selection !== null) next?.setSelectionRange(selection, selection);
|
|
108
|
-
this.dispatchEvent(new CustomEvent("av-transcriptsearch", { detail: { query: this.#query }, bubbles: true, composed: true }));
|
|
109
|
-
});
|
|
110
|
-
this.#root.querySelectorAll("[data-start] button").forEach((button) => button.addEventListener("click", () => {
|
|
111
|
-
const time = Number(button.parentElement?.dataset.start);
|
|
112
|
-
if (!Number.isFinite(time)) return;
|
|
113
|
-
this.dispatchEvent(new CustomEvent("av-transcriptcueselect", { detail: { time }, bubbles: true, composed: true }));
|
|
114
|
-
this.controller.seekTo(time);
|
|
115
|
-
playFromInteraction(this.controller, this);
|
|
116
|
-
}));
|
|
117
|
-
}
|
|
118
|
-
async #loadTranscript() {
|
|
119
|
-
const resource = this.controller.canvas?.transcripts[this.#transcriptIndex];
|
|
120
|
-
if (!resource) return;
|
|
121
|
-
const token = ++this.#loadToken;
|
|
122
|
-
const options = {};
|
|
123
|
-
if (this.controller.config.fetch) options.fetch = this.controller.config.fetch;
|
|
124
|
-
if (this.controller.config.requestInit) options.requestInit = this.controller.config.requestInit;
|
|
125
|
-
if (this.controller.config.transcript.showNotes !== void 0) options.showNotes = this.controller.config.transcript.showNotes;
|
|
126
|
-
if (this.controller.config.transcript.showMetadata !== void 0) options.showMetadata = this.controller.config.transcript.showMetadata;
|
|
127
|
-
try {
|
|
128
|
-
const cues = await loadTranscript(resource, options);
|
|
129
|
-
if (token !== this.#loadToken) return;
|
|
130
|
-
this.#cues = cues;
|
|
131
|
-
this.#render();
|
|
132
|
-
} catch (cause) {
|
|
133
|
-
if (token !== this.#loadToken) return;
|
|
134
|
-
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
135
|
-
this.dispatchEvent(new CustomEvent("av-error", { detail: { error }, bubbles: true, composed: true }));
|
|
136
|
-
this.#cues = [];
|
|
137
|
-
this.#render();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
#sync() {
|
|
141
|
-
const time = this.controller.state.currentTime;
|
|
142
|
-
let active;
|
|
143
|
-
this.#root.querySelectorAll("[data-start][data-end]").forEach((row) => {
|
|
144
|
-
const start = Number(row.dataset.start);
|
|
145
|
-
const end = Number(row.dataset.end);
|
|
146
|
-
const isActive = Number.isFinite(start) && time >= start && (!Number.isFinite(end) || time < end);
|
|
147
|
-
row.querySelector("button")?.classList.toggle("active", isActive);
|
|
148
|
-
if (isActive) active = row;
|
|
149
|
-
});
|
|
150
|
-
if (active && this.controller.config.transcript.autoScroll) active.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
var defineMangoAVTranscript = (tagName = "mango-av-transcript") => {
|
|
154
|
-
if (typeof customElements === "undefined") return MangoAVTranscriptElement;
|
|
155
|
-
if (!customElements.get(tagName)) customElements.define(tagName, MangoAVTranscriptElement);
|
|
156
|
-
return MangoAVTranscriptElement;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
export {
|
|
160
|
-
MangoAVTranscriptElement,
|
|
161
|
-
defineMangoAVTranscript
|
|
162
|
-
};
|
|
163
|
-
//# sourceMappingURL=chunk-FVYEV2LO.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/mango-av-transcript.ts"],"sourcesContent":["import { AVControllerElement } from './controller-element';\nimport { componentTheme } from './component-styles';\nimport { loadTranscript } from '../transcript';\nimport { playFromInteraction } from './playback';\nimport type { AVPlayerEventMap, TranscriptCue } from '../types';\nimport { escapeHtml, formatTime, resolveStrings } from '../utils';\n\nconst styles = `${componentTheme}\n .transcript { background: var(--mango-av-background); border: 1px solid var(--mango-av-border); border-radius: var(--mango-av-radius); padding: .75rem; }\n h2 { font-size: 1rem; margin: 0 0 .5rem; }\n .tools { display: flex; flex-wrap: wrap; gap: .45rem; margin-bottom: .75rem; }\n .tools input, .tools select { border: 1px solid var(--mango-av-border); border-radius: .35rem; min-width: 0; padding: .45rem; }\n .tools input { flex: 1; }\n .tools a { align-content: center; color: var(--mango-av-accent); }\n ol { list-style: none; margin: 0; max-height: var(--mango-av-transcript-height, 34rem); overflow: auto; padding: 0; }\n button { background: transparent; border: 0; border-radius: .35rem; color: inherit; display: grid; gap: .2rem; padding: .5rem; text-align: left; width: 100%; }\n button:hover { background: var(--mango-av-surface); }\n button.active { background: color-mix(in srgb, var(--mango-av-accent) 14%, transparent); box-shadow: inset 3px 0 var(--mango-av-accent); }\n mark { background: #ffe770; }\n .speaker { font-weight: 700; }\n`;\n\nconst highlight = (text: string, query: string): string => {\n const escaped = escapeHtml(text);\n if (!query) return escaped;\n const safeQuery = query.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return escaped.replace(new RegExp(`(${safeQuery})`, 'gi'), '<mark>$1</mark>');\n};\n\nexport class MangoAVTranscriptElement extends AVControllerElement {\n #root: ShadowRoot;\n #cues: TranscriptCue[] = [];\n #transcriptIndex = 0;\n #query = '';\n #loadToken = 0;\n\n constructor() {\n super();\n this.#root = this.attachShadow({ mode: 'open' });\n }\n\n protected controllerChanged(): void {\n this.#cues = [];\n this.#transcriptIndex = 0;\n this.#render();\n void this.#loadTranscript();\n }\n\n protected handleControllerEvent<K extends keyof AVPlayerEventMap>(type: K, _event: CustomEvent<AVPlayerEventMap[K]>): void {\n if (type === 'av-ready' || type === 'av-canvaschange') {\n this.#cues = [];\n this.#transcriptIndex = 0;\n this.#render();\n void this.#loadTranscript();\n } else if (type === 'av-configchange') {\n this.#render();\n } else if (type === 'av-timeupdate' || type === 'av-seek' || type === 'av-statechange') {\n this.#sync();\n }\n }\n\n #render(): void {\n const canvas = this.controller.canvas;\n const transcripts = canvas?.transcripts ?? [];\n const resource = transcripts[this.#transcriptIndex];\n const query = this.#query.trim();\n const ui = resolveStrings(this.controller.config.strings);\n const cues = query ? this.#cues.filter((cue) => cue.text.toLowerCase().includes(query.toLowerCase())) : this.#cues;\n this.#root.innerHTML = `<style>${styles}</style><section class=\"transcript\" aria-label=\"${escapeHtml(ui.transcript)}\">\n <slot name=\"heading\"><h2>${escapeHtml(ui.transcript)}</h2></slot>\n ${!canvas ? `<p class=\"empty\">${escapeHtml(ui.noManifest)}</p>` : transcripts.length === 0 ? `<p class=\"empty\">${escapeHtml(ui.noTranscript)}</p>` : `\n <div class=\"tools\">\n ${transcripts.length > 1 ? `<select data-action=\"transcript\" aria-label=\"${escapeHtml(ui.transcript)}\">${transcripts.map((item, index) => `<option value=\"${index}\" ${index === this.#transcriptIndex ? 'selected' : ''}>${escapeHtml(item.label)}</option>`).join('')}</select>` : ''}\n ${this.controller.config.transcript.search ? `<input type=\"search\" data-action=\"search\" value=\"${escapeHtml(this.#query)}\" placeholder=\"${escapeHtml(ui.searchTranscript)}\" aria-label=\"${escapeHtml(ui.searchTranscript)}\">` : ''}\n ${this.controller.config.transcript.download && resource?.src ? `<a href=\"${escapeHtml(resource.src)}\" download>${escapeHtml(ui.download)}</a>` : ''}\n </div>\n ${this.#cues.length === 0 ? `<p class=\"empty\">${escapeHtml(ui.loadingTranscript)}</p>` : `<ol>${cues.map((cue) => `<li data-cue-id=\"${escapeHtml(cue.id)}\" ${cue.start !== undefined ? `data-start=\"${cue.start}\" data-end=\"${cue.end ?? cue.start}\"` : ''}>\n <button type=\"button\" ${cue.start === undefined ? 'disabled' : ''}>\n ${cue.start !== undefined ? `<span class=\"time\">${formatTime(cue.start)}</span>` : ''}\n ${cue.speaker ? `<span class=\"speaker\">${escapeHtml(cue.speaker)}</span>` : ''}\n <span>${highlight(cue.text, query)}</span>\n </button>\n </li>`).join('')}</ol>`}\n `}\n </section>`;\n this.#bind();\n this.#sync();\n }\n\n #bind(): void {\n this.#root.querySelector<HTMLSelectElement>('[data-action=transcript]')?.addEventListener('change', (event) => {\n this.#transcriptIndex = Number((event.target as HTMLSelectElement).value);\n this.#cues = [];\n this.#render();\n void this.#loadTranscript();\n });\n this.#root.querySelector<HTMLInputElement>('[data-action=search]')?.addEventListener('input', (event) => {\n const input = event.target as HTMLInputElement;\n const selection = input.selectionStart;\n this.#query = input.value;\n this.#render();\n const next = this.#root.querySelector<HTMLInputElement>('[data-action=search]');\n next?.focus();\n if (selection !== null) next?.setSelectionRange(selection, selection);\n this.dispatchEvent(new CustomEvent('av-transcriptsearch', { detail: { query: this.#query }, bubbles: true, composed: true }));\n });\n this.#root.querySelectorAll<HTMLElement>('[data-start] button').forEach((button) => button.addEventListener('click', () => {\n const time = Number(button.parentElement?.dataset.start);\n if (!Number.isFinite(time)) return;\n this.dispatchEvent(new CustomEvent('av-transcriptcueselect', { detail: { time }, bubbles: true, composed: true }));\n this.controller.seekTo(time);\n playFromInteraction(this.controller, this);\n }));\n }\n\n async #loadTranscript(): Promise<void> {\n const resource = this.controller.canvas?.transcripts[this.#transcriptIndex];\n if (!resource) return;\n const token = ++this.#loadToken;\n const options: Parameters<typeof loadTranscript>[1] = {};\n if (this.controller.config.fetch) options.fetch = this.controller.config.fetch;\n if (this.controller.config.requestInit) options.requestInit = this.controller.config.requestInit;\n if (this.controller.config.transcript.showNotes !== undefined) options.showNotes = this.controller.config.transcript.showNotes;\n if (this.controller.config.transcript.showMetadata !== undefined) options.showMetadata = this.controller.config.transcript.showMetadata;\n try {\n const cues = await loadTranscript(resource, options);\n if (token !== this.#loadToken) return;\n this.#cues = cues;\n this.#render();\n } catch (cause) {\n if (token !== this.#loadToken) return;\n const error = cause instanceof Error ? cause : new Error(String(cause));\n this.dispatchEvent(new CustomEvent('av-error', { detail: { error }, bubbles: true, composed: true }));\n this.#cues = [];\n this.#render();\n }\n }\n\n #sync(): void {\n const time = this.controller.state.currentTime;\n let active: HTMLElement | undefined;\n this.#root.querySelectorAll<HTMLElement>('[data-start][data-end]').forEach((row) => {\n const start = Number(row.dataset.start);\n const end = Number(row.dataset.end);\n const isActive = Number.isFinite(start) && time >= start && (!Number.isFinite(end) || time < end);\n row.querySelector('button')?.classList.toggle('active', isActive);\n if (isActive) active = row;\n });\n if (active && this.controller.config.transcript.autoScroll) active.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n }\n}\n\nexport const defineMangoAVTranscript = (tagName = 'mango-av-transcript'): typeof MangoAVTranscriptElement => {\n if (typeof customElements === 'undefined') return MangoAVTranscriptElement;\n if (!customElements.get(tagName)) customElements.define(tagName, MangoAVTranscriptElement);\n return MangoAVTranscriptElement;\n};\n"],"mappings":"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","names":[]}
|
package/dist/chunk-YX32ZQPR.js
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
playFromInteraction
|
|
3
|
-
} from "./chunk-CDLYT3KB.js";
|
|
4
|
-
import {
|
|
5
|
-
AVControllerElement,
|
|
6
|
-
componentTheme
|
|
7
|
-
} from "./chunk-C4H46HDS.js";
|
|
8
|
-
import {
|
|
9
|
-
escapeHtml,
|
|
10
|
-
formatTime,
|
|
11
|
-
resolveStrings
|
|
12
|
-
} from "./chunk-4NOWGMI5.js";
|
|
13
|
-
|
|
14
|
-
// src/components/mango-av-annotations.ts
|
|
15
|
-
var styles = `${componentTheme}
|
|
16
|
-
.annotations { background: var(--mango-av-background); border: 1px solid var(--mango-av-border); border-radius: var(--mango-av-radius); padding: .75rem; }
|
|
17
|
-
h2 { font-size: 1rem; margin: 0 0 .5rem; }
|
|
18
|
-
ol { list-style: none; margin: 0; padding: 0; }
|
|
19
|
-
button { background: transparent; border: 0; border-radius: .35rem; color: inherit; display: grid; gap: .2rem; padding: .5rem; text-align: left; width: 100%; }
|
|
20
|
-
button:hover { background: var(--mango-av-surface); }
|
|
21
|
-
button.active { background: color-mix(in srgb, var(--mango-av-accent) 14%, transparent); box-shadow: inset 3px 0 var(--mango-av-accent); }
|
|
22
|
-
.tags { display: flex; flex-wrap: wrap; gap: .25rem; }
|
|
23
|
-
.tag { background: var(--mango-av-surface); border-radius: 999px; font-size: .75rem; padding: .15rem .45rem; }
|
|
24
|
-
`;
|
|
25
|
-
var annotationsHtml = (annotations, ui) => {
|
|
26
|
-
if (annotations.length === 0) return `<p class="empty">${escapeHtml(ui.noAnnotations)}</p>`;
|
|
27
|
-
return `<ol>${annotations.map((annotation) => `<li data-start="${annotation.time?.start ?? ""}" data-end="${annotation.time?.end ?? annotation.time?.start ?? ""}">
|
|
28
|
-
<button type="button" ${annotation.time ? "" : "disabled"}>
|
|
29
|
-
${annotation.time ? `<span class="time">${formatTime(annotation.time.start)}</span>` : ""}
|
|
30
|
-
<span>${escapeHtml(annotation.text)}</span>
|
|
31
|
-
${annotation.tags.length > 0 ? `<span class="tags">${annotation.tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}</span>` : ""}
|
|
32
|
-
</button>
|
|
33
|
-
</li>`).join("")}</ol>`;
|
|
34
|
-
};
|
|
35
|
-
var MangoAVAnnotationsElement = class extends AVControllerElement {
|
|
36
|
-
#root;
|
|
37
|
-
constructor() {
|
|
38
|
-
super();
|
|
39
|
-
this.#root = this.attachShadow({ mode: "open" });
|
|
40
|
-
}
|
|
41
|
-
controllerChanged() {
|
|
42
|
-
this.#render();
|
|
43
|
-
}
|
|
44
|
-
handleControllerEvent(type, _event) {
|
|
45
|
-
if (type === "av-ready" || type === "av-canvaschange" || type === "av-annotationschange" || type === "av-configchange") this.#render();
|
|
46
|
-
else if (type === "av-timeupdate" || type === "av-seek" || type === "av-statechange") this.#sync();
|
|
47
|
-
}
|
|
48
|
-
#render() {
|
|
49
|
-
const canvas = this.controller.canvas;
|
|
50
|
-
const ui = resolveStrings(this.controller.config.strings);
|
|
51
|
-
this.#root.innerHTML = `<style>${styles}</style><section class="annotations" aria-label="${escapeHtml(ui.annotations)}">
|
|
52
|
-
<slot name="heading"><h2>${escapeHtml(ui.annotations)}</h2></slot>
|
|
53
|
-
${canvas ? annotationsHtml(canvas.annotations, ui) : `<p class="empty">${escapeHtml(ui.noManifest)}</p>`}
|
|
54
|
-
</section>`;
|
|
55
|
-
this.#root.querySelectorAll("[data-start] button").forEach((button) => button.addEventListener("click", () => {
|
|
56
|
-
const start = Number(button.parentElement?.dataset.start);
|
|
57
|
-
if (!Number.isFinite(start)) return;
|
|
58
|
-
this.dispatchEvent(new CustomEvent("av-annotationselect", { detail: { time: start }, bubbles: true, composed: true }));
|
|
59
|
-
this.controller.seekTo(start);
|
|
60
|
-
playFromInteraction(this.controller, this);
|
|
61
|
-
}));
|
|
62
|
-
this.#sync();
|
|
63
|
-
}
|
|
64
|
-
#sync() {
|
|
65
|
-
const time = this.controller.state.currentTime;
|
|
66
|
-
this.#root.querySelectorAll("[data-start][data-end]").forEach((row) => {
|
|
67
|
-
const start = Number(row.dataset.start);
|
|
68
|
-
const end = Number(row.dataset.end);
|
|
69
|
-
row.querySelector("button")?.classList.toggle("active", Number.isFinite(start) && time >= start && (!Number.isFinite(end) || time < end));
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
var defineMangoAVAnnotations = (tagName = "mango-av-annotations") => {
|
|
74
|
-
if (typeof customElements === "undefined") return MangoAVAnnotationsElement;
|
|
75
|
-
if (!customElements.get(tagName)) customElements.define(tagName, MangoAVAnnotationsElement);
|
|
76
|
-
return MangoAVAnnotationsElement;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export {
|
|
80
|
-
MangoAVAnnotationsElement,
|
|
81
|
-
defineMangoAVAnnotations
|
|
82
|
-
};
|
|
83
|
-
//# sourceMappingURL=chunk-YX32ZQPR.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/mango-av-annotations.ts"],"sourcesContent":["import { AVControllerElement } from './controller-element';\nimport { componentTheme } from './component-styles';\nimport { playFromInteraction } from './playback';\nimport type { AVAnnotation, AVPlayerEventMap, AVStrings } from '../types';\nimport { escapeHtml, formatTime, resolveStrings } from '../utils';\n\nconst styles = `${componentTheme}\n .annotations { background: var(--mango-av-background); border: 1px solid var(--mango-av-border); border-radius: var(--mango-av-radius); padding: .75rem; }\n h2 { font-size: 1rem; margin: 0 0 .5rem; }\n ol { list-style: none; margin: 0; padding: 0; }\n button { background: transparent; border: 0; border-radius: .35rem; color: inherit; display: grid; gap: .2rem; padding: .5rem; text-align: left; width: 100%; }\n button:hover { background: var(--mango-av-surface); }\n button.active { background: color-mix(in srgb, var(--mango-av-accent) 14%, transparent); box-shadow: inset 3px 0 var(--mango-av-accent); }\n .tags { display: flex; flex-wrap: wrap; gap: .25rem; }\n .tag { background: var(--mango-av-surface); border-radius: 999px; font-size: .75rem; padding: .15rem .45rem; }\n`;\n\nconst annotationsHtml = (annotations: AVAnnotation[], ui: AVStrings): string => {\n if (annotations.length === 0) return `<p class=\"empty\">${escapeHtml(ui.noAnnotations)}</p>`;\n return `<ol>${annotations.map((annotation) => `<li data-start=\"${annotation.time?.start ?? ''}\" data-end=\"${annotation.time?.end ?? annotation.time?.start ?? ''}\">\n <button type=\"button\" ${annotation.time ? '' : 'disabled'}>\n ${annotation.time ? `<span class=\"time\">${formatTime(annotation.time.start)}</span>` : ''}\n <span>${escapeHtml(annotation.text)}</span>\n ${annotation.tags.length > 0 ? `<span class=\"tags\">${annotation.tags.map((tag) => `<span class=\"tag\">${escapeHtml(tag)}</span>`).join('')}</span>` : ''}\n </button>\n </li>`).join('')}</ol>`;\n};\n\nexport class MangoAVAnnotationsElement extends AVControllerElement {\n #root: ShadowRoot;\n\n constructor() {\n super();\n this.#root = this.attachShadow({ mode: 'open' });\n }\n\n protected controllerChanged(): void { this.#render(); }\n\n protected handleControllerEvent<K extends keyof AVPlayerEventMap>(type: K, _event: CustomEvent<AVPlayerEventMap[K]>): void {\n if (type === 'av-ready' || type === 'av-canvaschange' || type === 'av-annotationschange' || type === 'av-configchange') this.#render();\n else if (type === 'av-timeupdate' || type === 'av-seek' || type === 'av-statechange') this.#sync();\n }\n\n #render(): void {\n const canvas = this.controller.canvas;\n const ui = resolveStrings(this.controller.config.strings);\n this.#root.innerHTML = `<style>${styles}</style><section class=\"annotations\" aria-label=\"${escapeHtml(ui.annotations)}\">\n <slot name=\"heading\"><h2>${escapeHtml(ui.annotations)}</h2></slot>\n ${canvas ? annotationsHtml(canvas.annotations, ui) : `<p class=\"empty\">${escapeHtml(ui.noManifest)}</p>`}\n </section>`;\n this.#root.querySelectorAll<HTMLElement>('[data-start] button').forEach((button) => button.addEventListener('click', () => {\n const start = Number(button.parentElement?.dataset.start);\n if (!Number.isFinite(start)) return;\n this.dispatchEvent(new CustomEvent('av-annotationselect', { detail: { time: start }, bubbles: true, composed: true }));\n this.controller.seekTo(start);\n playFromInteraction(this.controller, this);\n }));\n this.#sync();\n }\n\n #sync(): void {\n const time = this.controller.state.currentTime;\n this.#root.querySelectorAll<HTMLElement>('[data-start][data-end]').forEach((row) => {\n const start = Number(row.dataset.start);\n const end = Number(row.dataset.end);\n row.querySelector('button')?.classList.toggle('active', Number.isFinite(start) && time >= start && (!Number.isFinite(end) || time < end));\n });\n }\n}\n\nexport const defineMangoAVAnnotations = (tagName = 'mango-av-annotations'): typeof MangoAVAnnotationsElement => {\n if (typeof customElements === 'undefined') return MangoAVAnnotationsElement;\n if (!customElements.get(tagName)) customElements.define(tagName, MangoAVAnnotationsElement);\n return MangoAVAnnotationsElement;\n};\n"],"mappings":";;;;;;;;;;;;;;AAMA,IAAM,SAAS,GAAG,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWhC,IAAM,kBAAkB,CAAC,aAA6B,OAA0B;AAC9E,MAAI,YAAY,WAAW,EAAG,QAAO,oBAAoB,WAAW,GAAG,aAAa,CAAC;AACrF,SAAO,OAAO,YAAY,IAAI,CAAC,eAAe,mBAAmB,WAAW,MAAM,SAAS,EAAE,eAAe,WAAW,MAAM,OAAO,WAAW,MAAM,SAAS,EAAE;AAAA,4BACtI,WAAW,OAAO,KAAK,UAAU;AAAA,QACrD,WAAW,OAAO,sBAAsB,WAAW,WAAW,KAAK,KAAK,CAAC,YAAY,EAAE;AAAA,cACjF,WAAW,WAAW,IAAI,CAAC;AAAA,QACjC,WAAW,KAAK,SAAS,IAAI,sBAAsB,WAAW,KAAK,IAAI,CAAC,QAAQ,qBAAqB,WAAW,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,YAAY,EAAE;AAAA;AAAA,QAErJ,EAAE,KAAK,EAAE,CAAC;AAClB;AAEO,IAAM,4BAAN,cAAwC,oBAAoB;AAAA,EACjE;AAAA,EAEA,cAAc;AACZ,UAAM;AACN,SAAK,QAAQ,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAAA,EACjD;AAAA,EAEU,oBAA0B;AAAE,SAAK,QAAQ;AAAA,EAAG;AAAA,EAE5C,sBAAwD,MAAS,QAAgD;AACzH,QAAI,SAAS,cAAc,SAAS,qBAAqB,SAAS,0BAA0B,SAAS,kBAAmB,MAAK,QAAQ;AAAA,aAC5H,SAAS,mBAAmB,SAAS,aAAa,SAAS,iBAAkB,MAAK,MAAM;AAAA,EACnG;AAAA,EAEA,UAAgB;AACd,UAAM,SAAS,KAAK,WAAW;AAC/B,UAAM,KAAK,eAAe,KAAK,WAAW,OAAO,OAAO;AACxD,SAAK,MAAM,YAAY,UAAU,MAAM,oDAAoD,WAAW,GAAG,WAAW,CAAC;AAAA,iCACxF,WAAW,GAAG,WAAW,CAAC;AAAA,QACnD,SAAS,gBAAgB,OAAO,aAAa,EAAE,IAAI,oBAAoB,WAAW,GAAG,UAAU,CAAC,MAAM;AAAA;AAE1G,SAAK,MAAM,iBAA8B,qBAAqB,EAAE,QAAQ,CAAC,WAAW,OAAO,iBAAiB,SAAS,MAAM;AACzH,YAAM,QAAQ,OAAO,OAAO,eAAe,QAAQ,KAAK;AACxD,UAAI,CAAC,OAAO,SAAS,KAAK,EAAG;AAC7B,WAAK,cAAc,IAAI,YAAY,uBAAuB,EAAE,QAAQ,EAAE,MAAM,MAAM,GAAG,SAAS,MAAM,UAAU,KAAK,CAAC,CAAC;AACrH,WAAK,WAAW,OAAO,KAAK;AAC5B,0BAAoB,KAAK,YAAY,IAAI;AAAA,IAC3C,CAAC,CAAC;AACF,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,QAAc;AACZ,UAAM,OAAO,KAAK,WAAW,MAAM;AACnC,SAAK,MAAM,iBAA8B,wBAAwB,EAAE,QAAQ,CAAC,QAAQ;AAClF,YAAM,QAAQ,OAAO,IAAI,QAAQ,KAAK;AACtC,YAAM,MAAM,OAAO,IAAI,QAAQ,GAAG;AAClC,UAAI,cAAc,QAAQ,GAAG,UAAU,OAAO,UAAU,OAAO,SAAS,KAAK,KAAK,QAAQ,UAAU,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,IAAI;AAAA,IAC1I,CAAC;AAAA,EACH;AACF;AAEO,IAAM,2BAA2B,CAAC,UAAU,2BAA6D;AAC9G,MAAI,OAAO,mBAAmB,YAAa,QAAO;AAClD,MAAI,CAAC,eAAe,IAAI,OAAO,EAAG,gBAAe,OAAO,SAAS,yBAAyB;AAC1F,SAAO;AACT;","names":[]}
|