@scarlett-player/embed 0.4.1 → 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 +5 -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 +1213 -44
- 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 +1213 -44
- 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
|
@@ -397,7 +397,9 @@ function createHLSPlugin(config) {
|
|
|
397
397
|
const getRetryDelay = (retryCount) => {
|
|
398
398
|
const baseDelay = mergedConfig.retryDelayMs ?? 1e3;
|
|
399
399
|
const backoffFactor = mergedConfig.retryBackoffFactor ?? 2;
|
|
400
|
-
|
|
400
|
+
const delay = baseDelay * Math.pow(backoffFactor, retryCount);
|
|
401
|
+
const jitter = delay * (0.7 + Math.random() * 0.3);
|
|
402
|
+
return jitter;
|
|
401
403
|
};
|
|
402
404
|
const emitFatalError = (error, retriesExhausted) => {
|
|
403
405
|
const message = retriesExhausted ? `HLS error: ${error.details} (max retries exceeded)` : `HLS error: ${error.details}`;
|
|
@@ -886,7 +888,12 @@ var styles = `
|
|
|
886
888
|
transition: height 0.15s ease;
|
|
887
889
|
}
|
|
888
890
|
|
|
889
|
-
|
|
891
|
+
@media (hover: hover) {
|
|
892
|
+
.sp-progress-wrapper:hover .sp-progress {
|
|
893
|
+
height: 5px;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
890
897
|
.sp-progress--dragging {
|
|
891
898
|
height: 5px;
|
|
892
899
|
}
|
|
@@ -932,11 +939,34 @@ var styles = `
|
|
|
932
939
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
933
940
|
}
|
|
934
941
|
|
|
935
|
-
|
|
942
|
+
@media (hover: hover) {
|
|
943
|
+
.sp-progress-wrapper:hover .sp-progress__handle {
|
|
944
|
+
transform: translate(-50%, -50%) scale(1);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
936
948
|
.sp-progress--dragging .sp-progress__handle {
|
|
937
949
|
transform: translate(-50%, -50%) scale(1);
|
|
938
950
|
}
|
|
939
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
|
+
|
|
940
970
|
/* Progress Tooltip */
|
|
941
971
|
.sp-progress__tooltip {
|
|
942
972
|
position: absolute;
|
|
@@ -956,8 +986,10 @@ var styles = `
|
|
|
956
986
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
957
987
|
}
|
|
958
988
|
|
|
959
|
-
|
|
960
|
-
|
|
989
|
+
@media (hover: hover) {
|
|
990
|
+
.sp-progress-wrapper:hover .sp-progress__tooltip {
|
|
991
|
+
opacity: 1;
|
|
992
|
+
}
|
|
961
993
|
}
|
|
962
994
|
|
|
963
995
|
/* ============================================
|
|
@@ -977,9 +1009,11 @@ var styles = `
|
|
|
977
1009
|
flex-shrink: 0;
|
|
978
1010
|
}
|
|
979
1011
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1012
|
+
@media (hover: hover) {
|
|
1013
|
+
.sp-control:hover {
|
|
1014
|
+
color: #fff;
|
|
1015
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1016
|
+
}
|
|
983
1017
|
}
|
|
984
1018
|
|
|
985
1019
|
.sp-control:active {
|
|
@@ -1048,7 +1082,12 @@ var styles = `
|
|
|
1048
1082
|
transition: width 0.2s ease;
|
|
1049
1083
|
}
|
|
1050
1084
|
|
|
1051
|
-
|
|
1085
|
+
@media (hover: hover) {
|
|
1086
|
+
.sp-volume:hover .sp-volume__slider-wrap {
|
|
1087
|
+
width: 64px;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1052
1091
|
.sp-volume:focus-within .sp-volume__slider-wrap {
|
|
1053
1092
|
width: 64px;
|
|
1054
1093
|
}
|
|
@@ -1091,8 +1130,10 @@ var styles = `
|
|
|
1091
1130
|
transition: background 0.15s ease, opacity 0.15s ease;
|
|
1092
1131
|
}
|
|
1093
1132
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1133
|
+
@media (hover: hover) {
|
|
1134
|
+
.sp-live:hover {
|
|
1135
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1136
|
+
}
|
|
1096
1137
|
}
|
|
1097
1138
|
|
|
1098
1139
|
.sp-live__dot {
|
|
@@ -1111,6 +1152,16 @@ var styles = `
|
|
|
1111
1152
|
animation: none;
|
|
1112
1153
|
}
|
|
1113
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
|
+
|
|
1114
1165
|
@keyframes sp-pulse {
|
|
1115
1166
|
0%, 100% { opacity: 1; }
|
|
1116
1167
|
50% { opacity: 0.4; }
|
|
@@ -1191,6 +1242,169 @@ var styles = `
|
|
|
1191
1242
|
opacity: 1;
|
|
1192
1243
|
}
|
|
1193
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
|
+
|
|
1194
1408
|
/* ============================================
|
|
1195
1409
|
Cast Button States
|
|
1196
1410
|
============================================ */
|
|
@@ -1202,6 +1416,122 @@ var styles = `
|
|
|
1202
1416
|
opacity: 0.4;
|
|
1203
1417
|
}
|
|
1204
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
|
+
|
|
1205
1535
|
/* ============================================
|
|
1206
1536
|
Buffering Indicator
|
|
1207
1537
|
============================================ */
|
|
@@ -1249,7 +1579,14 @@ var styles = `
|
|
|
1249
1579
|
.sp-control,
|
|
1250
1580
|
.sp-volume__slider-wrap,
|
|
1251
1581
|
.sp-quality-menu,
|
|
1252
|
-
.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 {
|
|
1253
1590
|
transition: none;
|
|
1254
1591
|
}
|
|
1255
1592
|
|
|
@@ -1284,7 +1621,15 @@ var icons = {
|
|
|
1284
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>`,
|
|
1285
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>`,
|
|
1286
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>`,
|
|
1287
|
-
|
|
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>`
|
|
1288
1633
|
};
|
|
1289
1634
|
function createElement(tag, attrs, children) {
|
|
1290
1635
|
const el = document.createElement(tag);
|
|
@@ -1384,6 +1729,68 @@ var PlayButton = class {
|
|
|
1384
1729
|
this.el.remove();
|
|
1385
1730
|
}
|
|
1386
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
|
+
};
|
|
1387
1794
|
var ProgressBar = class {
|
|
1388
1795
|
constructor(api) {
|
|
1389
1796
|
this.isDragging = false;
|
|
@@ -1423,36 +1830,99 @@ var ProgressBar = class {
|
|
|
1423
1830
|
}
|
|
1424
1831
|
}
|
|
1425
1832
|
};
|
|
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);
|
|
1841
|
+
};
|
|
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
|
+
};
|
|
1426
1872
|
this.onMouseMove = (e) => {
|
|
1427
1873
|
this.updateTooltip(e.clientX);
|
|
1428
1874
|
};
|
|
1429
1875
|
this.onMouseLeave = () => {
|
|
1430
1876
|
if (!this.isDragging) {
|
|
1431
1877
|
this.tooltip.style.opacity = "0";
|
|
1878
|
+
this.thumbnailPreview.hide();
|
|
1432
1879
|
}
|
|
1433
1880
|
};
|
|
1434
1881
|
this.onKeyDown = (e) => {
|
|
1435
1882
|
const video = getVideo(this.api.container);
|
|
1436
1883
|
if (!video) return;
|
|
1437
1884
|
const step = 5;
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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
|
+
}
|
|
1456
1926
|
}
|
|
1457
1927
|
};
|
|
1458
1928
|
this.api = api;
|
|
@@ -1464,10 +1934,12 @@ var ProgressBar = class {
|
|
|
1464
1934
|
this.handle = createElement("div", { className: "sp-progress__handle" });
|
|
1465
1935
|
this.tooltip = createElement("div", { className: "sp-progress__tooltip" });
|
|
1466
1936
|
this.tooltip.textContent = "0:00";
|
|
1937
|
+
this.thumbnailPreview = new ThumbnailPreview();
|
|
1467
1938
|
track.appendChild(this.buffered);
|
|
1468
1939
|
track.appendChild(this.filled);
|
|
1469
1940
|
track.appendChild(this.handle);
|
|
1470
1941
|
this.el.appendChild(track);
|
|
1942
|
+
this.el.appendChild(this.thumbnailPreview.getElement());
|
|
1471
1943
|
this.el.appendChild(this.tooltip);
|
|
1472
1944
|
this.wrapper.appendChild(this.el);
|
|
1473
1945
|
this.el.setAttribute("role", "slider");
|
|
@@ -1477,9 +1949,13 @@ var ProgressBar = class {
|
|
|
1477
1949
|
this.wrapper.addEventListener("mousedown", this.onMouseDown);
|
|
1478
1950
|
this.wrapper.addEventListener("mousemove", this.onMouseMove);
|
|
1479
1951
|
this.wrapper.addEventListener("mouseleave", this.onMouseLeave);
|
|
1952
|
+
this.wrapper.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
1480
1953
|
this.el.addEventListener("keydown", this.onKeyDown);
|
|
1481
1954
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
1482
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);
|
|
1483
1959
|
}
|
|
1484
1960
|
render() {
|
|
1485
1961
|
return this.wrapper;
|
|
@@ -1492,11 +1968,40 @@ var ProgressBar = class {
|
|
|
1492
1968
|
hide() {
|
|
1493
1969
|
this.wrapper.classList.remove("sp-progress-wrapper--visible");
|
|
1494
1970
|
}
|
|
1971
|
+
/** Set thumbnail sprite configuration */
|
|
1972
|
+
setThumbnails(config) {
|
|
1973
|
+
this.thumbnailPreview.setConfig(config);
|
|
1974
|
+
}
|
|
1495
1975
|
update() {
|
|
1496
1976
|
const currentTime = this.api.getState("currentTime") || 0;
|
|
1497
1977
|
const duration = this.api.getState("duration") || 0;
|
|
1498
1978
|
const bufferedRanges = this.api.getState("buffered");
|
|
1499
|
-
|
|
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) {
|
|
1500
2005
|
const progress = currentTime / duration * 100;
|
|
1501
2006
|
this.filled.style.width = `${progress}%`;
|
|
1502
2007
|
this.handle.style.left = `${progress}%`;
|
|
@@ -1513,6 +2018,12 @@ var ProgressBar = class {
|
|
|
1513
2018
|
getTimeFromPosition(clientX) {
|
|
1514
2019
|
const rect = this.el.getBoundingClientRect();
|
|
1515
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
|
+
}
|
|
1516
2027
|
const duration = this.api.getState("duration") || 0;
|
|
1517
2028
|
return percent * duration;
|
|
1518
2029
|
}
|
|
@@ -1520,8 +2031,18 @@ var ProgressBar = class {
|
|
|
1520
2031
|
const rect = this.el.getBoundingClientRect();
|
|
1521
2032
|
const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1522
2033
|
const time = this.getTimeFromPosition(clientX);
|
|
1523
|
-
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
|
+
}
|
|
1524
2042
|
this.tooltip.style.left = `${percent * 100}%`;
|
|
2043
|
+
if (this.thumbnailPreview.isConfigured()) {
|
|
2044
|
+
this.thumbnailPreview.show(time, percent);
|
|
2045
|
+
}
|
|
1525
2046
|
}
|
|
1526
2047
|
updateVisualPosition(clientX) {
|
|
1527
2048
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -1544,8 +2065,13 @@ var ProgressBar = class {
|
|
|
1544
2065
|
this.wrapper.removeEventListener("mousedown", this.onMouseDown);
|
|
1545
2066
|
this.wrapper.removeEventListener("mousemove", this.onMouseMove);
|
|
1546
2067
|
this.wrapper.removeEventListener("mouseleave", this.onMouseLeave);
|
|
2068
|
+
this.wrapper.removeEventListener("touchstart", this.onTouchStart);
|
|
1547
2069
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
1548
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();
|
|
1549
2075
|
this.wrapper.remove();
|
|
1550
2076
|
}
|
|
1551
2077
|
};
|
|
@@ -1594,6 +2120,20 @@ var VolumeControl = class {
|
|
|
1594
2120
|
this.onMouseUp = () => {
|
|
1595
2121
|
this.isDragging = false;
|
|
1596
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
|
+
};
|
|
1597
2137
|
this.onKeyDown = (e) => {
|
|
1598
2138
|
const video = getVideo(this.api.container);
|
|
1599
2139
|
if (!video) return;
|
|
@@ -1633,9 +2173,13 @@ var VolumeControl = class {
|
|
|
1633
2173
|
this.el.appendChild(this.btn);
|
|
1634
2174
|
this.el.appendChild(sliderWrap);
|
|
1635
2175
|
this.slider.addEventListener("mousedown", this.onMouseDown);
|
|
2176
|
+
this.slider.addEventListener("touchstart", this.onTouchStart, { passive: false });
|
|
1636
2177
|
this.slider.addEventListener("keydown", this.onKeyDown);
|
|
1637
2178
|
document.addEventListener("mousemove", this.onDocMouseMove);
|
|
1638
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);
|
|
1639
2183
|
}
|
|
1640
2184
|
render() {
|
|
1641
2185
|
return this.el;
|
|
@@ -1680,26 +2224,40 @@ var VolumeControl = class {
|
|
|
1680
2224
|
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1681
2225
|
}
|
|
1682
2226
|
destroy() {
|
|
2227
|
+
this.slider.removeEventListener("mousedown", this.onMouseDown);
|
|
2228
|
+
this.slider.removeEventListener("touchstart", this.onTouchStart);
|
|
2229
|
+
this.slider.removeEventListener("keydown", this.onKeyDown);
|
|
1683
2230
|
document.removeEventListener("mousemove", this.onDocMouseMove);
|
|
1684
2231
|
document.removeEventListener("mouseup", this.onMouseUp);
|
|
2232
|
+
document.removeEventListener("touchmove", this.onDocTouchMove);
|
|
2233
|
+
document.removeEventListener("touchend", this.onTouchEnd);
|
|
2234
|
+
document.removeEventListener("touchcancel", this.onTouchEnd);
|
|
1685
2235
|
this.el.remove();
|
|
1686
2236
|
}
|
|
1687
2237
|
};
|
|
1688
2238
|
var LiveIndicator = class {
|
|
1689
2239
|
constructor(api) {
|
|
1690
|
-
this.
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
this.
|
|
1694
|
-
this.el.setAttribute("aria-label", "Seek to live");
|
|
1695
|
-
this.el.setAttribute("tabindex", "0");
|
|
1696
|
-
this.el.onclick = () => this.seekToLive();
|
|
1697
|
-
this.el.onkeydown = (e) => {
|
|
2240
|
+
this.handleClick = () => {
|
|
2241
|
+
this.seekToLive();
|
|
2242
|
+
};
|
|
2243
|
+
this.handleKeyDown = (e) => {
|
|
1698
2244
|
if (e.key === "Enter" || e.key === " ") {
|
|
1699
2245
|
e.preventDefault();
|
|
1700
2246
|
this.seekToLive();
|
|
1701
2247
|
}
|
|
1702
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);
|
|
1703
2261
|
}
|
|
1704
2262
|
render() {
|
|
1705
2263
|
return this.el;
|
|
@@ -1710,8 +2268,12 @@ var LiveIndicator = class {
|
|
|
1710
2268
|
this.el.style.display = live ? "" : "none";
|
|
1711
2269
|
if (liveEdge) {
|
|
1712
2270
|
this.el.classList.remove("sp-live--behind");
|
|
2271
|
+
this.label.textContent = "LIVE";
|
|
2272
|
+
this.el.setAttribute("aria-label", "At live edge");
|
|
1713
2273
|
} else {
|
|
1714
2274
|
this.el.classList.add("sp-live--behind");
|
|
2275
|
+
this.label.textContent = "GO LIVE";
|
|
2276
|
+
this.el.setAttribute("aria-label", "Seek to live");
|
|
1715
2277
|
}
|
|
1716
2278
|
}
|
|
1717
2279
|
seekToLive() {
|
|
@@ -1723,6 +2285,8 @@ var LiveIndicator = class {
|
|
|
1723
2285
|
}
|
|
1724
2286
|
}
|
|
1725
2287
|
destroy() {
|
|
2288
|
+
this.el.removeEventListener("click", this.handleClick);
|
|
2289
|
+
this.el.removeEventListener("keydown", this.handleKeyDown);
|
|
1726
2290
|
this.el.remove();
|
|
1727
2291
|
}
|
|
1728
2292
|
};
|
|
@@ -2022,13 +2586,579 @@ var Spacer = class {
|
|
|
2022
2586
|
this.el.remove();
|
|
2023
2587
|
}
|
|
2024
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
|
+
};
|
|
2025
3152
|
var DEFAULT_LAYOUT = [
|
|
2026
3153
|
"play",
|
|
3154
|
+
"skip-backward",
|
|
3155
|
+
"skip-forward",
|
|
2027
3156
|
"volume",
|
|
2028
3157
|
"time",
|
|
2029
3158
|
"live-indicator",
|
|
2030
3159
|
"spacer",
|
|
2031
|
-
"
|
|
3160
|
+
"settings",
|
|
3161
|
+
"captions",
|
|
2032
3162
|
"chromecast",
|
|
2033
3163
|
"airplay",
|
|
2034
3164
|
"pip",
|
|
@@ -2041,10 +3171,12 @@ function uiPlugin(config = {}) {
|
|
|
2041
3171
|
let gradient = null;
|
|
2042
3172
|
let progressBar = null;
|
|
2043
3173
|
let bufferingIndicator = null;
|
|
3174
|
+
let errorOverlay = null;
|
|
2044
3175
|
let styleEl = null;
|
|
2045
3176
|
let controls = [];
|
|
2046
3177
|
let hideTimeout = null;
|
|
2047
3178
|
let stateUnsubscribe = null;
|
|
3179
|
+
let errorUnsubscribe = null;
|
|
2048
3180
|
let controlsVisible = true;
|
|
2049
3181
|
const layout = config.controls || DEFAULT_LAYOUT;
|
|
2050
3182
|
const hideDelay = config.hideDelay ?? DEFAULT_HIDE_DELAY;
|
|
@@ -2052,6 +3184,10 @@ function uiPlugin(config = {}) {
|
|
|
2052
3184
|
switch (slot) {
|
|
2053
3185
|
case "play":
|
|
2054
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");
|
|
2055
3191
|
case "volume":
|
|
2056
3192
|
return new VolumeControl(api);
|
|
2057
3193
|
case "progress":
|
|
@@ -2062,6 +3198,10 @@ function uiPlugin(config = {}) {
|
|
|
2062
3198
|
return new LiveIndicator(api);
|
|
2063
3199
|
case "quality":
|
|
2064
3200
|
return new QualityMenu(api);
|
|
3201
|
+
case "settings":
|
|
3202
|
+
return new SettingsMenu(api);
|
|
3203
|
+
case "captions":
|
|
3204
|
+
return new CaptionsButton(api);
|
|
2065
3205
|
case "chromecast":
|
|
2066
3206
|
return new CastButton(api, "chromecast");
|
|
2067
3207
|
case "airplay":
|
|
@@ -2085,6 +3225,7 @@ function uiPlugin(config = {}) {
|
|
|
2085
3225
|
const isLoading = playbackState === "loading";
|
|
2086
3226
|
const showSpinner = waiting || seeking && !api?.getState("paused") || isLoading;
|
|
2087
3227
|
bufferingIndicator?.classList.toggle("sp-buffering--visible", !!showSpinner);
|
|
3228
|
+
errorOverlay?.update();
|
|
2088
3229
|
};
|
|
2089
3230
|
const showControls = () => {
|
|
2090
3231
|
if (controlsVisible) {
|
|
@@ -2123,8 +3264,14 @@ function uiPlugin(config = {}) {
|
|
|
2123
3264
|
};
|
|
2124
3265
|
const handleKeyDown = (e) => {
|
|
2125
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
|
+
}
|
|
2126
3271
|
const video = api.container.querySelector("video");
|
|
2127
3272
|
if (!video) return;
|
|
3273
|
+
const live = api.getState("live");
|
|
3274
|
+
const seekableRange = api.getState("seekableRange");
|
|
2128
3275
|
switch (e.key) {
|
|
2129
3276
|
case " ":
|
|
2130
3277
|
case "k":
|
|
@@ -2145,12 +3292,20 @@ function uiPlugin(config = {}) {
|
|
|
2145
3292
|
break;
|
|
2146
3293
|
case "ArrowLeft":
|
|
2147
3294
|
e.preventDefault();
|
|
2148
|
-
|
|
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
|
+
}
|
|
2149
3300
|
showControls();
|
|
2150
3301
|
break;
|
|
2151
3302
|
case "ArrowRight":
|
|
2152
3303
|
e.preventDefault();
|
|
2153
|
-
|
|
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
|
+
}
|
|
2154
3309
|
showControls();
|
|
2155
3310
|
break;
|
|
2156
3311
|
case "ArrowUp":
|
|
@@ -2196,6 +3351,14 @@ function uiPlugin(config = {}) {
|
|
|
2196
3351
|
bufferingIndicator.innerHTML = icons.spinner;
|
|
2197
3352
|
bufferingIndicator.setAttribute("aria-hidden", "true");
|
|
2198
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
|
+
});
|
|
2199
3362
|
progressBar = new ProgressBar(api);
|
|
2200
3363
|
container.appendChild(progressBar.render());
|
|
2201
3364
|
if (!isPlaying) {
|
|
@@ -2239,6 +3402,8 @@ function uiPlugin(config = {}) {
|
|
|
2239
3402
|
}
|
|
2240
3403
|
stateUnsubscribe?.();
|
|
2241
3404
|
stateUnsubscribe = null;
|
|
3405
|
+
errorUnsubscribe?.();
|
|
3406
|
+
errorUnsubscribe = null;
|
|
2242
3407
|
if (api?.container) {
|
|
2243
3408
|
api.container.removeEventListener("mousemove", handleInteraction);
|
|
2244
3409
|
api.container.removeEventListener("mouseenter", handleInteraction);
|
|
@@ -2252,6 +3417,8 @@ function uiPlugin(config = {}) {
|
|
|
2252
3417
|
controls = [];
|
|
2253
3418
|
progressBar?.destroy();
|
|
2254
3419
|
progressBar = null;
|
|
3420
|
+
errorOverlay?.destroy();
|
|
3421
|
+
errorOverlay = null;
|
|
2255
3422
|
controlBar?.remove();
|
|
2256
3423
|
controlBar = null;
|
|
2257
3424
|
gradient?.remove();
|
|
@@ -4335,6 +5502,8 @@ const DEFAULT_STATE = {
|
|
|
4335
5502
|
airplayActive: false,
|
|
4336
5503
|
chromecastAvailable: false,
|
|
4337
5504
|
chromecastActive: false,
|
|
5505
|
+
// Thumbnail Preview
|
|
5506
|
+
thumbnails: null,
|
|
4338
5507
|
// UI State
|
|
4339
5508
|
interacting: false,
|
|
4340
5509
|
hovering: false,
|