@lightbird/core 0.3.1 → 0.5.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 +144 -6
- package/dist/index.d.cts +51 -3
- package/dist/index.d.ts +51 -3
- package/dist/index.js +142 -7
- package/dist/react/index.cjs +313 -0
- package/dist/react/index.d.cts +114 -2
- package/dist/react/index.d.ts +114 -2
- package/dist/react/index.js +311 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -611,6 +611,78 @@ _MKVPlayer._canPlayNatively = canPlayNatively;
|
|
|
611
611
|
_MKVPlayer._workerFactory = null;
|
|
612
612
|
var MKVPlayer = _MKVPlayer;
|
|
613
613
|
|
|
614
|
+
// src/players/hls-player.ts
|
|
615
|
+
var HLS_MIME_HINTS = ["application/x-mpegurl", "application/vnd.apple.mpegurl"];
|
|
616
|
+
function isHlsUrl(url) {
|
|
617
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
618
|
+
const lower = url.toLowerCase();
|
|
619
|
+
if (HLS_MIME_HINTS.some((hint) => lower.includes(hint))) return true;
|
|
620
|
+
return lower.split(/[?#]/)[0].endsWith(".m3u8");
|
|
621
|
+
}
|
|
622
|
+
var HLSPlayer = class {
|
|
623
|
+
constructor(url) {
|
|
624
|
+
this.hls = null;
|
|
625
|
+
this.url = url;
|
|
626
|
+
this.playerFile = { url, qualityLevels: [] };
|
|
627
|
+
}
|
|
628
|
+
async initialize(videoElement) {
|
|
629
|
+
const { default: HlsCtor } = await import('hls.js');
|
|
630
|
+
if (!HlsCtor.isSupported()) {
|
|
631
|
+
videoElement.src = this.url;
|
|
632
|
+
return this.playerFile;
|
|
633
|
+
}
|
|
634
|
+
const hls = new HlsCtor();
|
|
635
|
+
this.hls = hls;
|
|
636
|
+
hls.loadSource(this.url);
|
|
637
|
+
hls.attachMedia(videoElement);
|
|
638
|
+
return this.playerFile;
|
|
639
|
+
}
|
|
640
|
+
getAudioTracks() {
|
|
641
|
+
if (!this.hls) return [];
|
|
642
|
+
return this.hls.audioTracks.map((track) => ({
|
|
643
|
+
id: String(track.id),
|
|
644
|
+
name: track.name,
|
|
645
|
+
lang: track.lang || "unknown"
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
getSubtitles() {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
switchAudioTrack(trackId) {
|
|
652
|
+
if (this.hls) {
|
|
653
|
+
const id = Number(trackId);
|
|
654
|
+
if (!Number.isNaN(id)) this.hls.audioTrack = id;
|
|
655
|
+
}
|
|
656
|
+
return Promise.resolve();
|
|
657
|
+
}
|
|
658
|
+
switchSubtitle() {
|
|
659
|
+
return Promise.resolve();
|
|
660
|
+
}
|
|
661
|
+
/** Quality renditions — not on `VideoPlayer`; read directly by the quality hook. */
|
|
662
|
+
getQualityLevels() {
|
|
663
|
+
if (!this.hls) return [];
|
|
664
|
+
return this.hls.levels.map((level, index) => ({
|
|
665
|
+
index,
|
|
666
|
+
height: level.height,
|
|
667
|
+
bitrate: level.bitrate,
|
|
668
|
+
name: level.name || (level.height > 0 ? `${level.height}p` : `Level ${index + 1}`)
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
671
|
+
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
672
|
+
setQualityLevel(levelIndex) {
|
|
673
|
+
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
674
|
+
}
|
|
675
|
+
destroy() {
|
|
676
|
+
if (this.hls) {
|
|
677
|
+
this.hls.destroy();
|
|
678
|
+
this.hls = null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
static isCompatible(url) {
|
|
682
|
+
return isHlsUrl(url);
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
614
686
|
// src/video-processor.ts
|
|
615
687
|
var SimplePlayerAdapter = class {
|
|
616
688
|
constructor(file, externalSubtitles = []) {
|
|
@@ -667,13 +739,21 @@ var MKVPlayerAdapter = class {
|
|
|
667
739
|
return this.player.tracksReady;
|
|
668
740
|
}
|
|
669
741
|
};
|
|
670
|
-
function createVideoPlayer(
|
|
671
|
-
if (
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
742
|
+
function createVideoPlayer(source, externalSubtitles = [], onProgress) {
|
|
743
|
+
if (typeof source === "string") {
|
|
744
|
+
if (isHlsUrl(source)) {
|
|
745
|
+
return new HLSPlayer(source);
|
|
746
|
+
}
|
|
747
|
+
throw new Error(
|
|
748
|
+
`createVideoPlayer: unsupported URL "${source}" \u2014 only HLS (.m3u8) stream URLs are supported; pass a File for other formats.`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (MKVPlayer.isCompatible(source)) {
|
|
752
|
+
return new MKVPlayerAdapter(source, onProgress);
|
|
753
|
+
} else if (SimplePlayer.isCompatible(source)) {
|
|
754
|
+
return new SimplePlayerAdapter(source, externalSubtitles);
|
|
675
755
|
} else {
|
|
676
|
-
return new SimplePlayerAdapter(
|
|
756
|
+
return new SimplePlayerAdapter(source, externalSubtitles);
|
|
677
757
|
}
|
|
678
758
|
}
|
|
679
759
|
|
|
@@ -1332,6 +1412,61 @@ async function captureVideoThumbnail(videoEl, atSeconds = 5) {
|
|
|
1332
1412
|
}
|
|
1333
1413
|
});
|
|
1334
1414
|
}
|
|
1415
|
+
async function captureFrameAt(videoEl, timeSeconds, width = 160, height = 90) {
|
|
1416
|
+
return new Promise((resolve) => {
|
|
1417
|
+
const canvas = document.createElement("canvas");
|
|
1418
|
+
canvas.width = width;
|
|
1419
|
+
canvas.height = height;
|
|
1420
|
+
const ctx = canvas.getContext("2d");
|
|
1421
|
+
if (!ctx) {
|
|
1422
|
+
resolve(null);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
let settled = false;
|
|
1426
|
+
const cleanup = () => {
|
|
1427
|
+
videoEl.removeEventListener("seeked", onSeeked);
|
|
1428
|
+
videoEl.removeEventListener("error", onError);
|
|
1429
|
+
videoEl.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
1430
|
+
};
|
|
1431
|
+
const finish = (value) => {
|
|
1432
|
+
if (settled) return;
|
|
1433
|
+
settled = true;
|
|
1434
|
+
cleanup();
|
|
1435
|
+
resolve(value);
|
|
1436
|
+
};
|
|
1437
|
+
const draw = () => {
|
|
1438
|
+
try {
|
|
1439
|
+
ctx.drawImage(videoEl, 0, 0, width, height);
|
|
1440
|
+
finish(canvas.toDataURL("image/jpeg", 0.6));
|
|
1441
|
+
} catch {
|
|
1442
|
+
finish(null);
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
const seekTo = () => {
|
|
1446
|
+
const duration = videoEl.duration || 0;
|
|
1447
|
+
const target = duration > 0 ? Math.max(0, Math.min(timeSeconds, duration)) : Math.max(0, timeSeconds);
|
|
1448
|
+
if (Math.abs(videoEl.currentTime - target) < 0.05) {
|
|
1449
|
+
draw();
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
videoEl.currentTime = target;
|
|
1454
|
+
} catch {
|
|
1455
|
+
finish(null);
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
const onSeeked = () => draw();
|
|
1459
|
+
const onError = () => finish(null);
|
|
1460
|
+
const onLoadedMetadata = () => seekTo();
|
|
1461
|
+
videoEl.addEventListener("seeked", onSeeked);
|
|
1462
|
+
videoEl.addEventListener("error", onError);
|
|
1463
|
+
if (videoEl.readyState >= 1) {
|
|
1464
|
+
seekTo();
|
|
1465
|
+
} else {
|
|
1466
|
+
videoEl.addEventListener("loadedmetadata", onLoadedMetadata, { once: true });
|
|
1467
|
+
}
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1335
1470
|
|
|
1336
1471
|
// src/utils/keyboard-shortcuts.ts
|
|
1337
1472
|
var DEFAULT_SHORTCUTS = [
|
|
@@ -1466,6 +1601,7 @@ exports.DEFAULT_SHORTCUTS = DEFAULT_SHORTCUTS;
|
|
|
1466
1601
|
exports.DEFAULT_TRACKERS = DEFAULT_TRACKERS;
|
|
1467
1602
|
exports.DISCLAIMER_KEY = DISCLAIMER_KEY;
|
|
1468
1603
|
exports.FLAG_MAGNET_LINK = FLAG_MAGNET_LINK;
|
|
1604
|
+
exports.HLSPlayer = HLSPlayer;
|
|
1469
1605
|
exports.MKVPlayer = MKVPlayer;
|
|
1470
1606
|
exports.ProgressEstimator = ProgressEstimator;
|
|
1471
1607
|
exports.SimplePlayer = SimplePlayer;
|
|
@@ -1474,6 +1610,7 @@ exports.UniversalSubtitleManager = UniversalSubtitleManager;
|
|
|
1474
1610
|
exports.VIDEO_EXTENSIONS = VIDEO_EXTENSIONS;
|
|
1475
1611
|
exports.acceptDisclaimer = acceptDisclaimer;
|
|
1476
1612
|
exports.applyOffsetToVtt = applyOffsetToVtt;
|
|
1613
|
+
exports.captureFrameAt = captureFrameAt;
|
|
1477
1614
|
exports.captureVideoThumbnail = captureVideoThumbnail;
|
|
1478
1615
|
exports.configureLightBird = configureLightBird;
|
|
1479
1616
|
exports.createOffsetVttUrl = createOffsetVttUrl;
|
|
@@ -1487,6 +1624,7 @@ exports.getVideoFiles = getVideoFiles;
|
|
|
1487
1624
|
exports.getWebTorrentClient = getWebTorrentClient;
|
|
1488
1625
|
exports.hasAcceptedDisclaimer = hasAcceptedDisclaimer;
|
|
1489
1626
|
exports.initFeatureFlags = initFeatureFlags;
|
|
1627
|
+
exports.isHlsUrl = isHlsUrl;
|
|
1490
1628
|
exports.isInteractiveElement = isInteractiveElement;
|
|
1491
1629
|
exports.isMagnetUri = isMagnetUri;
|
|
1492
1630
|
exports.isVideoFile = isVideoFile;
|
package/dist/index.d.cts
CHANGED
|
@@ -83,6 +83,18 @@ interface SubtitleTrackMeta {
|
|
|
83
83
|
format: string | null;
|
|
84
84
|
language: string | null;
|
|
85
85
|
}
|
|
86
|
+
/** A single HLS quality rendition (e.g. 1080p, 720p). */
|
|
87
|
+
interface QualityLevel {
|
|
88
|
+
index: number;
|
|
89
|
+
height: number;
|
|
90
|
+
bitrate: number;
|
|
91
|
+
name: string;
|
|
92
|
+
}
|
|
93
|
+
/** Result of initialising an HLS stream via `HLSPlayer`. */
|
|
94
|
+
interface HLSPlayerFile {
|
|
95
|
+
url: string;
|
|
96
|
+
qualityLevels: QualityLevel[];
|
|
97
|
+
}
|
|
86
98
|
|
|
87
99
|
interface SimplePlayerFile {
|
|
88
100
|
name: string;
|
|
@@ -168,7 +180,7 @@ declare class MKVPlayer {
|
|
|
168
180
|
static isCompatible(file: File): boolean;
|
|
169
181
|
}
|
|
170
182
|
|
|
171
|
-
type ProcessedFile = SimplePlayerFile | MKVPlayerFile;
|
|
183
|
+
type ProcessedFile = SimplePlayerFile | MKVPlayerFile | HLSPlayerFile;
|
|
172
184
|
interface VideoPlayer {
|
|
173
185
|
initialize(videoElement: HTMLVideoElement): Promise<ProcessedFile>;
|
|
174
186
|
getAudioTracks(): AudioTrack[];
|
|
@@ -185,7 +197,28 @@ interface VideoPlayer {
|
|
|
185
197
|
*/
|
|
186
198
|
tracksReady?: Promise<void>;
|
|
187
199
|
}
|
|
188
|
-
declare function createVideoPlayer(
|
|
200
|
+
declare function createVideoPlayer(source: File | string, externalSubtitles?: File[], onProgress?: (progress: number) => void): VideoPlayer;
|
|
201
|
+
|
|
202
|
+
/** True for HLS playlist URLs — a `.m3u8` path or an HLS MIME hint. */
|
|
203
|
+
declare function isHlsUrl(url: string): boolean;
|
|
204
|
+
/** Plays HLS (`.m3u8`) streams via hls.js, falling back to native HLS (Safari). */
|
|
205
|
+
declare class HLSPlayer implements VideoPlayer {
|
|
206
|
+
private readonly url;
|
|
207
|
+
private readonly playerFile;
|
|
208
|
+
private hls;
|
|
209
|
+
constructor(url: string);
|
|
210
|
+
initialize(videoElement: HTMLVideoElement): Promise<HLSPlayerFile>;
|
|
211
|
+
getAudioTracks(): AudioTrack[];
|
|
212
|
+
getSubtitles(): Subtitle[];
|
|
213
|
+
switchAudioTrack(trackId: string): Promise<void>;
|
|
214
|
+
switchSubtitle(): Promise<void>;
|
|
215
|
+
/** Quality renditions — not on `VideoPlayer`; read directly by the quality hook. */
|
|
216
|
+
getQualityLevels(): QualityLevel[];
|
|
217
|
+
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
218
|
+
setQualityLevel(levelIndex: number): void;
|
|
219
|
+
destroy(): void;
|
|
220
|
+
static isCompatible(url: string): boolean;
|
|
221
|
+
}
|
|
189
222
|
|
|
190
223
|
declare class UniversalSubtitleManager {
|
|
191
224
|
private records;
|
|
@@ -339,6 +372,21 @@ declare function validateFile(file: File): {
|
|
|
339
372
|
declare function extractNativeMetadata(videoEl: HTMLVideoElement, file?: File): Partial<VideoMetadata>;
|
|
340
373
|
|
|
341
374
|
declare function captureVideoThumbnail(videoEl: HTMLVideoElement, atSeconds?: number): Promise<string | null>;
|
|
375
|
+
/**
|
|
376
|
+
* Capture a single video frame at a specific timestamp.
|
|
377
|
+
*
|
|
378
|
+
* Unlike {@link captureVideoThumbnail}, this does not save/restore
|
|
379
|
+
* `currentTime` — it is built for a dedicated, offscreen preview video so the
|
|
380
|
+
* main playback element is never disturbed. Powers seek-bar hover previews.
|
|
381
|
+
*
|
|
382
|
+
* @param videoEl - A video element (typically offscreen) to seek and capture.
|
|
383
|
+
* @param timeSeconds - Timestamp to capture, clamped to the video duration.
|
|
384
|
+
* @param width - Output thumbnail width in pixels.
|
|
385
|
+
* @param height - Output thumbnail height in pixels.
|
|
386
|
+
* @returns A JPEG data URL, or `null` if capture failed (no 2d context, a
|
|
387
|
+
* tainted canvas, or a media error).
|
|
388
|
+
*/
|
|
389
|
+
declare function captureFrameAt(videoEl: HTMLVideoElement, timeSeconds: number, width?: number, height?: number): Promise<string | null>;
|
|
342
390
|
|
|
343
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';
|
|
344
392
|
interface ShortcutBinding {
|
|
@@ -393,4 +441,4 @@ declare class ProgressEstimator {
|
|
|
393
441
|
declare function getFFmpeg(): Promise<FFmpeg>;
|
|
394
442
|
declare function resetFFmpeg(): void;
|
|
395
443
|
|
|
396
|
-
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, 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, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
444
|
+
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, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.d.ts
CHANGED
|
@@ -83,6 +83,18 @@ interface SubtitleTrackMeta {
|
|
|
83
83
|
format: string | null;
|
|
84
84
|
language: string | null;
|
|
85
85
|
}
|
|
86
|
+
/** A single HLS quality rendition (e.g. 1080p, 720p). */
|
|
87
|
+
interface QualityLevel {
|
|
88
|
+
index: number;
|
|
89
|
+
height: number;
|
|
90
|
+
bitrate: number;
|
|
91
|
+
name: string;
|
|
92
|
+
}
|
|
93
|
+
/** Result of initialising an HLS stream via `HLSPlayer`. */
|
|
94
|
+
interface HLSPlayerFile {
|
|
95
|
+
url: string;
|
|
96
|
+
qualityLevels: QualityLevel[];
|
|
97
|
+
}
|
|
86
98
|
|
|
87
99
|
interface SimplePlayerFile {
|
|
88
100
|
name: string;
|
|
@@ -168,7 +180,7 @@ declare class MKVPlayer {
|
|
|
168
180
|
static isCompatible(file: File): boolean;
|
|
169
181
|
}
|
|
170
182
|
|
|
171
|
-
type ProcessedFile = SimplePlayerFile | MKVPlayerFile;
|
|
183
|
+
type ProcessedFile = SimplePlayerFile | MKVPlayerFile | HLSPlayerFile;
|
|
172
184
|
interface VideoPlayer {
|
|
173
185
|
initialize(videoElement: HTMLVideoElement): Promise<ProcessedFile>;
|
|
174
186
|
getAudioTracks(): AudioTrack[];
|
|
@@ -185,7 +197,28 @@ interface VideoPlayer {
|
|
|
185
197
|
*/
|
|
186
198
|
tracksReady?: Promise<void>;
|
|
187
199
|
}
|
|
188
|
-
declare function createVideoPlayer(
|
|
200
|
+
declare function createVideoPlayer(source: File | string, externalSubtitles?: File[], onProgress?: (progress: number) => void): VideoPlayer;
|
|
201
|
+
|
|
202
|
+
/** True for HLS playlist URLs — a `.m3u8` path or an HLS MIME hint. */
|
|
203
|
+
declare function isHlsUrl(url: string): boolean;
|
|
204
|
+
/** Plays HLS (`.m3u8`) streams via hls.js, falling back to native HLS (Safari). */
|
|
205
|
+
declare class HLSPlayer implements VideoPlayer {
|
|
206
|
+
private readonly url;
|
|
207
|
+
private readonly playerFile;
|
|
208
|
+
private hls;
|
|
209
|
+
constructor(url: string);
|
|
210
|
+
initialize(videoElement: HTMLVideoElement): Promise<HLSPlayerFile>;
|
|
211
|
+
getAudioTracks(): AudioTrack[];
|
|
212
|
+
getSubtitles(): Subtitle[];
|
|
213
|
+
switchAudioTrack(trackId: string): Promise<void>;
|
|
214
|
+
switchSubtitle(): Promise<void>;
|
|
215
|
+
/** Quality renditions — not on `VideoPlayer`; read directly by the quality hook. */
|
|
216
|
+
getQualityLevels(): QualityLevel[];
|
|
217
|
+
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
218
|
+
setQualityLevel(levelIndex: number): void;
|
|
219
|
+
destroy(): void;
|
|
220
|
+
static isCompatible(url: string): boolean;
|
|
221
|
+
}
|
|
189
222
|
|
|
190
223
|
declare class UniversalSubtitleManager {
|
|
191
224
|
private records;
|
|
@@ -339,6 +372,21 @@ declare function validateFile(file: File): {
|
|
|
339
372
|
declare function extractNativeMetadata(videoEl: HTMLVideoElement, file?: File): Partial<VideoMetadata>;
|
|
340
373
|
|
|
341
374
|
declare function captureVideoThumbnail(videoEl: HTMLVideoElement, atSeconds?: number): Promise<string | null>;
|
|
375
|
+
/**
|
|
376
|
+
* Capture a single video frame at a specific timestamp.
|
|
377
|
+
*
|
|
378
|
+
* Unlike {@link captureVideoThumbnail}, this does not save/restore
|
|
379
|
+
* `currentTime` — it is built for a dedicated, offscreen preview video so the
|
|
380
|
+
* main playback element is never disturbed. Powers seek-bar hover previews.
|
|
381
|
+
*
|
|
382
|
+
* @param videoEl - A video element (typically offscreen) to seek and capture.
|
|
383
|
+
* @param timeSeconds - Timestamp to capture, clamped to the video duration.
|
|
384
|
+
* @param width - Output thumbnail width in pixels.
|
|
385
|
+
* @param height - Output thumbnail height in pixels.
|
|
386
|
+
* @returns A JPEG data URL, or `null` if capture failed (no 2d context, a
|
|
387
|
+
* tainted canvas, or a media error).
|
|
388
|
+
*/
|
|
389
|
+
declare function captureFrameAt(videoEl: HTMLVideoElement, timeSeconds: number, width?: number, height?: number): Promise<string | null>;
|
|
342
390
|
|
|
343
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';
|
|
344
392
|
interface ShortcutBinding {
|
|
@@ -393,4 +441,4 @@ declare class ProgressEstimator {
|
|
|
393
441
|
declare function getFFmpeg(): Promise<FFmpeg>;
|
|
394
442
|
declare function resetFFmpeg(): void;
|
|
395
443
|
|
|
396
|
-
export { ASSRenderer, type AudioTrack, type AudioTrackMeta, CancellationError, type Chapter, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, type LightBirdConfig, MKVPlayer, type MKVPlayerFile, type MediaErrorType, type ParsedMediaError, type PlaylistItem, type ProcessedFile, ProgressEstimator, 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, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
444
|
+
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, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
package/dist/index.js
CHANGED
|
@@ -608,6 +608,78 @@ _MKVPlayer._canPlayNatively = canPlayNatively;
|
|
|
608
608
|
_MKVPlayer._workerFactory = null;
|
|
609
609
|
var MKVPlayer = _MKVPlayer;
|
|
610
610
|
|
|
611
|
+
// src/players/hls-player.ts
|
|
612
|
+
var HLS_MIME_HINTS = ["application/x-mpegurl", "application/vnd.apple.mpegurl"];
|
|
613
|
+
function isHlsUrl(url) {
|
|
614
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
615
|
+
const lower = url.toLowerCase();
|
|
616
|
+
if (HLS_MIME_HINTS.some((hint) => lower.includes(hint))) return true;
|
|
617
|
+
return lower.split(/[?#]/)[0].endsWith(".m3u8");
|
|
618
|
+
}
|
|
619
|
+
var HLSPlayer = class {
|
|
620
|
+
constructor(url) {
|
|
621
|
+
this.hls = null;
|
|
622
|
+
this.url = url;
|
|
623
|
+
this.playerFile = { url, qualityLevels: [] };
|
|
624
|
+
}
|
|
625
|
+
async initialize(videoElement) {
|
|
626
|
+
const { default: HlsCtor } = await import('hls.js');
|
|
627
|
+
if (!HlsCtor.isSupported()) {
|
|
628
|
+
videoElement.src = this.url;
|
|
629
|
+
return this.playerFile;
|
|
630
|
+
}
|
|
631
|
+
const hls = new HlsCtor();
|
|
632
|
+
this.hls = hls;
|
|
633
|
+
hls.loadSource(this.url);
|
|
634
|
+
hls.attachMedia(videoElement);
|
|
635
|
+
return this.playerFile;
|
|
636
|
+
}
|
|
637
|
+
getAudioTracks() {
|
|
638
|
+
if (!this.hls) return [];
|
|
639
|
+
return this.hls.audioTracks.map((track) => ({
|
|
640
|
+
id: String(track.id),
|
|
641
|
+
name: track.name,
|
|
642
|
+
lang: track.lang || "unknown"
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
getSubtitles() {
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
switchAudioTrack(trackId) {
|
|
649
|
+
if (this.hls) {
|
|
650
|
+
const id = Number(trackId);
|
|
651
|
+
if (!Number.isNaN(id)) this.hls.audioTrack = id;
|
|
652
|
+
}
|
|
653
|
+
return Promise.resolve();
|
|
654
|
+
}
|
|
655
|
+
switchSubtitle() {
|
|
656
|
+
return Promise.resolve();
|
|
657
|
+
}
|
|
658
|
+
/** Quality renditions — not on `VideoPlayer`; read directly by the quality hook. */
|
|
659
|
+
getQualityLevels() {
|
|
660
|
+
if (!this.hls) return [];
|
|
661
|
+
return this.hls.levels.map((level, index) => ({
|
|
662
|
+
index,
|
|
663
|
+
height: level.height,
|
|
664
|
+
bitrate: level.bitrate,
|
|
665
|
+
name: level.name || (level.height > 0 ? `${level.height}p` : `Level ${index + 1}`)
|
|
666
|
+
}));
|
|
667
|
+
}
|
|
668
|
+
/** Pins a quality level; `-1` restores automatic (ABR) selection. */
|
|
669
|
+
setQualityLevel(levelIndex) {
|
|
670
|
+
if (this.hls) this.hls.currentLevel = levelIndex;
|
|
671
|
+
}
|
|
672
|
+
destroy() {
|
|
673
|
+
if (this.hls) {
|
|
674
|
+
this.hls.destroy();
|
|
675
|
+
this.hls = null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
static isCompatible(url) {
|
|
679
|
+
return isHlsUrl(url);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
611
683
|
// src/video-processor.ts
|
|
612
684
|
var SimplePlayerAdapter = class {
|
|
613
685
|
constructor(file, externalSubtitles = []) {
|
|
@@ -664,13 +736,21 @@ var MKVPlayerAdapter = class {
|
|
|
664
736
|
return this.player.tracksReady;
|
|
665
737
|
}
|
|
666
738
|
};
|
|
667
|
-
function createVideoPlayer(
|
|
668
|
-
if (
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
739
|
+
function createVideoPlayer(source, externalSubtitles = [], onProgress) {
|
|
740
|
+
if (typeof source === "string") {
|
|
741
|
+
if (isHlsUrl(source)) {
|
|
742
|
+
return new HLSPlayer(source);
|
|
743
|
+
}
|
|
744
|
+
throw new Error(
|
|
745
|
+
`createVideoPlayer: unsupported URL "${source}" \u2014 only HLS (.m3u8) stream URLs are supported; pass a File for other formats.`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
if (MKVPlayer.isCompatible(source)) {
|
|
749
|
+
return new MKVPlayerAdapter(source, onProgress);
|
|
750
|
+
} else if (SimplePlayer.isCompatible(source)) {
|
|
751
|
+
return new SimplePlayerAdapter(source, externalSubtitles);
|
|
672
752
|
} else {
|
|
673
|
-
return new SimplePlayerAdapter(
|
|
753
|
+
return new SimplePlayerAdapter(source, externalSubtitles);
|
|
674
754
|
}
|
|
675
755
|
}
|
|
676
756
|
|
|
@@ -1329,6 +1409,61 @@ async function captureVideoThumbnail(videoEl, atSeconds = 5) {
|
|
|
1329
1409
|
}
|
|
1330
1410
|
});
|
|
1331
1411
|
}
|
|
1412
|
+
async function captureFrameAt(videoEl, timeSeconds, width = 160, height = 90) {
|
|
1413
|
+
return new Promise((resolve) => {
|
|
1414
|
+
const canvas = document.createElement("canvas");
|
|
1415
|
+
canvas.width = width;
|
|
1416
|
+
canvas.height = height;
|
|
1417
|
+
const ctx = canvas.getContext("2d");
|
|
1418
|
+
if (!ctx) {
|
|
1419
|
+
resolve(null);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
let settled = false;
|
|
1423
|
+
const cleanup = () => {
|
|
1424
|
+
videoEl.removeEventListener("seeked", onSeeked);
|
|
1425
|
+
videoEl.removeEventListener("error", onError);
|
|
1426
|
+
videoEl.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
1427
|
+
};
|
|
1428
|
+
const finish = (value) => {
|
|
1429
|
+
if (settled) return;
|
|
1430
|
+
settled = true;
|
|
1431
|
+
cleanup();
|
|
1432
|
+
resolve(value);
|
|
1433
|
+
};
|
|
1434
|
+
const draw = () => {
|
|
1435
|
+
try {
|
|
1436
|
+
ctx.drawImage(videoEl, 0, 0, width, height);
|
|
1437
|
+
finish(canvas.toDataURL("image/jpeg", 0.6));
|
|
1438
|
+
} catch {
|
|
1439
|
+
finish(null);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
const seekTo = () => {
|
|
1443
|
+
const duration = videoEl.duration || 0;
|
|
1444
|
+
const target = duration > 0 ? Math.max(0, Math.min(timeSeconds, duration)) : Math.max(0, timeSeconds);
|
|
1445
|
+
if (Math.abs(videoEl.currentTime - target) < 0.05) {
|
|
1446
|
+
draw();
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
videoEl.currentTime = target;
|
|
1451
|
+
} catch {
|
|
1452
|
+
finish(null);
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const onSeeked = () => draw();
|
|
1456
|
+
const onError = () => finish(null);
|
|
1457
|
+
const onLoadedMetadata = () => seekTo();
|
|
1458
|
+
videoEl.addEventListener("seeked", onSeeked);
|
|
1459
|
+
videoEl.addEventListener("error", onError);
|
|
1460
|
+
if (videoEl.readyState >= 1) {
|
|
1461
|
+
seekTo();
|
|
1462
|
+
} else {
|
|
1463
|
+
videoEl.addEventListener("loadedmetadata", onLoadedMetadata, { once: true });
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1332
1467
|
|
|
1333
1468
|
// src/utils/keyboard-shortcuts.ts
|
|
1334
1469
|
var DEFAULT_SHORTCUTS = [
|
|
@@ -1457,4 +1592,4 @@ function resetFFmpeg() {
|
|
|
1457
1592
|
loading = null;
|
|
1458
1593
|
}
|
|
1459
1594
|
|
|
1460
|
-
export { ASSRenderer, CancellationError, DEFAULT_SHORTCUTS, DEFAULT_TRACKERS, DISCLAIMER_KEY, FLAG_MAGNET_LINK, MKVPlayer, ProgressEstimator, SimplePlayer, SubtitleConverter, UniversalSubtitleManager, VIDEO_EXTENSIONS, acceptDisclaimer, applyOffsetToVtt, captureVideoThumbnail, configureLightBird, createOffsetVttUrl, createVideoPlayer, destroyWebTorrentClient, exportPlaylist, extractNativeMetadata, formatShortcutKey, getFFmpeg, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|
|
1595
|
+
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, getVideoFiles, getWebTorrentClient, hasAcceptedDisclaimer, initFeatureFlags, isHlsUrl, isInteractiveElement, isMagnetUri, isVideoFile, loadShortcuts, matchesShortcut, parseChaptersFromFFmpegLog, parseChaptersFromVtt, parseM3U8, parseMediaError, resetFFmpeg, saveShortcuts, validateFile };
|