@scarlett-player/embed 0.4.0 → 0.5.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/dist/embed.audio.js +22 -1
- package/dist/embed.audio.js.map +1 -1
- package/dist/embed.audio.umd.cjs +1 -1
- package/dist/embed.audio.umd.cjs.map +1 -1
- package/dist/embed.js +1245 -52
- package/dist/embed.js.map +1 -1
- package/dist/embed.umd.cjs +1 -1
- package/dist/embed.umd.cjs.map +1 -1
- package/dist/embed.video.js +1245 -52
- package/dist/embed.video.js.map +1 -1
- package/dist/embed.video.umd.cjs +1 -1
- package/dist/embed.video.umd.cjs.map +1 -1
- package/package.json +8 -8
package/dist/embed.js
CHANGED
|
@@ -144,6 +144,9 @@ function setupVideoEventHandlers(video, api) {
|
|
|
144
144
|
video.addEventListener(event, handler);
|
|
145
145
|
handlers.push({ event, handler });
|
|
146
146
|
};
|
|
147
|
+
addHandler("play", () => {
|
|
148
|
+
api.setState("paused", false);
|
|
149
|
+
});
|
|
147
150
|
addHandler("playing", () => {
|
|
148
151
|
api.setState("playing", true);
|
|
149
152
|
api.setState("paused", false);
|
|
@@ -394,7 +397,9 @@ function createHLSPlugin(config) {
|
|
|
394
397
|
const getRetryDelay = (retryCount) => {
|
|
395
398
|
const baseDelay = mergedConfig.retryDelayMs ?? 1e3;
|
|
396
399
|
const backoffFactor = mergedConfig.retryBackoffFactor ?? 2;
|
|
397
|
-
|
|
400
|
+
const delay = baseDelay * Math.pow(backoffFactor, retryCount);
|
|
401
|
+
const jitter = delay * (0.7 + Math.random() * 0.3);
|
|
402
|
+
return jitter;
|
|
398
403
|
};
|
|
399
404
|
const emitFatalError = (error, retriesExhausted) => {
|
|
400
405
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
@@ -608,6 +613,20 @@ function createHLSPlugin(config) {
|
|
|
608
613
|
if (!isNaN(levelIndex) && levelIndex >= 0 && levelIndex < hls.levels.length) {
|
|
609
614
|
hls.nextLevel = levelIndex;
|
|
610
615
|
api?.logger.debug(`Quality: queued switch to level ${levelIndex}`);
|
|
616
|
+
const targetLevel = hls.levels[levelIndex];
|
|
617
|
+
if (targetLevel) {
|
|
618
|
+
const label = formatLevel(targetLevel);
|
|
619
|
+
api?.setState("currentQuality", {
|
|
620
|
+
id: `level-${levelIndex}`,
|
|
621
|
+
label: `${label}...`,
|
|
622
|
+
// Ellipsis indicates switching in progress
|
|
623
|
+
width: targetLevel.width,
|
|
624
|
+
height: targetLevel.height,
|
|
625
|
+
bitrate: targetLevel.bitrate,
|
|
626
|
+
active: false
|
|
627
|
+
// Not yet active
|
|
628
|
+
});
|
|
629
|
+
}
|
|
611
630
|
}
|
|
612
631
|
}
|
|
613
632
|
});
|
|
@@ -869,7 +888,12 @@ var styles = `
|
|
|
869
888
|
transition: height 0.15s ease;
|
|
870
889
|
}
|
|
871
890
|
|
|
872
|
-
|
|
891
|
+
@media (hover: hover) {
|
|
892
|
+
.sp-progress-wrapper:hover .sp-progress {
|
|
893
|
+
height: 5px;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
873
897
|
.sp-progress--dragging {
|
|
874
898
|
height: 5px;
|
|
875
899
|
}
|
|
@@ -915,11 +939,34 @@ var styles = `
|
|
|
915
939
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
916
940
|
}
|
|
917
941
|
|
|
918
|
-
|
|
942
|
+
@media (hover: hover) {
|
|
943
|
+
.sp-progress-wrapper:hover .sp-progress__handle {
|
|
944
|
+
transform: translate(-50%, -50%) scale(1);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
919
948
|
.sp-progress--dragging .sp-progress__handle {
|
|
920
949
|
transform: translate(-50%, -50%) scale(1);
|
|
921
950
|
}
|
|
922
951
|
|
|
952
|
+
/* Thumbnail Preview */
|
|
953
|
+
.sp-thumbnail-preview {
|
|
954
|
+
position: absolute;
|
|
955
|
+
bottom: calc(100% + 8px);
|
|
956
|
+
transform: translateX(-50%);
|
|
957
|
+
pointer-events: none;
|
|
958
|
+
display: none;
|
|
959
|
+
z-index: 21;
|
|
960
|
+
border-radius: 4px;
|
|
961
|
+
overflow: hidden;
|
|
962
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
|
963
|
+
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.sp-thumbnail-preview__img {
|
|
967
|
+
background-repeat: no-repeat;
|
|
968
|
+
}
|
|
969
|
+
|
|
923
970
|
/* Progress Tooltip */
|
|
924
971
|
.sp-progress__tooltip {
|
|
925
972
|
position: absolute;
|
|
@@ -939,8 +986,10 @@ var styles = `
|
|
|
939
986
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
940
987
|
}
|
|
941
988
|
|
|
942
|
-
|
|
943
|
-
|
|
989
|
+
@media (hover: hover) {
|
|
990
|
+
.sp-progress-wrapper:hover .sp-progress__tooltip {
|
|
991
|
+
opacity: 1;
|
|
992
|
+
}
|
|
944
993
|
}
|
|
945
994
|
|
|
946
995
|
/* ============================================
|
|
@@ -960,9 +1009,11 @@ var styles = `
|
|
|
960
1009
|
flex-shrink: 0;
|
|
961
1010
|
}
|
|
962
1011
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1012
|
+
@media (hover: hover) {
|
|
1013
|
+
.sp-control:hover {
|
|
1014
|
+
color: #fff;
|
|
1015
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1016
|
+
}
|
|
966
1017
|
}
|
|
967
1018
|
|
|
968
1019
|
.sp-control:active {
|
|
@@ -1031,7 +1082,12 @@ var styles = `
|
|
|
1031
1082
|
transition: width 0.2s ease;
|
|
1032
1083
|
}
|
|
1033
1084
|
|
|
1034
|
-
|
|
1085
|
+
@media (hover: hover) {
|
|
1086
|
+
.sp-volume:hover .sp-volume__slider-wrap {
|
|
1087
|
+
width: 64px;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1035
1091
|
.sp-volume:focus-within .sp-volume__slider-wrap {
|
|
1036
1092
|
width: 64px;
|
|
1037
1093
|
}
|
|
@@ -1074,8 +1130,10 @@ var styles = `
|
|
|
1074
1130
|
transition: background 0.15s ease, opacity 0.15s ease;
|
|
1075
1131
|
}
|
|
1076
1132
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1133
|
+
@media (hover: hover) {
|
|
1134
|
+
.sp-live:hover {
|
|
1135
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1136
|
+
}
|
|
1079
1137
|
}
|
|
1080
1138
|
|
|
1081
1139
|
.sp-live__dot {
|
|
@@ -1094,6 +1152,16 @@ var styles = `
|
|
|
1094
1152
|
animation: none;
|
|
1095
1153
|
}
|
|
1096
1154
|
|
|
1155
|
+
.sp-live--behind span {
|
|
1156
|
+
text-decoration: underline;
|
|
1157
|
+
text-underline-offset: 2px;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/* Progress bar live mode: accent color for filled bar */
|
|
1161
|
+
.sp-progress--live .sp-progress__filled {
|
|
1162
|
+
background: var(--sp-accent, #e50914);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1097
1165
|
@keyframes sp-pulse {
|
|
1098
1166
|
0%, 100% { opacity: 1; }
|
|
1099
1167
|
50% { opacity: 0.4; }
|
|
@@ -1174,6 +1242,169 @@ var styles = `
|
|
|
1174
1242
|
opacity: 1;
|
|
1175
1243
|
}
|
|
1176
1244
|
|
|
1245
|
+
/* ============================================
|
|
1246
|
+
Settings Menu (Gear Icon)
|
|
1247
|
+
============================================ */
|
|
1248
|
+
.sp-settings {
|
|
1249
|
+
position: relative;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.sp-settings__btn {
|
|
1253
|
+
display: flex;
|
|
1254
|
+
align-items: center;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.sp-settings-panel {
|
|
1258
|
+
position: absolute;
|
|
1259
|
+
bottom: calc(100% + 8px);
|
|
1260
|
+
right: 0;
|
|
1261
|
+
background: rgba(20, 20, 20, 0.95);
|
|
1262
|
+
backdrop-filter: blur(8px);
|
|
1263
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1264
|
+
border-radius: 8px;
|
|
1265
|
+
min-width: 200px;
|
|
1266
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
|
1267
|
+
opacity: 0;
|
|
1268
|
+
visibility: hidden;
|
|
1269
|
+
transform: translateY(8px);
|
|
1270
|
+
transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
|
|
1271
|
+
z-index: 20;
|
|
1272
|
+
overflow: hidden;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
.sp-settings-panel--open {
|
|
1276
|
+
opacity: 1;
|
|
1277
|
+
visibility: visible;
|
|
1278
|
+
transform: translateY(0);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/* Main menu rows */
|
|
1282
|
+
.sp-settings-panel--main {
|
|
1283
|
+
padding: 4px 0;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
.sp-settings-panel__row {
|
|
1287
|
+
display: flex;
|
|
1288
|
+
align-items: center;
|
|
1289
|
+
justify-content: space-between;
|
|
1290
|
+
padding: 10px 16px;
|
|
1291
|
+
font-size: 13px;
|
|
1292
|
+
color: rgba(255, 255, 255, 0.9);
|
|
1293
|
+
cursor: pointer;
|
|
1294
|
+
transition: background 0.1s ease;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.sp-settings-panel__row:hover {
|
|
1298
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.sp-settings-panel__label {
|
|
1302
|
+
font-weight: 500;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.sp-settings-panel__value {
|
|
1306
|
+
display: flex;
|
|
1307
|
+
align-items: center;
|
|
1308
|
+
gap: 4px;
|
|
1309
|
+
color: rgba(255, 255, 255, 0.6);
|
|
1310
|
+
font-size: 12px;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
.sp-settings-panel__arrow {
|
|
1314
|
+
display: flex;
|
|
1315
|
+
align-items: center;
|
|
1316
|
+
transform: rotate(-90deg);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.sp-settings-panel__arrow svg {
|
|
1320
|
+
width: 16px;
|
|
1321
|
+
height: 16px;
|
|
1322
|
+
fill: currentColor;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/* Sub-menu panels */
|
|
1326
|
+
.sp-settings-panel--sub {
|
|
1327
|
+
padding: 0;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
.sp-settings-panel__header {
|
|
1331
|
+
display: flex;
|
|
1332
|
+
align-items: center;
|
|
1333
|
+
gap: 8px;
|
|
1334
|
+
padding: 10px 16px;
|
|
1335
|
+
font-size: 13px;
|
|
1336
|
+
font-weight: 600;
|
|
1337
|
+
color: rgba(255, 255, 255, 0.9);
|
|
1338
|
+
cursor: pointer;
|
|
1339
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
1340
|
+
transition: background 0.1s ease;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.sp-settings-panel__header:hover {
|
|
1344
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.sp-settings-panel__back {
|
|
1348
|
+
display: flex;
|
|
1349
|
+
align-items: center;
|
|
1350
|
+
transform: rotate(-90deg);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.sp-settings-panel__back svg {
|
|
1354
|
+
width: 16px;
|
|
1355
|
+
height: 16px;
|
|
1356
|
+
fill: currentColor;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
.sp-settings-panel__header-label {
|
|
1360
|
+
flex: 1;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
.sp-settings-panel__item {
|
|
1364
|
+
display: flex;
|
|
1365
|
+
align-items: center;
|
|
1366
|
+
justify-content: space-between;
|
|
1367
|
+
padding: 10px 16px;
|
|
1368
|
+
font-size: 13px;
|
|
1369
|
+
color: rgba(255, 255, 255, 0.8);
|
|
1370
|
+
cursor: pointer;
|
|
1371
|
+
transition: background 0.1s ease, color 0.1s ease;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
.sp-settings-panel__item:hover {
|
|
1375
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1376
|
+
color: #fff;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
.sp-settings-panel__item--active {
|
|
1380
|
+
color: var(--sp-accent, #e50914);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.sp-settings-panel__check {
|
|
1384
|
+
width: 16px;
|
|
1385
|
+
height: 16px;
|
|
1386
|
+
fill: currentColor;
|
|
1387
|
+
margin-left: 8px;
|
|
1388
|
+
opacity: 0;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
.sp-settings-panel__check svg {
|
|
1392
|
+
width: 16px;
|
|
1393
|
+
height: 16px;
|
|
1394
|
+
fill: currentColor;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
.sp-settings-panel__item--active .sp-settings-panel__check {
|
|
1398
|
+
opacity: 1;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/* ============================================
|
|
1402
|
+
Captions Button
|
|
1403
|
+
============================================ */
|
|
1404
|
+
.sp-captions--active {
|
|
1405
|
+
color: var(--sp-accent, #e50914);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1177
1408
|
/* ============================================
|
|
1178
1409
|
Cast Button States
|
|
1179
1410
|
============================================ */
|
|
@@ -1185,6 +1416,122 @@ var styles = `
|
|
|
1185
1416
|
opacity: 0.4;
|
|
1186
1417
|
}
|
|
1187
1418
|
|
|
1419
|
+
/* ============================================
|
|
1420
|
+
Error Overlay
|
|
1421
|
+
============================================ */
|
|
1422
|
+
.sp-error-overlay {
|
|
1423
|
+
position: absolute;
|
|
1424
|
+
top: 0;
|
|
1425
|
+
left: 0;
|
|
1426
|
+
right: 0;
|
|
1427
|
+
bottom: 0;
|
|
1428
|
+
background: rgba(0, 0, 0, 0.85);
|
|
1429
|
+
display: flex;
|
|
1430
|
+
align-items: center;
|
|
1431
|
+
justify-content: center;
|
|
1432
|
+
z-index: 25;
|
|
1433
|
+
opacity: 0;
|
|
1434
|
+
visibility: hidden;
|
|
1435
|
+
transition: opacity 0.25s ease, visibility 0.25s;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
.sp-error-overlay--visible {
|
|
1439
|
+
opacity: 1;
|
|
1440
|
+
visibility: visible;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
.sp-error-overlay__content {
|
|
1444
|
+
display: flex;
|
|
1445
|
+
flex-direction: column;
|
|
1446
|
+
align-items: center;
|
|
1447
|
+
text-align: center;
|
|
1448
|
+
padding: 24px;
|
|
1449
|
+
max-width: 360px;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.sp-error-overlay__icon {
|
|
1453
|
+
color: rgba(255, 255, 255, 0.7);
|
|
1454
|
+
margin-bottom: 16px;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.sp-error-overlay__icon svg {
|
|
1458
|
+
width: 48px;
|
|
1459
|
+
height: 48px;
|
|
1460
|
+
fill: currentColor;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
.sp-error-overlay__message {
|
|
1464
|
+
color: rgba(255, 255, 255, 0.9);
|
|
1465
|
+
font-size: 15px;
|
|
1466
|
+
line-height: 1.5;
|
|
1467
|
+
margin: 0 0 24px;
|
|
1468
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.sp-error-overlay__actions {
|
|
1472
|
+
display: flex;
|
|
1473
|
+
gap: 12px;
|
|
1474
|
+
flex-wrap: wrap;
|
|
1475
|
+
justify-content: center;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
.sp-error-overlay__retry {
|
|
1479
|
+
background: var(--sp-accent, #e50914);
|
|
1480
|
+
color: #fff;
|
|
1481
|
+
border: none;
|
|
1482
|
+
padding: 12px 24px;
|
|
1483
|
+
font-size: 14px;
|
|
1484
|
+
font-weight: 600;
|
|
1485
|
+
border-radius: 6px;
|
|
1486
|
+
cursor: pointer;
|
|
1487
|
+
min-width: 120px;
|
|
1488
|
+
min-height: 44px;
|
|
1489
|
+
transition: background 0.15s ease, transform 0.15s ease;
|
|
1490
|
+
font-family: inherit;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.sp-error-overlay__retry:hover {
|
|
1494
|
+
filter: brightness(1.1);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
.sp-error-overlay__retry:active {
|
|
1498
|
+
transform: scale(0.96);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
.sp-error-overlay__retry:focus-visible {
|
|
1502
|
+
outline: 2px solid #fff;
|
|
1503
|
+
outline-offset: 2px;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
.sp-error-overlay__dismiss {
|
|
1507
|
+
background: none;
|
|
1508
|
+
color: rgba(255, 255, 255, 0.7);
|
|
1509
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
1510
|
+
padding: 12px 24px;
|
|
1511
|
+
font-size: 14px;
|
|
1512
|
+
font-weight: 500;
|
|
1513
|
+
border-radius: 6px;
|
|
1514
|
+
cursor: pointer;
|
|
1515
|
+
min-width: 100px;
|
|
1516
|
+
min-height: 44px;
|
|
1517
|
+
transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
|
|
1518
|
+
font-family: inherit;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.sp-error-overlay__dismiss:hover {
|
|
1522
|
+
color: #fff;
|
|
1523
|
+
border-color: rgba(255, 255, 255, 0.5);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
.sp-error-overlay__dismiss:active {
|
|
1527
|
+
transform: scale(0.96);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
.sp-error-overlay__dismiss:focus-visible {
|
|
1531
|
+
outline: 2px solid #fff;
|
|
1532
|
+
outline-offset: 2px;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1188
1535
|
/* ============================================
|
|
1189
1536
|
Buffering Indicator
|
|
1190
1537
|
============================================ */
|
|
@@ -1232,7 +1579,14 @@ var styles = `
|
|
|
1232
1579
|
.sp-control,
|
|
1233
1580
|
.sp-volume__slider-wrap,
|
|
1234
1581
|
.sp-quality-menu,
|
|
1235
|
-
.sp-
|
|
1582
|
+
.sp-settings-panel,
|
|
1583
|
+
.sp-settings-panel__row,
|
|
1584
|
+
.sp-settings-panel__item,
|
|
1585
|
+
.sp-settings-panel__header,
|
|
1586
|
+
.sp-buffering,
|
|
1587
|
+
.sp-error-overlay,
|
|
1588
|
+
.sp-error-overlay__retry,
|
|
1589
|
+
.sp-error-overlay__dismiss {
|
|
1236
1590
|
transition: none;
|
|
1237
1591
|
}
|
|
1238
1592
|
|
|
@@ -1267,7 +1621,15 @@ var icons = {
|
|
|
1267
1621
|
chromecast: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm0-4v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
|
|
1268
1622
|
chromecastConnected: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm18-7H5v1.63c3.96 1.28 7.09 4.41 8.37 8.37H19V7zM1 10v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
|
|
1269
1623
|
airplay: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 22h12l-6-6-6 6zM21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4v-2H3V5h18v12h-4v2h4c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>`,
|
|
1270
|
-
|
|
1624
|
+
captions: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-8 7H9.5v-.5h-2v3h2V13H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V13H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z"/></svg>`,
|
|
1625
|
+
captionsOff: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.5 5.5v13h-15v-13h15zM19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2z"/></svg>`,
|
|
1626
|
+
checkmark: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>`,
|
|
1627
|
+
chevronUp: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></svg>`,
|
|
1628
|
+
chevronDown: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></svg>`,
|
|
1629
|
+
spinner: `<svg viewBox="0 0 24 24" fill="currentColor" class="sp-spin"><path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/></svg>`,
|
|
1630
|
+
forward10: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18 13c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6v4l5-5-5-5v4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8h-2z"/><path d="M10.9 16V11.73l-.72.36-.48-.86 1.48-.73h.85V16h-1.13zm2.77-2.14c0-.66.13-1.2.38-1.6.26-.41.66-.62 1.2-.62.55 0 .95.21 1.21.62.25.4.38.94.38 1.6 0 .67-.13 1.2-.38 1.61-.26.41-.66.61-1.21.61-.54 0-.94-.2-1.2-.61-.25-.41-.38-.94-.38-1.61zm1.12 0c0 .45.05.79.15 1.03.1.23.26.35.48.35s.38-.12.49-.35c.1-.24.15-.58.15-1.03s-.05-.78-.15-1.02c-.11-.23-.27-.35-.49-.35s-.38.12-.48.35c-.1.24-.15.57-.15 1.02z"/></svg>`,
|
|
1631
|
+
replay10: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/><path d="M10.9 16V11.73l-.72.36-.48-.86 1.48-.73h.85V16h-1.13zm2.77-2.14c0-.66.13-1.2.38-1.6.26-.41.66-.62 1.2-.62.55 0 .95.21 1.21.62.25.4.38.94.38 1.6 0 .67-.13 1.2-.38 1.61-.26.41-.66.61-1.21.61-.54 0-.94-.2-1.2-.61-.25-.41-.38-.94-.38-1.61zm1.12 0c0 .45.05.79.15 1.03.1.23.26.35.48.35s.38-.12.49-.35c.1-.24.15-.58.15-1.03s-.05-.78-.15-1.02c-.11-.23-.27-.35-.49-.35s-.38.12-.48.35c-.1.24-.15.57-.15 1.02z"/></svg>`,
|
|
1632
|
+
error: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>`
|
|
1271
1633
|
};
|
|
1272
1634
|
function createElement(tag, attrs, children) {
|
|
1273
1635
|
const el = document.createElement(tag);
|
|
@@ -1351,12 +1713,11 @@ var PlayButton = class {
|
|
|
1351
1713
|
const video = getVideo(this.api.container);
|
|
1352
1714
|
if (!video) return;
|
|
1353
1715
|
const ended = this.api.getState("ended");
|
|
1354
|
-
const playing = this.api.getState("playing");
|
|
1355
1716
|
if (ended) {
|
|
1356
1717
|
video.currentTime = 0;
|
|
1357
1718
|
video.play().catch(() => {
|
|
1358
1719
|
});
|
|
1359
|
-
} else if (
|
|
1720
|
+
} else if (!video.paused) {
|
|
1360
1721
|
video.pause();
|
|
1361
1722
|
} else {
|
|
1362
1723
|
video.play().catch(() => {
|
|
@@ -1368,6 +1729,68 @@ var PlayButton = class {
|
|
|
1368
1729
|
this.el.remove();
|
|
1369
1730
|
}
|
|
1370
1731
|
};
|
|
1732
|
+
var ThumbnailPreview = class {
|
|
1733
|
+
constructor() {
|
|
1734
|
+
this.config = null;
|
|
1735
|
+
this.loaded = false;
|
|
1736
|
+
this.el = createElement("div", { className: "sp-thumbnail-preview" });
|
|
1737
|
+
this.img = createElement("div", { className: "sp-thumbnail-preview__img" });
|
|
1738
|
+
this.el.appendChild(this.img);
|
|
1739
|
+
}
|
|
1740
|
+
getElement() {
|
|
1741
|
+
return this.el;
|
|
1742
|
+
}
|
|
1743
|
+
setConfig(config) {
|
|
1744
|
+
this.config = config;
|
|
1745
|
+
this.loaded = false;
|
|
1746
|
+
if (config) {
|
|
1747
|
+
this.img.style.width = `${config.width}px`;
|
|
1748
|
+
this.img.style.height = `${config.height}px`;
|
|
1749
|
+
this.el.style.width = `${config.width}px`;
|
|
1750
|
+
this.el.style.height = `${config.height}px`;
|
|
1751
|
+
const preload = new Image();
|
|
1752
|
+
preload.onload = () => {
|
|
1753
|
+
this.loaded = true;
|
|
1754
|
+
};
|
|
1755
|
+
preload.onerror = () => {
|
|
1756
|
+
this.config = null;
|
|
1757
|
+
this.loaded = false;
|
|
1758
|
+
};
|
|
1759
|
+
preload.src = config.src;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Update the thumbnail to show the frame at the given time.
|
|
1764
|
+
* @param time Time in seconds
|
|
1765
|
+
* @param percent Position as 0-1 fraction (for horizontal positioning)
|
|
1766
|
+
*/
|
|
1767
|
+
show(time, percent) {
|
|
1768
|
+
if (!this.config || !this.loaded) {
|
|
1769
|
+
this.el.style.display = "none";
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const { src, width, height, columns, interval } = this.config;
|
|
1773
|
+
const index = Math.floor(time / interval);
|
|
1774
|
+
const col = index % columns;
|
|
1775
|
+
const row = Math.floor(index / columns);
|
|
1776
|
+
this.img.style.backgroundImage = `url(${src})`;
|
|
1777
|
+
this.img.style.backgroundPosition = `-${col * width}px -${row * height}px`;
|
|
1778
|
+
this.img.style.backgroundSize = `${columns * width}px auto`;
|
|
1779
|
+
this.img.style.width = `${width}px`;
|
|
1780
|
+
this.img.style.height = `${height}px`;
|
|
1781
|
+
this.el.style.left = `${percent * 100}%`;
|
|
1782
|
+
this.el.style.display = "";
|
|
1783
|
+
}
|
|
1784
|
+
hide() {
|
|
1785
|
+
this.el.style.display = "none";
|
|
1786
|
+
}
|
|
1787
|
+
isConfigured() {
|
|
1788
|
+
return this.config !== null;
|
|
1789
|
+
}
|
|
1790
|
+
destroy() {
|
|
1791
|
+
this.el.remove();
|
|
1792
|
+
}
|
|
1793
|
+
};
|
|
1371
1794
|
var ProgressBar = class {
|
|
1372
1795
|
constructor(api) {
|
|
1373
1796
|
this.isDragging = false;
|
|
@@ -1407,36 +1830,99 @@ var ProgressBar = class {
|
|
|
1407
1830
|
}
|
|
1408
1831
|
}
|
|
1409
1832
|
};
|
|
1410
|
-
this.
|
|
1411
|
-
|
|
1833
|
+
this.onTouchStart = (e) => {
|
|
1834
|
+
e.preventDefault();
|
|
1835
|
+
const video = getVideo(this.api.container);
|
|
1836
|
+
this.wasPlayingBeforeDrag = video ? !video.paused : false;
|
|
1837
|
+
this.isDragging = true;
|
|
1838
|
+
this.el.classList.add("sp-progress--dragging");
|
|
1839
|
+
this.lastSeekTime = 0;
|
|
1840
|
+
this.seek(e.touches[0].clientX, true);
|
|
1412
1841
|
};
|
|
1413
|
-
this.
|
|
1842
|
+
this.onDocTouchMove = (e) => {
|
|
1843
|
+
if (this.isDragging) {
|
|
1844
|
+
e.preventDefault();
|
|
1845
|
+
this.seek(e.touches[0].clientX);
|
|
1846
|
+
this.updateVisualPosition(e.touches[0].clientX);
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
this.onTouchEnd = (e) => {
|
|
1850
|
+
if (this.isDragging) {
|
|
1851
|
+
const clientX = e.changedTouches?.[0]?.clientX;
|
|
1852
|
+
if (clientX !== void 0) {
|
|
1853
|
+
this.seek(clientX, true);
|
|
1854
|
+
}
|
|
1855
|
+
this.isDragging = false;
|
|
1856
|
+
this.el.classList.remove("sp-progress--dragging");
|
|
1857
|
+
if (this.wasPlayingBeforeDrag) {
|
|
1858
|
+
const video = getVideo(this.api.container);
|
|
1859
|
+
if (video && video.paused) {
|
|
1860
|
+
const resumePlayback = () => {
|
|
1861
|
+
video.removeEventListener("seeked", resumePlayback);
|
|
1862
|
+
video.play().catch(() => {
|
|
1863
|
+
});
|
|
1864
|
+
};
|
|
1865
|
+
video.addEventListener("seeked", resumePlayback);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
this.tooltip.style.opacity = "0";
|
|
1869
|
+
this.thumbnailPreview.hide();
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
this.onMouseMove = (e) => {
|
|
1873
|
+
this.updateTooltip(e.clientX);
|
|
1874
|
+
};
|
|
1875
|
+
this.onMouseLeave = () => {
|
|
1414
1876
|
if (!this.isDragging) {
|
|
1415
1877
|
this.tooltip.style.opacity = "0";
|
|
1878
|
+
this.thumbnailPreview.hide();
|
|
1416
1879
|
}
|
|
1417
1880
|
};
|
|
1418
1881
|
this.onKeyDown = (e) => {
|
|
1419
1882
|
const video = getVideo(this.api.container);
|
|
1420
1883
|
if (!video) return;
|
|
1421
1884
|
const step = 5;
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1885
|
+
const live = this.api.getState("live");
|
|
1886
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1887
|
+
if (live && seekableRange) {
|
|
1888
|
+
switch (e.key) {
|
|
1889
|
+
case "ArrowLeft":
|
|
1890
|
+
e.preventDefault();
|
|
1891
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - step);
|
|
1892
|
+
break;
|
|
1893
|
+
case "ArrowRight":
|
|
1894
|
+
e.preventDefault();
|
|
1895
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + step);
|
|
1896
|
+
break;
|
|
1897
|
+
case "Home":
|
|
1898
|
+
e.preventDefault();
|
|
1899
|
+
video.currentTime = seekableRange.start;
|
|
1900
|
+
break;
|
|
1901
|
+
case "End":
|
|
1902
|
+
e.preventDefault();
|
|
1903
|
+
video.currentTime = seekableRange.end;
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1906
|
+
} else {
|
|
1907
|
+
const duration = this.api.getState("duration") || 0;
|
|
1908
|
+
switch (e.key) {
|
|
1909
|
+
case "ArrowLeft":
|
|
1910
|
+
e.preventDefault();
|
|
1911
|
+
video.currentTime = Math.max(0, video.currentTime - step);
|
|
1912
|
+
break;
|
|
1913
|
+
case "ArrowRight":
|
|
1914
|
+
e.preventDefault();
|
|
1915
|
+
video.currentTime = Math.min(duration, video.currentTime + step);
|
|
1916
|
+
break;
|
|
1917
|
+
case "Home":
|
|
1918
|
+
e.preventDefault();
|
|
1919
|
+
video.currentTime = 0;
|
|
1920
|
+
break;
|
|
1921
|
+
case "End":
|
|
1922
|
+
e.preventDefault();
|
|
1923
|
+
video.currentTime = duration;
|
|
1924
|
+
break;
|
|
1925
|
+
}
|
|
1440
1926
|
}
|
|
1441
1927
|
};
|
|
1442
1928
|
this.api = api;
|
|
@@ -1448,10 +1934,12 @@ var ProgressBar = class {
|
|
|
1448
1934
|
this.handle = createElement("div", { className: "sp-progress__handle" });
|
|
1449
1935
|
this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
|
|
1450
1936
|
this.tooltip.textContent = "0:00";
|
|
1937
|
+
this.thumbnailPreview = new ThumbnailPreview();
|
|
1451
1938
|
track.appendChild(this.buffered);
|
|
1452
1939
|
track.appendChild(this.filled);
|
|
1453
1940
|
track.appendChild(this.handle);
|
|
1454
1941
|
this.el.appendChild(track);
|
|
1942
|
+
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
1455
1943
|
this.el.appendChild(this.tooltip);
|
|
1456
1944
|
this.wrapper.appendChild(this.el);
|
|
1457
1945
|
this.el.setAttribute("role", "slider");
|
|
@@ -1461,9 +1949,13 @@ var ProgressBar = class {
|
|
|
1461
1949
|
this.wrapper.addEventListener("mousedown", this.onMouseDown);
|
|
1462
1950
|
this.wrapper.addEventListener("mousemove", this.onMouseMove);
|
|
1463
1951
|
this.wrapper.addEventListener("mouseleave", this.onMouseLeave);
|
|
1952
|
+
this.wrapper.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
1464
1953
|
this.el.addEventListener("keydown", this.onKeyDown);
|
|
1465
1954
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
1466
1955
|
document.addEventListener("mouseup", this.onMouseUp);
|
|
1956
|
+
document.addEventListener("touchmove", this.onDocTouchMove, { passive: false });
|
|
1957
|
+
document.addEventListener("touchend", this.onTouchEnd);
|
|
1958
|
+
document.addEventListener("touchcancel", this.onTouchEnd);
|
|
1467
1959
|
}
|
|
1468
1960
|
render() {
|
|
1469
1961
|
return this.wrapper;
|
|
@@ -1476,11 +1968,40 @@ var ProgressBar = class {
|
|
|
1476
1968
|
hide() {
|
|
1477
1969
|
this.wrapper.classList.remove("sp-progress-wrapper--visible");
|
|
1478
1970
|
}
|
|
1971
|
+
/** Set thumbnail sprite configuration */
|
|
1972
|
+
setThumbnails(config) {
|
|
1973
|
+
this.thumbnailPreview.setConfig(config);
|
|
1974
|
+
}
|
|
1479
1975
|
update() {
|
|
1480
1976
|
const currentTime = this.api.getState("currentTime") || 0;
|
|
1481
1977
|
const duration = this.api.getState("duration") || 0;
|
|
1482
1978
|
const bufferedRanges = this.api.getState("buffered");
|
|
1483
|
-
|
|
1979
|
+
const live = this.api.getState("live");
|
|
1980
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
1981
|
+
const thumbnails = this.api.getState("thumbnails");
|
|
1982
|
+
if (thumbnails && !this.thumbnailPreview.isConfigured()) {
|
|
1983
|
+
this.thumbnailPreview.setConfig(thumbnails);
|
|
1984
|
+
}
|
|
1985
|
+
this.el.classList.toggle("sp-progress--live", !!live);
|
|
1986
|
+
if (live && seekableRange) {
|
|
1987
|
+
const rangeLength = seekableRange.end - seekableRange.start;
|
|
1988
|
+
if (rangeLength > 0) {
|
|
1989
|
+
const progress = (currentTime - seekableRange.start) / rangeLength * 100;
|
|
1990
|
+
this.filled.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
|
1991
|
+
this.handle.style.left = `${Math.max(0, Math.min(100, progress))}%`;
|
|
1992
|
+
}
|
|
1993
|
+
if (bufferedRanges && bufferedRanges.length > 0) {
|
|
1994
|
+
const rangeLength2 = seekableRange.end - seekableRange.start;
|
|
1995
|
+
if (rangeLength2 > 0) {
|
|
1996
|
+
const bufferedEnd = bufferedRanges.end(bufferedRanges.length - 1);
|
|
1997
|
+
const bufferedPercent = (bufferedEnd - seekableRange.start) / rangeLength2 * 100;
|
|
1998
|
+
this.buffered.style.width = `${Math.max(0, Math.min(100, bufferedPercent))}%`;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
this.el.setAttribute("aria-valuemax", String(Math.floor(seekableRange.end)));
|
|
2002
|
+
this.el.setAttribute("aria-valuenow", String(Math.floor(currentTime)));
|
|
2003
|
+
this.el.setAttribute("aria-valuetext", `${Math.floor(seekableRange.end - currentTime)} seconds behind live`);
|
|
2004
|
+
} else if (duration > 0) {
|
|
1484
2005
|
const progress = currentTime / duration * 100;
|
|
1485
2006
|
this.filled.style.width = `${progress}%`;
|
|
1486
2007
|
this.handle.style.left = `${progress}%`;
|
|
@@ -1497,6 +2018,12 @@ var ProgressBar = class {
|
|
|
1497
2018
|
getTimeFromPosition(clientX) {
|
|
1498
2019
|
const rect = this.el.getBoundingClientRect();
|
|
1499
2020
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
2021
|
+
const live = this.api.getState("live");
|
|
2022
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
2023
|
+
if (live && seekableRange) {
|
|
2024
|
+
const rangeLength = seekableRange.end - seekableRange.start;
|
|
2025
|
+
return seekableRange.start + percent * rangeLength;
|
|
2026
|
+
}
|
|
1500
2027
|
const duration = this.api.getState("duration") || 0;
|
|
1501
2028
|
return percent * duration;
|
|
1502
2029
|
}
|
|
@@ -1504,8 +2031,18 @@ var ProgressBar = class {
|
|
|
1504
2031
|
const rect = this.el.getBoundingClientRect();
|
|
1505
2032
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1506
2033
|
const time = this.getTimeFromPosition(clientX);
|
|
1507
|
-
this.
|
|
2034
|
+
const live = this.api.getState("live");
|
|
2035
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
2036
|
+
if (live && seekableRange) {
|
|
2037
|
+
const behindLive = seekableRange.end - time;
|
|
2038
|
+
this.tooltip.textContent = formatLiveTime(behindLive);
|
|
2039
|
+
} else {
|
|
2040
|
+
this.tooltip.textContent = formatTime$1(time);
|
|
2041
|
+
}
|
|
1508
2042
|
this.tooltip.style.left = `${percent * 100}%`;
|
|
2043
|
+
if (this.thumbnailPreview.isConfigured()) {
|
|
2044
|
+
this.thumbnailPreview.show(time, percent);
|
|
2045
|
+
}
|
|
1509
2046
|
}
|
|
1510
2047
|
updateVisualPosition(clientX) {
|
|
1511
2048
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -1528,8 +2065,13 @@ var ProgressBar = class {
|
|
|
1528
2065
|
this.wrapper.removeEventListener("mousedown", this.onMouseDown);
|
|
1529
2066
|
this.wrapper.removeEventListener("mousemove", this.onMouseMove);
|
|
1530
2067
|
this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
|
|
2068
|
+
this.wrapper.removeEventListener("touchstart", this.onTouchStart);
|
|
1531
2069
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
1532
2070
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
2071
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
2072
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
2073
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
2074
|
+
this.thumbnailPreview.destroy();
|
|
1533
2075
|
this.wrapper.remove();
|
|
1534
2076
|
}
|
|
1535
2077
|
};
|
|
@@ -1578,6 +2120,20 @@ var VolumeControl = class {
|
|
|
1578
2120
|
this.onMouseUp = () => {
|
|
1579
2121
|
this.isDragging = false;
|
|
1580
2122
|
};
|
|
2123
|
+
this.onTouchStart = (e) => {
|
|
2124
|
+
e.preventDefault();
|
|
2125
|
+
this.isDragging = true;
|
|
2126
|
+
this.setVolume(this.getVolumeFromPosition(e.touches[0].clientX));
|
|
2127
|
+
};
|
|
2128
|
+
this.onDocTouchMove = (e) => {
|
|
2129
|
+
if (this.isDragging) {
|
|
2130
|
+
e.preventDefault();
|
|
2131
|
+
this.setVolume(this.getVolumeFromPosition(e.touches[0].clientX));
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
this.onTouchEnd = () => {
|
|
2135
|
+
this.isDragging = false;
|
|
2136
|
+
};
|
|
1581
2137
|
this.onKeyDown = (e) => {
|
|
1582
2138
|
const video = getVideo(this.api.container);
|
|
1583
2139
|
if (!video) return;
|
|
@@ -1617,9 +2173,13 @@ var VolumeControl = class {
|
|
|
1617
2173
|
this.el.appendChild(this.btn);
|
|
1618
2174
|
this.el.appendChild(sliderWrap);
|
|
1619
2175
|
this.slider.addEventListener("mousedown", this.onMouseDown);
|
|
2176
|
+
this.slider.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
1620
2177
|
this.slider.addEventListener("keydown", this.onKeyDown);
|
|
1621
2178
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
1622
2179
|
document.addEventListener("mouseup", this.onMouseUp);
|
|
2180
|
+
document.addEventListener("touchmove", this.onDocTouchMove, { passive: false });
|
|
2181
|
+
document.addEventListener("touchend", this.onTouchEnd);
|
|
2182
|
+
document.addEventListener("touchcancel", this.onTouchEnd);
|
|
1623
2183
|
}
|
|
1624
2184
|
render() {
|
|
1625
2185
|
return this.el;
|
|
@@ -1664,26 +2224,40 @@ var VolumeControl = class {
|
|
|
1664
2224
|
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1665
2225
|
}
|
|
1666
2226
|
destroy() {
|
|
2227
|
+
this.slider.removeEventListener("mousedown", this.onMouseDown);
|
|
2228
|
+
this.slider.removeEventListener("touchstart", this.onTouchStart);
|
|
2229
|
+
this.slider.removeEventListener("keydown", this.onKeyDown);
|
|
1667
2230
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
1668
2231
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
2232
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
2233
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
2234
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
1669
2235
|
this.el.remove();
|
|
1670
2236
|
}
|
|
1671
2237
|
};
|
|
1672
2238
|
var LiveIndicator = class {
|
|
1673
2239
|
constructor(api) {
|
|
1674
|
-
this.
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
this.
|
|
1678
|
-
this.el.setAttribute("aria-label", "Seek to live");
|
|
1679
|
-
this.el.setAttribute("tabindex", "0");
|
|
1680
|
-
this.el.onclick = () => this.seekToLive();
|
|
1681
|
-
this.el.onkeydown = (e) => {
|
|
2240
|
+
this.handleClick = () => {
|
|
2241
|
+
this.seekToLive();
|
|
2242
|
+
};
|
|
2243
|
+
this.handleKeyDown = (e) => {
|
|
1682
2244
|
if (e.key === "Enter" || e.key === " ") {
|
|
1683
2245
|
e.preventDefault();
|
|
1684
2246
|
this.seekToLive();
|
|
1685
2247
|
}
|
|
1686
2248
|
};
|
|
2249
|
+
this.api = api;
|
|
2250
|
+
this.el = createElement("div", { className: "sp-live" });
|
|
2251
|
+
this.dot = createElement("div", { className: "sp-live__dot" });
|
|
2252
|
+
this.label = document.createElement("span");
|
|
2253
|
+
this.label.textContent = "LIVE";
|
|
2254
|
+
this.el.appendChild(this.dot);
|
|
2255
|
+
this.el.appendChild(this.label);
|
|
2256
|
+
this.el.setAttribute("role", "button");
|
|
2257
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
2258
|
+
this.el.setAttribute("tabindex", "0");
|
|
2259
|
+
this.el.addEventListener("click", this.handleClick);
|
|
2260
|
+
this.el.addEventListener("keydown", this.handleKeyDown);
|
|
1687
2261
|
}
|
|
1688
2262
|
render() {
|
|
1689
2263
|
return this.el;
|
|
@@ -1694,8 +2268,12 @@ var LiveIndicator = class {
|
|
|
1694
2268
|
this.el.style.display = live ? "" : "none";
|
|
1695
2269
|
if (liveEdge) {
|
|
1696
2270
|
this.el.classList.remove("sp-live--behind");
|
|
2271
|
+
this.label.textContent = "LIVE";
|
|
2272
|
+
this.el.setAttribute("aria-label", "At live edge");
|
|
1697
2273
|
} else {
|
|
1698
2274
|
this.el.classList.add("sp-live--behind");
|
|
2275
|
+
this.label.textContent = "GO LIVE";
|
|
2276
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
1699
2277
|
}
|
|
1700
2278
|
}
|
|
1701
2279
|
seekToLive() {
|
|
@@ -1707,6 +2285,8 @@ var LiveIndicator = class {
|
|
|
1707
2285
|
}
|
|
1708
2286
|
}
|
|
1709
2287
|
destroy() {
|
|
2288
|
+
this.el.removeEventListener("click", this.handleClick);
|
|
2289
|
+
this.el.removeEventListener("keydown", this.handleKeyDown);
|
|
1710
2290
|
this.el.remove();
|
|
1711
2291
|
}
|
|
1712
2292
|
};
|
|
@@ -2006,13 +2586,579 @@ var Spacer = class {
|
|
|
2006
2586
|
this.el.remove();
|
|
2007
2587
|
}
|
|
2008
2588
|
};
|
|
2589
|
+
function getUserMessage(error) {
|
|
2590
|
+
if (!error) return "Something went wrong.";
|
|
2591
|
+
const msg = error.message?.toLowerCase() || "";
|
|
2592
|
+
if (msg.includes("network") || msg.includes("timeout") || msg.includes("fetch") || msg.includes("connection")) {
|
|
2593
|
+
return "Having trouble connecting. Check your internet and try again.";
|
|
2594
|
+
}
|
|
2595
|
+
if (msg.includes("manifest")) {
|
|
2596
|
+
return "Unable to load video. Please try again.";
|
|
2597
|
+
}
|
|
2598
|
+
if (msg.includes("decode") || msg.includes("media") || msg.includes("format") || msg.includes("codec")) {
|
|
2599
|
+
return "This video can't be played right now.";
|
|
2600
|
+
}
|
|
2601
|
+
if (msg.includes("not found") || msg.includes("404") || msg.includes("source") || msg.includes("not supported")) {
|
|
2602
|
+
return "Video not found.";
|
|
2603
|
+
}
|
|
2604
|
+
return "Something went wrong.";
|
|
2605
|
+
}
|
|
2606
|
+
var ErrorOverlay = class {
|
|
2607
|
+
constructor(api) {
|
|
2608
|
+
this.visible = false;
|
|
2609
|
+
this.lastSource = null;
|
|
2610
|
+
this.handleRetry = () => {
|
|
2611
|
+
if (this.retryBtn.disabled) return;
|
|
2612
|
+
this.retryBtn.disabled = true;
|
|
2613
|
+
this.hide();
|
|
2614
|
+
const source = this.api.getState("source");
|
|
2615
|
+
const src = source?.src || this.lastSource;
|
|
2616
|
+
if (src) {
|
|
2617
|
+
this.api.emit("error:retry", { src });
|
|
2618
|
+
const video = this.api.container.querySelector("video");
|
|
2619
|
+
if (video) {
|
|
2620
|
+
video.src = src;
|
|
2621
|
+
video.load();
|
|
2622
|
+
video.play().catch(() => {
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
setTimeout(() => {
|
|
2627
|
+
this.retryBtn.disabled = false;
|
|
2628
|
+
}, 1e3);
|
|
2629
|
+
};
|
|
2630
|
+
this.handleDismiss = () => {
|
|
2631
|
+
this.hide();
|
|
2632
|
+
this.api.emit("error:dismiss", void 0);
|
|
2633
|
+
};
|
|
2634
|
+
this.api = api;
|
|
2635
|
+
const overlay = document.createElement("div");
|
|
2636
|
+
overlay.className = "sp-error-overlay";
|
|
2637
|
+
overlay.setAttribute("role", "alert");
|
|
2638
|
+
overlay.setAttribute("aria-live", "assertive");
|
|
2639
|
+
const content = document.createElement("div");
|
|
2640
|
+
content.className = "sp-error-overlay__content";
|
|
2641
|
+
const iconEl = document.createElement("div");
|
|
2642
|
+
iconEl.className = "sp-error-overlay__icon";
|
|
2643
|
+
iconEl.innerHTML = icons.error;
|
|
2644
|
+
const messageEl = document.createElement("p");
|
|
2645
|
+
messageEl.className = "sp-error-overlay__message";
|
|
2646
|
+
messageEl.textContent = "Something went wrong.";
|
|
2647
|
+
const actions = document.createElement("div");
|
|
2648
|
+
actions.className = "sp-error-overlay__actions";
|
|
2649
|
+
this.retryBtn = document.createElement("button");
|
|
2650
|
+
this.retryBtn.className = "sp-error-overlay__retry";
|
|
2651
|
+
this.retryBtn.setAttribute("type", "button");
|
|
2652
|
+
this.retryBtn.setAttribute("aria-label", "Try again");
|
|
2653
|
+
this.retryBtn.textContent = "Try Again";
|
|
2654
|
+
this.retryBtn.addEventListener("click", this.handleRetry);
|
|
2655
|
+
this.dismissBtn = document.createElement("button");
|
|
2656
|
+
this.dismissBtn.className = "sp-error-overlay__dismiss";
|
|
2657
|
+
this.dismissBtn.setAttribute("type", "button");
|
|
2658
|
+
this.dismissBtn.setAttribute("aria-label", "Go back");
|
|
2659
|
+
this.dismissBtn.textContent = "Go Back";
|
|
2660
|
+
this.dismissBtn.addEventListener("click", this.handleDismiss);
|
|
2661
|
+
actions.appendChild(this.retryBtn);
|
|
2662
|
+
actions.appendChild(this.dismissBtn);
|
|
2663
|
+
content.appendChild(iconEl);
|
|
2664
|
+
content.appendChild(messageEl);
|
|
2665
|
+
content.appendChild(actions);
|
|
2666
|
+
overlay.appendChild(content);
|
|
2667
|
+
this.el = overlay;
|
|
2668
|
+
}
|
|
2669
|
+
render() {
|
|
2670
|
+
return this.el;
|
|
2671
|
+
}
|
|
2672
|
+
/** Show the error overlay with the given error */
|
|
2673
|
+
show(error) {
|
|
2674
|
+
const message = getUserMessage(error);
|
|
2675
|
+
const messageEl = this.el.querySelector(".sp-error-overlay__message");
|
|
2676
|
+
if (messageEl) {
|
|
2677
|
+
messageEl.textContent = message;
|
|
2678
|
+
}
|
|
2679
|
+
const source = this.api.getState("source");
|
|
2680
|
+
if (source?.src) {
|
|
2681
|
+
this.lastSource = source.src;
|
|
2682
|
+
}
|
|
2683
|
+
this.visible = true;
|
|
2684
|
+
this.retryBtn.disabled = false;
|
|
2685
|
+
this.el.classList.add("sp-error-overlay--visible");
|
|
2686
|
+
}
|
|
2687
|
+
/** Hide the error overlay */
|
|
2688
|
+
hide() {
|
|
2689
|
+
this.visible = false;
|
|
2690
|
+
this.el.classList.remove("sp-error-overlay--visible");
|
|
2691
|
+
}
|
|
2692
|
+
isVisible() {
|
|
2693
|
+
return this.visible;
|
|
2694
|
+
}
|
|
2695
|
+
update() {
|
|
2696
|
+
const playbackState = this.api.getState("playbackState");
|
|
2697
|
+
if (this.visible && playbackState !== "error" && playbackState !== "loading") {
|
|
2698
|
+
const playing = this.api.getState("playing");
|
|
2699
|
+
if (playing) {
|
|
2700
|
+
this.hide();
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
destroy() {
|
|
2705
|
+
this.retryBtn.removeEventListener("click", this.handleRetry);
|
|
2706
|
+
this.dismissBtn.removeEventListener("click", this.handleDismiss);
|
|
2707
|
+
this.el.remove();
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
var SPEED_OPTIONS = [
|
|
2711
|
+
{ label: "0.5x", value: 0.5 },
|
|
2712
|
+
{ label: "0.75x", value: 0.75 },
|
|
2713
|
+
{ label: "Normal", value: 1 },
|
|
2714
|
+
{ label: "1.25x", value: 1.25 },
|
|
2715
|
+
{ label: "1.5x", value: 1.5 },
|
|
2716
|
+
{ label: "2x", value: 2 }
|
|
2717
|
+
];
|
|
2718
|
+
var SettingsMenu = class {
|
|
2719
|
+
constructor(api) {
|
|
2720
|
+
this.isOpen = false;
|
|
2721
|
+
this.currentPanel = "main";
|
|
2722
|
+
this.lastQualitiesJson = "";
|
|
2723
|
+
this.api = api;
|
|
2724
|
+
this.el = createElement("div", { className: "sp-settings" });
|
|
2725
|
+
this.btn = createButton("sp-settings__btn", "Settings", icons.settings);
|
|
2726
|
+
this.btn.setAttribute("aria-haspopup", "true");
|
|
2727
|
+
this.btn.setAttribute("aria-expanded", "false");
|
|
2728
|
+
this.btn.addEventListener("click", (e) => {
|
|
2729
|
+
e.stopPropagation();
|
|
2730
|
+
this.toggle();
|
|
2731
|
+
});
|
|
2732
|
+
this.panel = createElement("div", { className: "sp-settings-panel" });
|
|
2733
|
+
this.panel.setAttribute("role", "menu");
|
|
2734
|
+
this.panel.addEventListener("click", (e) => e.stopPropagation());
|
|
2735
|
+
this.el.appendChild(this.btn);
|
|
2736
|
+
this.el.appendChild(this.panel);
|
|
2737
|
+
this.closeHandler = (e) => {
|
|
2738
|
+
if (!this.el.contains(e.target)) {
|
|
2739
|
+
this.close();
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
document.addEventListener("click", this.closeHandler);
|
|
2743
|
+
this.keyHandler = (e) => {
|
|
2744
|
+
if (!this.isOpen) return;
|
|
2745
|
+
if (e.key === "Escape") {
|
|
2746
|
+
e.preventDefault();
|
|
2747
|
+
e.stopPropagation();
|
|
2748
|
+
if (this.currentPanel !== "main") {
|
|
2749
|
+
this.showPanel("main");
|
|
2750
|
+
} else {
|
|
2751
|
+
this.close();
|
|
2752
|
+
this.btn.focus();
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2756
|
+
document.addEventListener("keydown", this.keyHandler);
|
|
2757
|
+
}
|
|
2758
|
+
render() {
|
|
2759
|
+
return this.el;
|
|
2760
|
+
}
|
|
2761
|
+
update() {
|
|
2762
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2763
|
+
const qualitiesJson = JSON.stringify(qualities.map((q) => q.id));
|
|
2764
|
+
if (qualitiesJson !== this.lastQualitiesJson) {
|
|
2765
|
+
this.lastQualitiesJson = qualitiesJson;
|
|
2766
|
+
if (this.isOpen && this.currentPanel === "quality") {
|
|
2767
|
+
this.renderQualityPanel();
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
if (this.isOpen) {
|
|
2771
|
+
if (this.currentPanel === "quality") {
|
|
2772
|
+
this.updateQualityActiveStates();
|
|
2773
|
+
} else if (this.currentPanel === "speed") {
|
|
2774
|
+
this.updateSpeedActiveStates();
|
|
2775
|
+
} else if (this.currentPanel === "captions") {
|
|
2776
|
+
this.updateCaptionsActiveStates();
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
toggle() {
|
|
2781
|
+
this.isOpen ? this.close() : this.open();
|
|
2782
|
+
}
|
|
2783
|
+
open() {
|
|
2784
|
+
this.isOpen = true;
|
|
2785
|
+
this.currentPanel = "main";
|
|
2786
|
+
this.renderMainPanel();
|
|
2787
|
+
this.panel.classList.add("sp-settings-panel--open");
|
|
2788
|
+
this.btn.setAttribute("aria-expanded", "true");
|
|
2789
|
+
}
|
|
2790
|
+
close() {
|
|
2791
|
+
this.isOpen = false;
|
|
2792
|
+
this.currentPanel = "main";
|
|
2793
|
+
this.panel.classList.remove("sp-settings-panel--open");
|
|
2794
|
+
this.btn.setAttribute("aria-expanded", "false");
|
|
2795
|
+
}
|
|
2796
|
+
showPanel(panel) {
|
|
2797
|
+
this.currentPanel = panel;
|
|
2798
|
+
switch (panel) {
|
|
2799
|
+
case "main":
|
|
2800
|
+
this.renderMainPanel();
|
|
2801
|
+
break;
|
|
2802
|
+
case "quality":
|
|
2803
|
+
this.renderQualityPanel();
|
|
2804
|
+
break;
|
|
2805
|
+
case "speed":
|
|
2806
|
+
this.renderSpeedPanel();
|
|
2807
|
+
break;
|
|
2808
|
+
case "captions":
|
|
2809
|
+
this.renderCaptionsPanel();
|
|
2810
|
+
break;
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
renderMainPanel() {
|
|
2814
|
+
this.panel.innerHTML = "";
|
|
2815
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--main";
|
|
2816
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2817
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
2818
|
+
const playbackRate = this.api.getState("playbackRate") ?? 1;
|
|
2819
|
+
if (qualities.length > 0) {
|
|
2820
|
+
const qualityRow = this.createMainRow(
|
|
2821
|
+
"Quality",
|
|
2822
|
+
currentQuality?.label || "Auto",
|
|
2823
|
+
() => this.showPanel("quality")
|
|
2824
|
+
);
|
|
2825
|
+
this.panel.appendChild(qualityRow);
|
|
2826
|
+
}
|
|
2827
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2828
|
+
if (textTracks.length > 0) {
|
|
2829
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2830
|
+
const captionsLabel = currentTextTrack ? currentTextTrack.label : "Off";
|
|
2831
|
+
const captionsRow = this.createMainRow(
|
|
2832
|
+
"Captions",
|
|
2833
|
+
captionsLabel,
|
|
2834
|
+
() => this.showPanel("captions")
|
|
2835
|
+
);
|
|
2836
|
+
this.panel.appendChild(captionsRow);
|
|
2837
|
+
}
|
|
2838
|
+
const speedLabel = playbackRate === 1 ? "Normal" : `${playbackRate}x`;
|
|
2839
|
+
const speedRow = this.createMainRow(
|
|
2840
|
+
"Speed",
|
|
2841
|
+
speedLabel,
|
|
2842
|
+
() => this.showPanel("speed")
|
|
2843
|
+
);
|
|
2844
|
+
this.panel.appendChild(speedRow);
|
|
2845
|
+
}
|
|
2846
|
+
createMainRow(label, value, onClick2) {
|
|
2847
|
+
const row = createElement("div", { className: "sp-settings-panel__row" });
|
|
2848
|
+
row.setAttribute("role", "menuitem");
|
|
2849
|
+
row.setAttribute("tabindex", "0");
|
|
2850
|
+
row.setAttribute("aria-haspopup", "true");
|
|
2851
|
+
const labelEl = createElement("span", { className: "sp-settings-panel__label" });
|
|
2852
|
+
labelEl.textContent = label;
|
|
2853
|
+
const rightSide = createElement("span", { className: "sp-settings-panel__value" });
|
|
2854
|
+
rightSide.textContent = value;
|
|
2855
|
+
const arrow = createElement("span", { className: "sp-settings-panel__arrow" });
|
|
2856
|
+
arrow.innerHTML = icons.chevronDown;
|
|
2857
|
+
rightSide.appendChild(arrow);
|
|
2858
|
+
row.appendChild(labelEl);
|
|
2859
|
+
row.appendChild(rightSide);
|
|
2860
|
+
row.addEventListener("click", (e) => {
|
|
2861
|
+
e.preventDefault();
|
|
2862
|
+
onClick2();
|
|
2863
|
+
});
|
|
2864
|
+
row.addEventListener("keydown", (e) => {
|
|
2865
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2866
|
+
e.preventDefault();
|
|
2867
|
+
onClick2();
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
return row;
|
|
2871
|
+
}
|
|
2872
|
+
renderQualityPanel() {
|
|
2873
|
+
this.panel.innerHTML = "";
|
|
2874
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2875
|
+
const header = this.createSubHeader("Quality");
|
|
2876
|
+
this.panel.appendChild(header);
|
|
2877
|
+
const qualities = this.api.getState("qualities") || [];
|
|
2878
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
2879
|
+
const activeId = currentQuality?.id || "auto";
|
|
2880
|
+
const autoItem = this.createMenuItem("Auto", "auto", activeId === "auto");
|
|
2881
|
+
autoItem.addEventListener("click", (e) => {
|
|
2882
|
+
e.preventDefault();
|
|
2883
|
+
this.selectQuality("auto");
|
|
2884
|
+
});
|
|
2885
|
+
this.panel.appendChild(autoItem);
|
|
2886
|
+
const sorted = [...qualities].sort(
|
|
2887
|
+
(a, b) => b.height - a.height
|
|
2888
|
+
);
|
|
2889
|
+
for (const q of sorted) {
|
|
2890
|
+
if (q.id === "auto") continue;
|
|
2891
|
+
const item = this.createMenuItem(q.label, q.id, q.id === activeId);
|
|
2892
|
+
item.addEventListener("click", (e) => {
|
|
2893
|
+
e.preventDefault();
|
|
2894
|
+
this.selectQuality(q.id);
|
|
2895
|
+
});
|
|
2896
|
+
this.panel.appendChild(item);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
renderSpeedPanel() {
|
|
2900
|
+
this.panel.innerHTML = "";
|
|
2901
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2902
|
+
const header = this.createSubHeader("Speed");
|
|
2903
|
+
this.panel.appendChild(header);
|
|
2904
|
+
const currentRate = this.api.getState("playbackRate") ?? 1;
|
|
2905
|
+
for (const opt of SPEED_OPTIONS) {
|
|
2906
|
+
const isActive = Math.abs(currentRate - opt.value) < 0.01;
|
|
2907
|
+
const item = this.createMenuItem(opt.label, String(opt.value), isActive);
|
|
2908
|
+
item.addEventListener("click", (e) => {
|
|
2909
|
+
e.preventDefault();
|
|
2910
|
+
this.selectSpeed(opt.value);
|
|
2911
|
+
});
|
|
2912
|
+
this.panel.appendChild(item);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
renderCaptionsPanel() {
|
|
2916
|
+
this.panel.innerHTML = "";
|
|
2917
|
+
this.panel.className = "sp-settings-panel sp-settings-panel--open sp-settings-panel--sub";
|
|
2918
|
+
const header = this.createSubHeader("Captions");
|
|
2919
|
+
this.panel.appendChild(header);
|
|
2920
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
2921
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2922
|
+
const activeId = currentTextTrack?.id || "off";
|
|
2923
|
+
const offItem = this.createMenuItem("Off", "off", activeId === "off");
|
|
2924
|
+
offItem.addEventListener("click", (e) => {
|
|
2925
|
+
e.preventDefault();
|
|
2926
|
+
this.selectCaption(null);
|
|
2927
|
+
});
|
|
2928
|
+
this.panel.appendChild(offItem);
|
|
2929
|
+
for (const track of textTracks) {
|
|
2930
|
+
const item = this.createMenuItem(track.label, track.id, track.id === activeId);
|
|
2931
|
+
item.addEventListener("click", (e) => {
|
|
2932
|
+
e.preventDefault();
|
|
2933
|
+
this.selectCaption(track.id);
|
|
2934
|
+
});
|
|
2935
|
+
this.panel.appendChild(item);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
selectCaption(trackId) {
|
|
2939
|
+
this.api.emit("track:text", { trackId });
|
|
2940
|
+
this.close();
|
|
2941
|
+
}
|
|
2942
|
+
updateCaptionsActiveStates() {
|
|
2943
|
+
const currentTextTrack = this.api.getState("currentTextTrack");
|
|
2944
|
+
const activeId = currentTextTrack?.id || "off";
|
|
2945
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
2946
|
+
items.forEach((item) => {
|
|
2947
|
+
const id = item.getAttribute("data-id");
|
|
2948
|
+
item.classList.toggle("sp-settings-panel__item--active", id === activeId);
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
createSubHeader(title) {
|
|
2952
|
+
const header = createElement("div", { className: "sp-settings-panel__header" });
|
|
2953
|
+
header.setAttribute("role", "menuitem");
|
|
2954
|
+
header.setAttribute("tabindex", "0");
|
|
2955
|
+
const backArrow = createElement("span", { className: "sp-settings-panel__back" });
|
|
2956
|
+
backArrow.innerHTML = icons.chevronUp;
|
|
2957
|
+
const label = createElement("span", { className: "sp-settings-panel__header-label" });
|
|
2958
|
+
label.textContent = title;
|
|
2959
|
+
header.appendChild(backArrow);
|
|
2960
|
+
header.appendChild(label);
|
|
2961
|
+
header.addEventListener("click", (e) => {
|
|
2962
|
+
e.preventDefault();
|
|
2963
|
+
this.showPanel("main");
|
|
2964
|
+
});
|
|
2965
|
+
header.addEventListener("keydown", (e) => {
|
|
2966
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2967
|
+
e.preventDefault();
|
|
2968
|
+
this.showPanel("main");
|
|
2969
|
+
}
|
|
2970
|
+
});
|
|
2971
|
+
return header;
|
|
2972
|
+
}
|
|
2973
|
+
createMenuItem(label, dataId, isActive) {
|
|
2974
|
+
const item = createElement("div", {
|
|
2975
|
+
className: `sp-settings-panel__item${isActive ? " sp-settings-panel__item--active" : ""}`
|
|
2976
|
+
});
|
|
2977
|
+
item.setAttribute("role", "menuitem");
|
|
2978
|
+
item.setAttribute("tabindex", "0");
|
|
2979
|
+
item.setAttribute("data-id", dataId);
|
|
2980
|
+
const labelEl = createElement("span");
|
|
2981
|
+
labelEl.textContent = label;
|
|
2982
|
+
const check = createElement("span", { className: "sp-settings-panel__check" });
|
|
2983
|
+
check.innerHTML = icons.checkmark;
|
|
2984
|
+
item.appendChild(labelEl);
|
|
2985
|
+
item.appendChild(check);
|
|
2986
|
+
item.addEventListener("keydown", (e) => {
|
|
2987
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2988
|
+
e.preventDefault();
|
|
2989
|
+
item.click();
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
return item;
|
|
2993
|
+
}
|
|
2994
|
+
selectQuality(qualityId) {
|
|
2995
|
+
this.api.emit("quality:select", {
|
|
2996
|
+
quality: qualityId,
|
|
2997
|
+
auto: qualityId === "auto"
|
|
2998
|
+
});
|
|
2999
|
+
this.close();
|
|
3000
|
+
}
|
|
3001
|
+
selectSpeed(rate) {
|
|
3002
|
+
this.api.emit("playback:ratechange", { rate });
|
|
3003
|
+
const video = this.api.container.querySelector("video");
|
|
3004
|
+
if (video) {
|
|
3005
|
+
video.playbackRate = rate;
|
|
3006
|
+
}
|
|
3007
|
+
this.close();
|
|
3008
|
+
}
|
|
3009
|
+
updateQualityActiveStates() {
|
|
3010
|
+
const currentQuality = this.api.getState("currentQuality");
|
|
3011
|
+
const activeId = currentQuality?.id || "auto";
|
|
3012
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
3013
|
+
items.forEach((item) => {
|
|
3014
|
+
const id = item.getAttribute("data-id");
|
|
3015
|
+
item.classList.toggle("sp-settings-panel__item--active", id === activeId);
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
updateSpeedActiveStates() {
|
|
3019
|
+
const currentRate = this.api.getState("playbackRate") ?? 1;
|
|
3020
|
+
const items = this.panel.querySelectorAll(".sp-settings-panel__item");
|
|
3021
|
+
items.forEach((item) => {
|
|
3022
|
+
const id = item.getAttribute("data-id");
|
|
3023
|
+
const value = parseFloat(id || "1");
|
|
3024
|
+
item.classList.toggle(
|
|
3025
|
+
"sp-settings-panel__item--active",
|
|
3026
|
+
Math.abs(currentRate - value) < 0.01
|
|
3027
|
+
);
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
getPanel() {
|
|
3031
|
+
return this.currentPanel;
|
|
3032
|
+
}
|
|
3033
|
+
isMenuOpen() {
|
|
3034
|
+
return this.isOpen;
|
|
3035
|
+
}
|
|
3036
|
+
destroy() {
|
|
3037
|
+
document.removeEventListener("click", this.closeHandler);
|
|
3038
|
+
document.removeEventListener("keydown", this.keyHandler);
|
|
3039
|
+
this.el.remove();
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
var DEFAULT_SKIP_SECONDS = 10;
|
|
3043
|
+
var SkipButton = class {
|
|
3044
|
+
constructor(api, direction, seconds = DEFAULT_SKIP_SECONDS) {
|
|
3045
|
+
this.clickHandler = () => {
|
|
3046
|
+
this.skip();
|
|
3047
|
+
};
|
|
3048
|
+
this.api = api;
|
|
3049
|
+
this.direction = direction;
|
|
3050
|
+
this.seconds = seconds;
|
|
3051
|
+
const icon = direction === "backward" ? icons.replay10 : icons.forward10;
|
|
3052
|
+
const label = direction === "backward" ? `Rewind ${seconds} seconds` : `Forward ${seconds} seconds`;
|
|
3053
|
+
this.el = createButton(
|
|
3054
|
+
`sp-skip sp-skip--${direction}`,
|
|
3055
|
+
label,
|
|
3056
|
+
icon
|
|
3057
|
+
);
|
|
3058
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
3059
|
+
}
|
|
3060
|
+
render() {
|
|
3061
|
+
return this.el;
|
|
3062
|
+
}
|
|
3063
|
+
update() {
|
|
3064
|
+
const live = this.api.getState("live");
|
|
3065
|
+
const duration = this.api.getState("duration") ?? 0;
|
|
3066
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
3067
|
+
if (live && !seekableRange) {
|
|
3068
|
+
this.el.style.display = "none";
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
if (live && seekableRange) {
|
|
3072
|
+
this.el.style.display = "";
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
if (duration === 0) {
|
|
3076
|
+
this.el.style.display = "none";
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
this.el.style.display = "";
|
|
3080
|
+
}
|
|
3081
|
+
skip() {
|
|
3082
|
+
const video = getVideo(this.api.container);
|
|
3083
|
+
if (!video) return;
|
|
3084
|
+
const live = this.api.getState("live");
|
|
3085
|
+
const seekableRange = this.api.getState("seekableRange");
|
|
3086
|
+
if (live && seekableRange) {
|
|
3087
|
+
if (this.direction === "backward") {
|
|
3088
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - this.seconds);
|
|
3089
|
+
} else {
|
|
3090
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + this.seconds);
|
|
3091
|
+
}
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
const duration = video.duration || 0;
|
|
3095
|
+
if (!duration || !isFinite(duration)) return;
|
|
3096
|
+
if (this.direction === "backward") {
|
|
3097
|
+
video.currentTime = Math.max(0, video.currentTime - this.seconds);
|
|
3098
|
+
} else {
|
|
3099
|
+
video.currentTime = Math.min(duration, video.currentTime + this.seconds);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
destroy() {
|
|
3103
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
3104
|
+
this.el.remove();
|
|
3105
|
+
}
|
|
3106
|
+
};
|
|
3107
|
+
var CaptionsButton = class {
|
|
3108
|
+
constructor(api) {
|
|
3109
|
+
this.clickHandler = () => {
|
|
3110
|
+
this.toggle();
|
|
3111
|
+
};
|
|
3112
|
+
this.api = api;
|
|
3113
|
+
this.el = createButton("sp-captions", "Captions", icons.captionsOff);
|
|
3114
|
+
this.el.addEventListener("click", this.clickHandler);
|
|
3115
|
+
}
|
|
3116
|
+
render() {
|
|
3117
|
+
return this.el;
|
|
3118
|
+
}
|
|
3119
|
+
update() {
|
|
3120
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
3121
|
+
const currentTrack = this.api.getState("currentTextTrack");
|
|
3122
|
+
if (textTracks.length === 0) {
|
|
3123
|
+
this.el.style.display = "none";
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
this.el.style.display = "";
|
|
3127
|
+
if (currentTrack) {
|
|
3128
|
+
this.el.innerHTML = icons.captions;
|
|
3129
|
+
this.el.setAttribute("aria-label", `Captions: ${currentTrack.label}`);
|
|
3130
|
+
this.el.classList.add("sp-captions--active");
|
|
3131
|
+
} else {
|
|
3132
|
+
this.el.innerHTML = icons.captionsOff;
|
|
3133
|
+
this.el.setAttribute("aria-label", "Captions");
|
|
3134
|
+
this.el.classList.remove("sp-captions--active");
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
toggle() {
|
|
3138
|
+
const textTracks = this.api.getState("textTracks") || [];
|
|
3139
|
+
const currentTrack = this.api.getState("currentTextTrack");
|
|
3140
|
+
if (textTracks.length === 0) return;
|
|
3141
|
+
if (currentTrack) {
|
|
3142
|
+
this.api.emit("track:text", { trackId: null });
|
|
3143
|
+
} else {
|
|
3144
|
+
this.api.emit("track:text", { trackId: textTracks[0].id });
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
destroy() {
|
|
3148
|
+
this.el.removeEventListener("click", this.clickHandler);
|
|
3149
|
+
this.el.remove();
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
2009
3152
|
var DEFAULT_LAYOUT = [
|
|
2010
3153
|
"play",
|
|
3154
|
+
"skip-backward",
|
|
3155
|
+
"skip-forward",
|
|
2011
3156
|
"volume",
|
|
2012
3157
|
"time",
|
|
2013
3158
|
"live-indicator",
|
|
2014
3159
|
"spacer",
|
|
2015
|
-
"
|
|
3160
|
+
"settings",
|
|
3161
|
+
"captions",
|
|
2016
3162
|
"chromecast",
|
|
2017
3163
|
"airplay",
|
|
2018
3164
|
"pip",
|
|
@@ -2025,10 +3171,12 @@ function uiPlugin(config = {}) {
|
|
|
2025
3171
|
let gradient = null;
|
|
2026
3172
|
let progressBar = null;
|
|
2027
3173
|
let bufferingIndicator = null;
|
|
3174
|
+
let errorOverlay = null;
|
|
2028
3175
|
let styleEl = null;
|
|
2029
3176
|
let controls = [];
|
|
2030
3177
|
let hideTimeout = null;
|
|
2031
3178
|
let stateUnsubscribe = null;
|
|
3179
|
+
let errorUnsubscribe = null;
|
|
2032
3180
|
let controlsVisible = true;
|
|
2033
3181
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
2034
3182
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
@@ -2036,6 +3184,10 @@ function uiPlugin(config = {}) {
|
|
|
2036
3184
|
switch (slot) {
|
|
2037
3185
|
case "play":
|
|
2038
3186
|
return new PlayButton(api);
|
|
3187
|
+
case "skip-backward":
|
|
3188
|
+
return new SkipButton(api, "backward");
|
|
3189
|
+
case "skip-forward":
|
|
3190
|
+
return new SkipButton(api, "forward");
|
|
2039
3191
|
case "volume":
|
|
2040
3192
|
return new VolumeControl(api);
|
|
2041
3193
|
case "progress":
|
|
@@ -2046,6 +3198,10 @@ function uiPlugin(config = {}) {
|
|
|
2046
3198
|
return new LiveIndicator(api);
|
|
2047
3199
|
case "quality":
|
|
2048
3200
|
return new QualityMenu(api);
|
|
3201
|
+
case "settings":
|
|
3202
|
+
return new SettingsMenu(api);
|
|
3203
|
+
case "captions":
|
|
3204
|
+
return new CaptionsButton(api);
|
|
2049
3205
|
case "chromecast":
|
|
2050
3206
|
return new CastButton(api, "chromecast");
|
|
2051
3207
|
case "airplay":
|
|
@@ -2069,6 +3225,7 @@ function uiPlugin(config = {}) {
|
|
|
2069
3225
|
const isLoading = playbackState === "loading";
|
|
2070
3226
|
const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
|
|
2071
3227
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
3228
|
+
errorOverlay?.update();
|
|
2072
3229
|
};
|
|
2073
3230
|
const showControls = () => {
|
|
2074
3231
|
if (controlsVisible) {
|
|
@@ -2107,8 +3264,14 @@ function uiPlugin(config = {}) {
|
|
|
2107
3264
|
};
|
|
2108
3265
|
const handleKeyDown = (e) => {
|
|
2109
3266
|
if (!api.container.contains(document.activeElement)) return;
|
|
3267
|
+
const activeEl = document.activeElement;
|
|
3268
|
+
if (activeEl instanceof HTMLInputElement || activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLSelectElement || activeEl?.isContentEditable) {
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
2110
3271
|
const video = api.container.querySelector("video");
|
|
2111
3272
|
if (!video) return;
|
|
3273
|
+
const live = api.getState("live");
|
|
3274
|
+
const seekableRange = api.getState("seekableRange");
|
|
2112
3275
|
switch (e.key) {
|
|
2113
3276
|
case " ":
|
|
2114
3277
|
case "k":
|
|
@@ -2129,12 +3292,20 @@ function uiPlugin(config = {}) {
|
|
|
2129
3292
|
break;
|
|
2130
3293
|
case "ArrowLeft":
|
|
2131
3294
|
e.preventDefault();
|
|
2132
|
-
|
|
3295
|
+
if (live && seekableRange) {
|
|
3296
|
+
video.currentTime = Math.max(seekableRange.start, video.currentTime - 5);
|
|
3297
|
+
} else {
|
|
3298
|
+
video.currentTime = Math.max(0, video.currentTime - 5);
|
|
3299
|
+
}
|
|
2133
3300
|
showControls();
|
|
2134
3301
|
break;
|
|
2135
3302
|
case "ArrowRight":
|
|
2136
3303
|
e.preventDefault();
|
|
2137
|
-
|
|
3304
|
+
if (live && seekableRange) {
|
|
3305
|
+
video.currentTime = Math.min(seekableRange.end, video.currentTime + 5);
|
|
3306
|
+
} else {
|
|
3307
|
+
video.currentTime = Math.min(video.duration || 0, video.currentTime + 5);
|
|
3308
|
+
}
|
|
2138
3309
|
showControls();
|
|
2139
3310
|
break;
|
|
2140
3311
|
case "ArrowUp":
|
|
@@ -2171,19 +3342,30 @@ function uiPlugin(config = {}) {
|
|
|
2171
3342
|
if (containerStyle.position === "static") {
|
|
2172
3343
|
container.style.position = "relative";
|
|
2173
3344
|
}
|
|
3345
|
+
const isPlaying = api.getState("playing");
|
|
2174
3346
|
gradient = document.createElement("div");
|
|
2175
|
-
gradient.className = "sp-gradient sp-gradient--visible";
|
|
3347
|
+
gradient.className = isPlaying ? "sp-gradient" : "sp-gradient sp-gradient--visible";
|
|
2176
3348
|
container.appendChild(gradient);
|
|
2177
3349
|
bufferingIndicator = document.createElement("div");
|
|
2178
3350
|
bufferingIndicator.className = "sp-buffering";
|
|
2179
3351
|
bufferingIndicator.innerHTML = icons.spinner;
|
|
2180
3352
|
bufferingIndicator.setAttribute("aria-hidden", "true");
|
|
2181
3353
|
container.appendChild(bufferingIndicator);
|
|
3354
|
+
errorOverlay = new ErrorOverlay(api);
|
|
3355
|
+
container.appendChild(errorOverlay.render());
|
|
3356
|
+
errorUnsubscribe = api.on("error", (payload) => {
|
|
3357
|
+
if (payload?.fatal) {
|
|
3358
|
+
const error = api.getState("error") || new Error(payload.message || "Playback error");
|
|
3359
|
+
errorOverlay?.show(error);
|
|
3360
|
+
}
|
|
3361
|
+
});
|
|
2182
3362
|
progressBar = new ProgressBar(api);
|
|
2183
3363
|
container.appendChild(progressBar.render());
|
|
2184
|
-
|
|
3364
|
+
if (!isPlaying) {
|
|
3365
|
+
progressBar.show();
|
|
3366
|
+
}
|
|
2185
3367
|
controlBar = document.createElement("div");
|
|
2186
|
-
controlBar.className = "sp-controls sp-controls--visible";
|
|
3368
|
+
controlBar.className = isPlaying ? "sp-controls sp-controls--hidden" : "sp-controls sp-controls--visible";
|
|
2187
3369
|
controlBar.setAttribute("role", "toolbar");
|
|
2188
3370
|
controlBar.setAttribute("aria-label", "Video controls");
|
|
2189
3371
|
for (const slot of layout) {
|
|
@@ -2206,6 +3388,11 @@ function uiPlugin(config = {}) {
|
|
|
2206
3388
|
if (!container.hasAttribute("tabindex")) {
|
|
2207
3389
|
container.setAttribute("tabindex", "0");
|
|
2208
3390
|
}
|
|
3391
|
+
controlsVisible = !isPlaying;
|
|
3392
|
+
api.setState("controlsVisible", controlsVisible);
|
|
3393
|
+
if (isPlaying) {
|
|
3394
|
+
resetHideTimer();
|
|
3395
|
+
}
|
|
2209
3396
|
api.logger.debug("UI controls plugin initialized");
|
|
2210
3397
|
},
|
|
2211
3398
|
async destroy() {
|
|
@@ -2215,6 +3402,8 @@ function uiPlugin(config = {}) {
|
|
|
2215
3402
|
}
|
|
2216
3403
|
stateUnsubscribe?.();
|
|
2217
3404
|
stateUnsubscribe = null;
|
|
3405
|
+
errorUnsubscribe?.();
|
|
3406
|
+
errorUnsubscribe = null;
|
|
2218
3407
|
if (api?.container) {
|
|
2219
3408
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
2220
3409
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -2228,6 +3417,8 @@ function uiPlugin(config = {}) {
|
|
|
2228
3417
|
controls = [];
|
|
2229
3418
|
progressBar?.destroy();
|
|
2230
3419
|
progressBar = null;
|
|
3420
|
+
errorOverlay?.destroy();
|
|
3421
|
+
errorOverlay = null;
|
|
2231
3422
|
controlBar?.remove();
|
|
2232
3423
|
controlBar = null;
|
|
2233
3424
|
gradient?.remove();
|
|
@@ -4311,6 +5502,8 @@ const DEFAULT_STATE = {
|
|
|
4311
5502
|
airplayActive: false,
|
|
4312
5503
|
chromecastAvailable: false,
|
|
4313
5504
|
chromecastActive: false,
|
|
5505
|
+
// Thumbnail Preview
|
|
5506
|
+
thumbnails: null,
|
|
4314
5507
|
// UI State
|
|
4315
5508
|
interacting: false,
|
|
4316
5509
|
hovering: false,
|