@lightbird/core 0.6.0 → 0.7.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/index.cjs CHANGED
@@ -1630,7 +1630,11 @@ var DEFAULT_SHORTCUTS = [
1630
1630
  { action: "screenshot", label: "Screenshot", defaultKey: "s", key: "s", modifiers: { ctrl: true } },
1631
1631
  { action: "show-shortcuts", label: "Show Shortcuts Help", defaultKey: "?", key: "?" },
1632
1632
  { action: "next-chapter", label: "Next Chapter", defaultKey: "]", key: "]" },
1633
- { action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" }
1633
+ { action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" },
1634
+ { action: "frame-step-forward", label: "Step Forward One Frame", defaultKey: ".", key: "." },
1635
+ { action: "frame-step-backward", label: "Step Backward One Frame", defaultKey: ",", key: "," },
1636
+ { action: "loop-toggle", label: "Toggle Loop", defaultKey: "l", key: "l" },
1637
+ { action: "ab-loop-cycle", label: "A-B Loop (Set/Cycle)", defaultKey: "r", key: "r" }
1634
1638
  ];
1635
1639
  var STORAGE_KEY = "lightbird-shortcuts";
1636
1640
  function loadShortcuts() {
@@ -1664,7 +1668,14 @@ function matchesShortcut(e, binding) {
1664
1668
  function isInteractiveElement(el) {
1665
1669
  if (!el || !(el instanceof HTMLElement)) return false;
1666
1670
  const tag = el.tagName.toLowerCase();
1667
- return ["input", "textarea", "select", "button", "a"].includes(tag) || el.contentEditable === "true" || el.getAttribute("contenteditable") !== null;
1671
+ if (["input", "textarea", "select"].includes(tag)) return true;
1672
+ if (el.contentEditable === "true" || el.getAttribute("contenteditable") !== null) return true;
1673
+ if (typeof el.closest === "function" && el.closest(
1674
+ '[role="dialog"], [role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]'
1675
+ )) {
1676
+ return true;
1677
+ }
1678
+ return false;
1668
1679
  }
1669
1680
  function formatShortcutKey(binding) {
1670
1681
  const mods = [];
package/dist/index.d.cts CHANGED
@@ -388,7 +388,7 @@ declare function captureVideoThumbnail(videoEl: HTMLVideoElement, atSeconds?: nu
388
388
  */
389
389
  declare function captureFrameAt(videoEl: HTMLVideoElement, timeSeconds: number, width?: number, height?: number): Promise<string | null>;
390
390
 
391
- type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter';
391
+ type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter' | 'frame-step-forward' | 'frame-step-backward' | 'loop-toggle' | 'ab-loop-cycle';
392
392
  interface ShortcutBinding {
393
393
  action: ShortcutAction;
394
394
  label: string;
package/dist/index.d.ts CHANGED
@@ -388,7 +388,7 @@ declare function captureVideoThumbnail(videoEl: HTMLVideoElement, atSeconds?: nu
388
388
  */
389
389
  declare function captureFrameAt(videoEl: HTMLVideoElement, timeSeconds: number, width?: number, height?: number): Promise<string | null>;
390
390
 
391
- type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter';
391
+ type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter' | 'frame-step-forward' | 'frame-step-backward' | 'loop-toggle' | 'ab-loop-cycle';
392
392
  interface ShortcutBinding {
393
393
  action: ShortcutAction;
394
394
  label: string;
package/dist/index.js CHANGED
@@ -1627,7 +1627,11 @@ var DEFAULT_SHORTCUTS = [
1627
1627
  { action: "screenshot", label: "Screenshot", defaultKey: "s", key: "s", modifiers: { ctrl: true } },
1628
1628
  { action: "show-shortcuts", label: "Show Shortcuts Help", defaultKey: "?", key: "?" },
1629
1629
  { action: "next-chapter", label: "Next Chapter", defaultKey: "]", key: "]" },
1630
- { action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" }
1630
+ { action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" },
1631
+ { action: "frame-step-forward", label: "Step Forward One Frame", defaultKey: ".", key: "." },
1632
+ { action: "frame-step-backward", label: "Step Backward One Frame", defaultKey: ",", key: "," },
1633
+ { action: "loop-toggle", label: "Toggle Loop", defaultKey: "l", key: "l" },
1634
+ { action: "ab-loop-cycle", label: "A-B Loop (Set/Cycle)", defaultKey: "r", key: "r" }
1631
1635
  ];
1632
1636
  var STORAGE_KEY = "lightbird-shortcuts";
1633
1637
  function loadShortcuts() {
@@ -1661,7 +1665,14 @@ function matchesShortcut(e, binding) {
1661
1665
  function isInteractiveElement(el) {
1662
1666
  if (!el || !(el instanceof HTMLElement)) return false;
1663
1667
  const tag = el.tagName.toLowerCase();
1664
- return ["input", "textarea", "select", "button", "a"].includes(tag) || el.contentEditable === "true" || el.getAttribute("contenteditable") !== null;
1668
+ if (["input", "textarea", "select"].includes(tag)) return true;
1669
+ if (el.contentEditable === "true" || el.getAttribute("contenteditable") !== null) return true;
1670
+ if (typeof el.closest === "function" && el.closest(
1671
+ '[role="dialog"], [role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]'
1672
+ )) {
1673
+ return true;
1674
+ }
1675
+ return false;
1665
1676
  }
1666
1677
  function formatShortcutKey(binding) {
1667
1678
  const mods = [];
@@ -673,7 +673,14 @@ function matchesShortcut(e, binding) {
673
673
  function isInteractiveElement(el) {
674
674
  if (!el || !(el instanceof HTMLElement)) return false;
675
675
  const tag = el.tagName.toLowerCase();
676
- return ["input", "textarea", "select", "button", "a"].includes(tag) || el.contentEditable === "true" || el.getAttribute("contenteditable") !== null;
676
+ if (["input", "textarea", "select"].includes(tag)) return true;
677
+ if (el.contentEditable === "true" || el.getAttribute("contenteditable") !== null) return true;
678
+ if (typeof el.closest === "function" && el.closest(
679
+ '[role="dialog"], [role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]'
680
+ )) {
681
+ return true;
682
+ }
683
+ return false;
677
684
  }
678
685
 
679
686
  // src/react/use-keyboard-shortcuts.ts
@@ -1332,6 +1339,66 @@ function useABLoop(videoRef) {
1332
1339
  clear
1333
1340
  };
1334
1341
  }
1342
+ function useSmoothProgress(videoRef, { isPlaying, fallback = 0 }) {
1343
+ const [progress, setProgress] = react.useState(() => {
1344
+ const el = videoRef.current;
1345
+ return el ? el.currentTime : fallback;
1346
+ });
1347
+ const rafRef = react.useRef(null);
1348
+ react.useEffect(() => {
1349
+ const el = videoRef.current;
1350
+ if (!el) {
1351
+ setProgress(fallback);
1352
+ return;
1353
+ }
1354
+ if (!isPlaying) {
1355
+ setProgress(el.currentTime);
1356
+ return;
1357
+ }
1358
+ const isHidden = () => typeof document !== "undefined" && document.visibilityState === "hidden";
1359
+ let cancelled = false;
1360
+ const tick = () => {
1361
+ if (cancelled) return;
1362
+ const current = videoRef.current;
1363
+ if (!current) {
1364
+ setProgress(fallback);
1365
+ stop();
1366
+ return;
1367
+ }
1368
+ setProgress(current.currentTime);
1369
+ rafRef.current = requestAnimationFrame(tick);
1370
+ };
1371
+ const start = () => {
1372
+ if (rafRef.current != null || isHidden()) return;
1373
+ rafRef.current = requestAnimationFrame(tick);
1374
+ };
1375
+ const stop = () => {
1376
+ if (rafRef.current != null) {
1377
+ cancelAnimationFrame(rafRef.current);
1378
+ rafRef.current = null;
1379
+ }
1380
+ };
1381
+ const onVisibility = () => {
1382
+ if (isHidden()) {
1383
+ stop();
1384
+ } else {
1385
+ start();
1386
+ }
1387
+ };
1388
+ start();
1389
+ if (typeof document !== "undefined") {
1390
+ document.addEventListener("visibilitychange", onVisibility);
1391
+ }
1392
+ return () => {
1393
+ cancelled = true;
1394
+ stop();
1395
+ if (typeof document !== "undefined") {
1396
+ document.removeEventListener("visibilitychange", onVisibility);
1397
+ }
1398
+ };
1399
+ }, [isPlaying, videoRef, fallback]);
1400
+ return progress;
1401
+ }
1335
1402
  var SWIPE_THRESHOLD = 10;
1336
1403
  var DOUBLE_TAP_DISTANCE = 40;
1337
1404
  function useTouchGestures(targetRef, handlers, options = {}) {
@@ -1463,6 +1530,7 @@ exports.usePictureInPicture = usePictureInPicture;
1463
1530
  exports.usePlaylist = usePlaylist;
1464
1531
  exports.useProgressPersistence = useProgressPersistence;
1465
1532
  exports.useSeekPreview = useSeekPreview;
1533
+ exports.useSmoothProgress = useSmoothProgress;
1466
1534
  exports.useSubtitles = useSubtitles;
1467
1535
  exports.useTouchGestures = useTouchGestures;
1468
1536
  exports.useVideoFilters = useVideoFilters;
@@ -183,7 +183,7 @@ declare function usePlaylist(): {
183
183
  setCurrentIndex: react.Dispatch<react.SetStateAction<number | null>>;
184
184
  };
185
185
 
186
- type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter';
186
+ type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter' | 'frame-step-forward' | 'frame-step-backward' | 'loop-toggle' | 'ab-loop-cycle';
187
187
  interface ShortcutBinding {
188
188
  action: ShortcutAction;
189
189
  label: string;
@@ -335,6 +335,20 @@ interface ABLoopState {
335
335
  */
336
336
  declare function useABLoop(videoRef: RefObject<HTMLVideoElement | null>): ABLoopState;
337
337
 
338
+ interface UseSmoothProgressOptions {
339
+ isPlaying: boolean;
340
+ fallback?: number;
341
+ }
342
+ /**
343
+ * Drives a `progress` value at requestAnimationFrame rate by reading
344
+ * `videoRef.current.currentTime` each frame while playing. Decouples the
345
+ * visual seek-bar position from the video element's coarse `timeupdate`
346
+ * events (~4Hz) so the thumb glides instead of stepping.
347
+ *
348
+ * Pauses the rAF loop when not playing or when the tab is hidden.
349
+ */
350
+ declare function useSmoothProgress(videoRef: RefObject<HTMLVideoElement | null>, { isPlaying, fallback }: UseSmoothProgressOptions): number;
351
+
338
352
  interface TouchGestureHandlers {
339
353
  /** Seek relative to the current time by a signed number of seconds. */
340
354
  seekBy?: (seconds: number) => void;
@@ -384,4 +398,4 @@ interface TouchGesturesState {
384
398
  */
385
399
  declare function useTouchGestures(targetRef: RefObject<HTMLElement | null>, handlers: TouchGestureHandlers, options?: UseTouchGesturesOptions): TouchGesturesState;
386
400
 
387
- export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
401
+ export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
@@ -183,7 +183,7 @@ declare function usePlaylist(): {
183
183
  setCurrentIndex: react.Dispatch<react.SetStateAction<number | null>>;
184
184
  };
185
185
 
186
- type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter';
186
+ type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter' | 'frame-step-forward' | 'frame-step-backward' | 'loop-toggle' | 'ab-loop-cycle';
187
187
  interface ShortcutBinding {
188
188
  action: ShortcutAction;
189
189
  label: string;
@@ -335,6 +335,20 @@ interface ABLoopState {
335
335
  */
336
336
  declare function useABLoop(videoRef: RefObject<HTMLVideoElement | null>): ABLoopState;
337
337
 
338
+ interface UseSmoothProgressOptions {
339
+ isPlaying: boolean;
340
+ fallback?: number;
341
+ }
342
+ /**
343
+ * Drives a `progress` value at requestAnimationFrame rate by reading
344
+ * `videoRef.current.currentTime` each frame while playing. Decouples the
345
+ * visual seek-bar position from the video element's coarse `timeupdate`
346
+ * events (~4Hz) so the thumb glides instead of stepping.
347
+ *
348
+ * Pauses the rAF loop when not playing or when the tab is hidden.
349
+ */
350
+ declare function useSmoothProgress(videoRef: RefObject<HTMLVideoElement | null>, { isPlaying, fallback }: UseSmoothProgressOptions): number;
351
+
338
352
  interface TouchGestureHandlers {
339
353
  /** Seek relative to the current time by a signed number of seconds. */
340
354
  seekBy?: (seconds: number) => void;
@@ -384,4 +398,4 @@ interface TouchGesturesState {
384
398
  */
385
399
  declare function useTouchGestures(targetRef: RefObject<HTMLElement | null>, handlers: TouchGestureHandlers, options?: UseTouchGesturesOptions): TouchGesturesState;
386
400
 
387
- export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
401
+ export { type ABLoopState, type SeekPreviewState, type ShortcutHandlers, type TouchGestureFeedback, type TouchGestureHandlers, type TouchGesturesState, type UseMagnetReturn, type UseMediaSessionOptions, type UseSeekPreviewOptions, type UseSubtitlesOptions, type UseTouchGesturesOptions, useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
@@ -671,7 +671,14 @@ function matchesShortcut(e, binding) {
671
671
  function isInteractiveElement(el) {
672
672
  if (!el || !(el instanceof HTMLElement)) return false;
673
673
  const tag = el.tagName.toLowerCase();
674
- return ["input", "textarea", "select", "button", "a"].includes(tag) || el.contentEditable === "true" || el.getAttribute("contenteditable") !== null;
674
+ if (["input", "textarea", "select"].includes(tag)) return true;
675
+ if (el.contentEditable === "true" || el.getAttribute("contenteditable") !== null) return true;
676
+ if (typeof el.closest === "function" && el.closest(
677
+ '[role="dialog"], [role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]'
678
+ )) {
679
+ return true;
680
+ }
681
+ return false;
675
682
  }
676
683
 
677
684
  // src/react/use-keyboard-shortcuts.ts
@@ -1330,6 +1337,66 @@ function useABLoop(videoRef) {
1330
1337
  clear
1331
1338
  };
1332
1339
  }
1340
+ function useSmoothProgress(videoRef, { isPlaying, fallback = 0 }) {
1341
+ const [progress, setProgress] = useState(() => {
1342
+ const el = videoRef.current;
1343
+ return el ? el.currentTime : fallback;
1344
+ });
1345
+ const rafRef = useRef(null);
1346
+ useEffect(() => {
1347
+ const el = videoRef.current;
1348
+ if (!el) {
1349
+ setProgress(fallback);
1350
+ return;
1351
+ }
1352
+ if (!isPlaying) {
1353
+ setProgress(el.currentTime);
1354
+ return;
1355
+ }
1356
+ const isHidden = () => typeof document !== "undefined" && document.visibilityState === "hidden";
1357
+ let cancelled = false;
1358
+ const tick = () => {
1359
+ if (cancelled) return;
1360
+ const current = videoRef.current;
1361
+ if (!current) {
1362
+ setProgress(fallback);
1363
+ stop();
1364
+ return;
1365
+ }
1366
+ setProgress(current.currentTime);
1367
+ rafRef.current = requestAnimationFrame(tick);
1368
+ };
1369
+ const start = () => {
1370
+ if (rafRef.current != null || isHidden()) return;
1371
+ rafRef.current = requestAnimationFrame(tick);
1372
+ };
1373
+ const stop = () => {
1374
+ if (rafRef.current != null) {
1375
+ cancelAnimationFrame(rafRef.current);
1376
+ rafRef.current = null;
1377
+ }
1378
+ };
1379
+ const onVisibility = () => {
1380
+ if (isHidden()) {
1381
+ stop();
1382
+ } else {
1383
+ start();
1384
+ }
1385
+ };
1386
+ start();
1387
+ if (typeof document !== "undefined") {
1388
+ document.addEventListener("visibilitychange", onVisibility);
1389
+ }
1390
+ return () => {
1391
+ cancelled = true;
1392
+ stop();
1393
+ if (typeof document !== "undefined") {
1394
+ document.removeEventListener("visibilitychange", onVisibility);
1395
+ }
1396
+ };
1397
+ }, [isPlaying, videoRef, fallback]);
1398
+ return progress;
1399
+ }
1333
1400
  var SWIPE_THRESHOLD = 10;
1334
1401
  var DOUBLE_TAP_DISTANCE = 40;
1335
1402
  function useTouchGestures(targetRef, handlers, options = {}) {
@@ -1451,4 +1518,4 @@ function useTouchGestures(targetRef, handlers, options = {}) {
1451
1518
  return { feedback };
1452
1519
  }
1453
1520
 
1454
- export { useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
1521
+ export { useABLoop, useChapters, useFullscreen, useKeyboardShortcuts, useMagnet, useMediaSession, usePictureInPicture, usePlaylist, useProgressPersistence, useSeekPreview, useSmoothProgress, useSubtitles, useTouchGestures, useVideoFilters, useVideoInfo, useVideoPlayback };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightbird/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Client-side video player engine. Plays MKV, MP4, WebM with full subtitle, audio track, and chapter support. No server required.",
5
5
  "license": "MIT",
6
6
  "author": "Punyam Singh",