@intellectif/lk-react 6.1.0 → 7.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.
- package/CHANGELOG.md +86 -0
- package/README.md +2 -0
- package/dist/{WrittenResponse-BXHidIaK.d.cts → WrittenResponse-BJL4dGsR.d.ts} +5 -1
- package/dist/{WrittenResponse-DJYGuMKk.d.ts → WrittenResponse-zWT-Go1g.d.cts} +5 -1
- package/dist/{chunk-IUODGQXE.js → chunk-3VQV7ICQ.js} +110 -33
- package/dist/chunk-3VQV7ICQ.js.map +1 -0
- package/dist/chunk-52A5UNCH.js +540 -0
- package/dist/chunk-52A5UNCH.js.map +1 -0
- package/dist/{chunk-NPY2F7F6.cjs → chunk-6DSA75TA.cjs} +16 -4
- package/dist/chunk-6DSA75TA.cjs.map +1 -0
- package/dist/{chunk-MDZMKC27.js → chunk-7YLY3G3E.js} +21 -4
- package/dist/chunk-7YLY3G3E.js.map +1 -0
- package/dist/{chunk-NEWGUDA5.cjs → chunk-ABHJW4LC.cjs} +21 -5
- package/dist/chunk-ABHJW4LC.cjs.map +1 -0
- package/dist/{chunk-ZLUMUJUQ.js → chunk-C3HR55JD.js} +16 -4
- package/dist/chunk-C3HR55JD.js.map +1 -0
- package/dist/{chunk-DUIWK272.cjs → chunk-FB6XKQFV.cjs} +112 -35
- package/dist/chunk-FB6XKQFV.cjs.map +1 -0
- package/dist/{chunk-MDYKYDRN.cjs → chunk-PPJ47THY.cjs} +21 -4
- package/dist/chunk-PPJ47THY.cjs.map +1 -0
- package/dist/chunk-UFWF3BKA.cjs +542 -0
- package/dist/chunk-UFWF3BKA.cjs.map +1 -0
- package/dist/{chunk-57E3E2H4.js → chunk-VHULVDPG.js} +16 -4
- package/dist/chunk-VHULVDPG.js.map +1 -0
- package/dist/{chunk-BE7R2X3S.cjs → chunk-XGQKID2P.cjs} +16 -4
- package/dist/chunk-XGQKID2P.cjs.map +1 -0
- package/dist/{chunk-UQ3BBEIT.js → chunk-XS2SZINH.js} +21 -5
- package/dist/chunk-XS2SZINH.js.map +1 -0
- package/dist/components/ActivitySequence.cjs +7 -7
- package/dist/components/ActivitySequence.d.cts +13 -15
- package/dist/components/ActivitySequence.d.ts +13 -15
- package/dist/components/ActivitySequence.js +6 -6
- package/dist/components/FillInTheBlanks.cjs +3 -3
- package/dist/components/FillInTheBlanks.d.cts +1 -1
- package/dist/components/FillInTheBlanks.d.ts +1 -1
- package/dist/components/FillInTheBlanks.js +2 -2
- package/dist/components/MultipleChoice.cjs +3 -3
- package/dist/components/MultipleChoice.d.cts +1 -1
- package/dist/components/MultipleChoice.d.ts +1 -1
- package/dist/components/MultipleChoice.js +2 -2
- package/dist/components/StimulusPanel.cjs +3 -3
- package/dist/components/StimulusPanel.d.cts +19 -3
- package/dist/components/StimulusPanel.d.ts +19 -3
- package/dist/components/StimulusPanel.js +2 -2
- package/dist/components/WrittenResponse.cjs +3 -3
- package/dist/components/WrittenResponse.d.cts +3 -3
- package/dist/components/WrittenResponse.d.ts +3 -3
- package/dist/components/WrittenResponse.js +2 -2
- package/dist/index.cjs +11 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/theme/skin.css +204 -1
- package/dist/{types-9iS1Vs2I.d.ts → types-eXX4hzSj.d.cts} +128 -2
- package/dist/{types-9iS1Vs2I.d.cts → types-eXX4hzSj.d.ts} +128 -2
- package/package.json +3 -3
- package/dist/chunk-4HZ7LQIH.cjs +0 -41
- package/dist/chunk-4HZ7LQIH.cjs.map +0 -1
- package/dist/chunk-57E3E2H4.js.map +0 -1
- package/dist/chunk-BE7R2X3S.cjs.map +0 -1
- package/dist/chunk-DUIWK272.cjs.map +0 -1
- package/dist/chunk-GB5URWM4.js +0 -39
- package/dist/chunk-GB5URWM4.js.map +0 -1
- package/dist/chunk-IUODGQXE.js.map +0 -1
- package/dist/chunk-MDYKYDRN.cjs.map +0 -1
- package/dist/chunk-MDZMKC27.js.map +0 -1
- package/dist/chunk-NEWGUDA5.cjs.map +0 -1
- package/dist/chunk-NPY2F7F6.cjs.map +0 -1
- package/dist/chunk-UQ3BBEIT.js.map +0 -1
- package/dist/chunk-ZLUMUJUQ.js.map +0 -1
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var lkCore = require('@intellectif/lk-core');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/components/shared/ActivityMedia.tsx
|
|
9
|
+
var DEFAULT_STRINGS = {
|
|
10
|
+
play: "Play",
|
|
11
|
+
pause: "Pause",
|
|
12
|
+
preparing: "Preparing\u2026",
|
|
13
|
+
mute: "Mute",
|
|
14
|
+
unmute: "Unmute",
|
|
15
|
+
volume: "Volume",
|
|
16
|
+
speed: "Playback speed",
|
|
17
|
+
seek: "Seek",
|
|
18
|
+
playsRemaining: (remaining, max) => `${remaining} of ${max} play${max === 1 ? "" : "s"} remaining`,
|
|
19
|
+
noPlaysRemaining: "No plays remaining",
|
|
20
|
+
lastPlayConfirm: "This is your last play. Start it now?",
|
|
21
|
+
lastPlayStart: "Start last play",
|
|
22
|
+
lastPlayCancel: "Not yet",
|
|
23
|
+
seekBlocked: "Rewinding is not available for this recording.",
|
|
24
|
+
rateBlocked: "Playback speed is fixed for this recording.",
|
|
25
|
+
playFailed: "The recording could not be started. Try again."
|
|
26
|
+
};
|
|
27
|
+
var RESUME_EPSILON = 0.25;
|
|
28
|
+
var SEEK_EPSILON = 0.35;
|
|
29
|
+
var NO_PROGRESS = 0.25;
|
|
30
|
+
var formatTime = (seconds) => {
|
|
31
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
32
|
+
return "0:00";
|
|
33
|
+
}
|
|
34
|
+
const whole = Math.floor(seconds);
|
|
35
|
+
return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
|
|
36
|
+
};
|
|
37
|
+
function AudioTransport({
|
|
38
|
+
media,
|
|
39
|
+
policy,
|
|
40
|
+
renderMode = "practice",
|
|
41
|
+
disabled = false,
|
|
42
|
+
locale,
|
|
43
|
+
mediaBudget,
|
|
44
|
+
mediaStrings,
|
|
45
|
+
onInteraction
|
|
46
|
+
}) {
|
|
47
|
+
const elementRef = react.useRef(null);
|
|
48
|
+
const statusId = react.useId();
|
|
49
|
+
const suppliedStrings = mediaBudget?.strings ?? mediaStrings;
|
|
50
|
+
const s = { ...DEFAULT_STRINGS, ...mediaStrings, ...mediaBudget?.strings };
|
|
51
|
+
const enforced = mediaBudget?.enforced ?? renderMode !== "review";
|
|
52
|
+
const budgeted = enforced && policy.maxPlays !== null && mediaBudget !== void 0;
|
|
53
|
+
const maxPlays = policy.maxPlays ?? 0;
|
|
54
|
+
const usedRef = react.useRef(mediaBudget?.entry?.plays ?? 0);
|
|
55
|
+
const pendingRef = react.useRef(false);
|
|
56
|
+
const resumeAtRef = react.useRef(null);
|
|
57
|
+
const highWaterRef = react.useRef(0);
|
|
58
|
+
const chargeRef = react.useRef(null);
|
|
59
|
+
const seededRef = react.useRef(false);
|
|
60
|
+
const lastPositionRef = react.useRef(-1);
|
|
61
|
+
const selfStartRef = react.useRef(false);
|
|
62
|
+
const [used, setUsed] = react.useState(usedRef.current);
|
|
63
|
+
const [playing, setPlaying] = react.useState(false);
|
|
64
|
+
const [pending, setPending] = react.useState(false);
|
|
65
|
+
const [elapsed, setElapsed] = react.useState(0);
|
|
66
|
+
const [duration, setDuration] = react.useState(0);
|
|
67
|
+
const [muted, setMuted] = react.useState(false);
|
|
68
|
+
const [volume, setVolume] = react.useState(1);
|
|
69
|
+
const [confirming, setConfirming] = react.useState(false);
|
|
70
|
+
const [notice, setNotice] = react.useState("");
|
|
71
|
+
const [noticeNonce, setNoticeNonce] = react.useState(0);
|
|
72
|
+
const remaining = Math.max(0, maxPlays - used);
|
|
73
|
+
const announce = react.useCallback((message) => {
|
|
74
|
+
setNotice(message);
|
|
75
|
+
setNoticeNonce((n) => n + 1);
|
|
76
|
+
}, []);
|
|
77
|
+
const emit = react.useCallback(
|
|
78
|
+
(type, playsUsed) => {
|
|
79
|
+
if (!budgeted || mediaBudget === void 0 || onInteraction === void 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
onInteraction({
|
|
83
|
+
type,
|
|
84
|
+
activityId: mediaBudget.activityId ?? mediaBudget.slotId,
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
payload: {
|
|
87
|
+
mediaKey: mediaBudget.key,
|
|
88
|
+
mediaType: "audio",
|
|
89
|
+
playsUsed,
|
|
90
|
+
maxPlays,
|
|
91
|
+
playsRemaining: Math.max(0, maxPlays - playsUsed)
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
[budgeted, mediaBudget, maxPlays, onInteraction]
|
|
96
|
+
);
|
|
97
|
+
const reportPosition = react.useCallback(
|
|
98
|
+
(seconds) => {
|
|
99
|
+
mediaBudget?.onPosition?.(mediaBudget.key, seconds);
|
|
100
|
+
},
|
|
101
|
+
[mediaBudget]
|
|
102
|
+
);
|
|
103
|
+
const isResume = react.useCallback((element) => {
|
|
104
|
+
const at = resumeAtRef.current;
|
|
105
|
+
return at !== null && Math.abs(element.currentTime - at) < RESUME_EPSILON;
|
|
106
|
+
}, []);
|
|
107
|
+
const buildClaim = react.useCallback(() => {
|
|
108
|
+
if (mediaBudget === void 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
key: mediaBudget.key,
|
|
113
|
+
previousPlaysUsed: usedRef.current,
|
|
114
|
+
playsUsed: usedRef.current + 1,
|
|
115
|
+
maxPlays,
|
|
116
|
+
playsRemaining: Math.max(0, maxPlays - (usedRef.current + 1)),
|
|
117
|
+
slotId: mediaBudget.slotId,
|
|
118
|
+
index: mediaBudget.index,
|
|
119
|
+
...mediaBudget.activityId !== void 0 ? { activityId: mediaBudget.activityId } : {}
|
|
120
|
+
};
|
|
121
|
+
}, [mediaBudget, maxPlays]);
|
|
122
|
+
const refuse = react.useCallback(() => {
|
|
123
|
+
announce(s.noPlaysRemaining);
|
|
124
|
+
emit("media-play-refused", usedRef.current);
|
|
125
|
+
}, [announce, emit, s.noPlaysRemaining]);
|
|
126
|
+
const startPlayback = react.useCallback(() => {
|
|
127
|
+
const element = elementRef.current;
|
|
128
|
+
if (element === null) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
highWaterRef.current = element.currentTime;
|
|
132
|
+
selfStartRef.current = true;
|
|
133
|
+
void element.play().catch(() => {
|
|
134
|
+
selfStartRef.current = false;
|
|
135
|
+
});
|
|
136
|
+
}, []);
|
|
137
|
+
const commit = react.useCallback(
|
|
138
|
+
(claim) => {
|
|
139
|
+
const element = elementRef.current;
|
|
140
|
+
chargeRef.current = { claim, at: element?.currentTime ?? 0 };
|
|
141
|
+
usedRef.current = claim.playsUsed;
|
|
142
|
+
setUsed(claim.playsUsed);
|
|
143
|
+
emit("media-play-consumed", claim.playsUsed);
|
|
144
|
+
},
|
|
145
|
+
[emit]
|
|
146
|
+
);
|
|
147
|
+
const claimAndStart = react.useCallback(
|
|
148
|
+
(element, alreadyPlaying) => {
|
|
149
|
+
const claim = buildClaim();
|
|
150
|
+
if (claim === null) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const result = mediaBudget?.onPlayConsumed?.(claim);
|
|
154
|
+
if (result !== void 0 && typeof result.then === "function") {
|
|
155
|
+
if (alreadyPlaying) {
|
|
156
|
+
element.pause();
|
|
157
|
+
}
|
|
158
|
+
pendingRef.current = true;
|
|
159
|
+
setPending(true);
|
|
160
|
+
void result.then((grant) => {
|
|
161
|
+
const granted = grant !== void 0 && Number.isInteger(grant.playsUsed) ? grant.playsUsed : claim.playsUsed;
|
|
162
|
+
if (granted > maxPlays) {
|
|
163
|
+
usedRef.current = granted;
|
|
164
|
+
setUsed(granted);
|
|
165
|
+
refuse();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
commit({ ...claim, playsUsed: granted });
|
|
169
|
+
startPlayback();
|
|
170
|
+
}).catch(() => {
|
|
171
|
+
announce(s.playFailed);
|
|
172
|
+
emit("media-play-errored", usedRef.current);
|
|
173
|
+
}).finally(() => {
|
|
174
|
+
pendingRef.current = false;
|
|
175
|
+
setPending(false);
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
commit(claim);
|
|
180
|
+
if (!alreadyPlaying) {
|
|
181
|
+
startPlayback();
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
[
|
|
185
|
+
buildClaim,
|
|
186
|
+
mediaBudget,
|
|
187
|
+
maxPlays,
|
|
188
|
+
commit,
|
|
189
|
+
startPlayback,
|
|
190
|
+
refuse,
|
|
191
|
+
announce,
|
|
192
|
+
emit,
|
|
193
|
+
s.playFailed
|
|
194
|
+
]
|
|
195
|
+
);
|
|
196
|
+
const handleButton = react.useCallback(() => {
|
|
197
|
+
const element = elementRef.current;
|
|
198
|
+
if (element === null || pendingRef.current || disabled) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!element.paused) {
|
|
202
|
+
element.pause();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!budgeted || isResume(element)) {
|
|
206
|
+
startPlayback();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (remaining === 0) {
|
|
210
|
+
refuse();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (remaining === 1 && !confirming) {
|
|
214
|
+
setConfirming(true);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
setConfirming(false);
|
|
218
|
+
claimAndStart(element, false);
|
|
219
|
+
}, [disabled, budgeted, isResume, startPlayback, remaining, refuse, confirming, claimAndStart]);
|
|
220
|
+
const handleLoadedMetadata = react.useCallback(() => {
|
|
221
|
+
const element = elementRef.current;
|
|
222
|
+
if (element === null) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
setDuration(Number.isFinite(element.duration) ? element.duration : 0);
|
|
226
|
+
const at = mediaBudget?.entry?.at;
|
|
227
|
+
if (!seededRef.current && typeof at === "number" && Number.isFinite(at) && at > 0) {
|
|
228
|
+
seededRef.current = true;
|
|
229
|
+
highWaterRef.current = at;
|
|
230
|
+
resumeAtRef.current = at;
|
|
231
|
+
element.currentTime = at;
|
|
232
|
+
setElapsed(at);
|
|
233
|
+
}
|
|
234
|
+
}, [mediaBudget]);
|
|
235
|
+
const handlePlay = react.useCallback(() => {
|
|
236
|
+
const element = elementRef.current;
|
|
237
|
+
if (element === null) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (disabled) {
|
|
241
|
+
element.pause();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (selfStartRef.current) {
|
|
245
|
+
selfStartRef.current = false;
|
|
246
|
+
setPlaying(true);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (pendingRef.current) {
|
|
250
|
+
setPlaying(true);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (isResume(element)) {
|
|
254
|
+
setPlaying(true);
|
|
255
|
+
resumeAtRef.current = null;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!budgeted) {
|
|
259
|
+
setPlaying(true);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (remaining === 0) {
|
|
263
|
+
element.pause();
|
|
264
|
+
refuse();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
setPlaying(true);
|
|
268
|
+
claimAndStart(element, true);
|
|
269
|
+
}, [disabled, isResume, budgeted, remaining, refuse, claimAndStart]);
|
|
270
|
+
const handlePause = react.useCallback(() => {
|
|
271
|
+
const element = elementRef.current;
|
|
272
|
+
setPlaying(false);
|
|
273
|
+
if (element === null) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
resumeAtRef.current = element.currentTime;
|
|
277
|
+
reportPosition(element.currentTime);
|
|
278
|
+
}, [reportPosition]);
|
|
279
|
+
const handleEnded = react.useCallback(() => {
|
|
280
|
+
setPlaying(false);
|
|
281
|
+
resumeAtRef.current = null;
|
|
282
|
+
highWaterRef.current = 0;
|
|
283
|
+
chargeRef.current = null;
|
|
284
|
+
setElapsed(0);
|
|
285
|
+
reportPosition(0);
|
|
286
|
+
}, [reportPosition]);
|
|
287
|
+
const handleTimeUpdate = react.useCallback(() => {
|
|
288
|
+
const element = elementRef.current;
|
|
289
|
+
if (element === null) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
setElapsed(element.currentTime);
|
|
293
|
+
highWaterRef.current = Math.max(highWaterRef.current, element.currentTime);
|
|
294
|
+
const whole = Math.floor(element.currentTime);
|
|
295
|
+
if (whole !== lastPositionRef.current) {
|
|
296
|
+
lastPositionRef.current = whole;
|
|
297
|
+
reportPosition(element.currentTime);
|
|
298
|
+
}
|
|
299
|
+
}, [reportPosition]);
|
|
300
|
+
const handleSeeking = react.useCallback(() => {
|
|
301
|
+
const element = elementRef.current;
|
|
302
|
+
if (element === null || policy.seek !== "none") {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (Math.abs(element.currentTime - highWaterRef.current) > SEEK_EPSILON) {
|
|
306
|
+
element.currentTime = highWaterRef.current;
|
|
307
|
+
announce(s.seekBlocked);
|
|
308
|
+
}
|
|
309
|
+
}, [policy.seek, announce, s.seekBlocked]);
|
|
310
|
+
const handleRateChange = react.useCallback(() => {
|
|
311
|
+
const element = elementRef.current;
|
|
312
|
+
if (element === null || policy.rate !== "fixed" || element.playbackRate === 1) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
element.playbackRate = 1;
|
|
316
|
+
announce(s.rateBlocked);
|
|
317
|
+
}, [policy.rate, announce, s.rateBlocked]);
|
|
318
|
+
const handleError = react.useCallback(() => {
|
|
319
|
+
const charge = chargeRef.current;
|
|
320
|
+
const element = elementRef.current;
|
|
321
|
+
const progressed = charge !== null && element !== null && element.currentTime - charge.at >= NO_PROGRESS;
|
|
322
|
+
if (charge !== null && !progressed) {
|
|
323
|
+
if (mediaBudget?.onPlayRefunded !== void 0) {
|
|
324
|
+
usedRef.current = charge.claim.previousPlaysUsed;
|
|
325
|
+
setUsed(charge.claim.previousPlaysUsed);
|
|
326
|
+
mediaBudget.onPlayRefunded(charge.claim);
|
|
327
|
+
emit("media-play-refunded", charge.claim.previousPlaysUsed);
|
|
328
|
+
} else {
|
|
329
|
+
emit("media-play-errored", usedRef.current);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
chargeRef.current = null;
|
|
333
|
+
setPlaying(false);
|
|
334
|
+
announce(s.playFailed);
|
|
335
|
+
}, [mediaBudget, emit, announce, s.playFailed]);
|
|
336
|
+
react.useEffect(() => {
|
|
337
|
+
const element = elementRef.current;
|
|
338
|
+
if (element !== null) {
|
|
339
|
+
element.volume = volume;
|
|
340
|
+
element.muted = muted;
|
|
341
|
+
}
|
|
342
|
+
}, [volume, muted]);
|
|
343
|
+
const exhausted = budgeted && remaining === 0;
|
|
344
|
+
const buttonLabel = pending ? s.preparing : playing ? s.pause : s.play;
|
|
345
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("figure", { className: "lk-media", "data-controls": "minimal", children: [
|
|
346
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
347
|
+
"audio",
|
|
348
|
+
{
|
|
349
|
+
ref: elementRef,
|
|
350
|
+
className: "lk-media-el",
|
|
351
|
+
preload: "metadata",
|
|
352
|
+
"aria-label": media.alt || void 0,
|
|
353
|
+
onLoadedMetadata: handleLoadedMetadata,
|
|
354
|
+
onPlay: handlePlay,
|
|
355
|
+
onPause: handlePause,
|
|
356
|
+
onEnded: handleEnded,
|
|
357
|
+
onTimeUpdate: handleTimeUpdate,
|
|
358
|
+
onSeeking: handleSeeking,
|
|
359
|
+
onRateChange: handleRateChange,
|
|
360
|
+
onError: handleError,
|
|
361
|
+
children: [
|
|
362
|
+
/* @__PURE__ */ jsxRuntime.jsx("source", { src: media.url }),
|
|
363
|
+
media.captionsUrl ? /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions", src: media.captionsUrl, default: true }) : null
|
|
364
|
+
]
|
|
365
|
+
}
|
|
366
|
+
),
|
|
367
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
368
|
+
"div",
|
|
369
|
+
{
|
|
370
|
+
className: "lk-media-transport",
|
|
371
|
+
dir: "ltr",
|
|
372
|
+
...suppliedStrings !== void 0 && locale !== void 0 ? { lang: locale } : {},
|
|
373
|
+
children: [
|
|
374
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
375
|
+
"button",
|
|
376
|
+
{
|
|
377
|
+
type: "button",
|
|
378
|
+
className: "lk-media-play",
|
|
379
|
+
onClick: handleButton,
|
|
380
|
+
"aria-disabled": exhausted || disabled || void 0,
|
|
381
|
+
"aria-busy": pending || void 0,
|
|
382
|
+
"aria-label": exhausted ? `${s.play} \u2014 ${s.noPlaysRemaining}` : void 0,
|
|
383
|
+
"aria-describedby": budgeted ? statusId : void 0,
|
|
384
|
+
children: buttonLabel
|
|
385
|
+
}
|
|
386
|
+
),
|
|
387
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-media-time", children: [
|
|
388
|
+
formatTime(elapsed),
|
|
389
|
+
" / ",
|
|
390
|
+
formatTime(duration)
|
|
391
|
+
] }),
|
|
392
|
+
policy.seek === "allow" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
393
|
+
"input",
|
|
394
|
+
{
|
|
395
|
+
type: "range",
|
|
396
|
+
className: "lk-media-scrub",
|
|
397
|
+
min: 0,
|
|
398
|
+
max: duration || 0,
|
|
399
|
+
step: 0.1,
|
|
400
|
+
value: Math.min(elapsed, duration || 0),
|
|
401
|
+
"aria-label": s.seek,
|
|
402
|
+
"aria-valuetext": `${formatTime(elapsed)} of ${formatTime(duration)}`,
|
|
403
|
+
onChange: (event) => {
|
|
404
|
+
const element = elementRef.current;
|
|
405
|
+
if (element !== null) {
|
|
406
|
+
element.currentTime = Number(event.target.value);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
) : null,
|
|
411
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
412
|
+
"button",
|
|
413
|
+
{
|
|
414
|
+
type: "button",
|
|
415
|
+
className: "lk-media-mute",
|
|
416
|
+
"aria-pressed": muted,
|
|
417
|
+
onClick: () => setMuted((m) => !m),
|
|
418
|
+
children: muted ? s.unmute : s.mute
|
|
419
|
+
}
|
|
420
|
+
),
|
|
421
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
422
|
+
"input",
|
|
423
|
+
{
|
|
424
|
+
type: "range",
|
|
425
|
+
className: "lk-media-volume",
|
|
426
|
+
min: 0,
|
|
427
|
+
max: 1,
|
|
428
|
+
step: 0.05,
|
|
429
|
+
value: volume,
|
|
430
|
+
"aria-label": s.volume,
|
|
431
|
+
onChange: (event) => setVolume(Number(event.target.value))
|
|
432
|
+
}
|
|
433
|
+
),
|
|
434
|
+
policy.rate === "allow" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
435
|
+
"select",
|
|
436
|
+
{
|
|
437
|
+
className: "lk-media-rate",
|
|
438
|
+
"aria-label": s.speed,
|
|
439
|
+
defaultValue: "1",
|
|
440
|
+
onChange: (event) => {
|
|
441
|
+
const element = elementRef.current;
|
|
442
|
+
if (element !== null) {
|
|
443
|
+
element.playbackRate = Number(event.target.value);
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
children: ["0.75", "1", "1.25", "1.5"].map((rate) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: rate, children: [
|
|
447
|
+
rate,
|
|
448
|
+
"\xD7"
|
|
449
|
+
] }, rate))
|
|
450
|
+
}
|
|
451
|
+
) : null
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
),
|
|
455
|
+
budgeted ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-media-plays", id: statusId, role: "status", children: remaining > 0 ? s.playsRemaining(remaining, maxPlays) : s.noPlaysRemaining }) : null,
|
|
456
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-media-notice", role: "alert", children: notice }, noticeNonce),
|
|
457
|
+
confirming ? /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "lk-media-confirm", children: [
|
|
458
|
+
/* @__PURE__ */ jsxRuntime.jsx("legend", { children: s.lastPlayConfirm }),
|
|
459
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
460
|
+
"button",
|
|
461
|
+
{
|
|
462
|
+
type: "button",
|
|
463
|
+
className: "lk-media-confirm-start",
|
|
464
|
+
autoFocus: true,
|
|
465
|
+
onClick: () => {
|
|
466
|
+
const element = elementRef.current;
|
|
467
|
+
setConfirming(false);
|
|
468
|
+
if (element !== null) {
|
|
469
|
+
claimAndStart(element, false);
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
children: s.lastPlayStart
|
|
473
|
+
}
|
|
474
|
+
),
|
|
475
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
476
|
+
"button",
|
|
477
|
+
{
|
|
478
|
+
type: "button",
|
|
479
|
+
className: "lk-media-confirm-cancel",
|
|
480
|
+
onClick: () => setConfirming(false),
|
|
481
|
+
children: s.lastPlayCancel
|
|
482
|
+
}
|
|
483
|
+
)
|
|
484
|
+
] }) : null
|
|
485
|
+
] });
|
|
486
|
+
}
|
|
487
|
+
function ActivityMedia(props) {
|
|
488
|
+
const { media, renderMode = "practice" } = props;
|
|
489
|
+
if (media.type === "image") {
|
|
490
|
+
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsx("img", { className: "lk-media-el", src: media.url, alt: media.alt ?? "" }) });
|
|
491
|
+
}
|
|
492
|
+
if (media.type === "audio") {
|
|
493
|
+
const policy = lkCore.resolvePlaybackPolicy(media);
|
|
494
|
+
if (policy.maxPlays !== null && renderMode === "exam" && props.mediaBudget === void 0) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`ActivityMedia: audio ${JSON.stringify(media.url)} declares maxPlays and is rendered in renderMode "exam" without a \`mediaBudget\` binding. Nothing would persist the count, so a refresh restores the full budget while the page says plays remain. Render it through <ActivitySequence mediaBudget={\u2026}> or pass \`mediaBudget\` yourself.`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
if (policy.controls === "minimal" && renderMode !== "review") {
|
|
500
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AudioTransport, { ...props, media, policy });
|
|
501
|
+
}
|
|
502
|
+
const hints = renderMode === "review" ? [] : policy.nativeControlHints;
|
|
503
|
+
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
504
|
+
"audio",
|
|
505
|
+
{
|
|
506
|
+
className: "lk-media-el",
|
|
507
|
+
controls: true,
|
|
508
|
+
"aria-label": media.alt || void 0,
|
|
509
|
+
...hints.length > 0 ? {
|
|
510
|
+
controlsList: hints.map((hint) => hint === "hide-download" ? "nodownload" : "noplaybackrate").join(" ")
|
|
511
|
+
} : {},
|
|
512
|
+
children: [
|
|
513
|
+
/* @__PURE__ */ jsxRuntime.jsx("source", { src: media.url }),
|
|
514
|
+
media.captionsUrl ? /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions", src: media.captionsUrl, default: true }) : null
|
|
515
|
+
]
|
|
516
|
+
}
|
|
517
|
+
) });
|
|
518
|
+
}
|
|
519
|
+
if (media.type === "embed") {
|
|
520
|
+
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-media-embed", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
521
|
+
"iframe",
|
|
522
|
+
{
|
|
523
|
+
className: "lk-media-el",
|
|
524
|
+
src: media.url,
|
|
525
|
+
title: media.alt ?? "Embedded media",
|
|
526
|
+
loading: "lazy",
|
|
527
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
528
|
+
sandbox: "allow-scripts allow-same-origin allow-presentation",
|
|
529
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
530
|
+
allowFullScreen: true
|
|
531
|
+
}
|
|
532
|
+
) }) });
|
|
533
|
+
}
|
|
534
|
+
return /* @__PURE__ */ jsxRuntime.jsx("figure", { className: "lk-media", children: /* @__PURE__ */ jsxRuntime.jsxs("video", { className: "lk-media-el", controls: true, "aria-label": media.alt || void 0, children: [
|
|
535
|
+
/* @__PURE__ */ jsxRuntime.jsx("source", { src: media.url }),
|
|
536
|
+
media.captionsUrl ? /* @__PURE__ */ jsxRuntime.jsx("track", { kind: "captions", src: media.captionsUrl, default: true }) : null
|
|
537
|
+
] }) });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
exports.ActivityMedia = ActivityMedia;
|
|
541
|
+
//# sourceMappingURL=chunk-UFWF3BKA.cjs.map
|
|
542
|
+
//# sourceMappingURL=chunk-UFWF3BKA.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/shared/AudioTransport.tsx","../src/components/shared/ActivityMedia.tsx"],"names":["useRef","useId","useState","useCallback","useEffect","jsxs","jsx","resolvePlaybackPolicy"],"mappings":"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","file":"chunk-UFWF3BKA.cjs","sourcesContent":["'use client';\n\nimport type {\n ActivityMedia as ActivityMediaData,\n MediaPlayClaim,\n ResolvedPlaybackPolicy,\n} from '@intellectif/lk-core';\nimport { useCallback, useEffect, useId, useRef, useState } from 'react';\nimport type { MediaBudgetBinding, MediaTransportStrings } from '../types.js';\n\n/**\n * The SDK's own audio player, used whenever a policy has something to enforce.\n *\n * The browser's control bar cannot express a spent budget — its play button\n * stays enabled — and a control that looks operable and does nothing is a\n * WCAG 3.2.2 / 4.1.3 failure, so an enforcing policy replaces the bar rather\n * than lying with it. What replaces it is a SUPERSET minus what the policy\n * removes on purpose: play/pause, elapsed and total time, mute, volume, an\n * optional speed control, an optional scrubber, and a live plays-remaining\n * status. Volume and mute are never restricted by any policy — they affect no\n * assessment property, and a learner in a lab with a locked OS volume has no\n * other lever once the native bar is gone.\n */\n\nconst DEFAULT_STRINGS: MediaTransportStrings = {\n play: 'Play',\n pause: 'Pause',\n preparing: 'Preparing…',\n mute: 'Mute',\n unmute: 'Unmute',\n volume: 'Volume',\n speed: 'Playback speed',\n seek: 'Seek',\n playsRemaining: (remaining, max) =>\n `${remaining} of ${max} play${max === 1 ? '' : 's'} remaining`,\n noPlaysRemaining: 'No plays remaining',\n lastPlayConfirm: 'This is your last play. Start it now?',\n lastPlayStart: 'Start last play',\n lastPlayCancel: 'Not yet',\n seekBlocked: 'Rewinding is not available for this recording.',\n rateBlocked: 'Playback speed is fixed for this recording.',\n playFailed: 'The recording could not be started. Try again.',\n};\n\n/** A resume is a start within a quarter-second of where playback last stopped. */\nconst RESUME_EPSILON = 0.25;\n/** Below this, a seek is the browser settling rather than the learner moving. */\nconst SEEK_EPSILON = 0.35;\n/** A charged play that errors before this much audio played produced nothing. */\nconst NO_PROGRESS = 0.25;\n\nconst formatTime = (seconds: number): string => {\n if (!Number.isFinite(seconds) || seconds < 0) {\n return '0:00';\n }\n const whole = Math.floor(seconds);\n return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, '0')}`;\n};\n\nexport interface AudioTransportProps {\n media: ActivityMediaData;\n policy: ResolvedPlaybackPolicy;\n renderMode?: 'practice' | 'exam' | 'review';\n disabled?: boolean;\n locale?: string;\n mediaBudget?: MediaBudgetBinding;\n mediaStrings?: Partial<MediaTransportStrings>;\n onInteraction?: (event: {\n type: string;\n activityId: string;\n timestamp: number;\n payload: Record<string, unknown>;\n }) => void;\n}\n\nexport function AudioTransport({\n media,\n policy,\n renderMode = 'practice',\n disabled = false,\n locale,\n mediaBudget,\n mediaStrings,\n onInteraction,\n}: AudioTransportProps): React.JSX.Element {\n const elementRef = useRef<HTMLAudioElement>(null);\n const statusId = useId();\n\n const suppliedStrings = mediaBudget?.strings ?? mediaStrings;\n const s: MediaTransportStrings = { ...DEFAULT_STRINGS, ...mediaStrings, ...mediaBudget?.strings };\n\n const enforced = mediaBudget?.enforced ?? renderMode !== 'review';\n const budgeted = enforced && policy.maxPlays !== null && mediaBudget !== undefined;\n const maxPlays = policy.maxPlays ?? 0;\n\n // Written synchronously inside handlers and mirrored into state for\n // rendering, for the same reason `ActivitySequence` keeps `outcomesRef` in a\n // ref: two events in one tick would otherwise both read the pre-update value.\n const usedRef = useRef(mediaBudget?.entry?.plays ?? 0);\n const pendingRef = useRef(false);\n const resumeAtRef = useRef<number | null>(null);\n const highWaterRef = useRef(0);\n const chargeRef = useRef<{ claim: MediaPlayClaim; at: number } | null>(null);\n const seededRef = useRef(false);\n const lastPositionRef = useRef(-1);\n // Marks a `play()` this component itself issued after charging. The element's\n // `play` event is where the budget is enforced — so that a media key or a\n // scripted `play()` is charged too — which means our OWN start re-enters it.\n // In the confirmed tier `pendingRef` covered that; in the optimistic tier\n // nothing did, and `remaining` is still the pre-charge value in that closure,\n // so a single press was charged twice.\n const selfStartRef = useRef(false);\n\n const [used, setUsed] = useState(usedRef.current);\n const [playing, setPlaying] = useState(false);\n const [pending, setPending] = useState(false);\n const [elapsed, setElapsed] = useState(0);\n const [duration, setDuration] = useState(0);\n const [muted, setMuted] = useState(false);\n const [volume, setVolume] = useState(1);\n const [confirming, setConfirming] = useState(false);\n const [notice, setNotice] = useState('');\n // A repeated identical message must announce again, so the alert node is\n // REPLACED rather than mutated. Screen readers ignore an unchanged one.\n const [noticeNonce, setNoticeNonce] = useState(0);\n\n const remaining = Math.max(0, maxPlays - used);\n\n const announce = useCallback((message: string) => {\n setNotice(message);\n setNoticeNonce((n) => n + 1);\n }, []);\n\n const emit = useCallback(\n (type: string, playsUsed: number) => {\n if (!budgeted || mediaBudget === undefined || onInteraction === undefined) {\n return;\n }\n onInteraction({\n type,\n activityId: mediaBudget.activityId ?? mediaBudget.slotId,\n timestamp: Date.now(),\n payload: {\n mediaKey: mediaBudget.key,\n mediaType: 'audio',\n playsUsed,\n maxPlays,\n playsRemaining: Math.max(0, maxPlays - playsUsed),\n },\n });\n },\n [budgeted, mediaBudget, maxPlays, onInteraction],\n );\n\n const reportPosition = useCallback(\n (seconds: number) => {\n mediaBudget?.onPosition?.(mediaBudget.key, seconds);\n },\n [mediaBudget],\n );\n\n /** A start within a hair of where playback last stopped is a resume, not a play. */\n const isResume = useCallback((element: HTMLAudioElement): boolean => {\n const at = resumeAtRef.current;\n return at !== null && Math.abs(element.currentTime - at) < RESUME_EPSILON;\n }, []);\n\n const buildClaim = useCallback((): MediaPlayClaim | null => {\n if (mediaBudget === undefined) {\n return null;\n }\n return {\n key: mediaBudget.key,\n previousPlaysUsed: usedRef.current,\n playsUsed: usedRef.current + 1,\n maxPlays,\n playsRemaining: Math.max(0, maxPlays - (usedRef.current + 1)),\n slotId: mediaBudget.slotId,\n index: mediaBudget.index,\n ...(mediaBudget.activityId !== undefined ? { activityId: mediaBudget.activityId } : {}),\n };\n }, [mediaBudget, maxPlays]);\n\n const refuse = useCallback(() => {\n announce(s.noPlaysRemaining);\n emit('media-play-refused', usedRef.current);\n }, [announce, emit, s.noPlaysRemaining]);\n\n const startPlayback = useCallback(() => {\n const element = elementRef.current;\n if (element === null) {\n return;\n }\n highWaterRef.current = element.currentTime;\n selfStartRef.current = true;\n void element.play().catch(() => {\n selfStartRef.current = false;\n /* autoplay policies and races surface through the `error` handler */\n });\n }, []);\n\n const commit = useCallback(\n (claim: MediaPlayClaim) => {\n const element = elementRef.current;\n chargeRef.current = { claim, at: element?.currentTime ?? 0 };\n usedRef.current = claim.playsUsed;\n setUsed(claim.playsUsed);\n emit('media-play-consumed', claim.playsUsed);\n },\n [emit],\n );\n\n /**\n * Charges a play, then starts it.\n *\n * The confirmed tier holds playback until the consumer's atomic write\n * settles, which is the only construction that catches a second tab: two\n * mounts seeded at 0 both claim 1, and only the server can tell them apart.\n */\n const claimAndStart = useCallback(\n (element: HTMLAudioElement, alreadyPlaying: boolean) => {\n const claim = buildClaim();\n if (claim === null) {\n return;\n }\n const result = mediaBudget?.onPlayConsumed?.(claim);\n\n if (result !== undefined && typeof (result as Promise<unknown>).then === 'function') {\n if (alreadyPlaying) {\n element.pause();\n }\n pendingRef.current = true;\n setPending(true);\n void (result as Promise<{ playsUsed: number } | undefined>)\n .then((grant) => {\n const granted =\n grant !== undefined && Number.isInteger(grant.playsUsed)\n ? grant.playsUsed\n : claim.playsUsed;\n if (granted > maxPlays) {\n usedRef.current = granted;\n setUsed(granted);\n refuse();\n return;\n }\n commit({ ...claim, playsUsed: granted });\n startPlayback();\n })\n .catch(() => {\n // Nothing charged: the learner may retry.\n announce(s.playFailed);\n emit('media-play-errored', usedRef.current);\n })\n .finally(() => {\n pendingRef.current = false;\n setPending(false);\n });\n return;\n }\n\n commit(claim);\n if (!alreadyPlaying) {\n startPlayback();\n }\n },\n [\n buildClaim,\n mediaBudget,\n maxPlays,\n commit,\n startPlayback,\n refuse,\n announce,\n emit,\n s.playFailed,\n ],\n );\n\n const handleButton = useCallback(() => {\n const element = elementRef.current;\n if (element === null || pendingRef.current || disabled) {\n return;\n }\n if (!element.paused) {\n element.pause();\n return;\n }\n if (!budgeted || isResume(element)) {\n startPlayback();\n return;\n }\n if (remaining === 0) {\n refuse();\n return;\n }\n if (remaining === 1 && !confirming) {\n setConfirming(true);\n return;\n }\n setConfirming(false);\n claimAndStart(element, false);\n }, [disabled, budgeted, isResume, startPlayback, remaining, refuse, confirming, claimAndStart]);\n\n // Seed the restored position once metadata is known, BEFORE the seek guard\n // can see it — otherwise the guard reverts the very restore it was given.\n const handleLoadedMetadata = useCallback(() => {\n const element = elementRef.current;\n if (element === null) {\n return;\n }\n setDuration(Number.isFinite(element.duration) ? element.duration : 0);\n const at = mediaBudget?.entry?.at;\n if (!seededRef.current && typeof at === 'number' && Number.isFinite(at) && at > 0) {\n seededRef.current = true;\n highWaterRef.current = at;\n resumeAtRef.current = at;\n element.currentTime = at;\n setElapsed(at);\n }\n }, [mediaBudget]);\n\n /**\n * The budget lives on the ELEMENT's `play` event, not on the button, so a\n * hardware media key, a browser extension or a scripted `el.play()` is\n * charged too — and an exhausted budget stops playback inside the event,\n * before a sample is audible.\n */\n const handlePlay = useCallback(() => {\n const element = elementRef.current;\n if (element === null) {\n return;\n }\n if (disabled) {\n element.pause();\n return;\n }\n if (selfStartRef.current) {\n selfStartRef.current = false;\n setPlaying(true);\n return;\n }\n if (pendingRef.current) {\n setPlaying(true);\n return;\n }\n if (isResume(element)) {\n setPlaying(true);\n resumeAtRef.current = null;\n return;\n }\n if (!budgeted) {\n setPlaying(true);\n return;\n }\n if (remaining === 0) {\n element.pause();\n refuse();\n return;\n }\n setPlaying(true);\n claimAndStart(element, true);\n }, [disabled, isResume, budgeted, remaining, refuse, claimAndStart]);\n\n const handlePause = useCallback(() => {\n const element = elementRef.current;\n setPlaying(false);\n if (element === null) {\n return;\n }\n // Every pause records the position — learner, browser, or the pager's own\n // pause as a pane hides. Without this, paging through a six-question\n // listening group and back would exhaust the budget by navigation alone.\n resumeAtRef.current = element.currentTime;\n reportPosition(element.currentTime);\n }, [reportPosition]);\n\n const handleEnded = useCallback(() => {\n setPlaying(false);\n resumeAtRef.current = null;\n highWaterRef.current = 0;\n chargeRef.current = null;\n setElapsed(0);\n reportPosition(0);\n }, [reportPosition]);\n\n const handleTimeUpdate = useCallback(() => {\n const element = elementRef.current;\n if (element === null) {\n return;\n }\n setElapsed(element.currentTime);\n highWaterRef.current = Math.max(highWaterRef.current, element.currentTime);\n const whole = Math.floor(element.currentTime);\n if (whole !== lastPositionRef.current) {\n lastPositionRef.current = whole;\n reportPosition(element.currentTime);\n }\n }, [reportPosition]);\n\n const handleSeeking = useCallback(() => {\n const element = elementRef.current;\n if (element === null || policy.seek !== 'none') {\n return;\n }\n if (Math.abs(element.currentTime - highWaterRef.current) > SEEK_EPSILON) {\n element.currentTime = highWaterRef.current;\n announce(s.seekBlocked);\n }\n }, [policy.seek, announce, s.seekBlocked]);\n\n const handleRateChange = useCallback(() => {\n const element = elementRef.current;\n if (element === null || policy.rate !== 'fixed' || element.playbackRate === 1) {\n return;\n }\n element.playbackRate = 1;\n announce(s.rateBlocked);\n }, [policy.rate, announce, s.rateBlocked]);\n\n const handleError = useCallback(() => {\n const charge = chargeRef.current;\n const element = elementRef.current;\n // Ordering matters: an element can error with nothing charged (a bad URL\n // the learner never pressed play on), so the charge must be proven to\n // exist before its position is read.\n const progressed =\n charge !== null && element !== null && element.currentTime - charge.at >= NO_PROGRESS;\n if (charge !== null && !progressed) {\n if (mediaBudget?.onPlayRefunded !== undefined) {\n usedRef.current = charge.claim.previousPlaysUsed;\n setUsed(charge.claim.previousPlaysUsed);\n mediaBudget.onPlayRefunded(charge.claim);\n emit('media-play-refunded', charge.claim.previousPlaysUsed);\n } else {\n emit('media-play-errored', usedRef.current);\n }\n }\n chargeRef.current = null;\n setPlaying(false);\n announce(s.playFailed);\n }, [mediaBudget, emit, announce, s.playFailed]);\n\n // Keep the element's volume in step with the controls.\n useEffect(() => {\n const element = elementRef.current;\n if (element !== null) {\n element.volume = volume;\n element.muted = muted;\n }\n }, [volume, muted]);\n\n const exhausted = budgeted && remaining === 0;\n const buttonLabel = pending ? s.preparing : playing ? s.pause : s.play;\n\n return (\n <figure className=\"lk-media\" data-controls=\"minimal\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <audio\n ref={elementRef}\n className=\"lk-media-el\"\n preload=\"metadata\"\n aria-label={media.alt || undefined}\n onLoadedMetadata={handleLoadedMetadata}\n onPlay={handlePlay}\n onPause={handlePause}\n onEnded={handleEnded}\n onTimeUpdate={handleTimeUpdate}\n onSeeking={handleSeeking}\n onRateChange={handleRateChange}\n onError={handleError}\n >\n <source src={media.url} />\n {media.captionsUrl ? <track kind=\"captions\" src={media.captionsUrl} default /> : null}\n </audio>\n\n {/* A timeline runs with the audio, not with the script. Forcing LTR here\n fixes seek direction for RTL locales by construction; the surrounding\n text keeps the document direction. */}\n <div\n className=\"lk-media-transport\"\n dir=\"ltr\"\n {...(suppliedStrings !== undefined && locale !== undefined ? { lang: locale } : {})}\n >\n <button\n type=\"button\"\n className=\"lk-media-play\"\n onClick={handleButton}\n // NEVER `disabled`: a natively disabled button leaves the focus\n // order, so a learner who tabs into nothing is told nothing — and\n // disabling it mid-attempt would drop focus to <body>. Kept\n // focusable, it re-announces the refusal when pressed.\n aria-disabled={exhausted || disabled || undefined}\n aria-busy={pending || undefined}\n aria-label={exhausted ? `${s.play} — ${s.noPlaysRemaining}` : undefined}\n aria-describedby={budgeted ? statusId : undefined}\n >\n {buttonLabel}\n </button>\n\n <span className=\"lk-media-time\">\n {formatTime(elapsed)} / {formatTime(duration)}\n </span>\n\n {policy.seek === 'allow' ? (\n <input\n type=\"range\"\n className=\"lk-media-scrub\"\n min={0}\n max={duration || 0}\n step={0.1}\n value={Math.min(elapsed, duration || 0)}\n aria-label={s.seek}\n aria-valuetext={`${formatTime(elapsed)} of ${formatTime(duration)}`}\n onChange={(event) => {\n const element = elementRef.current;\n if (element !== null) {\n element.currentTime = Number(event.target.value);\n }\n }}\n />\n ) : null}\n\n <button\n type=\"button\"\n className=\"lk-media-mute\"\n aria-pressed={muted}\n onClick={() => setMuted((m) => !m)}\n >\n {muted ? s.unmute : s.mute}\n </button>\n\n <input\n type=\"range\"\n className=\"lk-media-volume\"\n min={0}\n max={1}\n step={0.05}\n value={volume}\n aria-label={s.volume}\n onChange={(event) => setVolume(Number(event.target.value))}\n />\n\n {policy.rate === 'allow' ? (\n <select\n className=\"lk-media-rate\"\n aria-label={s.speed}\n defaultValue=\"1\"\n onChange={(event) => {\n const element = elementRef.current;\n if (element !== null) {\n element.playbackRate = Number(event.target.value);\n }\n }}\n >\n {['0.75', '1', '1.25', '1.5'].map((rate) => (\n <option key={rate} value={rate}>\n {rate}×\n </option>\n ))}\n </select>\n ) : null}\n </div>\n\n {budgeted ? (\n <p className=\"lk-media-plays\" id={statusId} role=\"status\">\n {remaining > 0 ? s.playsRemaining(remaining, maxPlays) : s.noPlaysRemaining}\n </p>\n ) : null}\n\n {/* Separate from the polite status on purpose: a refusal is a direct\n response to a user action and must not be the update a screen reader\n drops when two polite regions compete. */}\n <p className=\"lk-media-notice\" role=\"alert\" key={noticeNonce}>\n {notice}\n </p>\n\n {confirming ? (\n <fieldset className=\"lk-media-confirm\">\n <legend>{s.lastPlayConfirm}</legend>\n <button\n type=\"button\"\n className=\"lk-media-confirm-start\"\n // biome-ignore lint/a11y/noAutofocus: focus must land on the confirm action the press opened, or a keyboard learner is stranded on a button whose meaning just changed\n autoFocus\n onClick={() => {\n const element = elementRef.current;\n setConfirming(false);\n if (element !== null) {\n claimAndStart(element, false);\n }\n }}\n >\n {s.lastPlayStart}\n </button>\n <button\n type=\"button\"\n className=\"lk-media-confirm-cancel\"\n onClick={() => setConfirming(false)}\n >\n {s.lastPlayCancel}\n </button>\n </fieldset>\n ) : null}\n </figure>\n );\n}\n","import type { ActivityMedia as ActivityMediaData, InteractionEvent } from '@intellectif/lk-core';\nimport { resolvePlaybackPolicy } from '@intellectif/lk-core';\nimport type { MediaBudgetBinding, MediaTransportStrings, RenderMode } from '../types.js';\nimport { AudioTransport } from './AudioTransport.js';\n\n/**\n * Presentational media block shown above a question or passage. URL-only:\n * the SDK does not host media (consumer responsibility). Accessibility:\n * images use `alt` (schema requires it non-empty); audio/video expose an\n * optional label and a captions `<track>` when `captionsUrl` is provided;\n * `embed` renders a responsive sandboxed iframe with `alt` as its required\n * accessible `title` (Req 14.5).\n *\n * Audio additionally carries a playback policy (`media.playback`). With no\n * policy the emitted element is character-identical to what every release\n * before 0.8.0 produced; with something to enforce, the browser's control bar\n * is replaced by {@link AudioTransport}.\n */\nexport interface ActivityMediaProps {\n media: ActivityMediaData;\n renderMode?: RenderMode;\n disabled?: boolean;\n locale?: string;\n mediaBudget?: MediaBudgetBinding;\n mediaStrings?: Partial<MediaTransportStrings>;\n onInteraction?: (event: InteractionEvent) => void;\n}\n\nexport function ActivityMedia(props: ActivityMediaProps): React.JSX.Element {\n const { media, renderMode = 'practice' } = props;\n\n if (media.type === 'image') {\n return (\n <figure className=\"lk-media\">\n <img className=\"lk-media-el\" src={media.url} alt={media.alt ?? ''} />\n </figure>\n );\n }\n\n if (media.type === 'audio') {\n const policy = resolvePlaybackPolicy(media);\n\n // BEFORE the branch below, not after it. A budgeted policy always resolves\n // to `controls: 'minimal'`, so a guard placed after that early return\n // could never fire for the one case it exists to catch — the transport\n // rendered happily with no binding, counting nothing, while the paper\n // believed it had a budget.\n if (policy.maxPlays !== null && renderMode === 'exam' && props.mediaBudget === undefined) {\n throw new Error(\n `ActivityMedia: audio ${JSON.stringify(media.url)} declares maxPlays and is rendered in ` +\n 'renderMode \"exam\" without a `mediaBudget` binding. Nothing would persist the count, so ' +\n 'a refresh restores the full budget while the page says plays remain. Render it through ' +\n '<ActivitySequence mediaBudget={…}> or pass `mediaBudget` yourself.',\n );\n }\n\n // REVIEW ALWAYS GETS THE NATIVE BAR. The paper is graded; a learner\n // reviewing it cannot change an answer by listening again, so taking away\n // their scrubber, their speed control and their browser-localized controls\n // buys nothing — and costs a learner with a processing disability the\n // ability to work out what they got wrong.\n if (policy.controls === 'minimal' && renderMode !== 'review') {\n return <AudioTransport {...props} media={media} policy={policy} />;\n }\n\n const hints = renderMode === 'review' ? [] : policy.nativeControlHints;\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <audio\n className=\"lk-media-el\"\n controls\n aria-label={media.alt || undefined}\n {...(hints.length > 0\n ? {\n controlsList: hints\n .map((hint) => (hint === 'hide-download' ? 'nodownload' : 'noplaybackrate'))\n .join(' '),\n }\n : {})}\n >\n <source src={media.url} />\n {media.captionsUrl ? <track kind=\"captions\" src={media.captionsUrl} default /> : null}\n </audio>\n </figure>\n );\n }\n\n if (media.type === 'embed') {\n return (\n <figure className=\"lk-media\">\n <div className=\"lk-media-embed\">\n <iframe\n className=\"lk-media-el\"\n src={media.url}\n title={media.alt ?? 'Embedded media'}\n loading=\"lazy\"\n referrerPolicy=\"strict-origin-when-cross-origin\"\n // The docblock always promised a sandboxed iframe; now it is one.\n // allow-scripts + allow-same-origin are required by provider\n // players (YouTube/Vimeo); top-navigation, forms, downloads and\n // popups stay blocked. Scheme allow-listing in MediaSchema is the\n // primary defense; this is depth.\n sandbox=\"allow-scripts allow-same-origin allow-presentation\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowFullScreen\n />\n </div>\n </figure>\n );\n }\n\n return (\n <figure className=\"lk-media\">\n {/* biome-ignore lint/a11y/useMediaCaption: captions are optional in the data contract — a <track> is rendered when captionsUrl is provided; absence is the author's documented choice (Req 14.5) */}\n <video className=\"lk-media-el\" controls aria-label={media.alt || undefined}>\n <source src={media.url} />\n {media.captionsUrl ? <track kind=\"captions\" src={media.captionsUrl} default /> : null}\n </video>\n </figure>\n );\n}\n"]}
|