@lightbird/core 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +203 -45
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +203 -46
- 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
|
@@ -239,6 +239,121 @@ function parseVttTimestamp(ts) {
|
|
|
239
239
|
return NaN;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// src/utils/language-names.ts
|
|
243
|
+
var LANGUAGE_NAMES = {
|
|
244
|
+
// English
|
|
245
|
+
en: "English",
|
|
246
|
+
eng: "English",
|
|
247
|
+
// Japanese
|
|
248
|
+
ja: "Japanese",
|
|
249
|
+
jpn: "Japanese",
|
|
250
|
+
// Chinese
|
|
251
|
+
zh: "Chinese",
|
|
252
|
+
chi: "Chinese",
|
|
253
|
+
zho: "Chinese",
|
|
254
|
+
// Korean
|
|
255
|
+
ko: "Korean",
|
|
256
|
+
kor: "Korean",
|
|
257
|
+
// French
|
|
258
|
+
fr: "French",
|
|
259
|
+
fre: "French",
|
|
260
|
+
fra: "French",
|
|
261
|
+
// German
|
|
262
|
+
de: "German",
|
|
263
|
+
ger: "German",
|
|
264
|
+
deu: "German",
|
|
265
|
+
// Spanish
|
|
266
|
+
es: "Spanish",
|
|
267
|
+
spa: "Spanish",
|
|
268
|
+
// Italian
|
|
269
|
+
it: "Italian",
|
|
270
|
+
ita: "Italian",
|
|
271
|
+
// Portuguese
|
|
272
|
+
pt: "Portuguese",
|
|
273
|
+
por: "Portuguese",
|
|
274
|
+
// Russian
|
|
275
|
+
ru: "Russian",
|
|
276
|
+
rus: "Russian",
|
|
277
|
+
// Dutch
|
|
278
|
+
nl: "Dutch",
|
|
279
|
+
dut: "Dutch",
|
|
280
|
+
nld: "Dutch",
|
|
281
|
+
// Polish
|
|
282
|
+
pl: "Polish",
|
|
283
|
+
pol: "Polish",
|
|
284
|
+
// Arabic
|
|
285
|
+
ar: "Arabic",
|
|
286
|
+
ara: "Arabic",
|
|
287
|
+
// Hindi
|
|
288
|
+
hi: "Hindi",
|
|
289
|
+
hin: "Hindi",
|
|
290
|
+
// Bengali
|
|
291
|
+
bn: "Bengali",
|
|
292
|
+
ben: "Bengali",
|
|
293
|
+
// Turkish
|
|
294
|
+
tr: "Turkish",
|
|
295
|
+
tur: "Turkish",
|
|
296
|
+
// Swedish
|
|
297
|
+
sv: "Swedish",
|
|
298
|
+
swe: "Swedish",
|
|
299
|
+
// Norwegian
|
|
300
|
+
no: "Norwegian",
|
|
301
|
+
nor: "Norwegian",
|
|
302
|
+
// Danish
|
|
303
|
+
da: "Danish",
|
|
304
|
+
dan: "Danish",
|
|
305
|
+
// Finnish
|
|
306
|
+
fi: "Finnish",
|
|
307
|
+
fin: "Finnish",
|
|
308
|
+
// Greek
|
|
309
|
+
el: "Greek",
|
|
310
|
+
gre: "Greek",
|
|
311
|
+
ell: "Greek",
|
|
312
|
+
// Hebrew
|
|
313
|
+
he: "Hebrew",
|
|
314
|
+
heb: "Hebrew",
|
|
315
|
+
// Thai
|
|
316
|
+
th: "Thai",
|
|
317
|
+
tha: "Thai",
|
|
318
|
+
// Vietnamese
|
|
319
|
+
vi: "Vietnamese",
|
|
320
|
+
vie: "Vietnamese",
|
|
321
|
+
// Indonesian
|
|
322
|
+
id: "Indonesian",
|
|
323
|
+
ind: "Indonesian",
|
|
324
|
+
// Malay
|
|
325
|
+
ms: "Malay",
|
|
326
|
+
may: "Malay",
|
|
327
|
+
msa: "Malay",
|
|
328
|
+
// Czech
|
|
329
|
+
cs: "Czech",
|
|
330
|
+
cze: "Czech",
|
|
331
|
+
ces: "Czech",
|
|
332
|
+
// Hungarian
|
|
333
|
+
hu: "Hungarian",
|
|
334
|
+
hun: "Hungarian",
|
|
335
|
+
// Romanian
|
|
336
|
+
ro: "Romanian",
|
|
337
|
+
rum: "Romanian",
|
|
338
|
+
ron: "Romanian",
|
|
339
|
+
// Ukrainian
|
|
340
|
+
uk: "Ukrainian",
|
|
341
|
+
ukr: "Ukrainian",
|
|
342
|
+
// Tamil
|
|
343
|
+
ta: "Tamil",
|
|
344
|
+
tam: "Tamil",
|
|
345
|
+
// Telugu
|
|
346
|
+
te: "Telugu",
|
|
347
|
+
tel: "Telugu"
|
|
348
|
+
};
|
|
349
|
+
var UNDETERMINED = /* @__PURE__ */ new Set(["", "und", "unknown", "mis", "zxx", "mul"]);
|
|
350
|
+
function getLanguageName(code) {
|
|
351
|
+
if (!code) return void 0;
|
|
352
|
+
const normalized = code.trim().toLowerCase();
|
|
353
|
+
if (UNDETERMINED.has(normalized)) return void 0;
|
|
354
|
+
return LANGUAGE_NAMES[normalized] ?? code.trim();
|
|
355
|
+
}
|
|
356
|
+
|
|
242
357
|
// src/players/mkv-player.ts
|
|
243
358
|
async function canPlayNatively(objectUrl, timeoutMs = 3e3) {
|
|
244
359
|
return new Promise((resolve) => {
|
|
@@ -267,23 +382,76 @@ function parseStreamInfo(logs) {
|
|
|
267
382
|
const videoTracks = [];
|
|
268
383
|
const audioTracks = [];
|
|
269
384
|
const subtitleTracks = [];
|
|
385
|
+
let current = null;
|
|
270
386
|
const lines = logs.split("\n");
|
|
271
387
|
for (const line of lines) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
388
|
+
if (/^\s*Stream #\d+:\d+/.test(line)) {
|
|
389
|
+
current = null;
|
|
390
|
+
}
|
|
391
|
+
const streamMatch = line.match(
|
|
392
|
+
/Stream #\d+:\d+(?:\((\w+)\))?: (Video|Audio|Subtitle): (.+)$/i
|
|
393
|
+
);
|
|
394
|
+
if (streamMatch) {
|
|
395
|
+
const [, lang, type, rest] = streamMatch;
|
|
396
|
+
const codec = rest.trim().split(/[\s,]/)[0];
|
|
397
|
+
const forced = /\(forced\)/i.test(rest);
|
|
398
|
+
const isDefault = /\(default\)/i.test(rest);
|
|
399
|
+
const t = type.toLowerCase();
|
|
400
|
+
const bucket = t === "video" ? videoTracks : t === "audio" ? audioTracks : subtitleTracks;
|
|
401
|
+
current = {
|
|
402
|
+
index: bucket.length,
|
|
403
|
+
type: t,
|
|
404
|
+
codec,
|
|
405
|
+
lang,
|
|
406
|
+
forced,
|
|
407
|
+
default: isDefault
|
|
408
|
+
};
|
|
409
|
+
bucket.push(current);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (/^\s*(Chapter #|Program )/i.test(line)) {
|
|
413
|
+
current = null;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (current && current.title === void 0) {
|
|
417
|
+
const titleMatch = line.match(/^\s*title\s*:\s*(.+)$/i);
|
|
418
|
+
if (titleMatch) {
|
|
419
|
+
current.title = titleMatch[1].trim();
|
|
420
|
+
}
|
|
283
421
|
}
|
|
284
422
|
}
|
|
285
423
|
return { videoTracks, audioTracks, subtitleTracks };
|
|
286
424
|
}
|
|
425
|
+
function formatTrackName(index, t) {
|
|
426
|
+
let label = t.title?.trim() || `Track ${index + 1}`;
|
|
427
|
+
const langName = getLanguageName(t.lang);
|
|
428
|
+
if (langName) label += ` - [${langName}]`;
|
|
429
|
+
if (t.forced) label += " [Forced]";
|
|
430
|
+
return label;
|
|
431
|
+
}
|
|
432
|
+
function buildAudioTracks(tracks) {
|
|
433
|
+
if (tracks.length === 0) {
|
|
434
|
+
return [{ id: "0", name: "Default Audio", lang: "unknown" }];
|
|
435
|
+
}
|
|
436
|
+
return tracks.map((t, i) => ({
|
|
437
|
+
id: String(i),
|
|
438
|
+
name: formatTrackName(i, t),
|
|
439
|
+
lang: t.lang ?? "unknown"
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
function buildSubtitleTracks(tracks, trackMap) {
|
|
443
|
+
trackMap.clear();
|
|
444
|
+
return tracks.map((t, i) => {
|
|
445
|
+
const id = String(i);
|
|
446
|
+
trackMap.set(id, i);
|
|
447
|
+
return {
|
|
448
|
+
id,
|
|
449
|
+
name: formatTrackName(i, t),
|
|
450
|
+
lang: t.lang ?? "unknown",
|
|
451
|
+
type: "embedded"
|
|
452
|
+
};
|
|
453
|
+
});
|
|
454
|
+
}
|
|
287
455
|
var _MKVPlayer = class _MKVPlayer {
|
|
288
456
|
constructor(file, onProgress) {
|
|
289
457
|
this.videoElement = null;
|
|
@@ -402,22 +570,11 @@ var _MKVPlayer = class _MKVPlayer {
|
|
|
402
570
|
});
|
|
403
571
|
const { audioTracks, subtitleTracks } = parseStreamInfo(result.logs);
|
|
404
572
|
this.chapters = parseChaptersFromFFmpegLog(result.logs, videoElement.duration || 0);
|
|
405
|
-
this.playerFile.audioTracks = audioTracks
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
this.subtitleTrackMap.clear();
|
|
411
|
-
this.playerFile.subtitleTracks = subtitleTracks.map((t, i) => {
|
|
412
|
-
const id = String(i);
|
|
413
|
-
this.subtitleTrackMap.set(id, i);
|
|
414
|
-
return {
|
|
415
|
-
id,
|
|
416
|
-
name: t.title ?? (t.lang ? `Subtitle ${i + 1} (${t.lang})` : `Subtitle ${i + 1}`),
|
|
417
|
-
lang: t.lang ?? "unknown",
|
|
418
|
-
type: "embedded"
|
|
419
|
-
};
|
|
420
|
-
});
|
|
573
|
+
this.playerFile.audioTracks = buildAudioTracks(audioTracks);
|
|
574
|
+
this.playerFile.subtitleTracks = buildSubtitleTracks(
|
|
575
|
+
subtitleTracks,
|
|
576
|
+
this.subtitleTrackMap
|
|
577
|
+
);
|
|
421
578
|
const blob = new Blob([result.data], { type: "video/mp4" });
|
|
422
579
|
const url = URL.createObjectURL(blob);
|
|
423
580
|
this.remuxCache.set(0, url);
|
|
@@ -448,22 +605,11 @@ var _MKVPlayer = class _MKVPlayer {
|
|
|
448
605
|
});
|
|
449
606
|
if (this._cancelled) return;
|
|
450
607
|
const { audioTracks, subtitleTracks } = parseStreamInfo(probeResult.logs);
|
|
451
|
-
this.playerFile.audioTracks = audioTracks
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
this.subtitleTrackMap.clear();
|
|
457
|
-
this.playerFile.subtitleTracks = subtitleTracks.map((t, i) => {
|
|
458
|
-
const id = String(i);
|
|
459
|
-
this.subtitleTrackMap.set(id, i);
|
|
460
|
-
return {
|
|
461
|
-
id,
|
|
462
|
-
name: t.title ?? (t.lang ? `Subtitle ${i + 1} (${t.lang})` : `Subtitle ${i + 1}`),
|
|
463
|
-
lang: t.lang ?? "unknown",
|
|
464
|
-
type: "embedded"
|
|
465
|
-
};
|
|
466
|
-
});
|
|
608
|
+
this.playerFile.audioTracks = buildAudioTracks(audioTracks);
|
|
609
|
+
this.playerFile.subtitleTracks = buildSubtitleTracks(
|
|
610
|
+
subtitleTracks,
|
|
611
|
+
this.subtitleTrackMap
|
|
612
|
+
);
|
|
467
613
|
}
|
|
468
614
|
async _remux(audioTrackIndex) {
|
|
469
615
|
const cached = this.remuxCache.get(audioTrackIndex);
|
|
@@ -1484,7 +1630,11 @@ var DEFAULT_SHORTCUTS = [
|
|
|
1484
1630
|
{ action: "screenshot", label: "Screenshot", defaultKey: "s", key: "s", modifiers: { ctrl: true } },
|
|
1485
1631
|
{ action: "show-shortcuts", label: "Show Shortcuts Help", defaultKey: "?", key: "?" },
|
|
1486
1632
|
{ action: "next-chapter", label: "Next Chapter", defaultKey: "]", key: "]" },
|
|
1487
|
-
{ action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" }
|
|
1633
|
+
{ action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" },
|
|
1634
|
+
{ action: "frame-step-forward", label: "Step Forward One Frame", defaultKey: ".", key: "." },
|
|
1635
|
+
{ action: "frame-step-backward", label: "Step Backward One Frame", defaultKey: ",", key: "," },
|
|
1636
|
+
{ action: "loop-toggle", label: "Toggle Loop", defaultKey: "l", key: "l" },
|
|
1637
|
+
{ action: "ab-loop-cycle", label: "A-B Loop (Set/Cycle)", defaultKey: "r", key: "r" }
|
|
1488
1638
|
];
|
|
1489
1639
|
var STORAGE_KEY = "lightbird-shortcuts";
|
|
1490
1640
|
function loadShortcuts() {
|
|
@@ -1518,7 +1668,14 @@ function matchesShortcut(e, binding) {
|
|
|
1518
1668
|
function isInteractiveElement(el) {
|
|
1519
1669
|
if (!el || !(el instanceof HTMLElement)) return false;
|
|
1520
1670
|
const tag = el.tagName.toLowerCase();
|
|
1521
|
-
|
|
1671
|
+
if (["input", "textarea", "select"].includes(tag)) return true;
|
|
1672
|
+
if (el.contentEditable === "true" || el.getAttribute("contenteditable") !== null) return true;
|
|
1673
|
+
if (typeof el.closest === "function" && el.closest(
|
|
1674
|
+
'[role="dialog"], [role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]'
|
|
1675
|
+
)) {
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
return false;
|
|
1522
1679
|
}
|
|
1523
1680
|
function formatShortcutKey(binding) {
|
|
1524
1681
|
const mods = [];
|
|
@@ -1620,6 +1777,7 @@ exports.exportPlaylist = exportPlaylist;
|
|
|
1620
1777
|
exports.extractNativeMetadata = extractNativeMetadata;
|
|
1621
1778
|
exports.formatShortcutKey = formatShortcutKey;
|
|
1622
1779
|
exports.getFFmpeg = getFFmpeg;
|
|
1780
|
+
exports.getLanguageName = getLanguageName;
|
|
1623
1781
|
exports.getVideoFiles = getVideoFiles;
|
|
1624
1782
|
exports.getWebTorrentClient = getWebTorrentClient;
|
|
1625
1783
|
exports.hasAcceptedDisclaimer = hasAcceptedDisclaimer;
|
package/dist/index.d.cts
CHANGED
|
@@ -388,7 +388,7 @@ declare function captureVideoThumbnail(videoEl: HTMLVideoElement, atSeconds?: nu
|
|
|
388
388
|
*/
|
|
389
389
|
declare function captureFrameAt(videoEl: HTMLVideoElement, timeSeconds: number, width?: number, height?: number): Promise<string | null>;
|
|
390
390
|
|
|
391
|
-
type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter';
|
|
391
|
+
type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter' | 'frame-step-forward' | 'frame-step-backward' | 'loop-toggle' | 'ab-loop-cycle';
|
|
392
392
|
interface ShortcutBinding {
|
|
393
393
|
action: ShortcutAction;
|
|
394
394
|
label: string;
|
|
@@ -441,4 +441,13 @@ declare class ProgressEstimator {
|
|
|
441
441
|
declare function getFFmpeg(): Promise<FFmpeg>;
|
|
442
442
|
declare function resetFFmpeg(): void;
|
|
443
443
|
|
|
444
|
-
|
|
444
|
+
/**
|
|
445
|
+
* Resolve an ISO 639 language code to its English language name, mirroring how
|
|
446
|
+
* VLC labels tracks.
|
|
447
|
+
*
|
|
448
|
+
* @returns The full language name, or the original code if it's unrecognised
|
|
449
|
+
* (VLC behaviour), or `undefined` if the code is absent/undetermined.
|
|
450
|
+
*/
|
|
451
|
+
declare function getLanguageName(code?: string | null): string | undefined;
|
|
452
|
+
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -388,7 +388,7 @@ declare function captureVideoThumbnail(videoEl: HTMLVideoElement, atSeconds?: nu
|
|
|
388
388
|
*/
|
|
389
389
|
declare function captureFrameAt(videoEl: HTMLVideoElement, timeSeconds: number, width?: number, height?: number): Promise<string | null>;
|
|
390
390
|
|
|
391
|
-
type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter';
|
|
391
|
+
type ShortcutAction = 'play-pause' | 'seek-forward-5' | 'seek-backward-5' | 'seek-forward-30' | 'seek-backward-30' | 'volume-up' | 'volume-down' | 'mute' | 'fullscreen' | 'next-item' | 'prev-item' | 'screenshot' | 'show-shortcuts' | 'next-chapter' | 'prev-chapter' | 'frame-step-forward' | 'frame-step-backward' | 'loop-toggle' | 'ab-loop-cycle';
|
|
392
392
|
interface ShortcutBinding {
|
|
393
393
|
action: ShortcutAction;
|
|
394
394
|
label: string;
|
|
@@ -441,4 +441,13 @@ declare class ProgressEstimator {
|
|
|
441
441
|
declare function getFFmpeg(): Promise<FFmpeg>;
|
|
442
442
|
declare function resetFFmpeg(): void;
|
|
443
443
|
|
|
444
|
-
|
|
444
|
+
/**
|
|
445
|
+
* Resolve an ISO 639 language code to its English language name, mirroring how
|
|
446
|
+
* VLC labels tracks.
|
|
447
|
+
*
|
|
448
|
+
* @returns The full language name, or the original code if it's unrecognised
|
|
449
|
+
* (VLC behaviour), or `undefined` if the code is absent/undetermined.
|
|
450
|
+
*/
|
|
451
|
+
declare function getLanguageName(code?: string | null): string | undefined;
|
|
452
|
+
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -236,6 +236,121 @@ function parseVttTimestamp(ts) {
|
|
|
236
236
|
return NaN;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
// src/utils/language-names.ts
|
|
240
|
+
var LANGUAGE_NAMES = {
|
|
241
|
+
// English
|
|
242
|
+
en: "English",
|
|
243
|
+
eng: "English",
|
|
244
|
+
// Japanese
|
|
245
|
+
ja: "Japanese",
|
|
246
|
+
jpn: "Japanese",
|
|
247
|
+
// Chinese
|
|
248
|
+
zh: "Chinese",
|
|
249
|
+
chi: "Chinese",
|
|
250
|
+
zho: "Chinese",
|
|
251
|
+
// Korean
|
|
252
|
+
ko: "Korean",
|
|
253
|
+
kor: "Korean",
|
|
254
|
+
// French
|
|
255
|
+
fr: "French",
|
|
256
|
+
fre: "French",
|
|
257
|
+
fra: "French",
|
|
258
|
+
// German
|
|
259
|
+
de: "German",
|
|
260
|
+
ger: "German",
|
|
261
|
+
deu: "German",
|
|
262
|
+
// Spanish
|
|
263
|
+
es: "Spanish",
|
|
264
|
+
spa: "Spanish",
|
|
265
|
+
// Italian
|
|
266
|
+
it: "Italian",
|
|
267
|
+
ita: "Italian",
|
|
268
|
+
// Portuguese
|
|
269
|
+
pt: "Portuguese",
|
|
270
|
+
por: "Portuguese",
|
|
271
|
+
// Russian
|
|
272
|
+
ru: "Russian",
|
|
273
|
+
rus: "Russian",
|
|
274
|
+
// Dutch
|
|
275
|
+
nl: "Dutch",
|
|
276
|
+
dut: "Dutch",
|
|
277
|
+
nld: "Dutch",
|
|
278
|
+
// Polish
|
|
279
|
+
pl: "Polish",
|
|
280
|
+
pol: "Polish",
|
|
281
|
+
// Arabic
|
|
282
|
+
ar: "Arabic",
|
|
283
|
+
ara: "Arabic",
|
|
284
|
+
// Hindi
|
|
285
|
+
hi: "Hindi",
|
|
286
|
+
hin: "Hindi",
|
|
287
|
+
// Bengali
|
|
288
|
+
bn: "Bengali",
|
|
289
|
+
ben: "Bengali",
|
|
290
|
+
// Turkish
|
|
291
|
+
tr: "Turkish",
|
|
292
|
+
tur: "Turkish",
|
|
293
|
+
// Swedish
|
|
294
|
+
sv: "Swedish",
|
|
295
|
+
swe: "Swedish",
|
|
296
|
+
// Norwegian
|
|
297
|
+
no: "Norwegian",
|
|
298
|
+
nor: "Norwegian",
|
|
299
|
+
// Danish
|
|
300
|
+
da: "Danish",
|
|
301
|
+
dan: "Danish",
|
|
302
|
+
// Finnish
|
|
303
|
+
fi: "Finnish",
|
|
304
|
+
fin: "Finnish",
|
|
305
|
+
// Greek
|
|
306
|
+
el: "Greek",
|
|
307
|
+
gre: "Greek",
|
|
308
|
+
ell: "Greek",
|
|
309
|
+
// Hebrew
|
|
310
|
+
he: "Hebrew",
|
|
311
|
+
heb: "Hebrew",
|
|
312
|
+
// Thai
|
|
313
|
+
th: "Thai",
|
|
314
|
+
tha: "Thai",
|
|
315
|
+
// Vietnamese
|
|
316
|
+
vi: "Vietnamese",
|
|
317
|
+
vie: "Vietnamese",
|
|
318
|
+
// Indonesian
|
|
319
|
+
id: "Indonesian",
|
|
320
|
+
ind: "Indonesian",
|
|
321
|
+
// Malay
|
|
322
|
+
ms: "Malay",
|
|
323
|
+
may: "Malay",
|
|
324
|
+
msa: "Malay",
|
|
325
|
+
// Czech
|
|
326
|
+
cs: "Czech",
|
|
327
|
+
cze: "Czech",
|
|
328
|
+
ces: "Czech",
|
|
329
|
+
// Hungarian
|
|
330
|
+
hu: "Hungarian",
|
|
331
|
+
hun: "Hungarian",
|
|
332
|
+
// Romanian
|
|
333
|
+
ro: "Romanian",
|
|
334
|
+
rum: "Romanian",
|
|
335
|
+
ron: "Romanian",
|
|
336
|
+
// Ukrainian
|
|
337
|
+
uk: "Ukrainian",
|
|
338
|
+
ukr: "Ukrainian",
|
|
339
|
+
// Tamil
|
|
340
|
+
ta: "Tamil",
|
|
341
|
+
tam: "Tamil",
|
|
342
|
+
// Telugu
|
|
343
|
+
te: "Telugu",
|
|
344
|
+
tel: "Telugu"
|
|
345
|
+
};
|
|
346
|
+
var UNDETERMINED = /* @__PURE__ */ new Set(["", "und", "unknown", "mis", "zxx", "mul"]);
|
|
347
|
+
function getLanguageName(code) {
|
|
348
|
+
if (!code) return void 0;
|
|
349
|
+
const normalized = code.trim().toLowerCase();
|
|
350
|
+
if (UNDETERMINED.has(normalized)) return void 0;
|
|
351
|
+
return LANGUAGE_NAMES[normalized] ?? code.trim();
|
|
352
|
+
}
|
|
353
|
+
|
|
239
354
|
// src/players/mkv-player.ts
|
|
240
355
|
async function canPlayNatively(objectUrl, timeoutMs = 3e3) {
|
|
241
356
|
return new Promise((resolve) => {
|
|
@@ -264,23 +379,76 @@ function parseStreamInfo(logs) {
|
|
|
264
379
|
const videoTracks = [];
|
|
265
380
|
const audioTracks = [];
|
|
266
381
|
const subtitleTracks = [];
|
|
382
|
+
let current = null;
|
|
267
383
|
const lines = logs.split("\n");
|
|
268
384
|
for (const line of lines) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
385
|
+
if (/^\s*Stream #\d+:\d+/.test(line)) {
|
|
386
|
+
current = null;
|
|
387
|
+
}
|
|
388
|
+
const streamMatch = line.match(
|
|
389
|
+
/Stream #\d+:\d+(?:\((\w+)\))?: (Video|Audio|Subtitle): (.+)$/i
|
|
390
|
+
);
|
|
391
|
+
if (streamMatch) {
|
|
392
|
+
const [, lang, type, rest] = streamMatch;
|
|
393
|
+
const codec = rest.trim().split(/[\s,]/)[0];
|
|
394
|
+
const forced = /\(forced\)/i.test(rest);
|
|
395
|
+
const isDefault = /\(default\)/i.test(rest);
|
|
396
|
+
const t = type.toLowerCase();
|
|
397
|
+
const bucket = t === "video" ? videoTracks : t === "audio" ? audioTracks : subtitleTracks;
|
|
398
|
+
current = {
|
|
399
|
+
index: bucket.length,
|
|
400
|
+
type: t,
|
|
401
|
+
codec,
|
|
402
|
+
lang,
|
|
403
|
+
forced,
|
|
404
|
+
default: isDefault
|
|
405
|
+
};
|
|
406
|
+
bucket.push(current);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (/^\s*(Chapter #|Program )/i.test(line)) {
|
|
410
|
+
current = null;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (current && current.title === void 0) {
|
|
414
|
+
const titleMatch = line.match(/^\s*title\s*:\s*(.+)$/i);
|
|
415
|
+
if (titleMatch) {
|
|
416
|
+
current.title = titleMatch[1].trim();
|
|
417
|
+
}
|
|
280
418
|
}
|
|
281
419
|
}
|
|
282
420
|
return { videoTracks, audioTracks, subtitleTracks };
|
|
283
421
|
}
|
|
422
|
+
function formatTrackName(index, t) {
|
|
423
|
+
let label = t.title?.trim() || `Track ${index + 1}`;
|
|
424
|
+
const langName = getLanguageName(t.lang);
|
|
425
|
+
if (langName) label += ` - [${langName}]`;
|
|
426
|
+
if (t.forced) label += " [Forced]";
|
|
427
|
+
return label;
|
|
428
|
+
}
|
|
429
|
+
function buildAudioTracks(tracks) {
|
|
430
|
+
if (tracks.length === 0) {
|
|
431
|
+
return [{ id: "0", name: "Default Audio", lang: "unknown" }];
|
|
432
|
+
}
|
|
433
|
+
return tracks.map((t, i) => ({
|
|
434
|
+
id: String(i),
|
|
435
|
+
name: formatTrackName(i, t),
|
|
436
|
+
lang: t.lang ?? "unknown"
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
function buildSubtitleTracks(tracks, trackMap) {
|
|
440
|
+
trackMap.clear();
|
|
441
|
+
return tracks.map((t, i) => {
|
|
442
|
+
const id = String(i);
|
|
443
|
+
trackMap.set(id, i);
|
|
444
|
+
return {
|
|
445
|
+
id,
|
|
446
|
+
name: formatTrackName(i, t),
|
|
447
|
+
lang: t.lang ?? "unknown",
|
|
448
|
+
type: "embedded"
|
|
449
|
+
};
|
|
450
|
+
});
|
|
451
|
+
}
|
|
284
452
|
var _MKVPlayer = class _MKVPlayer {
|
|
285
453
|
constructor(file, onProgress) {
|
|
286
454
|
this.videoElement = null;
|
|
@@ -399,22 +567,11 @@ var _MKVPlayer = class _MKVPlayer {
|
|
|
399
567
|
});
|
|
400
568
|
const { audioTracks, subtitleTracks } = parseStreamInfo(result.logs);
|
|
401
569
|
this.chapters = parseChaptersFromFFmpegLog(result.logs, videoElement.duration || 0);
|
|
402
|
-
this.playerFile.audioTracks = audioTracks
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
this.subtitleTrackMap.clear();
|
|
408
|
-
this.playerFile.subtitleTracks = subtitleTracks.map((t, i) => {
|
|
409
|
-
const id = String(i);
|
|
410
|
-
this.subtitleTrackMap.set(id, i);
|
|
411
|
-
return {
|
|
412
|
-
id,
|
|
413
|
-
name: t.title ?? (t.lang ? `Subtitle ${i + 1} (${t.lang})` : `Subtitle ${i + 1}`),
|
|
414
|
-
lang: t.lang ?? "unknown",
|
|
415
|
-
type: "embedded"
|
|
416
|
-
};
|
|
417
|
-
});
|
|
570
|
+
this.playerFile.audioTracks = buildAudioTracks(audioTracks);
|
|
571
|
+
this.playerFile.subtitleTracks = buildSubtitleTracks(
|
|
572
|
+
subtitleTracks,
|
|
573
|
+
this.subtitleTrackMap
|
|
574
|
+
);
|
|
418
575
|
const blob = new Blob([result.data], { type: "video/mp4" });
|
|
419
576
|
const url = URL.createObjectURL(blob);
|
|
420
577
|
this.remuxCache.set(0, url);
|
|
@@ -445,22 +602,11 @@ var _MKVPlayer = class _MKVPlayer {
|
|
|
445
602
|
});
|
|
446
603
|
if (this._cancelled) return;
|
|
447
604
|
const { audioTracks, subtitleTracks } = parseStreamInfo(probeResult.logs);
|
|
448
|
-
this.playerFile.audioTracks = audioTracks
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
this.subtitleTrackMap.clear();
|
|
454
|
-
this.playerFile.subtitleTracks = subtitleTracks.map((t, i) => {
|
|
455
|
-
const id = String(i);
|
|
456
|
-
this.subtitleTrackMap.set(id, i);
|
|
457
|
-
return {
|
|
458
|
-
id,
|
|
459
|
-
name: t.title ?? (t.lang ? `Subtitle ${i + 1} (${t.lang})` : `Subtitle ${i + 1}`),
|
|
460
|
-
lang: t.lang ?? "unknown",
|
|
461
|
-
type: "embedded"
|
|
462
|
-
};
|
|
463
|
-
});
|
|
605
|
+
this.playerFile.audioTracks = buildAudioTracks(audioTracks);
|
|
606
|
+
this.playerFile.subtitleTracks = buildSubtitleTracks(
|
|
607
|
+
subtitleTracks,
|
|
608
|
+
this.subtitleTrackMap
|
|
609
|
+
);
|
|
464
610
|
}
|
|
465
611
|
async _remux(audioTrackIndex) {
|
|
466
612
|
const cached = this.remuxCache.get(audioTrackIndex);
|
|
@@ -1481,7 +1627,11 @@ var DEFAULT_SHORTCUTS = [
|
|
|
1481
1627
|
{ action: "screenshot", label: "Screenshot", defaultKey: "s", key: "s", modifiers: { ctrl: true } },
|
|
1482
1628
|
{ action: "show-shortcuts", label: "Show Shortcuts Help", defaultKey: "?", key: "?" },
|
|
1483
1629
|
{ action: "next-chapter", label: "Next Chapter", defaultKey: "]", key: "]" },
|
|
1484
|
-
{ action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" }
|
|
1630
|
+
{ action: "prev-chapter", label: "Previous Chapter", defaultKey: "[", key: "[" },
|
|
1631
|
+
{ action: "frame-step-forward", label: "Step Forward One Frame", defaultKey: ".", key: "." },
|
|
1632
|
+
{ action: "frame-step-backward", label: "Step Backward One Frame", defaultKey: ",", key: "," },
|
|
1633
|
+
{ action: "loop-toggle", label: "Toggle Loop", defaultKey: "l", key: "l" },
|
|
1634
|
+
{ action: "ab-loop-cycle", label: "A-B Loop (Set/Cycle)", defaultKey: "r", key: "r" }
|
|
1485
1635
|
];
|
|
1486
1636
|
var STORAGE_KEY = "lightbird-shortcuts";
|
|
1487
1637
|
function loadShortcuts() {
|
|
@@ -1515,7 +1665,14 @@ function matchesShortcut(e, binding) {
|
|
|
1515
1665
|
function isInteractiveElement(el) {
|
|
1516
1666
|
if (!el || !(el instanceof HTMLElement)) return false;
|
|
1517
1667
|
const tag = el.tagName.toLowerCase();
|
|
1518
|
-
|
|
1668
|
+
if (["input", "textarea", "select"].includes(tag)) return true;
|
|
1669
|
+
if (el.contentEditable === "true" || el.getAttribute("contenteditable") !== null) return true;
|
|
1670
|
+
if (typeof el.closest === "function" && el.closest(
|
|
1671
|
+
'[role="dialog"], [role="alertdialog"], [role="menu"], [data-radix-popper-content-wrapper]'
|
|
1672
|
+
)) {
|
|
1673
|
+
return true;
|
|
1674
|
+
}
|
|
1675
|
+
return false;
|
|
1519
1676
|
}
|
|
1520
1677
|
function formatShortcutKey(binding) {
|
|
1521
1678
|
const mods = [];
|
|
@@ -1592,4 +1749,4 @@ function resetFFmpeg() {
|
|
|
1592
1749
|
loading = null;
|
|
1593
1750
|
}
|
|
1594
1751
|
|
|
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 };
|
|
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 };
|
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.7.0",
|
|
4
4
|
"description": "Client-side video player engine. Plays MKV, MP4, WebM with full subtitle, audio track, and chapter support. No server required.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Punyam Singh",
|