@lightbird/core 0.6.0 → 0.8.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 +46 -2
- package/dist/index.d.cts +47 -2
- package/dist/index.d.ts +47 -2
- package/dist/index.js +44 -3
- package/dist/react/index.cjs +69 -1
- package/dist/react/index.d.cts +16 -2
- package/dist/react/index.d.ts +16 -2
- package/dist/react/index.js +69 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1614,6 +1614,36 @@ async function captureFrameAt(videoEl, timeSeconds, width = 160, height = 90) {
|
|
|
1614
1614
|
});
|
|
1615
1615
|
}
|
|
1616
1616
|
|
|
1617
|
+
// src/utils/frame-export.ts
|
|
1618
|
+
function exportVideoFrame(videoEl, options = {}) {
|
|
1619
|
+
const { type = "image/png", quality, filter } = options;
|
|
1620
|
+
const width = videoEl.videoWidth;
|
|
1621
|
+
const height = videoEl.videoHeight;
|
|
1622
|
+
if (!width || !height) return null;
|
|
1623
|
+
const canvas = document.createElement("canvas");
|
|
1624
|
+
canvas.width = width;
|
|
1625
|
+
canvas.height = height;
|
|
1626
|
+
const ctx = canvas.getContext("2d");
|
|
1627
|
+
if (!ctx) return null;
|
|
1628
|
+
try {
|
|
1629
|
+
if (filter) ctx.filter = filter;
|
|
1630
|
+
ctx.drawImage(videoEl, 0, 0, width, height);
|
|
1631
|
+
return canvas.toDataURL(type, quality);
|
|
1632
|
+
} catch {
|
|
1633
|
+
return null;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
function downloadDataUrl(dataUrl, filename) {
|
|
1637
|
+
const a = document.createElement("a");
|
|
1638
|
+
a.href = dataUrl;
|
|
1639
|
+
a.download = filename;
|
|
1640
|
+
a.click();
|
|
1641
|
+
}
|
|
1642
|
+
function frameExportFilename(extension = "png") {
|
|
1643
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
|
|
1644
|
+
return `lightbird-screenshot-${stamp}.${extension}`;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1617
1647
|
// src/utils/keyboard-shortcuts.ts
|
|
1618
1648
|
var DEFAULT_SHORTCUTS = [
|
|
1619
1649
|
{ action: "play-pause", label: "Play / Pause", defaultKey: " ", key: " " },
|
|
@@ -1630,7 +1660,11 @@ var DEFAULT_SHORTCUTS = [
|
|
|
1630
1660
|
{ action: "screenshot", label: "Screenshot", defaultKey: "s", key: "s", modifiers: { ctrl: true } },
|
|
1631
1661
|
{ action: "show-shortcuts", label: "Show Shortcuts Help", defaultKey: "?", key: "?" },
|
|
1632
1662
|
{ action: "next-chapter", label: "Next Chapter", defaultKey: "]", key: "]" },
|
|
1633
|
-
{ action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" }
|
|
1663
|
+
{ action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" },
|
|
1664
|
+
{ action: "frame-step-forward", label: "Step Forward One Frame", defaultKey: ".", key: "." },
|
|
1665
|
+
{ action: "frame-step-backward", label: "Step Backward One Frame", defaultKey: ",", key: "," },
|
|
1666
|
+
{ action: "loop-toggle", label: "Toggle Loop", defaultKey: "l", key: "l" },
|
|
1667
|
+
{ action: "ab-loop-cycle", label: "A-B Loop (Set/Cycle)", defaultKey: "r", key: "r" }
|
|
1634
1668
|
];
|
|
1635
1669
|
var STORAGE_KEY = "lightbird-shortcuts";
|
|
1636
1670
|
function loadShortcuts() {
|
|
@@ -1664,7 +1698,14 @@ function matchesShortcut(e, binding) {
|
|
|
1664
1698
|
function isInteractiveElement(el) {
|
|
1665
1699
|
if (!el || !(el instanceof HTMLElement)) return false;
|
|
1666
1700
|
const tag = el.tagName.toLowerCase();
|
|
1667
|
-
|
|
1701
|
+
if (["input", "textarea", "select"].includes(tag)) return true;
|
|
1702
|
+
if (el.contentEditable === "true" || el.getAttribute("contenteditable") !== null) return true;
|
|
1703
|
+
if (typeof el.closest === "function" && el.closest(
|
|
1704
|
+
'[role="dialog"], [role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]'
|
|
1705
|
+
)) {
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
return false;
|
|
1668
1709
|
}
|
|
1669
1710
|
function formatShortcutKey(binding) {
|
|
1670
1711
|
const mods = [];
|
|
@@ -1762,9 +1803,12 @@ exports.configureLightBird = configureLightBird;
|
|
|
1762
1803
|
exports.createOffsetVttUrl = createOffsetVttUrl;
|
|
1763
1804
|
exports.createVideoPlayer = createVideoPlayer;
|
|
1764
1805
|
exports.destroyWebTorrentClient = destroyWebTorrentClient;
|
|
1806
|
+
exports.downloadDataUrl = downloadDataUrl;
|
|
1765
1807
|
exports.exportPlaylist = exportPlaylist;
|
|
1808
|
+
exports.exportVideoFrame = exportVideoFrame;
|
|
1766
1809
|
exports.extractNativeMetadata = extractNativeMetadata;
|
|
1767
1810
|
exports.formatShortcutKey = formatShortcutKey;
|
|
1811
|
+
exports.frameExportFilename = frameExportFilename;
|
|
1768
1812
|
exports.getFFmpeg = getFFmpeg;
|
|
1769
1813
|
exports.getLanguageName = getLanguageName;
|
|
1770
1814
|
exports.getVideoFiles = getVideoFiles;
|
package/dist/index.d.cts
CHANGED
|
@@ -388,7 +388,52 @@ 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
|
-
|
|
391
|
+
/**
|
|
392
|
+
* Frame / screenshot export.
|
|
393
|
+
*
|
|
394
|
+
* Captures the currently displayed frame of a playing video element to an
|
|
395
|
+
* image and (optionally) triggers a browser download. Unlike the seek-hover
|
|
396
|
+
* preview in {@link ./video-thumbnail}, this grabs the frame *in place* at the
|
|
397
|
+
* video's native resolution without seeking, so it never disturbs playback.
|
|
398
|
+
*/
|
|
399
|
+
interface ExportFrameOptions {
|
|
400
|
+
/** Output image MIME type. Defaults to `"image/png"`. */
|
|
401
|
+
type?: string;
|
|
402
|
+
/** Quality (0–1) for lossy formats such as jpeg/webp. Ignored for png. */
|
|
403
|
+
quality?: number;
|
|
404
|
+
/**
|
|
405
|
+
* CSS `filter` string to bake into the exported image (e.g. the
|
|
406
|
+
* brightness/contrast filters applied to the video element). When omitted,
|
|
407
|
+
* the raw frame is captured.
|
|
408
|
+
*/
|
|
409
|
+
filter?: string;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Capture the current frame of a video element at its native resolution.
|
|
413
|
+
*
|
|
414
|
+
* @param videoEl - The video element to capture from.
|
|
415
|
+
* @param options - Output format and an optional CSS filter to bake in.
|
|
416
|
+
* @returns An image data URL, or `null` if capture failed — no 2d context, the
|
|
417
|
+
* video has no decoded dimensions yet, or the canvas is tainted by a
|
|
418
|
+
* cross-origin source.
|
|
419
|
+
*/
|
|
420
|
+
declare function exportVideoFrame(videoEl: HTMLVideoElement, options?: ExportFrameOptions): string | null;
|
|
421
|
+
/**
|
|
422
|
+
* Trigger a browser download of a data URL by synthesizing an anchor click.
|
|
423
|
+
*
|
|
424
|
+
* @param dataUrl - The data (or object) URL to download.
|
|
425
|
+
* @param filename - The suggested filename for the saved file.
|
|
426
|
+
*/
|
|
427
|
+
declare function downloadDataUrl(dataUrl: string, filename: string): void;
|
|
428
|
+
/**
|
|
429
|
+
* Build a timestamped default filename for an exported frame.
|
|
430
|
+
*
|
|
431
|
+
* @param extension - File extension without a leading dot. Defaults to `"png"`.
|
|
432
|
+
* @returns e.g. `lightbird-screenshot-2026-05-30T12-00-00-000Z.png`.
|
|
433
|
+
*/
|
|
434
|
+
declare function frameExportFilename(extension?: string): string;
|
|
435
|
+
|
|
436
|
+
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
437
|
interface ShortcutBinding {
|
|
393
438
|
action: ShortcutAction;
|
|
394
439
|
label: string;
|
|
@@ -450,4 +495,4 @@ declare function resetFFmpeg(): void;
|
|
|
450
495
|
*/
|
|
451
496
|
declare function getLanguageName(code?: string | null): string | undefined;
|
|
452
497
|
|
|
453
|
-
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, HLSPlayer, type HLSPlayerFile, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type QualityLevel, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
498
|
+
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, type ExportFrameOptions, FLAG_MAGNET_LINK, HLSPlayer, type HLSPlayerFile, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type QualityLevel, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.d.ts
CHANGED
|
@@ -388,7 +388,52 @@ 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
|
-
|
|
391
|
+
/**
|
|
392
|
+
* Frame / screenshot export.
|
|
393
|
+
*
|
|
394
|
+
* Captures the currently displayed frame of a playing video element to an
|
|
395
|
+
* image and (optionally) triggers a browser download. Unlike the seek-hover
|
|
396
|
+
* preview in {@link ./video-thumbnail}, this grabs the frame *in place* at the
|
|
397
|
+
* video's native resolution without seeking, so it never disturbs playback.
|
|
398
|
+
*/
|
|
399
|
+
interface ExportFrameOptions {
|
|
400
|
+
/** Output image MIME type. Defaults to `"image/png"`. */
|
|
401
|
+
type?: string;
|
|
402
|
+
/** Quality (0–1) for lossy formats such as jpeg/webp. Ignored for png. */
|
|
403
|
+
quality?: number;
|
|
404
|
+
/**
|
|
405
|
+
* CSS `filter` string to bake into the exported image (e.g. the
|
|
406
|
+
* brightness/contrast filters applied to the video element). When omitted,
|
|
407
|
+
* the raw frame is captured.
|
|
408
|
+
*/
|
|
409
|
+
filter?: string;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Capture the current frame of a video element at its native resolution.
|
|
413
|
+
*
|
|
414
|
+
* @param videoEl - The video element to capture from.
|
|
415
|
+
* @param options - Output format and an optional CSS filter to bake in.
|
|
416
|
+
* @returns An image data URL, or `null` if capture failed — no 2d context, the
|
|
417
|
+
* video has no decoded dimensions yet, or the canvas is tainted by a
|
|
418
|
+
* cross-origin source.
|
|
419
|
+
*/
|
|
420
|
+
declare function exportVideoFrame(videoEl: HTMLVideoElement, options?: ExportFrameOptions): string | null;
|
|
421
|
+
/**
|
|
422
|
+
* Trigger a browser download of a data URL by synthesizing an anchor click.
|
|
423
|
+
*
|
|
424
|
+
* @param dataUrl - The data (or object) URL to download.
|
|
425
|
+
* @param filename - The suggested filename for the saved file.
|
|
426
|
+
*/
|
|
427
|
+
declare function downloadDataUrl(dataUrl: string, filename: string): void;
|
|
428
|
+
/**
|
|
429
|
+
* Build a timestamped default filename for an exported frame.
|
|
430
|
+
*
|
|
431
|
+
* @param extension - File extension without a leading dot. Defaults to `"png"`.
|
|
432
|
+
* @returns e.g. `lightbird-screenshot-2026-05-30T12-00-00-000Z.png`.
|
|
433
|
+
*/
|
|
434
|
+
declare function frameExportFilename(extension?: string): string;
|
|
435
|
+
|
|
436
|
+
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
437
|
interface ShortcutBinding {
|
|
393
438
|
action: ShortcutAction;
|
|
394
439
|
label: string;
|
|
@@ -450,4 +495,4 @@ declare function resetFFmpeg(): void;
|
|
|
450
495
|
*/
|
|
451
496
|
declare function getLanguageName(code?: string | null): string | undefined;
|
|
452
497
|
|
|
453
|
-
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, HLSPlayer, type HLSPlayerFile, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type QualityLevel, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
498
|
+
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, type ExportFrameOptions, FLAG_MAGNET_LINK, HLSPlayer, type HLSPlayerFile, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, type QualityLevel, type ShortcutAction, type ShortcutBinding, SimplePlayer, type SimplePlayerFile, type Subtitle, SubtitleConverter, type SubtitleCue, type SubtitleTrackMeta, type TorrentStatus, UniversalSubtitleManager, VIDEO_EXTENSIONS, type VideoFilters, type VideoMetadata, type VideoPlayer, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.js
CHANGED
|
@@ -1611,6 +1611,36 @@ async function captureFrameAt(videoEl, timeSeconds, width = 160, height = 90) {
|
|
|
1611
1611
|
});
|
|
1612
1612
|
}
|
|
1613
1613
|
|
|
1614
|
+
// src/utils/frame-export.ts
|
|
1615
|
+
function exportVideoFrame(videoEl, options = {}) {
|
|
1616
|
+
const { type = "image/png", quality, filter } = options;
|
|
1617
|
+
const width = videoEl.videoWidth;
|
|
1618
|
+
const height = videoEl.videoHeight;
|
|
1619
|
+
if (!width || !height) return null;
|
|
1620
|
+
const canvas = document.createElement("canvas");
|
|
1621
|
+
canvas.width = width;
|
|
1622
|
+
canvas.height = height;
|
|
1623
|
+
const ctx = canvas.getContext("2d");
|
|
1624
|
+
if (!ctx) return null;
|
|
1625
|
+
try {
|
|
1626
|
+
if (filter) ctx.filter = filter;
|
|
1627
|
+
ctx.drawImage(videoEl, 0, 0, width, height);
|
|
1628
|
+
return canvas.toDataURL(type, quality);
|
|
1629
|
+
} catch {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function downloadDataUrl(dataUrl, filename) {
|
|
1634
|
+
const a = document.createElement("a");
|
|
1635
|
+
a.href = dataUrl;
|
|
1636
|
+
a.download = filename;
|
|
1637
|
+
a.click();
|
|
1638
|
+
}
|
|
1639
|
+
function frameExportFilename(extension = "png") {
|
|
1640
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
|
|
1641
|
+
return `lightbird-screenshot-${stamp}.${extension}`;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1614
1644
|
// src/utils/keyboard-shortcuts.ts
|
|
1615
1645
|
var DEFAULT_SHORTCUTS = [
|
|
1616
1646
|
{ action: "play-pause", label: "Play / Pause", defaultKey: " ", key: " " },
|
|
@@ -1627,7 +1657,11 @@ var DEFAULT_SHORTCUTS = [
|
|
|
1627
1657
|
{ action: "screenshot", label: "Screenshot", defaultKey: "s", key: "s", modifiers: { ctrl: true } },
|
|
1628
1658
|
{ action: "show-shortcuts", label: "Show Shortcuts Help", defaultKey: "?", key: "?" },
|
|
1629
1659
|
{ action: "next-chapter", label: "Next Chapter", defaultKey: "]", key: "]" },
|
|
1630
|
-
{ action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" }
|
|
1660
|
+
{ action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" },
|
|
1661
|
+
{ action: "frame-step-forward", label: "Step Forward One Frame", defaultKey: ".", key: "." },
|
|
1662
|
+
{ action: "frame-step-backward", label: "Step Backward One Frame", defaultKey: ",", key: "," },
|
|
1663
|
+
{ action: "loop-toggle", label: "Toggle Loop", defaultKey: "l", key: "l" },
|
|
1664
|
+
{ action: "ab-loop-cycle", label: "A-B Loop (Set/Cycle)", defaultKey: "r", key: "r" }
|
|
1631
1665
|
];
|
|
1632
1666
|
var STORAGE_KEY = "lightbird-shortcuts";
|
|
1633
1667
|
function loadShortcuts() {
|
|
@@ -1661,7 +1695,14 @@ function matchesShortcut(e, binding) {
|
|
|
1661
1695
|
function isInteractiveElement(el) {
|
|
1662
1696
|
if (!el || !(el instanceof HTMLElement)) return false;
|
|
1663
1697
|
const tag = el.tagName.toLowerCase();
|
|
1664
|
-
|
|
1698
|
+
if (["input", "textarea", "select"].includes(tag)) return true;
|
|
1699
|
+
if (el.contentEditable === "true" || el.getAttribute("contenteditable") !== null) return true;
|
|
1700
|
+
if (typeof el.closest === "function" && el.closest(
|
|
1701
|
+
'[role="dialog"], [role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]'
|
|
1702
|
+
)) {
|
|
1703
|
+
return true;
|
|
1704
|
+
}
|
|
1705
|
+
return false;
|
|
1665
1706
|
}
|
|
1666
1707
|
function formatShortcutKey(binding) {
|
|
1667
1708
|
const mods = [];
|
|
@@ -1738,4 +1779,4 @@ function resetFFmpeg() {
|
|
|
1738
1779
|
loading = null;
|
|
1739
1780
|
}
|
|
1740
1781
|
|
|
1741
|
-
export { ASSRenderer, CancellationError, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, HLSPlayer, MKVPlayer, ProgressEstimator, SimplePlayer, SubtitleConverter, UniversalSubtitleManager, VIDEO_EXTENSIONS, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
1782
|
+
export { ASSRenderer, CancellationError, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, HLSPlayer, MKVPlayer, ProgressEstimator, SimplePlayer, SubtitleConverter, UniversalSubtitleManager, VIDEO_EXTENSIONS, acceptDisclaimer, applyOffsetToVtt, captureFrameAt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, downloadDataUrl, exportPlaylist, exportVideoFrame, extractNativeMetadata, formatShortcutKey, frameExportFilename, getFFmpeg, getLanguageName, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/react/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/react/index.d.cts
CHANGED
|
@@ -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 };
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/react/index.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "0.8.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",
|