bloom-player 2.20.0-alpha.1 → 2.20.0-alpha.2
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-DHiLPE1q.css → bloomPlayer-CpTXugnQ.css} +1 -1
- package/dist/{bloomPlayer.Bmk9o7f9.js → bloomPlayer.CdHp5c3Q.js} +23 -23
- package/dist/bloomplayer.htm +2 -2
- package/lib/narration.d.ts +6 -1
- package/lib/shared.es.js +1768 -1733
- package/lib/shared.es.js.map +1 -1
- package/package.json +1 -1
- package/src/shared/narration.ts +91 -5
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.20.0-alpha.
|
|
6
|
+
"version": "2.20.0-alpha.2",
|
|
7
7
|
"packageManager": "pnpm@11.1.2",
|
|
8
8
|
"private": false,
|
|
9
9
|
"// sideeffects might need to be ['*.css'] to avoid 'shaking' our CSS, if we ever get tree shaking working": "",
|
package/src/shared/narration.ts
CHANGED
|
@@ -226,6 +226,8 @@ export const PlayCompleted = new LiteEvent<HTMLElement>();
|
|
|
226
226
|
// Raised when we can't play narration, specifically because the browser won't allow it until
|
|
227
227
|
// the user has interacted with the page.
|
|
228
228
|
export const PlayFailed = new LiteEvent<HTMLElement>();
|
|
229
|
+
// Raised when a user interaction resolves an autoplay-blocked state and media is about to resume.
|
|
230
|
+
export const PlayUnblocked = new LiteEvent<void>();
|
|
229
231
|
|
|
230
232
|
// This event allows Narration to inform its controllers when we start/stop reading
|
|
231
233
|
// image descriptions. It is raised for each segment we read and passed true if the one
|
|
@@ -1271,6 +1273,7 @@ export function hidingPage() {
|
|
|
1271
1273
|
|
|
1272
1274
|
let playAllVideoGeneration = 0;
|
|
1273
1275
|
let activePlayAllVideoElement: HTMLVideoElement | undefined;
|
|
1276
|
+
const transientVideoRetryCounts = new WeakMap<HTMLVideoElement, number>();
|
|
1274
1277
|
|
|
1275
1278
|
export function stopPlayAllVideoPlayback() {
|
|
1276
1279
|
playAllVideoGeneration++;
|
|
@@ -1281,12 +1284,26 @@ export function stopPlayAllVideoPlayback() {
|
|
|
1281
1284
|
}
|
|
1282
1285
|
}
|
|
1283
1286
|
|
|
1287
|
+
export function isTransientVideoPlayFailure(reason: any): boolean {
|
|
1288
|
+
if (!reason) {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
if (reason.name === "AbortError") {
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
const message =
|
|
1295
|
+
(typeof reason.message === "string" && reason.message) ||
|
|
1296
|
+
(typeof reason.toString === "function" ? reason.toString() : "");
|
|
1297
|
+
return message.includes("interrupted by a call to pause()");
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1284
1300
|
// Attempt to show a video's first frame by briefly starting playback and then pausing.
|
|
1285
1301
|
// The callback allows callers to decide at the moment playback starts whether pausing
|
|
1286
1302
|
// is still desired (for example, page-level logic may stop pausing after initial load).
|
|
1287
1303
|
export function showVideoFirstFrameWhenReady(
|
|
1288
1304
|
video: HTMLVideoElement,
|
|
1289
1305
|
shouldPauseAfterPlaying: () => boolean = () => true,
|
|
1306
|
+
onAutoplayBlocked: () => void = () => showVideoAutoplayBlockedHint(video),
|
|
1290
1307
|
) {
|
|
1291
1308
|
let canceled = false;
|
|
1292
1309
|
const attempt = () => {
|
|
@@ -1300,6 +1317,7 @@ export function showVideoFirstFrameWhenReady(
|
|
|
1300
1317
|
}
|
|
1301
1318
|
const playingListener = () => {
|
|
1302
1319
|
window.clearTimeout(removePlayingListenerTimeout);
|
|
1320
|
+
hideVideoAutoplayBlockedHint(video);
|
|
1303
1321
|
if (!shouldPauseAfterPlaying()) {
|
|
1304
1322
|
return;
|
|
1305
1323
|
}
|
|
@@ -1332,13 +1350,20 @@ export function showVideoFirstFrameWhenReady(
|
|
|
1332
1350
|
const removePlayingListenerTimeout = window.setTimeout(() => {
|
|
1333
1351
|
video.removeEventListener("playing", playingListener);
|
|
1334
1352
|
}, 1000);
|
|
1353
|
+
hideVideoAutoplayBlockedHint(video);
|
|
1335
1354
|
const promise = video.play();
|
|
1336
1355
|
if (promise && promise.catch) {
|
|
1337
|
-
promise.catch(() => {
|
|
1356
|
+
promise.catch((reason) => {
|
|
1338
1357
|
window.clearTimeout(removePlayingListenerTimeout);
|
|
1339
1358
|
// If play() fails (e.g., autoplay policy), remove the listener
|
|
1340
1359
|
// so it won't interfere if playback starts later by other means.
|
|
1341
1360
|
video.removeEventListener("playing", playingListener);
|
|
1361
|
+
// If autoplay is blocked, remove our transparent poster so a decoded
|
|
1362
|
+
// first frame can be shown when available.
|
|
1363
|
+
if (reason?.name === "NotAllowedError") {
|
|
1364
|
+
video.removeAttribute("poster");
|
|
1365
|
+
onAutoplayBlocked();
|
|
1366
|
+
}
|
|
1342
1367
|
});
|
|
1343
1368
|
}
|
|
1344
1369
|
};
|
|
@@ -1378,6 +1403,32 @@ export function playAllVideo(elements: HTMLVideoElement[], then: () => void) {
|
|
|
1378
1403
|
playAllVideoInternal(elements, then, generation);
|
|
1379
1404
|
}
|
|
1380
1405
|
|
|
1406
|
+
export function isDefiniteVideoPlaybackSupportFailure(
|
|
1407
|
+
video: HTMLVideoElement,
|
|
1408
|
+
reason?: any,
|
|
1409
|
+
): boolean {
|
|
1410
|
+
if (
|
|
1411
|
+
video.networkState === HTMLMediaElement.NETWORK_NO_SOURCE &&
|
|
1412
|
+
video.readyState === HTMLMediaElement.HAVE_NOTHING
|
|
1413
|
+
) {
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
if (!reason) {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (reason.name === "NotSupportedError") {
|
|
1422
|
+
return true;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Some browsers expose codec/source failures only through media.error.
|
|
1426
|
+
return (
|
|
1427
|
+
video.error?.code ===
|
|
1428
|
+
(window.MediaError ? window.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED : 4)
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1381
1432
|
function playAllVideoInternal(
|
|
1382
1433
|
elements: HTMLVideoElement[],
|
|
1383
1434
|
then: () => void,
|
|
@@ -1395,14 +1446,13 @@ function playAllVideoInternal(
|
|
|
1395
1446
|
activePlayAllVideoElement = video;
|
|
1396
1447
|
|
|
1397
1448
|
// If there is an error, try to continue with the next video.
|
|
1398
|
-
if (
|
|
1399
|
-
video
|
|
1400
|
-
video.readyState === HTMLMediaElement.HAVE_NOTHING
|
|
1401
|
-
) {
|
|
1449
|
+
if (isDefiniteVideoPlaybackSupportFailure(video)) {
|
|
1450
|
+
transientVideoRetryCounts.delete(video);
|
|
1402
1451
|
showVideoError(video);
|
|
1403
1452
|
playAllVideoInternal(elements.slice(1), then, generation);
|
|
1404
1453
|
} else {
|
|
1405
1454
|
hideVideoError(video);
|
|
1455
|
+
hideVideoAutoplayBlockedHint(video);
|
|
1406
1456
|
setCurrentPlaybackMode(PlaybackMode.VideoPlaying);
|
|
1407
1457
|
const promise = video.play();
|
|
1408
1458
|
promise
|
|
@@ -1410,6 +1460,8 @@ function playAllVideoInternal(
|
|
|
1410
1460
|
if (generation !== playAllVideoGeneration) {
|
|
1411
1461
|
return;
|
|
1412
1462
|
}
|
|
1463
|
+
transientVideoRetryCounts.delete(video);
|
|
1464
|
+
hideVideoAutoplayBlockedHint(video);
|
|
1413
1465
|
// The promise resolves when the video starts playing. We want to know when it ends.
|
|
1414
1466
|
// Note: in Bloom Desktop, sometimes this event does not fire normally, even when the video is
|
|
1415
1467
|
// played to the end. I have not figured out why. It may be something to do with how we are
|
|
@@ -1438,7 +1490,25 @@ function playAllVideoInternal(
|
|
|
1438
1490
|
if (generation !== playAllVideoGeneration) {
|
|
1439
1491
|
return;
|
|
1440
1492
|
}
|
|
1493
|
+
if (reason?.name === "NotAllowedError") {
|
|
1494
|
+
console.debug(
|
|
1495
|
+
"Video autoplay blocked until user interaction",
|
|
1496
|
+
);
|
|
1497
|
+
showVideoAutoplayBlockedHint(video);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
const retries = transientVideoRetryCounts.get(video) ?? 0;
|
|
1501
|
+
if (isTransientVideoPlayFailure(reason)) {
|
|
1502
|
+
if (retries < 2) {
|
|
1503
|
+
transientVideoRetryCounts.set(video, retries + 1);
|
|
1504
|
+
window.setTimeout(() => {
|
|
1505
|
+
playAllVideoInternal(elements, then, generation);
|
|
1506
|
+
}, 50);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1441
1510
|
console.error("Video play failed", reason);
|
|
1511
|
+
transientVideoRetryCounts.delete(video);
|
|
1442
1512
|
showVideoError(video);
|
|
1443
1513
|
playAllVideoInternal(elements.slice(1), then, generation);
|
|
1444
1514
|
});
|
|
@@ -1467,10 +1537,26 @@ export function showVideoError(video: HTMLVideoElement): void {
|
|
|
1467
1537
|
msgDiv.style.top = "10%";
|
|
1468
1538
|
msgDiv.style.width = "80%";
|
|
1469
1539
|
msgDiv.style.fontSize = "x-large";
|
|
1540
|
+
msgDiv.style.pointerEvents = "none";
|
|
1470
1541
|
parent.appendChild(msgDiv);
|
|
1471
1542
|
}
|
|
1472
1543
|
}
|
|
1473
1544
|
}
|
|
1545
|
+
|
|
1546
|
+
export function showVideoAutoplayBlockedHint(video: HTMLVideoElement): void {
|
|
1547
|
+
const container =
|
|
1548
|
+
(video.closest(".bloom-videoContainer") as HTMLElement | null) ||
|
|
1549
|
+
video.parentElement;
|
|
1550
|
+
container?.classList.add("autoplayBlocked");
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
export function hideVideoAutoplayBlockedHint(video: HTMLVideoElement): void {
|
|
1554
|
+
const container =
|
|
1555
|
+
(video.closest(".bloom-videoContainer") as HTMLElement | null) ||
|
|
1556
|
+
video.parentElement;
|
|
1557
|
+
container?.classList.remove("autoplayBlocked");
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1474
1560
|
export function hideVideoError(video: HTMLVideoElement): void {
|
|
1475
1561
|
const parent = video.parentElement;
|
|
1476
1562
|
if (parent) {
|