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/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.5",
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 = (
@@ -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
- playAllVideo(elements.slice(1), then);
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
- playAllVideo(elements.slice(1), then);
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
- playAllVideo(elements.slice(1), then);
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 > 1) parent.removeChild(divs[0]);
1462
+ while (divs.length > 0) parent.removeChild(divs[0]);
1357
1463
  }
1358
1464
  }