@lightbird/core 0.7.0 → 0.9.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 +96 -0
- package/dist/index.d.cts +71 -1
- package/dist/index.d.ts +71 -1
- package/dist/index.js +93 -1
- package/dist/react/index.d.cts +6 -0
- package/dist/react/index.d.ts +6 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -765,14 +765,32 @@ function isHlsUrl(url) {
|
|
|
765
765
|
if (HLS_MIME_HINTS.some((hint) => lower.includes(hint))) return true;
|
|
766
766
|
return lower.split(/[?#]/)[0].endsWith(".m3u8");
|
|
767
767
|
}
|
|
768
|
+
function parseHlsCodec(codec) {
|
|
769
|
+
if (!codec) return null;
|
|
770
|
+
switch (codec.slice(0, 4).toLowerCase()) {
|
|
771
|
+
case "avc1":
|
|
772
|
+
return "H.264 (AVC)";
|
|
773
|
+
case "hvc1":
|
|
774
|
+
case "hev1":
|
|
775
|
+
return "H.265 (HEVC)";
|
|
776
|
+
case "vp09":
|
|
777
|
+
return "VP9";
|
|
778
|
+
case "av01":
|
|
779
|
+
return "AV1";
|
|
780
|
+
default:
|
|
781
|
+
return codec;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
768
784
|
var HLSPlayer = class {
|
|
769
785
|
constructor(url) {
|
|
770
786
|
this.hls = null;
|
|
787
|
+
this.hlsCtor = null;
|
|
771
788
|
this.url = url;
|
|
772
789
|
this.playerFile = { url, qualityLevels: [] };
|
|
773
790
|
}
|
|
774
791
|
async initialize(videoElement) {
|
|
775
792
|
const { default: HlsCtor } = await import('hls.js');
|
|
793
|
+
this.hlsCtor = HlsCtor;
|
|
776
794
|
if (!HlsCtor.isSupported()) {
|
|
777
795
|
videoElement.src = this.url;
|
|
778
796
|
return this.playerFile;
|
|
@@ -818,6 +836,50 @@ var HLSPlayer = class {
|
|
|
818
836
|
setQualityLevel(levelIndex) {
|
|
819
837
|
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
820
838
|
}
|
|
839
|
+
/**
|
|
840
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
841
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
842
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
843
|
+
*/
|
|
844
|
+
getMetadata() {
|
|
845
|
+
const hls = this.hls;
|
|
846
|
+
if (!hls) return {};
|
|
847
|
+
const levels = hls.levels ?? [];
|
|
848
|
+
const activeIndex = hls.currentLevel >= 0 && hls.currentLevel < levels.length ? hls.currentLevel : 0;
|
|
849
|
+
const active = levels[activeIndex];
|
|
850
|
+
return {
|
|
851
|
+
container: "HLS",
|
|
852
|
+
videoCodec: parseHlsCodec(active?.videoCodec),
|
|
853
|
+
videoBitrate: active?.bitrate ?? null,
|
|
854
|
+
streamRenditions: levels.length,
|
|
855
|
+
audioTracks: (hls.audioTracks ?? []).map((track, index) => ({
|
|
856
|
+
index,
|
|
857
|
+
codec: track.audioCodec ?? null,
|
|
858
|
+
channels: null,
|
|
859
|
+
sampleRate: null,
|
|
860
|
+
language: track.lang ?? null,
|
|
861
|
+
bitrate: null
|
|
862
|
+
}))
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
867
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
868
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
869
|
+
*/
|
|
870
|
+
onMetadataChange(callback) {
|
|
871
|
+
const hls = this.hls;
|
|
872
|
+
const HlsCtor = this.hlsCtor;
|
|
873
|
+
if (!hls || !HlsCtor) return () => {
|
|
874
|
+
};
|
|
875
|
+
const events = [
|
|
876
|
+
HlsCtor.Events.MANIFEST_PARSED,
|
|
877
|
+
HlsCtor.Events.LEVEL_SWITCHED,
|
|
878
|
+
HlsCtor.Events.AUDIO_TRACKS_UPDATED
|
|
879
|
+
];
|
|
880
|
+
events.forEach((event) => hls.on(event, callback));
|
|
881
|
+
return () => events.forEach((event) => hls.off(event, callback));
|
|
882
|
+
}
|
|
821
883
|
destroy() {
|
|
822
884
|
if (this.hls) {
|
|
823
885
|
this.hls.destroy();
|
|
@@ -1614,6 +1676,36 @@ async function captureFrameAt(videoEl, timeSeconds, width = 160, height = 90) {
|
|
|
1614
1676
|
});
|
|
1615
1677
|
}
|
|
1616
1678
|
|
|
1679
|
+
// src/utils/frame-export.ts
|
|
1680
|
+
function exportVideoFrame(videoEl, options = {}) {
|
|
1681
|
+
const { type = "image/png", quality, filter } = options;
|
|
1682
|
+
const width = videoEl.videoWidth;
|
|
1683
|
+
const height = videoEl.videoHeight;
|
|
1684
|
+
if (!width || !height) return null;
|
|
1685
|
+
const canvas = document.createElement("canvas");
|
|
1686
|
+
canvas.width = width;
|
|
1687
|
+
canvas.height = height;
|
|
1688
|
+
const ctx = canvas.getContext("2d");
|
|
1689
|
+
if (!ctx) return null;
|
|
1690
|
+
try {
|
|
1691
|
+
if (filter) ctx.filter = filter;
|
|
1692
|
+
ctx.drawImage(videoEl, 0, 0, width, height);
|
|
1693
|
+
return canvas.toDataURL(type, quality);
|
|
1694
|
+
} catch {
|
|
1695
|
+
return null;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function downloadDataUrl(dataUrl, filename) {
|
|
1699
|
+
const a = document.createElement("a");
|
|
1700
|
+
a.href = dataUrl;
|
|
1701
|
+
a.download = filename;
|
|
1702
|
+
a.click();
|
|
1703
|
+
}
|
|
1704
|
+
function frameExportFilename(extension = "png") {
|
|
1705
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
|
|
1706
|
+
return `lightbird-screenshot-${stamp}.${extension}`;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1617
1709
|
// src/utils/keyboard-shortcuts.ts
|
|
1618
1710
|
var DEFAULT_SHORTCUTS = [
|
|
1619
1711
|
{ action: "play-pause", label: "Play / Pause", defaultKey: " ", key: " " },
|
|
@@ -1773,9 +1865,12 @@ exports.configureLightBird = configureLightBird;
|
|
|
1773
1865
|
exports.createOffsetVttUrl = createOffsetVttUrl;
|
|
1774
1866
|
exports.createVideoPlayer = createVideoPlayer;
|
|
1775
1867
|
exports.destroyWebTorrentClient = destroyWebTorrentClient;
|
|
1868
|
+
exports.downloadDataUrl = downloadDataUrl;
|
|
1776
1869
|
exports.exportPlaylist = exportPlaylist;
|
|
1870
|
+
exports.exportVideoFrame = exportVideoFrame;
|
|
1777
1871
|
exports.extractNativeMetadata = extractNativeMetadata;
|
|
1778
1872
|
exports.formatShortcutKey = formatShortcutKey;
|
|
1873
|
+
exports.frameExportFilename = frameExportFilename;
|
|
1779
1874
|
exports.getFFmpeg = getFFmpeg;
|
|
1780
1875
|
exports.getLanguageName = getLanguageName;
|
|
1781
1876
|
exports.getVideoFiles = getVideoFiles;
|
|
@@ -1790,6 +1885,7 @@ exports.loadShortcuts = loadShortcuts;
|
|
|
1790
1885
|
exports.matchesShortcut = matchesShortcut;
|
|
1791
1886
|
exports.parseChaptersFromFFmpegLog = parseChaptersFromFFmpegLog;
|
|
1792
1887
|
exports.parseChaptersFromVtt = parseChaptersFromVtt;
|
|
1888
|
+
exports.parseHlsCodec = parseHlsCodec;
|
|
1793
1889
|
exports.parseM3U8 = parseM3U8;
|
|
1794
1890
|
exports.parseMediaError = parseMediaError;
|
|
1795
1891
|
exports.resetFFmpeg = resetFFmpeg;
|
package/dist/index.d.cts
CHANGED
|
@@ -69,6 +69,8 @@ interface VideoMetadata {
|
|
|
69
69
|
colorSpace: string | null;
|
|
70
70
|
audioTracks: AudioTrackMeta[];
|
|
71
71
|
subtitleTracks: SubtitleTrackMeta[];
|
|
72
|
+
/** Number of HLS/adaptive renditions, when playing an adaptive stream. */
|
|
73
|
+
streamRenditions?: number | null;
|
|
72
74
|
}
|
|
73
75
|
interface AudioTrackMeta {
|
|
74
76
|
index: number;
|
|
@@ -196,16 +198,27 @@ interface VideoPlayer {
|
|
|
196
198
|
* For all other players/paths this is already resolved when initialize() returns.
|
|
197
199
|
*/
|
|
198
200
|
tracksReady?: Promise<void>;
|
|
201
|
+
/** HLS-only: metadata derived from the active rendition (see HLSPlayer). */
|
|
202
|
+
getMetadata?(): Partial<VideoMetadata>;
|
|
203
|
+
/** HLS-only: subscribe to stream events that change metadata. Returns an unsubscribe fn. */
|
|
204
|
+
onMetadataChange?(callback: () => void): () => void;
|
|
199
205
|
}
|
|
200
206
|
declare function createVideoPlayer(source: File | string, externalSubtitles?: File[], onProgress?: (progress: number) => void): VideoPlayer;
|
|
201
207
|
|
|
202
208
|
/** True for HLS playlist URLs — a `.m3u8` path or an HLS MIME hint. */
|
|
203
209
|
declare function isHlsUrl(url: string): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Map an hls.js MIME codec string (e.g. `avc1.640028`) to a human-friendly
|
|
212
|
+
* label by inspecting the leading four-character codec family. Unknown
|
|
213
|
+
* families fall back to the raw string; empty input yields `null`.
|
|
214
|
+
*/
|
|
215
|
+
declare function parseHlsCodec(codec: string | null | undefined): string | null;
|
|
204
216
|
/** Plays HLS (`.m3u8`) streams via hls.js, falling back to native HLS (Safari). */
|
|
205
217
|
declare class HLSPlayer implements VideoPlayer {
|
|
206
218
|
private readonly url;
|
|
207
219
|
private readonly playerFile;
|
|
208
220
|
private hls;
|
|
221
|
+
private hlsCtor;
|
|
209
222
|
constructor(url: string);
|
|
210
223
|
initialize(videoElement: HTMLVideoElement): Promise<HLSPlayerFile>;
|
|
211
224
|
getAudioTracks(): AudioTrack[];
|
|
@@ -216,6 +229,18 @@ declare class HLSPlayer implements VideoPlayer {
|
|
|
216
229
|
getQualityLevels(): QualityLevel[];
|
|
217
230
|
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
218
231
|
setQualityLevel(levelIndex: number): void;
|
|
232
|
+
/**
|
|
233
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
234
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
235
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
236
|
+
*/
|
|
237
|
+
getMetadata(): Partial<VideoMetadata>;
|
|
238
|
+
/**
|
|
239
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
240
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
241
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
242
|
+
*/
|
|
243
|
+
onMetadataChange(callback: () => void): () => void;
|
|
219
244
|
destroy(): void;
|
|
220
245
|
static isCompatible(url: string): boolean;
|
|
221
246
|
}
|
|
@@ -388,6 +413,51 @@ declare function captureVideoThumbnail(videoEl: HTMLVideoElement, atSeconds?: nu
|
|
|
388
413
|
*/
|
|
389
414
|
declare function captureFrameAt(videoEl: HTMLVideoElement, timeSeconds: number, width?: number, height?: number): Promise<string | null>;
|
|
390
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Frame / screenshot export.
|
|
418
|
+
*
|
|
419
|
+
* Captures the currently displayed frame of a playing video element to an
|
|
420
|
+
* image and (optionally) triggers a browser download. Unlike the seek-hover
|
|
421
|
+
* preview in {@link ./video-thumbnail}, this grabs the frame *in place* at the
|
|
422
|
+
* video's native resolution without seeking, so it never disturbs playback.
|
|
423
|
+
*/
|
|
424
|
+
interface ExportFrameOptions {
|
|
425
|
+
/** Output image MIME type. Defaults to `"image/png"`. */
|
|
426
|
+
type?: string;
|
|
427
|
+
/** Quality (0–1) for lossy formats such as jpeg/webp. Ignored for png. */
|
|
428
|
+
quality?: number;
|
|
429
|
+
/**
|
|
430
|
+
* CSS `filter` string to bake into the exported image (e.g. the
|
|
431
|
+
* brightness/contrast filters applied to the video element). When omitted,
|
|
432
|
+
* the raw frame is captured.
|
|
433
|
+
*/
|
|
434
|
+
filter?: string;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Capture the current frame of a video element at its native resolution.
|
|
438
|
+
*
|
|
439
|
+
* @param videoEl - The video element to capture from.
|
|
440
|
+
* @param options - Output format and an optional CSS filter to bake in.
|
|
441
|
+
* @returns An image data URL, or `null` if capture failed — no 2d context, the
|
|
442
|
+
* video has no decoded dimensions yet, or the canvas is tainted by a
|
|
443
|
+
* cross-origin source.
|
|
444
|
+
*/
|
|
445
|
+
declare function exportVideoFrame(videoEl: HTMLVideoElement, options?: ExportFrameOptions): string | null;
|
|
446
|
+
/**
|
|
447
|
+
* Trigger a browser download of a data URL by synthesizing an anchor click.
|
|
448
|
+
*
|
|
449
|
+
* @param dataUrl - The data (or object) URL to download.
|
|
450
|
+
* @param filename - The suggested filename for the saved file.
|
|
451
|
+
*/
|
|
452
|
+
declare function downloadDataUrl(dataUrl: string, filename: string): void;
|
|
453
|
+
/**
|
|
454
|
+
* Build a timestamped default filename for an exported frame.
|
|
455
|
+
*
|
|
456
|
+
* @param extension - File extension without a leading dot. Defaults to `"png"`.
|
|
457
|
+
* @returns e.g. `lightbird-screenshot-2026-05-30T12-00-00-000Z.png`.
|
|
458
|
+
*/
|
|
459
|
+
declare function frameExportFilename(extension?: string): string;
|
|
460
|
+
|
|
391
461
|
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
462
|
interface ShortcutBinding {
|
|
393
463
|
action: ShortcutAction;
|
|
@@ -450,4 +520,4 @@ declare function resetFFmpeg(): void;
|
|
|
450
520
|
*/
|
|
451
521
|
declare function getLanguageName(code?: string | null): string | undefined;
|
|
452
522
|
|
|
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 };
|
|
523
|
+
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, parseHlsCodec, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ interface VideoMetadata {
|
|
|
69
69
|
colorSpace: string | null;
|
|
70
70
|
audioTracks: AudioTrackMeta[];
|
|
71
71
|
subtitleTracks: SubtitleTrackMeta[];
|
|
72
|
+
/** Number of HLS/adaptive renditions, when playing an adaptive stream. */
|
|
73
|
+
streamRenditions?: number | null;
|
|
72
74
|
}
|
|
73
75
|
interface AudioTrackMeta {
|
|
74
76
|
index: number;
|
|
@@ -196,16 +198,27 @@ interface VideoPlayer {
|
|
|
196
198
|
* For all other players/paths this is already resolved when initialize() returns.
|
|
197
199
|
*/
|
|
198
200
|
tracksReady?: Promise<void>;
|
|
201
|
+
/** HLS-only: metadata derived from the active rendition (see HLSPlayer). */
|
|
202
|
+
getMetadata?(): Partial<VideoMetadata>;
|
|
203
|
+
/** HLS-only: subscribe to stream events that change metadata. Returns an unsubscribe fn. */
|
|
204
|
+
onMetadataChange?(callback: () => void): () => void;
|
|
199
205
|
}
|
|
200
206
|
declare function createVideoPlayer(source: File | string, externalSubtitles?: File[], onProgress?: (progress: number) => void): VideoPlayer;
|
|
201
207
|
|
|
202
208
|
/** True for HLS playlist URLs — a `.m3u8` path or an HLS MIME hint. */
|
|
203
209
|
declare function isHlsUrl(url: string): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Map an hls.js MIME codec string (e.g. `avc1.640028`) to a human-friendly
|
|
212
|
+
* label by inspecting the leading four-character codec family. Unknown
|
|
213
|
+
* families fall back to the raw string; empty input yields `null`.
|
|
214
|
+
*/
|
|
215
|
+
declare function parseHlsCodec(codec: string | null | undefined): string | null;
|
|
204
216
|
/** Plays HLS (`.m3u8`) streams via hls.js, falling back to native HLS (Safari). */
|
|
205
217
|
declare class HLSPlayer implements VideoPlayer {
|
|
206
218
|
private readonly url;
|
|
207
219
|
private readonly playerFile;
|
|
208
220
|
private hls;
|
|
221
|
+
private hlsCtor;
|
|
209
222
|
constructor(url: string);
|
|
210
223
|
initialize(videoElement: HTMLVideoElement): Promise<HLSPlayerFile>;
|
|
211
224
|
getAudioTracks(): AudioTrack[];
|
|
@@ -216,6 +229,18 @@ declare class HLSPlayer implements VideoPlayer {
|
|
|
216
229
|
getQualityLevels(): QualityLevel[];
|
|
217
230
|
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
218
231
|
setQualityLevel(levelIndex: number): void;
|
|
232
|
+
/**
|
|
233
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
234
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
235
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
236
|
+
*/
|
|
237
|
+
getMetadata(): Partial<VideoMetadata>;
|
|
238
|
+
/**
|
|
239
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
240
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
241
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
242
|
+
*/
|
|
243
|
+
onMetadataChange(callback: () => void): () => void;
|
|
219
244
|
destroy(): void;
|
|
220
245
|
static isCompatible(url: string): boolean;
|
|
221
246
|
}
|
|
@@ -388,6 +413,51 @@ declare function captureVideoThumbnail(videoEl: HTMLVideoElement, atSeconds?: nu
|
|
|
388
413
|
*/
|
|
389
414
|
declare function captureFrameAt(videoEl: HTMLVideoElement, timeSeconds: number, width?: number, height?: number): Promise<string | null>;
|
|
390
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Frame / screenshot export.
|
|
418
|
+
*
|
|
419
|
+
* Captures the currently displayed frame of a playing video element to an
|
|
420
|
+
* image and (optionally) triggers a browser download. Unlike the seek-hover
|
|
421
|
+
* preview in {@link ./video-thumbnail}, this grabs the frame *in place* at the
|
|
422
|
+
* video's native resolution without seeking, so it never disturbs playback.
|
|
423
|
+
*/
|
|
424
|
+
interface ExportFrameOptions {
|
|
425
|
+
/** Output image MIME type. Defaults to `"image/png"`. */
|
|
426
|
+
type?: string;
|
|
427
|
+
/** Quality (0–1) for lossy formats such as jpeg/webp. Ignored for png. */
|
|
428
|
+
quality?: number;
|
|
429
|
+
/**
|
|
430
|
+
* CSS `filter` string to bake into the exported image (e.g. the
|
|
431
|
+
* brightness/contrast filters applied to the video element). When omitted,
|
|
432
|
+
* the raw frame is captured.
|
|
433
|
+
*/
|
|
434
|
+
filter?: string;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Capture the current frame of a video element at its native resolution.
|
|
438
|
+
*
|
|
439
|
+
* @param videoEl - The video element to capture from.
|
|
440
|
+
* @param options - Output format and an optional CSS filter to bake in.
|
|
441
|
+
* @returns An image data URL, or `null` if capture failed — no 2d context, the
|
|
442
|
+
* video has no decoded dimensions yet, or the canvas is tainted by a
|
|
443
|
+
* cross-origin source.
|
|
444
|
+
*/
|
|
445
|
+
declare function exportVideoFrame(videoEl: HTMLVideoElement, options?: ExportFrameOptions): string | null;
|
|
446
|
+
/**
|
|
447
|
+
* Trigger a browser download of a data URL by synthesizing an anchor click.
|
|
448
|
+
*
|
|
449
|
+
* @param dataUrl - The data (or object) URL to download.
|
|
450
|
+
* @param filename - The suggested filename for the saved file.
|
|
451
|
+
*/
|
|
452
|
+
declare function downloadDataUrl(dataUrl: string, filename: string): void;
|
|
453
|
+
/**
|
|
454
|
+
* Build a timestamped default filename for an exported frame.
|
|
455
|
+
*
|
|
456
|
+
* @param extension - File extension without a leading dot. Defaults to `"png"`.
|
|
457
|
+
* @returns e.g. `lightbird-screenshot-2026-05-30T12-00-00-000Z.png`.
|
|
458
|
+
*/
|
|
459
|
+
declare function frameExportFilename(extension?: string): string;
|
|
460
|
+
|
|
391
461
|
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
462
|
interface ShortcutBinding {
|
|
393
463
|
action: ShortcutAction;
|
|
@@ -450,4 +520,4 @@ declare function resetFFmpeg(): void;
|
|
|
450
520
|
*/
|
|
451
521
|
declare function getLanguageName(code?: string | null): string | undefined;
|
|
452
522
|
|
|
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 };
|
|
523
|
+
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, parseHlsCodec, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.js
CHANGED
|
@@ -762,14 +762,32 @@ function isHlsUrl(url) {
|
|
|
762
762
|
if (HLS_MIME_HINTS.some((hint) => lower.includes(hint))) return true;
|
|
763
763
|
return lower.split(/[?#]/)[0].endsWith(".m3u8");
|
|
764
764
|
}
|
|
765
|
+
function parseHlsCodec(codec) {
|
|
766
|
+
if (!codec) return null;
|
|
767
|
+
switch (codec.slice(0, 4).toLowerCase()) {
|
|
768
|
+
case "avc1":
|
|
769
|
+
return "H.264 (AVC)";
|
|
770
|
+
case "hvc1":
|
|
771
|
+
case "hev1":
|
|
772
|
+
return "H.265 (HEVC)";
|
|
773
|
+
case "vp09":
|
|
774
|
+
return "VP9";
|
|
775
|
+
case "av01":
|
|
776
|
+
return "AV1";
|
|
777
|
+
default:
|
|
778
|
+
return codec;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
765
781
|
var HLSPlayer = class {
|
|
766
782
|
constructor(url) {
|
|
767
783
|
this.hls = null;
|
|
784
|
+
this.hlsCtor = null;
|
|
768
785
|
this.url = url;
|
|
769
786
|
this.playerFile = { url, qualityLevels: [] };
|
|
770
787
|
}
|
|
771
788
|
async initialize(videoElement) {
|
|
772
789
|
const { default: HlsCtor } = await import('hls.js');
|
|
790
|
+
this.hlsCtor = HlsCtor;
|
|
773
791
|
if (!HlsCtor.isSupported()) {
|
|
774
792
|
videoElement.src = this.url;
|
|
775
793
|
return this.playerFile;
|
|
@@ -815,6 +833,50 @@ var HLSPlayer = class {
|
|
|
815
833
|
setQualityLevel(levelIndex) {
|
|
816
834
|
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
817
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* HLS-specific metadata for the video info panel, derived from the active
|
|
838
|
+
* rendition. Returns an empty object before the manifest loads or on the
|
|
839
|
+
* native-HLS path (where hls.js is never instantiated).
|
|
840
|
+
*/
|
|
841
|
+
getMetadata() {
|
|
842
|
+
const hls = this.hls;
|
|
843
|
+
if (!hls) return {};
|
|
844
|
+
const levels = hls.levels ?? [];
|
|
845
|
+
const activeIndex = hls.currentLevel >= 0 && hls.currentLevel < levels.length ? hls.currentLevel : 0;
|
|
846
|
+
const active = levels[activeIndex];
|
|
847
|
+
return {
|
|
848
|
+
container: "HLS",
|
|
849
|
+
videoCodec: parseHlsCodec(active?.videoCodec),
|
|
850
|
+
videoBitrate: active?.bitrate ?? null,
|
|
851
|
+
streamRenditions: levels.length,
|
|
852
|
+
audioTracks: (hls.audioTracks ?? []).map((track, index) => ({
|
|
853
|
+
index,
|
|
854
|
+
codec: track.audioCodec ?? null,
|
|
855
|
+
channels: null,
|
|
856
|
+
sampleRate: null,
|
|
857
|
+
language: track.lang ?? null,
|
|
858
|
+
bitrate: null
|
|
859
|
+
}))
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Subscribe to HLS events that change the info-panel metadata (manifest
|
|
864
|
+
* parsed, quality level switched, audio tracks updated). Returns an
|
|
865
|
+
* unsubscribe function. A no-op on the native-HLS path.
|
|
866
|
+
*/
|
|
867
|
+
onMetadataChange(callback) {
|
|
868
|
+
const hls = this.hls;
|
|
869
|
+
const HlsCtor = this.hlsCtor;
|
|
870
|
+
if (!hls || !HlsCtor) return () => {
|
|
871
|
+
};
|
|
872
|
+
const events = [
|
|
873
|
+
HlsCtor.Events.MANIFEST_PARSED,
|
|
874
|
+
HlsCtor.Events.LEVEL_SWITCHED,
|
|
875
|
+
HlsCtor.Events.AUDIO_TRACKS_UPDATED
|
|
876
|
+
];
|
|
877
|
+
events.forEach((event) => hls.on(event, callback));
|
|
878
|
+
return () => events.forEach((event) => hls.off(event, callback));
|
|
879
|
+
}
|
|
818
880
|
destroy() {
|
|
819
881
|
if (this.hls) {
|
|
820
882
|
this.hls.destroy();
|
|
@@ -1611,6 +1673,36 @@ async function captureFrameAt(videoEl, timeSeconds, width = 160, height = 90) {
|
|
|
1611
1673
|
});
|
|
1612
1674
|
}
|
|
1613
1675
|
|
|
1676
|
+
// src/utils/frame-export.ts
|
|
1677
|
+
function exportVideoFrame(videoEl, options = {}) {
|
|
1678
|
+
const { type = "image/png", quality, filter } = options;
|
|
1679
|
+
const width = videoEl.videoWidth;
|
|
1680
|
+
const height = videoEl.videoHeight;
|
|
1681
|
+
if (!width || !height) return null;
|
|
1682
|
+
const canvas = document.createElement("canvas");
|
|
1683
|
+
canvas.width = width;
|
|
1684
|
+
canvas.height = height;
|
|
1685
|
+
const ctx = canvas.getContext("2d");
|
|
1686
|
+
if (!ctx) return null;
|
|
1687
|
+
try {
|
|
1688
|
+
if (filter) ctx.filter = filter;
|
|
1689
|
+
ctx.drawImage(videoEl, 0, 0, width, height);
|
|
1690
|
+
return canvas.toDataURL(type, quality);
|
|
1691
|
+
} catch {
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function downloadDataUrl(dataUrl, filename) {
|
|
1696
|
+
const a = document.createElement("a");
|
|
1697
|
+
a.href = dataUrl;
|
|
1698
|
+
a.download = filename;
|
|
1699
|
+
a.click();
|
|
1700
|
+
}
|
|
1701
|
+
function frameExportFilename(extension = "png") {
|
|
1702
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
|
|
1703
|
+
return `lightbird-screenshot-${stamp}.${extension}`;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1614
1706
|
// src/utils/keyboard-shortcuts.ts
|
|
1615
1707
|
var DEFAULT_SHORTCUTS = [
|
|
1616
1708
|
{ action: "play-pause", label: "Play / Pause", defaultKey: " ", key: " " },
|
|
@@ -1749,4 +1841,4 @@ function resetFFmpeg() {
|
|
|
1749
1841
|
loading = null;
|
|
1750
1842
|
}
|
|
1751
1843
|
|
|
1752
|
-
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 };
|
|
1844
|
+
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, parseHlsCodec, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/react/index.d.cts
CHANGED
|
@@ -81,6 +81,8 @@ interface VideoMetadata {
|
|
|
81
81
|
colorSpace: string | null;
|
|
82
82
|
audioTracks: AudioTrackMeta[];
|
|
83
83
|
subtitleTracks: SubtitleTrackMeta[];
|
|
84
|
+
/** Number of HLS/adaptive renditions, when playing an adaptive stream. */
|
|
85
|
+
streamRenditions?: number | null;
|
|
84
86
|
}
|
|
85
87
|
interface AudioTrackMeta {
|
|
86
88
|
index: number;
|
|
@@ -264,6 +266,10 @@ interface VideoPlayer {
|
|
|
264
266
|
* For all other players/paths this is already resolved when initialize() returns.
|
|
265
267
|
*/
|
|
266
268
|
tracksReady?: Promise<void>;
|
|
269
|
+
/** HLS-only: metadata derived from the active rendition (see HLSPlayer). */
|
|
270
|
+
getMetadata?(): Partial<VideoMetadata>;
|
|
271
|
+
/** HLS-only: subscribe to stream events that change metadata. Returns an unsubscribe fn. */
|
|
272
|
+
onMetadataChange?(callback: () => void): () => void;
|
|
267
273
|
}
|
|
268
274
|
|
|
269
275
|
declare function useChapters(videoRef: RefObject<HTMLVideoElement>, playerRef: RefObject<VideoPlayer | null>): {
|
package/dist/react/index.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ interface VideoMetadata {
|
|
|
81
81
|
colorSpace: string | null;
|
|
82
82
|
audioTracks: AudioTrackMeta[];
|
|
83
83
|
subtitleTracks: SubtitleTrackMeta[];
|
|
84
|
+
/** Number of HLS/adaptive renditions, when playing an adaptive stream. */
|
|
85
|
+
streamRenditions?: number | null;
|
|
84
86
|
}
|
|
85
87
|
interface AudioTrackMeta {
|
|
86
88
|
index: number;
|
|
@@ -264,6 +266,10 @@ interface VideoPlayer {
|
|
|
264
266
|
* For all other players/paths this is already resolved when initialize() returns.
|
|
265
267
|
*/
|
|
266
268
|
tracksReady?: Promise<void>;
|
|
269
|
+
/** HLS-only: metadata derived from the active rendition (see HLSPlayer). */
|
|
270
|
+
getMetadata?(): Partial<VideoMetadata>;
|
|
271
|
+
/** HLS-only: subscribe to stream events that change metadata. Returns an unsubscribe fn. */
|
|
272
|
+
onMetadataChange?(callback: () => void): () => void;
|
|
267
273
|
}
|
|
268
274
|
|
|
269
275
|
declare function useChapters(videoRef: RefObject<HTMLVideoElement>, playerRef: RefObject<VideoPlayer | null>): {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightbird/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|