@lightbird/core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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
- const streamMatch = line.match(/Stream #\d+:\d+(?:\((\w+)\))?: (Video|Audio|Subtitle): (\S+)/i);
273
- if (!streamMatch) continue;
274
- const [, lang, type, codec] = streamMatch;
275
- const titleMatch = line.match(/\btitle\s*:\s*([^,\n]+)/i);
276
- const title = titleMatch ? titleMatch[1].trim() : void 0;
277
- if (type.toLowerCase() === "video") {
278
- videoTracks.push({ index: videoTracks.length, type: "video", codec, lang, title });
279
- } else if (type.toLowerCase() === "audio") {
280
- audioTracks.push({ index: audioTracks.length, type: "audio", codec, lang, title });
281
- } else if (type.toLowerCase() === "subtitle") {
282
- subtitleTracks.push({ index: subtitleTracks.length, type: "subtitle", codec, lang, title });
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.length > 0 ? audioTracks.map((t, i) => ({
406
- id: String(i),
407
- name: t.title ?? (t.lang ? `Audio ${i + 1} (${t.lang})` : `Audio ${i + 1}`),
408
- lang: t.lang ?? "unknown"
409
- })) : [{ id: "0", name: "Default Audio", lang: "unknown" }];
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.length > 0 ? audioTracks.map((t, i) => ({
452
- id: String(i),
453
- name: t.title ?? (t.lang ? `Audio ${i + 1} (${t.lang})` : `Audio ${i + 1}`),
454
- lang: t.lang ?? "unknown"
455
- })) : [{ id: "0", name: "Default Audio", lang: "unknown" }];
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);
@@ -1412,6 +1558,61 @@ async function captureVideoThumbnail(videoEl, atSeconds = 5) {
1412
1558
  }
1413
1559
  });
1414
1560
  }
1561
+ async function captureFrameAt(videoEl, timeSeconds, width = 160, height = 90) {
1562
+ return new Promise((resolve) => {
1563
+ const canvas = document.createElement("canvas");
1564
+ canvas.width = width;
1565
+ canvas.height = height;
1566
+ const ctx = canvas.getContext("2d");
1567
+ if (!ctx) {
1568
+ resolve(null);
1569
+ return;
1570
+ }
1571
+ let settled = false;
1572
+ const cleanup = () => {
1573
+ videoEl.removeEventListener("seeked", onSeeked);
1574
+ videoEl.removeEventListener("error", onError);
1575
+ videoEl.removeEventListener("loadedmetadata", onLoadedMetadata);
1576
+ };
1577
+ const finish = (value) => {
1578
+ if (settled) return;
1579
+ settled = true;
1580
+ cleanup();
1581
+ resolve(value);
1582
+ };
1583
+ const draw = () => {
1584
+ try {
1585
+ ctx.drawImage(videoEl, 0, 0, width, height);
1586
+ finish(canvas.toDataURL("image/jpeg", 0.6));
1587
+ } catch {
1588
+ finish(null);
1589
+ }
1590
+ };
1591
+ const seekTo = () => {
1592
+ const duration = videoEl.duration || 0;
1593
+ const target = duration > 0 ? Math.max(0, Math.min(timeSeconds, duration)) : Math.max(0, timeSeconds);
1594
+ if (Math.abs(videoEl.currentTime - target) < 0.05) {
1595
+ draw();
1596
+ return;
1597
+ }
1598
+ try {
1599
+ videoEl.currentTime = target;
1600
+ } catch {
1601
+ finish(null);
1602
+ }
1603
+ };
1604
+ const onSeeked = () => draw();
1605
+ const onError = () => finish(null);
1606
+ const onLoadedMetadata = () => seekTo();
1607
+ videoEl.addEventListener("seeked", onSeeked);
1608
+ videoEl.addEventListener("error", onError);
1609
+ if (videoEl.readyState >= 1) {
1610
+ seekTo();
1611
+ } else {
1612
+ videoEl.addEventListener("loadedmetadata", onLoadedMetadata, { once: true });
1613
+ }
1614
+ });
1615
+ }
1415
1616
 
1416
1617
  // src/utils/keyboard-shortcuts.ts
1417
1618
  var DEFAULT_SHORTCUTS = [
@@ -1555,6 +1756,7 @@ exports.UniversalSubtitleManager = UniversalSubtitleManager;
1555
1756
  exports.VIDEO_EXTENSIONS = VIDEO_EXTENSIONS;
1556
1757
  exports.acceptDisclaimer = acceptDisclaimer;
1557
1758
  exports.applyOffsetToVtt = applyOffsetToVtt;
1759
+ exports.captureFrameAt = captureFrameAt;
1558
1760
  exports.captureVideoThumbnail = captureVideoThumbnail;
1559
1761
  exports.configureLightBird = configureLightBird;
1560
1762
  exports.createOffsetVttUrl = createOffsetVttUrl;
@@ -1564,6 +1766,7 @@ exports.exportPlaylist = exportPlaylist;
1564
1766
  exports.extractNativeMetadata = extractNativeMetadata;
1565
1767
  exports.formatShortcutKey = formatShortcutKey;
1566
1768
  exports.getFFmpeg = getFFmpeg;
1769
+ exports.getLanguageName = getLanguageName;
1567
1770
  exports.getVideoFiles = getVideoFiles;
1568
1771
  exports.getWebTorrentClient = getWebTorrentClient;
1569
1772
  exports.hasAcceptedDisclaimer = hasAcceptedDisclaimer;
package/dist/index.d.cts CHANGED
@@ -372,6 +372,21 @@ declare function validateFile(file: File): {
372
372
  declare function extractNativeMetadata(videoEl: HTMLVideoElement, file?: File): Partial<VideoMetadata>;
373
373
 
374
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>;
375
390
 
376
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';
377
392
  interface ShortcutBinding {
@@ -426,4 +441,13 @@ declare class ProgressEstimator {
426
441
  declare function getFFmpeg(): Promise<FFmpeg>;
427
442
  declare function resetFFmpeg(): void;
428
443
 
429
- 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, 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 };
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
@@ -372,6 +372,21 @@ declare function validateFile(file: File): {
372
372
  declare function extractNativeMetadata(videoEl: HTMLVideoElement, file?: File): Partial<VideoMetadata>;
373
373
 
374
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>;
375
390
 
376
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';
377
392
  interface ShortcutBinding {
@@ -426,4 +441,13 @@ declare class ProgressEstimator {
426
441
  declare function getFFmpeg(): Promise<FFmpeg>;
427
442
  declare function resetFFmpeg(): void;
428
443
 
429
- 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, 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 };
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 };