@nikelroot/video-player 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -11,7 +11,7 @@ npm install @nikelroot/video-player
11
11
  Peer dependencies:
12
12
 
13
13
  ```bash
14
- npm install react react-dom antd @ant-design/icons styled-components
14
+ npm install react react-dom styled-components
15
15
  ```
16
16
 
17
17
  ## Usage
package/dist/index.cjs CHANGED
@@ -39,13 +39,9 @@ module.exports = __toCommonJS(index_exports);
39
39
  // src/VideoPlayer.tsx
40
40
  var import_react2 = require("react");
41
41
 
42
- // src/controls.tsx
43
- var import_antd = require("antd");
44
- var import_icons = require("@ant-design/icons");
45
-
46
42
  // src/styles.ts
47
- var import_styled_components = __toESM(require("styled-components"), 1);
48
- var VideoPlayerControlStyles = import_styled_components.default.div`
43
+ var import_styled_components = require("styled-components");
44
+ var VideoPlayerControlStyles = import_styled_components.styled.div`
49
45
  position: absolute;
50
46
  left: 0;
51
47
  right: 0;
@@ -62,13 +58,61 @@ var VideoPlayerControlStyles = import_styled_components.default.div`
62
58
  flex: 1 auto;
63
59
  }
64
60
 
61
+ .video-player-control-row {
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: space-between;
65
+ gap: 8px;
66
+ min-height: 30px;
67
+ }
68
+
69
+ .video-player-control-button {
70
+ display: inline-flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ width: 28px;
74
+ height: 28px;
75
+ flex: 0 0 28px;
76
+ border: 1px solid rgba(255, 255, 255, 0.24);
77
+ border-radius: 4px;
78
+ background: rgba(255, 255, 255, 0.12);
79
+ color: #fff;
80
+ cursor: pointer;
81
+ padding: 0;
82
+ }
83
+
84
+ .video-player-control-button:hover,
85
+ .video-player-control-button:focus-visible {
86
+ background: rgba(255, 255, 255, 0.22);
87
+ outline: none;
88
+ }
89
+
90
+ .video-player-icon {
91
+ width: 17px;
92
+ height: 17px;
93
+ fill: currentColor;
94
+ stroke: currentColor;
95
+ stroke-width: 1.8;
96
+ stroke-linecap: round;
97
+ stroke-linejoin: round;
98
+ }
99
+
65
100
  .video-player-control-title {
66
101
  color: #fff;
67
102
  max-width: 40%;
103
+ overflow: hidden;
104
+ text-overflow: ellipsis;
105
+ white-space: nowrap;
106
+ }
107
+
108
+ .video-player-time {
109
+ color: #fff;
110
+ white-space: nowrap;
111
+ font-size: 14px;
68
112
  }
69
113
 
70
114
  ${(props) => !props.$showControl && import_styled_components.css`
71
- .ant-flex {
115
+ .video-player-control-row {
72
116
  > * {
73
117
  opacity: 0;
74
118
  display: none;
@@ -85,7 +129,7 @@ var VideoPlayerControlStyles = import_styled_components.default.div`
85
129
  padding: 0;
86
130
  `};
87
131
  `;
88
- var VideoPlayerStyles = import_styled_components.default.div`
132
+ var VideoPlayerStyles = import_styled_components.styled.div`
89
133
  position: relative;
90
134
  aspect-ratio: 16 / 9;
91
135
  max-height: 100%;
@@ -240,42 +284,25 @@ var VideoPlayerStyles = import_styled_components.default.div`
240
284
  }
241
285
  `};
242
286
  `;
243
- var SeekSliderStyled = import_styled_components.default.div`
287
+ var SeekSliderStyled = import_styled_components.styled.div`
244
288
  position: absolute;
245
289
  left: 0;
246
290
  right: 0;
247
291
  top: -12px;
248
292
 
249
- .ant-slider-rail {
293
+ .seek-range {
294
+ display: block;
295
+ width: 100%;
250
296
  height: 20px;
251
- top: auto;
252
- bottom: 0;
253
- background: transparent !important;
254
- }
255
-
256
- .ant-slider {
257
- &:hover {
258
- .ant-slider-rail {
259
- background: transparent !important;
260
- }
261
- }
262
- }
263
-
264
- .ant-slider-track {
265
- top: 6px;
266
- }
267
-
268
- .ant-slider-handle {
269
- top: 3px;
270
- }
271
-
272
- .ant-slider-step {
273
- height: 8px;
274
- background: rgba(255, 255, 255, 0.1);
297
+ margin: 0;
298
+ accent-color: #fff;
299
+ background: transparent;
300
+ cursor: pointer;
275
301
  }
276
302
 
277
- .ant-slider {
278
- margin: 0;
303
+ .seek-range:disabled {
304
+ cursor: default;
305
+ opacity: 0.65;
279
306
  }
280
307
  `;
281
308
 
@@ -384,36 +411,83 @@ var createThrottledNumberFn = (fn, waitMs) => {
384
411
  // src/controls.tsx
385
412
  var import_jsx_runtime = require("react/jsx-runtime");
386
413
  var SeekSlider = ({ playAction, duration, currentTime, seekSetTime }) => {
387
- const formatter = (value) => {
388
- return formatTime(Number(value != null ? value : 0));
389
- };
390
414
  const clickHandler = (e) => {
391
415
  e.preventDefault();
392
416
  e.stopPropagation();
393
417
  };
394
418
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
395
419
  const safeCurrentTime = Math.max(0, Math.min(Number.isFinite(currentTime) ? currentTime : 0, safeDuration || Infinity));
420
+ const completeChange = () => {
421
+ void playAction();
422
+ };
396
423
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SeekSliderStyled, { onClick: clickHandler, className: "seek-slider", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
397
- import_antd.Slider,
424
+ "input",
398
425
  {
399
- onChange: seekSetTime,
400
- onChangeComplete: () => {
401
- void playAction();
426
+ "aria-label": "Seek",
427
+ className: "seek-range",
428
+ disabled: safeDuration <= 0,
429
+ max: safeDuration,
430
+ min: 0,
431
+ onChange: (e) => seekSetTime(Number(e.currentTarget.value)),
432
+ onKeyUp: (e) => {
433
+ if (e.key === "Enter" || e.key === " ") completeChange();
402
434
  },
435
+ onPointerUp: completeChange,
436
+ step: "any",
437
+ type: "range",
403
438
  value: safeCurrentTime,
404
- tooltip: { formatter },
405
- max: safeDuration,
406
- keyboard: false
439
+ title: formatTime(safeCurrentTime)
407
440
  }
408
441
  ) });
409
442
  };
410
443
  var TimeBlock = ({ duration, currentTime }) => {
411
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Typography.Text, { style: { color: "#fff", whiteSpace: "nowrap" }, children: [
444
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "video-player-time", children: [
412
445
  formatTime(currentTime),
413
446
  " / ",
414
447
  formatTime(duration)
415
448
  ] });
416
449
  };
450
+ var Icon = ({ name }) => {
451
+ const paths = {
452
+ back: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
453
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M11 7l-6 5 6 5V7z" }),
454
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19 7l-6 5 6 5V7z" })
455
+ ] }),
456
+ forward: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
457
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 7l6 5-6 5V7z" }),
458
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M13 7l6 5-6 5V7z" })
459
+ ] }),
460
+ fullscreen: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
461
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5 10V5h5" }),
462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M14 5h5v5" }),
463
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M19 14v5h-5" }),
464
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M10 19H5v-5" })
465
+ ] }),
466
+ mute: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
467
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M4 10v4h4l5 4V6l-5 4H4z" }),
468
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M17 9l4 6" }),
469
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M21 9l-4 6" })
470
+ ] }),
471
+ pause: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
472
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 6h3v12H8z" }),
473
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M13 6h3v12h-3z" })
474
+ ] }),
475
+ play: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M8 5v14l11-7L8 5z" }),
476
+ sound: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
477
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M4 10v4h4l5 4V6l-5 4H4z" }),
478
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M16 9a4 4 0 010 6" }),
479
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18.5 6.5a8 8 0 010 11" })
480
+ ] })
481
+ };
482
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { "aria-hidden": "true", className: "video-player-icon", focusable: "false", viewBox: "0 0 24 24", children: paths[name] });
483
+ };
484
+ var ControlButton = ({
485
+ icon,
486
+ label,
487
+ onClick
488
+ }) => {
489
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { "aria-label": label, className: "video-player-control-button", onClick: () => void onClick(), title: label, type: "button", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: icon }) });
490
+ };
417
491
  var stopControlClick = (e) => {
418
492
  e.preventDefault();
419
493
  e.stopPropagation();
@@ -423,7 +497,6 @@ var VideoPlayerControls = (props) => {
423
497
  variant,
424
498
  fullScreenAction,
425
499
  showControl,
426
- videoRef,
427
500
  playAction,
428
501
  pauseAction,
429
502
  seekSetTime,
@@ -438,30 +511,29 @@ var VideoPlayerControls = (props) => {
438
511
  } = props;
439
512
  const [, isMobile] = useWindowWidth();
440
513
  if (variant === "none") return null;
441
- if (!videoRef.current) return null;
442
514
  if (variant === "tiny") {
443
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VideoPlayerControlStyles, { $tiny: true, $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Flex, { gap: "small", align: "center", justify: "space-between", children: [
515
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VideoPlayerControlStyles, { $tiny: true, $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "video-player-control-row", children: [
444
516
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "full" }),
445
517
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SeekSlider, { playAction, duration, currentTime, seekSetTime })
446
518
  ] }) });
447
519
  }
448
520
  if (variant === "fullscreen-only") {
449
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VideoPlayerControlStyles, { $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Flex, { gap: "small", align: "center", justify: "space-between", children: [
521
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VideoPlayerControlStyles, { $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "video-player-control-row", children: [
450
522
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "full" }),
451
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => onMutedChange(!muted), icon: muted ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.MutedFilled, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.SoundFilled, {}) }),
452
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: fullScreenAction, icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.FullscreenOutlined, {}) })
523
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlButton, { icon: muted ? "mute" : "sound", label: muted ? "Unmute" : "Mute", onClick: () => onMutedChange(!muted) }),
524
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlButton, { icon: "fullscreen", label: "Fullscreen", onClick: fullScreenAction })
453
525
  ] }) });
454
526
  }
455
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VideoPlayerControlStyles, { $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_antd.Flex, { gap: "small", align: "center", justify: "space-between", children: [
456
- playing ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => pauseAction(), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.PauseOutlined, {}) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => playAction(), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.PlaySquareFilled, {}) }),
457
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => seekPrevAction(), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.BackwardFilled, {}) }),
458
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => seekNextAction(), icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.FastForwardFilled, {}) }),
459
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Typography.Text, { ellipsis: true, className: "video-player-control-title", children: title }),
527
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VideoPlayerControlStyles, { $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "video-player-control-row", children: [
528
+ playing ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlButton, { icon: "pause", label: "Pause", onClick: pauseAction }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlButton, { icon: "play", label: "Play", onClick: playAction }),
529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlButton, { icon: "back", label: "Back 15 seconds", onClick: () => seekPrevAction() }),
530
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlButton, { icon: "forward", label: "Forward 15 seconds", onClick: () => seekNextAction() }),
531
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "video-player-control-title", title: typeof title === "string" ? title : void 0, children: title }),
460
532
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "full" }),
461
533
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SeekSlider, { playAction, duration, currentTime, seekSetTime }),
462
534
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TimeBlock, { duration, currentTime }),
463
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: () => onMutedChange(!muted), icon: muted ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.MutedFilled, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.SoundFilled, {}) }),
464
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_antd.Button, { size: "small", onClick: fullScreenAction, icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.FullscreenOutlined, {}) })
535
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlButton, { icon: muted ? "mute" : "sound", label: muted ? "Unmute" : "Mute", onClick: () => onMutedChange(!muted) }),
536
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlButton, { icon: "fullscreen", label: "Fullscreen", onClick: fullScreenAction })
465
537
  ] }) });
466
538
  };
467
539
 
@@ -660,6 +732,14 @@ var clampTime = (time, duration) => {
660
732
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
661
733
  return safeDuration > 0 ? Math.max(0, Math.min(safeTime, safeDuration)) : Math.max(0, safeTime);
662
734
  };
735
+ var isLikelyHlsSource = (src) => {
736
+ try {
737
+ const url = new URL(src, "http://video-player.local");
738
+ return /\.m3u8$/i.test(url.pathname) || /\.m3u8($|[?#])/i.test(src);
739
+ } catch {
740
+ return /\.m3u8($|[?#])/i.test(src);
741
+ }
742
+ };
663
743
  var VideoPlayerBase = (props, ref) => {
664
744
  const {
665
745
  controlsVariant = "none",
@@ -943,10 +1023,16 @@ var VideoPlayerBase = (props, ref) => {
943
1023
  );
944
1024
  }, [hlsConfig, hlsCredentials, live, liveHlsConfig, vodHlsConfig]);
945
1025
  const loadVideo = (0, import_react2.useCallback)(
946
- async (startAt = 0) => {
1026
+ async (startAt = initialTime) => {
947
1027
  const startVideoRef = videoRef.current;
948
1028
  if (!startVideoRef) return;
949
- const HlsCtor = await loadHlsCtor();
1029
+ let HlsCtor;
1030
+ try {
1031
+ HlsCtor = await loadHlsCtor();
1032
+ } catch (error) {
1033
+ reportError(error, { type: "hls-loader", src: videoSrc }, messages.streamLoadFailed);
1034
+ return;
1035
+ }
950
1036
  if (!videoRef.current || videoRef.current !== startVideoRef) return;
951
1037
  const video = videoRef.current;
952
1038
  if (!video) return;
@@ -1040,7 +1126,6 @@ var VideoPlayerBase = (props, ref) => {
1040
1126
  }
1041
1127
  void loadVideo(resumeTime);
1042
1128
  });
1043
- h.attachMedia(video);
1044
1129
  h.on(HlsCtor.Events.MEDIA_ATTACHED, () => {
1045
1130
  h.loadSource(videoSrc);
1046
1131
  });
@@ -1056,11 +1141,13 @@ var VideoPlayerBase = (props, ref) => {
1056
1141
  }
1057
1142
  if (autoPlay) void playAction();
1058
1143
  });
1144
+ h.attachMedia(video);
1059
1145
  },
1060
1146
  [
1061
1147
  autoPlay,
1062
1148
  clearErrorState,
1063
1149
  clearTimers,
1150
+ initialTime,
1064
1151
  live,
1065
1152
  messages.streamDecodeFailed,
1066
1153
  messages.streamLoadFailed,
@@ -1082,18 +1169,24 @@ var VideoPlayerBase = (props, ref) => {
1082
1169
  const loadVideoHls = (0, import_react2.useCallback)(async () => {
1083
1170
  const video = videoRef.current;
1084
1171
  const canNativeHls = !!(video == null ? void 0 : video.canPlayType) && video.canPlayType("application/vnd.apple.mpegurl") !== "";
1085
- const HlsCtor = await loadHlsCtor();
1172
+ let HlsCtor;
1173
+ try {
1174
+ HlsCtor = await loadHlsCtor();
1175
+ } catch (error) {
1176
+ reportError(error, { type: "hls-loader", src: videoSrc }, messages.streamLoadFailed);
1177
+ return;
1178
+ }
1086
1179
  if (!videoRef.current || videoRef.current !== video) return;
1087
1180
  if (HlsCtor.isSupported()) {
1088
- void loadVideo();
1181
+ void loadVideo(initialTime);
1089
1182
  return;
1090
1183
  }
1091
1184
  if (canNativeHls) {
1092
1185
  loadVideoNative();
1093
1186
  return;
1094
1187
  }
1095
- void loadVideo();
1096
- }, [loadVideo, loadVideoNative]);
1188
+ reportError(null, { type: "hls", src: videoSrc, reason: "HLS is not supported in this browser" }, messages.unsupported);
1189
+ }, [initialTime, loadVideo, loadVideoNative, messages.streamLoadFailed, messages.unsupported, reportError, videoSrc]);
1097
1190
  const destroy = (0, import_react2.useCallback)(() => {
1098
1191
  clearTimers();
1099
1192
  if (hlsRef.current) {
@@ -1224,8 +1317,9 @@ var VideoPlayerBase = (props, ref) => {
1224
1317
  hlsRef.current = null;
1225
1318
  }
1226
1319
  if (sourceType === "native" || type === "video") loadVideoNative();
1227
- else if (sourceType === "hls") void loadVideo();
1228
- else void loadVideoHls();
1320
+ else if (sourceType === "hls") void loadVideo(initialTime);
1321
+ else if (isLikelyHlsSource(videoSrc)) void loadVideoHls();
1322
+ else loadVideoNative();
1229
1323
  return () => {
1230
1324
  clearTimers();
1231
1325
  if (hlsRef.current) {
@@ -1239,6 +1333,7 @@ var VideoPlayerBase = (props, ref) => {
1239
1333
  loadVideo,
1240
1334
  loadVideoHls,
1241
1335
  loadVideoNative,
1336
+ initialTime,
1242
1337
  reloadKey,
1243
1338
  reloadToken,
1244
1339
  sourceType,
@@ -1305,6 +1400,7 @@ var VideoPlayerBase = (props, ref) => {
1305
1400
  "video",
1306
1401
  {
1307
1402
  ...videoProps,
1403
+ controls: false,
1308
1404
  ref: setVideoEl,
1309
1405
  crossOrigin,
1310
1406
  playsInline: true,
@@ -1334,7 +1430,6 @@ var VideoPlayerBase = (props, ref) => {
1334
1430
  variant: controlsVariant,
1335
1431
  fullScreenAction,
1336
1432
  showControl,
1337
- videoRef,
1338
1433
  playAction,
1339
1434
  pauseAction,
1340
1435
  seekSetTime,
package/dist/index.d.cts CHANGED
@@ -36,7 +36,7 @@ interface VideoPlayerProps extends Omit<React.VideoHTMLAttributes<HTMLVideoEleme
36
36
  videoSrc: string;
37
37
  sourceType?: 'auto' | 'hls' | 'native';
38
38
  type?: string;
39
- controlsVariant?: ControlsVariant;
39
+ controlsVariant: ControlsVariant;
40
40
  autoPlay?: boolean;
41
41
  preload?: '' | 'none' | 'auto' | 'metadata';
42
42
  loop?: boolean;
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ interface VideoPlayerProps extends Omit<React.VideoHTMLAttributes<HTMLVideoEleme
36
36
  videoSrc: string;
37
37
  sourceType?: 'auto' | 'hls' | 'native';
38
38
  type?: string;
39
- controlsVariant?: ControlsVariant;
39
+ controlsVariant: ControlsVariant;
40
40
  autoPlay?: boolean;
41
41
  preload?: '' | 'none' | 'auto' | 'metadata';
42
42
  loop?: boolean;
package/dist/index.js CHANGED
@@ -10,20 +10,8 @@ import {
10
10
  useState
11
11
  } from "react";
12
12
 
13
- // src/controls.tsx
14
- import { Flex, Button, Slider, Typography } from "antd";
15
- import {
16
- BackwardFilled,
17
- FastForwardFilled,
18
- FullscreenOutlined,
19
- MutedFilled,
20
- PauseOutlined,
21
- PlaySquareFilled,
22
- SoundFilled
23
- } from "@ant-design/icons";
24
-
25
13
  // src/styles.ts
26
- import styled, { css } from "styled-components";
14
+ import { css, styled } from "styled-components";
27
15
  var VideoPlayerControlStyles = styled.div`
28
16
  position: absolute;
29
17
  left: 0;
@@ -41,13 +29,61 @@ var VideoPlayerControlStyles = styled.div`
41
29
  flex: 1 auto;
42
30
  }
43
31
 
32
+ .video-player-control-row {
33
+ display: flex;
34
+ align-items: center;
35
+ justify-content: space-between;
36
+ gap: 8px;
37
+ min-height: 30px;
38
+ }
39
+
40
+ .video-player-control-button {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ width: 28px;
45
+ height: 28px;
46
+ flex: 0 0 28px;
47
+ border: 1px solid rgba(255, 255, 255, 0.24);
48
+ border-radius: 4px;
49
+ background: rgba(255, 255, 255, 0.12);
50
+ color: #fff;
51
+ cursor: pointer;
52
+ padding: 0;
53
+ }
54
+
55
+ .video-player-control-button:hover,
56
+ .video-player-control-button:focus-visible {
57
+ background: rgba(255, 255, 255, 0.22);
58
+ outline: none;
59
+ }
60
+
61
+ .video-player-icon {
62
+ width: 17px;
63
+ height: 17px;
64
+ fill: currentColor;
65
+ stroke: currentColor;
66
+ stroke-width: 1.8;
67
+ stroke-linecap: round;
68
+ stroke-linejoin: round;
69
+ }
70
+
44
71
  .video-player-control-title {
45
72
  color: #fff;
46
73
  max-width: 40%;
74
+ overflow: hidden;
75
+ text-overflow: ellipsis;
76
+ white-space: nowrap;
77
+ }
78
+
79
+ .video-player-time {
80
+ color: #fff;
81
+ white-space: nowrap;
82
+ font-size: 14px;
47
83
  }
48
84
 
49
85
  ${(props) => !props.$showControl && css`
50
- .ant-flex {
86
+ .video-player-control-row {
51
87
  > * {
52
88
  opacity: 0;
53
89
  display: none;
@@ -225,36 +261,19 @@ var SeekSliderStyled = styled.div`
225
261
  right: 0;
226
262
  top: -12px;
227
263
 
228
- .ant-slider-rail {
264
+ .seek-range {
265
+ display: block;
266
+ width: 100%;
229
267
  height: 20px;
230
- top: auto;
231
- bottom: 0;
232
- background: transparent !important;
233
- }
234
-
235
- .ant-slider {
236
- &:hover {
237
- .ant-slider-rail {
238
- background: transparent !important;
239
- }
240
- }
241
- }
242
-
243
- .ant-slider-track {
244
- top: 6px;
245
- }
246
-
247
- .ant-slider-handle {
248
- top: 3px;
249
- }
250
-
251
- .ant-slider-step {
252
- height: 8px;
253
- background: rgba(255, 255, 255, 0.1);
268
+ margin: 0;
269
+ accent-color: #fff;
270
+ background: transparent;
271
+ cursor: pointer;
254
272
  }
255
273
 
256
- .ant-slider {
257
- margin: 0;
274
+ .seek-range:disabled {
275
+ cursor: default;
276
+ opacity: 0.65;
258
277
  }
259
278
  `;
260
279
 
@@ -361,38 +380,85 @@ var createThrottledNumberFn = (fn, waitMs) => {
361
380
  };
362
381
 
363
382
  // src/controls.tsx
364
- import { jsx, jsxs } from "react/jsx-runtime";
383
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
365
384
  var SeekSlider = ({ playAction, duration, currentTime, seekSetTime }) => {
366
- const formatter = (value) => {
367
- return formatTime(Number(value != null ? value : 0));
368
- };
369
385
  const clickHandler = (e) => {
370
386
  e.preventDefault();
371
387
  e.stopPropagation();
372
388
  };
373
389
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
374
390
  const safeCurrentTime = Math.max(0, Math.min(Number.isFinite(currentTime) ? currentTime : 0, safeDuration || Infinity));
391
+ const completeChange = () => {
392
+ void playAction();
393
+ };
375
394
  return /* @__PURE__ */ jsx(SeekSliderStyled, { onClick: clickHandler, className: "seek-slider", children: /* @__PURE__ */ jsx(
376
- Slider,
395
+ "input",
377
396
  {
378
- onChange: seekSetTime,
379
- onChangeComplete: () => {
380
- void playAction();
397
+ "aria-label": "Seek",
398
+ className: "seek-range",
399
+ disabled: safeDuration <= 0,
400
+ max: safeDuration,
401
+ min: 0,
402
+ onChange: (e) => seekSetTime(Number(e.currentTarget.value)),
403
+ onKeyUp: (e) => {
404
+ if (e.key === "Enter" || e.key === " ") completeChange();
381
405
  },
406
+ onPointerUp: completeChange,
407
+ step: "any",
408
+ type: "range",
382
409
  value: safeCurrentTime,
383
- tooltip: { formatter },
384
- max: safeDuration,
385
- keyboard: false
410
+ title: formatTime(safeCurrentTime)
386
411
  }
387
412
  ) });
388
413
  };
389
414
  var TimeBlock = ({ duration, currentTime }) => {
390
- return /* @__PURE__ */ jsxs(Typography.Text, { style: { color: "#fff", whiteSpace: "nowrap" }, children: [
415
+ return /* @__PURE__ */ jsxs("span", { className: "video-player-time", children: [
391
416
  formatTime(currentTime),
392
417
  " / ",
393
418
  formatTime(duration)
394
419
  ] });
395
420
  };
421
+ var Icon = ({ name }) => {
422
+ const paths = {
423
+ back: /* @__PURE__ */ jsxs(Fragment, { children: [
424
+ /* @__PURE__ */ jsx("path", { d: "M11 7l-6 5 6 5V7z" }),
425
+ /* @__PURE__ */ jsx("path", { d: "M19 7l-6 5 6 5V7z" })
426
+ ] }),
427
+ forward: /* @__PURE__ */ jsxs(Fragment, { children: [
428
+ /* @__PURE__ */ jsx("path", { d: "M5 7l6 5-6 5V7z" }),
429
+ /* @__PURE__ */ jsx("path", { d: "M13 7l6 5-6 5V7z" })
430
+ ] }),
431
+ fullscreen: /* @__PURE__ */ jsxs(Fragment, { children: [
432
+ /* @__PURE__ */ jsx("path", { d: "M5 10V5h5" }),
433
+ /* @__PURE__ */ jsx("path", { d: "M14 5h5v5" }),
434
+ /* @__PURE__ */ jsx("path", { d: "M19 14v5h-5" }),
435
+ /* @__PURE__ */ jsx("path", { d: "M10 19H5v-5" })
436
+ ] }),
437
+ mute: /* @__PURE__ */ jsxs(Fragment, { children: [
438
+ /* @__PURE__ */ jsx("path", { d: "M4 10v4h4l5 4V6l-5 4H4z" }),
439
+ /* @__PURE__ */ jsx("path", { d: "M17 9l4 6" }),
440
+ /* @__PURE__ */ jsx("path", { d: "M21 9l-4 6" })
441
+ ] }),
442
+ pause: /* @__PURE__ */ jsxs(Fragment, { children: [
443
+ /* @__PURE__ */ jsx("path", { d: "M8 6h3v12H8z" }),
444
+ /* @__PURE__ */ jsx("path", { d: "M13 6h3v12h-3z" })
445
+ ] }),
446
+ play: /* @__PURE__ */ jsx("path", { d: "M8 5v14l11-7L8 5z" }),
447
+ sound: /* @__PURE__ */ jsxs(Fragment, { children: [
448
+ /* @__PURE__ */ jsx("path", { d: "M4 10v4h4l5 4V6l-5 4H4z" }),
449
+ /* @__PURE__ */ jsx("path", { d: "M16 9a4 4 0 010 6" }),
450
+ /* @__PURE__ */ jsx("path", { d: "M18.5 6.5a8 8 0 010 11" })
451
+ ] })
452
+ };
453
+ return /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", className: "video-player-icon", focusable: "false", viewBox: "0 0 24 24", children: paths[name] });
454
+ };
455
+ var ControlButton = ({
456
+ icon,
457
+ label,
458
+ onClick
459
+ }) => {
460
+ return /* @__PURE__ */ jsx("button", { "aria-label": label, className: "video-player-control-button", onClick: () => void onClick(), title: label, type: "button", children: /* @__PURE__ */ jsx(Icon, { name: icon }) });
461
+ };
396
462
  var stopControlClick = (e) => {
397
463
  e.preventDefault();
398
464
  e.stopPropagation();
@@ -402,7 +468,6 @@ var VideoPlayerControls = (props) => {
402
468
  variant,
403
469
  fullScreenAction,
404
470
  showControl,
405
- videoRef,
406
471
  playAction,
407
472
  pauseAction,
408
473
  seekSetTime,
@@ -417,30 +482,29 @@ var VideoPlayerControls = (props) => {
417
482
  } = props;
418
483
  const [, isMobile] = useWindowWidth();
419
484
  if (variant === "none") return null;
420
- if (!videoRef.current) return null;
421
485
  if (variant === "tiny") {
422
- return /* @__PURE__ */ jsx(VideoPlayerControlStyles, { $tiny: true, $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ jsxs(Flex, { gap: "small", align: "center", justify: "space-between", children: [
486
+ return /* @__PURE__ */ jsx(VideoPlayerControlStyles, { $tiny: true, $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ jsxs("div", { className: "video-player-control-row", children: [
423
487
  /* @__PURE__ */ jsx("div", { className: "full" }),
424
488
  /* @__PURE__ */ jsx(SeekSlider, { playAction, duration, currentTime, seekSetTime })
425
489
  ] }) });
426
490
  }
427
491
  if (variant === "fullscreen-only") {
428
- return /* @__PURE__ */ jsx(VideoPlayerControlStyles, { $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ jsxs(Flex, { gap: "small", align: "center", justify: "space-between", children: [
492
+ return /* @__PURE__ */ jsx(VideoPlayerControlStyles, { $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ jsxs("div", { className: "video-player-control-row", children: [
429
493
  /* @__PURE__ */ jsx("div", { className: "full" }),
430
- /* @__PURE__ */ jsx(Button, { size: "small", onClick: () => onMutedChange(!muted), icon: muted ? /* @__PURE__ */ jsx(MutedFilled, {}) : /* @__PURE__ */ jsx(SoundFilled, {}) }),
431
- /* @__PURE__ */ jsx(Button, { size: "small", onClick: fullScreenAction, icon: /* @__PURE__ */ jsx(FullscreenOutlined, {}) })
494
+ /* @__PURE__ */ jsx(ControlButton, { icon: muted ? "mute" : "sound", label: muted ? "Unmute" : "Mute", onClick: () => onMutedChange(!muted) }),
495
+ /* @__PURE__ */ jsx(ControlButton, { icon: "fullscreen", label: "Fullscreen", onClick: fullScreenAction })
432
496
  ] }) });
433
497
  }
434
- return /* @__PURE__ */ jsx(VideoPlayerControlStyles, { $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ jsxs(Flex, { gap: "small", align: "center", justify: "space-between", children: [
435
- playing ? /* @__PURE__ */ jsx(Button, { size: "small", onClick: () => pauseAction(), icon: /* @__PURE__ */ jsx(PauseOutlined, {}) }) : /* @__PURE__ */ jsx(Button, { size: "small", onClick: () => playAction(), icon: /* @__PURE__ */ jsx(PlaySquareFilled, {}) }),
436
- /* @__PURE__ */ jsx(Button, { size: "small", onClick: () => seekPrevAction(), icon: /* @__PURE__ */ jsx(BackwardFilled, {}) }),
437
- /* @__PURE__ */ jsx(Button, { size: "small", onClick: () => seekNextAction(), icon: /* @__PURE__ */ jsx(FastForwardFilled, {}) }),
438
- /* @__PURE__ */ jsx(Typography.Text, { ellipsis: true, className: "video-player-control-title", children: title }),
498
+ return /* @__PURE__ */ jsx(VideoPlayerControlStyles, { $isMobile: isMobile, onClick: stopControlClick, $showControl: showControl, children: /* @__PURE__ */ jsxs("div", { className: "video-player-control-row", children: [
499
+ playing ? /* @__PURE__ */ jsx(ControlButton, { icon: "pause", label: "Pause", onClick: pauseAction }) : /* @__PURE__ */ jsx(ControlButton, { icon: "play", label: "Play", onClick: playAction }),
500
+ /* @__PURE__ */ jsx(ControlButton, { icon: "back", label: "Back 15 seconds", onClick: () => seekPrevAction() }),
501
+ /* @__PURE__ */ jsx(ControlButton, { icon: "forward", label: "Forward 15 seconds", onClick: () => seekNextAction() }),
502
+ /* @__PURE__ */ jsx("span", { className: "video-player-control-title", title: typeof title === "string" ? title : void 0, children: title }),
439
503
  /* @__PURE__ */ jsx("div", { className: "full" }),
440
504
  /* @__PURE__ */ jsx(SeekSlider, { playAction, duration, currentTime, seekSetTime }),
441
505
  /* @__PURE__ */ jsx(TimeBlock, { duration, currentTime }),
442
- /* @__PURE__ */ jsx(Button, { size: "small", onClick: () => onMutedChange(!muted), icon: muted ? /* @__PURE__ */ jsx(MutedFilled, {}) : /* @__PURE__ */ jsx(SoundFilled, {}) }),
443
- /* @__PURE__ */ jsx(Button, { size: "small", onClick: fullScreenAction, icon: /* @__PURE__ */ jsx(FullscreenOutlined, {}) })
506
+ /* @__PURE__ */ jsx(ControlButton, { icon: muted ? "mute" : "sound", label: muted ? "Unmute" : "Mute", onClick: () => onMutedChange(!muted) }),
507
+ /* @__PURE__ */ jsx(ControlButton, { icon: "fullscreen", label: "Fullscreen", onClick: fullScreenAction })
444
508
  ] }) });
445
509
  };
446
510
 
@@ -639,6 +703,14 @@ var clampTime = (time, duration) => {
639
703
  const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
640
704
  return safeDuration > 0 ? Math.max(0, Math.min(safeTime, safeDuration)) : Math.max(0, safeTime);
641
705
  };
706
+ var isLikelyHlsSource = (src) => {
707
+ try {
708
+ const url = new URL(src, "http://video-player.local");
709
+ return /\.m3u8$/i.test(url.pathname) || /\.m3u8($|[?#])/i.test(src);
710
+ } catch {
711
+ return /\.m3u8($|[?#])/i.test(src);
712
+ }
713
+ };
642
714
  var VideoPlayerBase = (props, ref) => {
643
715
  const {
644
716
  controlsVariant = "none",
@@ -922,10 +994,16 @@ var VideoPlayerBase = (props, ref) => {
922
994
  );
923
995
  }, [hlsConfig, hlsCredentials, live, liveHlsConfig, vodHlsConfig]);
924
996
  const loadVideo = useCallback(
925
- async (startAt = 0) => {
997
+ async (startAt = initialTime) => {
926
998
  const startVideoRef = videoRef.current;
927
999
  if (!startVideoRef) return;
928
- const HlsCtor = await loadHlsCtor();
1000
+ let HlsCtor;
1001
+ try {
1002
+ HlsCtor = await loadHlsCtor();
1003
+ } catch (error) {
1004
+ reportError(error, { type: "hls-loader", src: videoSrc }, messages.streamLoadFailed);
1005
+ return;
1006
+ }
929
1007
  if (!videoRef.current || videoRef.current !== startVideoRef) return;
930
1008
  const video = videoRef.current;
931
1009
  if (!video) return;
@@ -1019,7 +1097,6 @@ var VideoPlayerBase = (props, ref) => {
1019
1097
  }
1020
1098
  void loadVideo(resumeTime);
1021
1099
  });
1022
- h.attachMedia(video);
1023
1100
  h.on(HlsCtor.Events.MEDIA_ATTACHED, () => {
1024
1101
  h.loadSource(videoSrc);
1025
1102
  });
@@ -1035,11 +1112,13 @@ var VideoPlayerBase = (props, ref) => {
1035
1112
  }
1036
1113
  if (autoPlay) void playAction();
1037
1114
  });
1115
+ h.attachMedia(video);
1038
1116
  },
1039
1117
  [
1040
1118
  autoPlay,
1041
1119
  clearErrorState,
1042
1120
  clearTimers,
1121
+ initialTime,
1043
1122
  live,
1044
1123
  messages.streamDecodeFailed,
1045
1124
  messages.streamLoadFailed,
@@ -1061,18 +1140,24 @@ var VideoPlayerBase = (props, ref) => {
1061
1140
  const loadVideoHls = useCallback(async () => {
1062
1141
  const video = videoRef.current;
1063
1142
  const canNativeHls = !!(video == null ? void 0 : video.canPlayType) && video.canPlayType("application/vnd.apple.mpegurl") !== "";
1064
- const HlsCtor = await loadHlsCtor();
1143
+ let HlsCtor;
1144
+ try {
1145
+ HlsCtor = await loadHlsCtor();
1146
+ } catch (error) {
1147
+ reportError(error, { type: "hls-loader", src: videoSrc }, messages.streamLoadFailed);
1148
+ return;
1149
+ }
1065
1150
  if (!videoRef.current || videoRef.current !== video) return;
1066
1151
  if (HlsCtor.isSupported()) {
1067
- void loadVideo();
1152
+ void loadVideo(initialTime);
1068
1153
  return;
1069
1154
  }
1070
1155
  if (canNativeHls) {
1071
1156
  loadVideoNative();
1072
1157
  return;
1073
1158
  }
1074
- void loadVideo();
1075
- }, [loadVideo, loadVideoNative]);
1159
+ reportError(null, { type: "hls", src: videoSrc, reason: "HLS is not supported in this browser" }, messages.unsupported);
1160
+ }, [initialTime, loadVideo, loadVideoNative, messages.streamLoadFailed, messages.unsupported, reportError, videoSrc]);
1076
1161
  const destroy = useCallback(() => {
1077
1162
  clearTimers();
1078
1163
  if (hlsRef.current) {
@@ -1203,8 +1288,9 @@ var VideoPlayerBase = (props, ref) => {
1203
1288
  hlsRef.current = null;
1204
1289
  }
1205
1290
  if (sourceType === "native" || type === "video") loadVideoNative();
1206
- else if (sourceType === "hls") void loadVideo();
1207
- else void loadVideoHls();
1291
+ else if (sourceType === "hls") void loadVideo(initialTime);
1292
+ else if (isLikelyHlsSource(videoSrc)) void loadVideoHls();
1293
+ else loadVideoNative();
1208
1294
  return () => {
1209
1295
  clearTimers();
1210
1296
  if (hlsRef.current) {
@@ -1218,6 +1304,7 @@ var VideoPlayerBase = (props, ref) => {
1218
1304
  loadVideo,
1219
1305
  loadVideoHls,
1220
1306
  loadVideoNative,
1307
+ initialTime,
1221
1308
  reloadKey,
1222
1309
  reloadToken,
1223
1310
  sourceType,
@@ -1284,6 +1371,7 @@ var VideoPlayerBase = (props, ref) => {
1284
1371
  "video",
1285
1372
  {
1286
1373
  ...videoProps,
1374
+ controls: false,
1287
1375
  ref: setVideoEl,
1288
1376
  crossOrigin,
1289
1377
  playsInline: true,
@@ -1313,7 +1401,6 @@ var VideoPlayerBase = (props, ref) => {
1313
1401
  variant: controlsVariant,
1314
1402
  fullScreenAction,
1315
1403
  showControl,
1316
- videoRef,
1317
1404
  playAction,
1318
1405
  pauseAction,
1319
1406
  seekSetTime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikelroot/video-player",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Standalone React video player with HLS support.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -26,18 +26,14 @@
26
26
  "hls.js": "^1.6.15"
27
27
  },
28
28
  "peerDependencies": {
29
- "@ant-design/icons": "^6.1.0",
30
- "antd": "^6.3.1",
31
29
  "react": ">=18",
32
30
  "react-dom": ">=18",
33
31
  "styled-components": "^6.0.0"
34
32
  },
35
33
  "devDependencies": {
36
- "@ant-design/icons": "^6.1.0",
37
34
  "@types/node": "^25.3.5",
38
35
  "@types/react": "^19.2.14",
39
36
  "@types/react-dom": "^19.2.3",
40
- "antd": "^6.3.1",
41
37
  "react": "^19.2.4",
42
38
  "react-dom": "^19.2.4",
43
39
  "styled-components": "^6.3.11",