bloom-player 2.19.5 → 2.19.6
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/dist/bloomPlayer.Ds4dC-C9.js +8060 -0
- package/dist/bloomplayer.htm +1 -1
- package/lib/narration.d.ts +2 -0
- package/lib/shared.es.js +2030 -1972
- package/lib/shared.es.js.map +1 -1
- package/package.json +1 -1
- package/src/shared/dragActivityRuntime.ts +32 -0
- package/src/shared/narration.ts +110 -4
- package/dist/bloomPlayer.ChhMHDsd.js +0 -8060
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "A library for displaying Bloom books in iframes or WebViews",
|
|
4
4
|
"author": "SIL Global",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "2.19.
|
|
6
|
+
"version": "2.19.6",
|
|
7
7
|
"private": false,
|
|
8
8
|
"// sideeffects might need to be ['*.css'] to avoid 'shaking' our CSS, if we ever get tree shaking working": "",
|
|
9
9
|
"sideEffects": false,
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
kAudioSentence,
|
|
18
18
|
playAllAudio,
|
|
19
19
|
playAllVideo,
|
|
20
|
+
showVideoFirstFrameWhenReady,
|
|
21
|
+
stopPlayAllVideoPlayback,
|
|
20
22
|
urlPrefix,
|
|
21
23
|
} from "./narration";
|
|
22
24
|
|
|
@@ -175,6 +177,13 @@ export function prepareActivity(
|
|
|
175
177
|
// non-draggable video click detectors are handled separately, see handleVideoClick in video.ts,
|
|
176
178
|
// and in BloomDesktop handleVideoClick in bloomVideo.ts.
|
|
177
179
|
video.addEventListener("pointerdown", playVideo);
|
|
180
|
+
// Ensure the first frame is visible. The transparent poster (set globally by
|
|
181
|
+
// bloom-player at book load) hides the video until playback begins. Non-draggable
|
|
182
|
+
// videos get a play+pause first-frame trick in video.ts HandlePageVisible, but
|
|
183
|
+
// draggable videos are skipped there. If the video source hasn't loaded yet
|
|
184
|
+
// (e.g. cold cache after a build), play() fails silently, leaving the video blank.
|
|
185
|
+
// We use a loadeddata listener so the trick runs whenever the data is available.
|
|
186
|
+
showVideoFirstFrameWhenReady(video);
|
|
178
187
|
}
|
|
179
188
|
});
|
|
180
189
|
|
|
@@ -272,6 +281,7 @@ const playVideo = (e: MouseEvent) => {
|
|
|
272
281
|
// May also be useful to do when switching pages in player. If not, we may want to move
|
|
273
282
|
// this out of this runtime file; but it's nice to keep it with prepareActivity.
|
|
274
283
|
export function undoPrepareActivity(page: HTMLElement) {
|
|
284
|
+
stopPlayAllVideoPlayback();
|
|
275
285
|
restorePositions();
|
|
276
286
|
// In case we do more editing after leaving the Play tab, we don't want to restore the same positions again
|
|
277
287
|
// if we leave the page completely.
|
|
@@ -631,6 +641,26 @@ const showCorrect = (e: MouseEvent) => {
|
|
|
631
641
|
});
|
|
632
642
|
classSetter(currentPage!, "drag-activity-wrong", false);
|
|
633
643
|
classSetter(currentPage!, "drag-activity-solution", true);
|
|
644
|
+
|
|
645
|
+
// Play any videos that are part of a correct answer, in document order.
|
|
646
|
+
const videoElements: HTMLVideoElement[] = [];
|
|
647
|
+
currentPage!
|
|
648
|
+
.querySelectorAll("[data-draggable-id]")
|
|
649
|
+
.forEach((elt: HTMLElement) => {
|
|
650
|
+
const targetId = elt.getAttribute("data-draggable-id");
|
|
651
|
+
const target = currentPage?.querySelector(
|
|
652
|
+
`[data-target-of="${targetId}"]`,
|
|
653
|
+
) as HTMLElement;
|
|
654
|
+
if (!target) {
|
|
655
|
+
return; // not a required draggable
|
|
656
|
+
}
|
|
657
|
+
videoElements.push(
|
|
658
|
+
...Array.from(elt.getElementsByTagName("video")),
|
|
659
|
+
);
|
|
660
|
+
});
|
|
661
|
+
if (videoElements.length > 0) {
|
|
662
|
+
playAllVideo(videoElements, () => {});
|
|
663
|
+
}
|
|
634
664
|
};
|
|
635
665
|
|
|
636
666
|
// where the mouse started the drag, relative to the top left of dragTarget
|
|
@@ -783,6 +813,8 @@ export const performTryAgain = (e: MouseEvent) => {
|
|
|
783
813
|
classSetter(page, "drag-activity-solution", false);
|
|
784
814
|
// Restore everything to the starting positions. BL-14482.
|
|
785
815
|
restorePositions();
|
|
816
|
+
// If we're still playing video, e.g. a 'wrong' video, stop it.
|
|
817
|
+
stopPlayAllVideoPlayback();
|
|
786
818
|
};
|
|
787
819
|
|
|
788
820
|
export const classSetter = (
|
package/src/shared/narration.ts
CHANGED
|
@@ -1266,6 +1266,85 @@ export function hidingPage() {
|
|
|
1266
1266
|
// If it DOES have audio, a pause here can interfere with playing it.
|
|
1267
1267
|
//pausePlaying(); // Doesn't set AudioPaused state. Caller sets NewPage state.
|
|
1268
1268
|
clearTimeout(fakeNarrationTimer);
|
|
1269
|
+
stopPlayAllVideoPlayback();
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
let playAllVideoGeneration = 0;
|
|
1273
|
+
let activePlayAllVideoElement: HTMLVideoElement | undefined;
|
|
1274
|
+
|
|
1275
|
+
export function stopPlayAllVideoPlayback() {
|
|
1276
|
+
playAllVideoGeneration++;
|
|
1277
|
+
if (activePlayAllVideoElement) {
|
|
1278
|
+
activePlayAllVideoElement.pause();
|
|
1279
|
+
activePlayAllVideoElement.currentTime = 0;
|
|
1280
|
+
activePlayAllVideoElement = undefined;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Attempt to show a video's first frame by briefly starting playback and then pausing.
|
|
1285
|
+
// The callback allows callers to decide at the moment playback starts whether pausing
|
|
1286
|
+
// is still desired (for example, page-level logic may stop pausing after initial load).
|
|
1287
|
+
export function showVideoFirstFrameWhenReady(
|
|
1288
|
+
video: HTMLVideoElement,
|
|
1289
|
+
shouldPauseAfterPlaying: () => boolean = () => true,
|
|
1290
|
+
) {
|
|
1291
|
+
let canceled = false;
|
|
1292
|
+
const attempt = () => {
|
|
1293
|
+
if (canceled) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
// If something else has already started playback, don't attach our
|
|
1297
|
+
// pause-on-playing behavior.
|
|
1298
|
+
if (!video.paused) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const playingListener = () => {
|
|
1302
|
+
window.clearTimeout(removePlayingListenerTimeout);
|
|
1303
|
+
if (!shouldPauseAfterPlaying()) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
// Pause after about one frame so the first frame stays visible.
|
|
1307
|
+
setTimeout(() => {
|
|
1308
|
+
if (shouldPauseAfterPlaying()) {
|
|
1309
|
+
video.pause();
|
|
1310
|
+
}
|
|
1311
|
+
}, 4);
|
|
1312
|
+
};
|
|
1313
|
+
video.addEventListener("playing", playingListener, { once: true });
|
|
1314
|
+
// If our play() attempt doesn't lead to playback soon, remove the
|
|
1315
|
+
// listener so it can't affect unrelated later playback.
|
|
1316
|
+
const removePlayingListenerTimeout = window.setTimeout(() => {
|
|
1317
|
+
video.removeEventListener("playing", playingListener);
|
|
1318
|
+
}, 1000);
|
|
1319
|
+
const promise = video.play();
|
|
1320
|
+
if (promise && promise.catch) {
|
|
1321
|
+
promise.catch(() => {
|
|
1322
|
+
window.clearTimeout(removePlayingListenerTimeout);
|
|
1323
|
+
// If play() fails (e.g., autoplay policy), remove the listener
|
|
1324
|
+
// so it won't interfere if playback starts later by other means.
|
|
1325
|
+
video.removeEventListener("playing", playingListener);
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
// HAVE_CURRENT_DATA (2) means at least the first frame is decoded.
|
|
1330
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
|
|
1331
|
+
attempt();
|
|
1332
|
+
} else {
|
|
1333
|
+
let cancelAttempt: () => void;
|
|
1334
|
+
const onLoadedData = () => {
|
|
1335
|
+
video.removeEventListener("play", cancelAttempt);
|
|
1336
|
+
attempt();
|
|
1337
|
+
};
|
|
1338
|
+
cancelAttempt = () => {
|
|
1339
|
+
canceled = true;
|
|
1340
|
+
video.removeEventListener("loadeddata", onLoadedData);
|
|
1341
|
+
video.removeEventListener("play", cancelAttempt);
|
|
1342
|
+
};
|
|
1343
|
+
// If something requests playback before the video has loaded enough data,
|
|
1344
|
+
// don't run this first-frame trick when loadeddata eventually fires.
|
|
1345
|
+
video.addEventListener("play", cancelAttempt, { once: true });
|
|
1346
|
+
video.addEventListener("loadeddata", onLoadedData, { once: true });
|
|
1347
|
+
}
|
|
1269
1348
|
}
|
|
1270
1349
|
|
|
1271
1350
|
// Play the specified elements, one after the other. When the last completes (or at once if the array is empty),
|
|
@@ -1279,11 +1358,25 @@ export function hidingPage() {
|
|
|
1279
1358
|
// (This function would be more natural in video.ts. But at least for now I'm trying to minimize the
|
|
1280
1359
|
// number of source files shared with Bloom Desktop, and we need this for Bloom Games.)
|
|
1281
1360
|
export function playAllVideo(elements: HTMLVideoElement[], then: () => void) {
|
|
1361
|
+
const generation = ++playAllVideoGeneration;
|
|
1362
|
+
playAllVideoInternal(elements, then, generation);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function playAllVideoInternal(
|
|
1366
|
+
elements: HTMLVideoElement[],
|
|
1367
|
+
then: () => void,
|
|
1368
|
+
generation: number,
|
|
1369
|
+
) {
|
|
1370
|
+
if (generation !== playAllVideoGeneration) {
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1282
1373
|
if (elements.length === 0) {
|
|
1374
|
+
activePlayAllVideoElement = undefined;
|
|
1283
1375
|
then();
|
|
1284
1376
|
return;
|
|
1285
1377
|
}
|
|
1286
1378
|
const video = elements[0];
|
|
1379
|
+
activePlayAllVideoElement = video;
|
|
1287
1380
|
|
|
1288
1381
|
// If there is an error, try to continue with the next video.
|
|
1289
1382
|
if (
|
|
@@ -1291,13 +1384,16 @@ export function playAllVideo(elements: HTMLVideoElement[], then: () => void) {
|
|
|
1291
1384
|
video.readyState === HTMLMediaElement.HAVE_NOTHING
|
|
1292
1385
|
) {
|
|
1293
1386
|
showVideoError(video);
|
|
1294
|
-
|
|
1387
|
+
playAllVideoInternal(elements.slice(1), then, generation);
|
|
1295
1388
|
} else {
|
|
1296
1389
|
hideVideoError(video);
|
|
1297
1390
|
setCurrentPlaybackMode(PlaybackMode.VideoPlaying);
|
|
1298
1391
|
const promise = video.play();
|
|
1299
1392
|
promise
|
|
1300
1393
|
.then(() => {
|
|
1394
|
+
if (generation !== playAllVideoGeneration) {
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1301
1397
|
// The promise resolves when the video starts playing. We want to know when it ends.
|
|
1302
1398
|
// Note: in Bloom Desktop, sometimes this event does not fire normally, even when the video is
|
|
1303
1399
|
// played to the end. I have not figured out why. It may be something to do with how we are
|
|
@@ -1310,15 +1406,25 @@ export function playAllVideo(elements: HTMLVideoElement[], then: () => void) {
|
|
|
1310
1406
|
video.addEventListener(
|
|
1311
1407
|
"ended",
|
|
1312
1408
|
() => {
|
|
1313
|
-
|
|
1409
|
+
if (generation !== playAllVideoGeneration) {
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
playAllVideoInternal(
|
|
1413
|
+
elements.slice(1),
|
|
1414
|
+
then,
|
|
1415
|
+
generation,
|
|
1416
|
+
);
|
|
1314
1417
|
},
|
|
1315
1418
|
{ once: true },
|
|
1316
1419
|
);
|
|
1317
1420
|
})
|
|
1318
1421
|
.catch((reason) => {
|
|
1422
|
+
if (generation !== playAllVideoGeneration) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1319
1425
|
console.error("Video play failed", reason);
|
|
1320
1426
|
showVideoError(video);
|
|
1321
|
-
|
|
1427
|
+
playAllVideoInternal(elements.slice(1), then, generation);
|
|
1322
1428
|
});
|
|
1323
1429
|
}
|
|
1324
1430
|
}
|
|
@@ -1353,6 +1459,6 @@ export function hideVideoError(video: HTMLVideoElement): void {
|
|
|
1353
1459
|
const parent = video.parentElement;
|
|
1354
1460
|
if (parent) {
|
|
1355
1461
|
const divs = parent.getElementsByClassName("video-error-message");
|
|
1356
|
-
while (divs.length >
|
|
1462
|
+
while (divs.length > 0) parent.removeChild(divs[0]);
|
|
1357
1463
|
}
|
|
1358
1464
|
}
|