@banou/media-player 0.8.11 → 0.8.12

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/index.js CHANGED
@@ -117,14 +117,26 @@ var ae = {
117
117
  ready: !1
118
118
  });
119
119
  let u = (t, n) => {
120
- let r = {
121
- at: Date.now(),
120
+ let r = Date.now(), i = R(t), o = fe(t), s = a.playbackErrors, c = s[s.length - 1];
121
+ if (c && c.message === i && c.detail === o && c.recovered === n) {
122
+ let e = {
123
+ ...c,
124
+ count: c.count + 1,
125
+ lastAt: r
126
+ };
127
+ a.setSourceState({ playbackErrors: [...s.slice(0, -1), e] });
128
+ return;
129
+ }
130
+ let l = {
131
+ at: r,
132
+ lastAt: r,
133
+ count: 1,
122
134
  atMediaTime: Number.isFinite(e.currentTime) ? e.currentTime : void 0,
123
- message: R(t),
124
- detail: fe(t),
135
+ message: i,
136
+ detail: o,
125
137
  recovered: n
126
138
  };
127
- a.setSourceState({ playbackErrors: [...a.playbackErrors, r] });
139
+ a.setSourceState({ playbackErrors: [...s, l] });
128
140
  }, d = (e) => {
129
141
  if (r) return;
130
142
  let t = n(e);
@@ -1046,6 +1058,12 @@ ${X}
1046
1058
  .when {
1047
1059
  color: #bbb;
1048
1060
  ${V.bSmall.regular}
1061
+
1062
+ /* the one part of the line worth scanning for, so it is the one part at full brightness */
1063
+ .count {
1064
+ margin-left: 6px;
1065
+ color: #fff;
1066
+ }
1049
1067
  }
1050
1068
 
1051
1069
  .what {
@@ -1062,21 +1080,21 @@ ${X}
1062
1080
  }
1063
1081
  }
1064
1082
  }
1065
- `, Me = (e) => new Date(e).toLocaleTimeString(), Q = (e) => e === void 0 || !Number.isFinite(e) ? void 0 : K(Math.max(0, e)), Ne = (e) => e.map((e, t) => {
1066
- let n = Q(e.atMediaTime);
1083
+ `, Me = (e) => new Date(e).toLocaleTimeString(), Q = (e) => e === void 0 || !Number.isFinite(e) ? void 0 : K(Math.max(0, e)), Ne = (e) => e.reduce((e, t) => e + t.count, 0), Pe = (e) => e.map((e, t) => {
1084
+ let n = Q(e.atMediaTime), r = e.count > 1 ? `${new Date(e.at).toISOString()} to ${new Date(e.lastAt).toISOString()} ×${e.count}` : new Date(e.at).toISOString();
1067
1085
  return [
1068
- `${t + 1}. ${new Date(e.at).toISOString()}${n ? ` (at ${n})` : ""}${e.recovered ? " [recovered]" : ""}`,
1086
+ `${t + 1}. ${r}${n ? ` (at ${n})` : ""}${e.recovered ? " [recovered]" : ""}`,
1069
1087
  e.message,
1070
1088
  e.detail
1071
1089
  ].filter(Boolean).join("\n");
1072
- }).join("\n\n"), Pe = () => {
1090
+ }).join("\n\n"), Fe = () => {
1073
1091
  let e = L((e) => e.playbackErrors), { open: t, toggle: n, containerRef: r } = Z(), [i, a] = m(!1);
1074
1092
  if (e.length === 0) return null;
1075
1093
  let o = () => {
1076
- navigator.clipboard?.writeText(Ne(e)).then(() => {
1094
+ navigator.clipboard?.writeText(Pe(e)).then(() => {
1077
1095
  a(!0), setTimeout(() => a(!1), 2e3);
1078
1096
  }, (e) => console.warn("[media-player] could not copy the errors:", e));
1079
- }, s = `Playback errors (${e.length})`;
1097
+ }, s = `Playback errors (${Ne(e)})`;
1080
1098
  return /* @__PURE__ */ w("div", {
1081
1099
  css: je,
1082
1100
  ref: r,
@@ -1110,8 +1128,13 @@ ${X}
1110
1128
  className: "when",
1111
1129
  children: [
1112
1130
  Me(e.at),
1131
+ e.count > 1 ? ` to ${Me(e.lastAt)}` : "",
1113
1132
  Q(e.atMediaTime) ? ` at ${Q(e.atMediaTime)}` : "",
1114
- e.recovered ? " recovered" : ""
1133
+ e.recovered ? " recovered" : "",
1134
+ e.count > 1 ? /* @__PURE__ */ C("span", {
1135
+ className: "count",
1136
+ children: `×${e.count}`
1137
+ }) : null
1115
1138
  ]
1116
1139
  }),
1117
1140
  /* @__PURE__ */ C("span", {
@@ -1126,7 +1149,7 @@ ${X}
1126
1149
  }, `${e.at}-${t}`))]
1127
1150
  })]
1128
1151
  });
1129
- }, Fe = h`
1152
+ }, Ie = h`
1130
1153
  display: flex;
1131
1154
  align-items: center;
1132
1155
  justify-content: center;
@@ -1179,7 +1202,7 @@ width: 100%;
1179
1202
  }
1180
1203
  }
1181
1204
  }
1182
- `, Ie = () => {
1205
+ `, Le = () => {
1183
1206
  let e = L(), t = L((e) => e.playbackRate), n = p(null), r = p(null), i = .25, a = 3 - i, o = (e, t = .05) => Math.round(e / t) * t, s = o(t || 1), c = (e) => (e - i) / a * 100, l = (e) => o(i + e * a), { handlers: u } = J({
1184
1207
  ref: r,
1185
1208
  onChange: (t) => e.setPlaybackRate(l(t))
@@ -1196,7 +1219,7 @@ width: 100%;
1196
1219
  }, [t]);
1197
1220
  let m = c(s);
1198
1221
  return /* @__PURE__ */ w("div", {
1199
- css: Fe,
1222
+ css: Ie,
1200
1223
  children: [/* @__PURE__ */ w("div", { children: [s.toFixed(2), "x"] }), /* @__PURE__ */ C("div", {
1201
1224
  className: "playback-slider",
1202
1225
  ref: n,
@@ -1214,7 +1237,7 @@ width: 100%;
1214
1237
  })
1215
1238
  })]
1216
1239
  });
1217
- }, Le = h`
1240
+ }, Re = h`
1218
1241
  /* Deliberately NOT a containing block. The menu anchors to the control bar instead, which is exactly
1219
1242
  as wide as the player box and can therefore be clamped to it. The outside-click check uses
1220
1243
  \`contains\` on the ref, which is DOM containment and needs no position, and the bar's other
@@ -1273,7 +1296,7 @@ ${X}
1273
1296
  }
1274
1297
  }
1275
1298
 
1276
- `, Re = () => {
1299
+ `, ze = () => {
1277
1300
  let e = L(), t = L((e) => e.playbackRate), n = L((e) => e.audioTracks), r = L((e) => e.selectedAudioTrack), i = L((e) => e.selectAudioTrack), [a, o] = m(0), { open: s, toggle: c, containerRef: l, pending: u, failed: d, runSelect: f } = Z({ onReset: () => o(0) }), p = (t) => {
1278
1301
  e.setPlaybackRate(t), o(0);
1279
1302
  }, h = (e) => {
@@ -1282,7 +1305,7 @@ ${X}
1282
1305
  o(e);
1283
1306
  };
1284
1307
  return /* @__PURE__ */ w("div", {
1285
- css: Le,
1308
+ css: Re,
1286
1309
  ref: l,
1287
1310
  children: [
1288
1311
  /* @__PURE__ */ C(q, {
@@ -1321,7 +1344,7 @@ ${X}
1321
1344
  }),
1322
1345
  /* @__PURE__ */ C("div", {
1323
1346
  className: "slider no-hover",
1324
- children: /* @__PURE__ */ C(Ie, {})
1347
+ children: /* @__PURE__ */ C(Le, {})
1325
1348
  }),
1326
1349
  /* @__PURE__ */ w("div", {
1327
1350
  className: "options no-hover",
@@ -1357,7 +1380,7 @@ ${X}
1357
1380
  })
1358
1381
  ]
1359
1382
  });
1360
- }, ze = h`
1383
+ }, Be = h`
1361
1384
  /* Not a containing block, for the reason given on popoverStyle: the menu anchors to the control bar,
1362
1385
  which is the width of the player box, so it can be clamped to it. */
1363
1386
  position: static;
@@ -1374,12 +1397,12 @@ position: static;
1374
1397
  }
1375
1398
 
1376
1399
  ${X}
1377
- `, Be = () => {
1400
+ `, Ve = () => {
1378
1401
  let e = L((e) => e.subtitleTracks), t = L((e) => e.selectedSubtitleTrack), n = L((e) => e.selectSubtitleTrack), r = L((e) => e.subtitleOffLabel), { open: i, toggle: a, containerRef: o, pending: s, failed: c, runSelect: l } = Z();
1379
1402
  if (e.length === 0) return null;
1380
1403
  let u = t !== void 0;
1381
1404
  return /* @__PURE__ */ w("div", {
1382
- css: ze,
1405
+ css: Be,
1383
1406
  ref: o,
1384
1407
  children: [/* @__PURE__ */ C(q, {
1385
1408
  id: "subtitles",
@@ -1404,14 +1427,14 @@ ${X}
1404
1427
  failed: c
1405
1428
  })]
1406
1429
  });
1407
- }, Ve = {
1430
+ }, He = {
1408
1431
  primary: "#EAEBEE",
1409
1432
  secondary: "#D0D0D9",
1410
1433
  hover: "rgba(255, 255, 255, 0.13)",
1411
1434
  accent: "#6EA8FE",
1412
1435
  borderPrimary: "#384F70",
1413
1436
  backgroundTooltip: "#222222"
1414
- }, He = h`
1437
+ }, Ue = h`
1415
1438
  display: flex;
1416
1439
  align-items: center;
1417
1440
 
@@ -1454,7 +1477,7 @@ ${X}
1454
1477
  width: 10px;
1455
1478
  height: 10px;
1456
1479
  }
1457
- `, Ue = ({ value: e, onChange: t }) => {
1480
+ `, We = ({ value: e, onChange: t }) => {
1458
1481
  let n = L((e) => e.volume), r = L((e) => e.muted), i = p(null), a = p(null), { handlers: o } = J({
1459
1482
  ref: a,
1460
1483
  onChange: t
@@ -1474,7 +1497,7 @@ ${X}
1474
1497
  id: "volume-slider",
1475
1498
  text: /* @__PURE__ */ C("div", {
1476
1499
  ref: i,
1477
- css: He,
1500
+ css: Ue,
1478
1501
  ...o,
1479
1502
  children: /* @__PURE__ */ C("div", {
1480
1503
  ref: a,
@@ -1493,7 +1516,7 @@ ${X}
1493
1516
  children: [l, "%"]
1494
1517
  })
1495
1518
  });
1496
- }, We = h`
1519
+ }, Ge = h`
1497
1520
  position: relative;
1498
1521
  display: flex;
1499
1522
  align-items: center;
@@ -1556,10 +1579,10 @@ ${X}
1556
1579
  }
1557
1580
  }
1558
1581
 
1559
- `, Ge = ({ ref: e }) => {
1582
+ `, Ke = ({ ref: e }) => {
1560
1583
  let t = L(), n = L((e) => e.volume), r = L((e) => e.muted), i = f(() => G(n), [n]);
1561
1584
  return /* @__PURE__ */ w("div", {
1562
- css: We,
1585
+ css: Ge,
1563
1586
  children: [/* @__PURE__ */ C(q, {
1564
1587
  id: "sound",
1565
1588
  text: /* @__PURE__ */ C("button", {
@@ -1579,7 +1602,7 @@ ${X}
1579
1602
  className: "volume-slider-container",
1580
1603
  children: /* @__PURE__ */ C("div", {
1581
1604
  className: "volume-slider-background",
1582
- children: /* @__PURE__ */ C(Ue, {
1605
+ children: /* @__PURE__ */ C(We, {
1583
1606
  value: r ? 0 : i,
1584
1607
  onChange: (e) => {
1585
1608
  t.setVolume(W(e));
@@ -1588,7 +1611,7 @@ ${X}
1588
1611
  })
1589
1612
  })]
1590
1613
  });
1591
- }, $ = .05, Ke = 5, qe = h`
1614
+ }, $ = .05, qe = 5, Je = h`
1592
1615
  display: flex;
1593
1616
  flex-direction: column;
1594
1617
  gap: calc(0.4 * var(--mp-unit));
@@ -1599,7 +1622,7 @@ ${X}
1599
1622
  opacity: 0.72;
1600
1623
  font-size: 0.9em;
1601
1624
  }
1602
- `, Je = h`
1625
+ `, Ye = h`
1603
1626
  position: absolute;
1604
1627
  bottom: 0;
1605
1628
  width: 100%;
@@ -1664,7 +1687,7 @@ ${X}
1664
1687
  * drawn in the hover colour is invisible exactly when it is being looked for.
1665
1688
  */
1666
1689
  button[aria-pressed='true'] svg {
1667
- stroke: ${Ve.accent};
1690
+ stroke: ${He.accent};
1668
1691
  }
1669
1692
 
1670
1693
  .play, .sound, .time, .errors, .subtitles, .settings, .picture-in-picture, .full-screen {
@@ -1699,7 +1722,7 @@ ${X}
1699
1722
  cursor: pointer;
1700
1723
 
1701
1724
  :hover {
1702
- background-color: ${Ve.hover};
1725
+ background-color: ${He.hover};
1703
1726
  }
1704
1727
 
1705
1728
  /* the hit area grows, the icon does not, keyed on the pointer because a narrow desktop
@@ -1737,7 +1760,7 @@ ${X}
1737
1760
  }
1738
1761
  }
1739
1762
  }
1740
- `, Ye = () => {
1763
+ `, Xe = () => {
1741
1764
  let e = L(), t = L((e) => e.paused), n = L((e) => e.currentTime), r = L((e) => e.duration), i = L((e) => e.fullscreen), a = L((e) => e.hideUI), o = L((e) => e.togglePictureInPicture), s = L((e) => e.pictureInPictureMode), c = L((e) => e.burnedInSubtitles), l = L((e) => e.subtitleTracks), [f, p] = m(null), h = s === "burn-in", g = l.length > 0, _ = r > 0 && n === r, v = u(({ direction: t, stepSize: n }) => {
1742
1765
  let r = G(e.volume), i = t === "up" ? n : -n, a = Math.max(0, Math.min(1, r + i)), o = e.muted;
1743
1766
  e.setVolume(W(a)), o && !e.muted && e.toggleMuted();
@@ -1761,8 +1784,8 @@ ${X}
1761
1784
  });
1762
1785
  else if (t.key === "ArrowRight") {
1763
1786
  if (!e.duration) return;
1764
- e.seek(Math.min(e.currentTime + Ke, e.duration));
1765
- } else t.key === "ArrowLeft" ? e.seek(Math.max(e.currentTime - Ke, 0)) : r = !1;
1787
+ e.seek(Math.min(e.currentTime + qe, e.duration));
1788
+ } else t.key === "ArrowLeft" ? e.seek(Math.max(e.currentTime - qe, 0)) : r = !1;
1766
1789
  r && t.preventDefault();
1767
1790
  };
1768
1791
  return window.addEventListener("keydown", t), () => window.removeEventListener("keydown", t);
@@ -1779,7 +1802,7 @@ ${X}
1779
1802
  return d(() => {
1780
1803
  if (f) return f.addEventListener("wheel", y, { passive: !1 }), () => f.removeEventListener("wheel", y);
1781
1804
  }, [f, y]), /* @__PURE__ */ w("div", {
1782
- css: Je,
1805
+ css: Ye,
1783
1806
  style: { ...a ? {
1784
1807
  opacity: "0",
1785
1808
  pointerEvents: "none"
@@ -1801,7 +1824,7 @@ ${X}
1801
1824
  }),
1802
1825
  toolTipText: /* @__PURE__ */ C("span", { children: _ ? "Replay (k)" : t ? "Play (k)" : "Pause (k)" })
1803
1826
  }),
1804
- /* @__PURE__ */ C(Ge, { ref: p }),
1827
+ /* @__PURE__ */ C(Ke, { ref: p }),
1805
1828
  /* @__PURE__ */ C("div", {
1806
1829
  className: "time",
1807
1830
  children: Se(n, r)
@@ -1810,9 +1833,9 @@ ${X}
1810
1833
  }), /* @__PURE__ */ w("div", {
1811
1834
  className: "right",
1812
1835
  children: [
1813
- /* @__PURE__ */ C(Pe, {}),
1814
- /* @__PURE__ */ C(Be, {}),
1815
- /* @__PURE__ */ C(Re, {}),
1836
+ /* @__PURE__ */ C(Fe, {}),
1837
+ /* @__PURE__ */ C(Ve, {}),
1838
+ /* @__PURE__ */ C(ze, {}),
1816
1839
  o ? /* @__PURE__ */ C(q, {
1817
1840
  id: "picture-in-picture",
1818
1841
  tooltipPlace: "top-end",
@@ -1829,7 +1852,7 @@ ${X}
1829
1852
  })
1830
1853
  }),
1831
1854
  toolTipText: /* @__PURE__ */ w("span", {
1832
- css: qe,
1855
+ css: Je,
1833
1856
  children: [/* @__PURE__ */ C("span", {
1834
1857
  className: "lead",
1835
1858
  children: h ? g ? c ? "Subtitles are in the picture" : "Put the subtitles in the picture" : "This file has no subtitles" : "Picture in picture"
@@ -1856,7 +1879,7 @@ ${X}
1856
1879
  })]
1857
1880
  })]
1858
1881
  });
1859
- }, Xe = 9e3, Ze = h`
1882
+ }, Ze = 9e3, Qe = h`
1860
1883
  position: absolute;
1861
1884
  inset: 0;
1862
1885
  z-index: 2;
@@ -1910,7 +1933,7 @@ ${X}
1910
1933
  line-height: 1;
1911
1934
  color: #6EA8FE;
1912
1935
  }
1913
- `, Qe = () => {
1936
+ `, $e = () => {
1914
1937
  let e = L((e) => e.burnedInSubtitles), t = L((e) => e.pictureInPictureMode), [n, r] = m(!1);
1915
1938
  return d(() => {
1916
1939
  if (t !== "burn-in" || !e) {
@@ -1918,10 +1941,10 @@ ${X}
1918
1941
  return;
1919
1942
  }
1920
1943
  r(!0);
1921
- let n = setTimeout(() => r(!1), Xe);
1944
+ let n = setTimeout(() => r(!1), Ze);
1922
1945
  return () => clearTimeout(n);
1923
1946
  }, [t, e]), t !== "burn-in" || !e ? null : /* @__PURE__ */ C("div", {
1924
- css: Ze,
1947
+ css: Qe,
1925
1948
  className: n ? "show" : "",
1926
1949
  role: "status",
1927
1950
  children: /* @__PURE__ */ w("div", {
@@ -1942,7 +1965,7 @@ ${X}
1942
1965
  })]
1943
1966
  })
1944
1967
  });
1945
- }, $e = 3e3, et = h`
1968
+ }, et = 3e3, tt = h`
1946
1969
  position: absolute;
1947
1970
  inset: 0;
1948
1971
  z-index: 2;
@@ -1953,7 +1976,7 @@ ${X}
1953
1976
  hoverable while faded out, popping a tooltip over nothing. It cascades where pointer-events does
1954
1977
  not, and transitioning it holds the item on screen for the length of the fade. */
1955
1978
  transition: opacity 0.1s cubic-bezier(.4,0,1,1), visibility 0.1s;
1956
- `, tt = h`
1979
+ `, nt = h`
1957
1980
  position: relative;
1958
1981
  background-color: black;
1959
1982
  display: flex;
@@ -1985,20 +2008,20 @@ ${X}
1985
2008
  &.hide {
1986
2009
  cursor: none;
1987
2010
  }
1988
- `, nt = (e, t = "") => s.toArray(e).flatMap((e, n) => {
2011
+ `, rt = (e, t = "") => s.toArray(e).flatMap((e, n) => {
1989
2012
  let r = `${t}${l(e) && e.key != null ? e.key : n}`;
1990
- return l(e) && e.type === c ? nt(e.props.children, `${r}/`) : [{
2013
+ return l(e) && e.type === c ? rt(e.props.children, `${r}/`) : [{
1991
2014
  key: r,
1992
2015
  item: e
1993
2016
  }];
1994
- }), rt = ({ ref: e, onVideoRef: t, onCanvasRef: n, overlay: r, controls: i, children: a }) => {
2017
+ }), it = ({ ref: e, onVideoRef: t, onCanvasRef: n, overlay: r, controls: i, children: a }) => {
1995
2018
  let o = L(), s = L((e) => e.hideUI), c = L((e) => e.setHideUI), l = p(void 0), u = p("mouse");
1996
2019
  d(() => () => clearTimeout(l.current), []);
1997
2020
  let f = () => {
1998
- c(!1), clearTimeout(l.current), l.current = setTimeout(() => c(!0), $e);
2021
+ c(!1), clearTimeout(l.current), l.current = setTimeout(() => c(!0), et);
1999
2022
  };
2000
2023
  return /* @__PURE__ */ w("div", {
2001
- css: tt,
2024
+ css: nt,
2002
2025
  ref: e,
2003
2026
  onPointerMove: (e) => {
2004
2027
  u.current = e.pointerType, e.pointerType === "mouse" && f();
@@ -2015,8 +2038,8 @@ ${X}
2015
2038
  className: s ? "hide" : "",
2016
2039
  children: [
2017
2040
  /* @__PURE__ */ C(xe, { onCanvasRef: n }),
2018
- nt(r).map(({ key: e, item: t }) => /* @__PURE__ */ C("div", {
2019
- css: et,
2041
+ rt(r).map(({ key: e, item: t }) => /* @__PURE__ */ C("div", {
2042
+ css: tt,
2020
2043
  style: { ...s ? {
2021
2044
  opacity: "0",
2022
2045
  visibility: "hidden",
@@ -2024,8 +2047,8 @@ ${X}
2024
2047
  } : {} },
2025
2048
  children: t
2026
2049
  }, e)),
2027
- i === !1 ? null : /* @__PURE__ */ C(Ye, {}),
2028
- /* @__PURE__ */ C(Qe, {}),
2050
+ i === !1 ? null : /* @__PURE__ */ C(Xe, {}),
2051
+ /* @__PURE__ */ C($e, {}),
2029
2052
  /* @__PURE__ */ w("div", {
2030
2053
  className: "video",
2031
2054
  onClick: () => {
@@ -2042,7 +2065,7 @@ ${X}
2042
2065
  })
2043
2066
  ]
2044
2067
  });
2045
- }, it = ({ options: e, children: t }) => {
2068
+ }, at = ({ options: e, children: t }) => {
2046
2069
  let n = "media" in e ? e : null, r = n ? null : e, { title: i } = e, { size: a, downloadedRanges: o, publicPath: s, libavWorkerUrl: c, read: l, thumbnailRead: u, thumbnailsEnabled: f } = r ?? {}, p = y(), h = v(), [g, _] = m(null), [b, x] = m(null), S = n?.media ?? g;
2047
2070
  d(() => (p?.(S), () => p?.(null)), [S, p]), pe(g, b, r);
2048
2071
  let w = z({
@@ -2103,7 +2126,7 @@ ${X}
2103
2126
  selectedAudioTrack: M.selectedId ?? void 0,
2104
2127
  selectAudioTrack: (e) => M.select(String(e))
2105
2128
  });
2106
- }, [k, M]), /* @__PURE__ */ C(rt, {
2129
+ }, [k, M]), /* @__PURE__ */ C(it, {
2107
2130
  ref: h,
2108
2131
  onVideoRef: n ? void 0 : _,
2109
2132
  onCanvasRef: x,
@@ -2111,7 +2134,7 @@ ${X}
2111
2134
  controls: e.controls,
2112
2135
  children: t
2113
2136
  });
2114
- }, at = h`
2137
+ }, ot = h`
2115
2138
  /**
2116
2139
  * The chrome's whole scale, in one place.
2117
2140
  *
@@ -2181,17 +2204,17 @@ ${X}
2181
2204
  height: 100%;
2182
2205
  width: 100%;
2183
2206
  overflow: hidden;
2184
- `, ot = (e) => /* @__PURE__ */ C("div", {
2185
- css: at,
2186
- children: /* @__PURE__ */ C(I.Provider, { children: /* @__PURE__ */ C(it, {
2207
+ `, st = (e) => /* @__PURE__ */ C("div", {
2208
+ css: ot,
2209
+ children: /* @__PURE__ */ C(I.Provider, { children: /* @__PURE__ */ C(at, {
2187
2210
  options: e,
2188
2211
  children: e.children
2189
2212
  }) })
2190
- }), st = (e) => !!e && typeof e == "object" && "at" in e, ct = (e) => !!e && typeof e == "object" && "selection" in e, lt = ({ blob: e, name: t }) => ({
2213
+ }), ct = (e) => !!e && typeof e == "object" && "at" in e, lt = (e) => !!e && typeof e == "object" && "selection" in e, ut = ({ blob: e, name: t }) => ({
2191
2214
  length: e.size,
2192
2215
  name: t ?? (e instanceof File ? e.name : void 0),
2193
2216
  read: (t, n) => e.slice(t, t + n).arrayBuffer()
2194
- }), ut = async (e, t) => {
2217
+ }), dt = async (e, t) => {
2195
2218
  let n = await fetch(e, {
2196
2219
  headers: { Range: "bytes=0-1" },
2197
2220
  credentials: t
@@ -2201,8 +2224,8 @@ ${X}
2201
2224
  if (!Number.isFinite(i) || i <= 0) throw Error("The source did not report its length");
2202
2225
  if (!r && n.status !== 206) throw Error("The source does not support range requests");
2203
2226
  return i;
2204
- }, dt = async ({ url: e, length: t, name: n, credentials: r }) => {
2205
- let i = t ?? await ut(e, r);
2227
+ }, ft = async ({ url: e, length: t, name: n, credentials: r }) => {
2228
+ let i = t ?? await dt(e, r);
2206
2229
  return {
2207
2230
  length: i,
2208
2231
  name: n,
@@ -2215,9 +2238,9 @@ ${X}
2215
2238
  return a.arrayBuffer();
2216
2239
  }
2217
2240
  };
2218
- }, ft = async (e) => {
2219
- if ("blob" in e) return lt(e);
2220
- if ("url" in e) return dt(e);
2241
+ }, pt = async (e) => {
2242
+ if ("blob" in e) return ut(e);
2243
+ if ("url" in e) return ft(e);
2221
2244
  if ("read" in e) return {
2222
2245
  length: e.length,
2223
2246
  name: e.name,
@@ -2226,4 +2249,4 @@ ${X}
2226
2249
  throw Error(`Unknown source type: ${e}`);
2227
2250
  };
2228
2251
  //#endregion
2229
- export { ot as MediaPlayer, ot as default, I as Player, e as SUBTITLES_OFF, a as createPictureInPicture, o as createSubtitleRenderer, r as createThumbnailGenerator, ft as inputToRemuxerInput, ct as isDelegatedTracks, st as isExternalThumbnails, F as sourceFeature, i as startPlayback, B as usePictureInPicture, L as usePlayer, z as useSeekThumbnails };
2252
+ export { st as MediaPlayer, st as default, I as Player, e as SUBTITLES_OFF, a as createPictureInPicture, o as createSubtitleRenderer, r as createThumbnailGenerator, pt as inputToRemuxerInput, lt as isDelegatedTracks, ct as isExternalThumbnails, F as sourceFeature, i as startPlayback, B as usePictureInPicture, L as usePlayer, z as useSeekThumbnails };
@@ -1,4 +1,6 @@
1
1
  import type { PlaybackErrorEntry } from '../source-feature';
2
+ /** How many failures there have been, which is not how many rows: a repeat folds into its row. */
3
+ export declare const countErrors: (errors: PlaybackErrorEntry[]) => number;
2
4
  /** What lands on the clipboard: the same thing the panel shows, in the order it happened. */
3
5
  export declare const formatErrors: (errors: PlaybackErrorEntry[]) => string;
4
6
  /**
@@ -26,9 +26,20 @@ export type TrackChoice = {
26
26
  * `cause` may be a `MediaError` that reads differently once the element has moved on.
27
27
  */
28
28
  export type PlaybackErrorEntry = {
29
- /** Wall clock, so the report can be read next to a console log or a torrent's timeline. */
29
+ /** Wall clock of the FIRST of them, so the report can be read next to a console log. */
30
30
  at: number;
31
- /** Seconds into the media, which is usually the first question asked of a playback failure. */
31
+ /** Wall clock of the most recent one. Equal to `at` until a repeat has folded into this row. */
32
+ lastAt: number;
33
+ /**
34
+ * How many times in a row this exact failure happened.
35
+ *
36
+ * A source that stays broken reports the same sentence every few seconds for as long as it stays
37
+ * broken, so consecutive identical failures fold into one row rather than filling the panel with a
38
+ * wall of one message. This is also what keeps the list from growing without end in that case,
39
+ * since there is no ceiling on how many failures are kept.
40
+ */
41
+ count: number;
42
+ /** Seconds into the media, at the first of them, which is usually the first question asked. */
32
43
  atMediaTime?: number;
33
44
  message: string;
34
45
  /** The `cause` chain, already unwound, one line per level. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
4
4
  "description": "A video player for containers and codecs the browser cannot play natively, remuxed on the fly",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -77,6 +77,12 @@ ${popoverStyle}
77
77
  .when {
78
78
  color: #bbb;
79
79
  ${fonts.bSmall.regular}
80
+
81
+ /* the one part of the line worth scanning for, so it is the one part at full brightness */
82
+ .count {
83
+ margin-left: 6px;
84
+ color: #fff;
85
+ }
80
86
  }
81
87
 
82
88
  .what {
@@ -101,12 +107,21 @@ const clock = (at: number) => new Date(at).toLocaleTimeString()
101
107
  const mediaTime = (seconds: number | undefined) =>
102
108
  seconds === undefined || !Number.isFinite(seconds) ? undefined : formatTime(Math.max(0, seconds))
103
109
 
110
+ /** How many failures there have been, which is not how many rows: a repeat folds into its row. */
111
+ export const countErrors = (errors: PlaybackErrorEntry[]) =>
112
+ errors.reduce((total, entry) => total + entry.count, 0)
113
+
104
114
  /** What lands on the clipboard: the same thing the panel shows, in the order it happened. */
105
115
  export const formatErrors = (errors: PlaybackErrorEntry[]) =>
106
116
  errors
107
117
  .map((entry, index) => {
108
118
  const at = mediaTime(entry.atMediaTime)
109
- const head = `${index + 1}. ${new Date(entry.at).toISOString()}${at ? ` (at ${at})` : ''}${entry.recovered ? ' [recovered]' : ''}`
119
+ // A repeat carries the span it covers, so "40 times over two minutes" and "40 times in one
120
+ // second" are distinguishable in a report. Both happen, and they are different faults.
121
+ const when = entry.count > 1
122
+ ? `${new Date(entry.at).toISOString()} to ${new Date(entry.lastAt).toISOString()} ×${entry.count}`
123
+ : new Date(entry.at).toISOString()
124
+ const head = `${index + 1}. ${when}${at ? ` (at ${at})` : ''}${entry.recovered ? ' [recovered]' : ''}`
110
125
  return [head, entry.message, entry.detail].filter(Boolean).join('\n')
111
126
  })
112
127
  .join('\n\n')
@@ -142,7 +157,8 @@ export const ErrorsAction = () => {
142
157
  )
143
158
  }
144
159
 
145
- const label = `Playback errors (${errors.length})`
160
+ // counted in failures rather than in rows, because a row that says ×40 is forty of them
161
+ const label = `Playback errors (${countErrors(errors)})`
146
162
 
147
163
  return (
148
164
  <div css={style} ref={containerRef}>
@@ -177,8 +193,11 @@ export const ErrorsAction = () => {
177
193
  <div className='entry no-hover' key={`${entry.at}-${index}`}>
178
194
  <span className='when'>
179
195
  {clock(entry.at)}
196
+ {/* the span, so a row that is still growing is distinguishable from one that stopped */}
197
+ {entry.count > 1 ? ` to ${clock(entry.lastAt)}` : ''}
180
198
  {mediaTime(entry.atMediaTime) ? ` at ${mediaTime(entry.atMediaTime)}` : ''}
181
199
  {entry.recovered ? ' recovered' : ''}
200
+ {entry.count > 1 ? <span className='count'>{`×${entry.count}`}</span> : null}
182
201
  </span>
183
202
  <span className='what'>{entry.message}</span>
184
203
  {entry.detail ? <span className='cause'>{entry.detail}</span> : null}
@@ -158,14 +158,33 @@ export const usePlayback = (
158
158
  * `playbackError` null and would otherwise erase the only evidence that anything went wrong.
159
159
  */
160
160
  const record = (error: unknown, recovered: boolean) => {
161
+ const at = Date.now()
162
+ const message = messageOf(error)
163
+ const detail = causeChain(error)
164
+ const errors = player.playbackErrors
165
+ const last = errors[errors.length - 1]
166
+
167
+ // A source that stays broken repeats one sentence for as long as it stays broken, so a
168
+ // consecutive repeat folds into the row it repeats rather than adding another. Only
169
+ // CONSECUTIVE ones: an identical failure either side of a different one is a second episode,
170
+ // and collapsing the two would lose the order that makes a report readable.
171
+ if (last && last.message === message && last.detail === detail && last.recovered === recovered) {
172
+ const folded: PlaybackErrorEntry = { ...last, count: last.count + 1, lastAt: at }
173
+ player.setSourceState({ playbackErrors: [...errors.slice(0, -1), folded] })
174
+ return
175
+ }
176
+
161
177
  const entry: PlaybackErrorEntry = {
162
- at: Date.now(),
178
+ at,
179
+ lastAt: at,
180
+ count: 1,
181
+ // where it STARTED, kept as the row grows, because that is the position worth reporting
163
182
  atMediaTime: Number.isFinite(video.currentTime) ? video.currentTime : undefined,
164
- message: messageOf(error),
165
- detail: causeChain(error),
183
+ message,
184
+ detail,
166
185
  recovered,
167
186
  }
168
- player.setSourceState({ playbackErrors: [...player.playbackErrors, entry] })
187
+ player.setSourceState({ playbackErrors: [...errors, entry] })
169
188
  }
170
189
  const fail = (error: unknown) => {
171
190
  if (cancelled) return
@@ -30,9 +30,20 @@ export type TrackChoice = {
30
30
  * `cause` may be a `MediaError` that reads differently once the element has moved on.
31
31
  */
32
32
  export type PlaybackErrorEntry = {
33
- /** Wall clock, so the report can be read next to a console log or a torrent's timeline. */
33
+ /** Wall clock of the FIRST of them, so the report can be read next to a console log. */
34
34
  at: number
35
- /** Seconds into the media, which is usually the first question asked of a playback failure. */
35
+ /** Wall clock of the most recent one. Equal to `at` until a repeat has folded into this row. */
36
+ lastAt: number
37
+ /**
38
+ * How many times in a row this exact failure happened.
39
+ *
40
+ * A source that stays broken reports the same sentence every few seconds for as long as it stays
41
+ * broken, so consecutive identical failures fold into one row rather than filling the panel with a
42
+ * wall of one message. This is also what keeps the list from growing without end in that case,
43
+ * since there is no ceiling on how many failures are kept.
44
+ */
45
+ count: number
46
+ /** Seconds into the media, at the first of them, which is usually the first question asked. */
36
47
  atMediaTime?: number
37
48
  message: string
38
49
  /** The `cause` chain, already unwound, one line per level. */